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.NamingException;
028    import javax.naming.directory.Attribute;
029    import javax.naming.directory.Attributes;
030    import javax.naming.directory.BasicAttributes;
031    
032    import org.apache.directory.shared.i18n.I18n;
033    import org.apache.directory.shared.ldap.util.StringTools;
034    import org.slf4j.Logger;
035    import org.slf4j.LoggerFactory;
036    
037    /**
038     * <pre>
039     *  &lt;ldif-file&gt; ::= &quot;version:&quot; &lt;fill&gt; &lt;number&gt; &lt;seps&gt; &lt;dn-spec&gt; &lt;sep&gt; 
040     *  &lt;ldif-content-change&gt;
041     *  
042     *  &lt;ldif-content-change&gt; ::= 
043     *    &lt;number&gt; &lt;oid&gt; &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; &lt;attrval-specs-e&gt; 
044     *    &lt;ldif-attrval-record-e&gt; | 
045     *    &lt;alpha&gt; &lt;chars-e&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     *    &quot;control:&quot; &lt;fill&gt; &lt;number&gt; &lt;oid&gt; &lt;spaces-e&gt; &lt;criticality&gt; 
048     *    &lt;value-spec-e&gt; &lt;sep&gt; &lt;controls-e&gt; 
049     *        &quot;changetype:&quot; &lt;fill&gt; &lt;changerecord-type&gt; &lt;ldif-change-record-e&gt; |
050     *    &quot;changetype:&quot; &lt;fill&gt; &lt;changerecord-type&gt; &lt;ldif-change-record-e&gt;
051     *                              
052     *  &lt;ldif-attrval-record-e&gt; ::= &lt;seps&gt; &lt;dn-spec&gt; &lt;sep&gt; &lt;attributeType&gt; 
053     *    &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; &lt;attrval-specs-e&gt; 
054     *    &lt;ldif-attrval-record-e&gt; | e
055     *                              
056     *  &lt;ldif-change-record-e&gt; ::= &lt;seps&gt; &lt;dn-spec&gt; &lt;sep&gt; &lt;controls-e&gt; 
057     *    &quot;changetype:&quot; &lt;fill&gt; &lt;changerecord-type&gt; &lt;ldif-change-record-e&gt; | e
058     *                              
059     *  &lt;dn-spec&gt; ::= &quot;dn:&quot; &lt;fill&gt; &lt;safe-string&gt; | &quot;dn::&quot; &lt;fill&gt; &lt;base64-string&gt;
060     *                              
061     *  &lt;controls-e&gt; ::= &quot;control:&quot; &lt;fill&gt; &lt;number&gt; &lt;oid&gt; &lt;spaces-e&gt; &lt;criticality&gt; 
062     *    &lt;value-spec-e&gt; &lt;sep&gt; &lt;controls-e&gt; | e
063     *                              
064     *  &lt;criticality&gt; ::= &quot;true&quot; | &quot;false&quot; | e
065     *                              
066     *  &lt;oid&gt; ::= '.' &lt;number&gt; &lt;oid&gt; | e
067     *                              
068     *  &lt;attrval-specs-e&gt; ::= &lt;number&gt; &lt;oid&gt; &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; 
069     *  &lt;attrval-specs-e&gt; | 
070     *    &lt;alpha&gt; &lt;chars-e&gt; &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; &lt;attrval-specs-e&gt; | e
071     *                              
072     *  &lt;value-spec-e&gt; ::= &lt;value-spec&gt; | e
073     *  
074     *  &lt;value-spec&gt; ::= ':' &lt;fill&gt; &lt;safe-string-e&gt; | 
075     *    &quot;::&quot; &lt;fill&gt; &lt;base64-chars&gt; | 
076     *    &quot;:&lt;&quot; &lt;fill&gt; &lt;url&gt;
077     *  
078     *  &lt;attributeType&gt; ::= &lt;number&gt; &lt;oid&gt; | &lt;alpha&gt; &lt;chars-e&gt;
079     *  
080     *  &lt;options-e&gt; ::= ';' &lt;char&gt; &lt;chars-e&gt; &lt;options-e&gt; |e
081     *                              
082     *  &lt;chars-e&gt; ::= &lt;char&gt; &lt;chars-e&gt; |  e
083     *  
084     *  &lt;changerecord-type&gt; ::= &quot;add&quot; &lt;sep&gt; &lt;attributeType&gt; &lt;options-e&gt; &lt;value-spec&gt; 
085     *  &lt;sep&gt; &lt;attrval-specs-e&gt; | 
086     *    &quot;delete&quot; &lt;sep&gt; | 
087     *    &quot;modify&quot; &lt;sep&gt; &lt;mod-type&gt; &lt;fill&gt; &lt;attributeType&gt; &lt;options-e&gt; &lt;sep&gt; 
088     *    &lt;attrval-specs-e&gt; &lt;sep&gt; '-' &lt;sep&gt; &lt;mod-specs-e&gt; | 
089     *    &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; 
090     *    &lt;newsuperior-e&gt; &lt;sep&gt; |
091     *    &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; 
092     *    &lt;newsuperior-e&gt; &lt;sep&gt;
093     *  
094     *  &lt;newrdn&gt; ::= ':' &lt;fill&gt; &lt;safe-string&gt; | &quot;::&quot; &lt;fill&gt; &lt;base64-chars&gt;
095     *  
096     *  &lt;newsuperior-e&gt; ::= &quot;newsuperior&quot; &lt;newrdn&gt; | e
097     *  
098     *  &lt;mod-specs-e&gt; ::= &lt;mod-type&gt; &lt;fill&gt; &lt;attributeType&gt; &lt;options-e&gt; 
099     *    &lt;sep&gt; &lt;attrval-specs-e&gt; &lt;sep&gt; '-' &lt;sep&gt; &lt;mod-specs-e&gt; | e
100     *  
101     *  &lt;mod-type&gt; ::= &quot;add:&quot; | &quot;delete:&quot; | &quot;replace:&quot;
102     *  
103     *  &lt;url&gt; ::= &lt;a Uniform Resource Locator, as defined in [6]&gt;
104     *  
105     *  
106     *  
107     *  LEXICAL
108     *  -------
109     *  
110     *  &lt;fill&gt;           ::= ' ' &lt;fill&gt; | e
111     *  &lt;char&gt;           ::= &lt;alpha&gt; | &lt;digit&gt; | '-'
112     *  &lt;number&gt;         ::= &lt;digit&gt; &lt;digits&gt;
113     *  &lt;0-1&gt;            ::= '0' | '1'
114     *  &lt;digits&gt;         ::= &lt;digit&gt; &lt;digits&gt; | e
115     *  &lt;digit&gt;          ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
116     *  &lt;seps&gt;           ::= &lt;sep&gt; &lt;seps-e&gt; 
117     *  &lt;seps-e&gt;         ::= &lt;sep&gt; &lt;seps-e&gt; | e
118     *  &lt;sep&gt;            ::= 0x0D 0x0A | 0x0A
119     *  &lt;spaces&gt;         ::= ' ' &lt;spaces-e&gt;
120     *  &lt;spaces-e&gt;       ::= ' ' &lt;spaces-e&gt; | e
121     *  &lt;safe-string-e&gt;  ::= &lt;safe-string&gt; | e
122     *  &lt;safe-string&gt;    ::= &lt;safe-init-char&gt; &lt;safe-chars&gt;
123     *  &lt;safe-init-char&gt; ::= [0x01-0x09] | 0x0B | 0x0C | [0x0E-0x1F] | [0x21-0x39] | 0x3B | [0x3D-0x7F]
124     *  &lt;safe-chars&gt;     ::= &lt;safe-char&gt; &lt;safe-chars&gt; | e
125     *  &lt;safe-char&gt;      ::= [0x01-0x09] | 0x0B | 0x0C | [0x0E-0x7F]
126     *  &lt;base64-string&gt;  ::= &lt;base64-char&gt; &lt;base64-chars&gt;
127     *  &lt;base64-chars&gt;   ::= &lt;base64-char&gt; &lt;base64-chars&gt; | e
128     *  &lt;base64-char&gt;    ::= 0x2B | 0x2F | [0x30-0x39] | 0x3D | [0x41-9x5A] | [0x61-0x7A]
129     *  &lt;alpha&gt;          ::= [0x41-0x5A] | [0x61-0x7A]
130     *  
131     *  COMMENTS
132     *  --------
133     *  - 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
134     *  DIGIT+ (&quot;.&quot; DIGIT+)*
135     *  - The mod-spec lacks a sep between *attrval-spec and &quot;-&quot;.
136     *  - The BASE64-UTF8-STRING should be BASE64-CHAR BASE64-STRING
137     *  - The ValueSpec rule must accept multilines values. In this case, we have a LF followed by a 
138     *  single space before the continued value.
139     * </pre>
140     * 
141     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
142     * @version $Rev$, $Date$
143     */
144    public class LdifAttributesReader extends LdifReader
145    {
146        /** A logger */
147        private static final Logger LOG = LoggerFactory.getLogger( LdifAttributesReader.class );
148    
149        /**
150         * Constructors
151         */
152        public LdifAttributesReader()
153        {
154            lines = new ArrayList<String>();
155            position = new Position();
156            version = DEFAULT_VERSION;
157        }
158    
159    
160        /**
161         * Parse an AttributeType/AttributeValue
162         * 
163         * @param attributes The entry where to store the value
164         * @param line The line to parse
165         * @param lowerLine The same line, lowercased
166         * @throws NamingException If anything goes wrong
167         */
168        private void parseAttribute( Attributes attributes, String line, String lowerLine ) throws NamingException
169        {
170            int colonIndex = line.indexOf( ':' );
171    
172            String attributeType = lowerLine.substring( 0, colonIndex );
173    
174            // We should *not* have a DN twice
175            if ( attributeType.equals( "dn" ) )
176            {
177                LOG.error( I18n.err( I18n.ERR_12002 ) );
178                throw new NamingException( I18n.err( I18n.ERR_12003 ) );
179            }
180    
181            Object attributeValue = parseValue( line, colonIndex );
182    
183            // Update the entry
184            Attribute attribute = attributes.get( attributeType );
185            
186            if ( attribute == null )
187            {
188                attributes.put( attributeType, attributeValue );
189            }
190            else
191            {
192                attribute.add( attributeValue );
193            }
194        }
195    
196        /**
197         * Parse a ldif file. The following rules are processed :
198         * 
199         * &lt;ldif-file&gt; ::= &lt;ldif-attrval-record&gt; &lt;ldif-attrval-records&gt; |
200         * &lt;ldif-change-record&gt; &lt;ldif-change-records&gt; &lt;ldif-attrval-record&gt; ::=
201         * &lt;dn-spec&gt; &lt;sep&gt; &lt;attrval-spec&gt; &lt;attrval-specs&gt; &lt;ldif-change-record&gt; ::=
202         * &lt;dn-spec&gt; &lt;sep&gt; &lt;controls-e&gt; &lt;changerecord&gt; &lt;dn-spec&gt; ::= "dn:" &lt;fill&gt;
203         * &lt;distinguishedName&gt; | "dn::" &lt;fill&gt; &lt;base64-distinguishedName&gt;
204         * &lt;changerecord&gt; ::= "changetype:" &lt;fill&gt; &lt;change-op&gt;
205         * 
206         * @return The read entry
207         * @throws NamingException If the entry can't be read or is invalid
208         */
209        private Attributes parseAttributes() throws NamingException
210        {
211            if ( ( lines == null ) || ( lines.size() == 0 ) )
212            {
213                LOG.debug( "The entry is empty : end of ldif file" );
214                return null;
215            }
216    
217            Attributes attributes = new BasicAttributes( true );
218    
219            // Now, let's iterate through the other lines
220            for ( String line:lines )
221            {
222                // Each line could start either with an OID, an attribute type, with
223                // "control:" or with "changetype:"
224                String lowerLine = line.toLowerCase();
225    
226                // We have three cases :
227                // 1) The first line after the DN is a "control:" -> this is an error
228                // 2) The first line after the DN is a "changeType:" -> this is an error
229                // 3) The first line after the DN is anything else
230                if ( lowerLine.startsWith( "control:" ) )
231                {
232                    LOG.error( I18n.err( I18n.ERR_12004 ) );
233                    throw new NamingException( I18n.err( I18n.ERR_12005 ) );
234                }
235                else if ( lowerLine.startsWith( "changetype:" ) )
236                {
237                    LOG.error( I18n.err( I18n.ERR_12004 ) );
238                    throw new NamingException( I18n.err( I18n.ERR_12005 ) );
239                }
240                else if ( line.indexOf( ':' ) > 0 )
241                {
242                    parseAttribute( attributes, line, lowerLine );
243                }
244                else
245                {
246                    // Invalid attribute Value
247                    LOG.error( I18n.err( I18n.ERR_12006 ) );
248                    throw new NamingException( I18n.err( I18n.ERR_12007 ) );
249                }
250            }
251    
252            LOG.debug( "Read an attributes : {}", attributes );
253            
254            return attributes;
255        }
256    
257    
258        /**
259         * A method which parses a ldif string and returns a list of entries.
260         * 
261         * @param ldif The ldif string
262         * @return A list of entries, or an empty List
263         * @throws NamingException
264         *             If something went wrong
265         */
266        public Attributes parseAttributes( String ldif ) throws NamingException
267        {
268            lines = new ArrayList<String>();
269            position = new Position();
270    
271            LOG.debug( "Starts parsing ldif buffer" );
272    
273            if ( StringTools.isEmpty( ldif ) )
274            {
275                return new BasicAttributes( true );
276            }
277    
278            StringReader strIn = new StringReader( ldif );
279            reader = new BufferedReader( strIn );
280    
281            try
282            {
283                readLines();
284                
285                Attributes attributes = parseAttributes();
286    
287                if ( LOG.isDebugEnabled() )
288                {
289                    LOG.debug( "Parsed {} entries.", ( attributes == null ? 0 : 1 ) );
290                }
291    
292                return attributes;
293            }
294            catch (NamingException ne)
295            {
296                LOG.error( I18n.err( I18n.ERR_12008, ne.getLocalizedMessage() ) );
297                throw new NamingException( I18n.err( I18n.ERR_12009 ) );
298            }
299            finally
300            {
301                try
302                {
303                    reader.close();
304                }
305                catch ( IOException ioe )
306                {
307                    // Do nothing
308                }
309            }
310        }
311    }