Return-Path: Delivered-To: apmail-directory-commits-archive@www.apache.org Received: (qmail 14162 invoked from network); 26 Jan 2010 22:44:00 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (140.211.11.3) by minotaur.apache.org with SMTP; 26 Jan 2010 22:44:00 -0000 Received: (qmail 19287 invoked by uid 500); 26 Jan 2010 22:43:59 -0000 Delivered-To: apmail-directory-commits-archive@directory.apache.org Received: (qmail 19221 invoked by uid 500); 26 Jan 2010 22:43:59 -0000 Mailing-List: contact commits-help@directory.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@directory.apache.org Delivered-To: mailing list commits@directory.apache.org Received: (qmail 19189 invoked by uid 99); 26 Jan 2010 22:43:59 -0000 Received: from nike.apache.org (HELO nike.apache.org) (192.87.106.230) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 26 Jan 2010 22:43:59 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=10.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Tue, 26 Jan 2010 22:43:52 +0000 Received: by eris.apache.org (Postfix, from userid 65534) id D085F23889C5; Tue, 26 Jan 2010 22:43:29 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Subject: svn commit: r903465 [2/5] - in /directory/shared/trunk/ldap-ldif: ./ src/ src/main/ src/main/java/ src/main/java/org/ src/main/java/org/apache/ src/main/java/org/apache/directory/ src/main/java/org/apache/directory/shared/ src/main/java/org/apache/dire... Date: Tue, 26 Jan 2010 22:43:29 -0000 To: commits@directory.apache.org From: elecharny@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20100126224329.D085F23889C5@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org Added: directory/shared/trunk/ldap-ldif/src/main/java/org/apache/directory/shared/ldap/ldif/LdifReader.java URL: http://svn.apache.org/viewvc/directory/shared/trunk/ldap-ldif/src/main/java/org/apache/directory/shared/ldap/ldif/LdifReader.java?rev=903465&view=auto ============================================================================== --- directory/shared/trunk/ldap-ldif/src/main/java/org/apache/directory/shared/ldap/ldif/LdifReader.java (added) +++ directory/shared/trunk/ldap-ldif/src/main/java/org/apache/directory/shared/ldap/ldif/LdifReader.java Tue Jan 26 22:43:27 2010 @@ -0,0 +1,1888 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.directory.shared.ldap.ldif; + + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import javax.naming.InvalidNameException; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.BasicAttribute; + +import org.apache.directory.shared.asn1.codec.DecoderException; +import org.apache.directory.shared.asn1.primitives.OID; +import org.apache.directory.shared.ldap.entry.EntryAttribute; +import org.apache.directory.shared.ldap.entry.ModificationOperation; +import org.apache.directory.shared.ldap.entry.client.DefaultClientAttribute; +import org.apache.directory.shared.ldap.message.control.Control; +import org.apache.directory.shared.ldap.name.LdapDN; +import org.apache.directory.shared.ldap.name.LdapDnParser; +import org.apache.directory.shared.ldap.name.RDN; +import org.apache.directory.shared.ldap.util.Base64; +import org.apache.directory.shared.ldap.util.StringTools; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + *
+ *  <ldif-file> ::= "version:" <fill> <number> <seps> <dn-spec> <sep> 
+ *  <ldif-content-change>
+ *  
+ *  <ldif-content-change> ::= 
+ *    <number> <oid> <options-e> <value-spec> <sep> 
+ *    <attrval-specs-e> <ldif-attrval-record-e> | 
+ *    <alpha> <chars-e> <options-e> <value-spec> <sep> 
+ *    <attrval-specs-e> <ldif-attrval-record-e> | 
+ *    "control:" <fill> <number> <oid> <spaces-e> 
+ *    <criticality> <value-spec-e> <sep> <controls-e> 
+ *        "changetype:" <fill> <changerecord-type> <ldif-change-record-e> |
+ *    "changetype:" <fill> <changerecord-type> <ldif-change-record-e>
+ *                              
+ *  <ldif-attrval-record-e> ::= <seps> <dn-spec> <sep> <attributeType> 
+ *    <options-e> <value-spec> <sep> <attrval-specs-e> 
+ *    <ldif-attrval-record-e> | e
+ *                              
+ *  <ldif-change-record-e> ::= <seps> <dn-spec> <sep> <controls-e> 
+ *    "changetype:" <fill> <changerecord-type> <ldif-change-record-e> | e
+ *                              
+ *  <dn-spec> ::= "dn:" <fill> <safe-string> | "dn::" <fill> <base64-string>
+ *                              
+ *  <controls-e> ::= "control:" <fill> <number> <oid> <spaces-e> <criticality> 
+ *    <value-spec-e> <sep> <controls-e> | e
+ *                              
+ *  <criticality> ::= "true" | "false" | e
+ *                              
+ *  <oid> ::= '.' <number> <oid> | e
+ *                              
+ *  <attrval-specs-e> ::= <number> <oid> <options-e> <value-spec> 
+ *  <sep> <attrval-specs-e> | 
+ *    <alpha> <chars-e> <options-e> <value-spec> <sep> <attrval-specs-e> | e
+ *                              
+ *  <value-spec-e> ::= <value-spec> | e
+ *  
+ *  <value-spec> ::= ':' <fill> <safe-string-e> | 
+ *    "::" <fill> <base64-chars> | 
+ *    ":<" <fill> <url>
+ *  
+ *  <attributeType> ::= <number> <oid> | <alpha> <chars-e>
+ *  
+ *  <options-e> ::= ';' <char> <chars-e> <options-e> |e
+ *                              
+ *  <chars-e> ::= <char> <chars-e> |  e
+ *  
+ *  <changerecord-type> ::= "add" <sep> <attributeType> 
+ *  <options-e> <value-spec> <sep> <attrval-specs-e> | 
+ *    "delete" <sep> | 
+ *    "modify" <sep> <mod-type> <fill> <attributeType> 
+ *    <options-e> <sep> <attrval-specs-e> <sep> '-' <sep> <mod-specs-e> | 
+ *    "moddn" <sep> <newrdn> <sep> "deleteoldrdn:" 
+ *    <fill> <0-1> <sep> <newsuperior-e> <sep> |
+ *    "modrdn" <sep> <newrdn> <sep> "deleteoldrdn:" 
+ *    <fill> <0-1> <sep> <newsuperior-e> <sep>
+ *  
+ *  <newrdn> ::= ':' <fill> <safe-string> | "::" <fill> <base64-chars>
+ *  
+ *  <newsuperior-e> ::= "newsuperior" <newrdn> | e
+ *  
+ *  <mod-specs-e> ::= <mod-type> <fill> <attributeType> <options-e> 
+ *    <sep> <attrval-specs-e> <sep> '-' <sep> <mod-specs-e> | e
+ *  
+ *  <mod-type> ::= "add:" | "delete:" | "replace:"
+ *  
+ *  <url> ::= <a Uniform Resource Locator, as defined in [6]>
+ *  
+ *  
+ *  
+ *  LEXICAL
+ *  -------
+ *  
+ *  <fill>           ::= ' ' <fill> | e
+ *  <char>           ::= <alpha> | <digit> | '-'
+ *  <number>         ::= <digit> <digits>
+ *  <0-1>            ::= '0' | '1'
+ *  <digits>         ::= <digit> <digits> | e
+ *  <digit>          ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
+ *  <seps>           ::= <sep> <seps-e> 
+ *  <seps-e>         ::= <sep> <seps-e> | e
+ *  <sep>            ::= 0x0D 0x0A | 0x0A
+ *  <spaces>         ::= ' ' <spaces-e>
+ *  <spaces-e>       ::= ' ' <spaces-e> | e
+ *  <safe-string-e>  ::= <safe-string> | e
+ *  <safe-string>    ::= <safe-init-char> <safe-chars>
+ *  <safe-init-char> ::= [0x01-0x09] | 0x0B | 0x0C | [0x0E-0x1F] | [0x21-0x39] | 0x3B | [0x3D-0x7F]
+ *  <safe-chars>     ::= <safe-char> <safe-chars> | e
+ *  <safe-char>      ::= [0x01-0x09] | 0x0B | 0x0C | [0x0E-0x7F]
+ *  <base64-string>  ::= <base64-char> <base64-chars>
+ *  <base64-chars>   ::= <base64-char> <base64-chars> | e
+ *  <base64-char>    ::= 0x2B | 0x2F | [0x30-0x39] | 0x3D | [0x41-9x5A] | [0x61-0x7A]
+ *  <alpha>          ::= [0x41-0x5A] | [0x61-0x7A]
+ *  
+ *  COMMENTS
+ *  --------
+ *  - The ldap-oid VN is not correct in the RFC-2849. It has been changed from 1*DIGIT 0*1("." 1*DIGIT) to
+ *  DIGIT+ ("." DIGIT+)*
+ *  - The mod-spec lacks a sep between *attrval-spec and "-".
+ *  - The BASE64-UTF8-STRING should be BASE64-CHAR BASE64-STRING
+ *  - The ValueSpec rule must accept multilines values. In this case, we have a LF followed by a 
+ *  single space before the continued value.
+ * 
+ * + * @author Apache Directory Project + * @version $Rev$, $Date$ + */ +public class LdifReader implements Iterable, Closeable +{ + /** A logger */ + private static final Logger LOG = LoggerFactory.getLogger( LdifReader.class ); + + /** + * A private class to track the current position in a line + * @author Apache Directory Project + * @version $Rev$, $Date$ + */ + public class Position + { + /** The current position */ + private int pos; + + + /** + * Creates a new instance of Position. + */ + public Position() + { + pos = 0; + } + + + /** + * Increment the current position by one + * + */ + public void inc() + { + pos++; + } + + + /** + * Increment the current position by the given value + * + * @param val The value to add to the current position + */ + public void inc( int val ) + { + pos += val; + } + } + + /** A list of read lines */ + protected List lines; + + /** The current position */ + protected Position position; + + /** The ldif file version default value */ + protected static final int DEFAULT_VERSION = 1; + + /** The ldif version */ + protected int version; + + /** Type of element read */ + protected static final int LDIF_ENTRY = 0; + + protected static final int CHANGE = 1; + + protected static final int UNKNOWN = 2; + + /** Size limit for file contained values */ + protected long sizeLimit = SIZE_LIMIT_DEFAULT; + + /** The default size limit : 1Mo */ + protected static final long SIZE_LIMIT_DEFAULT = 1024000; + + /** State values for the modify operation */ + protected static final int MOD_SPEC = 0; + + protected static final int ATTRVAL_SPEC = 1; + + protected static final int ATTRVAL_SPEC_OR_SEP = 2; + + /** Iterator prefetched entry */ + protected LdifEntry prefetched; + + /** The ldif Reader */ + protected Reader reader; + + /** A flag set if the ldif contains entries */ + protected boolean containsEntries; + + /** A flag set if the ldif contains changes */ + protected boolean containsChanges; + + /** + * An Exception to handle error message, has Iterator.next() can't throw + * exceptions + */ + protected Exception error; + + + /** + * Constructors + */ + public LdifReader() + { + lines = new ArrayList(); + position = new Position(); + version = DEFAULT_VERSION; + } + + + private void init( BufferedReader reader ) throws NamingException + { + this.reader = reader; + lines = new ArrayList(); + position = new Position(); + version = DEFAULT_VERSION; + containsChanges = false; + containsEntries = false; + + // First get the version - if any - + version = parseVersion(); + prefetched = parseEntry(); + } + + + /** + * A constructor which takes a file name + * + * @param ldifFileName A file name containing ldif formated input + * @throws NamingException + * If the file cannot be processed or if the format is incorrect + */ + public LdifReader( String ldifFileName ) throws NamingException + { + File file = new File( ldifFileName ); + + if ( !file.exists() ) + { + LOG.error( "File {} cannot be found", file.getAbsoluteFile() ); + throw new NamingException( "Cannot find file " + file.getAbsoluteFile() ); + } + + if ( !file.canRead() ) + { + LOG.error( "File {} cannot be read", file.getName() ); + throw new NamingException( "Cannot read file " + file.getName() ); + } + + try + { + init( new BufferedReader( new FileReader( file ) ) ); + } + catch ( FileNotFoundException fnfe ) + { + LOG.error( "File {} cannot be found", file.getAbsoluteFile() ); + throw new NamingException( "Cannot find file " + file.getAbsoluteFile() ); + } + } + + + /** + * A constructor which takes a Reader + * + * @param in + * A Reader containing ldif formated input + * @throws NamingException + * If the file cannot be processed or if the format is incorrect + */ + public LdifReader( Reader in ) throws NamingException + { + init( new BufferedReader( in ) ); + } + + + /** + * A constructor which takes an InputStream + * + * @param in + * An InputStream containing ldif formated input + * @throws NamingException + * If the file cannot be processed or if the format is incorrect + */ + public LdifReader( InputStream in ) throws NamingException + { + init( new BufferedReader( new InputStreamReader( in ) ) ); + } + + + /** + * A constructor which takes a File + * + * @param in + * A File containing ldif formated input + * @throws NamingException + * If the file cannot be processed or if the format is incorrect + */ + public LdifReader( File file ) throws NamingException + { + if ( !file.exists() ) + { + LOG.error( "File {} cannot be found", file.getAbsoluteFile() ); + throw new NamingException( "Cannot find file " + file.getAbsoluteFile() ); + } + + if ( !file.canRead() ) + { + LOG.error( "File {} cannot be read", file.getName() ); + throw new NamingException( "Cannot read file " + file.getName() ); + } + + try + { + init( new BufferedReader( new FileReader( file ) ) ); + } + catch ( FileNotFoundException fnfe ) + { + LOG.error( "File {} cannot be found", file.getAbsoluteFile() ); + throw new NamingException( "Cannot find file " + file.getAbsoluteFile() ); + } + } + + + /** + * @return The ldif file version + */ + public int getVersion() + { + return version; + } + + + /** + * @return The maximum size of a file which is used into an attribute value. + */ + public long getSizeLimit() + { + return sizeLimit; + } + + + /** + * Set the maximum file size that can be accepted for an attribute value + * + * @param sizeLimit + * The size in bytes + */ + public void setSizeLimit( long sizeLimit ) + { + this.sizeLimit = sizeLimit; + } + + + // ::= ' ' | � + private static void parseFill( char[] document, Position position ) + { + + while ( StringTools.isCharASCII( document, position.pos, ' ' ) ) + { + position.inc(); + } + } + + + /** + * Parse a number following the rules : + * + * <number> ::= <digit> <digits> <digits> ::= <digit> <digits> | e <digit> + * ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' + * + * Check that the number is in the interval + * + * @param document The document containing the number to parse + * @param position The current position in the document + * @return a String representing the parsed number + */ + private static String parseNumber( char[] document, Position position ) + { + int initPos = position.pos; + + while ( true ) + { + if ( StringTools.isDigit( document, position.pos ) ) + { + position.inc(); + } + else + { + break; + } + } + + if ( position.pos == initPos ) + { + return null; + } + else + { + return new String( document, initPos, position.pos - initPos ); + } + } + + + /** + * Parse the changeType + * + * @param line + * The line which contains the changeType + * @return The operation. + */ + private ChangeType parseChangeType( String line ) + { + ChangeType operation = ChangeType.Add; + + String modOp = StringTools.trim( line.substring( "changetype:".length() + 1 ) ); + + if ( "add".equalsIgnoreCase( modOp ) ) + { + operation = ChangeType.Add; + } + else if ( "delete".equalsIgnoreCase( modOp ) ) + { + operation = ChangeType.Delete; + } + else if ( "modify".equalsIgnoreCase( modOp ) ) + { + operation = ChangeType.Modify; + } + else if ( "moddn".equalsIgnoreCase( modOp ) ) + { + operation = ChangeType.ModDn; + } + else if ( "modrdn".equalsIgnoreCase( modOp ) ) + { + operation = ChangeType.ModRdn; + } + + return operation; + } + + + /** + * Parse the DN of an entry + * + * @param line + * The line to parse + * @return A DN + * @throws NamingException + * If the DN is invalid + */ + private String parseDn( String line ) throws NamingException + { + String dn = null; + + String lowerLine = line.toLowerCase(); + + if ( lowerLine.startsWith( "dn:" ) || lowerLine.startsWith( "DN:" ) ) + { + // Ok, we have a DN. Is it base 64 encoded ? + int length = line.length(); + + if ( length == 3 ) + { + // The DN is empty : error + LOG.error( "A ldif entry must have a non empty DN" ); + throw new NamingException( "No DN for entry" ); + } + else if ( line.charAt( 3 ) == ':' ) + { + if ( length > 4 ) + { + // This is a base 64 encoded DN. + String trimmedLine = line.substring( 4 ).trim(); + + try + { + dn = new String( Base64.decode( trimmedLine.toCharArray() ), "UTF-8" ); + } + catch ( UnsupportedEncodingException uee ) + { + // The DN is not base 64 encoded + LOG.error( "The ldif entry is supposed to have a base 64 encoded DN" ); + throw new NamingException( "Invalid base 64 encoded DN" ); + } + } + else + { + // The DN is empty : error + LOG.error( "A ldif entry must have a non empty DN" ); + throw new NamingException( "No DN for entry" ); + } + } + else + { + dn = line.substring( 3 ).trim(); + } + } + else + { + LOG.error( "A ldif entry must start with a DN" ); + throw new NamingException( "No DN for entry" ); + } + + // Check that the DN is valid. If not, an exception will be thrown + try + { + LdapDnParser.parseInternal( dn, new ArrayList() ); + } + catch ( InvalidNameException ine ) + { + LOG.error( "The DN {} is not valid" ); + throw ine; + } + + return dn; + } + + + /** + * Parse the value part. + * + * @param line + * The line which contains the value + * @param pos + * The starting position in the line + * @return A String or a byte[], depending of the kind of value we get + */ + protected static Object parseSimpleValue( String line, int pos ) + { + if ( line.length() > pos + 1 ) + { + char c = line.charAt( pos + 1 ); + + if ( c == ':' ) + { + String value = StringTools.trim( line.substring( pos + 2 ) ); + + return Base64.decode( value.toCharArray() ); + } + else + { + return StringTools.trim( line.substring( pos + 1 ) ); + } + } + else + { + return null; + } + } + + + /** + * Parse the value part. + * + * @param line + * The line which contains the value + * @param pos + * The starting position in the line + * @return A String or a byte[], depending of the kind of value we get + * @throws NamingException + * If something went wrong + */ + protected Object parseValue( String line, int pos ) throws NamingException + { + if ( line.length() > pos + 1 ) + { + char c = line.charAt( pos + 1 ); + + if ( c == ':' ) + { + String value = StringTools.trim( line.substring( pos + 2 ) ); + + return Base64.decode( value.toCharArray() ); + } + else if ( c == '<' ) + { + String urlName = StringTools.trim( line.substring( pos + 2 ) ); + + try + { + URL url = new URL( urlName ); + + if ( "file".equals( url.getProtocol() ) ) + { + String fileName = url.getFile(); + + File file = new File( fileName ); + + if ( !file.exists() ) + { + LOG.error( "File {} not found", fileName ); + throw new NamingException( "Bad URL, file not found" ); + } + else + { + long length = file.length(); + + if ( length > sizeLimit ) + { + LOG.error( "File {} is too big", fileName ); + throw new NamingException( "File too big" ); + } + else + { + byte[] data = new byte[( int ) length]; + DataInputStream inf = null; + + try + { + inf = new DataInputStream( new FileInputStream( file ) ); + inf.read( data ); + + return data; + } + catch ( FileNotFoundException fnfe ) + { + // We can't reach this point, the file + // existence has already been + // checked + LOG.error( "File {} not found", fileName ); + throw new NamingException( "Bad URL, file not found" ); + } + catch ( IOException ioe ) + { + LOG.error( "File {} error reading", fileName ); + throw new NamingException( "Bad URL, file can't be read" ); + } + finally + { + try + { + inf.close(); + } + catch ( IOException ioe ) + { + LOG.error( "Error while closing the stream : {}", ioe.getMessage() ); + // Just do nothing ... + } + } + } + } + } + else + { + LOG.error( "Protocols other than file: are not supported" ); + throw new NamingException( "Unsupported URL protocol" ); + } + } + catch ( MalformedURLException mue ) + { + LOG.error( "Bad URL {}", urlName ); + throw new NamingException( "Bad URL" ); + } + } + else + { + return StringTools.trim( line.substring( pos + 1 ) ); + } + } + else + { + return null; + } + } + + + /** + * Parse a control. The grammar is : <control> ::= "control:" <fill> + * <ldap-oid> <critical-e> <value-spec-e> <sep> <critical-e> ::= <spaces> + * <boolean> | e <boolean> ::= "true" | "false" <value-spec-e> ::= + * <value-spec> | e <value-spec> ::= ":" <fill> <SAFE-STRING-e> | "::" + * <fill> <BASE64-STRING> | ":<" <fill> <url> + * + * It can be read as : "control:" <fill> <ldap-oid> [ " "+ ( "true" | + * "false") ] [ ":" <fill> <SAFE-STRING-e> | "::" <fill> <BASE64-STRING> | ":<" + * <fill> <url> ] + * + * @param line The line containing the control + * @return A control + * @exception NamingException If the control has no OID or if the OID is incorrect, + * of if the criticality is not set when it's mandatory. + */ + private Control parseControl( String line ) throws NamingException + { + String lowerLine = line.toLowerCase().trim(); + char[] controlValue = line.trim().toCharArray(); + int pos = 0; + int length = controlValue.length; + + // Get the + if ( pos > length ) + { + // No OID : error ! + LOG.error( "The control does not have an OID" ); + throw new NamingException( "Bad control, no oid" ); + } + + int initPos = pos; + + while ( StringTools.isCharASCII( controlValue, pos, '.' ) || StringTools.isDigit( controlValue, pos ) ) + { + pos++; + } + + if ( pos == initPos ) + { + // Not a valid OID ! + LOG.error( "The control does not have an OID" ); + throw new NamingException( "Bad control, no oid" ); + } + + // Create and check the OID + String oidString = lowerLine.substring( 0, pos ); + + OID oid = null; + + try + { + oid = new OID( oidString ); + } + catch ( DecoderException de ) + { + LOG.error( "The OID {} is not valid", oidString ); + throw new NamingException( "Bad control oid" ); + } + + LdifControl control = new LdifControl( oidString ); + + // Get the criticality, if any + // Skip the + while ( StringTools.isCharASCII( controlValue, pos, ' ' ) ) + { + pos++; + } + + // Check if we have a "true" or a "false" + int criticalPos = lowerLine.indexOf( ':' ); + + int criticalLength = 0; + + if ( criticalPos == -1 ) + { + criticalLength = length - pos; + } + else + { + criticalLength = criticalPos - pos; + } + + if ( ( criticalLength == 4 ) && ( "true".equalsIgnoreCase( lowerLine.substring( pos, pos + 4 ) ) ) ) + { + control.setCritical( true ); + } + else if ( ( criticalLength == 5 ) && ( "false".equalsIgnoreCase( lowerLine.substring( pos, pos + 5 ) ) ) ) + { + control.setCritical( false ); + } + else if ( criticalLength != 0 ) + { + // If we have a criticality, it should be either "true" or "false", + // nothing else + LOG.error( "The control muts have a valid criticality" ); + throw new NamingException( "Bad control criticality" ); + } + + if ( criticalPos > 0 ) + { + // We have a value. It can be a normal value, a base64 encoded value + // or a file contained value + if ( StringTools.isCharASCII( controlValue, criticalPos + 1, ':' ) ) + { + // Base 64 encoded value + byte[] value = Base64.decode( line.substring( criticalPos + 2 ).toCharArray() ); + control.setValue( value ); + } + else if ( StringTools.isCharASCII( controlValue, criticalPos + 1, '<' ) ) + { + // File contained value + } + else + { + // Standard value + byte[] value = new byte[length - criticalPos - 1]; + + for ( int i = 0; i < length - criticalPos - 1; i++ ) + { + value[i] = ( byte ) controlValue[i + criticalPos + 1]; + } + + control.setValue( value ); + } + } + + return control; + } + + + /** + * Parse an AttributeType/AttributeValue + * + * @param line The line to parse + * @return the parsed Attribute + */ + public static Attribute parseAttributeValue( String line ) + { + int colonIndex = line.indexOf( ':' ); + + if ( colonIndex != -1 ) + { + String attributeType = line.toLowerCase().substring( 0, colonIndex ); + Object attributeValue = parseSimpleValue( line, colonIndex ); + + // Create an attribute + return new BasicAttribute( attributeType, attributeValue ); + } + else + { + return null; + } + } + + + /** + * Parse an AttributeType/AttributeValue + * + * @param entry The entry where to store the value + * @param line The line to parse + * @param lowerLine The same line, lowercased + * @throws NamingException If anything goes wrong + */ + public void parseAttributeValue( LdifEntry entry, String line, String lowerLine ) throws NamingException + { + int colonIndex = line.indexOf( ':' ); + + String attributeType = lowerLine.substring( 0, colonIndex ); + + // We should *not* have a DN twice + if ( attributeType.equals( "dn" ) ) + { + LOG.error( "An entry must not have two DNs" ); + throw new NamingException( "A ldif entry should not have two DN" ); + } + + Object attributeValue = parseValue( line, colonIndex ); + + // Update the entry + entry.addAttribute( attributeType, attributeValue ); + } + + + /** + * Parse a ModRDN operation + * + * @param entry + * The entry to update + * @param iter + * The lines iterator + * @throws NamingException + * If anything goes wrong + */ + private void parseModRdn( LdifEntry entry, Iterator iter ) throws NamingException + { + // We must have two lines : one starting with "newrdn:" or "newrdn::", + // and the second starting with "deleteoldrdn:" + if ( iter.hasNext() ) + { + String line = iter.next(); + String lowerLine = line.toLowerCase(); + + if ( lowerLine.startsWith( "newrdn::" ) || lowerLine.startsWith( "newrdn:" ) ) + { + int colonIndex = line.indexOf( ':' ); + Object attributeValue = parseValue( line, colonIndex ); + entry.setNewRdn( attributeValue instanceof String ? ( String ) attributeValue : StringTools + .utf8ToString( ( byte[] ) attributeValue ) ); + } + else + { + LOG.error( "A modrdn operation must start with a \"newrdn:\"" ); + throw new NamingException( "Bad modrdn operation" ); + } + + } + else + { + LOG.error( "A modrdn operation must start with a \"newrdn:\"" ); + throw new NamingException( "Bad modrdn operation, no newrdn" ); + } + + if ( iter.hasNext() ) + { + String line = iter.next(); + String lowerLine = line.toLowerCase(); + + if ( lowerLine.startsWith( "deleteoldrdn:" ) ) + { + int colonIndex = line.indexOf( ':' ); + Object attributeValue = parseValue( line, colonIndex ); + entry.setDeleteOldRdn( "1".equals( attributeValue ) ); + } + else + { + LOG.error( "A modrdn operation must contains a \"deleteoldrdn:\"" ); + throw new NamingException( "Bad modrdn operation, no deleteoldrdn" ); + } + } + else + { + LOG.error( "A modrdn operation must contains a \"deleteoldrdn:\"" ); + throw new NamingException( "Bad modrdn operation, no deleteoldrdn" ); + } + + return; + } + + + /** + * Parse a modify change type. + * + * The grammar is : <changerecord> ::= "changetype:" FILL "modify" SEP + * <mod-spec> <mod-specs-e> <mod-spec> ::= "add:" <mod-val> | "delete:" + * <mod-val-del> | "replace:" <mod-val> <mod-specs-e> ::= <mod-spec> + * <mod-specs-e> | e <mod-val> ::= FILL ATTRIBUTE-DESCRIPTION SEP + * ATTRVAL-SPEC <attrval-specs-e> "-" SEP <mod-val-del> ::= FILL + * ATTRIBUTE-DESCRIPTION SEP <attrval-specs-e> "-" SEP <attrval-specs-e> ::= + * ATTRVAL-SPEC <attrval-specs> | e * + * + * @param entry The entry to feed + * @param iter The lines + * @exception NamingException If the modify operation is invalid + */ + private void parseModify( LdifEntry entry, Iterator iter ) throws NamingException + { + int state = MOD_SPEC; + String modified = null; + ModificationOperation modificationType = ModificationOperation.ADD_ATTRIBUTE; + EntryAttribute attribute = null; + + // The following flag is used to deal with empty modifications + boolean isEmptyValue = true; + + while ( iter.hasNext() ) + { + String line = iter.next(); + String lowerLine = line.toLowerCase(); + + if ( lowerLine.startsWith( "-" ) ) + { + if ( state != ATTRVAL_SPEC_OR_SEP ) + { + LOG.error( "Bad state : we should have come from an ATTRVAL_SPEC" ); + throw new NamingException( "Bad modify separator" ); + } + else + { + if ( isEmptyValue ) + { + // Update the entry + entry.addModificationItem( modificationType, modified, null ); + } + else + { + // Update the entry with the attribute + entry.addModificationItem( modificationType, attribute ); + } + + state = MOD_SPEC; + isEmptyValue = true; + continue; + } + } + else if ( lowerLine.startsWith( "add:" ) ) + { + if ( ( state != MOD_SPEC ) && ( state != ATTRVAL_SPEC ) ) + { + LOG.error( "Bad state : we should have come from a MOD_SPEC or an ATTRVAL_SPEC" ); + throw new NamingException( "Bad modify state" ); + } + + modified = StringTools.trim( line.substring( "add:".length() ) ); + modificationType = ModificationOperation.ADD_ATTRIBUTE; + attribute = new DefaultClientAttribute( modified ); + + state = ATTRVAL_SPEC; + } + else if ( lowerLine.startsWith( "delete:" ) ) + { + if ( ( state != MOD_SPEC ) && ( state != ATTRVAL_SPEC ) ) + { + LOG.error( "Bad state : we should have come from a MOD_SPEC or an ATTRVAL_SPEC" ); + throw new NamingException( "Bad modify state" ); + } + + modified = StringTools.trim( line.substring( "delete:".length() ) ); + modificationType = ModificationOperation.REMOVE_ATTRIBUTE; + attribute = new DefaultClientAttribute( modified ); + + state = ATTRVAL_SPEC_OR_SEP; + } + else if ( lowerLine.startsWith( "replace:" ) ) + { + if ( ( state != MOD_SPEC ) && ( state != ATTRVAL_SPEC ) ) + { + LOG.error( "Bad state : we should have come from a MOD_SPEC or an ATTRVAL_SPEC" ); + throw new NamingException( "Bad modify state" ); + } + + modified = StringTools.trim( line.substring( "replace:".length() ) ); + modificationType = ModificationOperation.REPLACE_ATTRIBUTE; + attribute = new DefaultClientAttribute( modified ); + + state = ATTRVAL_SPEC_OR_SEP; + } + else + { + if ( ( state != ATTRVAL_SPEC ) && ( state != ATTRVAL_SPEC_OR_SEP ) ) + { + LOG.error( "Bad state : we should have come from an ATTRVAL_SPEC" ); + throw new NamingException( "Bad modify state" ); + } + + // A standard AttributeType/AttributeValue pair + int colonIndex = line.indexOf( ':' ); + + String attributeType = line.substring( 0, colonIndex ); + + if ( !attributeType.equalsIgnoreCase( modified ) ) + { + LOG.error( "The modified attribute and the attribute value spec must be equal" ); + throw new NamingException( "Bad modify attribute" ); + } + + // We should *not* have a DN twice + if ( attributeType.equalsIgnoreCase( "dn" ) ) + { + LOG.error( "An entry must not have two DNs" ); + throw new NamingException( "A ldif entry should not have two DN" ); + } + + Object attributeValue = parseValue( line, colonIndex ); + + if ( attributeValue instanceof String ) + { + attribute.add( ( String ) attributeValue ); + } + else + { + attribute.add( ( byte[] ) attributeValue ); + } + + isEmptyValue = false; + + state = ATTRVAL_SPEC_OR_SEP; + } + } + } + + + /** + * Parse a change operation. We have to handle different cases depending on + * the operation. 1) Delete : there should *not* be any line after the + * "changetype: delete" 2) Add : we must have a list of AttributeType : + * AttributeValue elements 3) ModDN : we must have two following lines: a + * "newrdn:" and a "deleteoldrdn:" 4) ModRDN : the very same, but a + * "newsuperior:" line is expected 5) Modify : + * + * The grammar is : <changerecord> ::= "changetype:" FILL "add" SEP + * <attrval-spec> <attrval-specs-e> | "changetype:" FILL "delete" | + * "changetype:" FILL "modrdn" SEP <newrdn> SEP <deleteoldrdn> SEP | // To + * be checked "changetype:" FILL "moddn" SEP <newrdn> SEP <deleteoldrdn> SEP + * <newsuperior> SEP | "changetype:" FILL "modify" SEP <mod-spec> + * <mod-specs-e> <newrdn> ::= "newrdn:" FILL RDN | "newrdn::" FILL + * BASE64-RDN <deleteoldrdn> ::= "deleteoldrdn:" FILL "0" | "deleteoldrdn:" + * FILL "1" <newsuperior> ::= "newsuperior:" FILL DN | "newsuperior::" FILL + * BASE64-DN <mod-specs-e> ::= <mod-spec> <mod-specs-e> | e <mod-spec> ::= + * "add:" <mod-val> | "delete:" <mod-val> | "replace:" <mod-val> <mod-val> + * ::= FILL ATTRIBUTE-DESCRIPTION SEP ATTRVAL-SPEC <attrval-specs-e> "-" SEP + * <attrval-specs-e> ::= ATTRVAL-SPEC <attrval-specs> | e + * + * @param entry The entry to feed + * @param iter The lines iterator + * @param operation The change operation (add, modify, delete, moddn or modrdn) + * @exception NamingException If the change operation is invalid + */ + private void parseChange( LdifEntry entry, Iterator iter, ChangeType operation ) throws NamingException + { + // The changetype and operation has already been parsed. + entry.setChangeType( operation ); + + switch ( operation.getChangeType() ) + { + case ChangeType.DELETE_ORDINAL: + // The change type will tell that it's a delete operation, + // the dn is used as a key. + return; + + case ChangeType.ADD_ORDINAL: + // We will iterate through all attribute/value pairs + while ( iter.hasNext() ) + { + String line = iter.next(); + String lowerLine = line.toLowerCase(); + parseAttributeValue( entry, line, lowerLine ); + } + + return; + + case ChangeType.MODIFY_ORDINAL: + parseModify( entry, iter ); + return; + + case ChangeType.MODRDN_ORDINAL:// They are supposed to have the same syntax ??? + case ChangeType.MODDN_ORDINAL: + // First, parse the modrdn part + parseModRdn( entry, iter ); + + // The next line should be the new superior + if ( iter.hasNext() ) + { + String line = iter.next(); + String lowerLine = line.toLowerCase(); + + if ( lowerLine.startsWith( "newsuperior:" ) ) + { + int colonIndex = line.indexOf( ':' ); + Object attributeValue = parseValue( line, colonIndex ); + entry.setNewSuperior( attributeValue instanceof String ? ( String ) attributeValue + : StringTools.utf8ToString( ( byte[] ) attributeValue ) ); + } + else + { + if ( operation == ChangeType.ModDn ) + { + LOG.error( "A moddn operation must contains a \"newsuperior:\"" ); + throw new NamingException( "Bad moddn operation, no newsuperior" ); + } + } + } + else + { + if ( operation == ChangeType.ModDn ) + { + LOG.error( "A moddn operation must contains a \"newsuperior:\"" ); + throw new NamingException( "Bad moddn operation, no newsuperior" ); + } + } + + return; + + default: + // This is an error + LOG.error( "Unknown operation" ); + throw new NamingException( "Bad operation" ); + } + } + + + /** + * Parse a ldif file. The following rules are processed : + * + * <ldif-file> ::= <ldif-attrval-record> <ldif-attrval-records> | + * <ldif-change-record> <ldif-change-records> <ldif-attrval-record> ::= + * <dn-spec> <sep> <attrval-spec> <attrval-specs> <ldif-change-record> ::= + * <dn-spec> <sep> <controls-e> <changerecord> <dn-spec> ::= "dn:" <fill> + * <distinguishedName> | "dn::" <fill> <base64-distinguishedName> + * <changerecord> ::= "changetype:" <fill> <change-op> + * + * @return the parsed ldifEntry + * @exception NamingException If the ldif file does not contain a valid entry + */ + private LdifEntry parseEntry() throws NamingException + { + if ( ( lines == null ) || ( lines.size() == 0 ) ) + { + LOG.debug( "The entry is empty : end of ldif file" ); + return null; + } + + // The entry must start with a dn: or a dn:: + String line = lines.get( 0 ); + + String name = parseDn( line ); + + LdapDN dn = new LdapDN( name ); + + // Ok, we have found a DN + LdifEntry entry = new LdifEntry(); + entry.setDn( dn ); + + // We remove this dn from the lines + lines.remove( 0 ); + + // Now, let's iterate through the other lines + Iterator iter = lines.iterator(); + + // This flag is used to distinguish between an entry and a change + int type = UNKNOWN; + + // The following boolean is used to check that a control is *not* + // found elswhere than just after the dn + boolean controlSeen = false; + + // We use this boolean to check that we do not have AttributeValues + // after a change operation + boolean changeTypeSeen = false; + + ChangeType operation = ChangeType.Add; + String lowerLine = null; + Control control = null; + + while ( iter.hasNext() ) + { + // Each line could start either with an OID, an attribute type, with + // "control:" or with "changetype:" + line = iter.next(); + lowerLine = line.toLowerCase(); + + // We have three cases : + // 1) The first line after the DN is a "control:" + // 2) The first line after the DN is a "changeType:" + // 3) The first line after the DN is anything else + if ( lowerLine.startsWith( "control:" ) ) + { + if ( containsEntries ) + { + LOG.error( "We cannot have changes when reading a file which already contains entries" ); + throw new NamingException( "No changes withing entries" ); + } + + containsChanges = true; + + if ( controlSeen ) + { + LOG.error( "We already have had a control" ); + throw new NamingException( "Control misplaced" ); + } + + // Parse the control + control = parseControl( line.substring( "control:".length() ) ); + entry.setControl( control ); + } + else if ( lowerLine.startsWith( "changetype:" ) ) + { + if ( containsEntries ) + { + LOG.error( "We cannot have changes when reading a file which already contains entries" ); + throw new NamingException( "No changes withing entries" ); + } + + containsChanges = true; + + if ( changeTypeSeen ) + { + LOG.error( "We already have had a changeType" ); + throw new NamingException( "ChangeType misplaced" ); + } + + // A change request + type = CHANGE; + controlSeen = true; + + operation = parseChangeType( line ); + + // Parse the change operation in a separate function + parseChange( entry, iter, operation ); + changeTypeSeen = true; + } + else if ( line.indexOf( ':' ) > 0 ) + { + if ( containsChanges ) + { + LOG.error( "We cannot have entries when reading a file which already contains changes" ); + throw new NamingException( "No entries within changes" ); + } + + containsEntries = true; + + if ( controlSeen || changeTypeSeen ) + { + LOG.error( "We can't have a Attribute/Value pair after a control or a changeType" ); + throw new NamingException( "AttributeType misplaced" ); + } + + parseAttributeValue( entry, line, lowerLine ); + type = LDIF_ENTRY; + } + else + { + // Invalid attribute Value + LOG.error( "Expecting an attribute type" ); + throw new NamingException( "Bad attribute" ); + } + } + + if ( type == LDIF_ENTRY ) + { + LOG.debug( "Read an entry : {}", entry ); + } + else if ( type == CHANGE ) + { + entry.setChangeType( operation ); + LOG.debug( "Read a modification : {}", entry ); + } + else + { + LOG.error( "Unknown entry type" ); + throw new NamingException( "Unknown entry" ); + } + + return entry; + } + + + /** + * Parse the version from the ldif input. + * + * @return A number representing the version (default to 1) + * @throws NamingException + * If the version is incorrect + * @throws NamingException + * If the input is incorrect + */ + private int parseVersion() throws NamingException + { + int ver = DEFAULT_VERSION; + + // First, read a list of lines + readLines(); + + if ( lines.size() == 0 ) + { + LOG.warn( "The ldif file is empty" ); + return ver; + } + + // get the first line + String line = lines.get( 0 ); + + // ::= "version:" + char[] document = line.toCharArray(); + String versionNumber = null; + + if ( line.startsWith( "version:" ) ) + { + position.inc( "version:".length() ); + parseFill( document, position ); + + // Version number. Must be '1' in this version + versionNumber = parseNumber( document, position ); + + // We should not have any other chars after the number + if ( position.pos != document.length ) + { + LOG.error( "The version is not a number" ); + throw new NamingException( "Ldif parsing error" ); + } + + try + { + ver = Integer.parseInt( versionNumber ); + } + catch ( NumberFormatException nfe ) + { + LOG.error( "The version is not a number" ); + throw new NamingException( "Ldif parsing error" ); + } + + LOG.debug( "Ldif version : {}", versionNumber ); + + // We have found the version, just discard the line from the list + lines.remove( 0 ); + + // and read the next lines if the current buffer is empty + if ( lines.size() == 0 ) + { + readLines(); + } + } + else + { + LOG.warn( "No version information : assuming version: 1" ); + } + + return ver; + } + + + /** + * Reads an entry in a ldif buffer, and returns the resulting lines, without + * comments, and unfolded. + * + * The lines represent *one* entry. + * + * @throws NamingException If something went wrong + */ + protected void readLines() throws NamingException + { + String line = null; + boolean insideComment = true; + boolean isFirstLine = true; + + lines.clear(); + StringBuffer sb = new StringBuffer(); + + try + { + while ( ( line = ( ( BufferedReader ) reader ).readLine() ) != null ) + { + if ( line.length() == 0 ) + { + if ( isFirstLine ) + { + continue; + } + else + { + // The line is empty, we have read an entry + insideComment = false; + break; + } + } + + // We will read the first line which is not a comment + switch ( line.charAt( 0 ) ) + { + case '#': + insideComment = true; + break; + + case ' ': + isFirstLine = false; + + if ( insideComment ) + { + continue; + } + else if ( sb.length() == 0 ) + { + LOG.error( "Cannot have an empty continuation line" ); + throw new NamingException( "Ldif Parsing error" ); + } + else + { + sb.append( line.substring( 1 ) ); + } + + insideComment = false; + break; + + default: + isFirstLine = false; + + // We have found a new entry + // First, stores the previous one if any. + if ( sb.length() != 0 ) + { + lines.add( sb.toString() ); + } + + sb = new StringBuffer( line ); + insideComment = false; + break; + } + } + } + catch ( IOException ioe ) + { + throw new NamingException( "Error while reading ldif lines" ); + } + + // Stores the current line if necessary. + if ( sb.length() != 0 ) + { + lines.add( sb.toString() ); + } + + return; + } + + + /** + * Parse a ldif file (using the default encoding). + * + * @param fileName + * The ldif file + * @return A list of entries + * @throws NamingException + * If the parsing fails + */ + public List parseLdifFile( String fileName ) throws NamingException + { + return parseLdifFile( fileName, Charset.forName( StringTools.getDefaultCharsetName() ).toString() ); + } + + + /** + * Parse a ldif file, decoding it using the given charset encoding + * + * @param fileName + * The ldif file + * @param encoding + * The charset encoding to use + * @return A list of entries + * @throws NamingException + * If the parsing fails + */ + public List parseLdifFile( String fileName, String encoding ) throws NamingException + { + if ( StringTools.isEmpty( fileName ) ) + { + LOG.error( "Cannot parse an empty file name !" ); + throw new NamingException( "Empty filename" ); + } + + File file = new File( fileName ); + + if ( !file.exists() ) + { + LOG.error( "Cannot parse the file {}, it does not exist", fileName ); + throw new NamingException( "Filename " + fileName + " not found." ); + } + + BufferedReader reader = null; + + // Open the file and then get a channel from the stream + try + { + reader = new BufferedReader( + new InputStreamReader( + new FileInputStream( file ), Charset.forName( encoding ) ) ); + + return parseLdif( reader ); + } + catch ( FileNotFoundException fnfe ) + { + LOG.error( "Cannot find file {}", fileName ); + throw new NamingException( "Filename " + fileName + " not found." ); + } + finally + { + // close the reader + try + { + if ( reader != null ) + { + reader.close(); + } + } + catch ( IOException ioe ) + { + // Nothing to do + } + } + } + + + /** + * A method which parses a ldif string and returns a list of entries. + * + * @param ldif + * The ldif string + * @return A list of entries, or an empty List + * @throws NamingException + * If something went wrong + */ + public List parseLdif( String ldif ) throws NamingException + { + LOG.debug( "Starts parsing ldif buffer" ); + + if ( StringTools.isEmpty( ldif ) ) + { + return new ArrayList(); + } + + BufferedReader reader = new BufferedReader( + new StringReader( ldif ) ); + + try + { + List entries = parseLdif( reader ); + + if ( LOG.isDebugEnabled() ) + { + LOG.debug( "Parsed {} entries.", ( entries == null ? Integer.valueOf( 0 ) : Integer.valueOf( entries + .size() ) ) ); + } + + return entries; + } + catch ( NamingException ne ) + { + LOG.error( "Cannot parse the ldif buffer : {}", ne.getMessage() ); + throw new NamingException( "Error while parsing the ldif buffer" ); + } + finally + { + // Close the reader + try + { + if ( reader != null ) + { + reader.close(); + } + } + catch ( IOException ioe ) + { + // Nothing to do + } + + } + } + + + // ------------------------------------------------------------------------ + // Iterator Methods + // ------------------------------------------------------------------------ + + /** + * Gets the next LDIF on the channel. + * + * @return the next LDIF as a String. + * @exception NoSuchElementException If we can't read the next entry + */ + private LdifEntry nextInternal() + { + try + { + LOG.debug( "next(): -- called" ); + + LdifEntry entry = prefetched; + readLines(); + + try + { + prefetched = parseEntry(); + } + catch ( NamingException ne ) + { + error = ne; + throw new NoSuchElementException( ne.getMessage() ); + } + + LOG.debug( "next(): -- returning ldif {}\n", entry ); + + return entry; + } + catch ( NamingException ne ) + { + LOG.error( "Premature termination of LDIF iterator" ); + error = ne; + return null; + } + } + + + /** + * Gets the next LDIF on the channel. + * + * @return the next LDIF as a String. + * @exception NoSuchElementException If we can't read the next entry + */ + public LdifEntry next() + { + return nextInternal(); + } + + + /** + * Tests to see if another LDIF is on the input channel. + * + * @return true if another LDIF is available false otherwise. + */ + private boolean hasNextInternal() + { + return null != prefetched; + } + + + /** + * Tests to see if another LDIF is on the input channel. + * + * @return true if another LDIF is available false otherwise. + */ + public boolean hasNext() + { + LOG.debug( "hasNext(): -- returning {}", ( prefetched != null ) ? Boolean.TRUE : Boolean.FALSE ); + + return hasNextInternal(); + } + + + /** + * Always throws UnsupportedOperationException! + * + * @see java.util.Iterator#remove() + */ + private void removeInternal() + { + throw new UnsupportedOperationException(); + } + + + /** + * Always throws UnsupportedOperationException! + * + * @see java.util.Iterator#remove() + */ + public void remove() + { + removeInternal(); + } + + + /** + * @return An iterator on the file + */ + public Iterator iterator() + { + return new Iterator() + { + public boolean hasNext() + { + return hasNextInternal(); + } + + + public LdifEntry next() + { + return nextInternal(); + } + + + public void remove() + { + throw new UnsupportedOperationException(); + } + }; + } + + + /** + * @return True if an error occured during parsing + */ + public boolean hasError() + { + return error != null; + } + + + /** + * @return The exception that occurs during an entry parsing + */ + public Exception getError() + { + return error; + } + + + /** + * The main entry point of the LdifParser. It reads a buffer and returns a + * List of entries. + * + * @param inf + * The buffer being processed + * @return A list of entries + * @throws NamingException + * If something went wrong + */ + public List parseLdif( BufferedReader reader ) throws NamingException + { + // Create a list that will contain the read entries + List entries = new ArrayList(); + + this.reader = reader; + + // First get the version - if any - + version = parseVersion(); + prefetched = parseEntry(); + + // When done, get the entries one by one. + try + { + for ( LdifEntry entry : this ) + { + if ( entry != null ) + { + entries.add( entry ); + } + } + } + catch ( NoSuchElementException nsee ) + { + throw new NamingException( "Error while parsing ldif : " + error.getMessage() ); + } + + return entries; + } + + + /** + * @return True if the ldif file contains entries, fals if it contains + * changes + */ + public boolean containsEntries() + { + return containsEntries; + } + + + /** + * {@inheritDoc} + */ + public void close() throws IOException + { + if ( reader != null ) + { + position = new Position(); + reader.close(); + } + } +} \ No newline at end of file Added: directory/shared/trunk/ldap-ldif/src/main/java/org/apache/directory/shared/ldap/ldif/LdifRevertor.java URL: http://svn.apache.org/viewvc/directory/shared/trunk/ldap-ldif/src/main/java/org/apache/directory/shared/ldap/ldif/LdifRevertor.java?rev=903465&view=auto ============================================================================== --- directory/shared/trunk/ldap-ldif/src/main/java/org/apache/directory/shared/ldap/ldif/LdifRevertor.java (added) +++ directory/shared/trunk/ldap-ldif/src/main/java/org/apache/directory/shared/ldap/ldif/LdifRevertor.java Tue Jan 26 22:43:27 2010 @@ -0,0 +1,633 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + */ +package org.apache.directory.shared.ldap.ldif; + + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.naming.InvalidNameException; +import javax.naming.NamingException; + +import org.apache.directory.shared.ldap.entry.Entry; +import org.apache.directory.shared.ldap.entry.EntryAttribute; +import org.apache.directory.shared.ldap.entry.Modification; +import org.apache.directory.shared.ldap.entry.ModificationOperation; +import org.apache.directory.shared.ldap.entry.client.ClientModification; +import org.apache.directory.shared.ldap.entry.client.DefaultClientAttribute; +import org.apache.directory.shared.ldap.name.AVA; +import org.apache.directory.shared.ldap.name.LdapDN; +import org.apache.directory.shared.ldap.name.RDN; +import org.apache.directory.shared.ldap.util.AttributeUtils; + + +/** + * A helper class which provides methods to reverse a LDIF modification operation. + * + * @author Apache Directory Project + * @version $Rev$, $Date$ + */ +public class LdifRevertor +{ + /** Two constants for the deleteOldRdn flag */ + public static final boolean DELETE_OLD_RDN = true; + public static final boolean KEEP_OLD_RDN = false; + + /** + * Compute a reverse LDIF of an AddRequest. It's simply a delete request + * of the added entry + * + * @param dn the dn of the added entry + * @return a reverse LDIF + */ + public static LdifEntry reverseAdd( LdapDN dn ) + { + LdifEntry entry = new LdifEntry(); + entry.setChangeType( ChangeType.Delete ); + entry.setDn( dn ); + return entry; + } + + + /** + * Compute a reverse LDIF of a DeleteRequest. We have to get the previous + * entry in order to restore it. + * + * @param dn The deleted entry DN + * @param deletedEntry The entry which has been deleted + * @return A reverse LDIF + */ + public static LdifEntry reverseDel( LdapDN dn, Entry deletedEntry ) throws NamingException + { + LdifEntry entry = new LdifEntry(); + + entry.setDn( dn ); + entry.setChangeType( ChangeType.Add ); + + for ( EntryAttribute attribute : deletedEntry ) + { + entry.addAttribute( attribute ); + } + + return entry; + } + + + /** + * + * Compute the reversed LDIF for a modify request. We will deal with the + * three kind of modifications : + * - add + * - remove + * - replace + * + * As the modifications should be issued in a reversed order ( ie, for + * the initials modifications {A, B, C}, the reversed modifications will + * be ordered like {C, B, A}), we will change the modifications order. + * + * @param dn the dn of the modified entry + * @param forwardModifications the modification items for the forward change + * @param modifiedEntry The modified entry. Necessary for the destructive modifications + * @return A reversed LDIF + * @throws NamingException If something went wrong + */ + public static LdifEntry reverseModify( LdapDN dn, List forwardModifications, Entry modifiedEntry ) + throws NamingException + { + // First, protect the original entry by cloning it : we will modify it + Entry clonedEntry = ( Entry ) modifiedEntry.clone(); + + LdifEntry entry = new LdifEntry(); + entry.setChangeType( ChangeType.Modify ); + + entry.setDn( dn ); + + // As the reversed modifications should be pushed in reversed order, + // we create a list to temporarily store the modifications. + List reverseModifications = new ArrayList(); + + // Loop through all the modifications. For each modification, we will + // have to apply it to the modified entry in order to be able to generate + // the reversed modification + for ( Modification modification : forwardModifications ) + { + switch ( modification.getOperation() ) + { + case ADD_ATTRIBUTE: + EntryAttribute mod = modification.getAttribute(); + + EntryAttribute previous = clonedEntry.get( mod.getId() ); + + if ( mod.equals( previous ) ) + { + continue; + } + + Modification reverseModification = new ClientModification( ModificationOperation.REMOVE_ATTRIBUTE, + mod ); + reverseModifications.add( 0, reverseModification ); + break; + + case REMOVE_ATTRIBUTE: + mod = modification.getAttribute(); + + previous = clonedEntry.get( mod.getId() ); + + if ( previous == null ) + { + // Nothing to do if the previous attribute didn't exist + continue; + } + + if ( mod.get() == null ) + { + reverseModification = new ClientModification( ModificationOperation.ADD_ATTRIBUTE, previous ); + reverseModifications.add( 0, reverseModification ); + break; + } + + reverseModification = new ClientModification( ModificationOperation.ADD_ATTRIBUTE, mod ); + reverseModifications.add( 0, reverseModification ); + break; + + case REPLACE_ATTRIBUTE: + mod = modification.getAttribute(); + + previous = clonedEntry.get( mod.getId() ); + + /* + * The server accepts without complaint replace + * modifications to non-existing attributes in the + * entry. When this occurs nothing really happens + * but this method freaks out. To prevent that we + * make such no-op modifications produce the same + * modification for the reverse direction which should + * do nothing as well. + */ + if ( ( mod.get() == null ) && ( previous == null ) ) + { + reverseModification = new ClientModification( ModificationOperation.REPLACE_ATTRIBUTE, + new DefaultClientAttribute( mod.getId() ) ); + reverseModifications.add( 0, reverseModification ); + continue; + } + + if ( mod.get() == null ) + { + reverseModification = new ClientModification( ModificationOperation.REPLACE_ATTRIBUTE, previous ); + reverseModifications.add( 0, reverseModification ); + continue; + } + + if ( previous == null ) + { + EntryAttribute emptyAttribute = new DefaultClientAttribute( mod.getId() ); + reverseModification = new ClientModification( ModificationOperation.REPLACE_ATTRIBUTE, + emptyAttribute ); + reverseModifications.add( 0, reverseModification ); + continue; + } + + reverseModification = new ClientModification( ModificationOperation.REPLACE_ATTRIBUTE, previous ); + reverseModifications.add( 0, reverseModification ); + break; + + default: + break; // Do nothing + + } + + AttributeUtils.applyModification( clonedEntry, modification ); + + } + + // Special case if we don't have any reverse modifications + if ( reverseModifications.size() == 0 ) + { + throw new IllegalArgumentException( "Could not deduce reverse modifications from provided modifications: " + + forwardModifications ); + } + + // Now, push the reversed list into the entry + for ( Modification modification : reverseModifications ) + { + entry.addModificationItem( modification ); + } + + // Return the reverted entry + return entry; + } + + + /** + * Compute a reverse LDIF for a forward change which if in LDIF format + * would represent a Move operation. Hence there is no newRdn in the + * picture here. + * + * @param newSuperiorDn the new parent dn to be (must not be null) + * @param modifiedDn the dn of the entry being moved (must not be null) + * @return a reverse LDIF + * @throws NamingException if something went wrong + */ + public static LdifEntry reverseMove( LdapDN newSuperiorDn, LdapDN modifiedDn ) throws NamingException + { + LdifEntry entry = new LdifEntry(); + LdapDN currentParent = null; + RDN currentRdn = null; + LdapDN newDn = null; + + if ( newSuperiorDn == null ) + { + throw new NullPointerException( "newSuperiorDn must not be null" ); + } + + if ( modifiedDn == null ) + { + throw new NullPointerException( "modifiedDn must not be null" ); + } + + if ( modifiedDn.size() == 0 ) + { + throw new IllegalArgumentException( "Don't think about moving the rootDSE." ); + } + + currentParent = ( LdapDN ) modifiedDn.clone(); + currentRdn = currentParent.getRdn(); + currentParent.remove( currentParent.size() - 1 ); + + newDn = ( LdapDN ) newSuperiorDn.clone(); + newDn.add( modifiedDn.getRdn() ); + + entry.setChangeType( ChangeType.ModDn ); + entry.setDn( newDn ); + entry.setNewRdn( currentRdn.getUpName() ); + entry.setNewSuperior( currentParent.getName() ); + entry.setDeleteOldRdn( false ); + return entry; + } + + + /** + * A small helper class to compute the simple revert. + */ + private static LdifEntry revertEntry( List entries, Entry entry, LdapDN newDn, + LdapDN newSuperior, RDN oldRdn, RDN newRdn ) throws InvalidNameException + { + LdifEntry reverted = new LdifEntry(); + + // We have a composite old RDN, something like A=a+B=b + // It does not matter if the RDNs overlap + reverted.setChangeType( ChangeType.ModRdn ); + + if ( newSuperior != null ) + { + LdapDN restoredDn = (LdapDN)((LdapDN)newSuperior.clone()).add( newRdn ); + reverted.setDn( restoredDn ); + } + else + { + reverted.setDn( newDn ); + } + + reverted.setNewRdn( oldRdn.getUpName() ); + + // Is the newRdn's value present in the entry ? + // ( case 3, 4 and 5) + // If keepOldRdn = true, we cover case 4 and 5 + boolean keepOldRdn = entry.contains( newRdn.getNormType(), newRdn.getNormValue() ); + + reverted.setDeleteOldRdn( !keepOldRdn ); + + if ( newSuperior != null ) + { + LdapDN oldSuperior = ( LdapDN ) entry.getDn().clone(); + + oldSuperior.remove( oldSuperior.size() - 1 ); + reverted.setNewSuperior( oldSuperior.getName() ); + } + + return reverted; + } + + + /** + * A helper method to generate the modified attribute after a rename. + */ + private static LdifEntry generateModify( LdapDN parentDn, Entry entry, RDN oldRdn, RDN newRdn ) + { + LdifEntry restored = new LdifEntry(); + restored.setChangeType( ChangeType.Modify ); + + // We have to use the parent DN, the entry has already + // been renamed + restored.setDn( parentDn ); + + for ( AVA ava:newRdn ) + { + // No need to add something which has already been added + // in the previous modification + if ( !entry.contains( ava.getNormType(), ava.getNormValue().getString() ) && + !(ava.getNormType().equals( oldRdn.getNormType() ) && + ava.getNormValue().equals( oldRdn.getNormValue() ) ) ) + { + // Create the modification, which is an Remove + Modification modification = new ClientModification( + ModificationOperation.REMOVE_ATTRIBUTE, + new DefaultClientAttribute( ava.getUpType(), ava.getUpValue().getString() ) ); + + restored.addModificationItem( modification ); + } + } + + return restored; + } + + + /** + * A helper method which generates a reverted entry + */ + private static LdifEntry generateReverted( LdapDN newSuperior, RDN newRdn, LdapDN newDn, + RDN oldRdn, boolean deleteOldRdn ) throws InvalidNameException + { + LdifEntry reverted = new LdifEntry(); + reverted.setChangeType( ChangeType.ModRdn ); + + if ( newSuperior != null ) + { + LdapDN restoredDn = (LdapDN)((LdapDN)newSuperior.clone()).add( newRdn ); + reverted.setDn( restoredDn ); + } + else + { + reverted.setDn( newDn ); + } + + reverted.setNewRdn( oldRdn.getUpName() ); + + if ( newSuperior != null ) + { + LdapDN oldSuperior = ( LdapDN ) newDn.clone(); + + oldSuperior.remove( oldSuperior.size() - 1 ); + reverted.setNewSuperior( oldSuperior.getName() ); + } + + // Delete the newRDN values + reverted.setDeleteOldRdn( deleteOldRdn ); + + return reverted; + } + + + /** + * Revert a DN to it's previous version by removing the first RDN and adding the given RDN. + * It's a rename operation. The biggest issue is that we have many corner cases, depending + * on the RDNs we are manipulating, and on the content of the initial entry. + * + * @param entry The initial Entry + * @param newRdn The new RDN + * @param deleteOldRdn A flag which tells to delete the old RDN AVAs + * @return A list of LDIF reverted entries + * @throws NamingException If the name reverting failed + */ + public static List reverseRename( Entry entry, RDN newRdn, boolean deleteOldRdn ) throws NamingException + { + return reverseMoveAndRename( entry, null, newRdn, deleteOldRdn ); + } + + + /** + * Revert a DN to it's previous version by removing the first RDN and adding the given RDN. + * It's a rename operation. The biggest issue is that we have many corner cases, depending + * on the RDNs we are manipulating, and on the content of the initial entry. + * + * @param entry The initial Entry + * @param newSuperior The new superior DN (can be null if it's just a rename) + * @param newRdn The new RDN + * @param deleteOldRdn A flag which tells to delete the old RDN AVAs + * @return A list of LDIF reverted entries + * @throws NamingException If the name reverting failed + */ + public static List reverseMoveAndRename( Entry entry, LdapDN newSuperior, RDN newRdn, boolean deleteOldRdn ) throws NamingException + { + LdapDN parentDn = entry.getDn(); + LdapDN newDn = null; + + if ( newRdn == null ) + { + throw new NullPointerException( "The newRdn must not be null" ); + } + + if ( parentDn == null ) + { + throw new NullPointerException( "The modified Dn must not be null" ); + } + + if ( parentDn.size() == 0 ) + { + throw new IllegalArgumentException( "Don't think about renaming the rootDSE." ); + } + + parentDn = ( LdapDN ) entry.getDn().clone(); + RDN oldRdn = parentDn.getRdn(); + + newDn = ( LdapDN ) parentDn.clone(); + newDn.remove( newDn.size() - 1 ); + newDn.add( newRdn ); + + List entries = new ArrayList( 1 ); + LdifEntry reverted = new LdifEntry(); + + // Start with the cases here + if ( newRdn.size() == 1 ) + { + // We have a simple new RDN, something like A=a + if ( ( oldRdn.size() == 1 ) && ( oldRdn.equals( newRdn ) ) ) + { + // We have a simple old RDN, something like A=a + // If the values overlap, we can't rename the entry, just get out + // with an error + throw new NamingException( "Can't rename an entry using the same name ..." ); + } + + reverted = + revertEntry( entries, entry, newDn, newSuperior, oldRdn, newRdn ); + + entries.add( reverted ); + } + else + { + // We have a composite new RDN, something like A=a+B=b + if ( oldRdn.size() == 1 ) + { + // The old RDN is simple + boolean overlapping = false; + boolean existInEntry = false; + + // Does it overlap ? + // Is the new RDN AVAs contained into the entry? + for ( AVA atav:newRdn ) + { + if ( atav.equals( oldRdn.getAtav() ) ) + { + // They overlap + overlapping = true; + } + else + { + if ( entry.contains( atav.getNormType(), atav.getNormValue().getString() ) ) + { + existInEntry = true; + } + } + } + + if ( overlapping ) + { + // The new RDN includes the old one + if ( existInEntry ) + { + // Some of the new RDN AVAs existed in the entry + // We have to restore them, but we also have to remove + // the new values + reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, KEEP_OLD_RDN ); + + entries.add( reverted ); + + // Now, restore the initial values + LdifEntry restored = generateModify( parentDn, entry, oldRdn, newRdn ); + + entries.add( restored ); + } + else + { + // This is the simplest case, we don't have to restore + // some existing values (case 8.1 and 9.1) + reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, DELETE_OLD_RDN ); + + entries.add( reverted ); + } + } + else + { + if ( existInEntry ) + { + // Some of the new RDN AVAs existed in the entry + // We have to restore them, but we also have to remove + // the new values + reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, KEEP_OLD_RDN ); + + entries.add( reverted ); + + LdifEntry restored = generateModify( parentDn, entry, oldRdn, newRdn ); + + entries.add( restored ); + } + else + { + // A much simpler case, as we just have to remove the newRDN + reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, DELETE_OLD_RDN ); + + entries.add( reverted ); + } + } + } + else + { + // We have a composite new RDN, something like A=a+B=b + // Does the RDN overlap ? + boolean overlapping = false; + boolean existInEntry = false; + + Set oldAtavs = new HashSet(); + + // We first build a set with all the oldRDN ATAVs + for ( AVA atav:oldRdn ) + { + oldAtavs.add( atav ); + } + + // Now we loop on the newRDN ATAVs to evaluate if the Rdns are overlaping + // and if the newRdn ATAVs are present in the entry + for ( AVA atav:newRdn ) + { + if ( oldAtavs.contains( atav ) ) + { + overlapping = true; + } + else if ( entry.contains( atav.getNormType(), atav.getNormValue().getString() ) ) + { + existInEntry = true; + } + } + + if ( overlapping ) + { + // They overlap + if ( existInEntry ) + { + // In this case, we have to reestablish the removed ATAVs + // (Cases 12.2 and 13.2) + reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, KEEP_OLD_RDN ); + + entries.add( reverted ); + } + else + { + // We can simply remove all the new RDN atavs, as the + // overlapping values will be re-created. + // (Cases 12.1 and 13.1) + reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, DELETE_OLD_RDN ); + + entries.add( reverted ); + } + } + else + { + // No overlapping + if ( existInEntry ) + { + // In this case, we have to reestablish the removed ATAVs + // (Cases 10.2 and 11.2) + reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, KEEP_OLD_RDN ); + + entries.add( reverted ); + + LdifEntry restored = generateModify( parentDn, entry, oldRdn, newRdn ); + + entries.add( restored ); + } + else + { + // We are safe ! We can delete all the new Rdn ATAVs + // (Cases 10.1 and 11.1) + reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, DELETE_OLD_RDN ); + + entries.add( reverted ); + } + } + } + } + + return entries; + } +}