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