From a37627c2a86f954cec0b359ad483ff42a0a33d9b Mon Sep 17 00:00:00 2001
From: Nicolas Capponi <nicolas.capponi@forgerock.com>
Date: Fri, 27 Sep 2013 14:28:00 +0000
Subject: [PATCH] Fix OPENDJ-972 - OpenDJ should set size limit to 1 when performing single entry searches Review CR-2378

---
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core_fr.properties                     |    4 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/SdkTestCase.java                            |   13 +
 opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnectionTestCase.java |  162 ++++++++++++++++-
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequestImpl.java             |    7 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java                     |  154 +++++++++++------
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core.properties                        |    2 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java                      |   56 ++++++
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSearchRequestImpl.java |    7 
 opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/requests/SearchRequestTestCase.java         |   82 +++++++++
 opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequest.java                 |   17 +
 10 files changed, 429 insertions(+), 75 deletions(-)

diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java
index e4a5349..1ae4d82 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java
@@ -29,7 +29,9 @@
 
 import static com.forgerock.opendj.ldap.CoreMessages.ERR_NO_SEARCH_RESULT_ENTRIES;
 import static com.forgerock.opendj.ldap.CoreMessages.ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES;
+import static com.forgerock.opendj.ldap.CoreMessages.ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT;
 import static com.forgerock.opendj.ldap.CoreMessages.ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES;
+
 import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
 
 import java.util.Collection;
@@ -67,11 +69,7 @@
             SearchResultHandler {
         private final ResultHandler<? super SearchResultEntry> handler;
 
-        private volatile SearchResultEntry firstEntry = null;
-
-        private volatile SearchResultReference firstReference = null;
-
-        private volatile int entryCount = 0;
+        private final SingleEntryHandler singleEntryHandler = new SingleEntryHandler();
 
         private volatile FutureResult<Result> future = null;
 
@@ -86,14 +84,22 @@
 
         @Override
         public SearchResultEntry get() throws ErrorResultException, InterruptedException {
-            future.get();
+            try {
+                future.get();
+            } catch (ErrorResultException e) {
+                throw singleEntryHandler.filterError(e);
+            }
             return get0();
         }
 
         @Override
-        public SearchResultEntry get(final long timeout, final TimeUnit unit)
-                throws ErrorResultException, TimeoutException, InterruptedException {
-            future.get(timeout, unit);
+        public SearchResultEntry get(final long timeout, final TimeUnit unit) throws ErrorResultException,
+                TimeoutException, InterruptedException {
+            try {
+                future.get(timeout, unit);
+            } catch (ErrorResultException e) {
+                throw singleEntryHandler.filterError(e);
+            }
             return get0();
         }
 
@@ -104,26 +110,20 @@
 
         @Override
         public boolean handleEntry(final SearchResultEntry entry) {
-            if (firstEntry == null) {
-                firstEntry = entry;
-            }
-            entryCount++;
-            return true;
+            return singleEntryHandler.handleEntry(entry);
         }
 
         @Override
         public void handleErrorResult(final ErrorResultException error) {
             if (handler != null) {
-                handler.handleErrorResult(error);
+                ErrorResultException finalError = singleEntryHandler.filterError(error);
+                handler.handleErrorResult(finalError);
             }
         }
 
         @Override
         public boolean handleReference(final SearchResultReference reference) {
-            if (firstReference == null) {
-                firstReference = reference;
-            }
-            return true;
+            return singleEntryHandler.handleReference(reference);
         }
 
         @Override
@@ -148,21 +148,11 @@
         }
 
         private SearchResultEntry get0() throws ErrorResultException {
-            if (entryCount == 0) {
-                // Did not find any entries.
-                throw newErrorResult(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED,
-                        ERR_NO_SEARCH_RESULT_ENTRIES.get().toString());
-            } else if (entryCount > 1) {
-                // Got more entries than expected.
-                throw newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
-                        ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES.get(entryCount).toString());
-            } else if (firstReference != null) {
-                // Got an unexpected search result reference.
-                throw newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
-                        ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES.get(
-                                firstReference.getURIs().iterator().next()).toString());
+            ErrorResultException exception = singleEntryHandler.checkForClientSideError();
+            if (exception == null) {
+                return singleEntryHandler.firstEntry;
             } else {
-                return firstEntry;
+                throw exception;
             }
         }
 
