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 (<)</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        LdifAttributesReader reader = new LdifAttributesReader();
280
281        try
282        {
283            Attributes attributes = AttributeUtils.toAttributes( reader.parseEntry( ldif ) );
284
285            reader.close();
286
287            return attributes;
288        }
289        catch ( IOException ioe )
290        {
291            throw new LdapLdifException( ioe.getMessage() );
292        }
293    }
294
295
296    /**
297     * Convert an Entry as LDIF
298     * 
299     * @param entry the Entry to convert
300     * @param length the expected line length
301     * @return the corresponding LDIF code as a String
302     * @throws LdapException If a naming exception is encountered.
303     */
304    public static String convertToLdif( Entry entry, int length ) throws LdapException
305    {
306        StringBuilder sb = new StringBuilder();
307
308        if ( entry.getDn() != null )
309        {
310            // First, dump the Dn
311            if ( isLDIFSafe( entry.getDn().getName() ) )
312            {
313                sb.append( stripLineToNChars( "dn: " + entry.getDn().getName(), length ) );
314            }
315            else
316            {
317                sb.append( stripLineToNChars( "dn:: " + encodeBase64( entry.getDn().getName() ), length ) );
318            }
319
320            sb.append( '\n' );
321        }
322
323        // Then all the attributes
324        for ( Attribute attribute : entry )
325        {
326            sb.append( convertToLdif( attribute, length ) );
327        }
328
329        return sb.toString();
330    }
331
332
333    /**
334     * Convert the Entry's attributes to LDIF. The Dn is not written.
335     * 
336     * @param entry the Entry to convert
337     * @param length the expected line length
338     * @return the corresponding LDIF code as a String
339     * @throws LdapException If a naming exception is encountered.
340     */
341    public static String convertAttributesToLdif( Entry entry, int length ) throws LdapException
342    {
343        StringBuilder sb = new StringBuilder();
344
345        // Then all the attributes
346        for ( Attribute attribute : entry )
347        {
348            sb.append( convertToLdif( attribute, length ) );
349        }
350
351        return sb.toString();
352    }
353
354
355    /**
356     * Convert an LdifEntry to LDIF
357     * 
358     * @param entry the LdifEntry to convert
359     * @return the corresponding LDIF as a String
360     * @throws LdapException If a naming exception is encountered.
361     */
362    public static String convertToLdif( LdifEntry entry ) throws LdapException
363    {
364        return convertToLdif( entry, DEFAULT_LINE_LENGTH );
365    }
366
367
368    /**
369     * Convert an LdifEntry to LDIF
370     * 
371     * @param entry the LdifEntry to convert
372     * @param length The maximum line's length
373     * @return the corresponding LDIF as a String
374     * @throws LdapException If a naming exception is encountered.
375     */
376    public static String convertToLdif( LdifEntry entry, int length ) throws LdapException
377    {
378        StringBuilder sb = new StringBuilder();
379
380        // First, dump the Dn
381        if ( isLDIFSafe( entry.getDn().getName() ) )
382        {
383            sb.append( stripLineToNChars( "dn: " + entry.getDn(), length ) );
384        }
385        else
386        {
387            sb.append( stripLineToNChars( "dn:: " + encodeBase64( entry.getDn().getName() ), length ) );
388        }
389
390        sb.append( '\n' );
391
392        // Dump the ChangeType
393        String changeType = Strings.toLowerCase( entry.getChangeType().toString() );
394
395        if ( entry.getChangeType() != ChangeType.None )
396        {
397            // First dump the controls if any
398            if ( entry.hasControls() )
399            {
400                for ( LdifControl control : entry.getControls().values() )
401                {
402                    StringBuilder controlStr = new StringBuilder();
403
404                    controlStr.append( "control: " ).append( control.getOid() );
405                    controlStr.append( " " ).append( control.isCritical() );
406
407                    if ( control.hasValue() )
408                    {
409                        controlStr.append( "::" ).append( Base64.encode( control.getValue() ) );
410                    }
411
412                    sb.append( stripLineToNChars( controlStr.toString(), length ) );
413                    sb.append( '\n' );
414                }
415            }
416
417            sb.append( stripLineToNChars( "changetype: " + changeType, length ) );
418            sb.append( '\n' );
419        }
420
421        switch ( entry.getChangeType() )
422        {
423            case None:
424                if ( entry.hasControls() )
425                {
426                    sb.append( stripLineToNChars( "changetype: " + ChangeType.Add, length ) );
427                }
428
429                // Fallthrough
430
431            case Add:
432                if ( ( entry.getEntry() == null ) )
433                {
434                    throw new LdapException( I18n.err( I18n.ERR_12082 ) );
435                }
436
437                // Now, iterate through all the attributes
438                for ( Attribute attribute : entry.getEntry() )
439                {
440                    sb.append( convertToLdif( attribute, length ) );
441                }
442
443                break;
444
445            case Delete:
446                if ( entry.getEntry() != null )
447                {
448                    throw new LdapException( I18n.err( I18n.ERR_12081 ) );
449                }
450
451                break;
452
453            case ModDn:
454            case ModRdn:
455                if ( entry.getEntry() != null )
456                {
457                    throw new LdapException( I18n.err( I18n.ERR_12083 ) );
458                }
459
460                // Stores the new Rdn
461                Attribute newRdn = new DefaultAttribute( "newrdn", entry.getNewRdn() );
462                sb.append( convertToLdif( newRdn, length ) );
463
464                // Stores the deleteoldrdn flag
465                sb.append( "deleteoldrdn: " );
466
467                if ( entry.isDeleteOldRdn() )
468                {
469                    sb.append( "1" );
470                }
471                else
472                {
473                    sb.append( "0" );
474                }
475
476                sb.append( '\n' );
477
478                // Stores the optional newSuperior
479                if ( !Strings.isEmpty( entry.getNewSuperior() ) )
480                {
481                    Attribute newSuperior = new DefaultAttribute( "newsuperior", entry.getNewSuperior() );
482                    sb.append( convertToLdif( newSuperior, length ) );
483                }
484
485                break;
486
487            case Modify:
488                boolean isFirst = true;
489                
490                for ( Modification modification : entry.getModifications() )
491                {
492                    
493                    if ( isFirst )
494                    {
495                        isFirst = false;
496                    }
497                    else
498                    {
499                        sb.append( "-\n" );
500                    }
501
502                    switch ( modification.getOperation() )
503                    {
504                        case ADD_ATTRIBUTE:
505                            sb.append( "add: " );
506                            break;
507
508                        case REMOVE_ATTRIBUTE:
509                            sb.append( "delete: " );
510                            break;
511
512                        case REPLACE_ATTRIBUTE:
513                            sb.append( "replace: " );
514                            break;
515
516                        default:
517                            throw new IllegalArgumentException( "Unexpected ModificationOperation: "
518                                + modification.getOperation() );
519                    }
520
521                    sb.append( modification.getAttribute().getUpId() );
522                    sb.append( '\n' );
523
524                    sb.append( convertToLdif( modification.getAttribute(), length ) );
525                }
526
527                sb.append( '-' );
528                break;
529
530            default:
531                throw new IllegalArgumentException( "Unexpected ChangeType: " + entry.getChangeType() );
532        }
533
534        sb.append( '\n' );
535
536        return sb.toString();
537    }
538
539
540    /**
541     * Base64 encode a String
542     * 
543     * @param str The string to encode
544     * @return the base 64 encoded string
545     */
546    private static String encodeBase64( String str )
547    {
548        // force encoding using UTF-8 charset, as required in RFC2849 note 7
549        return new String( Base64.encode( Strings.getBytesUtf8( str ) ) );
550    }
551
552
553    /**
554     * Converts an EntryAttribute to LDIF
555     * 
556     * @param attr the >EntryAttribute to convert
557     * @return the corresponding LDIF code as a String
558     * @throws LdapException If a naming exception is encountered.
559     */
560    public static String convertToLdif( Attribute attr ) throws LdapException
561    {
562        return convertToLdif( attr, DEFAULT_LINE_LENGTH );
563    }
564
565
566    /**
567     * Converts an EntryAttribute as LDIF
568     * 
569     * @param attr the EntryAttribute to convert
570     * @param length the expected line length
571     * @return the corresponding LDIF code as a String
572     * @throws LdapException If a naming exception is encountered.
573     */
574    public static String convertToLdif( Attribute attr, int length ) throws LdapException
575    {
576        StringBuilder sb = new StringBuilder();
577        
578        if ( attr.size() == 0 )
579        {
580            // Special case : we don't have any value
581            return "";
582        }
583
584        for ( Value<?> value : attr )
585        {
586            StringBuilder lineBuffer = new StringBuilder();
587
588            lineBuffer.append( attr.getUpId() );
589
590            // First, deal with null value (which is valid)
591            if ( value.isNull() )
592            {
593                lineBuffer.append( ':' );
594            }
595            else if ( value.isHumanReadable() )
596            {
597                // It's a String but, we have to check if encoding isn't required
598                String str = value.getString();
599
600                if ( !LdifUtils.isLDIFSafe( str ) )
601                {
602                    lineBuffer.append( ":: " ).append( encodeBase64( str ) );
603                }
604                else
605                {
606                    lineBuffer.append( ':' );
607
608                    if ( str != null )
609                    {
610                        lineBuffer.append( ' ' ).append( str );
611                    }
612                }
613            }
614            else
615            {
616                // It is binary, so we have to encode it using Base64 before adding it
617                char[] encoded = Base64.encode( value.getBytes() );
618
619                lineBuffer.append( ":: " + new String( encoded ) );
620            }
621
622            lineBuffer.append( '\n' );
623            sb.append( stripLineToNChars( lineBuffer.toString(), length ) );
624        }
625
626        return sb.toString();
627    }
628
629
630    /**
631     * Strips the String every n specified characters
632     * 
633     * @param str the string to strip
634     * @param nbChars the number of characters
635     * @return the stripped String
636     */
637    public static String stripLineToNChars( String str, int nbChars )
638    {
639        int strLength = str.length();
640
641        if ( strLength <= nbChars )
642        {
643            return str;
644        }
645
646        if ( nbChars < 2 )
647        {
648            throw new IllegalArgumentException( I18n.err( I18n.ERR_12084 ) );
649        }
650
651        // We will first compute the new size of the LDIF result
652        // It's at least nbChars chars plus one for \n
653        int charsPerLine = nbChars - 1;
654
655        int remaining = ( strLength - nbChars ) % charsPerLine;
656
657        int nbLines = 1 + ( ( strLength - nbChars ) / charsPerLine ) + ( remaining == 0 ? 0 : 1 );
658
659        int nbCharsTotal = strLength + nbLines + nbLines - 2;
660
661        char[] buffer = new char[nbCharsTotal];
662        char[] orig = str.toCharArray();
663
664        int posSrc = 0;
665        int posDst = 0;
666
667        System.arraycopy( orig, posSrc, buffer, posDst, nbChars );
668        posSrc += nbChars;
669        posDst += nbChars;
670
671        for ( int i = 0; i < nbLines - 2; i++ )
672        {
673            buffer[posDst++] = '\n';
674            buffer[posDst++] = ' ';
675
676            System.arraycopy( orig, posSrc, buffer, posDst, charsPerLine );
677            posSrc += charsPerLine;
678            posDst += charsPerLine;
679        }
680
681        buffer[posDst++] = '\n';
682        buffer[posDst++] = ' ';
683        System.arraycopy( orig, posSrc, buffer, posDst, remaining == 0 ? charsPerLine : remaining );
684
685        return new String( buffer );
686    }
687
688
689    /**
690     * Build a new Attributes instance from a LDIF list of lines. The values can be
691     * either a complete Ava, or a couple of AttributeType ID and a value (a String or
692     * a byte[]). The following sample shows the three cases :
693     *
694     * <pre>
695     * Attribute attr = AttributeUtils.createAttributes(
696     *     "objectclass: top",
697     *     "cn", "My name",
698     *     "jpegPhoto", new byte[]{0x01, 0x02} );
699     * </pre>
700     *
701     * @param avas The AttributeType and Values, using a ldif format, or a couple of
702     * Attribute ID/Value
703     * @return An Attributes instance
704     * @throws LdapException If the data are invalid
705     */
706    public static Attributes createJndiAttributes( Object... avas ) throws LdapException
707    {
708        StringBuilder sb = new StringBuilder();
709        int pos = 0;
710        boolean valueExpected = false;
711
712        for ( Object ava : avas )
713        {
714            if ( !valueExpected )
715            {
716                if ( !( ava instanceof String ) )
717                {
718                    throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n.err(
719                        I18n.ERR_12085, ( pos + 1 ) ) );
720                }
721
722                String attribute = ( String ) ava;
723                sb.append( attribute );
724
725                if ( attribute.indexOf( ':' ) != -1 )
726                {
727                    sb.append( '\n' );
728                }
729                else
730                {
731                    valueExpected = true;
732                }
733            }
734            else
735            {
736                if ( ava instanceof String )
737                {
738                    sb.append( ": " ).append( ( String ) ava ).append( '\n' );
739                }
740                else if ( ava instanceof byte[] )
741                {
742                    sb.append( ":: " );
743                    sb.append( new String( Base64.encode( ( byte[] ) ava ) ) );
744                    sb.append( '\n' );
745                }
746                else
747                {
748                    throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n.err(
749                        I18n.ERR_12086, ( pos + 1 ) ) );
750                }
751
752                valueExpected = false;
753            }
754        }
755
756        if ( valueExpected )
757        {
758            throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n
759                .err( I18n.ERR_12087 ) );
760        }
761
762        LdifAttributesReader reader = new LdifAttributesReader();
763        Attributes attributes = AttributeUtils.toAttributes( reader.parseEntry( sb.toString() ) );
764
765        try
766        {
767            reader.close();
768        }
769        catch ( IOException e )
770        {
771            e.printStackTrace();
772        }
773
774        return attributes;
775    }
776}