001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.directory.api.ldap.model.ldif;
021
022
023import java.io.IOException;
024
025import javax.naming.directory.Attributes;
026
027import org.apache.directory.api.i18n.I18n;
028import org.apache.directory.api.ldap.model.entry.Attribute;
029import org.apache.directory.api.ldap.model.entry.AttributeUtils;
030import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
031import org.apache.directory.api.ldap.model.entry.Entry;
032import org.apache.directory.api.ldap.model.entry.Modification;
033import org.apache.directory.api.ldap.model.entry.Value;
034import org.apache.directory.api.ldap.model.exception.LdapException;
035import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
036import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
037import org.apache.directory.api.ldap.model.name.Dn;
038import org.apache.directory.api.util.Base64;
039import org.apache.directory.api.util.Strings;
040
041
042/**
043 * Some LDIF helper methods.
044 *
045 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
046 */
047public final class LdifUtils
048{
049    /** The array that will be used to match the first char.*/
050    private static final boolean[] LDIF_SAFE_STARTING_CHAR_ALPHABET = new boolean[128];
051
052    /** The array that will be used to match the other chars.*/
053    private static final boolean[] LDIF_SAFE_OTHER_CHARS_ALPHABET = new boolean[128];
054
055    /** The default length for a line in a ldif file */
056    private static final int DEFAULT_LINE_LENGTH = 80;
057
058    /** The file separator */
059    private static final String LINE_SEPARATOR = System.getProperty( "line.separator" );
060
061    static
062    {
063        // Initialization of the array that will be used to match the first char.
064        for ( int i = 0; i < 128; i++ )
065        {
066            LDIF_SAFE_STARTING_CHAR_ALPHABET[i] = true;
067        }
068
069        // 0 (NUL)
070        LDIF_SAFE_STARTING_CHAR_ALPHABET[0] = false;
071        // 10 (LF)
072        LDIF_SAFE_STARTING_CHAR_ALPHABET[10] = false;
073        // 13 (CR)
074        LDIF_SAFE_STARTING_CHAR_ALPHABET[13] = false;
075        // 32 (SPACE)
076        LDIF_SAFE_STARTING_CHAR_ALPHABET[32] = false;
077        // 58 (:)
078        LDIF_SAFE_STARTING_CHAR_ALPHABET[58] = false;
079        // 60 (>)
080        LDIF_SAFE_STARTING_CHAR_ALPHABET[60] = false;
081
082        // Initialization of the array that will be used to match the other chars.
083        for ( int i = 0; i < 128; i++ )
084        {
085            LDIF_SAFE_OTHER_CHARS_ALPHABET[i] = true;
086        }
087
088        // 0 (NUL)
089        LDIF_SAFE_OTHER_CHARS_ALPHABET[0] = false;
090        // 10 (LF)
091        LDIF_SAFE_OTHER_CHARS_ALPHABET[10] = false;
092        // 13 (CR)
093        LDIF_SAFE_OTHER_CHARS_ALPHABET[13] = false;
094    }
095
096
097    /**
098     * Private constructor.
099     */
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}