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