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