mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Nicolas Capponi
30.42.2013 9f2f22bff5de4acc97f18326da924edb24cd4a7e
Fix OPENDJ-916 - MemoryBackend in SDK should support size limit
Review CR-2386

Change is limited to MemoryBackend class :
* added size limit support in handleSearch() method, throwing error with ResultCode.SIZE_LIMIT_EXCEEDED if limit is exceeded
* refactored handleSearch to avoid duplicate code for SearchScope.SINGLE_LEVEL and SearchScope.WHOLE_SUBTREE, adding new private method searchWithSubordinates()

Updated test class MemoryBackendTestCase
* added tests for size limit for both SearchScope.SINGLE_LEVEL and SearchScope.WHOLE_SUBTREE
* added test to ensure that we can get all entries from the memory backend.
2 files modified
221 ■■■■■ changed files
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java 75 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java 146 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
@@ -34,7 +34,7 @@
import java.io.IOException;
import java.util.Collection;
import java.util.NavigableMap;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
import org.forgerock.i18n.LocalizedIllegalArgumentException;
@@ -385,7 +385,6 @@
            final SearchResultHandler resultHandler) {
        try {
            final DN dn = request.getName();
            final Entry baseEntry = getRequiredEntry(request, dn);
            final SearchScope scope = request.getScope();
            final Filter filter = request.getFilter();
            final Matcher matcher = filter.matcher(schema);
@@ -393,36 +392,14 @@
                    new AttributeFilter(request.getAttributes(), schema).typesOnly(request
                            .isTypesOnly());
            if (scope.equals(SearchScope.BASE_OBJECT)) {
                final Entry baseEntry = getRequiredEntry(request, dn);
                if (matcher.matches(baseEntry).toBoolean()) {
                    sendEntry(attributeFilter, resultHandler, baseEntry);
                }
            } else if (scope.equals(SearchScope.SINGLE_LEVEL)) {
                final NavigableMap<DN, Entry> subtree =
                        entries.subMap(dn, dn.child(RDN.maxValue()));
                for (final Entry entry : subtree.values()) {
                    // Check for cancellation.
                    requestContext.checkIfCancelled(false);
                    final DN childDN = entry.getName();
                    if (childDN.isChildOf(dn)) {
                        if (matcher.matches(entry).toBoolean()
                                && !sendEntry(attributeFilter, resultHandler, entry)) {
                            // Caller has asked to stop sending results.
                            break;
                        }
                    }
                }
            } else if (scope.equals(SearchScope.WHOLE_SUBTREE)) {
                final NavigableMap<DN, Entry> subtree =
                        entries.subMap(dn, dn.child(RDN.maxValue()));
                for (final Entry entry : subtree.values()) {
                    // Check for cancellation.
                    requestContext.checkIfCancelled(false);
                    if (matcher.matches(entry).toBoolean()
                            && !sendEntry(attributeFilter, resultHandler, entry)) {
                        // Caller has asked to stop sending results.
                        break;
                    }
                }
            } else if (scope.equals(SearchScope.SINGLE_LEVEL) || scope.equals(SearchScope.WHOLE_SUBTREE)) {
                searchWithSubordinates(
                        requestContext, resultHandler, dn, matcher,
                        attributeFilter, request.getSizeLimit(), scope);
            } else {
                throw newErrorResult(ResultCode.PROTOCOL_ERROR,
                        "Search request contains an unsupported search scope");
@@ -490,6 +467,46 @@
        return entries.size();
    }
    /**
     * Perform a search for scope that includes subordinates, i.e., either
     * <code>SearchScope.SINGLE_LEVEL</code> or <code>SearchScope.WHOLE_SUBTREE</code>.
     *
     * @param requestContext context of this request
     * @param resultHandler handler which should be used to send back the search results to the client.
     * @param dn distinguished name of the base entry used for this request
     * @param matcher to filter entries that matches this request
     * @param attributeFilter to select attributes to return in search results
     * @param sizeLimit maximum number of entries to return. A value of zero indicates no restriction
     *          on number of entries.
     * @throws CancelledResultException
     *           If a cancellation request has been received and processing of
     *           the request should be aborted if possible.
     * @throws ErrorResultException
     *           If the request is unsuccessful.
     */
    private void searchWithSubordinates(final RequestContext requestContext, final SearchResultHandler resultHandler,
            final DN dn, final Matcher matcher, final AttributeFilter attributeFilter, final int sizeLimit,
            SearchScope scope) throws CancelledResultException, ErrorResultException {
        final Map<DN, Entry> subtree = entries.subMap(dn, dn.child(RDN.maxValue()));
        int numberOfResults = 0;
        for (final Entry entry : subtree.values()) {
            requestContext.checkIfCancelled(false);
            if (scope.equals(SearchScope.WHOLE_SUBTREE) || entry.getName().isChildOf(dn)) {
                if (matcher.matches(entry).toBoolean()) {
                    if (sizeLimit > 0 && numberOfResults >= sizeLimit) {
                        throw newErrorResult(newResult(ResultCode.SIZE_LIMIT_EXCEEDED));
                    }
                    numberOfResults++;
                    boolean acceptMoreResults = sendEntry(attributeFilter, resultHandler, entry);
                    if (!acceptMoreResults) {
                        break;
                    }
                }
            }
        }
    }
    private <R extends Result> R addResultControls(final Request request, final Entry before,
            final Entry after, final R result) throws ErrorResultException {
        try {
opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
@@ -34,7 +34,9 @@
import static org.forgerock.opendj.ldif.LDIFEntryReader.valueOfLDIFEntry;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import org.forgerock.opendj.ldap.controls.AssertionRequestControl;
import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl;
@@ -43,6 +45,7 @@
import org.forgerock.opendj.ldap.controls.PreReadRequestControl;
import org.forgerock.opendj.ldap.controls.PreReadResponseControl;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.ldif.ConnectionEntryReader;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.testng.annotations.Test;
@@ -53,6 +56,8 @@
@SuppressWarnings("javadoc")
public class MemoryBackendTestCase extends SdkTestCase {
    int numberOfEntriesInBackend;
    @Test
    public void testAdd() throws Exception {
        final Connection connection = getConnection();
@@ -410,11 +415,50 @@
    }
    @Test
    public void testSearchOneLevelWithSizeLimit() throws Exception {
        final Connection connection = getConnection();
        final ConnectionEntryReader reader =
                connection.search(Requests.newSearchRequest("dc=com", SearchScope.SINGLE_LEVEL, "(objectClass=*)").
                        setSizeLimit(1));
        assertThat(reader.readEntry()).isEqualTo(
                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
                        "objectClass: top", "dc: example"));
        try {
            reader.hasNext();
            failWasExpected(ErrorResultIOException.class);
        } catch (ErrorResultIOException e) {
            assertThat(e.getCause().getResult().getResultCode()).isEqualTo(ResultCode.SIZE_LIMIT_EXCEEDED);
        }
    }
    @Test
    public void testSearchSubtree() throws Exception {
        final Connection connection = getConnection();
        assertThat(
                connection.searchSingleEntry("dc=example,dc=com", SearchScope.WHOLE_SUBTREE,
                        "(uid=test1)")).isEqualTo(getUser1Entry());
        assertThat(connection.searchSingleEntry("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(uid=test1)")).
                isEqualTo(getUser1Entry());
    }
    @Test
    public void testSearchSubtreeReturnsAllEntries() throws Exception {
        final Connection connection = getConnection();
        Collection<SearchResultEntry> entries = new ArrayList<SearchResultEntry>();
        connection.search(Requests.newSearchRequest("dc=com", SearchScope.WHOLE_SUBTREE, "(objectclass=*)"), entries);
        assertThat(entries).hasSize(numberOfEntriesInBackend);
    }
    @Test
    public void testSearchSubtreeWithSizeLimit() throws Exception {
        final Connection connection = getConnection();
        Collection<SearchResultEntry> entries = new ArrayList<SearchResultEntry>();
        try {
            connection.search(
                    Requests.newSearchRequest("dc=example,dc=com", SearchScope.WHOLE_SUBTREE, "(objectClass=*)").
                    setSizeLimit(2), entries);
            failWasExpected(ErrorResultException.class);
        } catch (ErrorResultException e) {
            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.SIZE_LIMIT_EXCEEDED);
            assertThat(entries).hasSize(2);
        }
    }
    @Test(expectedExceptions = EntryNotFoundException.class)
@@ -468,53 +512,63 @@
    private Connection getConnection() throws IOException {
        // @formatter:off
        final MemoryBackend backend =
                new MemoryBackend(new LDIFEntryReader(
                        "dn: dc=com",
                        "objectClass: domain",
                        "objectClass: top",
                        "dc: com",
                        "",
                        "dn: dc=example,dc=com",
                        "objectClass: domain",
                        "objectClass: top",
                        "dc: example",
                        "entryDN: dc=example,dc=com",
                        "entryUUID: fc252fd9-b982-3ed6-b42a-c76d2546312c",
                        "",
                        "dn: ou=People,dc=example,dc=com",
                        "objectClass: organizationalunit",
                        "objectClass: top",
                        "ou: People",
                        "",
                        "dn: uid=test1,ou=People,dc=example,dc=com",
                        "objectClass: top",
                        "objectClass: person",
                        "uid: test1",
                        "userpassword: password",
                        "cn: test user 1",
                        "sn: user 1",
                        "entryDN: uid=test1,ou=people,dc=example,dc=com",
                        "entryUUID: fc252fd9-b982-3ed6-b42a-c76d2546312c",
                        "",
                        "dn: uid=test2,ou=People,dc=example,dc=com",
                        "objectClass: top",
                        "objectClass: person",
                        "uid: test2",
                        "userpassword: password",
                        "cn: test user 2",
                        "sn: user 2",
                        "",
                        "dn: dc=xxx,dc=com",
                        "objectClass: domain",
                        "objectClass: top",
                        "dc: xxx"
                ));
        String[] ldifEntries = new String[] {
            "dn: dc=com",
            "objectClass: domain",
            "objectClass: top",
            "dc: com",
            "",
            "dn: dc=example,dc=com",
            "objectClass: domain",
            "objectClass: top",
            "dc: example",
            "entryDN: dc=example,dc=com",
            "entryUUID: fc252fd9-b982-3ed6-b42a-c76d2546312c",
            "",
            "dn: ou=People,dc=example,dc=com",
            "objectClass: organizationalunit",
            "objectClass: top",
            "ou: People",
            "",
            "dn: uid=test1,ou=People,dc=example,dc=com",
            "objectClass: top",
            "objectClass: person",
            "uid: test1",
            "userpassword: password",
            "cn: test user 1",
            "sn: user 1",
            "entryDN: uid=test1,ou=people,dc=example,dc=com",
            "entryUUID: fc252fd9-b982-3ed6-b42a-c76d2546312c",
            "",
            "dn: uid=test2,ou=People,dc=example,dc=com",
            "objectClass: top",
            "objectClass: person",
            "uid: test2",
            "userpassword: password",
            "cn: test user 2",
            "sn: user 2",
            "",
            "dn: dc=xxx,dc=com",
            "objectClass: domain",
            "objectClass: top",
            "dc: xxx"
        };
        // @formatter:on
        numberOfEntriesInBackend = getNumberOfEntries(ldifEntries);
        final MemoryBackend backend = new MemoryBackend(new LDIFEntryReader(ldifEntries));
        return newInternalConnection(backend);
    }
    private int getNumberOfEntries(String[] ldifEntries) {
        int entries = 0;
        for (int i = 0; i < ldifEntries.length; i++) {
            if (ldifEntries[i].startsWith("dn: ")) {
                entries++;
            }
        }
        return entries;
    }
    private Entry getUser1Entry() {
        return valueOfLDIFEntry("dn: uid=test1,ou=People,dc=example,dc=com", "objectClass: top",
                "objectClass: person", "uid: test1", "userpassword: password", "cn: test user 1",