@@ -192,7 +182,7 @@
          */
         @Override
         public void handleErrorResult(final ErrorResultException error) {
-            // Ignore.
+            // Ignore
         }
 
         @Override
@@ -211,6 +201,50 @@
             // Ignore.
         }
 
+        /**
+         * Filter the provided error in order to transform size limit exceeded error to a client side error,
+         * or leave it as is for any other error.
+         *
+         * @param error to filter
+         * @return provided error in most case, or <code>ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED</code>
+         * error if provided error is <code>ResultCode.SIZE_LIMIT_EXCEEDED</code>
+         */
+        public ErrorResultException filterError(final ErrorResultException error) {
+            if (error.getResult().getResultCode().equals(ResultCode.SIZE_LIMIT_EXCEEDED)) {
+                return newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
+                        ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT.get().toString());
+            } else {
+                return error;
+            }
+        }
+
+        /**
+         * Check for any error related to number of search result at client-side level: no result,
+         * too many result, search result reference.
+         *
+         * This method should be called only after search operation is finished.
+         *
+         * @return an <code>ErrorResultException</code> if an error is detected, <code>null</code> otherwise
+         */
+        public ErrorResultException checkForClientSideError() {
+            ErrorResultException exception = null;
+            if (entryCount == 0) {
+                // Did not find any entries.
+                exception = newErrorResult(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED, ERR_NO_SEARCH_RESULT_ENTRIES
+                        .get().toString());
+            } else if (entryCount > 1) {
+                // Got more entries than expected.
+                exception = newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
+                        ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES.get(entryCount).toString());
+            } else if (firstReference != null) {
+                // Got an unexpected search result reference.
+                exception = newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
+                        ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES.get(firstReference.getURIs().iterator().next())
+                                .toString());
+            }
+            return exception;
+        }
+
     }
 
     // Visitor used for processing synchronous change requests.
@@ -410,7 +444,7 @@
     public SearchResultEntry readEntry(final DN baseObject, final String... attributeDescriptions)
             throws ErrorResultException {
         final SearchRequest request =
-                Requests.newSearchRequest(baseObject, SearchScope.BASE_OBJECT, Filter
+                Requests.newSingleEntrySearchRequest(baseObject, SearchScope.BASE_OBJECT, Filter
                         .objectClassPresent(), attributeDescriptions);
         return searchSingleEntry(request);
     }
@@ -432,8 +466,9 @@
             final Collection<String> attributeDescriptions,
             final ResultHandler<? super SearchResultEntry> handler) {
         final SearchRequest request =
-                Requests.newSearchRequest(name, SearchScope.BASE_OBJECT, Filter
-                        .objectClassPresent());
+                Requests.newSingleEntrySearchRequest(
+                        name, SearchScope.BASE_OBJECT,
+                        Filter.objectClassPresent());
         if (attributeDescriptions != null) {
             request.getAttributes().addAll(attributeDescriptions);
         }
@@ -521,23 +556,16 @@
     public SearchResultEntry searchSingleEntry(final SearchRequest request)
             throws ErrorResultException {
         final SingleEntryHandler handler = new SingleEntryHandler();
-        search(request, handler);
-
-        if (handler.entryCount == 0) {
-            // Did not find any entries.
-            throw newErrorResult(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED,
-                    ERR_NO_SEARCH_RESULT_ENTRIES.get().toString());
-        } else if (handler.entryCount > 1) {
-            // Got more entries than expected.
-            throw newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
-                    ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES.get(handler.entryCount).toString());
-        } else if (handler.firstReference != null) {
-            // Got an unexpected search result reference.
-            throw newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
-                    ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES.get(
-                            handler.firstReference.getURIs().iterator().next()).toString());
-        } else {
+        try {
+            search(enforceSingleEntrySearchRequest(request), handler);
+        } catch (ErrorResultException e) {
+            throw handler.filterError(e);
+        }
+        ErrorResultException error = handler.checkForClientSideError();
+        if (error == null) {
             return handler.firstEntry;
+        } else {
+            throw error;
         }
     }
 
