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