blob: 8c47c290df1805ac06d80f65ee1e197cf81385c7 [file] [log] [blame]
/*
* 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.api.ldap.model.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 org.apache.directory.api.asn1.util.Oid;
import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
import org.apache.directory.api.ldap.model.entry.ModificationOperation;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.message.Control;
import org.apache.directory.api.ldap.model.name.Ava;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.MutableAttributeType;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.util.Base64;
import org.apache.directory.api.util.Chars;
import org.apache.directory.api.util.Strings;
import org.apache.directory.api.util.exception.NotImplementedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <pre>
* &lt;ldif-file&gt; ::= &quot;version:&quot; &lt;fill&gt; &lt;number&gt; &lt;seps&gt; &lt;dn-spec&gt; &lt;sep&gt;
* &lt;ldif-content-change&gt;
*
* &lt;ldif-content-change&gt; ::=
* &lt;number&gt; &lt;oid&gt; &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt;
* &lt;attrval-specs-e&gt; &lt;ldif-attrval-record-e&gt; |
* &lt;alpha&gt; &lt;chars-e&gt; &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt;
* &lt;attrval-specs-e&gt; &lt;ldif-attrval-record-e&gt; |
* &quot;control:&quot; &lt;fill&gt; &lt;number&gt; &lt;oid&gt; &lt;spaces-e&gt;
* &lt;criticality&gt; &lt;value-spec-e&gt; &lt;sep&gt; &lt;controls-e&gt;
* &quot;changetype:&quot; &lt;fill&gt; &lt;changerecord-type&gt; &lt;ldif-change-record-e&gt; |
* &quot;changetype:&quot; &lt;fill&gt; &lt;changerecord-type&gt; &lt;ldif-change-record-e&gt;
*
* &lt;ldif-attrval-record-e&gt; ::= &lt;seps&gt; &lt;dn-spec&gt; &lt;sep&gt; &lt;attributeType&gt;
* &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; &lt;attrval-specs-e&gt;
* &lt;ldif-attrval-record-e&gt; | e
*
* &lt;ldif-change-record-e&gt; ::= &lt;seps&gt; &lt;dn-spec&gt; &lt;sep&gt; &lt;controls-e&gt;
* &quot;changetype:&quot; &lt;fill&gt; &lt;changerecord-type&gt; &lt;ldif-change-record-e&gt; | e
*
* &lt;dn-spec&gt; ::= &quot;dn:&quot; &lt;fill&gt; &lt;safe-string&gt; | &quot;dn::&quot; &lt;fill&gt; &lt;base64-string&gt;
*
* &lt;controls-e&gt; ::= &quot;control:&quot; &lt;fill&gt; &lt;number&gt; &lt;oid&gt; &lt;spaces-e&gt; &lt;criticality&gt;
* &lt;value-spec-e&gt; &lt;sep&gt; &lt;controls-e&gt; | e
*
* &lt;criticality&gt; ::= &quot;true&quot; | &quot;false&quot; | e
*
* &lt;oid&gt; ::= '.' &lt;number&gt; &lt;oid&gt; | e
*
* &lt;attrval-specs-e&gt; ::= &lt;number&gt; &lt;oid&gt; &lt;options-e&gt; &lt;value-spec&gt;
* &lt;sep&gt; &lt;attrval-specs-e&gt; |
* &lt;alpha&gt; &lt;chars-e&gt; &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; &lt;attrval-specs-e&gt; | e
*
* &lt;value-spec-e&gt; ::= &lt;value-spec&gt; | e
*
* &lt;value-spec&gt; ::= ':' &lt;fill&gt; &lt;safe-string-e&gt; |
* &quot;::&quot; &lt;fill&gt; &lt;base64-chars&gt; |
* &quot;:&lt;&quot; &lt;fill&gt; &lt;url&gt;
*
* &lt;attributeType&gt; ::= &lt;number&gt; &lt;oid&gt; | &lt;alpha&gt; &lt;chars-e&gt;
*
* &lt;options-e&gt; ::= ';' &lt;char&gt; &lt;chars-e&gt; &lt;options-e&gt; |e
*
* &lt;chars-e&gt; ::= &lt;char&gt; &lt;chars-e&gt; | e
*
* &lt;changerecord-type&gt; ::= &quot;add&quot; &lt;sep&gt; &lt;attributeType&gt;
* &lt;options-e&gt; &lt;value-spec&gt; &lt;sep&gt; &lt;attrval-specs-e&gt; |
* &quot;delete&quot; &lt;sep&gt; |
* &quot;modify&quot; &lt;sep&gt; &lt;mod-type&gt; &lt;fill&gt; &lt;attributeType&gt;
* &lt;options-e&gt; &lt;sep&gt; &lt;attrval-specs-e&gt; &lt;sep&gt; '-' &lt;sep&gt; &lt;mod-specs-e&gt; |
* &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; &lt;newsuperior-e&gt; &lt;sep&gt; |
* &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; &lt;newsuperior-e&gt; &lt;sep&gt;
*
* &lt;newrdn&gt; ::= ':' &lt;fill&gt; &lt;safe-string&gt; | &quot;::&quot; &lt;fill&gt; &lt;base64-chars&gt;
*
* &lt;newsuperior-e&gt; ::= &quot;newsuperior&quot; &lt;newrdn&gt; | e
*
* &lt;mod-specs-e&gt; ::= &lt;mod-type&gt; &lt;fill&gt; &lt;attributeType&gt; &lt;options-e&gt;
* &lt;sep&gt; &lt;attrval-specs-e&gt; &lt;sep&gt; '-' &lt;sep&gt; &lt;mod-specs-e&gt; | e
*
* &lt;mod-type&gt; ::= &quot;add:&quot; | &quot;delete:&quot; | &quot;replace:&quot;
*
* &lt;url&gt; ::= &lt;a Uniform Resource Locator, as defined in [6]&gt;
*
*
*
* LEXICAL
* -------
*
* &lt;fill&gt; ::= ' ' &lt;fill&gt; | e
* &lt;char&gt; ::= &lt;alpha&gt; | &lt;digit&gt; | '-'
* &lt;number&gt; ::= &lt;digit&gt; &lt;digits&gt;
* &lt;0-1&gt; ::= '0' | '1'
* &lt;digits&gt; ::= &lt;digit&gt; &lt;digits&gt; | e
* &lt;digit&gt; ::= '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
* &lt;seps&gt; ::= &lt;sep&gt; &lt;seps-e&gt;
* &lt;seps-e&gt; ::= &lt;sep&gt; &lt;seps-e&gt; | e
* &lt;sep&gt; ::= 0x0D 0x0A | 0x0A
* &lt;spaces&gt; ::= ' ' &lt;spaces-e&gt;
* &lt;spaces-e&gt; ::= ' ' &lt;spaces-e&gt; | e
* &lt;safe-string-e&gt; ::= &lt;safe-string&gt; | e
* &lt;safe-string&gt; ::= &lt;safe-init-char&gt; &lt;safe-chars&gt;
* &lt;safe-init-char&gt; ::= [0x01-0x09] | 0x0B | 0x0C | [0x0E-0x1F] | [0x21-0x39] | 0x3B | [0x3D-0x7F]
* &lt;safe-chars&gt; ::= &lt;safe-char&gt; &lt;safe-chars&gt; | e
* &lt;safe-char&gt; ::= [0x01-0x09] | 0x0B | 0x0C | [0x0E-0x7F]
* &lt;base64-string&gt; ::= &lt;base64-char&gt; &lt;base64-chars&gt;
* &lt;base64-chars&gt; ::= &lt;base64-char&gt; &lt;base64-chars&gt; | e
* &lt;base64-char&gt; ::= 0x2B | 0x2F | [0x30-0x39] | 0x3D | [0x41-9x5A] | [0x61-0x7A]
* &lt;alpha&gt; ::= [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(&quot;.&quot; 1*DIGIT) to
* DIGIT+ (&quot;.&quot; DIGIT+)*
* - The mod-spec lacks a sep between *attrval-spec and &quot;-&quot;.
* - 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.
* </pre>
* The relaxed mode is used when a SchemaManager is injected.
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
*/
public class LdifReader implements Iterable<LdifEntry>, Closeable
{
/** A logger */
private static final Logger LOG = LoggerFactory.getLogger( LdifReader.class );
/** A list of read lines */
protected List<String> lines;
/** The current position */
protected int position;
/** The ldif file version default value */
protected static final int DEFAULT_VERSION = 1;
/** The ldif version */
protected int version;
/** Type of element read : ENTRY */
protected static final int LDIF_ENTRY = 0;
/** Type of element read : CHANGE */
protected static final int CHANGE = 1;
/** Type of element read : UNKNOWN */
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 : MOD_SPEC */
protected static final int MOD_SPEC = 0;
/** State values for the modify operation : ATTRVAL_SPEC */
protected static final int ATTRVAL_SPEC = 1;
/** State values for the modify operation : ATTRVAL_SPEC_OR_SEP */
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;
/** The SchemaManager instance, if any */
protected SchemaManager schemaManager;
/**
* An Exception to handle error message, has Iterator.next() can't throw
* exceptions
*/
protected Exception error;
/** total length of an LDIF entry including the comments */
protected int entryLen = 0;
/** the parsed entry's starting position */
protected long entryOffset = 0;
/** the current offset of the reader */
protected long offset = 0;
/** the numer of the current line being parsed by the reader */
protected int lineNumber;
/** flag to turn on/off of the DN validation. By default DNs are validated after parsing */
protected boolean validateDn = true;
/** A counter used to create facked OIDs */
private int oidCounter = 0;
/**
* Constructors
*/
public LdifReader()
{
lines = new ArrayList<String>();
position = 0;
version = DEFAULT_VERSION;
}
/**
* Constructors
*/
public LdifReader( SchemaManager schemaManager )
{
lines = new ArrayList<String>();
position = 0;
version = DEFAULT_VERSION;
this.schemaManager = schemaManager;
}
/**
* Store the reader and intialize the LdifReader
*/
private void initReader( BufferedReader reader ) throws LdapException
{
this.reader = reader;
init();
}
/**
* Initialize the LdifReader
*
* @throws LdapException If the initialization failed
*/
public void init() throws LdapException
{
lines = new ArrayList<String>();
position = 0;
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 LdapLdifException If the file cannot be processed or if the format is incorrect
*/
public LdifReader( String ldifFileName ) throws LdapLdifException
{
File file = new File( ldifFileName );
if ( !file.exists() )
{
String msg = I18n.err( I18n.ERR_12010_CANNOT_FIND_FILE, file.getAbsoluteFile() );
LOG.error( msg );
throw new LdapLdifException( msg );
}
if ( !file.canRead() )
{
String msg = I18n.err( I18n.ERR_12011_CANNOT_READ_FILE, file.getName() );
LOG.error( msg );
throw new LdapLdifException( msg );
}
try
{
initReader( new BufferedReader( new FileReader( file ) ) );
}
catch ( FileNotFoundException fnfe )
{
String msg = I18n.err( I18n.ERR_12010_CANNOT_FIND_FILE, file.getAbsoluteFile() );
LOG.error( msg );
throw new LdapLdifException( msg, fnfe );
}
catch ( LdapInvalidDnException lide )
{
throw new LdapLdifException( lide.getMessage(), lide );
}
catch ( LdapException le )
{
throw new LdapLdifException( le.getMessage(), le );
}
}
/**
* A constructor which takes a Reader
*
* @param in A Reader containing ldif formated input
* @throws LdapException If the file cannot be processed or if the format is incorrect
*/
public LdifReader( Reader in ) throws LdapException
{
initReader( new BufferedReader( in ) );
}
/**
* A constructor which takes an InputStream
*
* @param in An InputStream containing ldif formated input
* @throws LdapException If the file cannot be processed or if the format is incorrect
*/
public LdifReader( InputStream in ) throws LdapException
{
initReader( new BufferedReader( new InputStreamReader( in ) ) );
}
/**
* A constructor which takes a File
*
* @param file A File containing ldif formated input
* @throws LdapLdifException If the file cannot be processed or if the format is incorrect
*/
public LdifReader( File file ) throws LdapLdifException
{
if ( !file.exists() )
{
String msg = I18n.err( I18n.ERR_12010_CANNOT_FIND_FILE, file.getAbsoluteFile() );
LOG.error( msg );
throw new LdapLdifException( msg );
}
if ( !file.canRead() )
{
String msg = I18n.err( I18n.ERR_12011_CANNOT_READ_FILE, file.getName() );
LOG.error( msg );
throw new LdapLdifException( msg );
}
try
{
initReader( new BufferedReader( new FileReader( file ) ) );
}
catch ( FileNotFoundException fnfe )
{
String msg = I18n.err( I18n.ERR_12010_CANNOT_FIND_FILE, file.getAbsoluteFile() );
LOG.error( msg );
throw new LdapLdifException( msg, fnfe );
}
catch ( LdapInvalidDnException lide )
{
throw new LdapLdifException( lide.getMessage(), lide );
}
catch ( LdapException le )
{
throw new LdapLdifException( le.getMessage(), le );
}
}
/**
* A constructor which takes a File and a SchemaManager
*
* @param file A File containing ldif formated input
* @param schemaManager The SchemaManager instance to use
* @throws LdapLdifException If the file cannot be processed or if the format is incorrect
*/
public LdifReader( File file, SchemaManager schemaManager ) throws LdapLdifException
{
if ( !file.exists() )
{
String msg = I18n.err( I18n.ERR_12010_CANNOT_FIND_FILE, file.getAbsoluteFile() );
LOG.error( msg );
throw new LdapLdifException( msg );
}
if ( !file.canRead() )
{
String msg = I18n.err( I18n.ERR_12011_CANNOT_READ_FILE, file.getName() );
LOG.error( msg );
throw new LdapLdifException( msg );
}
this.schemaManager = schemaManager;
try
{
initReader( new BufferedReader( new FileReader( file ) ) );
}
catch ( FileNotFoundException fnfe )
{
String msg = I18n.err( I18n.ERR_12010_CANNOT_FIND_FILE, file.getAbsoluteFile() );
LOG.error( msg );
throw new LdapLdifException( msg, fnfe );
}
catch ( LdapInvalidDnException lide )
{
throw new LdapLdifException( lide.getMessage(), lide );
}
catch ( LdapException le )
{
throw new LdapLdifException( le.getMessage(), le );
}
}
/**
* @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;
}
// <fill> ::= ' ' <fill> | e
private void parseFill( char[] document )
{
while ( Chars.isCharASCII( document, position, ' ' ) )
{
position++;
}
}
/**
* Parse a number following the rules :
*
* &lt;number&gt; ::= &lt;digit&gt; &lt;digits&gt; &lt;digits&gt; ::= &lt;digit&gt; &lt;digits&gt; | e &lt;digit&gt;
* ::= '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
* @return a String representing the parsed number
*/
private String parseNumber( char[] document )
{
int initPos = position;
while ( true )
{
if ( Chars.isDigit( document, position ) )
{
position++;
}
else
{
break;
}
}
if ( position == initPos )
{
return null;
}
else
{
return new String( document, initPos, position - initPos );
}
}
/**
* Parse the changeType
*
* @param line The line which contains the changeType
* @return The operation.
*/
protected ChangeType parseChangeType( String line )
{
ChangeType operation = ChangeType.Add;
String modOp = Strings.trim( line.substring( "changetype:".length() ) );
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 LdapLdifException If the Dn is invalid
*/
protected String parseDn( String line ) throws LdapLdifException
{
String dn;
String lowerLine = Strings.toLowerCaseAscii( line );
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 : it's a rootDSE
dn = "";
}
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( I18n.err( I18n.ERR_12014_BASE64_DN_EXPECTED, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12015_INVALID_BASE64_DN ), uee );
}
}
else
{
// The Dn is empty : error
LOG.error( I18n.err( I18n.ERR_12012_EMPTY_DN_NOT_ALLOWED, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12013_NO_DN ) );
}
}
else
{
dn = line.substring( 3 ).trim();
}
}
else
{
LOG.error( I18n.err( I18n.ERR_12016_DN_EXPECTED, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12013_NO_DN ) );
}
// Check that the Dn is valid. If not, an exception will be thrown
if ( validateDn && !Dn.isValid( dn ) )
{
String message = I18n.err( I18n.ERR_12017_INVALID_DN, dn, lineNumber );
LOG.error( message );
throw new LdapLdifException( message );
}
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 = Strings.trim( line.substring( pos + 2 ) );
return Base64.decode( value.toCharArray() );
}
else
{
return Strings.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 LdapLdifException If something went wrong
*/
protected Object parseValue( String line, int pos ) throws LdapLdifException
{
if ( line.length() > pos + 1 )
{
char c = line.charAt( pos + 1 );
if ( c == ':' )
{
String value = Strings.trim( line.substring( pos + 2 ) );
return Base64.decode( value.toCharArray() );
}
else if ( c == '<' )
{
String urlName = Strings.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( I18n.err( I18n.ERR_12018_FILE_NOT_FOUND, fileName, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12019_BAD_URL_FILE_NOT_FOUND ) );
}
else
{
long length = file.length();
if ( length > sizeLimit )
{
String message = I18n.err( I18n.ERR_12020_FILE_TOO_BIG, fileName, lineNumber );
LOG.error( message );
throw new LdapLdifException( message );
}
else
{
byte[] data = new byte[( int ) length];
DataInputStream inf = null;
try
{
inf = new DataInputStream( new FileInputStream( file ) );
inf.readFully( data );
return data;
}
catch ( FileNotFoundException fnfe )
{
// We can't reach this point, the file
// existence has already been
// checked
LOG.error( I18n.err( I18n.ERR_12018_FILE_NOT_FOUND, fileName, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12019_BAD_URL_FILE_NOT_FOUND ),
fnfe );
}
catch ( IOException ioe )
{
LOG.error( I18n.err( I18n.ERR_12022_ERROR_READING_FILE, fileName, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12023_ERROR_READING_BAD_URL ), ioe );
}
finally
{
try
{
if ( inf != null )
{
inf.close();
}
}
catch ( IOException ioe )
{
LOG.error(
I18n.err( I18n.ERR_12024_CANNOT_CLOSE_FILE, ioe.getMessage(), lineNumber ),
ioe );
// Just do nothing ...
}
}
}
}
}
else
{
LOG.error( I18n.err( I18n.ERR_12025_BAD_PROTOCOL ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12026_UNSUPPORTED_PROTOCOL, lineNumber ) );
}
}
catch ( MalformedURLException mue )
{
String message = I18n.err( I18n.ERR_12027_BAD_URL, urlName, lineNumber );
LOG.error( message );
throw new LdapLdifException( message, mue );
}
}
else
{
String value = Strings.trimLeft( line.substring( pos + 1 ) );
int end = value.length();
for ( int i = value.length() - 1; i > 0; i-- )
{
char cc = value.charAt( i );
if ( cc == ' ' )
{
if ( value.charAt( i - 1 ) == '\\' )
{
// Escaped space : do nothing
break;
}
else
{
end = i;
}
}
else
{
break;
}
}
String result = null;
result = value.substring( 0, end );
return result;
}
}
else
{
return null;
}
}
/**
* Parse a control. The grammar is :
* <pre>
* &lt;control&gt; ::= "control:" &lt;fill&gt; &lt;ldap-oid&gt; &lt;critical-e&gt; &lt;value-spec-e&gt; &lt;sep&gt;
* &lt;critical-e&gt; ::= &lt;spaces&gt; &lt;boolean&gt; | e
* &lt;boolean&gt; ::= "true" | "false"
* &lt;value-spec-e&gt; ::= &lt;value-spec&gt; | e
* &lt;value-spec&gt; ::= ":" &lt;fill&gt; &lt;SAFE-STRING-e&gt; | "::" &lt;fill&gt; &lt;BASE64-STRING&gt; | ":<" &lt;fill&gt; &lt;url&gt;
* </pre>
*
* It can be read as :
* <pre>
* "control:" &lt;fill&gt; &lt;ldap-oid&gt; [ " "+ ( "true" |
* "false") ] [ ":" &lt;fill&gt; &lt;SAFE-STRING-e&gt; | "::" &lt;fill&gt; &lt;BASE64-STRING&gt; | ":<"
* &lt;fill&gt; &lt;url&gt; ]
* </pre>
*
* @param line The line containing the control
* @return A control
* @exception LdapLdifException 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 LdapLdifException
{
String lowerLine = Strings.toLowerCaseAscii( line ).trim();
char[] controlValue = line.trim().toCharArray();
int pos = 0;
int length = controlValue.length;
// Get the <ldap-oid>
if ( pos > length )
{
// No OID : error !
LOG.error( I18n.err( I18n.ERR_12029_CONTROL_WITHOUT_OID, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12029_CONTROL_WITHOUT_OID ) );
}
int initPos = pos;
while ( Chars.isCharASCII( controlValue, pos, '.' ) || Chars.isDigit( controlValue, pos ) )
{
pos++;
}
if ( pos == initPos )
{
// Not a valid OID !
LOG.error( I18n.err( I18n.ERR_12029_CONTROL_WITHOUT_OID, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12029_CONTROL_WITHOUT_OID ) );
}
// Create and check the OID
String oidString = lowerLine.substring( 0, pos );
if ( !Oid.isOid( oidString ) )
{
String message = I18n.err( I18n.ERR_12031_INVALID_OID, oidString, lineNumber );
LOG.error( message );
throw new LdapLdifException( message );
}
LdifControl control = new LdifControl( oidString );
// Get the criticality, if any
// Skip the <fill>
while ( Chars.isCharASCII( controlValue, pos, ' ' ) )
{
pos++;
}
// Check if we have a "true" or a "false"
int criticalPos = lowerLine.indexOf( ':' );
int criticalLength;
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( I18n.err( I18n.ERR_12033_INVALID_CRITICALITY, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12033_INVALID_CRITICALITY ) );
}
if ( criticalPos > 0 )
{
// We have a value. It can be a normal value, a base64 encoded value
// or a file contained value
if ( Chars.isCharASCII( controlValue, criticalPos + 1, ':' ) )
{
// Base 64 encoded value
// Skip the <fill>
pos = criticalPos + 2;
while ( Chars.isCharASCII( controlValue, pos, ' ' ) )
{
pos++;
}
byte[] value = Base64.decode( line.substring( pos ).toCharArray() );
control.setValue( value );
}
else if ( Chars.isCharASCII( controlValue, criticalPos + 1, '<' ) )
{
// File contained value
throw new NotImplementedException( "See DIRSERVER-1547" );
}
else
{
// Skip the <fill>
pos = criticalPos + 1;
while ( Chars.isCharASCII( controlValue, pos, ' ' ) )
{
pos++;
}
// Standard value
byte[] value = new byte[length - pos];
for ( int i = 0; i < length - pos; i++ )
{
value[i] = ( byte ) controlValue[i + pos];
}
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.substring( 0, colonIndex );
Object attributeValue = parseSimpleValue( line, colonIndex );
// Create an attribute
if ( attributeValue instanceof String )
{
return new DefaultAttribute( attributeType, ( String ) attributeValue );
}
else
{
return new DefaultAttribute( attributeType, ( byte[] ) 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 LdapException If anything goes wrong
*/
public void parseAttributeValue( LdifEntry entry, String line, String lowerLine ) throws LdapException
{
int colonIndex = line.indexOf( ':' );
String attributeType = lowerLine.substring( 0, colonIndex );
// We should *not* have a Dn twice
if ( attributeType.equals( "dn" ) )
{
LOG.error( I18n.err( I18n.ERR_12002_ENTRY_WITH_TWO_DNS, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12003_LDIF_ENTRY_WITH_TWO_DNS ) );
}
Object attributeValue = parseValue( line, colonIndex );
if ( schemaManager != null )
{
AttributeType at = schemaManager.getAttributeType( attributeType );
if ( at != null )
{
if ( at.getSyntax().isHumanReadable() )
{
if ( attributeValue instanceof byte[] )
{
attributeValue = Strings.utf8ToString( ( byte[] ) attributeValue );
}
}
else
{
if ( attributeValue instanceof String )
{
attributeValue = Strings.getBytesUtf8( ( String ) attributeValue );
}
}
}
}
// Update the entry
try
{
entry.addAttribute( attributeType, attributeValue );
}
catch ( Exception e )
{
// The attribute does not exist already, create a fake one
if ( ( schemaManager != null ) && schemaManager.isRelaxed() )
{
MutableAttributeType newAttributeType = new MutableAttributeType( "1.3.6.1.4.1.18060.0.9999." + oidCounter++ );
newAttributeType.setNames( attributeType );
newAttributeType.setSyntax( schemaManager.getLdapSyntaxRegistry().get( SchemaConstants.DIRECTORY_STRING_SYNTAX ) );
schemaManager.add( newAttributeType );
entry.addAttribute( attributeType, attributeValue );
}
}
}
/**
* Parse a ModRDN operation
*
* @param entry The entry to update
* @param iter The lines iterator
* @throws LdapLdifException If anything goes wrong
*/
private void parseModRdn( LdifEntry entry, Iterator<String> iter ) throws LdapLdifException
{
// 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 = Strings.toLowerCaseAscii( line );
if ( lowerLine.startsWith( "newrdn::" ) || lowerLine.startsWith( "newrdn:" ) )
{
int colonIndex = line.indexOf( ':' );
Object attributeValue = parseValue( line, colonIndex );
if ( attributeValue instanceof String )
{
entry.setNewRdn( ( String ) attributeValue );
}
else
{
entry.setNewRdn( Strings.utf8ToString( ( byte[] ) attributeValue ) );
}
}
else
{
LOG.error( I18n.err( I18n.ERR_12035_BAD_MODRDN_OPERATION, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12035_BAD_MODRDN_OPERATION ) );
}
}
else
{
LOG.error( I18n.err( I18n.ERR_12035_BAD_MODRDN_OPERATION, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12035_BAD_MODRDN_OPERATION ) );
}
if ( iter.hasNext() )
{
String line = iter.next();
String lowerLine = Strings.toLowerCaseAscii( line );
if ( lowerLine.startsWith( "deleteoldrdn:" ) )
{
int colonIndex = line.indexOf( ':' );
Object attributeValue = parseValue( line, colonIndex );
entry.setDeleteOldRdn( "1".equals( attributeValue ) );
}
else
{
LOG.error( I18n.err( I18n.ERR_12038_NO_DELETEOLDRDN, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12038_NO_DELETEOLDRDN ) );
}
}
else
{
LOG.error( I18n.err( I18n.ERR_12038_NO_DELETEOLDRDN, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12038_NO_DELETEOLDRDN ) );
}
}
/**
* Parse a modify change type.
*
* The grammar is :
* <pre>
* &lt;changerecord&gt; ::= "changetype:" FILL "modify" SEP &lt;mod-spec&gt; &lt;mod-specs-e&gt;
* &lt;mod-spec&gt; ::= "add:" &lt;mod-val&gt; | "delete:" &lt;mod-val-del&gt; | "replace:" &lt;mod-val&gt;
* &lt;mod-specs-e&gt; ::= &lt;mod-spec&gt;
* &lt;mod-specs-e&gt; | e
* &lt;mod-val&gt; ::= FILL ATTRIBUTE-DESCRIPTION SEP ATTRVAL-SPEC &lt;attrval-specs-e&gt; "-" SEP
* &lt;mod-val-del&gt; ::= FILL ATTRIBUTE-DESCRIPTION SEP &lt;attrval-specs-e&gt; "-" SEP
* &lt;attrval-specs-e&gt; ::= ATTRVAL-SPEC &lt;attrval-specs&gt; | e
* </pre>
*
* @param entry The entry to feed
* @param iter The lines
* @exception LdapLdifException If the modify operation is invalid
*/
private void parseModify( LdifEntry entry, Iterator<String> iter ) throws LdapLdifException
{
int state = MOD_SPEC;
String modified = null;
ModificationOperation modificationType = ModificationOperation.ADD_ATTRIBUTE;
Attribute attribute = null;
// The following flag is used to deal with empty modifications
boolean isEmptyValue = true;
while ( iter.hasNext() )
{
String line = iter.next();
String lowerLine = Strings.toLowerCaseAscii( line );
if ( lowerLine.startsWith( "-" ) )
{
if ( ( state != ATTRVAL_SPEC_OR_SEP ) && ( state != ATTRVAL_SPEC ) )
{
LOG.error( I18n.err( I18n.ERR_12040_BAD_MODIFY_SEPARATOR, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12040_BAD_MODIFY_SEPARATOR ) );
}
else
{
if ( isEmptyValue )
{
if ( state == ATTRVAL_SPEC_OR_SEP )
{
entry.addModification( modificationType, modified );
}
else
{
// Update the entry with a null value
entry.addModification( modificationType, modified, null );
}
}
else
{
// Update the entry with the attribute
entry.addModification( modificationType, attribute );
}
state = MOD_SPEC;
isEmptyValue = true;
}
}
else if ( lowerLine.startsWith( "add:" ) )
{
if ( ( state != MOD_SPEC ) && ( state != ATTRVAL_SPEC ) )
{
LOG.error( I18n.err( I18n.ERR_12042_BAD_MODIFY_SEPARATOR_2, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12042_BAD_MODIFY_SEPARATOR_2 ) );
}
modified = Strings.trim( line.substring( "add:".length() ) );
modificationType = ModificationOperation.ADD_ATTRIBUTE;
attribute = new DefaultAttribute( modified );
state = ATTRVAL_SPEC;
}
else if ( lowerLine.startsWith( "delete:" ) )
{
if ( ( state != MOD_SPEC ) && ( state != ATTRVAL_SPEC ) )
{
LOG.error( I18n.err( I18n.ERR_12042_BAD_MODIFY_SEPARATOR_2, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12042_BAD_MODIFY_SEPARATOR_2 ) );
}
modified = Strings.trim( line.substring( "delete:".length() ) );
modificationType = ModificationOperation.REMOVE_ATTRIBUTE;
attribute = new DefaultAttribute( modified );
isEmptyValue = false;
state = ATTRVAL_SPEC_OR_SEP;
}
else if ( lowerLine.startsWith( "replace:" ) )
{
if ( ( state != MOD_SPEC ) && ( state != ATTRVAL_SPEC ) )
{
LOG.error( I18n.err( I18n.ERR_12042_BAD_MODIFY_SEPARATOR_2, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12042_BAD_MODIFY_SEPARATOR_2 ) );
}
modified = Strings.trim( line.substring( "replace:".length() ) );
modificationType = ModificationOperation.REPLACE_ATTRIBUTE;
attribute = new DefaultAttribute( modified );
state = ATTRVAL_SPEC_OR_SEP;
}
else
{
if ( ( state != ATTRVAL_SPEC ) && ( state != ATTRVAL_SPEC_OR_SEP ) )
{
LOG.error( I18n.err( I18n.ERR_12040_BAD_MODIFY_SEPARATOR, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12040_BAD_MODIFY_SEPARATOR ) );
}
// A standard AttributeType/AttributeValue pair
int colonIndex = line.indexOf( ':' );
String attributeType = line.substring( 0, colonIndex );
if ( !attributeType.equalsIgnoreCase( modified ) )
{
LOG.error( I18n.err( I18n.ERR_12044, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12045 ) );
}
// We should *not* have a Dn twice
if ( attributeType.equalsIgnoreCase( "dn" ) )
{
LOG.error( I18n.err( I18n.ERR_12002_ENTRY_WITH_TWO_DNS, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12003_LDIF_ENTRY_WITH_TWO_DNS ) );
}
Object attributeValue = parseValue( line, colonIndex );
try
{
if ( attributeValue instanceof String )
{
attribute.add( ( String ) attributeValue );
}
else
{
attribute.add( ( byte[] ) attributeValue );
}
}
catch ( LdapInvalidAttributeValueException liave )
{
throw new LdapLdifException( liave.getMessage(), liave );
}
isEmptyValue = false;
state = ATTRVAL_SPEC_OR_SEP;
}
}
if ( state != MOD_SPEC )
{
LOG.error( I18n.err( I18n.ERR_12042_BAD_MODIFY_SEPARATOR_2, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12042_BAD_MODIFY_SEPARATOR_2, lineNumber ) );
}
}
/**
* Parse a change operation. We have to handle different cases depending on
* the operation.
* <ul>
* <li>1) Delete : there should *not* be any line after the "changetype: delete" </li>
* <li>2) Add : we must have a list of AttributeType : AttributeValue elements </li>
* <li>3) ModDN : we must have two following lines: a "newrdn:" and a "deleteoldrdn:" </li>
* <li>4) ModRDN : the very same, but a "newsuperior:" line is expected </li>
* <li>5) Modify</li>
* </ul>
*
* The grammar is :
* <pre>
* &lt;changerecord&gt; ::= "changetype:" FILL "add" SEP &lt;attrval-spec&gt; &lt;attrval-specs-e&gt; |
* "changetype:" FILL "delete" |
* "changetype:" FILL "modrdn" SEP &lt;newrdn&gt; SEP &lt;deleteoldrdn&gt; SEP |
* // To be checked
* "changetype:" FILL "moddn" SEP &lt;newrdn&gt; SEP &lt;deleteoldrdn&gt; SEP &lt;newsuperior&gt; SEP |
* "changetype:" FILL "modify" SEP &lt;mod-spec&gt; &lt;mod-specs-e&gt;
* &lt;newrdn&gt; ::= "newrdn:" FILL Rdn | "newrdn::" FILL BASE64-Rdn
* &lt;deleteoldrdn&gt; ::= "deleteoldrdn:" FILL "0" | "deleteoldrdn:" FILL "1"
* &lt;newsuperior&gt; ::= "newsuperior:" FILL Dn | "newsuperior::" FILL BASE64-Dn
* &lt;mod-specs-e&gt; ::= &lt;mod-spec&gt; &lt;mod-specs-e&gt; | e
* &lt;mod-spec&gt; ::= "add:" &lt;mod-val&gt; | "delete:" &lt;mod-val&gt; | "replace:" &lt;mod-val&gt;
* &lt;mod-val&gt; ::= FILL ATTRIBUTE-DESCRIPTION SEP ATTRVAL-SPEC &lt;attrval-specs-e&gt; "-" SEP
* &lt;attrval-specs-e&gt; ::= ATTRVAL-SPEC &lt;attrval-specs&gt; | e
* </pre>
*
* @param entry The entry to feed
* @param iter The lines iterator
* @param operation The change operation (add, modify, delete, moddn or modrdn)
* @exception LdapException If the change operation is invalid
*/
private void parseChange( LdifEntry entry, Iterator<String> iter, ChangeType operation ) throws LdapException
{
// The changetype and operation has already been parsed.
entry.setChangeType( operation );
switch ( operation )
{
case Delete:
// The change type will tell that it's a delete operation,
// the dn is used as a key.
return;
case Add:
// We will iterate through all attribute/value pairs
while ( iter.hasNext() )
{
String line = iter.next();
String lowerLine = Strings.toLowerCaseAscii( line );
parseAttributeValue( entry, line, lowerLine );
}
return;
case Modify:
parseModify( entry, iter );
return;
case ModDn:
// They are supposed to have the same syntax :
// No break !
case ModRdn:
// First, parse the modrdn part
parseModRdn( entry, iter );
// The next line should be the new superior, if we have one
if ( iter.hasNext() )
{
String line = iter.next();
String lowerLine = Strings.toLowerCaseAscii( line );
if ( lowerLine.startsWith( "newsuperior:" ) )
{
int colonIndex = line.indexOf( ':' );
Object attributeValue = parseValue( line, colonIndex );
if ( attributeValue instanceof String )
{
entry.setNewSuperior( ( String ) attributeValue );
}
else
{
entry.setNewSuperior( Strings.utf8ToString( ( byte[] ) attributeValue ) );
}
}
else
{
if ( operation == ChangeType.ModDn )
{
LOG.error( I18n.err( I18n.ERR_12046, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12047 ) );
}
}
}
return;
default:
// This is an error
LOG.error( I18n.err( I18n.ERR_12048, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12049 ) );
}
}
/**
* Parse a ldif file. The following rules are processed :
* <pre>
* &lt;ldif-file&gt; ::= &lt;ldif-attrval-record&gt; &lt;ldif-attrval-records&gt; |
* &lt;ldif-change-record&gt; &lt;ldif-change-records&gt;
* &lt;ldif-attrval-record&gt; ::= &lt;dn-spec&gt; &lt;sep&gt; &lt;attrval-spec&gt; &lt;attrval-specs&gt;
* &lt;ldif-change-record&gt; ::= &lt;dn-spec&gt; &lt;sep&gt; &lt;controls-e&gt; &lt;changerecord&gt;
* &lt;dn-spec&gt; ::= "dn:" &lt;fill&gt; &lt;distinguishedName&gt; | "dn::" &lt;fill&gt; &lt;base64-distinguishedName&gt;
* &lt;changerecord&gt; ::= "changetype:" &lt;fill&gt; &lt;change-op&gt;
* </pre>
*
* @return the parsed ldifEntry
* @exception LdapException If the ldif file does not contain a valid entry
*/
protected LdifEntry parseEntry() throws LdapException
{
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 );
lineNumber -= ( lines.size() - 1 );
String name = parseDn( line );
Dn dn = null;
try
{
dn = new Dn( schemaManager, name );
}
catch ( LdapInvalidDnException lide )
{
// Deal with the RDN whihc is not in the schema
// First parse the DN without the schema
dn = new Dn( name );
Rdn rdn = dn.getRdn();
// Process each Ava
for ( Ava ava : rdn )
{
if ( ( schemaManager != null ) && ( schemaManager.getAttributeType( ava.getType() ) == null )
&& schemaManager.isRelaxed() )
{
// Not found : create a new one
MutableAttributeType newAttributeType = new MutableAttributeType( "1.3.6.1.4.1.18060.0.9999." + oidCounter++ );
newAttributeType.setNames( ava.getType() );
newAttributeType.setSyntax( schemaManager.getLdapSyntaxRegistry().get( SchemaConstants.DIRECTORY_STRING_SYNTAX ) );
schemaManager.add( newAttributeType );
}
}
dn = new Dn( schemaManager, name );
}
// Ok, we have found a Dn
LdifEntry entry = createLdifEntry( schemaManager );
entry.setLengthBeforeParsing( entryLen );
entry.setOffset( entryOffset );
entry.setDn( dn );
// We remove this dn from the lines
lines.remove( 0 );
// Now, let's iterate through the other lines
Iterator<String> iter = lines.iterator();
// This flag is used to distinguish between an entry and a change
int type = LDIF_ENTRY;
// 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;
Control control;
while ( iter.hasNext() )
{
lineNumber++;
// Each line could start either with an OID, an attribute type, with
// "control:" or with "changetype:"
line = iter.next();
lowerLine = Strings.toLowerCaseAscii( line );
// 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( I18n.err( I18n.ERR_12004_CHANGE_NOT_ALLOWED, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12005_NO_CHANGE ) );
}
containsChanges = true;
if ( controlSeen )
{
LOG.error( I18n.err( I18n.ERR_12050, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12051 ) );
}
// Parse the control
control = parseControl( line.substring( "control:".length() ) );
entry.addControl( control );
}
else if ( lowerLine.startsWith( "changetype:" ) )
{
if ( containsEntries )
{
LOG.error( I18n.err( I18n.ERR_12004_CHANGE_NOT_ALLOWED, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12005_NO_CHANGE ) );
}
containsChanges = true;
if ( changeTypeSeen )
{
LOG.error( I18n.err( I18n.ERR_12052, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12053 ) );
}
// 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( I18n.err( I18n.ERR_12004_CHANGE_NOT_ALLOWED, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12005_NO_CHANGE ) );
}
containsEntries = true;
if ( controlSeen || changeTypeSeen )
{
LOG.error( I18n.err( I18n.ERR_12054, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12055 ) );
}
parseAttributeValue( entry, line, lowerLine );
type = LDIF_ENTRY;
}
else
{
// Invalid attribute Value
LOG.error( I18n.err( I18n.ERR_12056, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12057_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( I18n.err( I18n.ERR_12058_UNKNOWN_ENTRY_TYPE, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12059_UNKNOWN_ENTRY ) );
}
return entry;
}
/**
* Parse the version from the ldif input.
*
* @return A number representing the version (default to 1)
* @throws LdapLdifException If the version is incorrect or if the input is incorrect
*/
protected int parseVersion() throws LdapLdifException
{
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 );
// <ldif-file> ::= "version:" <fill> <number>
char[] document = line.toCharArray();
String versionNumber;
if ( line.startsWith( "version:" ) )
{
position += "version:".length();
parseFill( document );
// Version number. Must be '1' in this version
versionNumber = parseNumber( document );
// We should not have any other chars after the number
if ( position != document.length )
{
LOG.error( I18n.err( I18n.ERR_12060_VERSION_NOT_A_NUMBER, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12061_LDIF_PARSING_ERROR ) );
}
try
{
ver = Integer.parseInt( versionNumber );
}
catch ( NumberFormatException nfe )
{
LOG.error( I18n.err( I18n.ERR_12060_VERSION_NOT_A_NUMBER, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12061_LDIF_PARSING_ERROR ), nfe );
}
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 )
{
// include the version line as part of the first entry
int tmpEntryLen = entryLen;
readLines();
entryLen += tmpEntryLen;
}
}
else
{
LOG.info( "No version information : assuming version: 1" );
}
return ver;
}
/**
* gets a line from the underlying data store
*
* @return a line of characters or null if EOF reached
* @throws IOException on read failure
*/
protected String getLine() throws IOException
{
return ( ( BufferedReader ) reader ).readLine();
}
/**
* Reads an entry in a ldif buffer, and returns the resulting lines, without
* comments, and unfolded.
*
* The lines represent *one* entry.
*
* @throws LdapLdifException If something went wrong
*/
protected void readLines() throws LdapLdifException
{
String line;
boolean insideComment = true;
boolean isFirstLine = true;
lines.clear();
entryLen = 0;
entryOffset = offset;
StringBuffer sb = new StringBuffer();
try
{
while ( ( line = getLine() ) != null )
{
lineNumber++;
if ( line.length() == 0 )
{
if ( isFirstLine )
{
continue;
}
else
{
// The line is empty, we have read an entry
insideComment = false;
offset++;
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( I18n.err( I18n.ERR_12062_EMPTY_CONTINUATION_LINE, lineNumber ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12061_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;
}
byte[] data = Strings.getBytesUtf8( line );
// FIXME might fail on windows in the new line issue, yet to check
offset += ( data.length + 1 );
entryLen += ( data.length + 1 );
}
}
catch ( IOException ioe )
{
throw new LdapLdifException( I18n.err( I18n.ERR_12063_ERROR_WHILE_READING_LDIF_LINE ), ioe );
}
// Stores the current line if necessary.
if ( sb.length() != 0 )
{
lines.add( sb.toString() );
}
}
/**
* Parse a ldif file (using the default encoding).
*
* @param fileName The ldif file
* @return A list of entries
* @throws LdapLdifException If the parsing fails
*/
public List<LdifEntry> parseLdifFile( String fileName ) throws LdapLdifException
{
return parseLdifFile( fileName, Charset.forName( Strings.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 LdapLdifException If the parsing fails
*/
public List<LdifEntry> parseLdifFile( String fileName, String encoding ) throws LdapLdifException
{
if ( Strings.isEmpty( fileName ) )
{
LOG.error( I18n.err( I18n.ERR_12064_EMPTY_FILE_NAME ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12064_EMPTY_FILE_NAME ) );
}
File file = new File( fileName );
if ( !file.exists() )
{
LOG.error( I18n.err( I18n.ERR_12066, fileName ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12067, fileName ) );
}
BufferedReader bufferReader = null;
// Open the file and then get a channel from the stream
try
{
bufferReader = new BufferedReader(
new InputStreamReader( new FileInputStream( file ), Charset.forName( encoding ) ) );
return parseLdif( bufferReader );
}
catch ( FileNotFoundException fnfe )
{
LOG.error( I18n.err( I18n.ERR_12068, fileName ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12067, fileName ), fnfe );
}
catch ( LdapException le )
{
throw new LdapLdifException( le.getMessage(), le );
}
finally
{
// close the reader
try
{
if ( bufferReader != null )
{
bufferReader.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 LdapLdifException If something went wrong
*/
public List<LdifEntry> parseLdif( String ldif ) throws LdapLdifException
{
LOG.debug( "Starts parsing ldif buffer" );
if ( Strings.isEmpty( ldif ) )
{
return new ArrayList<LdifEntry>();
}
BufferedReader bufferReader = new BufferedReader( new StringReader( ldif ) );
try
{
List<LdifEntry> entries = parseLdif( bufferReader );
if ( LOG.isDebugEnabled() )
{
LOG.debug( "Parsed {} entries.", Integer.valueOf( entries.size() ) );
}
return entries;
}
catch ( LdapLdifException ne )
{
LOG.error( I18n.err( I18n.ERR_12069, ne.getLocalizedMessage() ) );
throw new LdapLdifException( I18n.err( I18n.ERR_12070 ), ne );
}
catch ( LdapException le )
{
throw new LdapLdifException( le.getMessage(), le );
}
finally
{
// Close the reader
try
{
bufferReader.close();
}
catch ( IOException ioe )
{
throw new LdapLdifException( I18n.err( I18n.ERR_12024_CANNOT_CLOSE_FILE ), ioe );
}
}
}
// ------------------------------------------------------------------------
// Iterator Methods
// ------------------------------------------------------------------------
/**
* Gets the next LDIF on the channel.
*
* @return the next LDIF as a String.
*/
private LdifEntry nextInternal()
{
try
{
LOG.debug( "next(): -- called" );
LdifEntry entry = prefetched;
readLines();
try
{
prefetched = parseEntry();
}
catch ( LdapLdifException ne )
{
error = ne;
throw new NoSuchElementException( ne.getMessage() );
}
catch ( LdapException le )
{
throw new NoSuchElementException( le.getMessage() );
}
LOG.debug( "next(): -- returning ldif {}\n", entry );
return entry;
}
catch ( LdapLdifException ne )
{
LOG.error( I18n.err( I18n.ERR_12071 ) );
error = ne;
return null;
}
}
/**
* Gets the next LDIF on the channel.
*
* @return the next LDIF as a String.
*/
public LdifEntry next()
{
return nextInternal();
}
/**
* Gets the current entry, but don't move forward.
*
* @return the pre-fetched entry
*/
public LdifEntry fetch()
{
return prefetched;
}
/**
* 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()
{
if ( prefetched != null )
{
LOG.debug( "hasNext(): -- returning true" );
}
else
{
LOG.debug( "hasNext(): -- returning 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<LdifEntry> iterator()
{
return new Iterator<LdifEntry>()
{
public boolean hasNext()
{
return hasNextInternal();
}
public LdifEntry next()
{
try
{
return nextInternal();
}
catch ( NoSuchElementException nse )
{
LOG.error( nse.getMessage() );
return null;
}
}
public void remove()
{
throw new UnsupportedOperationException();
}
};
}
/**
* @return True if an error occurred 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 reader The buffer being processed
* @return A list of entries
* @throws LdapException If something went wrong
*/
public List<LdifEntry> parseLdif( BufferedReader reader ) throws LdapException
{
// Create a list that will contain the read entries
List<LdifEntry> entries = new ArrayList<LdifEntry>();
this.reader = reader;
// First get the version - if any -
version = parseVersion();
prefetched = parseEntry();
// When done, get the entries one by one.
for ( LdifEntry entry : this )
{
if ( entry != null )
{
entries.add( entry );
}
else
{
throw new LdapLdifException( I18n.err( I18n.ERR_12072, error.getLocalizedMessage() ) );
}
}
return entries;
}
/**
* @return True if the ldif file contains entries, fals if it contains changes
*/
public boolean containsEntries()
{
return containsEntries;
}
/**
* @return the current line that is being processed by the reader
*/
public int getLineNumber()
{
return lineNumber;
}
/**
* creates a non-schemaaware LdifEntry
* @return an LdifEntry that is not schemaaware
*/
protected LdifEntry createLdifEntry( SchemaManager schemaManager )
{
if ( schemaManager != null )
{
return new LdifEntry( schemaManager );
}
else
{
return new LdifEntry();
}
}
/**
* @return true if the DN validation is turned on
*/
public boolean isValidateDn()
{
return validateDn;
}
/**
* Turns on/off the DN validation
*
* @param validateDn the boolean flag
*/
public void setValidateDn( boolean validateDn )
{
this.validateDn = validateDn;
}
/**
* @param schemaManager the schemaManager to set
*/
public void setSchemaManager( SchemaManager schemaManager )
{
this.schemaManager = schemaManager;
}
/**
* {@inheritDoc}
*/
public void close() throws IOException
{
if ( reader != null )
{
position = 0;
reader.close();
containsEntries = false;
containsChanges = false;
offset = 0;
entryOffset = 0;
lineNumber = 0;
}
}
}