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