@@ -548,7 +576,7 @@
     public SearchResultEntry searchSingleEntry(final String baseObject, final SearchScope scope,
             final String filter, final String... attributeDescriptions) throws ErrorResultException {
         final SearchRequest request =
-                Requests.newSearchRequest(baseObject, scope, filter, attributeDescriptions);
+                Requests.newSingleEntrySearchRequest(baseObject, scope, filter, attributeDescriptions);
         return searchSingleEntry(request);
     }
 
@@ -559,12 +587,28 @@
     public FutureResult<SearchResultEntry> searchSingleEntryAsync(final SearchRequest request,
             final ResultHandler<? super SearchResultEntry> handler) {
         final SingleEntryFuture innerFuture = new SingleEntryFuture(handler);
-        final FutureResult<Result> future = searchAsync(request, null, innerFuture);
+        final FutureResult<Result> future =
+                searchAsync(enforceSingleEntrySearchRequest(request), null, innerFuture);
         innerFuture.setResultFuture(future);
         return innerFuture;
     }
 
     /**
+     * Ensure that a single entry search request is returned, based on provided request.
+     *
+     * @param request
+     *            to be checked
+     * @return a single entry search request, equal to or based on the provided request
+     */
+    private SearchRequest enforceSingleEntrySearchRequest(final SearchRequest request) {
+        if (request.isSingleEntrySearch()) {
+            return request;
+        } else {
+            return Requests.copyOfSearchRequest(request).setSizeLimit(1);
+        }
+    }
+
+    /**
      * {@inheritDoc}
      * <p>
      * Sub-classes should provide an implementation which returns an appropriate
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java
index 3381a35..fadf1f3 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2011-2012 ForgeRock AS
+ *      Portions copyright 2011-2013 ForgeRock AS
  */
 
 package org.forgerock.opendj.ldap.requests;
@@ -1017,7 +1017,7 @@
 
     /**
      * Creates a new search request using the provided distinguished name,
-     * scope, and filter, decoded using the default schema.
+     * scope, and filter.
      *
      * @param name
      *            The distinguished name of the base entry relative to which the
@@ -1079,6 +1079,58 @@
     }
 
     /**
+     * Creates a new search request for a single entry, using the provided distinguished name,
+     * scope, and filter.
+     *
+     * @param name
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed.
+     * @param scope
+     *            The scope of the search.
+     * @param filter
+     *            The filter that defines the conditions that must be fulfilled
+     *            in order for an entry to be returned.
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     * @return The new search request.
+     * @throws NullPointerException
+     *             If the {@code name}, {@code scope}, or {@code filter} were
+     *             {@code null}.
+     */
+    public static SearchRequest newSingleEntrySearchRequest(final DN name, final SearchScope scope,
+            final Filter filter, final String... attributeDescriptions) {
+        return newSearchRequest(name, scope, filter, attributeDescriptions).setSizeLimit(1);
+    }
+
+    /**
+     * Creates a new search request for a single entry, using the provided distinguished name,
+     * scope, and filter, decoded using the default schema.
+     *
+     * @param name
+     *            The distinguished name of the base entry relative to which the
+     *            search is to be performed.
+     * @param scope
+     *            The scope of the search.
+     * @param filter
+     *            The filter that defines the conditions that must be fulfilled
+     *            in order for an entry to be returned.
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     * @return The new search request.
+     * @throws LocalizedIllegalArgumentException
+     *             If {@code name} could not be decoded using the default
+     *             schema, or if {@code filter} is not a valid LDAP string
+     *             representation of a filter.
+     * @throws NullPointerException
+     *             If the {@code name}, {@code scope}, or {@code filter} were
+     *             {@code null}.
+     */
+    public static SearchRequest newSingleEntrySearchRequest(final String name, final SearchScope scope,
+            final String filter, final String... attributeDescriptions) {
+        return newSearchRequest(name, scope, filter, attributeDescriptions).setSizeLimit(1);
+    }
+
+    /**
      * Creates a new simple bind request having an empty name and password
      * suitable for anonymous authentication.
      *
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequest.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequest.java
index ce830bf..9de33bd 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequest.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequest.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2009-2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.requests;
@@ -147,6 +147,19 @@
     int getSizeLimit();
 
     /**
+     * Indicates whether search result is expected to be limited to a single entry.
+     * <p>
+     * It is the case if size limit is equal to 1 or if scope is equal to <code>SearchScope.BASE_OBJECT</code>.
+     * <p>
+     * If search results contain more than one entry, the search operation will throw
+     * a <code>MultipleEntriesFoundException</code>.
+     *
+     * @return {@code true} if the search is limited to a single entry result,
+     *         or {@code false} (the default) otherwise.
+     */
+    boolean isSingleEntrySearch();
+
+    /**
      * Returns the time limit that should be used in order to restrict the
      * maximum time (in seconds) allowed for the search.
      * <p>
@@ -270,6 +283,8 @@
      * A value of zero (the default) in this field indicates that no
      * client-requested size limit restrictions are in effect. Servers may also
      * enforce a maximum number of entries to return.
+     * <p>
+     * This method overrides the size limit set using a previous call to {@link #setSingleEntrySearch()}.
      *
      * @param limit
      *            The size limit that should be used in order to restrict the
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequestImpl.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequestImpl.java
index 7df07e6..4b00351 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequestImpl.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequestImpl.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.requests;
@@ -107,6 +107,11 @@
     }
 
     @Override
+    public boolean isSingleEntrySearch() {
+        return sizeLimit == 1 || SearchScope.BASE_OBJECT.equals(scope);
+    }
+
+    @Override
     public int getTimeLimit() {
         return timeLimit;
     }
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSearchRequestImpl.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSearchRequestImpl.java
index 6388047..fd3bd76 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSearchRequestImpl.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSearchRequestImpl.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap.requests;
@@ -80,6 +80,11 @@
     }
 
     @Override
+    public boolean isSingleEntrySearch() {
+        return impl.isSingleEntrySearch();
+    }
+
+    @Override
     public int getTimeLimit() {
         return impl.getTimeLimit();
     }
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core.properties b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core.properties
index 1057f00..3103d70 100755
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core.properties
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core.properties
@@ -844,6 +844,8 @@
  but did not return any search result entries when one was expected
 ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES=The search request succeeded \
  but returned %d search result entry when only one was expected
+ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT=The search request succeeded \
+ but returned more than one search result entry when only one was expected
 ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES=The search request succeeded \
  but returned a search result reference containing the following URI: %s
 #
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core_fr.properties b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core_fr.properties
index aa9bf60..2c6f2ce 100755
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core_fr.properties
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core_fr.properties
@@ -20,6 +20,7 @@
 # CDDL HEADER END
 #
 #      Copyright 2009 Sun Microsystems, Inc.
+#      Portions copyright 2013 ForgeRock AS
 #
 ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH_9=La valeur indiqu\u00e9e "%s" n'est pas une cha\u00eene de pays valide car elle n'a pas une longueur de deux caract\u00e8res exactement
 ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS_11=La valeur indiqu\u00e9e "%s" n'est pas une m\u00e9thode de distribution valide car elle ne contient aucun \u00e9l\u00e9ment
@@ -94,3 +95,6 @@
 ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_STRUCTURAL_271=La r\u00e8gle de contenu DIT "%s" n'est pas valide car elle interdit l'utilisation du type d'attribut %s requis par la classe d'objet structurelle associ\u00e9e %s
 ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_AUXILIARY_272=La r\u00e8gle de contenu DIT "%s" n'est pas valide car elle interdit l'utilisation du type d'attribut %s requis par la classe d'objet structurelle auxiliaire %s
 ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR_282=Impossible d'analyser la valeur fournie "%s" en tant que nom valide distinctif car la valeur de l'attribut commence avec un caract\u00e8re en position %d qui doit \u00eatre \u00e9vit\u00e9
+ERR_NO_SEARCH_RESULT_ENTRIES=La recherche a \u00e9t\u00e9 effectu\u00e9e avec succ\u00e8s mais n'a retourn\u00e9 aucune entr\u00e9e alors qu'une entr\u00e9e \u00e9tait attendue
+ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES=La recherche a \u00e9t\u00e9 effectu\u00e9e avec succ\u00e8s mais a retourn\u00e9 %s entr\u00e9es alors qu'une seule entr\u00e9e \u00e9tait attendue
+ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT=La recherche a \u00e9t\u00e9 effectu\u00e9e avec succ\u00e8s mais a retourn\u00e9 plusieurs entr\u00e9es alors qu'une seule entr\u00e9e \u00e9tait attendue
\ No newline at end of file
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnectionTestCase.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnectionTestCase.java
index 6b3fc0f..6966aa4 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnectionTestCase.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnectionTestCase.java
@@ -22,7 +22,7 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2011 ForgeRock AS.
+ *      Portions copyright 2011-2013 ForgeRock AS.
  */
 
 package org.forgerock.opendj.ldap;
@@ -30,6 +30,7 @@
 import static org.fest.assertions.Assertions.assertThat;
 import static org.fest.assertions.Fail.fail;
 import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
+import static org.mockito.Mockito.*;
 
 import java.util.LinkedList;
 import java.util.List;
@@ -65,15 +66,11 @@
 
     private final class MockConnection extends AbstractAsynchronousConnection {
         private final ResultCode resultCode;
-        private final SearchResultEntry entry;
+        private final SearchResultEntry[] entries;
 
-        private MockConnection(ResultCode resultCode) {
-            this(resultCode, null);
-        }
-
-        private MockConnection(ResultCode resultCode, SearchResultEntry entry) {
+        private MockConnection(ResultCode resultCode, SearchResultEntry...entries) {
             this.resultCode = resultCode;
-            this.entry = entry;
+            this.entries = entries;
         }
 
         /**
@@ -222,14 +219,17 @@
         public FutureResult<Result> searchAsync(SearchRequest request,
                 IntermediateResponseHandler intermediateResponseHandler,
                 SearchResultHandler resultHandler) {
-            if (entry != null) {
+            for (SearchResultEntry entry : entries) {
                 resultHandler.handleEntry(entry);
             }
-
-            if (!resultCode.isExceptional()) {
-                return new CompletedFutureResult<Result>(Responses.newResult(resultCode));
+            if (resultCode.isExceptional()) {
+                ErrorResultException errorResult = newErrorResult(resultCode);
+                resultHandler.handleErrorResult(errorResult);
+                return new CompletedFutureResult<Result>(errorResult);
             } else {
-                return new CompletedFutureResult<Result>(newErrorResult(resultCode));
+                Result result = Responses.newResult(resultCode);
+                resultHandler.handleResult(result);
+                return new CompletedFutureResult<Result>(result);
             }
         }
 
@@ -401,10 +401,144 @@
         List<SearchResultEntry> entries = new LinkedList<SearchResultEntry>();
         try {
             mockConnection.search(searchRequest, entries);
-            fail();
+            failWasExpected(ErrorResultException.class);
         } catch (ErrorResultException e) {
             assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
             assertThat(entries.isEmpty());
         }
     }
+
+    @Test()
+    public void testSingleEntrySearchRequestSuccess() throws Exception {
+        final SearchResultEntry entry = Responses.newSearchResultEntry("cn=test");
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS, entry);
+        final SearchRequest request =
+                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        assertThat(mockConnection.searchSingleEntry(request)).isEqualTo(entry);
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test()
+    public void testSingleEntrySearchAsyncRequestSuccess() throws Exception {
+        final SearchResultEntry entry = Responses.newSearchResultEntry("cn=test");
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS, entry);
+        final SearchRequest request =
+                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        ResultHandler<SearchResultEntry> handler = mock(ResultHandler.class);
+
+        FutureResult<SearchResultEntry> futureResult = mockConnection.searchSingleEntryAsync(request, handler);
+
+        assertThat(futureResult.get()).isEqualTo(entry);
+        verify(handler).handleResult(any(SearchResultEntry.class));
+    }
+
+    @Test()
+    public void testSingleEntrySearchRequestNoEntryReturned() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS);
+        final SearchRequest request =
+                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        try {
+            mockConnection.searchSingleEntry(request);
+            failWasExpected(EntryNotFoundException.class);
+        } catch (EntryNotFoundException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED);
+        }
+    }
+
+    @Test()
+    public void testSingleEntrySearchRequestMultipleEntriesToReturn() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SIZE_LIMIT_EXCEEDED,
+                Responses.newSearchResultEntry("cn=test"));
+        final SearchRequest request =
+                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        try {
+            mockConnection.searchSingleEntry(request);
+            failWasExpected(MultipleEntriesFoundException.class);
+        } catch (MultipleEntriesFoundException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
+        }
+    }
+
+    @Test()
+    public void testSingleEntrySearchRequestMultipleEntriesReturnedByServer() throws Exception {
+        // could happen if server does not enforce size limit
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS,
+                Responses.newSearchResultEntry("cn=test"),
+                Responses.newSearchResultEntry("cn=test,ou=org"));
+        final SearchRequest request =
+                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.WHOLE_SUBTREE, "(objectClass=*)");
+        try {
+            mockConnection.searchSingleEntry(request);
+            failWasExpected(MultipleEntriesFoundException.class);
+        } catch (MultipleEntriesFoundException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Test()
+    public void testSingleEntrySearchAsyncRequestMultipleEntriesToReturn() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.SIZE_LIMIT_EXCEEDED,
+                Responses.newSearchResultEntry("cn=test"));
+        final SearchRequest request =
+                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        ResultHandler<SearchResultEntry> handler = mock(ResultHandler.class);
+
+        try {
+            mockConnection.searchSingleEntryAsync(request, handler).get();
+            failWasExpected(MultipleEntriesFoundException.class);
+        } catch (MultipleEntriesFoundException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
+            verify(handler).handleErrorResult(any(ErrorResultException.class));
+        }
+    }
+
+    @Test()
+    public void testSingleEntrySearchAsyncRequestMultipleEntriesReturnedByServer() throws Exception {
+        // could happen if server does not enfore size limit
+        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS,
+                Responses.newSearchResultEntry("cn=test"),
+                Responses.newSearchResultEntry("cn=test,ou=org"));
+        final SearchRequest request = Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT,
+                "(objectClass=*)");
+        ResultHandler<SearchResultEntry> handler = mock(ResultHandler.class);
+
+        try {
+            mockConnection.searchSingleEntryAsync(request, handler).get();
+            failWasExpected(MultipleEntriesFoundException.class);
+        } catch (MultipleEntriesFoundException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
+            verify(handler).handleErrorResult(any(ErrorResultException.class));
+        }
+    }
+
+    @Test()
+    public void testSingleEntrySearchRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final SearchRequest request =
+                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        try {
+            mockConnection.searchSingleEntry(request);
+            failWasExpected(ErrorResultException.class);
+        } catch (ErrorResultException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+        }
+    }
+
+    @Test()
+    public void testSingleEntrySearchAsyncRequestFail() throws Exception {
+        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
+        final SearchRequest request =
+                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
+        ResultHandler<SearchResultEntry> handler = mock(ResultHandler.class);
+
+        try {
+            mockConnection.searchSingleEntryAsync(request, handler).get();
+            failWasExpected(ErrorResultException.class);
+        } catch (ErrorResultException e) {
+            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
+            verify(handler).handleErrorResult(any(ErrorResultException.class));
+        }
+    }
+
 }
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/SdkTestCase.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/SdkTestCase.java
index 708776d..8cdf183 100644
--- a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/SdkTestCase.java
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/SdkTestCase.java
@@ -22,10 +22,12 @@
  *
  *
  *      Copyright 2010 Sun Microsystems, Inc.
- *      Portions copyright 2012 ForgeRock AS.
+ *      Portions copyright 2012-2013 ForgeRock AS.
  */
 package org.forgerock.opendj.ldap;
 
+import static org.fest.assertions.Fail.*;
+
 import org.forgerock.testng.ForgeRockTestCase;
 import org.testng.annotations.Test;
 
@@ -35,4 +37,13 @@
  */
 @Test(groups = { "precommit", "types", "sdk" })
 public abstract class SdkTestCase extends ForgeRockTestCase {
+
+    /**
+     * Fail with precise message giving the exception that was expected.
+     *
+     * @param exceptionClass expected exception
+     */
+    void failWasExpected(Class<? extends Throwable> exceptionClass) {
+        fail("should throw an exception " + exceptionClass.getSimpleName());
+    }
 }
diff --git a/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/requests/SearchRequestTestCase.java b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/requests/SearchRequestTestCase.java
new file mode 100644
index 0000000..2d92da6
--- /dev/null
+++ b/opendj-sdk/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/requests/SearchRequestTestCase.java
@@ -0,0 +1,82 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License").  You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file and include the License file at legal-notices/CDDLv1_0.txt.
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information:
+ *      Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ *      Copyright 2013 ForgeRock AS.
+ */
+
+package org.forgerock.opendj.ldap.requests;
+
+import static org.fest.assertions.Assertions.*;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Filter;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+@SuppressWarnings("javadoc")
+public class SearchRequestTestCase extends RequestTestCase {
+
+    @DataProvider(name = "SearchRequests")
+    public Object[][] getSearchRequests() throws Exception {
+        return getTestRequests();
+    }
+
+    @Override
+    protected SearchRequest[] createTestRequests() throws Exception {
+        return new SearchRequest[] {
+                Requests.newSearchRequest(
+                        "uid=user.0,ou=people,o=test",
+                        SearchScope.BASE_OBJECT, "(uid=user)", "uid", "ou"),
+                Requests.newSearchRequest("uid=user.0,ou=people,o=test",
+                        SearchScope.SINGLE_LEVEL, "(uid=user)", "uid", "ou") };
+    }
+
+    @Test
+    public void createRequestForSingleEntrySearch() throws Exception {
+        SearchRequest request = Requests.newSingleEntrySearchRequest(
+                DN.valueOf("uid=user.0,ou=people,o=test"),
+                SearchScope.BASE_OBJECT, Filter.equality("uid", "user"), "uid");
+
+        assertThat(request.getSizeLimit()).isEqualTo(1);
+        assertThat(request.isSingleEntrySearch()).isTrue();
+    }
+
+    @Test
+    public void createRequestForSingleEntrySearchWithStrings() throws Exception {
+        SearchRequest request = Requests.newSingleEntrySearchRequest(
+                "uid=user.0,ou=people,o=test",
+                SearchScope.BASE_OBJECT, "(uid=user)", "uid");
+
+        assertThat(request.getSizeLimit()).isEqualTo(1);
+        assertThat(request.isSingleEntrySearch()).isTrue();
+    }
+
+    @Test
+    public void createRequestWithBaseObjectScope() throws Exception {
+        SearchRequest request = Requests.newSearchRequest(
+                DN.valueOf("uid=user.0,ou=people,o=test"),
+                SearchScope.BASE_OBJECT, Filter.equality("uid", "user"), "uid");
+
+        assertThat(request.isSingleEntrySearch()).isTrue();
+    }
+}

--
Gitblit v1.10.0