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