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