All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.ldif.LDIFReader Maven / Gradle / Ivy

Go to download

The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use Java API for communicating with LDAP directory servers and performing related tasks like reading and writing LDIF, encoding and decoding data using base64 and ASN.1 BER, and performing secure communication. This package contains the Standard Edition of the LDAP SDK, which is a complete, general-purpose library for communicating with LDAPv3 directory servers.

There is a newer version: 7.0.1
Show newest version
/*
 * Copyright 2007-2023 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2007-2023 Ping Identity Corporation
 *
 * Licensed 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.
 */
/*
 * Copyright (C) 2007-2023 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.ldif;



import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.nio.charset.Charset;

import com.unboundid.asn1.ASN1OctetString;
import com.unboundid.ldap.matchingrules.CaseIgnoreStringMatchingRule;
import com.unboundid.ldap.matchingrules.MatchingRule;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.Modification;
import com.unboundid.ldap.sdk.ModificationType;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
import com.unboundid.ldap.sdk.schema.Schema;
import com.unboundid.util.AggregateInputStream;
import com.unboundid.util.Base64;
import com.unboundid.util.Debug;
import com.unboundid.util.LDAPSDKThreadFactory;
import com.unboundid.util.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.Validator;
import com.unboundid.util.parallel.AsynchronousParallelProcessor;
import com.unboundid.util.parallel.Result;
import com.unboundid.util.parallel.ParallelProcessor;
import com.unboundid.util.parallel.Processor;

import static com.unboundid.ldif.LDIFMessages.*;

/**
 * This class provides an LDIF reader, which can be used to read and decode
 * entries and change records from a data source using the LDAP Data Interchange
 * Format as per RFC 2849.
 * 
* This class is not synchronized. If multiple threads read from the * LDIFReader, they must be synchronized externally. *

*

Example

* The following example iterates through all entries contained in an LDIF file * and attempts to add them to a directory server: *
 * LDIFReader ldifReader = new LDIFReader(pathToLDIFFile);
 *
 * int entriesRead = 0;
 * int entriesAdded = 0;
 * int errorsEncountered = 0;
 * while (true)
 * {
 *   Entry entry;
 *   try
 *   {
 *     entry = ldifReader.readEntry();
 *     if (entry == null)
 *     {
 *       // All entries have been read.
 *       break;
 *     }
 *
 *     entriesRead++;
 *   }
 *   catch (LDIFException le)
 *   {
 *     errorsEncountered++;
 *     if (le.mayContinueReading())
 *     {
 *       // A recoverable error occurred while attempting to read a change
 *       // record, at or near line number le.getLineNumber()
 *       // The entry will be skipped, but we'll try to keep reading from the
 *       // LDIF file.
 *       continue;
 *     }
 *     else
 *     {
 *       // An unrecoverable error occurred while attempting to read an entry
 *       // at or near line number le.getLineNumber()
 *       // No further LDIF processing will be performed.
 *       break;
 *     }
 *   }
 *   catch (IOException ioe)
 *   {
 *     // An I/O error occurred while attempting to read from the LDIF file.
 *     // No further LDIF processing will be performed.
 *     errorsEncountered++;
 *     break;
 *   }
 *
 *   LDAPResult addResult;
 *   try
 *   {
 *     addResult = connection.add(entry);
 *     // If we got here, then the change should have been processed
 *     // successfully.
 *     entriesAdded++;
 *   }
 *   catch (LDAPException le)
 *   {
 *     // If we got here, then the change attempt failed.
 *     addResult = le.toLDAPResult();
 *     errorsEncountered++;
 *   }
 * }
 *
 * ldifReader.close();
 * 
*/ @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) public final class LDIFReader implements Closeable { /** * The default buffer size (128KB) that will be used when reading from the * data source. */ public static final int DEFAULT_BUFFER_SIZE = 128 * 1024; /* * When processing asynchronously, this determines how many of the allocated * worker threads are used to parse each batch of read entries. */ private static final int ASYNC_MIN_PER_PARSING_THREAD = 3; /** * When processing asynchronously, this specifies the size of the pending and * completed queues. */ private static final int ASYNC_QUEUE_SIZE = 500; /** * Special entry used internally to signal that the LDIFReaderEntryTranslator * has signalled that a read Entry should be skipped by returning null, * which normally implies EOF. */ @NotNull private static final Entry SKIP_ENTRY = new Entry("cn=skipped"); /** * The default base path that will be prepended to relative paths. It will * end with a trailing slash. */ @NotNull private static final String DEFAULT_RELATIVE_BASE_PATH; static { final File currentDir; final String currentDirString = StaticUtils.getSystemProperty("user.dir"); if (currentDirString == null) { currentDir = new File("."); } else { currentDir = new File(currentDirString); } final String currentDirAbsolutePath = currentDir.getAbsolutePath(); if (currentDirAbsolutePath.endsWith(File.separator)) { DEFAULT_RELATIVE_BASE_PATH = currentDirAbsolutePath; } else { DEFAULT_RELATIVE_BASE_PATH = currentDirAbsolutePath + File.separator; } } /** * A flag that indicates whether to support LDIF records that include * controls. If this is {@code true} (which is the default, unless the * {@code com.unboundid.ldif.LDIFReader.disableControlSupport} system property * is defined and set to a value of {@code true}), then a change record in * which the line immediately after the DN starts with "control" will be * interpreted as a control specification. */ @NotNull private static final AtomicBoolean SUPPORT_CONTROLS; static { final String propertyName = LDIFReader.class.getName() + ".disableControlSupport"; final String disableControlSupportPropertyValue = StaticUtils.getSystemProperty(propertyName); if ((disableControlSupportPropertyValue != null) && (disableControlSupportPropertyValue.equalsIgnoreCase("true"))) { SUPPORT_CONTROLS = new AtomicBoolean(false); } else { SUPPORT_CONTROLS = new AtomicBoolean(true); } } /** * The maximum allowed file size that we will attempt to read from a file * referenced by a URL. A default size limit of 10 megabytes will be imposed * unless overridden by the * {@code com.unboundid.ldif.LDIFReader.maxURLFileSizeBytes} system property. */ private static final long MAX_URL_FILE_SIZE; static { // Use a default size of 10 megabytes, but allow it to be o long maxURLFileSize = 10L * 1024L * 1024L; final String propertyName = LDIFReader.class.getName() + ".maxURLFileSizeBytes"; final String propertyValue = StaticUtils.getSystemProperty(propertyName); if (propertyValue != null) { try { maxURLFileSize = Long.parseLong(propertyValue); } catch (final Exception e) { Debug.debugException(e); } } MAX_URL_FILE_SIZE = maxURLFileSize; } // The buffered reader that will be used to read LDIF data. @NotNull private final BufferedReader reader; // The behavior that should be exhibited when encountering duplicate attribute // values. @NotNull private volatile DuplicateValueBehavior duplicateValueBehavior; // A line number counter. private long lineNumberCounter = 0; // The change record translator to use, if any. @Nullable private final LDIFReaderChangeRecordTranslator changeRecordTranslator; // The entry translator to use, if any. @Nullable private final LDIFReaderEntryTranslator entryTranslator; // The schema that will be used when processing, if applicable. @Nullable private Schema schema; // Specifies the base path that will be prepended to relative paths for file // URLs. @NotNull private volatile String relativeBasePath; // The behavior that should be exhibited with regard to illegal trailing // spaces in attribute values. @NotNull private volatile TrailingSpaceBehavior trailingSpaceBehavior; // True iff we are processing asynchronously. private final boolean isAsync; // // The following only apply to asynchronous processing. // // Parses entries asynchronously. @Nullable private final AsynchronousParallelProcessor asyncParser; // Set to true when the end of the input is reached. @Nullable private final AtomicBoolean asyncParsingComplete; // The records that have been read and parsed. @Nullable private final BlockingQueue> asyncParsedRecords; /** * Creates a new LDIF reader that will read data from the specified file. * * @param path The path to the file from which the data is to be read. It * must not be {@code null}. * * @throws IOException If a problem occurs while opening the file for * reading. */ public LDIFReader(@NotNull final String path) throws IOException { this(new FileInputStream(path)); } /** * Creates a new LDIF reader that will read data from the specified file * and parses the LDIF records asynchronously using the specified number of * threads. * * @param path The path to the file from which the data is to be read. It * must not be {@code null}. * @param numParseThreads If this value is greater than zero, then the * specified number of threads will be used to * asynchronously read and parse the LDIF file. * * @throws IOException If a problem occurs while opening the file for * reading. * * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator) * constructor for more details about asynchronous processing. */ public LDIFReader(@NotNull final String path, final int numParseThreads) throws IOException { this(new FileInputStream(path), numParseThreads); } /** * Creates a new LDIF reader that will read data from the specified file. * * @param file The file from which the data is to be read. It must not be * {@code null}. * * @throws IOException If a problem occurs while opening the file for * reading. */ public LDIFReader(@NotNull final File file) throws IOException { this(new FileInputStream(file)); } /** * Creates a new LDIF reader that will read data from the specified file * and optionally parses the LDIF records asynchronously using the specified * number of threads. * * @param file The file from which the data is to be read. It * must not be {@code null}. * @param numParseThreads If this value is greater than zero, then the * specified number of threads will be used to * asynchronously read and parse the LDIF file. * * @throws IOException If a problem occurs while opening the file for * reading. */ public LDIFReader(@NotNull final File file, final int numParseThreads) throws IOException { this(new FileInputStream(file), numParseThreads); } /** * Creates a new LDIF reader that will read data from the specified files in * the order in which they are provided and optionally parses the LDIF records * asynchronously using the specified number of threads. * * @param files The files from which the data is to be read. It * must not be {@code null} or empty. * @param numParseThreads If this value is greater than zero, then the * specified number of threads will be used to * asynchronously read and parse the LDIF file. * @param entryTranslator The LDIFReaderEntryTranslator to apply to entries * before they are returned. This is normally * {@code null}, which causes entries to be returned * unaltered. This is particularly useful when * parsing the input file in parallel because the * entry translation is also done in parallel. * * @throws IOException If a problem occurs while opening the file for * reading. */ public LDIFReader(@NotNull final File[] files, final int numParseThreads, @Nullable final LDIFReaderEntryTranslator entryTranslator) throws IOException { this(files, numParseThreads, entryTranslator, null); } /** * Creates a new LDIF reader that will read data from the specified files in * the order in which they are provided and optionally parses the LDIF records * asynchronously using the specified number of threads. * * @param files The files from which the data is to be * read. It must not be {@code null} or * empty. * @param numParseThreads If this value is greater than zero, then * the specified number of threads will be * used to asynchronously read and parse the * LDIF file. * @param entryTranslator The LDIFReaderEntryTranslator to apply to * entries before they are returned. This is * normally {@code null}, which causes entries * to be returned unaltered. This is * particularly useful when parsing the input * file in parallel because the entry * translation is also done in parallel. * @param changeRecordTranslator The LDIFReaderChangeRecordTranslator to * apply to change records before they are * returned. This is normally {@code null}, * which causes change records to be returned * unaltered. This is particularly useful * when parsing the input file in parallel * because the change record translation is * also done in parallel. * * @throws IOException If a problem occurs while opening the file for * reading. */ public LDIFReader(@NotNull final File[] files, final int numParseThreads, @Nullable final LDIFReaderEntryTranslator entryTranslator, @Nullable final LDIFReaderChangeRecordTranslator changeRecordTranslator) throws IOException { this(files, numParseThreads, entryTranslator, changeRecordTranslator, "UTF-8"); } /** * Creates a new LDIF reader that will read data from the specified files in * the order in which they are provided and optionally parses the LDIF records * asynchronously using the specified number of threads. * * @param files The files from which the data is to be * read. It must not be {@code null} or * empty. * @param numParseThreads If this value is greater than zero, then * the specified number of threads will be * used to asynchronously read and parse the * LDIF file. * @param entryTranslator The LDIFReaderEntryTranslator to apply to * entries before they are returned. This is * normally {@code null}, which causes entries * to be returned unaltered. This is * particularly useful when parsing the input * file in parallel because the entry * translation is also done in parallel. * @param changeRecordTranslator The LDIFReaderChangeRecordTranslator to * apply to change records before they are * returned. This is normally {@code null}, * which causes change records to be returned * unaltered. This is particularly useful * when parsing the input file in parallel * because the change record translation is * also done in parallel. * @param characterSet The character set to use when reading from * the input stream. It must not be * {@code null}. * * @throws IOException If a problem occurs while opening the file for * reading. */ public LDIFReader(@NotNull final File[] files, final int numParseThreads, @Nullable final LDIFReaderEntryTranslator entryTranslator, @Nullable final LDIFReaderChangeRecordTranslator changeRecordTranslator, @NotNull final String characterSet) throws IOException { this(createAggregateInputStream(files), numParseThreads, entryTranslator, changeRecordTranslator, characterSet); } /** * Creates a new aggregate input stream that will read data from the specified * files. If there are multiple files, then a "padding" file will be inserted * between them to ensure that there is at least one blank line between the * end of one file and the beginning of another. * * @param files The files from which the data is to be read. It must not be * {@code null} or empty. * * @return The input stream to use to read data from the provided files. * * @throws IOException If a problem is encountered while attempting to * create the input stream. */ @NotNull() private static InputStream createAggregateInputStream( @NotNull final File... files) throws IOException { if (files.length == 0) { throw new IOException(ERR_READ_NO_LDIF_FILES.get()); } else { return new AggregateInputStream(true, files); } } /** * Creates a new LDIF reader that will read data from the provided input * stream. * * @param inputStream The input stream from which the data is to be read. * It must not be {@code null}. */ public LDIFReader(@NotNull final InputStream inputStream) { this(inputStream, 0); } /** * Creates a new LDIF reader that will read data from the specified stream * and parses the LDIF records asynchronously using the specified number of * threads. * * @param inputStream The input stream from which the data is to be read. * It must not be {@code null}. * @param numParseThreads If this value is greater than zero, then the * specified number of threads will be used to * asynchronously read and parse the LDIF file. * * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator) * constructor for more details about asynchronous processing. */ public LDIFReader(@NotNull final InputStream inputStream, final int numParseThreads) { // UTF-8 is required by RFC 2849. Java guarantees it's always available. this(new BufferedReader( new InputStreamReader(inputStream, StandardCharsets.UTF_8), DEFAULT_BUFFER_SIZE), numParseThreads); } /** * Creates a new LDIF reader that will read data from the specified stream * and parses the LDIF records asynchronously using the specified number of * threads. * * @param inputStream The input stream from which the data is to be read. * It must not be {@code null}. * @param numParseThreads If this value is greater than zero, then the * specified number of threads will be used to * asynchronously read and parse the LDIF file. * @param entryTranslator The LDIFReaderEntryTranslator to apply to read * entries before they are returned. This is normally * {@code null}, which causes entries to be returned * unaltered. This is particularly useful when parsing * the input file in parallel because the entry * translation is also done in parallel. * * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator) * constructor for more details about asynchronous processing. */ public LDIFReader(@NotNull final InputStream inputStream, final int numParseThreads, @Nullable final LDIFReaderEntryTranslator entryTranslator) { this(inputStream, numParseThreads, entryTranslator, null); } /** * Creates a new LDIF reader that will read data from the specified stream * and parses the LDIF records asynchronously using the specified number of * threads. * * @param inputStream The input stream from which the data is to * be read. It must not be {@code null}. * @param numParseThreads If this value is greater than zero, then * the specified number of threads will be * used to asynchronously read and parse the * LDIF file. * @param entryTranslator The LDIFReaderEntryTranslator to apply to * entries before they are returned. This is * normally {@code null}, which causes entries * to be returned unaltered. This is * particularly useful when parsing the input * file in parallel because the entry * translation is also done in parallel. * @param changeRecordTranslator The LDIFReaderChangeRecordTranslator to * apply to change records before they are * returned. This is normally {@code null}, * which causes change records to be returned * unaltered. This is particularly useful * when parsing the input file in parallel * because the change record translation is * also done in parallel. * * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator) * constructor for more details about asynchronous processing. */ public LDIFReader(@NotNull final InputStream inputStream, final int numParseThreads, @Nullable final LDIFReaderEntryTranslator entryTranslator, @Nullable final LDIFReaderChangeRecordTranslator changeRecordTranslator) { // UTF-8 is required by RFC 2849. Java guarantees it's always available. this(inputStream, numParseThreads, entryTranslator, changeRecordTranslator, "UTF-8"); } /** * Creates a new LDIF reader that will read data from the specified stream * and parses the LDIF records asynchronously using the specified number of * threads. * * @param inputStream The input stream from which the data is to * be read. It must not be {@code null}. * @param numParseThreads If this value is greater than zero, then * the specified number of threads will be * used to asynchronously read and parse the * LDIF file. * @param entryTranslator The LDIFReaderEntryTranslator to apply to * entries before they are returned. This is * normally {@code null}, which causes entries * to be returned unaltered. This is * particularly useful when parsing the input * file in parallel because the entry * translation is also done in parallel. * @param changeRecordTranslator The LDIFReaderChangeRecordTranslator to * apply to change records before they are * returned. This is normally {@code null}, * which causes change records to be returned * unaltered. This is particularly useful * when parsing the input file in parallel * because the change record translation is * also done in parallel. * @param characterSet The character set to use when reading from * the input stream. It must not be * {@code null}. * * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator) * constructor for more details about asynchronous processing. */ public LDIFReader(@NotNull final InputStream inputStream, final int numParseThreads, @Nullable final LDIFReaderEntryTranslator entryTranslator, @Nullable final LDIFReaderChangeRecordTranslator changeRecordTranslator, @NotNull final String characterSet) { this(new BufferedReader( new InputStreamReader(inputStream, Charset.forName(characterSet)), DEFAULT_BUFFER_SIZE), numParseThreads, entryTranslator, changeRecordTranslator); } /** * Creates a new LDIF reader that will use the provided buffered reader to * read the LDIF data. The encoding of the underlying Reader must be set to * "UTF-8" as required by RFC 2849. * * @param reader The buffered reader that will be used to read the LDIF * data. It must not be {@code null}. */ public LDIFReader(@NotNull final BufferedReader reader) { this(reader, 0); } /** * Creates a new LDIF reader that will read data from the specified buffered * reader and parses the LDIF records asynchronously using the specified * number of threads. The encoding of the underlying Reader must be set to * "UTF-8" as required by RFC 2849. * * @param reader The buffered reader that will be used to read the LDIF data. * It must not be {@code null}. * @param numParseThreads If this value is greater than zero, then the * specified number of threads will be used to * asynchronously read and parse the LDIF file. * * @see #LDIFReader(BufferedReader, int, LDIFReaderEntryTranslator) * constructor for more details about asynchronous processing. */ public LDIFReader(@NotNull final BufferedReader reader, final int numParseThreads) { this(reader, numParseThreads, null); } /** * Creates a new LDIF reader that will read data from the specified buffered * reader and parses the LDIF records asynchronously using the specified * number of threads. The encoding of the underlying Reader must be set to * "UTF-8" as required by RFC 2849. * * @param reader The buffered reader that will be used to read the LDIF data. * It must not be {@code null}. * @param numParseThreads If this value is greater than zero, then the * specified number of threads will be used to * asynchronously read and parse the LDIF file. * This should only be set to greater than zero when * performance analysis has demonstrated that reading * and parsing the LDIF is a bottleneck. The default * synchronous processing is normally fast enough. * There is little benefit in passing in a value * greater than four (unless there is an * LDIFReaderEntryTranslator that does time-consuming * processing). A value of zero implies the * default behavior of reading and parsing LDIF * records synchronously when one of the read * methods is called. * @param entryTranslator The LDIFReaderEntryTranslator to apply to read * entries before they are returned. This is normally * {@code null}, which causes entries to be returned * unaltered. This is particularly useful when parsing * the input file in parallel because the entry * translation is also done in parallel. */ public LDIFReader(@NotNull final BufferedReader reader, final int numParseThreads, @Nullable final LDIFReaderEntryTranslator entryTranslator) { this(reader, numParseThreads, entryTranslator, null); } /** * Creates a new LDIF reader that will read data from the specified buffered * reader and parses the LDIF records asynchronously using the specified * number of threads. The encoding of the underlying Reader must be set to * "UTF-8" as required by RFC 2849. * * @param reader The buffered reader that will be used to * read the LDIF data. It must not be * {@code null}. * @param numParseThreads If this value is greater than zero, then * the specified number of threads will be * used to asynchronously read and parse the * LDIF file. * @param entryTranslator The LDIFReaderEntryTranslator to apply to * entries before they are returned. This is * normally {@code null}, which causes entries * to be returned unaltered. This is * particularly useful when parsing the input * file in parallel because the entry * translation is also done in parallel. * @param changeRecordTranslator The LDIFReaderChangeRecordTranslator to * apply to change records before they are * returned. This is normally {@code null}, * which causes change records to be returned * unaltered. This is particularly useful * when parsing the input file in parallel * because the change record translation is * also done in parallel. */ public LDIFReader(@NotNull final BufferedReader reader, final int numParseThreads, @Nullable final LDIFReaderEntryTranslator entryTranslator, @Nullable final LDIFReaderChangeRecordTranslator changeRecordTranslator) { Validator.ensureNotNull(reader); Validator.ensureTrue(numParseThreads >= 0, "LDIFReader.numParseThreads must not be negative."); this.reader = reader; this.entryTranslator = entryTranslator; this.changeRecordTranslator = changeRecordTranslator; duplicateValueBehavior = DuplicateValueBehavior.STRIP; trailingSpaceBehavior = TrailingSpaceBehavior.REJECT; relativeBasePath = DEFAULT_RELATIVE_BASE_PATH; if (numParseThreads == 0) { isAsync = false; asyncParser = null; asyncParsingComplete = null; asyncParsedRecords = null; } else { isAsync = true; asyncParsingComplete = new AtomicBoolean(false); // Decodes entries in parallel. final LDAPSDKThreadFactory threadFactory = new LDAPSDKThreadFactory("LDIFReader Worker", true, null); final ParallelProcessor parallelParser = new ParallelProcessor<>( new RecordParser(), threadFactory, numParseThreads, ASYNC_MIN_PER_PARSING_THREAD); final BlockingQueue pendingQueue = new ArrayBlockingQueue<>(ASYNC_QUEUE_SIZE); // The output queue must be a little more than twice as big as the input // queue to more easily handle being shutdown in the middle of processing // when the queues are full and threads are blocked. asyncParsedRecords = new ArrayBlockingQueue<>(2 * ASYNC_QUEUE_SIZE + 100); asyncParser = new AsynchronousParallelProcessor<>(pendingQueue, parallelParser, asyncParsedRecords); final LineReaderThread lineReaderThread = new LineReaderThread(); lineReaderThread.start(); } } /** * Reads entries from the LDIF file with the specified path and returns them * as a {@code List}. This is a convenience method that should only be used * for data sets that are small enough so that running out of memory isn't a * concern. * * @param path The path to the LDIF file containing the entries to be read. * * @return A list of the entries read from the given LDIF file. * * @throws IOException If a problem occurs while attempting to read data * from the specified file. * * @throws LDIFException If a problem is encountered while attempting to * decode data read as LDIF. */ @NotNull() public static List readEntries(@NotNull final String path) throws IOException, LDIFException { return readEntries(new LDIFReader(path)); } /** * Reads entries from the specified LDIF file and returns them as a * {@code List}. This is a convenience method that should only be used for * data sets that are small enough so that running out of memory isn't a * concern. * * @param file A reference to the LDIF file containing the entries to be * read. * * @return A list of the entries read from the given LDIF file. * * @throws IOException If a problem occurs while attempting to read data * from the specified file. * * @throws LDIFException If a problem is encountered while attempting to * decode data read as LDIF. */ @NotNull() public static List readEntries(@NotNull final File file) throws IOException, LDIFException { return readEntries(new LDIFReader(file)); } /** * Reads and decodes LDIF entries from the provided input stream and * returns them as a {@code List}. This is a convenience method that should * only be used for data sets that are small enough so that running out of * memory isn't a concern. * * @param inputStream The input stream from which the entries should be * read. The input stream will be closed before * returning. * * @return A list of the entries read from the given input stream. * * @throws IOException If a problem occurs while attempting to read data * from the input stream. * * @throws LDIFException If a problem is encountered while attempting to * decode data read as LDIF. */ @NotNull() public static List readEntries(@NotNull final InputStream inputStream) throws IOException, LDIFException { return readEntries(new LDIFReader(inputStream)); } /** * Reads entries from the provided LDIF reader and returns them as a list. * * @param reader The reader from which the entries should be read. It will * be closed before returning. * * @return A list of the entries read from the provided reader. * * @throws IOException If a problem was encountered while attempting to read * data from the LDIF data source. * * @throws LDIFException If a problem is encountered while attempting to * decode data read as LDIF. */ @NotNull() private static List readEntries(@NotNull final LDIFReader reader) throws IOException, LDIFException { try { final ArrayList entries = new ArrayList<>(10); while (true) { final Entry e = reader.readEntry(); if (e == null) { break; } entries.add(e); } return entries; } finally { reader.close(); } } /** * Closes this LDIF reader and the underlying LDIF source. * * @throws IOException If a problem occurs while closing the underlying LDIF * source. */ @Override() public void close() throws IOException { reader.close(); if (isAsync()) { // Closing the reader will trigger the LineReaderThread to complete, but // not if it's blocked submitting the next UnparsedLDIFRecord. To avoid // this, we clear out the completed output queue, which is larger than // the input queue, so the LineReaderThread will stop reading and // shutdown the asyncParser. asyncParsedRecords.clear(); } } /** * Indicates whether to ignore any duplicate values encountered while reading * LDIF records. * * @return {@code true} if duplicate values should be ignored, or * {@code false} if any LDIF records containing duplicate values * should be rejected. * * @deprecated Use the {@link #getDuplicateValueBehavior} method instead. */ @Deprecated() public boolean ignoreDuplicateValues() { return (duplicateValueBehavior == DuplicateValueBehavior.STRIP); } /** * Specifies whether to ignore any duplicate values encountered while reading * LDIF records. * * @param ignoreDuplicateValues Indicates whether to ignore duplicate * attribute values encountered while reading * LDIF records. * * @deprecated Use the {@link #setDuplicateValueBehavior} method instead. */ @Deprecated() public void setIgnoreDuplicateValues(final boolean ignoreDuplicateValues) { if (ignoreDuplicateValues) { duplicateValueBehavior = DuplicateValueBehavior.STRIP; } else { duplicateValueBehavior = DuplicateValueBehavior.REJECT; } } /** * Retrieves the behavior that should be exhibited if the LDIF reader * encounters an entry with duplicate values. * * @return The behavior that should be exhibited if the LDIF reader * encounters an entry with duplicate values. */ @NotNull() public DuplicateValueBehavior getDuplicateValueBehavior() { return duplicateValueBehavior; } /** * Specifies the behavior that should be exhibited if the LDIF reader * encounters an entry with duplicate values. * * @param duplicateValueBehavior The behavior that should be exhibited if * the LDIF reader encounters an entry with * duplicate values. */ public void setDuplicateValueBehavior( @NotNull final DuplicateValueBehavior duplicateValueBehavior) { this.duplicateValueBehavior = duplicateValueBehavior; } /** * Indicates whether to strip off any illegal trailing spaces that may appear * in LDIF records (e.g., after an entry DN or attribute value). The LDIF * specification strongly recommends that any value which legitimately * contains trailing spaces be base64-encoded, and any spaces which appear * after the end of non-base64-encoded values may therefore be considered * invalid. If any such trailing spaces are encountered in an LDIF record and * they are not to be stripped, then an {@link LDIFException} will be thrown * for that record. *

* Note that this applies only to spaces after the end of a value, and not to * spaces which may appear at the end of a line for a value that is wrapped * and continued on the next line. * * @return {@code true} if illegal trailing spaces should be stripped off, or * {@code false} if LDIF records containing illegal trailing spaces * should be rejected. * * @deprecated Use the {@link #getTrailingSpaceBehavior} method instead. */ @Deprecated() public boolean stripTrailingSpaces() { return (trailingSpaceBehavior == TrailingSpaceBehavior.STRIP); } /** * Specifies whether to strip off any illegal trailing spaces that may appear * in LDIF records (e.g., after an entry DN or attribute value). The LDIF * specification strongly recommends that any value which legitimately * contains trailing spaces be base64-encoded, and any spaces which appear * after the end of non-base64-encoded values may therefore be considered * invalid. If any such trailing spaces are encountered in an LDIF record and * they are not to be stripped, then an {@link LDIFException} will be thrown * for that record. *

* Note that this applies only to spaces after the end of a value, and not to * spaces which may appear at the end of a line for a value that is wrapped * and continued on the next line. * * @param stripTrailingSpaces Indicates whether to strip off any illegal * trailing spaces, or {@code false} if LDIF * records containing them should be rejected. * * @deprecated Use the {@link #setTrailingSpaceBehavior} method instead. */ @Deprecated() public void setStripTrailingSpaces(final boolean stripTrailingSpaces) { trailingSpaceBehavior = stripTrailingSpaces ? TrailingSpaceBehavior.STRIP : TrailingSpaceBehavior.REJECT; } /** * Retrieves the behavior that should be exhibited when encountering attribute * values which are not base64-encoded but contain trailing spaces. The LDIF * specification strongly recommends that any value which legitimately * contains trailing spaces be base64-encoded, but the LDAP SDK LDIF parser * may be configured to automatically strip these spaces, to preserve them, or * to reject any entry or change record containing them. * * @return The behavior that should be exhibited when encountering attribute * values which are not base64-encoded but contain trailing spaces. */ @NotNull() public TrailingSpaceBehavior getTrailingSpaceBehavior() { return trailingSpaceBehavior; } /** * Specifies the behavior that should be exhibited when encountering attribute * values which are not base64-encoded but contain trailing spaces. The LDIF * specification strongly recommends that any value which legitimately * contains trailing spaces be base64-encoded, but the LDAP SDK LDIF parser * may be configured to automatically strip these spaces, to preserve them, or * to reject any entry or change record containing them. * * @param trailingSpaceBehavior The behavior that should be exhibited when * encountering attribute values which are not * base64-encoded but contain trailing spaces. */ public void setTrailingSpaceBehavior( @NotNull final TrailingSpaceBehavior trailingSpaceBehavior) { this.trailingSpaceBehavior = trailingSpaceBehavior; } /** * Indicates whether the LDIF reader will attempt to handle LDAP controls * contained in LDIF records. See the documentation for the * {@link #setSupportControls} method for a more complete explanation. * * @return {@code true} if the LDIF reader will attempt to handle controls * contained in LDIF records, or {@code false} if it will not and * they will be treated as regular attributes. */ public static boolean supportControls() { return SUPPORT_CONTROLS.get(); } /** * Specifies whether the LDIF reader will attempt to handle LDAP controls * contained in LDIF records. By default, the LDAP SDK will handle controls * in accordance with RFC 2849, so that if the first unwrapped line after the * DN starts with "control:", then that record will be assumed to be an LDIF * change record that contains one or more LDAP controls. However, this can * potentially cause a problem in one corner case: the case in which an LDIF * record represents an entry in which the first attribute in the entry is * named "control", and when you're either reading a generic LDIF record or * you're reading an LDIF change record with {@code defaultAdd} set to * {@code true}. In such case, the LDIF reader would assume that the * "control" attribute is trying to specify an LDAP control, and it would * either throw an exception if it can't parse the value as a control, or it * would return an LDIF add change record without the "control" attribute but * with an LDAP control. Calling this method with a value of {@code false} * will disable the LDIF reader's support for controls so that any record in * which the unwrapped line immediately following the DN starts with * "control:" will be treated as specifying an attribute named "control" * rather than an LDAP control. * * @param supportControls Specifies whether the LDIF reader will attempt to * handle LDAP controls contained in LDIF records. */ public static void setSupportControls(final boolean supportControls) { SUPPORT_CONTROLS.set(supportControls); } /** * Retrieves the base path that will be prepended to relative paths in order * to obtain an absolute path. This will only be used for "file:" URLs that * have paths which do not begin with a slash. * * @return The base path that will be prepended to relative paths in order to * obtain an absolute path. */ @NotNull() public String getRelativeBasePath() { return relativeBasePath; } /** * Specifies the base path that will be prepended to relative paths in order * to obtain an absolute path. This will only be used for "file:" URLs that * have paths which do not begin with a space. * * @param relativeBasePath The base path that will be prepended to relative * paths in order to obtain an absolute path. */ public void setRelativeBasePath(@NotNull final String relativeBasePath) { setRelativeBasePath(new File(relativeBasePath)); } /** * Specifies the base path that will be prepended to relative paths in order * to obtain an absolute path. This will only be used for "file:" URLs that * have paths which do not begin with a space. * * @param relativeBasePath The base path that will be prepended to relative * paths in order to obtain an absolute path. */ public void setRelativeBasePath(@NotNull final File relativeBasePath) { final String path = relativeBasePath.getAbsolutePath(); if (path.endsWith(File.separator)) { this.relativeBasePath = path; } else { this.relativeBasePath = path + File.separator; } } /** * Retrieves the schema that will be used when reading LDIF records, if * defined. * * @return The schema that will be used when reading LDIF records, or * {@code null} if no schema should be used and all attributes should * be treated as case-insensitive strings. */ @Nullable() public Schema getSchema() { return schema; } /** * Specifies the schema that should be used when reading LDIF records. * * @param schema The schema that should be used when reading LDIF records, * or {@code null} if no schema should be used and all * attributes should be treated as case-insensitive strings. */ public void setSchema(@Nullable final Schema schema) { this.schema = schema; } /** * Reads a record from the LDIF source. It may be either an entry or an LDIF * change record. * * @return The record read from the LDIF source, or {@code null} if there are * no more entries to be read. * * @throws IOException If a problem occurs while trying to read from the * LDIF source. * * @throws LDIFException If the data read could not be parsed as an entry or * an LDIF change record. */ @Nullable() public LDIFRecord readLDIFRecord() throws IOException, LDIFException { if (isAsync()) { return readLDIFRecordAsync(); } else { return readLDIFRecordInternal(); } } /** * Decodes the provided set of lines as an LDIF record. It may be either an * entry or an LDIF change record. * * @param ldifLines The set of lines that comprise the LDIF representation * of the entry or change record. It must not be * {@code null} or empty. * * @return The decoded LDIF entry or change record. * * @throws LDIFException If the provided LDIF data cannot be decoded as an * LDIF record. */ @NotNull() public static LDIFRecord decodeLDIFRecord(@NotNull final String... ldifLines) throws LDIFException { return decodeLDIFRecord(DuplicateValueBehavior.STRIP, TrailingSpaceBehavior.REJECT, null, ldifLines); } /** * Decodes the provided set of lines as an LDIF record. It may be either an * entry or an LDIF change record. * * @param duplicateValueBehavior The behavior that should be exhibited when * encountering LDIF records that have * duplicate attribute values. It must not be * {@code null}. * @param trailingSpaceBehavior The behavior that should be exhibited when * encountering attribute values which are not * base64-encoded but contain trailing spaces. * It must not be {@code null}. * @param schema The schema to use when processing the * change record, or {@code null} if no schema * should be used and all values should be * treated as case-insensitive strings. * @param ldifLines The set of lines that comprise the LDIF * representation of the entry or change * record. It must not be {@code null} or * empty. * * @return The decoded LDIF entry or change record. * * @throws LDIFException If the provided LDIF data cannot be decoded as an * LDIF record. */ @NotNull() public static LDIFRecord decodeLDIFRecord( @NotNull final DuplicateValueBehavior duplicateValueBehavior, @NotNull final TrailingSpaceBehavior trailingSpaceBehavior, @Nullable final Schema schema, @NotNull final String... ldifLines) throws LDIFException { final UnparsedLDIFRecord unparsedRecord = prepareRecord( duplicateValueBehavior, trailingSpaceBehavior, schema, ldifLines); return decodeRecord(unparsedRecord, DEFAULT_RELATIVE_BASE_PATH, schema); } /** * Reads an entry from the LDIF source. * * @return The entry read from the LDIF source, or {@code null} if there are * no more entries to be read. * * @throws IOException If a problem occurs while attempting to read from the * LDIF source. * * @throws LDIFException If the data read could not be parsed as an entry. */ @Nullable() public Entry readEntry() throws IOException, LDIFException { if (isAsync()) { return readEntryAsync(); } else { return readEntryInternal(); } } /** * Reads an LDIF change record from the LDIF source. The LDIF record must * have a changetype. * * @return The change record read from the LDIF source, or {@code null} if * there are no more records to be read. * * @throws IOException If a problem occurs while attempting to read from the * LDIF source. * * @throws LDIFException If the data read could not be parsed as an LDIF * change record. */ @Nullable() public LDIFChangeRecord readChangeRecord() throws IOException, LDIFException { return readChangeRecord(false); } /** * Reads an LDIF change record from the LDIF source. Optionally, if the LDIF * record does not have a changetype, then it may be assumed to be an add * change record. * * @param defaultAdd Indicates whether an LDIF record not containing a * changetype should be retrieved as an add change record. * If this is {@code false} and the record read does not * include a changetype, then an {@link LDIFException} * will be thrown. * * @return The change record read from the LDIF source, or {@code null} if * there are no more records to be read. * * @throws IOException If a problem occurs while attempting to read from the * LDIF source. * * @throws LDIFException If the data read could not be parsed as an LDIF * change record. */ @Nullable() public LDIFChangeRecord readChangeRecord(final boolean defaultAdd) throws IOException, LDIFException { if (isAsync()) { return readChangeRecordAsync(defaultAdd); } else { return readChangeRecordInternal(defaultAdd); } } /** * Reads the next {@code LDIFRecord}, which was read and parsed by a different * thread. * * @return The next parsed record or {@code null} if there are no more * records to read. * * @throws IOException If IOException was thrown when reading or parsing * the record. * * @throws LDIFException If LDIFException was thrown parsing the record. */ @Nullable() private LDIFRecord readLDIFRecordAsync() throws IOException, LDIFException { Result result; LDIFRecord record = null; while (record == null) { result = readLDIFRecordResultAsync(); if (result == null) { return null; } record = result.getOutput(); // This is a special value that means we should skip this Entry. We have // to use something different than null because null means EOF. if (record == SKIP_ENTRY) { record = null; } } return record; } /** * Reads an entry asynchronously from the LDIF source. * * @return The entry read from the LDIF source, or {@code null} if there are * no more entries to be read. * * @throws IOException If a problem occurs while attempting to read from the * LDIF source. * @throws LDIFException If the data read could not be parsed as an entry. */ @Nullable() private Entry readEntryAsync() throws IOException, LDIFException { Result result = null; LDIFRecord record = null; while (record == null) { result = readLDIFRecordResultAsync(); if (result == null) { return null; } record = result.getOutput(); // This is a special value that means we should skip this Entry. We have // to use something different than null because null means EOF. if (record == SKIP_ENTRY) { record = null; } } if (record instanceof Entry) { return (Entry) record; } else if (record instanceof LDIFChangeRecord) { try { // Some LDIFChangeRecord can be converted to an Entry. This is really // an edge case though. return ((LDIFChangeRecord)record).toEntry(); } catch (final LDIFException e) { Debug.debugException(e); final long firstLineNumber = result.getInput().getFirstLineNumber(); throw new LDIFException(e.getExceptionMessage(), firstLineNumber, true, e); } } throw new AssertionError("LDIFRecords must either be an Entry or an " + "LDIFChangeRecord"); } /** * Reads an LDIF change record from the LDIF source asynchronously. * Optionally, if the LDIF record does not have a changetype, then it may be * assumed to be an add change record. * * @param defaultAdd Indicates whether an LDIF record not containing a * changetype should be retrieved as an add change record. * If this is {@code false} and the record read does not * include a changetype, then an {@link LDIFException} will * be thrown. * * @return The change record read from the LDIF source, or {@code null} if * there are no more records to be read. * * @throws IOException If a problem occurs while attempting to read from the * LDIF source. * @throws LDIFException If the data read could not be parsed as an LDIF * change record. */ @Nullable() private LDIFChangeRecord readChangeRecordAsync(final boolean defaultAdd) throws IOException, LDIFException { Result result = null; LDIFRecord record = null; while (record == null) { result = readLDIFRecordResultAsync(); if (result == null) { return null; } record = result.getOutput(); // This is a special value that means we should skip this Entry. We have // to use something different than null because null means EOF. if (record == SKIP_ENTRY) { record = null; } } if (record instanceof LDIFChangeRecord) { return (LDIFChangeRecord) record; } else if (record instanceof Entry) { if (defaultAdd) { return new LDIFAddChangeRecord((Entry) record); } else { final long firstLineNumber = result.getInput().getFirstLineNumber(); throw new LDIFException( ERR_READ_NOT_CHANGE_RECORD.get(firstLineNumber), firstLineNumber, true); } } throw new AssertionError("LDIFRecords must either be an Entry or an " + "LDIFChangeRecord"); } /** * Reads the next LDIF record, which was read and parsed asynchronously by * separate threads. * * @return The next LDIF record or {@code null} if there are no more records. * * @throws IOException If a problem occurs while attempting to read from the * LDIF source. * * @throws LDIFException If the data read could not be parsed as an entry. */ @Nullable() private Result readLDIFRecordResultAsync() throws IOException, LDIFException { Result result = null; // If the asynchronous reading and parsing is complete, then we don't have // to block waiting for the next record to show up on the queue. If there // isn't a record there, then return null (EOF) right away. if (asyncParsingComplete.get()) { result = asyncParsedRecords.poll(); } else { try { // We probably could just do a asyncParsedRecords.take() here, but // there are some edge case error scenarios where // asyncParsingComplete might be set without a special EOF sentinel // Result enqueued. So to guard against this, we have a very cautious // polling interval of 1 second. During normal processing, we never // have to wait for this to expire, when there is something to do // (like shutdown). while ((result == null) && (!asyncParsingComplete.get())) { result = asyncParsedRecords.poll(1, TimeUnit.SECONDS); } // There's a very small chance that we missed the value, so double-check if (result == null) { result = asyncParsedRecords.poll(); } } catch (final InterruptedException e) { Debug.debugException(e); Thread.currentThread().interrupt(); throw new IOException(e); } } if (result == null) { return null; } rethrow(result.getFailureCause()); // Check if we reached the end of the input final UnparsedLDIFRecord unparsedRecord = result.getInput(); if (unparsedRecord.isEOF()) { // This might have been set already by the LineReaderThread, but // just in case it hasn't gotten to it yet, do so here. asyncParsingComplete.set(true); // Enqueue this EOF result again for any other thread that might be // blocked in asyncParsedRecords.take() even though having multiple // threads call this method concurrently breaks the contract of this // class. try { asyncParsedRecords.put(result); } catch (final InterruptedException e) { // We shouldn't ever get interrupted because the put won't ever block. // Once we are done reading, this is the only item left in the queue, // so we should always be able to re-enqueue it. Debug.debugException(e); Thread.currentThread().interrupt(); } return null; } return result; } /** * Indicates whether this LDIF reader was constructed to perform asynchronous * processing. * * @return {@code true} if this LDIFReader was constructed to perform * asynchronous processing, or {@code false} if not. */ private boolean isAsync() { return isAsync; } /** * If not {@code null}, rethrows the specified Throwable as either an * IOException or LDIFException. * * @param t The exception to rethrow. If it's {@code null}, then nothing * is thrown. * * @throws IOException If t is an IOException or a checked Exception that * is not an LDIFException. * @throws LDIFException If t is an LDIFException. */ static void rethrow(@Nullable final Throwable t) throws IOException, LDIFException { if (t == null) { return; } if (t instanceof IOException) { throw (IOException) t; } else if (t instanceof LDIFException) { throw (LDIFException) t; } else if (t instanceof RuntimeException) { throw (RuntimeException) t; } else if (t instanceof Error) { throw (Error) t; } else { throw new IOException(t); } } /** * Reads a record from the LDIF source. It may be either an entry or an LDIF * change record. * * @return The record read from the LDIF source, or {@code null} if there are * no more entries to be read. * * @throws IOException If a problem occurs while trying to read from the * LDIF source. * @throws LDIFException If the data read could not be parsed as an entry or * an LDIF change record. */ @Nullable() private LDIFRecord readLDIFRecordInternal() throws IOException, LDIFException { final UnparsedLDIFRecord unparsedRecord = readUnparsedRecord(); return decodeRecord(unparsedRecord, relativeBasePath, schema); } /** * Reads an entry from the LDIF source. * * @return The entry read from the LDIF source, or {@code null} if there are * no more entries to be read. * * @throws IOException If a problem occurs while attempting to read from the * LDIF source. * @throws LDIFException If the data read could not be parsed as an entry. */ @Nullable() private Entry readEntryInternal() throws IOException, LDIFException { Entry e = null; while (e == null) { final UnparsedLDIFRecord unparsedRecord = readUnparsedRecord(); if (unparsedRecord.isEOF()) { return null; } e = decodeEntry(unparsedRecord, relativeBasePath); Debug.debugLDIFRead(e); if (entryTranslator != null) { e = entryTranslator.translate(e, unparsedRecord.getFirstLineNumber()); } } return e; } /** * Reads an LDIF change record from the LDIF source. Optionally, if the LDIF * record does not have a changetype, then it may be assumed to be an add * change record. * * @param defaultAdd Indicates whether an LDIF record not containing a * changetype should be retrieved as an add change record. * If this is {@code false} and the record read does not * include a changetype, then an {@link LDIFException} will * be thrown. * * @return The change record read from the LDIF source, or {@code null} if * there are no more records to be read. * * @throws IOException If a problem occurs while attempting to read from the * LDIF source. * @throws LDIFException If the data read could not be parsed as an LDIF * change record. */ @Nullable() private LDIFChangeRecord readChangeRecordInternal(final boolean defaultAdd) throws IOException, LDIFException { LDIFChangeRecord r = null; while (r == null) { final UnparsedLDIFRecord unparsedRecord = readUnparsedRecord(); if (unparsedRecord.isEOF()) { return null; } r = decodeChangeRecord(unparsedRecord, relativeBasePath, defaultAdd, schema); Debug.debugLDIFRead(r); if (changeRecordTranslator != null) { r = changeRecordTranslator.translate(r, unparsedRecord.getFirstLineNumber()); } } return r; } /** * Reads a record (either an entry or a change record) from the LDIF source * and places it in the line list. * * @return The unparsed record that was read. * * @throws IOException If a problem occurs while attempting to read from the * LDIF source. * * @throws LDIFException If the data read could not be parsed as a valid * LDIF record. */ @NotNull() private UnparsedLDIFRecord readUnparsedRecord() throws IOException, LDIFException { final ArrayList lineList = new ArrayList<>(20); boolean lastWasComment = false; long firstLineNumber = lineNumberCounter + 1; while (true) { final String line = reader.readLine(); lineNumberCounter++; if (line == null) { // We've hit the end of the LDIF source. If we haven't read any entry // data, then return null. Otherwise, the last entry wasn't followed by // a blank line, which is OK, and we should decode that entry. if (lineList.isEmpty()) { return new UnparsedLDIFRecord(new ArrayList(0), duplicateValueBehavior, trailingSpaceBehavior, schema, -1); } else { break; } } if (line.isEmpty()) { // It's a blank line. If we have read entry data, then this signals the // end of the entry. Otherwise, it's an extra space between entries, // which is OK. lastWasComment = false; if (lineList.isEmpty()) { firstLineNumber++; continue; } else { break; } } if (line.charAt(0) == ' ') { // The line starts with a space, which means that it must be a // continuation of the previous line. This is true even if the last // line was a comment. if (lastWasComment) { // What we've read is part of a comment, so we don't care about its // content. } else if (lineList.isEmpty()) { throw new LDIFException( ERR_READ_UNEXPECTED_FIRST_SPACE.get(lineNumberCounter), lineNumberCounter, false); } else { lineList.get(lineList.size() - 1).append(line.substring(1)); lastWasComment = false; } } else if (line.charAt(0) == '#') { lastWasComment = true; } else { // We want to make sure that we skip over the "version:" line if it // exists, but that should only occur at the beginning of an entry where // it can't be confused with a possible "version" attribute. if (lineList.isEmpty() && line.startsWith("version:")) { lastWasComment = true; } else { lineList.add(new StringBuilder(line)); lastWasComment = false; } } } return new UnparsedLDIFRecord(lineList, duplicateValueBehavior, trailingSpaceBehavior, schema, firstLineNumber); } /** * Decodes the provided set of LDIF lines as an entry. The provided set of * lines must contain exactly one entry. Long lines may be wrapped as per the * LDIF specification, and it is acceptable to have one or more blank lines * following the entry. A default trailing space behavior of * {@link TrailingSpaceBehavior#REJECT} will be used. * * @param ldifLines The set of lines that comprise the LDIF representation * of the entry. It must not be {@code null} or empty. * * @return The entry read from LDIF. * * @throws LDIFException If the provided LDIF data cannot be decoded as an * entry. */ @NotNull() public static Entry decodeEntry(@NotNull final String... ldifLines) throws LDIFException { final Entry e = decodeEntry(prepareRecord(DuplicateValueBehavior.STRIP, TrailingSpaceBehavior.REJECT, null, ldifLines), DEFAULT_RELATIVE_BASE_PATH); Debug.debugLDIFRead(e); return e; } /** * Decodes the provided set of LDIF lines as an entry. The provided set of * lines must contain exactly one entry. Long lines may be wrapped as per the * LDIF specification, and it is acceptable to have one or more blank lines * following the entry. A default trailing space behavior of * {@link TrailingSpaceBehavior#REJECT} will be used. * * @param ignoreDuplicateValues Indicates whether to ignore duplicate * attribute values encountered while parsing. * @param schema The schema to use when parsing the record, * if applicable. * @param ldifLines The set of lines that comprise the LDIF * representation of the entry. It must not be * {@code null} or empty. * * @return The entry read from LDIF. * * @throws LDIFException If the provided LDIF data cannot be decoded as an * entry. */ @NotNull() public static Entry decodeEntry(final boolean ignoreDuplicateValues, @Nullable final Schema schema, @NotNull final String... ldifLines) throws LDIFException { return decodeEntry(ignoreDuplicateValues, TrailingSpaceBehavior.REJECT, schema, ldifLines); } /** * Decodes the provided set of LDIF lines as an entry. The provided set of * lines must contain exactly one entry. Long lines may be wrapped as per the * LDIF specification, and it is acceptable to have one or more blank lines * following the entry. * * @param ignoreDuplicateValues Indicates whether to ignore duplicate * attribute values encountered while parsing. * @param trailingSpaceBehavior The behavior that should be exhibited when * encountering attribute values which are not * base64-encoded but contain trailing spaces. * It must not be {@code null}. * @param schema The schema to use when parsing the record, * if applicable. * @param ldifLines The set of lines that comprise the LDIF * representation of the entry. It must not be * {@code null} or empty. * * @return The entry read from LDIF. * * @throws LDIFException If the provided LDIF data cannot be decoded as an * entry. */ @NotNull() public static Entry decodeEntry( final boolean ignoreDuplicateValues, @NotNull final TrailingSpaceBehavior trailingSpaceBehavior, @Nullable final Schema schema, @NotNull final String... ldifLines) throws LDIFException { final Entry e = decodeEntry(prepareRecord( (ignoreDuplicateValues ? DuplicateValueBehavior.STRIP : DuplicateValueBehavior.REJECT), trailingSpaceBehavior, schema, ldifLines), DEFAULT_RELATIVE_BASE_PATH); Debug.debugLDIFRead(e); return e; } /** * Decodes the provided set of LDIF lines as an LDIF change record. The * provided set of lines must contain exactly one change record and it must * include a changetype. Long lines may be wrapped as per the LDIF * specification, and it is acceptable to have one or more blank lines * following the entry. * * @param ldifLines The set of lines that comprise the LDIF representation * of the change record. It must not be {@code null} or * empty. * * @return The change record read from LDIF. * * @throws LDIFException If the provided LDIF data cannot be decoded as a * change record. */ @NotNull() public static LDIFChangeRecord decodeChangeRecord( @NotNull final String... ldifLines) throws LDIFException { return decodeChangeRecord(false, ldifLines); } /** * Decodes the provided set of LDIF lines as an LDIF change record. The * provided set of lines must contain exactly one change record. Long lines * may be wrapped as per the LDIF specification, and it is acceptable to have * one or more blank lines following the entry. * * @param defaultAdd Indicates whether an LDIF record not containing a * changetype should be retrieved as an add change record. * If this is {@code false} and the record read does not * include a changetype, then an {@link LDIFException} * will be thrown. * @param ldifLines The set of lines that comprise the LDIF representation * of the change record. It must not be {@code null} or * empty. * * @return The change record read from LDIF. * * @throws LDIFException If the provided LDIF data cannot be decoded as a * change record. */ @NotNull() public static LDIFChangeRecord decodeChangeRecord(final boolean defaultAdd, @NotNull final String... ldifLines) throws LDIFException { final LDIFChangeRecord r = decodeChangeRecord( prepareRecord(DuplicateValueBehavior.STRIP, TrailingSpaceBehavior.REJECT, null, ldifLines), DEFAULT_RELATIVE_BASE_PATH, defaultAdd, null); Debug.debugLDIFRead(r); return r; } /** * Decodes the provided set of LDIF lines as an LDIF change record. The * provided set of lines must contain exactly one change record. Long lines * may be wrapped as per the LDIF specification, and it is acceptable to have * one or more blank lines following the entry. * * @param ignoreDuplicateValues Indicates whether to ignore duplicate * attribute values encountered while parsing. * @param schema The schema to use when processing the change * record, or {@code null} if no schema should * be used and all values should be treated as * case-insensitive strings. * @param defaultAdd Indicates whether an LDIF record not * containing a changetype should be retrieved * as an add change record. If this is * {@code false} and the record read does not * include a changetype, then an * {@link LDIFException} will be thrown. * @param ldifLines The set of lines that comprise the LDIF * representation of the change record. It * must not be {@code null} or empty. * * @return The change record read from LDIF. * * @throws LDIFException If the provided LDIF data cannot be decoded as a * change record. */ @NotNull() public static LDIFChangeRecord decodeChangeRecord( final boolean ignoreDuplicateValues, @Nullable final Schema schema, final boolean defaultAdd, @NotNull final String... ldifLines) throws LDIFException { return decodeChangeRecord(ignoreDuplicateValues, TrailingSpaceBehavior.REJECT, schema, defaultAdd, ldifLines); } /** * Decodes the provided set of LDIF lines as an LDIF change record. The * provided set of lines must contain exactly one change record. Long lines * may be wrapped as per the LDIF specification, and it is acceptable to have * one or more blank lines following the entry. * * @param ignoreDuplicateValues Indicates whether to ignore duplicate * attribute values encountered while parsing. * @param trailingSpaceBehavior The behavior that should be exhibited when * encountering attribute values which are not * base64-encoded but contain trailing spaces. * It must not be {@code null}. * @param schema The schema to use when processing the change * record, or {@code null} if no schema should * be used and all values should be treated as * case-insensitive strings. * @param defaultAdd Indicates whether an LDIF record not * containing a changetype should be retrieved * as an add change record. If this is * {@code false} and the record read does not * include a changetype, then an * {@link LDIFException} will be thrown. * @param ldifLines The set of lines that comprise the LDIF * representation of the change record. It * must not be {@code null} or empty. * * @return The change record read from LDIF. * * @throws LDIFException If the provided LDIF data cannot be decoded as a * change record. */ @NotNull() public static LDIFChangeRecord decodeChangeRecord( final boolean ignoreDuplicateValues, @NotNull final TrailingSpaceBehavior trailingSpaceBehavior, @Nullable final Schema schema, final boolean defaultAdd, @NotNull final String... ldifLines) throws LDIFException { final LDIFChangeRecord r = decodeChangeRecord( prepareRecord( (ignoreDuplicateValues ? DuplicateValueBehavior.STRIP : DuplicateValueBehavior.REJECT), trailingSpaceBehavior, schema, ldifLines), DEFAULT_RELATIVE_BASE_PATH, defaultAdd, null); Debug.debugLDIFRead(r); return r; } /** * Parses the provided set of lines into a list of {@code StringBuilder} * objects suitable for decoding into an entry or LDIF change record. * Comments will be ignored and wrapped lines will be unwrapped. * * @param duplicateValueBehavior The behavior that should be exhibited if * the LDIF reader encounters an entry with * duplicate values. * @param trailingSpaceBehavior The behavior that should be exhibited when * encountering attribute values which are not * base64-encoded but contain trailing spaces. * @param schema The schema to use when parsing the record, * if applicable. * @param ldifLines The set of lines that comprise the record * to decode. It must not be {@code null} or * empty. * * @return The prepared list of {@code StringBuilder} objects ready to be * decoded. * * @throws LDIFException If the provided lines do not contain valid LDIF * content. */ @NotNull() private static UnparsedLDIFRecord prepareRecord( @NotNull final DuplicateValueBehavior duplicateValueBehavior, @NotNull final TrailingSpaceBehavior trailingSpaceBehavior, @Nullable final Schema schema, @NotNull final String... ldifLines) throws LDIFException { Validator.ensureNotNull(ldifLines); Validator.ensureFalse(ldifLines.length == 0, "LDIFReader.prepareRecord.ldifLines must not be empty."); boolean lastWasComment = false; final ArrayList lineList = new ArrayList<>(ldifLines.length); for (int i=0; i < ldifLines.length; i++) { final String line = ldifLines[i]; if (line.isEmpty()) { // This is only acceptable if there are no more non-empty lines in the // array. for (int j=i+1; j < ldifLines.length; j++) { if (! ldifLines[j].isEmpty()) { throw new LDIFException(ERR_READ_UNEXPECTED_BLANK.get(i), i, true, ldifLines, null); } // If we've gotten here, then we know that we're at the end of the // entry. If we have read data, then we can decode it as an entry. // Otherwise, there was no real data in the provided LDIF lines. if (lineList.isEmpty()) { throw new LDIFException(ERR_READ_ONLY_BLANKS.get(), 0, true, ldifLines, null); } else { return new UnparsedLDIFRecord(lineList, duplicateValueBehavior, trailingSpaceBehavior, schema, 0); } } } if (line.charAt(0) == ' ') { if (i > 0) { if (! lastWasComment) { lineList.get(lineList.size() - 1).append(line.substring(1)); } } else { throw new LDIFException( ERR_READ_UNEXPECTED_FIRST_SPACE_NO_NUMBER.get(), 0, true, ldifLines, null); } } else if (line.charAt(0) == '#') { lastWasComment = true; } else { lineList.add(new StringBuilder(line)); lastWasComment = false; } } if (lineList.isEmpty()) { throw new LDIFException(ERR_READ_NO_DATA.get(), 0, true, ldifLines, null); } else { return new UnparsedLDIFRecord(lineList, duplicateValueBehavior, trailingSpaceBehavior, schema, 0); } } /** * Decodes the unparsed record that was read from the LDIF source. It may be * either an entry or an LDIF change record. * * @param unparsedRecord The unparsed LDIF record that was read from the * input. It must not be {@code null} or empty. * @param relativeBasePath The base path that will be prepended to relative * paths in order to obtain an absolute path. * @param schema The schema to use when parsing. * * @return The parsed record, or {@code null} if there are no more entries to * be read. * * @throws LDIFException If the data read could not be parsed as an entry or * an LDIF change record. */ @NotNull() private static LDIFRecord decodeRecord( @NotNull final UnparsedLDIFRecord unparsedRecord, @NotNull final String relativeBasePath, @Nullable final Schema schema) throws LDIFException { // If there was an error reading from the input, then we rethrow it here. final Exception readError = unparsedRecord.getFailureCause(); if (readError != null) { if (readError instanceof LDIFException) { // If the error was an LDIFException, which will normally be the case, // then rethrow it with all of the same state. We could just // throw (LDIFException) readError; // but that's considered bad form. final LDIFException ldifEx = (LDIFException) readError; throw new LDIFException(ldifEx.getMessage(), ldifEx.getLineNumber(), ldifEx.mayContinueReading(), ldifEx.getDataLines(), ldifEx.getCause()); } else { throw new LDIFException(StaticUtils.getExceptionMessage(readError), -1, true, readError); } } if (unparsedRecord.isEOF()) { return null; } final ArrayList lineList = unparsedRecord.getLineList(); if (unparsedRecord.getLineList() == null) { return null; // We can get here if there was an error reading the lines. } final LDIFRecord r; if (lineList.size() == 1) { r = decodeEntry(unparsedRecord, relativeBasePath); } else { final String lowerSecondLine = StaticUtils.toLowerCase(lineList.get(1).toString()); if (lowerSecondLine.startsWith("changetype:")) { r = decodeChangeRecord(unparsedRecord, relativeBasePath, true, schema); } else if (lowerSecondLine.startsWith("control:") && SUPPORT_CONTROLS.get()) { r = decodeChangeRecord(unparsedRecord, relativeBasePath, true, schema); } else { r = decodeEntry(unparsedRecord, relativeBasePath); } } Debug.debugLDIFRead(r); return r; } /** * Decodes the provided set of LDIF lines as an entry. The provided list must * not contain any blank lines or comments, and lines are not allowed to be * wrapped. * * @param unparsedRecord The unparsed LDIF record that was read from the * input. It must not be {@code null} or empty. * @param relativeBasePath The base path that will be prepended to relative * paths in order to obtain an absolute path. * * @return The entry read from LDIF. * * @throws LDIFException If the provided LDIF data cannot be read as an * entry. */ @NotNull() private static Entry decodeEntry( @NotNull final UnparsedLDIFRecord unparsedRecord, @NotNull final String relativeBasePath) throws LDIFException { final ArrayList ldifLines = unparsedRecord.getLineList(); final long firstLineNumber = unparsedRecord.getFirstLineNumber(); final Iterator iterator = ldifLines.iterator(); // The first line must start with either "version:" or "dn:". If the first // line starts with "version:" then the second must start with "dn:". StringBuilder line = iterator.next(); handleTrailingSpaces(line, null, firstLineNumber, unparsedRecord.getTrailingSpaceBehavior()); int colonPos = line.indexOf(":"); if ((colonPos > 0) && line.substring(0, colonPos).equalsIgnoreCase("version")) { // The first line is "version:". Under most conditions, this will be // handled by the LDIF reader, but this can happen if you call // decodeEntry with a set of data that includes a version. At any rate, // read the next line, which must specify the DN. line = iterator.next(); handleTrailingSpaces(line, null, firstLineNumber, unparsedRecord.getTrailingSpaceBehavior()); } colonPos = line.indexOf(":"); if ((colonPos < 0) || (! line.substring(0, colonPos).equalsIgnoreCase("dn"))) { throw new LDIFException( ERR_READ_DN_LINE_DOESNT_START_WITH_DN.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } final String dn; final int length = line.length(); if (length == (colonPos+1)) { // The colon was the last character on the line. This is acceptable and // indicates that the entry has the null DN. dn = ""; } else if (line.charAt(colonPos+1) == ':') { // Skip over any spaces leading up to the value, and then the rest of the // string is the base64-encoded DN. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } try { final byte[] dnBytes = Base64.decode(line.substring(pos)); dn = StaticUtils.toUTF8String(dnBytes); } catch (final ParseException pe) { Debug.debugException(pe); throw new LDIFException( ERR_READ_CANNOT_BASE64_DECODE_DN.get(firstLineNumber, pe.getMessage()), firstLineNumber, true, ldifLines, pe); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_CANNOT_BASE64_DECODE_DN.get(firstLineNumber, e), firstLineNumber, true, ldifLines, e); } } else { // Skip over any spaces leading up to the value, and then the rest of the // string is the DN. int pos = colonPos+1; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } dn = line.substring(pos); } // The remaining lines must be the attributes for the entry. However, we // will allow the case in which an entry does not have any attributes, to be // able to support reading search result entries in which no attributes were // returned. if (! iterator.hasNext()) { return new Entry(dn, unparsedRecord.getSchema()); } return new Entry(dn, unparsedRecord.getSchema(), parseAttributes(dn, unparsedRecord.getDuplicateValueBehavior(), unparsedRecord.getTrailingSpaceBehavior(), unparsedRecord.getSchema(), ldifLines, iterator, relativeBasePath, firstLineNumber)); } /** * Decodes the provided set of LDIF lines as a change record. The provided * list must not contain any blank lines or comments, and lines are not * allowed to be wrapped. * * @param unparsedRecord The unparsed LDIF record that was read from the * input. It must not be {@code null} or empty. * @param relativeBasePath The base path that will be prepended to relative * paths in order to obtain an absolute path. * @param defaultAdd Indicates whether an LDIF record not containing a * changetype should be retrieved as an add change * record. If this is {@code false} and the record * read does not include a changetype, then an * {@link LDIFException} will be thrown. * @param schema The schema to use in parsing. * * @return The change record read from LDIF. * * @throws LDIFException If the provided LDIF data cannot be decoded as a * change record. */ @NotNull() private static LDIFChangeRecord decodeChangeRecord( @NotNull final UnparsedLDIFRecord unparsedRecord, @NotNull final String relativeBasePath, final boolean defaultAdd, @Nullable final Schema schema) throws LDIFException { final ArrayList ldifLines = unparsedRecord.getLineList(); final long firstLineNumber = unparsedRecord.getFirstLineNumber(); Iterator iterator = ldifLines.iterator(); // The first line must start with either "version:" or "dn:". If the first // line starts with "version:" then the second must start with "dn:". StringBuilder line = iterator.next(); handleTrailingSpaces(line, null, firstLineNumber, unparsedRecord.getTrailingSpaceBehavior()); int colonPos = line.indexOf(":"); int linesRead = 1; if ((colonPos > 0) && line.substring(0, colonPos).equalsIgnoreCase("version")) { // The first line is "version:". Under most conditions, this will be // handled by the LDIF reader, but this can happen if you call // decodeEntry with a set of data that includes a version. At any rate, // read the next line, which must specify the DN. line = iterator.next(); linesRead++; handleTrailingSpaces(line, null, firstLineNumber, unparsedRecord.getTrailingSpaceBehavior()); } colonPos = line.indexOf(":"); if ((colonPos < 0) || (! line.substring(0, colonPos).equalsIgnoreCase("dn"))) { throw new LDIFException( ERR_READ_DN_LINE_DOESNT_START_WITH_DN.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } final String dn; final int length = line.length(); if (length == (colonPos+1)) { // The colon was the last character on the line. This is acceptable and // indicates that the entry has the null DN. dn = ""; } else if (line.charAt(colonPos+1) == ':') { // Skip over any spaces leading up to the value, and then the rest of the // string is the base64-encoded DN. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } try { final byte[] dnBytes = Base64.decode(line.substring(pos)); dn = StaticUtils.toUTF8String(dnBytes); } catch (final ParseException pe) { Debug.debugException(pe); throw new LDIFException( ERR_READ_CR_CANNOT_BASE64_DECODE_DN.get(firstLineNumber, pe.getMessage()), firstLineNumber, true, ldifLines, pe); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_CR_CANNOT_BASE64_DECODE_DN.get(firstLineNumber, e), firstLineNumber, true, ldifLines, e); } } else { // Skip over any spaces leading up to the value, and then the rest of the // string is the DN. int pos = colonPos+1; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } dn = line.substring(pos); } // An LDIF change record may contain zero or more controls, with the end of // the controls signified by the changetype. The changetype element must be // present, unless defaultAdd is true in which case the first thing that is // neither control or changetype will trigger the start of add attribute // parsing. If support for controls has been disabled, then a control // element before the changetype will be treated in the same way as any // other attribute before the changetype, and it will either be treated as // an attribute as part of an add change record (if defaultAdd is true) or // the LDIF record will be rejected as missing the changetype. if (! iterator.hasNext()) { throw new LDIFException(ERR_READ_CR_TOO_SHORT.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } String changeType; ArrayList controls = null; while (true) { line = iterator.next(); handleTrailingSpaces(line, dn, firstLineNumber, unparsedRecord.getTrailingSpaceBehavior()); colonPos = line.indexOf(":"); if (colonPos < 0) { throw new LDIFException( ERR_READ_CR_SECOND_LINE_MISSING_COLON.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } final String token = StaticUtils.toLowerCase(line.substring(0, colonPos)); if (token.equals("control") && SUPPORT_CONTROLS.get()) { if (controls == null) { controls = new ArrayList<>(5); } controls.add(decodeControl(line, colonPos, firstLineNumber, ldifLines, relativeBasePath)); } else if (token.equals("changetype")) { changeType = decodeChangeType(line, colonPos, firstLineNumber, ldifLines); break; } else if (defaultAdd) { // The line we read wasn't a control or changetype declaration, so we'll // assume it's an attribute in an add record. However, we're not ready // for that yet, and since we can't rewind an iterator we'll create a // new one that hasn't yet gotten to this line. changeType = "add"; iterator = ldifLines.iterator(); for (int i=0; i < linesRead; i++) { iterator.next(); } break; } else { throw new LDIFException( ERR_READ_CR_CT_LINE_DOESNT_START_WITH_CONTROL_OR_CT.get( firstLineNumber), firstLineNumber, true, ldifLines, null); } linesRead++; } // Make sure that the change type is acceptable and then decode the rest of // the change record accordingly. final String lowerChangeType = StaticUtils.toLowerCase(changeType); if (lowerChangeType.equals("add")) { // There must be at least one more line. If not, then that's an error. // Otherwise, parse the rest of the data as attribute-value pairs. if (iterator.hasNext()) { final Collection attrs = parseAttributes(dn, unparsedRecord.getDuplicateValueBehavior(), unparsedRecord.getTrailingSpaceBehavior(), unparsedRecord.getSchema(), ldifLines, iterator, relativeBasePath, firstLineNumber); final Attribute[] attributes = new Attribute[attrs.size()]; final Iterator attrIterator = attrs.iterator(); for (int i=0; i < attributes.length; i++) { attributes[i] = attrIterator.next(); } return new LDIFAddChangeRecord(dn, attributes, controls); } else { throw new LDIFException(ERR_READ_CR_NO_ATTRIBUTES.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } } else if (lowerChangeType.equals("delete")) { // There shouldn't be any more data. If there is, then that's an error. // Otherwise, we can just return the delete change record with what we // already know. if (iterator.hasNext()) { throw new LDIFException( ERR_READ_CR_EXTRA_DELETE_DATA.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } else { return new LDIFDeleteChangeRecord(dn, controls); } } else if (lowerChangeType.equals("modify")) { // There must be at least one more line. If not, then that's an error. // Otherwise, parse the rest of the data as a set of modifications. if (iterator.hasNext()) { final Modification[] mods = parseModifications(dn, unparsedRecord.getTrailingSpaceBehavior(), ldifLines, iterator, relativeBasePath, firstLineNumber, schema); return new LDIFModifyChangeRecord(dn, mods, controls); } else { throw new LDIFException(ERR_READ_CR_NO_MODS.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } } else if (lowerChangeType.equals("moddn") || lowerChangeType.equals("modrdn")) { // There must be at least one more line. If not, then that's an error. // Otherwise, parse the rest of the data as a set of modifications. if (iterator.hasNext()) { return parseModifyDNChangeRecord(ldifLines, iterator, dn, controls, unparsedRecord.getTrailingSpaceBehavior(), firstLineNumber); } else { throw new LDIFException(ERR_READ_CR_NO_NEWRDN.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } } else { throw new LDIFException(ERR_READ_CR_INVALID_CT.get(changeType, firstLineNumber), firstLineNumber, true, ldifLines, null); } } /** * Decodes information about a control from the provided line. * * @param line The line to process. * @param colonPos The position of the colon that separates the * control token string from tbe encoded control. * @param firstLineNumber The line number for the start of the record. * @param ldifLines The lines that comprise the LDIF representation * of the full record being parsed. * @param relativeBasePath The base path that will be prepended to relative * paths in order to obtain an absolute path. * * @return The decoded control. * * @throws LDIFException If a problem is encountered while trying to decode * the changetype. */ @NotNull() private static Control decodeControl(@NotNull final StringBuilder line, final int colonPos, final long firstLineNumber, @NotNull final ArrayList ldifLines, @NotNull final String relativeBasePath) throws LDIFException { final String controlString; int length = line.length(); if (length == (colonPos+1)) { // The colon was the last character on the line. This is not // acceptable. throw new LDIFException( ERR_READ_CONTROL_LINE_NO_CONTROL_VALUE.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } else if (line.charAt(colonPos+1) == ':') { // Skip over any spaces leading up to the value, and then the rest of // the string is the base64-encoded control representation. This is // unusual and unnecessary, but is nevertheless acceptable. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } try { final byte[] controlBytes = Base64.decode(line.substring(pos)); controlString = StaticUtils.toUTF8String(controlBytes); } catch (final ParseException pe) { Debug.debugException(pe); throw new LDIFException( ERR_READ_CANNOT_BASE64_DECODE_CONTROL.get( firstLineNumber, pe.getMessage()), firstLineNumber, true, ldifLines, pe); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_CANNOT_BASE64_DECODE_CONTROL.get(firstLineNumber, e), firstLineNumber, true, ldifLines, e); } } else { // Skip over any spaces leading up to the value, and then the rest of // the string is the encoded control. int pos = colonPos+1; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } controlString = line.substring(pos); } // If the resulting control definition is empty, then that's invalid. if (controlString.isEmpty()) { throw new LDIFException( ERR_READ_CONTROL_LINE_NO_CONTROL_VALUE.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } // The first element of the control must be the OID, and it must be followed // by a space (to separate it from the criticality), a colon (to separate it // from the value and indicate a default criticality of false), or the end // of the line (to indicate a default criticality of false and no value). String oid = null; boolean hasCriticality = false; boolean hasValue = false; int pos = 0; length = controlString.length(); while (pos < length) { final char c = controlString.charAt(pos); if (c == ':') { // This indicates that there is no criticality and that the value // immediately follows the OID. oid = controlString.substring(0, pos++); hasValue = true; break; } else if (c == ' ') { // This indicates that there is a criticality. We don't know anything // about the presence of a value yet. oid = controlString.substring(0, pos++); hasCriticality = true; break; } else { pos++; } } if (oid == null) { // This indicates that the string representation of the control is only // the OID. return new Control(controlString, false); } // See if we need to read the criticality. If so, then do so now. // Otherwise, assume a default criticality of false. final boolean isCritical; if (hasCriticality) { // Skip over any spaces before the criticality. while (controlString.charAt(pos) == ' ') { pos++; } // Read until we find a colon or the end of the string. final int criticalityStartPos = pos; while (pos < length) { final char c = controlString.charAt(pos); if (c == ':') { hasValue = true; break; } else { pos++; } } final String criticalityString = StaticUtils.toLowerCase(controlString.substring(criticalityStartPos, pos)); if (criticalityString.equals("true")) { isCritical = true; } else if (criticalityString.equals("false")) { isCritical = false; } else { throw new LDIFException( ERR_READ_CONTROL_LINE_INVALID_CRITICALITY.get(criticalityString, firstLineNumber), firstLineNumber, true, ldifLines, null); } if (hasValue) { pos++; } } else { isCritical = false; } // See if we need to read the value. If so, then do so now. It may be // a string, or it may be base64-encoded. It could conceivably even be read // from a URL. final ASN1OctetString value; if (hasValue) { // The character immediately after the colon that precedes the value may // be one of the following: // - A second colon (optionally followed by a single space) to indicate // that the value is base64-encoded. // - A less-than symbol to indicate that the value should be read from a // location specified by a URL. // - A single space that precedes the non-base64-encoded value. // - The first character of the non-base64-encoded value. switch (controlString.charAt(pos)) { case ':': try { if (controlString.length() == (pos+1)) { value = new ASN1OctetString(); } else if (controlString.charAt(pos+1) == ' ') { value = new ASN1OctetString( Base64.decode(controlString.substring(pos+2))); } else { value = new ASN1OctetString( Base64.decode(controlString.substring(pos+1))); } } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_CONTROL_LINE_CANNOT_BASE64_DECODE_VALUE.get( firstLineNumber, StaticUtils.getExceptionMessage(e)), firstLineNumber, true, ldifLines, e); } break; case '<': try { final String urlString; if (controlString.charAt(pos+1) == ' ') { urlString = controlString.substring(pos+2); } else { urlString = controlString.substring(pos+1); } value = new ASN1OctetString(retrieveURLBytes(urlString, relativeBasePath, firstLineNumber)); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_CONTROL_LINE_CANNOT_RETRIEVE_VALUE_FROM_URL.get( firstLineNumber, StaticUtils.getExceptionMessage(e)), firstLineNumber, true, ldifLines, e); } break; case ' ': value = new ASN1OctetString(controlString.substring(pos+1)); break; default: value = new ASN1OctetString(controlString.substring(pos)); break; } } else { value = null; } return new Control(oid, isCritical, value); } /** * Decodes the changetype element from the provided line. * * @param line The line to process. * @param colonPos The position of the colon that separates the * changetype string from its value. * @param firstLineNumber The line number for the start of the record. * @param ldifLines The lines that comprise the LDIF representation of * the full record being parsed. * * @return The decoded changetype string. * * @throws LDIFException If a problem is encountered while trying to decode * the changetype. */ @NotNull() private static String decodeChangeType(@NotNull final StringBuilder line, final int colonPos, final long firstLineNumber, @NotNull final ArrayList ldifLines) throws LDIFException { final int length = line.length(); if (length == (colonPos+1)) { // The colon was the last character on the line. This is not // acceptable. throw new LDIFException( ERR_READ_CT_LINE_NO_CT_VALUE.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } else if (line.charAt(colonPos+1) == ':') { // Skip over any spaces leading up to the value, and then the rest of // the string is the base64-encoded changetype. This is unusual and // unnecessary, but is nevertheless acceptable. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } try { final byte[] changeTypeBytes = Base64.decode(line.substring(pos)); return StaticUtils.toUTF8String(changeTypeBytes); } catch (final ParseException pe) { Debug.debugException(pe); throw new LDIFException( ERR_READ_CANNOT_BASE64_DECODE_CT.get(firstLineNumber, pe.getMessage()), firstLineNumber, true, ldifLines, pe); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_CANNOT_BASE64_DECODE_CT.get(firstLineNumber, e), firstLineNumber, true, ldifLines, e); } } else { // Skip over any spaces leading up to the value, and then the rest of // the string is the changetype. int pos = colonPos+1; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } return line.substring(pos); } } /** * Parses the data available through the provided iterator as a collection of * attributes suitable for use in an entry or an add change record. * * @param dn The DN of the record being read. * @param duplicateValueBehavior The behavior that should be exhibited if * the LDIF reader encounters an entry with * duplicate values. * @param trailingSpaceBehavior The behavior that should be exhibited when * encountering attribute values which are not * base64-encoded but contain trailing spaces. * @param schema The schema to use when parsing the * attributes, or {@code null} if none is * needed. * @param ldifLines The lines that comprise the LDIF * representation of the full record being * parsed. * @param iterator The iterator to use to access the attribute * lines. * @param relativeBasePath The base path that will be prepended to * relative paths in order to obtain an * absolute path. * @param firstLineNumber The line number for the start of the * record. * * @return The collection of attributes that were read. * * @throws LDIFException If the provided LDIF data cannot be decoded as a * set of attributes. */ @NotNull() private static ArrayList parseAttributes(@NotNull final String dn, @NotNull final DuplicateValueBehavior duplicateValueBehavior, @NotNull final TrailingSpaceBehavior trailingSpaceBehavior, @Nullable final Schema schema, @NotNull final ArrayList ldifLines, @NotNull final Iterator iterator, @NotNull final String relativeBasePath, final long firstLineNumber) throws LDIFException { final LinkedHashMap attributes = new LinkedHashMap<>(StaticUtils.computeMapCapacity(ldifLines.size())); while (iterator.hasNext()) { final StringBuilder line = iterator.next(); handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior); final int colonPos = line.indexOf(":"); if (colonPos <= 0) { throw new LDIFException(ERR_READ_NO_ATTR_COLON.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } final String attributeName = line.substring(0, colonPos); final String lowerName = StaticUtils.toLowerCase(attributeName); final MatchingRule matchingRule; if (schema == null) { matchingRule = CaseIgnoreStringMatchingRule.getInstance(); } else { matchingRule = MatchingRule.selectEqualityMatchingRule(attributeName, schema); } Attribute attr; final LDIFAttribute ldifAttr; final Object attrObject = attributes.get(lowerName); if (attrObject == null) { attr = null; ldifAttr = null; } else { if (attrObject instanceof Attribute) { attr = (Attribute) attrObject; ldifAttr = new LDIFAttribute(attr.getName(), matchingRule, attr.getRawValues()[0]); attributes.put(lowerName, ldifAttr); } else { attr = null; ldifAttr = (LDIFAttribute) attrObject; } } final int length = line.length(); if (length == (colonPos+1)) { // This means that the attribute has a zero-length value, which is // acceptable. if (attrObject == null) { attr = new Attribute(attributeName, matchingRule, ""); attributes.put(lowerName, attr); } else { try { if (! ldifAttr.addValue(new ASN1OctetString(), duplicateValueBehavior)) { if (duplicateValueBehavior != DuplicateValueBehavior.STRIP) { throw new LDIFException(ERR_READ_DUPLICATE_VALUE.get(dn, firstLineNumber, attributeName), firstLineNumber, true, ldifLines, null); } } } catch (final LDAPException le) { throw new LDIFException( ERR_READ_VALUE_SYNTAX_VIOLATION.get(dn, firstLineNumber, attributeName, StaticUtils.getExceptionMessage(le)), firstLineNumber, true, ldifLines, le); } } } else if (line.charAt(colonPos+1) == ':') { // Skip over any spaces leading up to the value, and then the rest of // the string is the base64-encoded attribute value. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } try { final byte[] valueBytes = Base64.decode(line.substring(pos)); if (attrObject == null) { attr = new Attribute(attributeName, matchingRule, valueBytes); attributes.put(lowerName, attr); } else { try { if (! ldifAttr.addValue(new ASN1OctetString(valueBytes), duplicateValueBehavior)) { if (duplicateValueBehavior != DuplicateValueBehavior.STRIP) { throw new LDIFException(ERR_READ_DUPLICATE_VALUE.get(dn, firstLineNumber, attributeName), firstLineNumber, true, ldifLines, null); } } } catch (final LDAPException le) { throw new LDIFException( ERR_READ_VALUE_SYNTAX_VIOLATION.get(dn, firstLineNumber, attributeName, StaticUtils.getExceptionMessage(le)), firstLineNumber, true, ldifLines, le); } } } catch (final ParseException pe) { Debug.debugException(pe); throw new LDIFException( ERR_READ_CANNOT_BASE64_DECODE_ATTR.get(attributeName, firstLineNumber, pe.getMessage()), firstLineNumber, true, ldifLines, pe); } } else if (line.charAt(colonPos+1) == '<') { // Skip over any spaces leading up to the value, and then the rest of // the string is a URL that indicates where to get the real content. // At the present time, we'll only support the file URLs. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } final byte[] urlBytes; final String urlString = line.substring(pos); try { urlBytes = retrieveURLBytes(urlString, relativeBasePath, firstLineNumber); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_URL_EXCEPTION.get(attributeName, urlString, firstLineNumber, e), firstLineNumber, true, ldifLines, e); } if (attrObject == null) { attr = new Attribute(attributeName, matchingRule, urlBytes); attributes.put(lowerName, attr); } else { try { if (! ldifAttr.addValue(new ASN1OctetString(urlBytes), duplicateValueBehavior)) { if (duplicateValueBehavior != DuplicateValueBehavior.STRIP) { throw new LDIFException(ERR_READ_DUPLICATE_VALUE.get(dn, firstLineNumber, attributeName), firstLineNumber, true, ldifLines, null); } } } catch (final LDIFException le) { Debug.debugException(le); throw le; } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_URL_EXCEPTION.get(attributeName, urlString, firstLineNumber, e), firstLineNumber, true, ldifLines, e); } } } else { // Skip over any spaces leading up to the value, and then the rest of // the string is the value. int pos = colonPos+1; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } final String valueString = line.substring(pos); if (attrObject == null) { attr = new Attribute(attributeName, matchingRule, valueString); attributes.put(lowerName, attr); } else { try { if (! ldifAttr.addValue(new ASN1OctetString(valueString), duplicateValueBehavior)) { if (duplicateValueBehavior != DuplicateValueBehavior.STRIP) { throw new LDIFException(ERR_READ_DUPLICATE_VALUE.get(dn, firstLineNumber, attributeName), firstLineNumber, true, ldifLines, null); } } } catch (final LDAPException le) { throw new LDIFException( ERR_READ_VALUE_SYNTAX_VIOLATION.get(dn, firstLineNumber, attributeName, StaticUtils.getExceptionMessage(le)), firstLineNumber, true, ldifLines, le); } } } } final ArrayList attrList = new ArrayList<>(attributes.size()); for (final Object o : attributes.values()) { if (o instanceof Attribute) { attrList.add((Attribute) o); } else { attrList.add(((LDIFAttribute) o).toAttribute()); } } return attrList; } /** * Retrieves the bytes that make up the file referenced by the given URL. * * @param urlString The string representation of the URL to retrieve. * @param relativeBasePath The base path that will be prepended to relative * paths in order to obtain an absolute path. * @param firstLineNumber The line number for the start of the record. * * @return The bytes contained in the specified file, or an empty array if * the specified file is empty. * * @throws LDIFException If the provided URL is malformed or references a * nonexistent file. * * @throws IOException If a problem is encountered while attempting to read * from the target file. */ @NotNull() private static byte[] retrieveURLBytes(@NotNull final String urlString, @NotNull final String relativeBasePath, final long firstLineNumber) throws LDIFException, IOException { int pos; final String path; final String lowerURLString = StaticUtils.toLowerCase(urlString); if (lowerURLString.startsWith("file:/")) { pos = 6; while ((pos < urlString.length()) && (urlString.charAt(pos) == '/')) { pos++; } path = urlString.substring(pos-1); } else if (lowerURLString.startsWith("file:")) { // A file: URL that doesn't include a slash will be interpreted as a // relative path. path = relativeBasePath + urlString.substring(5); } else { throw new LDIFException(ERR_READ_URL_INVALID_SCHEME.get(urlString), firstLineNumber, true); } final File f = new File(path); if (! f.exists()) { throw new LDIFException( ERR_READ_URL_NO_SUCH_FILE.get(urlString, f.getAbsolutePath()), firstLineNumber, true); } // In order to conserve memory, we'll only allow values to be read from // files no larger than 10 megabytes. final long fileSize = f.length(); if (fileSize > MAX_URL_FILE_SIZE) { throw new LDIFException( ERR_READ_URL_FILE_TOO_LARGE.get(urlString, f.getAbsolutePath(), MAX_URL_FILE_SIZE), firstLineNumber, true); } int fileBytesRemaining = (int) fileSize; final byte[] fileData = new byte[(int) fileSize]; final FileInputStream fis = new FileInputStream(f); try { int fileBytesRead = 0; while (fileBytesRead < fileSize) { final int bytesRead = fis.read(fileData, fileBytesRead, fileBytesRemaining); if (bytesRead < 0) { // We hit the end of the file before we expected to. This shouldn't // happen unless the file size changed since we first looked at it, // which we won't allow. throw new LDIFException( ERR_READ_URL_FILE_SIZE_CHANGED.get(urlString, f.getAbsolutePath()), firstLineNumber, true); } fileBytesRead += bytesRead; fileBytesRemaining -= bytesRead; } if (fis.read() != -1) { // There is still more data to read. This shouldn't happen unless the // file size changed since we first looked at it, which we won't allow. throw new LDIFException( ERR_READ_URL_FILE_SIZE_CHANGED.get(urlString, f.getAbsolutePath()), firstLineNumber, true); } } finally { fis.close(); } return fileData; } /** * Parses the data available through the provided iterator into an array of * modifications suitable for use in a modify change record. * * @param dn The DN of the entry being parsed. * @param trailingSpaceBehavior The behavior that should be exhibited when * encountering attribute values which are not * base64-encoded but contain trailing spaces. * @param ldifLines The lines that comprise the LDIF * representation of the full record being * parsed. * @param iterator The iterator to use to access the * modification data. * @param relativeBasePath The base path that will be prepended to * relative paths in order to obtain an * absolute path. * @param firstLineNumber The line number for the start of the record. * @param schema The schema to use in processing. * * @return An array containing the modifications that were read. * * @throws LDIFException If the provided LDIF data cannot be decoded as a * set of modifications. */ @NotNull() private static Modification[] parseModifications(@NotNull final String dn, @NotNull final TrailingSpaceBehavior trailingSpaceBehavior, @NotNull final ArrayList ldifLines, @NotNull final Iterator iterator, @NotNull final String relativeBasePath, final long firstLineNumber, @Nullable final Schema schema) throws LDIFException { final ArrayList modList = new ArrayList<>(ldifLines.size()); while (iterator.hasNext()) { // The first line must start with "add:", "delete:", "replace:", or // "increment:" followed by an attribute name. StringBuilder line = iterator.next(); handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior); int colonPos = line.indexOf(":"); if (colonPos < 0) { throw new LDIFException(ERR_READ_MOD_CR_NO_MODTYPE.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } final ModificationType modType; final String modTypeStr = StaticUtils.toLowerCase(line.substring(0, colonPos)); if (modTypeStr.equals("add")) { modType = ModificationType.ADD; } else if (modTypeStr.equals("delete")) { modType = ModificationType.DELETE; } else if (modTypeStr.equals("replace")) { modType = ModificationType.REPLACE; } else if (modTypeStr.equals("increment")) { modType = ModificationType.INCREMENT; } else { throw new LDIFException(ERR_READ_MOD_CR_INVALID_MODTYPE.get(modTypeStr, firstLineNumber), firstLineNumber, true, ldifLines, null); } String attributeName; int length = line.length(); if (length == (colonPos+1)) { // The colon was the last character on the line. This is not // acceptable. throw new LDIFException(ERR_READ_MOD_CR_MODTYPE_NO_ATTR.get( firstLineNumber), firstLineNumber, true, ldifLines, null); } else if (line.charAt(colonPos+1) == ':') { // Skip over any spaces leading up to the value, and then the rest of // the string is the base64-encoded attribute name. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } try { final byte[] dnBytes = Base64.decode(line.substring(pos)); attributeName = StaticUtils.toUTF8String(dnBytes); } catch (final ParseException pe) { Debug.debugException(pe); throw new LDIFException( ERR_READ_MOD_CR_MODTYPE_CANNOT_BASE64_DECODE_ATTR.get( firstLineNumber, pe.getMessage()), firstLineNumber, true, ldifLines, pe); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_MOD_CR_MODTYPE_CANNOT_BASE64_DECODE_ATTR.get( firstLineNumber, e), firstLineNumber, true, ldifLines, e); } } else { // Skip over any spaces leading up to the value, and then the rest of // the string is the attribute name. int pos = colonPos+1; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } attributeName = line.substring(pos); } if (attributeName.isEmpty()) { throw new LDIFException(ERR_READ_MOD_CR_MODTYPE_NO_ATTR.get( firstLineNumber), firstLineNumber, true, ldifLines, null); } // The next zero or more lines may be the set of attribute values. Keep // reading until we reach the end of the iterator or until we find a line // with just a "-". final ArrayList valueList = new ArrayList<>(ldifLines.size()); while (iterator.hasNext()) { line = iterator.next(); handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior); if (line.toString().equals("-")) { break; } colonPos = line.indexOf(":"); if (colonPos < 0) { throw new LDIFException(ERR_READ_NO_ATTR_COLON.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } else if (! line.substring(0, colonPos).equalsIgnoreCase(attributeName)) { // There are a couple of cases in which this might be acceptable: // - If the two names are logically equivalent, but have an alternate // name (or OID) for the target attribute type, or if there are // attribute options and the options are just in a different order. // - If this is the first value for the target attribute and the // alternate name includes a "binary" option that the original // attribute name did not have. In this case, all subsequent values // will also be required to have the binary option. final String alternateName = line.substring(0, colonPos); // Check to see if the base names are equivalent. boolean baseNameEquivalent = false; final String expectedBaseName = Attribute.getBaseName(attributeName); final String alternateBaseName = Attribute.getBaseName(alternateName); if (alternateBaseName.equalsIgnoreCase(expectedBaseName)) { baseNameEquivalent = true; } else { if (schema != null) { final AttributeTypeDefinition expectedAT = schema.getAttributeType(expectedBaseName); final AttributeTypeDefinition alternateAT = schema.getAttributeType(alternateBaseName); if ((expectedAT != null) && (alternateAT != null) && expectedAT.equals(alternateAT)) { baseNameEquivalent = true; } } } // Check to see if the attribute options are equivalent. final Set expectedOptions = Attribute.getOptions(attributeName); final Set lowerExpectedOptions = new HashSet<>( StaticUtils.computeMapCapacity(expectedOptions.size())); for (final String s : expectedOptions) { lowerExpectedOptions.add(StaticUtils.toLowerCase(s)); } final Set alternateOptions = Attribute.getOptions(alternateName); final Set lowerAlternateOptions = new HashSet<>( StaticUtils.computeMapCapacity(alternateOptions.size())); for (final String s : alternateOptions) { lowerAlternateOptions.add(StaticUtils.toLowerCase(s)); } final boolean optionsEquivalent = lowerAlternateOptions.equals(lowerExpectedOptions); if (baseNameEquivalent && optionsEquivalent) { // This is fine. The two attribute descriptions are logically // equivalent. We'll continue using the attribute description that // was provided first. } else if (valueList.isEmpty() && baseNameEquivalent && lowerAlternateOptions.remove("binary") && lowerAlternateOptions.equals(lowerExpectedOptions)) { // This means that the provided value is the first value for the // attribute, and that the only significant difference is that the // provided attribute description included an unexpected "binary" // option. We'll accept this, but will require any additional // values for this modification to also include the binary option, // and we'll use the binary option in the attribute that is // eventually created. attributeName = alternateName; } else { // This means that either the base names are different or the sets // of options are incompatible. This is not acceptable. throw new LDIFException(ERR_READ_MOD_CR_ATTR_MISMATCH.get( firstLineNumber, line.substring(0, colonPos), attributeName), firstLineNumber, true, ldifLines, null); } } length = line.length(); final ASN1OctetString value; if (length == (colonPos+1)) { // The colon was the last character on the line. This is fine. value = new ASN1OctetString(); } else if (line.charAt(colonPos+1) == ':') { // Skip over any spaces leading up to the value, and then the rest of // the string is the base64-encoded value. This is unusual and // unnecessary, but is nevertheless acceptable. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } try { value = new ASN1OctetString(Base64.decode(line.substring(pos))); } catch (final ParseException pe) { Debug.debugException(pe); throw new LDIFException(ERR_READ_CANNOT_BASE64_DECODE_ATTR.get( attributeName, firstLineNumber, pe.getMessage()), firstLineNumber, true, ldifLines, pe); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException(ERR_READ_CANNOT_BASE64_DECODE_ATTR.get( firstLineNumber, e), firstLineNumber, true, ldifLines, e); } } else if (line.charAt(colonPos+1) == '<') { // Skip over any spaces leading up to the value, and then the rest of // the string is a URL that indicates where to get the real content. // At the present time, we'll only support the file URLs. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } final String urlString = line.substring(pos); try { final byte[] urlBytes = retrieveURLBytes(urlString, relativeBasePath, firstLineNumber); value = new ASN1OctetString(urlBytes); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_URL_EXCEPTION.get(attributeName, urlString, firstLineNumber, e), firstLineNumber, true, ldifLines, e); } } else { // Skip over any spaces leading up to the value, and then the rest of // the string is the value. int pos = colonPos+1; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } value = new ASN1OctetString(line.substring(pos)); } valueList.add(value); } final ASN1OctetString[] values = new ASN1OctetString[valueList.size()]; valueList.toArray(values); // If it's an add modification type, then there must be at least one // value. if ((modType.intValue() == ModificationType.ADD.intValue()) && (values.length == 0)) { throw new LDIFException(ERR_READ_MOD_CR_NO_ADD_VALUES.get(attributeName, firstLineNumber), firstLineNumber, true, ldifLines, null); } // If it's an increment modification type, then there must be exactly one // value. if ((modType.intValue() == ModificationType.INCREMENT.intValue()) && (values.length != 1)) { throw new LDIFException(ERR_READ_MOD_CR_INVALID_INCR_VALUE_COUNT.get( firstLineNumber, attributeName), firstLineNumber, true, ldifLines, null); } modList.add(new Modification(modType, attributeName, values)); } final Modification[] mods = new Modification[modList.size()]; modList.toArray(mods); return mods; } /** * Parses the data available through the provided iterator as the body of a * modify DN change record (i.e., the newrdn, deleteoldrdn, and optional * newsuperior lines). * * @param ldifLines The lines that comprise the LDIF * representation of the full record being * parsed. * @param iterator The iterator to use to access the modify DN * data. * @param dn The current DN of the entry. * @param controls The set of controls to include in the change * record. * @param trailingSpaceBehavior The behavior that should be exhibited when * encountering attribute values which are not * base64-encoded but contain trailing spaces. * @param firstLineNumber The line number for the start of the record. * * @return The decoded modify DN change record. * * @throws LDIFException If the provided LDIF data cannot be decoded as a * modify DN change record. */ @NotNull() private static LDIFModifyDNChangeRecord parseModifyDNChangeRecord( @NotNull final ArrayList ldifLines, @NotNull final Iterator iterator, @NotNull final String dn, @Nullable final List controls, @NotNull final TrailingSpaceBehavior trailingSpaceBehavior, final long firstLineNumber) throws LDIFException { // The next line must be the new RDN, and it must start with "newrdn:". StringBuilder line = iterator.next(); handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior); int colonPos = line.indexOf(":"); if ((colonPos < 0) || (! line.substring(0, colonPos).equalsIgnoreCase("newrdn"))) { throw new LDIFException(ERR_READ_MODDN_CR_NO_NEWRDN_COLON.get( firstLineNumber), firstLineNumber, true, ldifLines, null); } final String newRDN; int length = line.length(); if (length == (colonPos+1)) { // The colon was the last character on the line. This is not acceptable. throw new LDIFException(ERR_READ_MODDN_CR_NO_NEWRDN_VALUE.get( firstLineNumber), firstLineNumber, true, ldifLines, null); } else if (line.charAt(colonPos+1) == ':') { // Skip over any spaces leading up to the value, and then the rest of the // string is the base64-encoded new RDN. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } try { final byte[] dnBytes = Base64.decode(line.substring(pos)); newRDN = StaticUtils.toUTF8String(dnBytes); } catch (final ParseException pe) { Debug.debugException(pe); throw new LDIFException( ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_NEWRDN.get(firstLineNumber, pe.getMessage()), firstLineNumber, true, ldifLines, pe); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_NEWRDN.get(firstLineNumber, e), firstLineNumber, true, ldifLines, e); } } else { // Skip over any spaces leading up to the value, and then the rest of the // string is the new RDN. int pos = colonPos+1; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } newRDN = line.substring(pos); } if (newRDN.isEmpty()) { throw new LDIFException(ERR_READ_MODDN_CR_NO_NEWRDN_VALUE.get( firstLineNumber), firstLineNumber, true, ldifLines, null); } // The next line must be the deleteOldRDN flag, and it must start with // 'deleteoldrdn:'. if (! iterator.hasNext()) { throw new LDIFException(ERR_READ_MODDN_CR_NO_DELOLDRDN_COLON.get( firstLineNumber), firstLineNumber, true, ldifLines, null); } line = iterator.next(); handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior); colonPos = line.indexOf(":"); if ((colonPos < 0) || (! line.substring(0, colonPos).equalsIgnoreCase("deleteoldrdn"))) { throw new LDIFException(ERR_READ_MODDN_CR_NO_DELOLDRDN_COLON.get( firstLineNumber), firstLineNumber, true, ldifLines, null); } final String deleteOldRDNStr; length = line.length(); if (length == (colonPos+1)) { // The colon was the last character on the line. This is not acceptable. throw new LDIFException(ERR_READ_MODDN_CR_NO_DELOLDRDN_VALUE.get( firstLineNumber), firstLineNumber, true, ldifLines, null); } else if (line.charAt(colonPos+1) == ':') { // Skip over any spaces leading up to the value, and then the rest of the // string is the base64-encoded value. This is unusual and // unnecessary, but is nevertheless acceptable. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } try { final byte[] changeTypeBytes = Base64.decode(line.substring(pos)); deleteOldRDNStr = StaticUtils.toUTF8String(changeTypeBytes); } catch (final ParseException pe) { Debug.debugException(pe); throw new LDIFException( ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_DELOLDRDN.get( firstLineNumber, pe.getMessage()), firstLineNumber, true, ldifLines, pe); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_DELOLDRDN.get( firstLineNumber, e), firstLineNumber, true, ldifLines, e); } } else { // Skip over any spaces leading up to the value, and then the rest of the // string is the value. int pos = colonPos+1; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } deleteOldRDNStr = line.substring(pos); } final boolean deleteOldRDN; if (deleteOldRDNStr.equals("0")) { deleteOldRDN = false; } else if (deleteOldRDNStr.equals("1")) { deleteOldRDN = true; } else if (deleteOldRDNStr.equalsIgnoreCase("false") || deleteOldRDNStr.equalsIgnoreCase("no")) { // This is technically illegal, but we'll allow it. deleteOldRDN = false; } else if (deleteOldRDNStr.equalsIgnoreCase("true") || deleteOldRDNStr.equalsIgnoreCase("yes")) { // This is also technically illegal, but we'll allow it. deleteOldRDN = false; } else { throw new LDIFException(ERR_READ_MODDN_CR_INVALID_DELOLDRDN.get( deleteOldRDNStr, firstLineNumber), firstLineNumber, true, ldifLines, null); } // If there is another line, then it must be the new superior DN and it must // start with "newsuperior:". If this is absent, then it's fine. final String newSuperiorDN; if (iterator.hasNext()) { line = iterator.next(); handleTrailingSpaces(line, dn, firstLineNumber, trailingSpaceBehavior); colonPos = line.indexOf(":"); if ((colonPos < 0) || (! line.substring(0, colonPos).equalsIgnoreCase("newsuperior"))) { throw new LDIFException(ERR_READ_MODDN_CR_NO_NEWSUPERIOR_COLON.get( firstLineNumber), firstLineNumber, true, ldifLines, null); } length = line.length(); if (length == (colonPos+1)) { // The colon was the last character on the line. This is fine. newSuperiorDN = ""; } else if (line.charAt(colonPos+1) == ':') { // Skip over any spaces leading up to the value, and then the rest of // the string is the base64-encoded new superior DN. int pos = colonPos+2; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } try { final byte[] dnBytes = Base64.decode(line.substring(pos)); newSuperiorDN = StaticUtils.toUTF8String(dnBytes); } catch (final ParseException pe) { Debug.debugException(pe); throw new LDIFException( ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_NEWSUPERIOR.get( firstLineNumber, pe.getMessage()), firstLineNumber, true, ldifLines, pe); } catch (final Exception e) { Debug.debugException(e); throw new LDIFException( ERR_READ_MODDN_CR_CANNOT_BASE64_DECODE_NEWSUPERIOR.get( firstLineNumber, e), firstLineNumber, true, ldifLines, e); } } else { // Skip over any spaces leading up to the value, and then the rest of // the string is the new superior DN. int pos = colonPos+1; while ((pos < length) && (line.charAt(pos) == ' ')) { pos++; } newSuperiorDN = line.substring(pos); } } else { newSuperiorDN = null; } // There must not be any more lines. if (iterator.hasNext()) { throw new LDIFException(ERR_READ_CR_EXTRA_MODDN_DATA.get(firstLineNumber), firstLineNumber, true, ldifLines, null); } return new LDIFModifyDNChangeRecord(dn, newRDN, deleteOldRDN, newSuperiorDN, controls); } /** * Examines the line contained in the provided buffer to determine whether it * may contain one or more illegal trailing spaces. If it does, then those * spaces will either be stripped out or an exception will be thrown to * indicate that they are illegal. * * @param buffer The buffer to be examined. * @param dn The DN of the LDIF record being parsed. It * may be {@code null} if the DN is not yet * known (e.g., because the provided line is * expected to contain that DN). * @param firstLineNumber The approximate line number in the LDIF * source on which the LDIF record begins. * @param trailingSpaceBehavior The behavior that should be exhibited when * encountering attribute values which are not * base64-encoded but contain trailing spaces. * * @throws LDIFException If the line contained in the provided buffer ends * with one or more illegal trailing spaces and * {@code stripTrailingSpaces} was provided with a * value of {@code false}. */ private static void handleTrailingSpaces(@NotNull final StringBuilder buffer, @Nullable final String dn, final long firstLineNumber, @NotNull final TrailingSpaceBehavior trailingSpaceBehavior) throws LDIFException { int pos = buffer.length() - 1; boolean trailingFound = false; while ((pos >= 0) && (buffer.charAt(pos) == ' ')) { trailingFound = true; pos--; } if (trailingFound && (buffer.charAt(pos) != ':')) { switch (trailingSpaceBehavior) { case STRIP: buffer.setLength(pos+1); break; case REJECT: if (dn == null) { throw new LDIFException( ERR_READ_ILLEGAL_TRAILING_SPACE_WITHOUT_DN.get(firstLineNumber, buffer.toString()), firstLineNumber, true); } else { throw new LDIFException( ERR_READ_ILLEGAL_TRAILING_SPACE_WITH_DN.get(dn, firstLineNumber, buffer.toString()), firstLineNumber, true); } case RETAIN: default: // No action will be taken. break; } } } /** * This represents an unparsed LDIFRecord. It stores the line number of the * first line of the record and each line of the record. */ private static final class UnparsedLDIFRecord { // The list of lines contained in this record. @Nullable private final ArrayList lineList; // The first line number for the LDIF record. private final long firstLineNumber; // The exception thrown while reading from the input. @Nullable private final Exception failureCause; // Indicates whether the end of the input has been reached. private final boolean isEOF; // The duplicate value behavior to use when parsing the record. @NotNull private final DuplicateValueBehavior duplicateValueBehavior; // The schema to use when parsing the record. @Nullable private final Schema schema; // The trailing space behavior to use when parsing the record. @NotNull private final TrailingSpaceBehavior trailingSpaceBehavior; /** * Creates a new instance of this record. * * @param lineList The lines that comprise the LDIF record. * @param duplicateValueBehavior The behavior to exhibit if the entry * contains duplicate attribute values. * @param trailingSpaceBehavior Specifies the behavior to exhibit when * encountering trailing spaces in * non-base64-encoded attribute values. * @param schema The schema to use when parsing, if * applicable. * @param firstLineNumber The first line number of the LDIF record. */ private UnparsedLDIFRecord(@NotNull final ArrayList lineList, @NotNull final DuplicateValueBehavior duplicateValueBehavior, @NotNull final TrailingSpaceBehavior trailingSpaceBehavior, @Nullable final Schema schema, final long firstLineNumber) { this.lineList = lineList; this.firstLineNumber = firstLineNumber; this.duplicateValueBehavior = duplicateValueBehavior; this.trailingSpaceBehavior = trailingSpaceBehavior; this.schema = schema; failureCause = null; isEOF = (firstLineNumber < 0) || ((lineList != null) && lineList.isEmpty()); } /** * Creates a new instance of this record. * * @param failureCause The Exception thrown when reading from the input. */ private UnparsedLDIFRecord(@NotNull final Exception failureCause) { this.failureCause = failureCause; lineList = null; firstLineNumber = 0; duplicateValueBehavior = DuplicateValueBehavior.REJECT; trailingSpaceBehavior = TrailingSpaceBehavior.REJECT; schema = null; isEOF = false; } /** * Return the lines that comprise the LDIF record. * * @return The lines that comprise the LDIF record, or {@code null} if this * is a failure record. */ @Nullable() private ArrayList getLineList() { return lineList; } /** * Retrieves the behavior to exhibit when encountering duplicate attribute * values. * * @return The behavior to exhibit when encountering duplicate attribute * values. */ @NotNull() private DuplicateValueBehavior getDuplicateValueBehavior() { return duplicateValueBehavior; } /** * Retrieves the behavior that should be exhibited when encountering * attribute values which are not base64-encoded but contain trailing * spaces. The LDIF specification strongly recommends that any value which * legitimately contains trailing spaces be base64-encoded, but the LDAP SDK * LDIF parser may be configured to automatically strip these spaces, to * preserve them, or to reject any entry or change record containing them. * * @return The behavior that should be exhibited when encountering * attribute values which are not base64-encoded but contain * trailing spaces. */ @NotNull() private TrailingSpaceBehavior getTrailingSpaceBehavior() { return trailingSpaceBehavior; } /** * Retrieves the schema that should be used when parsing the record, if * applicable. * * @return The schema that should be used when parsing the record, or * {@code null} if none should be used. */ @Nullable() private Schema getSchema() { return schema; } /** * Return the first line number of the LDIF record. * * @return The first line number of the LDIF record. */ private long getFirstLineNumber() { return firstLineNumber; } /** * Return {@code true} iff the end of the input was reached. * * @return {@code true} iff the end of the input was reached. */ private boolean isEOF() { return isEOF; } /** * Returns the reason that reading the record lines failed. This normally * is only non-null if something bad happened to the input stream (like * a disk read error). * * @return The reason that reading the record lines failed. */ @Nullable() private Exception getFailureCause() { return failureCause; } } /** * When processing in asynchronous mode, this thread is responsible for * reading the raw unparsed records from the input and submitting them for * processing. */ private final class LineReaderThread extends Thread { /** * Creates a new instance of this thread. */ private LineReaderThread() { super("Asynchronous LDIF line reader"); setDaemon(true); } /** * Reads raw, unparsed records from the input and submits them for * processing until the input is finished or closed. */ @Override() public void run() { try { boolean stopProcessing = false; while (!stopProcessing) { UnparsedLDIFRecord unparsedRecord; try { unparsedRecord = readUnparsedRecord(); } catch (final IOException e) { Debug.debugException(e); unparsedRecord = new UnparsedLDIFRecord(e); stopProcessing = true; } catch (final Exception e) { Debug.debugException(e); unparsedRecord = new UnparsedLDIFRecord(e); } try { asyncParser.submit(unparsedRecord); } catch (final InterruptedException e) { Debug.debugException(e); // If this thread is interrupted, then someone wants us to stop // processing, so that's what we'll do. Thread.currentThread().interrupt(); stopProcessing = true; } if ((unparsedRecord == null) || unparsedRecord.isEOF()) { stopProcessing = true; } } } finally { try { asyncParser.shutdown(); } catch (final InterruptedException e) { Debug.debugException(e); Thread.currentThread().interrupt(); } finally { asyncParsingComplete.set(true); } } } } /** * Used to parse Records asynchronously. */ private final class RecordParser implements Processor { /** * {@inheritDoc} */ @Override() public LDIFRecord process(@NotNull final UnparsedLDIFRecord input) throws LDIFException { LDIFRecord record = decodeRecord(input, relativeBasePath, schema); if ((record instanceof Entry) && (entryTranslator != null)) { record = entryTranslator.translate((Entry) record, input.getFirstLineNumber()); if (record == null) { record = SKIP_ENTRY; } } if ((record instanceof LDIFChangeRecord) && (changeRecordTranslator != null)) { record = changeRecordTranslator.translate((LDIFChangeRecord) record, input.getFirstLineNumber()); if (record == null) { record = SKIP_ENTRY; } } return record; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy