From d9722bcadc7bf619808426fc82cbb0c74b1646b0 Mon Sep 17 00:00:00 2001
From: matthew_swift <matthew_swift@localhost>
Date: Thu, 09 Sep 2010 17:31:59 +0000
Subject: [PATCH] Make EntryReader and ChangeRecordReader APIs easier to use:

---
 sdk/src/com/sun/opends/sdk/tools/LDAPModify.java                                  |    9 
 sdk/src/org/opends/sdk/ldif/ChangeRecordReader.java                               |   33 +
 sdk/src/org/opends/sdk/ldif/ConnectionEntryReader.java                            |  224 +++++++++++---
 sdk/tests/unit-tests-testng/src/org/opends/sdk/SynchronousConnectionTestCase.java |   29 +
 sdk/src/org/opends/sdk/ldif/EntryReader.java                                      |   31 +
 sdk/src/org/opends/sdk/ldif/AbstractLDIFReader.java                               |    9 
 sdk/src/org/opends/sdk/ldif/LDIFChangeRecordReader.java                           |  265 ++++++++++-------
 sdk/tests/unit-tests-testng/src/org/opends/sdk/ldif/LDIFEntryReaderTestCase.java  |   76 ++++
 sdk/src/org/opends/sdk/ldif/LDIFEntryReader.java                                  |  199 ++++++++-----
 9 files changed, 589 insertions(+), 286 deletions(-)

diff --git a/sdk/src/com/sun/opends/sdk/tools/LDAPModify.java b/sdk/src/com/sun/opends/sdk/tools/LDAPModify.java
index 885a450..c62ad71 100644
--- a/sdk/src/com/sun/opends/sdk/tools/LDAPModify.java
+++ b/sdk/src/com/sun/opends/sdk/tools/LDAPModify.java
@@ -22,7 +22,7 @@
  * CDDL HEADER END
  *
  *
- *      Copyright 2009 Sun Microsystems, Inc.
+ *      Copyright 2009-2010 Sun Microsystems, Inc.
  */
 
 package com.sun.opends.sdk.tools;
@@ -762,13 +762,12 @@
         reader = new LDIFChangeRecordReader(getInputStream());
       }
 
-      ChangeRecord cr;
       try
       {
-        int result;
-        while ((cr = reader.readChangeRecord()) != null)
+        while (reader.hasNext())
         {
-          result = cr.accept(visitor, null);
+          final ChangeRecord cr = reader.readChangeRecord();
+          final int result = cr.accept(visitor, null);
           if (result != 0 && !continueOnError.isPresent())
           {
             return result;
diff --git a/sdk/src/org/opends/sdk/ldif/AbstractLDIFReader.java b/sdk/src/org/opends/sdk/ldif/AbstractLDIFReader.java
index 8ff8698..418b08a 100644
--- a/sdk/src/org/opends/sdk/ldif/AbstractLDIFReader.java
+++ b/sdk/src/org/opends/sdk/ldif/AbstractLDIFReader.java
@@ -22,7 +22,7 @@
  * CDDL HEADER END
  *
  *
- *      Copyright 2009 Sun Microsystems, Inc.
+ *      Copyright 2009-2010 Sun Microsystems, Inc.
  */
 
 package org.opends.sdk.ldif;
@@ -141,8 +141,11 @@
      */
     public void close() throws IOException
     {
-      reader.close();
-      reader = null;
+      if (reader != null)
+      {
+        reader.close();
+        reader = null;
+      }
     }
 
 
diff --git a/sdk/src/org/opends/sdk/ldif/ChangeRecordReader.java b/sdk/src/org/opends/sdk/ldif/ChangeRecordReader.java
index 0e73da8..5364319 100644
--- a/sdk/src/org/opends/sdk/ldif/ChangeRecordReader.java
+++ b/sdk/src/org/opends/sdk/ldif/ChangeRecordReader.java
@@ -22,7 +22,7 @@
  * CDDL HEADER END
  *
  *
- *      Copyright 2009 Sun Microsystems, Inc.
+ *      Copyright 2009-2010 Sun Microsystems, Inc.
  */
 
 package org.opends.sdk.ldif;
@@ -31,8 +31,7 @@
 
 import java.io.Closeable;
 import java.io.IOException;
-
-import org.opends.sdk.DecodeException;
+import java.util.NoSuchElementException;
 
 
 
@@ -46,10 +45,6 @@
  * malformed change records and, if it is possible, how they are handled.
  * <li>Any synchronization limitations.
  * </ul>
- * <p>
- * TODO: LDIFInputStreamReader
- * <p>
- * TODO: SearchResultEntryReader
  */
 public interface ChangeRecordReader extends Closeable
 {
@@ -62,23 +57,35 @@
    * @throws IOException
    *           If an unexpected IO error occurred while closing.
    */
+  @Override
   void close() throws IOException;
 
 
 
   /**
+   * Returns {@code true} if this reader contains another change record,
+   * blocking if necessary until either the next change record is available or
+   * the end of the stream is reached.
+   *
+   * @return {@code true} if this reader contains another change record.
+   * @throws IOException
+   *           If an unexpected IO error occurred.
+   */
+  boolean hasNext() throws IOException;
+
+
+
+  /**
    * Reads the next change record, blocking if necessary until a change record
    * is available. If the next change record does not contain a change type then
    * it will be treated as an {@code Add} change record.
    *
-   * @return The next change record, or {@code null} if there are no more change
-   *         records to be read.
-   * @throws DecodeException
-   *           If the change record could not be decoded because it was
-   *           malformed.
+   * @return The next change record.
    * @throws IOException
    *           If an unexpected IO error occurred while reading the change
    *           record.
+   * @throws NoSuchElementException
+   *           If this reader does not contain any more change records.
    */
-  ChangeRecord readChangeRecord() throws DecodeException, IOException;
+  ChangeRecord readChangeRecord() throws IOException, NoSuchElementException;
 }
diff --git a/sdk/src/org/opends/sdk/ldif/ConnectionEntryReader.java b/sdk/src/org/opends/sdk/ldif/ConnectionEntryReader.java
index 17a0be2..8e6e353 100644
--- a/sdk/src/org/opends/sdk/ldif/ConnectionEntryReader.java
+++ b/sdk/src/org/opends/sdk/ldif/ConnectionEntryReader.java
@@ -30,6 +30,7 @@
 
 
 import java.io.InterruptedIOException;
+import java.util.NoSuchElementException;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
@@ -60,21 +61,29 @@
  *
  * <pre>
  * Connection connection = ...;
- * ConnectionEntryReader results = connection.search(
- *     &quot;dc=example,dc=com&quot;,
- *     SearchScope.WHOLE_SUBTREE,
- *     &quot;(objectClass=person)&quot;);
- * SearchResultEntry entry;
+ * ConnectionEntryReader results = connection.search(&quot;dc=example,dc=com&quot;,
+ *     SearchScope.WHOLE_SUBTREE, &quot;(objectClass=person)&quot;);
  * try
  * {
- *   while ((entry = results.readEntry()) != null)
+ *   while (reader.hasNext())
  *   {
- *     // Process search result entry.
+ *     if (!reader.isReference())
+ *     {
+ *       SearchResultEntry entry = reader.readEntry();
+ *
+ *       // Handle entry...
+ *     }
+ *     else
+ *     {
+ *       SearchResultReference ref = reader.readReference();
+ *
+ *       // Handle continuation reference...
+ *     }
  *   }
  * }
- * catch (Exception e)
+ * catch (IOException e)
  * {
- *   // Handle exceptions
+ *   // Handle exceptions...
  * }
  * finally
  * {
@@ -181,6 +190,7 @@
 
   private final BufferHandler buffer;
   private final FutureResult<Result> future;
+  private Response nextResponse = null;
 
 
 
@@ -242,65 +252,69 @@
 
 
   /**
-   * Returns the next search result entry contained in the search results,
-   * waiting if necessary until one becomes available.
+   * {@inheritDoc}
+   */
+  @Override
+  public boolean hasNext() throws ErrorResultIOException,
+      InterruptedIOException
+  {
+    // Poll for the next response if needed.
+    final Response r = getNextResponse();
+    if (!(r instanceof Result))
+    {
+      // Entry or reference.
+      return true;
+    }
+
+    // Final result.
+    final Result result = (Result) r;
+    if (result.isSuccess())
+    {
+      return false;
+    }
+
+    final ErrorResultException e = ErrorResultException.wrap(result);
+    throw new ErrorResultIOException(e);
+  }
+
+
+
+  /**
+   * Waits for the next search result entry or reference to become available and
+   * returns {@code true} if it is a reference, or {@code false} if it is an
+   * entry.
    *
-   * @return The next search result entry, or {@code null} if there are no more
-   *         entries in the search results.
-   * @throws SearchResultReferenceIOException
-   *           If the next search response was a search result reference. This
-   *           connection entry reader may still contain remaining search
-   *           results and references which can be retrieved using additional
-   *           calls to this method.
+   * @return {@code true} if the next search result is a reference, or
+   *         {@code false} if it is an entry.
    * @throws ErrorResultIOException
-   *           If the result code indicates that the search operation failed for
+   *           If there are no more search result entries or references and the
+   *           search result code indicates that the search operation failed for
    *           some reason.
    * @throws InterruptedIOException
    *           If the current thread was interrupted while waiting.
+   * @throws NoSuchElementException
+   *           If there are no more search result entries or references and the
+   *           search result code indicates that the search operation succeeded.
    */
-  @Override
-  public SearchResultEntry readEntry() throws SearchResultReferenceIOException,
-      ErrorResultIOException, InterruptedIOException
+  public boolean isReference() throws ErrorResultIOException,
+      InterruptedIOException, NoSuchElementException
   {
-    Response r;
-    try
+    // Throws ErrorResultIOException if search returned error.
+    if (!hasNext())
     {
-      while ((r = buffer.responses.poll(50, TimeUnit.MILLISECONDS)) == null)
-      {
-        if (buffer.isInterrupted)
-        {
-          // The worker thread processing the result was interrupted so no
-          // result will ever arrive. We don't want to hang this thread forever
-          // while we wait, so terminate now.
-          r = Responses.newResult(ResultCode.CLIENT_SIDE_LOCAL_ERROR);
-          break;
-        }
-      }
-    }
-    catch (final InterruptedException e)
-    {
-      throw new InterruptedIOException(e.getMessage());
+      // Search has completed successfully.
+      throw new NoSuchElementException();
     }
 
+    // Entry or reference.
+    final Response r = nextResponse;
     if (r instanceof SearchResultEntry)
     {
-      return (SearchResultEntry) r;
+      return false;
     }
     else if (r instanceof SearchResultReference)
     {
-      throw new SearchResultReferenceIOException((SearchResultReference) r);
-    }
-    else if (r instanceof Result)
-    {
-      final Result result = (Result) r;
-      if (result.isSuccess())
-      {
-        return null;
-      }
-      else
-      {
-        throw new ErrorResultIOException(ErrorResultException.wrap(result));
-      }
+      return true;
     }
     else
     {
@@ -308,4 +322,108 @@
           + r.getClass().toString());
     }
   }
+
+
+
+  /**
+   * Waits for the next search result entry or reference to become available
+   * and, if it is an entry, returns it as a {@code SearchResultEntry}. If the
+   * next search response is a reference then this method will throw a
+   * {@code SearchResultReferenceIOException}.
+   *
+   * @return The next search result entry.
+   * @throws SearchResultReferenceIOException
+   *           If the next search response was a search result reference. This
+   *           connection entry reader may still contain remaining search
+   *           results and references which can be retrieved using additional
+   *           calls to this method.
+   * @throws ErrorResultIOException
+   *           If there are no more search result entries or references and the
+   *           search result code indicates that the search operation failed for
+   *           some reason.
+   * @throws InterruptedIOException
+   *           If the current thread was interrupted while waiting.
+   * @throws NoSuchElementException
+   *           If there are no more search result entries or references and the
+   *           search result code indicates that the search operation succeeded.
+   */
+  @Override
+  public SearchResultEntry readEntry() throws SearchResultReferenceIOException,
+      ErrorResultIOException, InterruptedIOException, NoSuchElementException
+  {
+    if (!isReference())
+    {
+      final SearchResultEntry entry = (SearchResultEntry) nextResponse;
+      nextResponse = null;
+      return entry;
+    }
+    else
+    {
+      final SearchResultReference reference = (SearchResultReference) nextResponse;
+      nextResponse = null;
+      throw new SearchResultReferenceIOException(reference);
+    }
+  }
+
+
+
+  /**
+   * Waits for the next search result entry or reference to become available
+   * and, if it is a reference, returns it as a {@code SearchResultReference}.
+   * If the next search response is an entry then this method will return
+   * {@code null}.
+   *
+   * @return The next search result reference, or {@code null} if the next
+   *         response was a search result entry.
+   * @throws ErrorResultIOException
+   *           If there are no more search result entries or references and the
+   *           search result code indicates that the search operation failed for
+   *           some reason.
+   * @throws InterruptedIOException
+   *           If the current thread was interrupted while waiting.
+   * @throws NoSuchElementException
+   *           If there are no more search result entries or references and the
+   *           search result code indicates that the search operation succeeded.
+   */
+  public SearchResultReference readReference() throws ErrorResultIOException,
+      InterruptedIOException, NoSuchElementException
+  {
+    if (isReference())
+    {
+      final SearchResultReference reference = (SearchResultReference) nextResponse;
+      nextResponse = null;
+      return reference;
+    }
+    else
+    {
+      return null;
+    }
+  }
+
+
+
+  private Response getNextResponse() throws InterruptedIOException
+  {
+    while (nextResponse == null)
+    {
+      try
+      {
+        nextResponse = buffer.responses.poll(50, TimeUnit.MILLISECONDS);
+      }
+      catch (final InterruptedException e)
+      {
+        throw new InterruptedIOException(e.getMessage());
+      }
+
+      if (nextResponse == null && buffer.isInterrupted)
+      {
+        // The worker thread processing the result was interrupted so no
+        // result will ever arrive. We don't want to hang this thread
+        // forever while we wait, so terminate now.
+        nextResponse = Responses.newResult(ResultCode.CLIENT_SIDE_LOCAL_ERROR);
+        break;
+      }
+    }
+    return nextResponse;
+  }
 }
diff --git a/sdk/src/org/opends/sdk/ldif/EntryReader.java b/sdk/src/org/opends/sdk/ldif/EntryReader.java
index a42c293..688b827 100644
--- a/sdk/src/org/opends/sdk/ldif/EntryReader.java
+++ b/sdk/src/org/opends/sdk/ldif/EntryReader.java
@@ -22,7 +22,7 @@
  * CDDL HEADER END
  *
  *
- *      Copyright 2009 Sun Microsystems, Inc.
+ *      Copyright 2009-2010 Sun Microsystems, Inc.
  */
 
 package org.opends.sdk.ldif;
@@ -31,8 +31,8 @@
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.util.NoSuchElementException;
 
-import org.opends.sdk.DecodeException;
 import org.opends.sdk.Entry;
 
 
@@ -46,10 +46,6 @@
  * malformed change records and, if it is possible, how they are handled.
  * <li>Any synchronization limitations.
  * </ul>
- * <p>
- * TODO: LDIFInputStreamReader
- * <p>
- * TODO: SearchResultEntryReader
  */
 public interface EntryReader extends Closeable
 {
@@ -62,19 +58,32 @@
    * @throws IOException
    *           If an unexpected IO error occurred while closing.
    */
+  @Override
   void close() throws IOException;
 
 
 
   /**
+   * Returns {@code true} if this reader contains another entry, blocking if
+   * necessary until either the next entry is available or the end of the stream
+   * is reached.
+   *
+   * @return {@code true} if this reader contains another entry.
+   * @throws IOException
+   *           If an unexpected IO error occurred.
+   */
+  boolean hasNext() throws IOException;
+
+
+
+  /**
    * Reads the next entry, blocking if necessary until an entry is available.
    *
-   * @return The next entry or {@code null} if there are no more entries to be
-   *         read.
-   * @throws DecodeException
-   *           If the entry could not be decoded because it was malformed.
+   * @return The next entry.
    * @throws IOException
    *           If an unexpected IO error occurred while reading the entry.
+   * @throws NoSuchElementException
+   *           If this reader does not contain any more entries.
    */
-  Entry readEntry() throws DecodeException, IOException;
+  Entry readEntry() throws IOException, NoSuchElementException;
 }
diff --git a/sdk/src/org/opends/sdk/ldif/LDIFChangeRecordReader.java b/sdk/src/org/opends/sdk/ldif/LDIFChangeRecordReader.java
index 0825276..3a8fec6 100644
--- a/sdk/src/org/opends/sdk/ldif/LDIFChangeRecordReader.java
+++ b/sdk/src/org/opends/sdk/ldif/LDIFChangeRecordReader.java
@@ -22,7 +22,7 @@
  * CDDL HEADER END
  *
  *
- *      Copyright 2009 Sun Microsystems, Inc.
+ *      Copyright 2009-2010 Sun Microsystems, Inc.
  */
 
 package org.opends.sdk.ldif;
@@ -37,6 +37,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.NoSuchElementException;
 
 import org.opends.sdk.*;
 import org.opends.sdk.requests.ModifyDNRequest;
@@ -79,9 +80,7 @@
     final LDIFChangeRecordReader reader = new LDIFChangeRecordReader(ldifLines);
     try
     {
-      final ChangeRecord record = reader.readChangeRecord();
-
-      if (record == null)
+      if (!reader.hasNext())
       {
         // No change record found.
         final LocalizableMessage message = WARN_READ_LDIF_RECORD_NO_CHANGE_RECORD_FOUND
@@ -89,7 +88,9 @@
         throw new LocalizedIllegalArgumentException(message);
       }
 
-      if (reader.readChangeRecord() != null)
+      final ChangeRecord record = reader.readChangeRecord();
+
+      if (reader.hasNext())
       {
         // Multiple change records found.
         final LocalizableMessage message = WARN_READ_LDIF_RECORD_MULTIPLE_CHANGE_RECORDS_FOUND
@@ -115,6 +116,13 @@
 
 
 
+  private ChangeRecord nextChangeRecord = null;
+
+  // Poison used to indicate end of LDIF.
+  private static final ChangeRecord EOF = Requests.newAddRequest(DN.rootDN());
+
+
+
   /**
    * Creates a new LDIF change record reader whose source is the provided input
    * stream.
@@ -169,6 +177,7 @@
   /**
    * {@inheritDoc}
    */
+  @Override
   public void close() throws IOException
   {
     close0();
@@ -178,110 +187,37 @@
 
   /**
    * {@inheritDoc}
+   *
+   * @throws DecodeException
+   *           If the change record could not be decoded because it was
+   *           malformed.
    */
+  @Override
+  public boolean hasNext() throws DecodeException, IOException
+  {
+    return getNextChangeRecord() != EOF;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws DecodeException
+   *           If the entry could not be decoded because it was malformed.
+   */
+  @Override
   public ChangeRecord readChangeRecord() throws DecodeException, IOException
   {
-    // Continue until an unfiltered entry is obtained.
-    while (true)
+    if (!hasNext())
     {
-      LDIFRecord record = null;
-
-      // Read the set of lines that make up the next entry.
-      record = readLDIFRecord();
-      if (record == null)
-      {
-        return null;
-      }
-
-      // Read the DN of the entry and see if it is one that should be
-      // included in the import.
-      DN entryDN;
-      try
-      {
-        entryDN = readLDIFRecordDN(record);
-        if (entryDN == null)
-        {
-          // Skip version record.
-          continue;
-        }
-      }
-      catch (final DecodeException e)
-      {
-        rejectLDIFRecord(record, e.getMessageObject());
-        continue;
-      }
-
-      // Skip if branch containing the entry DN is excluded.
-      if (isBranchExcluded(entryDN))
-      {
-        final LocalizableMessage message = LocalizableMessage
-            .raw("Skipping entry because it is in excluded branch");
-        skipLDIFRecord(record, message);
-        continue;
-      }
-
-      ChangeRecord changeRecord = null;
-      try
-      {
-        if (!record.iterator.hasNext())
-        {
-          // FIXME: improve error.
-          final LocalizableMessage message = LocalizableMessage
-              .raw("Missing changetype");
-          throw DecodeException.error(message);
-        }
-
-        final KeyValuePair pair = new KeyValuePair();
-        final String ldifLine = readLDIFRecordKeyValuePair(record, pair, false);
-
-        if (!toLowerCase(pair.key).equals("changetype"))
-        {
-          // Default to add change record.
-          changeRecord = parseAddChangeRecordEntry(entryDN, ldifLine, record);
-        }
-        else
-        {
-          final String changeType = toLowerCase(pair.value);
-          if (changeType.equals("add"))
-          {
-            changeRecord = parseAddChangeRecordEntry(entryDN, null, record);
-          }
-          else if (changeType.equals("delete"))
-          {
-            changeRecord = parseDeleteChangeRecordEntry(entryDN, record);
-          }
-          else if (changeType.equals("modify"))
-          {
-            changeRecord = parseModifyChangeRecordEntry(entryDN, record);
-          }
-          else if (changeType.equals("modrdn"))
-          {
-            changeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
-          }
-          else if (changeType.equals("moddn"))
-          {
-            changeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
-          }
-          else
-          {
-            // FIXME: improve error.
-            final LocalizableMessage message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE
-                .get(pair.value, "add, delete, modify, moddn, modrdn");
-            throw DecodeException.error(message);
-          }
-        }
-      }
-      catch (final DecodeException e)
-      {
-        rejectLDIFRecord(record, e.getMessageObject());
-        continue;
-      }
-
-      if (changeRecord != null)
-      {
-        return changeRecord;
-      }
+      // LDIF reader has completed successfully.
+      throw new NoSuchElementException();
     }
+
+    final ChangeRecord changeRecord = nextChangeRecord;
+    nextChangeRecord = null;
+    return changeRecord;
   }
 
 
@@ -309,8 +245,8 @@
    * change records that are read from LDIF. The default is {@code false}.
    *
    * @param excludeUserAttributes
-   *          {@code true} if all user attributes should be excluded, or {@code
-   *          false} otherwise.
+   *          {@code true} if all user attributes should be excluded, or
+   *          {@code false} otherwise.
    * @return A reference to this {@code LDIFChangeRecordReader}.
    */
   public LDIFChangeRecordReader setExcludeAllUserAttributes(
@@ -418,8 +354,8 @@
    * records that are read from LDIF. The default is {@code true} .
    *
    * @param validateSchema
-   *          {@code true} if schema validation should be performed, or {@code
-   *          false} otherwise.
+   *          {@code true} if schema validation should be performed, or
+   *          {@code false} otherwise.
    * @return A reference to this {@code LDIFChangeRecordReader}.
    */
   public LDIFChangeRecordReader setValidateSchema(final boolean validateSchema)
@@ -430,6 +366,115 @@
 
 
 
+  private ChangeRecord getNextChangeRecord() throws DecodeException,
+      IOException
+  {
+    while (nextChangeRecord == null)
+    {
+      LDIFRecord record = null;
+
+      // Read the set of lines that make up the next entry.
+      record = readLDIFRecord();
+      if (record == null)
+      {
+        nextChangeRecord = EOF;
+        break;
+      }
+
+      // Read the DN of the entry and see if it is one that should be
+      // included in the import.
+      DN entryDN;
+      try
+      {
+        entryDN = readLDIFRecordDN(record);
+        if (entryDN == null)
+        {
+          // Skip version record.
+          continue;
+        }
+      }
+      catch (final DecodeException e)
+      {
+        rejectLDIFRecord(record, e.getMessageObject());
+        continue;
+      }
+
+      // Skip if branch containing the entry DN is excluded.
+      if (isBranchExcluded(entryDN))
+      {
+        final LocalizableMessage message = LocalizableMessage
+            .raw("Skipping entry because it is in excluded branch");
+        skipLDIFRecord(record, message);
+        continue;
+      }
+
+      ChangeRecord changeRecord = null;
+      try
+      {
+        if (!record.iterator.hasNext())
+        {
+          // FIXME: improve error.
+          final LocalizableMessage message = LocalizableMessage
+              .raw("Missing changetype");
+          throw DecodeException.error(message);
+        }
+
+        final KeyValuePair pair = new KeyValuePair();
+        final String ldifLine = readLDIFRecordKeyValuePair(record, pair, false);
+
+        if (!toLowerCase(pair.key).equals("changetype"))
+        {
+          // Default to add change record.
+          changeRecord = parseAddChangeRecordEntry(entryDN, ldifLine, record);
+        }
+        else
+        {
+          final String changeType = toLowerCase(pair.value);
+          if (changeType.equals("add"))
+          {
+            changeRecord = parseAddChangeRecordEntry(entryDN, null, record);
+          }
+          else if (changeType.equals("delete"))
+          {
+            changeRecord = parseDeleteChangeRecordEntry(entryDN, record);
+          }
+          else if (changeType.equals("modify"))
+          {
+            changeRecord = parseModifyChangeRecordEntry(entryDN, record);
+          }
+          else if (changeType.equals("modrdn"))
+          {
+            changeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
+          }
+          else if (changeType.equals("moddn"))
+          {
+            changeRecord = parseModifyDNChangeRecordEntry(entryDN, record);
+          }
+          else
+          {
+            // FIXME: improve error.
+            final LocalizableMessage message = ERR_LDIF_INVALID_CHANGETYPE_ATTRIBUTE
+                .get(pair.value, "add, delete, modify, moddn, modrdn");
+            throw DecodeException.error(message);
+          }
+        }
+      }
+      catch (final DecodeException e)
+      {
+        rejectLDIFRecord(record, e.getMessageObject());
+        continue;
+      }
+
+      if (changeRecord != null)
+      {
+        nextChangeRecord = changeRecord;
+      }
+    }
+    return nextChangeRecord;
+  }
+
+
+
   private ChangeRecord parseAddChangeRecordEntry(final DN entryDN,
       final String lastLDIFLine, final LDIFRecord record)
       throws DecodeException
@@ -581,8 +626,8 @@
         {
           // TODO: include line number.
           final LocalizableMessage message = ERR_LDIF_INVALID_CHANGERECORD_ATTRIBUTE
-              .get(attributeDescription2.toString(), attributeDescription
-                  .toString());
+              .get(attributeDescription2.toString(),
+                  attributeDescription.toString());
           throw DecodeException.error(message);
         }
 
diff --git a/sdk/src/org/opends/sdk/ldif/LDIFEntryReader.java b/sdk/src/org/opends/sdk/ldif/LDIFEntryReader.java
index 4eb257c..b51cd73 100644
--- a/sdk/src/org/opends/sdk/ldif/LDIFEntryReader.java
+++ b/sdk/src/org/opends/sdk/ldif/LDIFEntryReader.java
@@ -22,24 +22,20 @@
  * CDDL HEADER END
  *
  *
- *      Copyright 2009 Sun Microsystems, Inc.
+ *      Copyright 2009-2010 Sun Microsystems, Inc.
  */
 
 package org.opends.sdk.ldif;
 
 
 
-import static com.sun.opends.sdk.messages.Messages.
-  WARN_READ_LDIF_RECORD_MULTIPLE_CHANGE_RECORDS_FOUND;
-import static com.sun.opends.sdk.messages.Messages.
-  WARN_READ_LDIF_RECORD_NO_CHANGE_RECORD_FOUND;
-import static com.sun.opends.sdk.messages.Messages.
-  WARN_READ_LDIF_RECORD_UNEXPECTED_IO_ERROR;
+import static com.sun.opends.sdk.messages.Messages.*;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Arrays;
 import java.util.List;
+import java.util.NoSuchElementException;
 
 import org.opends.sdk.*;
 import org.opends.sdk.schema.Schema;
@@ -58,6 +54,11 @@
 public final class LDIFEntryReader extends AbstractLDIFReader implements
     EntryReader
 {
+  // Poison used to indicate end of LDIF.
+  private static final Entry EOF = new LinkedHashMapEntry();
+
+
+
   /**
    * Parses the provided array of LDIF lines as a single LDIF entry.
    *
@@ -77,9 +78,7 @@
     final LDIFEntryReader reader = new LDIFEntryReader(ldifLines);
     try
     {
-      final Entry entry = reader.readEntry();
-
-      if (entry == null)
+      if (!reader.hasNext())
       {
         // No change record found.
         final LocalizableMessage message = WARN_READ_LDIF_RECORD_NO_CHANGE_RECORD_FOUND
@@ -87,7 +86,9 @@
         throw new LocalizedIllegalArgumentException(message);
       }
 
-      if (reader.readEntry() != null)
+      final Entry entry = reader.readEntry();
+
+      if (reader.hasNext())
       {
         // Multiple change records found.
         final LocalizableMessage message = WARN_READ_LDIF_RECORD_MULTIPLE_CHANGE_RECORDS_FOUND
@@ -113,6 +114,10 @@
 
 
 
+  private Entry nextEntry = null;
+
+
+
   /**
    * Creates a new LDIF entry reader whose source is the provided input stream.
    *
@@ -164,6 +169,7 @@
   /**
    * {@inheritDoc}
    */
+  @Override
   public void close() throws IOException
   {
     close0();
@@ -173,75 +179,36 @@
 
   /**
    * {@inheritDoc}
+   *
+   * @throws DecodeException
+   *           If the entry could not be decoded because it was malformed.
    */
+  @Override
+  public boolean hasNext() throws DecodeException, IOException
+  {
+    return getNextEntry() != EOF;
+  }
+
+
+
+  /**
+   * {@inheritDoc}
+   *
+   * @throws DecodeException
+   *           If the entry could not be decoded because it was malformed.
+   */
+  @Override
   public Entry readEntry() throws DecodeException, IOException
   {
-    // Continue until an unfiltered entry is obtained.
-    while (true)
+    if (!hasNext())
     {
-      LDIFRecord record = null;
-
-      // Read the set of lines that make up the next entry.
-      record = readLDIFRecord();
-      if (record == null)
-      {
-        return null;
-      }
-
-      // Read the DN of the entry and see if it is one that should be
-      // included in the import.
-      DN entryDN;
-      try
-      {
-        entryDN = readLDIFRecordDN(record);
-        if (entryDN == null)
-        {
-          // Skip version record.
-          continue;
-        }
-      }
-      catch (final DecodeException e)
-      {
-        rejectLDIFRecord(record, e.getMessageObject());
-        continue;
-      }
-
-      // Skip if branch containing the entry DN is excluded.
-      if (isBranchExcluded(entryDN))
-      {
-        final LocalizableMessage message = LocalizableMessage
-            .raw("Skipping entry because it is in excluded branch");
-        skipLDIFRecord(record, message);
-        continue;
-      }
-
-      // Use an Entry for the AttributeSequence.
-      final Entry entry = new LinkedHashMapEntry(entryDN);
-      try
-      {
-        while (record.iterator.hasNext())
-        {
-          final String ldifLine = record.iterator.next();
-          readLDIFRecordAttributeValue(record, ldifLine, entry);
-        }
-      }
-      catch (final DecodeException e)
-      {
-        rejectLDIFRecord(record, e.getMessageObject());
-        continue;
-      }
-
-      // Skip if the entry is excluded by any filters.
-      if (isEntryExcluded(entry))
-      {
-        final LocalizableMessage message = LocalizableMessage
-            .raw("Skipping entry due to exclusing filters");
-        skipLDIFRecord(record, message);
-        continue;
-      }
-
-      return entry;
+      // LDIF reader has completed successfully.
+      throw new NoSuchElementException();
     }
+
+    final Entry entry = nextEntry;
+    nextEntry = null;
+    return entry;
   }
 
 
@@ -269,8 +236,8 @@
    * entries that are read from LDIF. The default is {@code false}.
    *
    * @param excludeUserAttributes
-   *          {@code true} if all user attributes should be excluded, or {@code
-   *          false} otherwise.
+   *          {@code true} if all user attributes should be excluded, or
+   *          {@code false} otherwise.
    * @return A reference to this {@code LDIFEntryReader}.
    */
   public LDIFEntryReader setExcludeAllUserAttributes(
@@ -414,8 +381,8 @@
    * that are read from LDIF. The default is {@code true}.
    *
    * @param validateSchema
-   *          {@code true} if schema validation should be performed, or {@code
-   *          false} otherwise.
+   *          {@code true} if schema validation should be performed, or
+   *          {@code false} otherwise.
    * @return A reference to this {@code LDIFEntryReader}.
    */
   public LDIFEntryReader setValidateSchema(final boolean validateSchema)
@@ -424,4 +391,78 @@
     return this;
   }
 
+
+
+  private Entry getNextEntry() throws DecodeException, IOException
+  {
+    while (nextEntry == null)
+    {
+      LDIFRecord record = null;
+
+      // Read the set of lines that make up the next entry.
+      record = readLDIFRecord();
+      if (record == null)
+      {
+        nextEntry = EOF;
+        break;
+      }
+
+      // Read the DN of the entry and see if it is one that should be
+      // included in the import.
+      DN entryDN;
+      try
+      {
+        entryDN = readLDIFRecordDN(record);
+        if (entryDN == null)
+        {
+          // Skip version record.
+          continue;
+        }
+      }
+      catch (final DecodeException e)
+      {
+        rejectLDIFRecord(record, e.getMessageObject());
+        continue;
+      }
+
+      // Skip if branch containing the entry DN is excluded.
+      if (isBranchExcluded(entryDN))
+      {
+        final LocalizableMessage message = LocalizableMessage
+            .raw("Skipping entry because it is in excluded branch");
+        skipLDIFRecord(record, message);
+        continue;
+      }
+
+      // Use an Entry for the AttributeSequence.
+      final Entry entry = new LinkedHashMapEntry(entryDN);
+      try
+      {
+        while (record.iterator.hasNext())
+        {
+          final String ldifLine = record.iterator.next();
+          readLDIFRecordAttributeValue(record, ldifLine, entry);
+        }
+      }
+      catch (final DecodeException e)
+      {
+        rejectLDIFRecord(record, e.getMessageObject());
+        continue;
+      }
+
+      // Skip if the entry is excluded by any filters.
+      if (isEntryExcluded(entry))
+      {
+        final LocalizableMessage message = LocalizableMessage
+            .raw("Skipping entry due to exclusing filters");
+        skipLDIFRecord(record, message);
+        continue;
+      }
+
+      nextEntry = entry;
+    }
+
+    return nextEntry;
+  }
+
 }
diff --git a/sdk/tests/unit-tests-testng/src/org/opends/sdk/SynchronousConnectionTestCase.java b/sdk/tests/unit-tests-testng/src/org/opends/sdk/SynchronousConnectionTestCase.java
index 8e2e26f..2a6bba7 100644
--- a/sdk/tests/unit-tests-testng/src/org/opends/sdk/SynchronousConnectionTestCase.java
+++ b/sdk/tests/unit-tests-testng/src/org/opends/sdk/SynchronousConnectionTestCase.java
@@ -29,17 +29,18 @@
 
 
 
-import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertTrue;
 
-import java.util.List;
+import java.util.NoSuchElementException;
 
-import org.opends.sdk.ldif.EntryReader;
+import org.opends.sdk.ldif.ConnectionEntryReader;
 import org.opends.sdk.requests.Requests;
 import org.opends.sdk.responses.BindResult;
 import org.opends.sdk.responses.CompareResult;
 import org.opends.sdk.responses.Result;
+import org.opends.sdk.responses.SearchResultEntry;
+import org.testng.Assert;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.BeforeClass;
 import org.testng.annotations.Test;
@@ -155,11 +156,27 @@
   public void testSearchRequest() throws Exception
   {
     final SynchronousConnection con = new SynchronousConnection(asyncCon);
-    final EntryReader reader = con.search(
+    final ConnectionEntryReader reader = con.search(
         "uid=user.0,ou=people,o=test", SearchScope.BASE_OBJECT,
         "objectclass=*", "cn");
-    reader.readEntry();
-    assertNull(reader.readEntry());
+    Assert.assertTrue(reader.hasNext());
+    Assert.assertFalse(reader.isReference());
+    Assert.assertTrue(reader.hasNext());
+    SearchResultEntry entry = reader.readEntry();
+    Assert.assertEquals(entry.getName(),
+        DN.valueOf("uid=user.0,ou=people,o=test"));
+    Assert.assertFalse(reader.hasNext());
+    try
+    {
+      reader.readEntry();
+      Assert
+          .fail("reader.readEntry() should have thrown NoSuchElementException");
+    }
+    catch (NoSuchElementException e)
+    {
+      // This is expected.
+    }
+    Assert.assertFalse(reader.hasNext());
   }
   // TODO: add more tests.
 }
diff --git a/sdk/tests/unit-tests-testng/src/org/opends/sdk/ldif/LDIFEntryReaderTestCase.java b/sdk/tests/unit-tests-testng/src/org/opends/sdk/ldif/LDIFEntryReaderTestCase.java
index 78c5b94..e4e345a 100644
--- a/sdk/tests/unit-tests-testng/src/org/opends/sdk/ldif/LDIFEntryReaderTestCase.java
+++ b/sdk/tests/unit-tests-testng/src/org/opends/sdk/ldif/LDIFEntryReaderTestCase.java
@@ -22,7 +22,7 @@
  * CDDL HEADER END
  *
  *
- *      Copyright 2009 Sun Microsystems, Inc.
+ *      Copyright 2009-2010 Sun Microsystems, Inc.
  */
 
 package org.opends.sdk.ldif;
@@ -32,9 +32,12 @@
 import static org.testng.Assert.assertNotNull;
 
 import java.io.FileInputStream;
+import java.util.NoSuchElementException;
 
-import org.opends.sdk.AbstractEntry;
+import org.opends.sdk.DN;
+import org.opends.sdk.Entry;
 import org.opends.sdk.TestCaseUtils;
+import org.testng.Assert;
 import org.testng.annotations.Test;
 
 
@@ -52,6 +55,45 @@
    *           If the test failed unexpectedly.
    */
   @Test()
+  public void testEmpty() throws Exception
+  {
+    final String path = TestCaseUtils.createTempFile("");
+    final FileInputStream in = new FileInputStream(path);
+    final LDIFEntryReader reader = new LDIFEntryReader(in);
+    try
+    {
+      reader.setValidateSchema(false);
+
+      Assert.assertFalse(reader.hasNext());
+      Assert.assertFalse(reader.hasNext());
+      try
+      {
+        reader.readEntry();
+        Assert
+            .fail("reader.readEntry() should have thrown NoSuchElementException");
+      }
+      catch (NoSuchElementException e)
+      {
+        // This is expected.
+      }
+      Assert.assertFalse(reader.hasNext());
+    }
+    finally
+    {
+      reader.close();
+    }
+  }
+
+
+
+  /**
+   * Tests readEntry method of LDIFEntryReader class.See
+   * https://opends.dev.java.net/issues/show_bug.cgi?id=4545 for more details.
+   *
+   * @throws Exception
+   *           If the test failed unexpectedly.
+   */
+  @Test()
   public void testReadEntry() throws Exception
   {
     final String path = TestCaseUtils
@@ -80,9 +122,31 @@
             "postalAddress: Aaccf Amar$01251 Chestnut Street$Panama City, DE  50369",
             "description: This is the description for Aaccf Amar.");
     final FileInputStream in = new FileInputStream(path);
-    final LDIFEntryReader entryReader = new LDIFEntryReader(in);
-    entryReader.setValidateSchema(false);
-    final AbstractEntry entry = (AbstractEntry) entryReader.readEntry();
-    assertNotNull(entry);
+    final LDIFEntryReader reader = new LDIFEntryReader(in);
+    try
+    {
+      reader.setValidateSchema(false);
+
+      Assert.assertTrue(reader.hasNext());
+      final Entry entry = reader.readEntry();
+      assertNotNull(entry);
+      Assert.assertEquals(entry.getName(),
+          DN.valueOf("uid=1,ou=people,dc=ucsf,dc=edu"));
+      Assert.assertFalse(reader.hasNext());
+      try
+      {
+        reader.readEntry();
+        Assert
+            .fail("reader.readEntry() should have thrown NoSuchElementException");
+      }
+      catch (NoSuchElementException e)
+      {
+        // This is expected.
+      }
+    }
+    finally
+    {
+      reader.close();
+    }
   }
 }

--
Gitblit v1.10.0