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.BufferedReader;
023    import java.io.IOException;
024    import java.io.StringReader;
025    import java.util.ArrayList;
026    
027    import javax.naming.directory.Attribute;
028    import javax.naming.directory.Attributes;
029    import javax.naming.directory.BasicAttributes;
030    
031    import org.apache.directory.shared.i18n.I18n;
032    import org.apache.directory.shared.ldap.entry.Entry;
033    import org.apache.directory.shared.ldap.entry.EntryAttribute;
034    import org.apache.directory.shared.ldap.entry.client.DefaultClientEntry;
035    import org.apache.directory.shared.ldap.util.StringTools;
036    import org.slf4j.Logger;
037    import org.slf4j.LoggerFactory;
038    
039    /**
040     * <pre>
041     *  &lt;ldif-file&gt; ::= &quot;version:&quot; &lt;fill&gt; &lt;number&gt; &lt;seps&gt; &lt;dn-spec&gt; &lt;sep&gt; 
042     *  &lt;ldif-content-change&gt;
043     *  
044     *  &lt;ldif-content-change&gt; ::= 
045     *    &lt;number&gt; &lt;oid&gt; &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; &lt;attrval-specs-e&gt; 
046     *    &lt;ldif-attrval-record-e&gt; | 
047     *    &lt;alpha&gt; &lt;chars-e&gt; &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; &lt;attrval-specs-e&gt; 
048     *    &lt;ldif-attrval-record-e&gt; | 
049     *    &quot;control:&quot; &lt;fill&gt; &lt;number&gt; &lt;oid&gt; &lt;spaces-e&gt; &lt;criticality&gt; 
050     *    &lt;value-spec-e&gt; &lt;sep&gt; &lt;controls-e&gt; 
051     *        &quot;changetype:&quot; &lt;fill&gt; &lt;changerecord-type&gt; &lt;ldif-change-record-e&gt; |
052     *    &quot;changetype:&quot; &lt;fill&gt; &lt;changerecord-type&gt; &lt;ldif-change-record-e&gt;
053     *                              
054     *  &lt;ldif-attrval-record-e&gt; ::= &lt;seps&gt; &lt;dn-spec&gt; &lt;sep&gt; &lt;attributeType&gt; 
055     *    &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; &lt;attrval-specs-e&gt; 
056     *    &lt;ldif-attrval-record-e&gt; | e
057     *                              
058     *  &lt;ldif-change-record-e&gt; ::= &lt;seps&gt; &lt;dn-spec&gt; &lt;sep&gt; &lt;controls-e&gt; 
059     *    &quot;changetype:&quot; &lt;fill&gt; &lt;changerecord-type&gt; &lt;ldif-change-record-e&gt; | e
060     *                              
061     *  &lt;dn-spec&gt; ::= &quot;dn:&quot; &lt;fill&gt; &lt;safe-string&gt; | &quot;dn::&quot; &lt;fill&gt; &lt;base64-string&gt;
062     *                              
063     *  &lt;controls-e&gt; ::= &quot;control:&quot; &lt;fill&gt; &lt;number&gt; &lt;oid&gt; &lt;spaces-e&gt; &lt;criticality&gt; 
064     *    &lt;value-spec-e&gt; &lt;sep&gt; &lt;controls-e&gt; | e
065     *                              
066     *  &lt;criticality&gt; ::= &quot;true&quot; | &quot;false&quot; | e
067     *                              
068     *  &lt;oid&gt; ::= '.' &lt;number&gt; &lt;oid&gt; | e
069     *                              
070     *  &lt;attrval-specs-e&gt; ::= &lt;number&gt; &lt;oid&gt; &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; 
071     *  &lt;attrval-specs-e&gt; | 
072     *    &lt;alpha&gt; &lt;chars-e&gt; &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; &lt;attrval-specs-e&gt; | e
073     *                              
074     *  &lt;value-spec-e&gt; ::= &lt;value-spec&gt; | e
075     *  
076     *  &lt;value-spec&gt; ::= ':' &lt;fill&gt; &lt;safe-string-e&gt; | 
077     *    &quot;::&quot; &lt;fill&gt; &lt;base64-chars&gt; | 
078     *    &quot;:&lt;&quot; &lt;fill&gt; &lt;url&gt;
079     *  
080     *  &lt;attributeType&gt; ::= &lt;number&gt; &lt;oid&gt; | &lt;alpha&gt; &lt;chars-e&gt;
081     *  
082     *  &lt;options-e&gt; ::= ';' &lt;char&gt; &lt;chars-e&gt; &lt;options-e&gt; |e
083     *                              
084     *  &lt;chars-e&gt; ::= &lt;char&gt; &lt;chars-e&gt; |  e
085     *  
086     *  &lt;changerecord-type&gt; ::= &quot;add&quot; &lt;sep&gt; &lt;attributeType&gt; &lt;options-e&gt; &lt;value-spec&gt; 
087     *  &lt;sep&gt; &lt;attrval-specs-e&gt; | 
088     *    &quot;delete&quot; &lt;sep&gt; | 
089     *    &quot;modify&quot; &lt;sep&gt; &lt;mod-type&gt; &lt;fill&gt; &lt;attributeType&gt; &lt;options-e&gt; &lt;sep&gt; 
090     *    &lt;attrval-specs-e&gt; &lt;sep&gt; '-' &lt;sep&gt; &lt;mod-specs-e&gt; | 
091     *    &quot;moddn&quot; &lt;sep&gt; &lt;newrdn&gt; &lt;sep&gt; &quot;deleteoldrdn:&quot; &lt;fill&gt; &lt;0-1&gt; &lt;sep&gt; 
092     *    &lt;newsuperior-e&gt; &lt;sep&gt; |
093     *    &quot;modrdn&quot; &lt;sep&gt; &lt;newrdn&gt; &lt;sep&gt; &quot;deleteoldrdn:&quot; &lt;fill&gt; &lt;0-1&gt; &lt;sep&gt; 
094     *    &lt;newsuperior-e&gt; &lt;sep&gt;
095     *  
096     *  &lt;newrdn&gt; ::= ':' &lt;fill&gt; &lt;safe-string&gt; | &quot;::&quot; &lt;fill&gt; &lt;base64-chars&gt;
097     *  
098     *  &lt;newsuperior-e&gt; ::= &quot;newsuperior&quot; &lt;newrdn&gt; | e
099     *  
100     *  &lt;mod-specs-e&gt; ::= &lt;mod-type&gt; &lt;fill&gt; &lt;attributeType&gt; &lt;options-e&gt; 
101     *    &lt;sep&gt; &lt;attrval-specs-e&gt; &lt;sep&gt; '-' &lt;sep&gt; &lt;mod-specs-e&gt; | e
102     *  
103     *  &lt;mod-type&gt; ::= &quot;add:&quot; | &quot;delete:&quot; | &quot;replace:&quot;
104     *  
105     *  &lt;url&gt; ::= &lt;a Uniform Resource Locator, as defined in [6]&gt;
106     *  
107     *  
108     *  
109     *  LEXICAL
110     *  -------
111     *  
112     *  &lt;fill&gt;           ::= ' ' &lt;fill&gt; | e
113     *  &lt;char&gt;           ::= &lt;alpha&gt; | &lt;digit&gt; | '-'
114     *  &lt;number&gt;         ::= &lt;digit&gt; &lt;digits&gt;
115     *  &lt;0-1&gt;            ::= '0' | '1'
116     *  &lt;digits&gt;         ::= &lt;digit&gt; &lt;digits&gt; | e
117     *  &lt;digit&gt;          ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
118     *  &lt;seps&gt;           ::= &lt;sep&gt; &lt;seps-e&gt; 
119     *  &lt;seps-e&gt;         ::= &lt;sep&gt; &lt;seps-e&gt; | e
120     *  &lt;sep&gt;            ::= 0x0D 0x0A | 0x0A
121     *  &lt;spaces&gt;         ::= ' ' &lt;spaces-e&gt;
122     *  &lt;spaces-e&gt;       ::= ' ' &lt;spaces-e&gt; | e
123     *  &lt;safe-string-e&gt;  ::= &lt;safe-string&gt; | e
124     *  &lt;safe-string&gt;    ::= &lt;safe-init-char&gt; &lt;safe-chars&gt;
125     *  &lt;safe-init-char&gt; ::= [0x01-0x09] | 0x0B | 0x0C | [0x0E-0x1F] | [0x21-0x39] | 0x3B | [0x3D-0x7F]
126     *  &lt;safe-chars&gt;     ::= &lt;safe-char&gt; &lt;safe-chars&gt; | e
127     *  &lt;safe-char&gt;      ::= [0x01-0x09] | 0x0B | 0x0C | [0x0E-0x7F]
128     *  &lt;base64-string&gt;  ::= &lt;base64-char&gt; &lt;base64-chars&gt;
129     *  &lt;base64-chars&gt;   ::= &lt;base64-char&gt; &lt;base64-chars&gt; | e
130     *  &lt;base64-char&gt;    ::= 0x2B | 0x2F | [0x30-0x39] | 0x3D | [0x41-9x5A] | [0x61-0x7A]
131     *  &lt;alpha&gt;          ::= [0x41-0x5A] | [0x61-0x7A]
132     *  
133     *  COMMENTS
134     *  --------
135     *  - The ldap-oid VN is not correct in the RFC-2849. It has been changed from 1*DIGIT 0*1(&quot;.&quot; 1*DIGIT) to
136     *  DIGIT+ (&quot;.&quot; DIGIT+)*
137     *  - The mod-spec lacks a sep between *attrval-spec and &quot;-&quot;.
138     *  - The BASE64-UTF8-STRING should be BASE64-CHAR BASE64-STRING
139     *  - The ValueSpec rule must accept multilines values. In this case, we have a LF followed by a 
140     *  single space before the continued value.
141     * </pre>
142     * 
143     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
144     * @version $Rev$, $Date$
145     */
146    public class LdifAttributesReader extends LdifReader
147    {
148        /** A logger */
149        private static final Logger LOG = LoggerFactory.getLogger( LdifAttributesReader.class );
150    
151        /**
152         * Constructors
153         */
154        public LdifAttributesReader()
155        {
156            lines = new ArrayList<String>();
157            position = new Position();
158            version = DEFAULT_VERSION;
159        }
160    
161    
162        /**
163         * Parse an AttributeType/AttributeValue
164         * 
165         * @param attributes The entry where to store the value
166         * @param line The line to parse
167         * @param lowerLine The same line, lowercased
168         * @throws NamingException If anything goes wrong
169         */
170        private void parseAttribute( Attributes attributes, String line, String lowerLine ) throws LdapLdifException
171        {
172            int colonIndex = line.indexOf( ':' );
173    
174            String attributeType = lowerLine.substring( 0, colonIndex );
175    
176            // We should *not* have a DN twice
177            if ( attributeType.equals( "dn" ) )
178            {
179                LOG.error( I18n.err( I18n.ERR_12002 ) );
180                throw new LdapLdifException( I18n.err( I18n.ERR_12003 ) );
181            }
182    
183            Object attributeValue = parseValue( line, colonIndex );
184    
185            // Update the entry
186            Attribute attribute = attributes.get( attributeType );
187            
188            if ( attribute == null )
189            {
190                attributes.put( attributeType, attributeValue );
191            }
192            else
193            {
194                attribute.add( attributeValue );
195            }
196        }
197    
198        
199    
200    
201        /**
202         * Parse an AttributeType/AttributeValue
203         * 
204         * @param attributes The entry where to store the value
205         * @param line The line to parse
206         * @param lowerLine The same line, lowercased
207         * @throws NamingException If anything goes wrong
208         */
209        private void parseEntryAttribute( Entry entry, String line, String lowerLine ) throws LdapLdifException
210        {
211            int colonIndex = line.indexOf( ':' );
212    
213            String attributeType = lowerLine.substring( 0, colonIndex );
214    
215            // We should *not* have a DN twice
216            if ( attributeType.equals( "dn" ) )
217            {
218                LOG.error( I18n.err( I18n.ERR_12002 ) );
219                throw new LdapLdifException( I18n.err( I18n.ERR_12003 ) );
220            }
221    
222            Object attributeValue = parseValue( line, colonIndex );
223    
224            // Update the entry
225            EntryAttribute attribute = entry.get( attributeType );
226            
227            if ( attribute == null )
228            {
229                if ( attributeValue instanceof String )
230                {
231                    entry.put( attributeType, (String)attributeValue );
232                }
233                else
234                {
235                    entry.put( attributeType, (byte[])attributeValue );
236                }
237            }
238            else
239            {
240                if ( attributeValue instanceof String )
241                {
242                    attribute.add( (String)attributeValue );
243                }
244                else
245                {
246                    attribute.add( (byte[])attributeValue );
247                }
248            }
249        }
250    
251        
252        /**
253         * Parse a ldif file. The following rules are processed :
254         * 
255         * &lt;ldif-file&gt; ::= &lt;ldif-attrval-record&gt; &lt;ldif-attrval-records&gt; |
256         * &lt;ldif-change-record&gt; &lt;ldif-change-records&gt; &lt;ldif-attrval-record&gt; ::=
257         * &lt;dn-spec&gt; &lt;sep&gt; &lt;attrval-spec&gt; &lt;attrval-specs&gt; &lt;ldif-change-record&gt; ::=
258         * &lt;dn-spec&gt; &lt;sep&gt; &lt;controls-e&gt; &lt;changerecord&gt; &lt;dn-spec&gt; ::= "dn:" &lt;fill&gt;
259         * &lt;distinguishedName&gt; | "dn::" &lt;fill&gt; &lt;base64-distinguishedName&gt;
260         * &lt;changerecord&gt; ::= "changetype:" &lt;fill&gt; &lt;change-op&gt;
261         * 
262         * @return The read entry
263         * @throws LdapLdifException If the entry can't be read or is invalid
264         */
265        private Entry parseEntry() throws LdapLdifException
266        {
267            if ( ( lines == null ) || ( lines.size() == 0 ) )
268            {
269                LOG.debug( "The entry is empty : end of ldif file" );
270                return null;
271            }
272    
273            Entry entry = new DefaultClientEntry();
274    
275            // Now, let's iterate through the other lines
276            for ( String line:lines )
277            {
278                // Each line could start either with an OID, an attribute type, with
279                // "control:" or with "changetype:"
280                String lowerLine = line.toLowerCase();
281    
282                // We have three cases :
283                // 1) The first line after the DN is a "control:" -> this is an error
284                // 2) The first line after the DN is a "changeType:" -> this is an error
285                // 3) The first line after the DN is anything else
286                if ( lowerLine.startsWith( "control:" ) )
287                {
288                    LOG.error( I18n.err( I18n.ERR_12004 ) );
289                    throw new LdapLdifException( I18n.err( I18n.ERR_12005 ) );
290                }
291                else if ( lowerLine.startsWith( "changetype:" ) )
292                {
293                    LOG.error( I18n.err( I18n.ERR_12004 ) );
294                    throw new LdapLdifException( I18n.err( I18n.ERR_12005 ) );
295                }
296                else if ( line.indexOf( ':' ) > 0 )
297                {
298                    parseEntryAttribute( entry, line, lowerLine );
299                }
300                else
301                {
302                    // Invalid attribute Value
303                    LOG.error( I18n.err( I18n.ERR_12006 ) );
304                    throw new LdapLdifException( I18n.err( I18n.ERR_12007 ) );
305                }
306            }
307    
308            LOG.debug( "Read an attributes : {}", entry );
309            
310            return entry;
311        }
312    
313    
314        /**
315         * Parse a ldif file. The following rules are processed :
316         * 
317         * &lt;ldif-file&gt; ::= &lt;ldif-attrval-record&gt; &lt;ldif-attrval-records&gt; |
318         * &lt;ldif-change-record&gt; &lt;ldif-change-records&gt; &lt;ldif-attrval-record&gt; ::=
319         * &lt;dn-spec&gt; &lt;sep&gt; &lt;attrval-spec&gt; &lt;attrval-specs&gt; &lt;ldif-change-record&gt; ::=
320         * &lt;dn-spec&gt; &lt;sep&gt; &lt;controls-e&gt; &lt;changerecord&gt; &lt;dn-spec&gt; ::= "dn:" &lt;fill&gt;
321         * &lt;distinguishedName&gt; | "dn::" &lt;fill&gt; &lt;base64-distinguishedName&gt;
322         * &lt;changerecord&gt; ::= "changetype:" &lt;fill&gt; &lt;change-op&gt;
323         * 
324         * @return The read entry
325         * @throws NamingException If the entry can't be read or is invalid
326         */
327        private Attributes parseAttributes() throws LdapLdifException
328        {
329            if ( ( lines == null ) || ( lines.size() == 0 ) )
330            {
331                LOG.debug( "The entry is empty : end of ldif file" );
332                return null;
333            }
334    
335            Attributes attributes = new BasicAttributes( true );
336    
337            // Now, let's iterate through the other lines
338            for ( String line:lines )
339            {
340                // Each line could start either with an OID, an attribute type, with
341                // "control:" or with "changetype:"
342                String lowerLine = line.toLowerCase();
343    
344                // We have three cases :
345                // 1) The first line after the DN is a "control:" -> this is an error
346                // 2) The first line after the DN is a "changeType:" -> this is an error
347                // 3) The first line after the DN is anything else
348                if ( lowerLine.startsWith( "control:" ) )
349                {
350                    LOG.error( I18n.err( I18n.ERR_12004 ) );
351                    throw new LdapLdifException( I18n.err( I18n.ERR_12005 ) );
352                }
353                else if ( lowerLine.startsWith( "changetype:" ) )
354                {
355                    LOG.error( I18n.err( I18n.ERR_12004 ) );
356                    throw new LdapLdifException( I18n.err( I18n.ERR_12005 ) );
357                }
358                else if ( line.indexOf( ':' ) > 0 )
359                {
360                    parseAttribute( attributes, line, lowerLine );
361                }
362                else
363                {
364                    // Invalid attribute Value
365                    LOG.error( I18n.err( I18n.ERR_12006 ) );
366                    throw new LdapLdifException( I18n.err( I18n.ERR_12007 ) );
367                }
368            }
369    
370            LOG.debug( "Read an attributes : {}", attributes );
371            
372            return attributes;
373        }
374    
375    
376        /**
377         * A method which parses a ldif string and returns a list of Attributes.
378         * 
379         * @param ldif The ldif string
380         * @return A list of Attributes, or an empty List
381         * @throws LdapLdifException If something went wrong
382         */
383        public Attributes parseAttributes( String ldif ) throws LdapLdifException
384        {
385            lines = new ArrayList<String>();
386            position = new Position();
387    
388            LOG.debug( "Starts parsing ldif buffer" );
389    
390            if ( StringTools.isEmpty( ldif ) )
391            {
392                return new BasicAttributes( true );
393            }
394    
395            StringReader strIn = new StringReader( ldif );
396            reader = new BufferedReader( strIn );
397    
398            try
399            {
400                readLines();
401                
402                Attributes attributes = parseAttributes();
403    
404                if ( LOG.isDebugEnabled() )
405                {
406                    LOG.debug( "Parsed {} entries.", ( attributes == null ? 0 : 1 ) );
407                }
408    
409                return attributes;
410            }
411            catch (LdapLdifException ne)
412            {
413                LOG.error( I18n.err( I18n.ERR_12008, ne.getLocalizedMessage() ) );
414                throw new LdapLdifException( I18n.err( I18n.ERR_12009 ) );
415            }
416            finally
417            {
418                try
419                {
420                    reader.close();
421                }
422                catch ( IOException ioe )
423                {
424                    // Do nothing
425                }
426            }
427        }
428        
429        
430        /**
431         * A method which parses a ldif string and returns a list of Entry.
432         * 
433         * @param ldif The ldif string
434         * @return A list of Entry, or an empty List
435         * @throws LdapLdifException If something went wrong
436         */
437        public Entry parseEntry( String ldif ) throws LdapLdifException
438        {
439            lines = new ArrayList<String>();
440            position = new Position();
441    
442            LOG.debug( "Starts parsing ldif buffer" );
443    
444            if ( StringTools.isEmpty( ldif ) )
445            {
446                return new DefaultClientEntry();
447            }
448    
449            StringReader strIn = new StringReader( ldif );
450            reader = new BufferedReader( strIn );
451    
452            try
453            {
454                readLines();
455                
456                Entry entry = parseEntry();
457    
458                if ( LOG.isDebugEnabled() )
459                {
460                    LOG.debug( "Parsed {} entries.", ( entry == null ? 0 : 1 ) );
461                }
462    
463                return entry;
464            }
465            catch (LdapLdifException ne)
466            {
467                LOG.error( I18n.err( I18n.ERR_12008, ne.getLocalizedMessage() ) );
468                throw new LdapLdifException( I18n.err( I18n.ERR_12009 ) );
469            }
470            finally
471            {
472                try
473                {
474                    reader.close();
475                }
476                catch ( IOException ioe )
477                {
478                    // Do nothing
479                }
480            }
481        }
482    }