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;
024import java.io.UnsupportedEncodingException;
025
026import javax.naming.directory.Attributes;
027
028import org.apache.directory.api.i18n.I18n;
029import org.apache.directory.api.ldap.model.entry.Attribute;
030import org.apache.directory.api.ldap.model.entry.AttributeUtils;
031import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
032import org.apache.directory.api.ldap.model.entry.Entry;
033import org.apache.directory.api.ldap.model.entry.Modification;
034import org.apache.directory.api.ldap.model.entry.Value;
035import org.apache.directory.api.ldap.model.exception.LdapException;
036import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
037import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
038import org.apache.directory.api.ldap.model.name.Dn;
039import org.apache.directory.api.util.Base64;
040import org.apache.directory.api.util.Strings;
041
042
043/**
044 * Some LDIF helper methods.
045 *
046 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
047 */
048public final class LdifUtils
049{
050    /** The array that will be used to match the first char.*/
051    private static final boolean[] LDIF_SAFE_STARTING_CHAR_ALPHABET = new boolean[128];
052
053    /** The array that will be used to match the other chars.*/
054    private static final boolean[] LDIF_SAFE_OTHER_CHARS_ALPHABET = new boolean[128];
055
056    /** The default length for a line in a ldif file */
057    private static final int DEFAULT_LINE_LENGTH = 80;
058
059    /** The file separator */
060    private static final String LINE_SEPARATOR = System.getProperty( "line.separator" );
061
062    static
063    {
064        // Initialization of the array that will be used to match the first char.
065        for ( int i = 0; i < 128; i++ )
066        {
067            LDIF_SAFE_STARTING_CHAR_ALPHABET[i] = true;
068        }
069
070        LDIF_SAFE_STARTING_CHAR_ALPHABET[0] = false; // 0 (NUL)
071        LDIF_SAFE_STARTING_CHAR_ALPHABET[10] = false; // 10 (LF)
072        LDIF_SAFE_STARTING_CHAR_ALPHABET[13] = false; // 13 (CR)
073        LDIF_SAFE_STARTING_CHAR_ALPHABET[32] = false; // 32 (SPACE)
074        LDIF_SAFE_STARTING_CHAR_ALPHABET[58] = false; // 58 (:)
075        LDIF_SAFE_STARTING_CHAR_ALPHABET[60] = false; // 60 (>)
076
077        // Initialization of the array that will be used to match the other chars.
078        for ( int i = 0; i < 128; i++ )
079        {
080            LDIF_SAFE_OTHER_CHARS_ALPHABET[i] = true;
081        }
082
083        LDIF_SAFE_OTHER_CHARS_ALPHABET[0] = false; // 0 (NUL)
084        LDIF_SAFE_OTHER_CHARS_ALPHABET[10] = false; // 10 (LF)
085        LDIF_SAFE_OTHER_CHARS_ALPHABET[13] = false; // 13 (CR)
086    }
087
088
089    /**
090     * Private constructor.
091     */
092    private LdifUtils()
093    {
094    }
095
096
097    /**
098     * Checks if the input String contains only safe values, that is, the data
099     * 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}