View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.directory.api.ldap.model.ldif;
21  
22  
23  import java.io.IOException;
24  import java.io.UnsupportedEncodingException;
25  
26  import javax.naming.directory.Attributes;
27  
28  import org.apache.directory.api.i18n.I18n;
29  import org.apache.directory.api.ldap.model.entry.Attribute;
30  import org.apache.directory.api.ldap.model.entry.AttributeUtils;
31  import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
32  import org.apache.directory.api.ldap.model.entry.Entry;
33  import org.apache.directory.api.ldap.model.entry.Modification;
34  import org.apache.directory.api.ldap.model.entry.Value;
35  import org.apache.directory.api.ldap.model.exception.LdapException;
36  import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
37  import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
38  import org.apache.directory.api.ldap.model.name.Dn;
39  import org.apache.directory.api.util.Base64;
40  import org.apache.directory.api.util.Strings;
41  
42  
43  /**
44   * Some LDIF helper methods.
45   *
46   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
47   */
48  public final class LdifUtils
49  {
50      /** The array that will be used to match the first char.*/
51      private static final boolean[] LDIF_SAFE_STARTING_CHAR_ALPHABET = new boolean[128];
52  
53      /** The array that will be used to match the other chars.*/
54      private static final boolean[] LDIF_SAFE_OTHER_CHARS_ALPHABET = new boolean[128];
55  
56      /** The default length for a line in a ldif file */
57      private static final int DEFAULT_LINE_LENGTH = 80;
58  
59      /** The file separator */
60      private static final String LINE_SEPARATOR = System.getProperty( "line.separator" );
61  
62      static
63      {
64          // Initialization of the array that will be used to match the first char.
65          for ( int i = 0; i < 128; i++ )
66          {
67              LDIF_SAFE_STARTING_CHAR_ALPHABET[i] = true;
68          }
69  
70          LDIF_SAFE_STARTING_CHAR_ALPHABET[0] = false; // 0 (NUL)
71          LDIF_SAFE_STARTING_CHAR_ALPHABET[10] = false; // 10 (LF)
72          LDIF_SAFE_STARTING_CHAR_ALPHABET[13] = false; // 13 (CR)
73          LDIF_SAFE_STARTING_CHAR_ALPHABET[32] = false; // 32 (SPACE)
74          LDIF_SAFE_STARTING_CHAR_ALPHABET[58] = false; // 58 (:)
75          LDIF_SAFE_STARTING_CHAR_ALPHABET[60] = false; // 60 (>)
76  
77          // Initialization of the array that will be used to match the other chars.
78          for ( int i = 0; i < 128; i++ )
79          {
80              LDIF_SAFE_OTHER_CHARS_ALPHABET[i] = true;
81          }
82  
83          LDIF_SAFE_OTHER_CHARS_ALPHABET[0] = false; // 0 (NUL)
84          LDIF_SAFE_OTHER_CHARS_ALPHABET[10] = false; // 10 (LF)
85          LDIF_SAFE_OTHER_CHARS_ALPHABET[13] = false; // 13 (CR)
86      }
87  
88  
89      /**
90       * Private constructor.
91       */
92      private LdifUtils()
93      {
94      }
95  
96  
97      /**
98       * Checks if the input String contains only safe values, that is, the data
99       * does not need to be encoded for use with LDIF. The rules for checking safety
100      * are based on the rules for LDIF (LDAP Data Interchange Format) per RFC 2849.
101      * The data does not need to be encoded if all the following are true:
102      *
103      * The data cannot start with the following char values:
104      * <ul>
105      * <li>00 (NUL)</li>
106      * <li>10 (LF)</li>
107      * <li>13 (CR)</li>
108      * <li>32 (SPACE)</li>
109      * <li>58 (:)</li>
110      * <li>60 (<)</li>
111      * <li>Any character with value greater than 127</li>
112      * </ul>
113      *
114      * The data cannot contain any of the following char values:
115      * <ul>
116      * <li>00 (NUL)</li>
117      * <li>10 (LF)</li>
118      * <li>13 (CR)</li>
119      * <li>Any character with value greater than 127</li>
120      * </ul>
121      *
122      * The data cannot end with a space.
123      *
124      * @param str the String to be checked
125      * @return true if encoding not required for LDIF
126      */
127     public static boolean isLDIFSafe( String str )
128     {
129         if ( Strings.isEmpty( str ) )
130         {
131             // A null string is LDIF safe
132             return true;
133         }
134 
135         // Checking the first char
136         char currentChar = str.charAt( 0 );
137 
138         if ( ( currentChar > 127 ) || !LDIF_SAFE_STARTING_CHAR_ALPHABET[currentChar] )
139         {
140             return false;
141         }
142 
143         // Checking the other chars
144         for ( int i = 1; i < str.length(); i++ )
145         {
146             currentChar = str.charAt( i );
147 
148             if ( ( currentChar > 127 ) || !LDIF_SAFE_OTHER_CHARS_ALPHABET[currentChar] )
149             {
150                 return false;
151             }
152         }
153 
154         // The String cannot end with a space
155         return ( currentChar != ' ' );
156     }
157 
158 
159     /**
160      * Convert an Attributes as LDIF
161      * 
162      * @param attrs the Attributes to convert
163      * @return the corresponding LDIF code as a String
164      * @throws LdapException If a naming exception is encountered.
165      */
166     public static String convertToLdif( Attributes attrs ) throws LdapException
167     {
168         return convertAttributesToLdif( AttributeUtils.toEntry( attrs, null ), DEFAULT_LINE_LENGTH );
169     }
170 
171 
172     /**
173      * Convert an Attributes as LDIF
174      * 
175      * @param attrs the Attributes to convert
176      * @param length The ldif line length
177      * @return the corresponding LDIF code as a String
178      * @throws LdapException If a naming exception is encountered.
179      */
180     public static String convertToLdif( Attributes attrs, int length ) throws LdapException
181     {
182         return convertAttributesToLdif( AttributeUtils.toEntry( attrs, null ), length );
183     }
184 
185 
186     /**
187      * Convert an Attributes as LDIF. The Dn is written.
188      * 
189      * @param attrs the Attributes to convert
190      * @param dn The Dn for this entry
191      * @param length The ldif line length
192      * @return the corresponding LDIF code as a String
193      * @throws LdapException If a naming exception is encountered.
194      */
195     public static String convertToLdif( Attributes attrs, Dn dn, int length ) throws LdapException
196     {
197         return convertToLdif( AttributeUtils.toEntry( attrs, dn ), length );
198     }
199 
200 
201     /**
202      * Convert an Attributes as LDIF. The Dn is written.
203      * 
204      * @param attrs the Attributes to convert
205      * @param dn The Dn for this entry
206      * @return the corresponding LDIF code as a String
207      * @throws LdapException If a naming exception is encountered.
208      */
209     public static String convertToLdif( Attributes attrs, Dn dn ) throws LdapException
210     {
211         return convertToLdif( AttributeUtils.toEntry( attrs, dn ), DEFAULT_LINE_LENGTH );
212     }
213 
214 
215     /**
216      * Convert an Entry to LDIF
217      * 
218      * @param entry the Entry to convert
219      * @return the corresponding LDIF code as a String
220      * @throws LdapException If a naming exception is encountered.
221      */
222     public static String convertToLdif( Entry entry ) throws LdapException
223     {
224         return convertToLdif( entry, DEFAULT_LINE_LENGTH );
225     }
226 
227 
228     /**
229      * Convert an Entry to LDIF including a version number at the top
230      * 
231      * @param entry the Entry to convert
232      * @param includeVersionInfo flag to tell whether to include version number or not
233      * @return the corresponding LDIF code as a String
234      * @throws org.apache.directory.api.ldap.model.exception.LdapException If a naming exception is encountered.
235      */
236     public static String convertToLdif( Entry entry, boolean includeVersionInfo ) throws LdapException
237     {
238         String ldif = convertToLdif( entry, DEFAULT_LINE_LENGTH );
239 
240         if ( includeVersionInfo )
241         {
242             ldif = "version: 1" + LINE_SEPARATOR + ldif;
243         }
244 
245         return ldif;
246     }
247 
248 
249     /**
250      * Convert all the Entry's attributes to LDIF. The Dn is not written
251      * 
252      * @param entry the Entry to convert
253      * @return the corresponding LDIF code as a String
254      * @throws LdapException If a naming exception is encountered.
255      */
256     public static String convertAttributesToLdif( Entry entry ) throws LdapException
257     {
258         return convertAttributesToLdif( entry, DEFAULT_LINE_LENGTH );
259     }
260 
261 
262     /**
263      * Convert a LDIF String to a JNDI attributes.
264      *
265      * @param ldif The LDIF string containing an attribute value
266      * @return An Attributes instance
267      * @exception LdapLdifException If the LDIF String cannot be converted to an Attributes
268      */
269     public static Attributes getJndiAttributesFromLdif( String ldif ) throws LdapLdifException
270     {
271         LdifAttributesReader reader = new LdifAttributesReader();
272 
273         try
274         {
275             Attributes attributes = AttributeUtils.toAttributes( reader.parseEntry( ldif ) );
276             
277             reader.close();
278             
279             return attributes;
280         }
281         catch ( IOException ioe )
282         {
283             throw new LdapLdifException( ioe.getMessage() );
284         }
285     }
286 
287 
288     /**
289      * Convert an Entry as LDIF
290      * 
291      * @param entry the Entry to convert
292      * @param length the expected line length
293      * @return the corresponding LDIF code as a String
294      * @throws LdapException If a naming exception is encountered.
295      */
296     public static String convertToLdif( Entry entry, int length ) throws LdapException
297     {
298         StringBuilder sb = new StringBuilder();
299 
300         if ( entry.getDn() != null )
301         {
302             // First, dump the Dn
303             if ( isLDIFSafe( entry.getDn().getName() ) )
304             {
305                 sb.append( stripLineToNChars( "dn: " + entry.getDn().getName(), length ) );
306             }
307             else
308             {
309                 sb.append( stripLineToNChars( "dn:: " + encodeBase64( entry.getDn().getName() ), length ) );
310             }
311 
312             sb.append( '\n' );
313         }
314 
315         // Then all the attributes
316         for ( Attribute attribute : entry )
317         {
318             sb.append( convertToLdif( attribute, length ) );
319         }
320 
321         return sb.toString();
322     }
323 
324 
325     /**
326      * Convert the Entry's attributes to LDIF. The Dn is not written.
327      * 
328      * @param entry the Entry to convert
329      * @param length the expected line length
330      * @return the corresponding LDIF code as a String
331      * @throws LdapException If a naming exception is encountered.
332      */
333     public static String convertAttributesToLdif( Entry entry, int length ) throws LdapException
334     {
335         StringBuilder sb = new StringBuilder();
336 
337         // Then all the attributes
338         for ( Attribute attribute : entry )
339         {
340             sb.append( convertToLdif( attribute, length ) );
341         }
342 
343         return sb.toString();
344     }
345 
346 
347     /**
348      * Convert an LdifEntry to LDIF
349      * 
350      * @param entry the LdifEntry to convert
351      * @return the corresponding LDIF as a String
352      * @throws LdapException If a naming exception is encountered.
353      */
354     public static String convertToLdif( LdifEntry entry ) throws LdapException
355     {
356         return convertToLdif( entry, DEFAULT_LINE_LENGTH );
357     }
358 
359 
360     /**
361      * Convert an LdifEntry to LDIF
362      * 
363      * @param entry the LdifEntry to convert
364      * @param length The maximum line's length
365      * @return the corresponding LDIF as a String
366      * @throws LdapException If a naming exception is encountered.
367      */
368     public static String convertToLdif( LdifEntry entry, int length ) throws LdapException
369     {
370         StringBuilder sb = new StringBuilder();
371 
372         // First, dump the Dn
373         if ( isLDIFSafe( entry.getDn().getName() ) )
374         {
375             sb.append( stripLineToNChars( "dn: " + entry.getDn(), length ) );
376         }
377         else
378         {
379             sb.append( stripLineToNChars( "dn:: " + encodeBase64( entry.getDn().getName() ), length ) );
380         }
381 
382         sb.append( '\n' );
383 
384         // Dump the ChangeType
385         String changeType = Strings.toLowerCase( entry.getChangeType().toString() );
386 
387         if ( entry.getChangeType() != ChangeType.None )
388         {
389             // First dump the controls if any
390             if ( entry.hasControls() )
391             {
392                 for ( LdifControl control : entry.getControls().values() )
393                 {
394                     StringBuilder controlStr = new StringBuilder();
395 
396                     controlStr.append( "control: " ).append( control.getOid() );
397                     controlStr.append( " " ).append( control.isCritical() );
398 
399                     if ( control.hasValue() )
400                     {
401                         controlStr.append( "::" ).append( Base64.encode( control.getValue() ) );
402                     }
403 
404                     sb.append( stripLineToNChars( controlStr.toString(), length ) );
405                     sb.append( '\n' );
406                 }
407             }
408 
409             sb.append( stripLineToNChars( "changetype: " + changeType, length ) );
410             sb.append( '\n' );
411         }
412 
413         switch ( entry.getChangeType() )
414         {
415             case None:
416                 if ( entry.hasControls() )
417                 {
418                     sb.append( stripLineToNChars( "changetype: " + ChangeType.Add, length ) );
419                 }
420 
421                 // Fallthrough
422 
423             case Add:
424                 if ( ( entry.getEntry() == null ) )
425                 {
426                     throw new LdapException( I18n.err( I18n.ERR_12082 ) );
427                 }
428 
429                 // Now, iterate through all the attributes
430                 for ( Attribute attribute : entry.getEntry() )
431                 {
432                     sb.append( convertToLdif( attribute, length ) );
433                 }
434 
435                 break;
436 
437             case Delete:
438                 if ( entry.getEntry() != null )
439                 {
440                     throw new LdapException( I18n.err( I18n.ERR_12081 ) );
441                 }
442 
443                 break;
444 
445             case ModDn:
446             case ModRdn:
447                 if ( entry.getEntry() != null )
448                 {
449                     throw new LdapException( I18n.err( I18n.ERR_12083 ) );
450                 }
451 
452                 // Stores the new Rdn
453                 Attribute newRdn = new DefaultAttribute( "newrdn", entry.getNewRdn() );
454                 sb.append( convertToLdif( newRdn, length ) );
455 
456                 // Stores the deleteoldrdn flag
457                 sb.append( "deleteoldrdn: " );
458 
459                 if ( entry.isDeleteOldRdn() )
460                 {
461                     sb.append( "1" );
462                 }
463                 else
464                 {
465                     sb.append( "0" );
466                 }
467 
468                 sb.append( '\n' );
469 
470                 // Stores the optional newSuperior
471                 if ( !Strings.isEmpty( entry.getNewSuperior() ) )
472                 {
473                     Attribute newSuperior = new DefaultAttribute( "newsuperior", entry.getNewSuperior() );
474                     sb.append( convertToLdif( newSuperior, length ) );
475                 }
476 
477                 break;
478 
479             case Modify:
480                 for ( Modification modification : entry.getModifications() )
481                 {
482                     switch ( modification.getOperation() )
483                     {
484                         case ADD_ATTRIBUTE:
485                             sb.append( "add: " );
486                             break;
487 
488                         case REMOVE_ATTRIBUTE:
489                             sb.append( "delete: " );
490                             break;
491 
492                         case REPLACE_ATTRIBUTE:
493                             sb.append( "replace: " );
494                             break;
495                     }
496 
497                     sb.append( modification.getAttribute().getUpId() );
498                     sb.append( '\n' );
499 
500                     sb.append( convertToLdif( modification.getAttribute() ) );
501                     sb.append( "-\n" );
502                 }
503 
504                 break;
505         }
506 
507         sb.append( '\n' );
508 
509         return sb.toString();
510     }
511 
512 
513     /**
514      * Base64 encode a String
515      * 
516      * @param str The string to encode
517      * @return the base 64 encoded string
518      */
519     private static String encodeBase64( String str )
520     {
521         char[] encoded = null;
522 
523         try
524         {
525             // force encoding using UTF-8 charset, as required in RFC2849 note 7
526             encoded = Base64.encode( str.getBytes( "UTF-8" ) );
527         }
528         catch ( UnsupportedEncodingException e )
529         {
530             encoded = Base64.encode( str.getBytes() );
531         }
532 
533         return new String( encoded );
534     }
535 
536 
537     /**
538      * Converts an EntryAttribute to LDIF
539      * 
540      * @param attr the >EntryAttribute to convert
541      * @return the corresponding LDIF code as a String
542      * @throws LdapException If a naming exception is encountered.
543      */
544     public static String convertToLdif( Attribute attr ) throws LdapException
545     {
546         return convertToLdif( attr, DEFAULT_LINE_LENGTH );
547     }
548 
549 
550     /**
551      * Converts an EntryAttribute as LDIF
552      * 
553      * @param attr the EntryAttribute to convert
554      * @param length the expected line length
555      * @return the corresponding LDIF code as a String
556      * @throws LdapException If a naming exception is encountered.
557      */
558     public static String convertToLdif( Attribute attr, int length ) throws LdapException
559     {
560         StringBuilder sb = new StringBuilder();
561 
562         for ( Value<?> value : attr )
563         {
564             StringBuilder lineBuffer = new StringBuilder();
565 
566             lineBuffer.append( attr.getUpId() );
567 
568             // First, deal with null value (which is valid)
569             if ( value.isNull() )
570             {
571                 lineBuffer.append( ':' );
572             }
573             else if ( value.isHumanReadable() )
574             {
575                 // It's a String but, we have to check if encoding isn't required
576                 String str = value.getString();
577 
578                 if ( !LdifUtils.isLDIFSafe( str ) )
579                 {
580                     lineBuffer.append( ":: " + encodeBase64( str ) );
581                 }
582                 else
583                 {
584                     lineBuffer.append( ":" );
585 
586                     if ( str != null )
587                     {
588                         lineBuffer.append( " " ).append( str );
589                     }
590                 }
591             }
592             else
593             {
594                 // It is binary, so we have to encode it using Base64 before adding it
595                 char[] encoded = Base64.encode( value.getBytes() );
596 
597                 lineBuffer.append( ":: " + new String( encoded ) );
598             }
599 
600             lineBuffer.append( "\n" );
601             sb.append( stripLineToNChars( lineBuffer.toString(), length ) );
602         }
603 
604         return sb.toString();
605     }
606 
607 
608     /**
609      * Strips the String every n specified characters
610      * 
611      * @param str the string to strip
612      * @param nbChars the number of characters
613      * @return the stripped String
614      */
615     public static String stripLineToNChars( String str, int nbChars )
616     {
617         int strLength = str.length();
618 
619         if ( strLength <= nbChars )
620         {
621             return str;
622         }
623 
624         if ( nbChars < 2 )
625         {
626             throw new IllegalArgumentException( I18n.err( I18n.ERR_12084 ) );
627         }
628 
629         // We will first compute the new size of the LDIF result
630         // It's at least nbChars chars plus one for \n
631         int charsPerLine = nbChars - 1;
632 
633         int remaining = ( strLength - nbChars ) % charsPerLine;
634 
635         int nbLines = 1 + ( ( strLength - nbChars ) / charsPerLine ) + ( remaining == 0 ? 0 : 1 );
636 
637         int nbCharsTotal = strLength + nbLines + nbLines - 2;
638 
639         char[] buffer = new char[nbCharsTotal];
640         char[] orig = str.toCharArray();
641 
642         int posSrc = 0;
643         int posDst = 0;
644 
645         System.arraycopy( orig, posSrc, buffer, posDst, nbChars );
646         posSrc += nbChars;
647         posDst += nbChars;
648 
649         for ( int i = 0; i < nbLines - 2; i++ )
650         {
651             buffer[posDst++] = '\n';
652             buffer[posDst++] = ' ';
653 
654             System.arraycopy( orig, posSrc, buffer, posDst, charsPerLine );
655             posSrc += charsPerLine;
656             posDst += charsPerLine;
657         }
658 
659         buffer[posDst++] = '\n';
660         buffer[posDst++] = ' ';
661         System.arraycopy( orig, posSrc, buffer, posDst, remaining == 0 ? charsPerLine : remaining );
662 
663         return new String( buffer );
664     }
665 
666 
667     /**
668      * Build a new Attributes instance from a LDIF list of lines. The values can be
669      * either a complete Ava, or a couple of AttributeType ID and a value (a String or
670      * a byte[]). The following sample shows the three cases :
671      *
672      * <pre>
673      * Attribute attr = AttributeUtils.createAttributes(
674      *     "objectclass: top",
675      *     "cn", "My name",
676      *     "jpegPhoto", new byte[]{0x01, 0x02} );
677      * </pre>
678      *
679      * @param avas The AttributeType and Values, using a ldif format, or a couple of
680      * Attribute ID/Value
681      * @return An Attributes instance
682      * @throws LdapException If the data are invalid
683      */
684     public static Attributes createJndiAttributes( Object... avas ) throws LdapException
685     {
686         StringBuilder sb = new StringBuilder();
687         int pos = 0;
688         boolean valueExpected = false;
689 
690         for ( Object ava : avas )
691         {
692             if ( !valueExpected )
693             {
694                 if ( !( ava instanceof String ) )
695                 {
696                     throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n.err(
697                         I18n.ERR_12085, ( pos + 1 ) ) );
698                 }
699 
700                 String attribute = ( String ) ava;
701                 sb.append( attribute );
702 
703                 if ( attribute.indexOf( ':' ) != -1 )
704                 {
705                     sb.append( '\n' );
706                 }
707                 else
708                 {
709                     valueExpected = true;
710                 }
711             }
712             else
713             {
714                 if ( ava instanceof String )
715                 {
716                     sb.append( ": " ).append( ( String ) ava ).append( '\n' );
717                 }
718                 else if ( ava instanceof byte[] )
719                 {
720                     sb.append( ":: " );
721                     sb.append( new String( Base64.encode( ( byte[] ) ava ) ) );
722                     sb.append( '\n' );
723                 }
724                 else
725                 {
726                     throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n.err(
727                         I18n.ERR_12086, ( pos + 1 ) ) );
728                 }
729 
730                 valueExpected = false;
731             }
732         }
733 
734         if ( valueExpected )
735         {
736             throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n
737                 .err( I18n.ERR_12087 ) );
738         }
739 
740         LdifAttributesReader reader = new LdifAttributesReader();
741         Attributes attributes = AttributeUtils.toAttributes( reader.parseEntry( sb.toString() ) );
742         
743         try
744         {
745             reader.close();
746         }
747         catch ( IOException e )
748         {
749             e.printStackTrace();
750         }
751 
752         return attributes;
753     }
754 }