From f2711b53bdd5f48eaf312981541b61c6e89bdfa1 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Fri, 29 Mar 2013 18:06:19 +0000
Subject: [PATCH] Additional change for OPENDJ-354: Implement a RequestHandler which provides an in-memory backend

---
 opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java |   99 ++++++++-
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java         |   33 +-
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java            |    8 
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeFilter.java       |  405 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 508 insertions(+), 37 deletions(-)

diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeFilter.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeFilter.java
new file mode 100644
index 0000000..f7ee890
--- /dev/null
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AttributeFilter.java
@@ -0,0 +1,405 @@
+/*
+ * 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;
+
+import static org.forgerock.opendj.ldap.Attributes.renameAttribute;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import org.forgerock.opendj.ldap.schema.AttributeType;
+import org.forgerock.opendj.ldap.schema.ObjectClass;
+import org.forgerock.opendj.ldap.schema.Schema;
+
+/**
+ * A configurable factory for filtering the attributes exposed by an entry. An
+ * {@code AttributeFilter} is useful for performing fine-grained access control,
+ * selecting attributes based on search request criteria, and selecting
+ * attributes based on post- and pre- read request control criteria.
+ * <p>
+ * In cases where methods accept a string based list of attribute descriptions,
+ * the following special attribute descriptions are permitted:
+ * <ul>
+ * <li><b>*</b> - include all user attributes
+ * <li><b>+</b> - include all operational attributes
+ * <li><b>1.1</b> - exclude all attributes
+ * <li><b>@<i>objectclass</i></b> - include all attributes identified by the
+ * named object class.
+ * </ul>
+ */
+public final class AttributeFilter {
+    // TODO: exclude specific attributes, matched values, types only, custom predicates, etc.
+    private boolean includeAllOperationalAttributes;
+    private boolean includeAllUserAttributes;
+
+    /**
+     * Use a map so that we can perform membership checks as well as recover the
+     * user requested attribute description.
+     */
+    private Map<AttributeDescription, AttributeDescription> requestedAttributes = Collections
+            .emptyMap();
+
+    /**
+     * Creates a new attribute filter which will include all user attributes but
+     * no operational attributes.
+     */
+    public AttributeFilter() {
+        includeAllUserAttributes = true;
+        includeAllOperationalAttributes = false;
+    }
+
+    /**
+     * Creates a new attribute filter which will include the attributes
+     * identified by the provided search request attribute list. Attributes will
+     * be decoded using the default schema. See the class description for
+     * details regarding the types of supported attribute description.
+     *
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     */
+    public AttributeFilter(final Collection<String> attributeDescriptions) {
+        this(attributeDescriptions, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Creates a new attribute filter which will include the attributes
+     * identified by the provided search request attribute list. Attributes will
+     * be decoded using the provided schema. See the class description for
+     * details regarding the types of supported attribute description.
+     *
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     * @param schema
+     *            The schema The schema to use when parsing attribute
+     *            descriptions and object class names.
+     */
+    public AttributeFilter(final Collection<String> attributeDescriptions, final Schema schema) {
+        if (attributeDescriptions == null || attributeDescriptions.isEmpty()) {
+            // Fast-path for common case.
+            includeAllUserAttributes = true;
+            includeAllOperationalAttributes = false;
+        } else {
+            for (final String attribute : attributeDescriptions) {
+                includeAttribute(attribute, schema);
+            }
+        }
+    }
+
+    /**
+     * Creates a new attribute filter which will include the attributes
+     * identified by the provided search request attribute list. Attributes will
+     * be decoded using the default schema. See the class description for
+     * details regarding the types of supported attribute description.
+     *
+     * @param attributeDescriptions
+     *            The names of the attributes to be included with each entry.
+     */
+    public AttributeFilter(final String... attributeDescriptions) {
+        this(Arrays.asList(attributeDescriptions));
+    }
+
+    /**
+     * Returns a modifiable filtered copy of the provided entry.
+     *
+     * @param entry
+     *            The entry to be filtered and copied.
+     * @return The modifiable filtered copy of the provided entry.
+     */
+    public Entry filteredCopyOf(final Entry entry) {
+        return new LinkedHashMapEntry(filteredViewOf(entry));
+    }
+
+    /**
+     * Returns an unmodifiable filtered view of the provided entry. The returned
+     * entry supports all operations except those which modify the contents of
+     * the entry.
+     *
+     * @param entry
+     *            The entry to be filtered.
+     * @return The unmodifiable filtered view of the provided entry.
+     */
+    public Entry filteredViewOf(final Entry entry) {
+        return new AbstractEntry() {
+
+            @Override
+            public boolean addAttribute(final Attribute attribute,
+                    final Collection<? super ByteString> duplicateValues) {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Entry clearAttributes() {
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Iterable<Attribute> getAllAttributes() {
+                /*
+                 * Unfortunately we cannot efficiently re-use the iterators in
+                 * {@code Iterators} because we need to transform and filter in
+                 * a single step. Transformation is required in order to ensure
+                 * that we return an attribute whose name is the same as the one
+                 * requested by the user.
+                 */
+                return new Iterable<Attribute>() {
+                    private boolean hasNextMustIterate = true;
+                    private final Iterator<Attribute> iterator = entry.getAllAttributes()
+                            .iterator();
+                    private Attribute next = null;
+
+                    @Override
+                    public Iterator<Attribute> iterator() {
+                        return new Iterator<Attribute>() {
+                            @Override
+                            public boolean hasNext() {
+                                if (hasNextMustIterate) {
+                                    hasNextMustIterate = false;
+                                    while (iterator.hasNext()) {
+                                        final Attribute attribute = iterator.next();
+                                        final AttributeDescription ad =
+                                                attribute.getAttributeDescription();
+                                        final AttributeType at = ad.getAttributeType();
+                                        final AttributeDescription requestedAd =
+                                                requestedAttributes.get(ad);
+                                        if (requestedAd != null) {
+                                            next = renameAttribute(attribute, requestedAd);
+                                            return true;
+                                        } else if ((at.isOperational() && includeAllOperationalAttributes)
+                                                || (!at.isOperational() && includeAllUserAttributes)) {
+                                            next = attribute;
+                                            return true;
+                                        }
+                                    }
+                                    next = null;
+                                    return false;
+                                } else {
+                                    return next != null;
+                                }
+                            }
+
+                            @Override
+                            public Attribute next() {
+                                if (!hasNext()) {
+                                    throw new NoSuchElementException();
+                                }
+                                hasNextMustIterate = true;
+                                return next;
+                            }
+
+                            @Override
+                            public void remove() {
+                                throw new UnsupportedOperationException();
+                            }
+                        };
+                    }
+                };
+            }
+
+            @Override
+            public Attribute getAttribute(final AttributeDescription attributeDescription) {
+                /*
+                 * It is tempting to filter based on the passed in attribute
+                 * description, but we may get inaccurate results due to
+                 * placeholder attribute names.
+                 */
+                final Attribute attribute = entry.getAttribute(attributeDescription);
+                if (attribute != null) {
+                    final AttributeDescription ad = attribute.getAttributeDescription();
+                    final AttributeType at = ad.getAttributeType();
+                    final AttributeDescription requestedAd = requestedAttributes.get(ad);
+                    if (requestedAd != null) {
+                        return renameAttribute(attribute, requestedAd);
+                    } else if ((at.isOperational() && includeAllOperationalAttributes)
+                            || (!at.isOperational() && includeAllUserAttributes)) {
+                        return attribute;
+                    }
+                }
+                return null;
+            }
+
+            @Override
+            @SuppressWarnings("unused")
+            public int getAttributeCount() {
+                int i = 0;
+                for (final Attribute attribute : getAllAttributes()) {
+                    i++;
+                }
+                return i;
+            }
+
+            @Override
+            public DN getName() {
+                return entry.getName();
+            }
+
+            @Override
+            public Entry setName(final DN dn) {
+                throw new UnsupportedOperationException();
+            }
+        };
+    }
+
+    /**
+     * Specifies whether or not all operational attributes should be included in
+     * filtered entries. By default operational attributes are not included.
+     *
+     * @param include
+     *            {@code true} if operational attributes should be included in
+     *            filtered entries.
+     * @return A reference to this attribute filter.
+     */
+    public AttributeFilter includeAllOperationalAttributes(final boolean include) {
+        this.includeAllOperationalAttributes = include;
+        return this;
+    }
+
+    /**
+     * Specifies whether or not all user attributes should be included in
+     * filtered entries. By default user attributes are included.
+     *
+     * @param include
+     *            {@code true} if user attributes should be included in filtered
+     *            entries.
+     * @return A reference to this attribute filter.
+     */
+    public AttributeFilter includeAllUserAttributes(final boolean include) {
+        this.includeAllUserAttributes = include;
+        return this;
+    }
+
+    /**
+     * Specifies that the named attribute should be included in filtered
+     * entries.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be included in filtered entries.
+     * @return A reference to this attribute filter.
+     */
+    public AttributeFilter includeAttribute(final AttributeDescription attributeDescription) {
+        allocatedRequestedAttributes();
+        requestedAttributes.put(attributeDescription, attributeDescription);
+        return this;
+    }
+
+    /**
+     * Specifies that the named attribute should be included in filtered
+     * entries. The attribute will be decoded using the default schema. See the
+     * class description for details regarding the types of supported attribute
+     * description.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be included in filtered entries.
+     * @return A reference to this attribute filter.
+     */
+    public AttributeFilter includeAttribute(final String attributeDescription) {
+        return includeAttribute(attributeDescription, Schema.getDefaultSchema());
+    }
+
+    /**
+     * Specifies that the named attribute should be included in filtered
+     * entries. The attribute will be decoded using the provided schema. See the
+     * class description for details regarding the types of supported attribute
+     * description.
+     *
+     * @param attributeDescription
+     *            The name of the attribute to be included in filtered entries.
+     * @param schema
+     *            The schema The schema to use when parsing attribute
+     *            descriptions and object class names.
+     * @return A reference to this attribute filter.
+     */
+    public AttributeFilter includeAttribute(final String attributeDescription, final Schema schema) {
+        if (attributeDescription.equals("*")) {
+            includeAllUserAttributes = true;
+        } else if (attributeDescription.equals("+")) {
+            includeAllOperationalAttributes = true;
+        } else if (attributeDescription.equals("1.1")) {
+            // Ignore - by default no attributes are included.
+        } else if (attributeDescription.startsWith("@") && attributeDescription.length() > 1) {
+            final String objectClassName = attributeDescription.substring(1);
+            final ObjectClass objectClass = schema.getObjectClass(objectClassName);
+            if (objectClass != null) {
+                allocatedRequestedAttributes();
+                for (final AttributeType at : objectClass.getRequiredAttributes()) {
+                    final AttributeDescription ad = AttributeDescription.create(at);
+                    requestedAttributes.put(ad, ad);
+                }
+                for (final AttributeType at : objectClass.getOptionalAttributes()) {
+                    final AttributeDescription ad = AttributeDescription.create(at);
+                    requestedAttributes.put(ad, ad);
+                }
+            }
+        } else {
+            allocatedRequestedAttributes();
+            final AttributeDescription ad =
+                    AttributeDescription.valueOf(attributeDescription, schema);
+            requestedAttributes.put(ad, ad);
+        }
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        if (!includeAllOperationalAttributes && !includeAllUserAttributes
+                && requestedAttributes.isEmpty()) {
+            return "1.1";
+        } else {
+            boolean isFirst = true;
+            final StringBuilder builder = new StringBuilder();
+            if (includeAllUserAttributes) {
+                builder.append('*');
+                isFirst = false;
+            }
+            if (includeAllOperationalAttributes) {
+                if (!isFirst) {
+                    builder.append(", ");
+                }
+                builder.append('+');
+                isFirst = false;
+            }
+            for (final AttributeDescription requestedAttribute : requestedAttributes.keySet()) {
+                if (!isFirst) {
+                    builder.append(", ");
+                }
+                builder.append(requestedAttribute.toString());
+                isFirst = false;
+            }
+            return builder.toString();
+        }
+    }
+
+    private void allocatedRequestedAttributes() {
+        if (requestedAttributes.isEmpty()) {
+            requestedAttributes = new HashMap<AttributeDescription, AttributeDescription>();
+        }
+    }
+}
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java
index 9d3d416..4514340 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/Attributes.java
@@ -496,7 +496,13 @@
     public static final Attribute renameAttribute(final Attribute attribute,
             final AttributeDescription attributeDescription) {
         Validator.ensureNotNull(attribute, attributeDescription);
-        return new RenamedAttribute(attribute, attributeDescription);
+
+        // Optimize for the case where no renaming is required.
+        if (attribute.getAttributeDescription() == attributeDescription) {
+            return attribute;
+        } else {
+            return new RenamedAttribute(attribute, attributeDescription);
+        }
     }
 
     /**
diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
index 92020ff..794ee02 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/MemoryBackend.java
@@ -33,7 +33,6 @@
 import static org.forgerock.opendj.ldap.responses.Responses.newSearchResultEntry;
 
 import java.io.IOException;
-import java.util.Collection;
 import java.util.NavigableMap;
 import java.util.concurrent.ConcurrentSkipListMap;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
@@ -324,10 +323,12 @@
             final SearchScope scope = request.getScope();
             final Filter filter = request.getFilter();
             final Matcher matcher = filter.matcher(schema);
+            final AttributeFilter attributeFilter =
+                    new AttributeFilter(request.getAttributes(), schema);
 
             if (scope.equals(SearchScope.BASE_OBJECT)) {
                 if (matcher.matches(baseEntry).toBoolean()) {
-                    sendEntry(request, resultHandler, baseEntry);
+                    sendEntry(attributeFilter, resultHandler, baseEntry);
                 }
             } else if (scope.equals(SearchScope.SINGLE_LEVEL)) {
                 final NavigableMap<DN, Entry> subtree =
@@ -338,7 +339,7 @@
                     final DN childDN = entry.getName();
                     if (childDN.isChildOf(dn)) {
                         if (matcher.matches(entry).toBoolean()
-                                && !sendEntry(request, resultHandler, entry)) {
+                                && !sendEntry(attributeFilter, resultHandler, entry)) {
                             // Caller has asked to stop sending results.
                             break;
                         }
@@ -351,7 +352,7 @@
                     // Check for cancellation.
                     requestContext.checkIfCancelled(false);
                     if (matcher.matches(entry).toBoolean()
-                            && !sendEntry(request, resultHandler, entry)) {
+                            && !sendEntry(attributeFilter, resultHandler, entry)) {
                         // Caller has asked to stop sending results.
                         break;
                     }
@@ -378,8 +379,10 @@
                 if (preRead.isCritical() && before == null) {
                     throw newErrorResult(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
                 } else {
-                    result.addControl(PreReadResponseControl.newControl(filter(before, preRead
-                            .getAttributes())));
+                    final AttributeFilter filter =
+                            new AttributeFilter(preRead.getAttributes(), schema);
+                    result.addControl(PreReadResponseControl.newControl(filter
+                            .filteredViewOf(before)));
                 }
             }
 
@@ -390,8 +393,10 @@
                 if (postRead.isCritical() && after == null) {
                     throw newErrorResult(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION);
                 } else {
-                    result.addControl(PostReadResponseControl.newControl(filter(after, postRead
-                            .getAttributes())));
+                    final AttributeFilter filter =
+                            new AttributeFilter(postRead.getAttributes(), schema);
+                    result.addControl(PostReadResponseControl.newControl(filter
+                            .filteredViewOf(after)));
                 }
             }
             return result;
@@ -400,11 +405,6 @@
         }
     }
 
-    private Entry filter(final Entry entry, final Collection<String> attributes) {
-        // FIXME: attribute filtering not supported yet.
-        return entry;
-    }
-
     private BindResult getBindResult(final BindRequest request, final Entry before,
             final Entry after) throws ErrorResultException {
         return addResultControls(request, before, after, newBindResult(ResultCode.SUCCESS));
@@ -453,9 +453,8 @@
                 + "' does not exist");
     }
 
-    private boolean sendEntry(final SearchRequest request, final SearchResultHandler resultHandler,
-            final Entry entry) {
-        return resultHandler
-                .handleEntry(newSearchResultEntry(filter(entry, request.getAttributes())));
+    private boolean sendEntry(final AttributeFilter filter,
+            final SearchResultHandler resultHandler, final Entry entry) {
+        return resultHandler.handleEntry(newSearchResultEntry(filter.filteredViewOf(entry)));
     }
 }
diff --git a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
index c695030..cdb7204 100644
--- a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
+++ b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/MemoryBackendTestCase.java
@@ -221,20 +221,6 @@
     }
 
     @Test
-    public void testModifyBindPostRead() throws Exception {
-        final Connection connection = getConnection();
-        assertThat(
-                connection.modify(
-                        newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
-                                "add: description", "description: test description").addControl(
-                                PostReadRequestControl.newControl(true))).getControl(
-                        PostReadResponseControl.DECODER, new DecodeOptions()).getEntry())
-                .isEqualTo(
-                        valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
-                                "objectClass: top", "dc: example", "description: test description"));
-    }
-
-    @Test
     public void testModifyIncrement() throws Exception {
         final Connection connection = getConnection();
         connection.modify("dn: dc=example,dc=com", "changetype: modify", "add: integer",
@@ -288,16 +274,45 @@
     }
 
     @Test
-    public void testModifyPreRead() throws Exception {
+    public void testModifyPostRead() throws Exception {
         final Connection connection = getConnection();
         assertThat(
                 connection.modify(
                         newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
                                 "add: description", "description: test description").addControl(
-                                PreReadRequestControl.newControl(true))).getControl(
-                        PreReadResponseControl.DECODER, new DecodeOptions()).getEntry()).isEqualTo(
-                valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
-                        "objectClass: top", "dc: example"));
+                                PostReadRequestControl.newControl(true))).getControl(
+                        PostReadResponseControl.DECODER, new DecodeOptions()).getEntry())
+                .isEqualTo(
+                        valueOfLDIFEntry("dn: dc=example,dc=com", "objectClass: domain",
+                                "objectClass: top", "dc: example", "description: test description"));
+    }
+
+    @Test
+    public void testModifyPostReadAttributesSelected() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.modify(
+                        newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                                "add: description", "description: test description").addControl(
+                                PostReadRequestControl.newControl(true, "dc", "entryDN")))
+                        .getControl(PostReadResponseControl.DECODER, new DecodeOptions())
+                        .getEntry()).isEqualTo(
+                valueOfLDIFEntry("dn: dc=example,dc=com", "dc: example",
+                        "entryDN: dc=example,dc=com"));
+    }
+
+    @Test
+    public void testModifyPreReadAttributesSelected() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(
+                connection.modify(
+                        newModifyRequest("dn: dc=example,dc=com", "changetype: modify",
+                                "add: description", "description: test description").addControl(
+                                PreReadRequestControl.newControl(true, "dc", "entryDN")))
+                        .getControl(PreReadResponseControl.DECODER, new DecodeOptions()).getEntry())
+                .isEqualTo(
+                        valueOfLDIFEntry("dn: dc=example,dc=com", "dc: example",
+                                "entryDN: dc=example,dc=com"));
     }
 
     @Test(expectedExceptions = ConstraintViolationException.class)
@@ -313,6 +328,48 @@
     }
 
     @Test
+    public void testSearchAttributesOperational() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.readEntry("uid=test1,ou=People,dc=example,dc=com", "+")).isEqualTo(
+                valueOfLDIFEntry("dn: uid=test1,ou=People,dc=example,dc=com",
+                        "entryDN: uid=test1,ou=people,dc=example,dc=com",
+                        "entryUUID: fc252fd9-b982-3ed6-b42a-c76d2546312c"));
+    }
+
+    @Test
+    public void testSearchAttributesSelected() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.readEntry("uid=test1,ou=People,dc=example,dc=com", "uid", "entryDN"))
+                .isEqualTo(
+                        valueOfLDIFEntry("dn: uid=test1,ou=People,dc=example,dc=com", "uid: test1",
+                                "entryDN: uid=test1,ou=People,dc=example,dc=com"));
+    }
+
+    @Test
+    public void testSearchAttributesRenamed() throws Exception {
+        final Connection connection = getConnection();
+        final Entry entry =
+                connection.readEntry("uid=test1,ou=People,dc=example,dc=com", "commonName",
+                        "ENTRYDN");
+        assertThat(entry)
+                .isEqualTo(
+                        valueOfLDIFEntry("dn: uid=test1,ou=People,dc=example,dc=com",
+                                "commonName: test user 1",
+                                "ENTRYDN: uid=test1,ou=People,dc=example,dc=com"));
+        assertThat(entry.getAttribute("cn").getAttributeDescriptionAsString()).isEqualTo(
+                "commonName");
+        assertThat(entry.getAttribute("entryDN").getAttributeDescriptionAsString()).isEqualTo(
+                "ENTRYDN");
+    }
+
+    @Test
+    public void testSearchAttributesUser() throws Exception {
+        final Connection connection = getConnection();
+        assertThat(connection.readEntry("uid=test1,ou=People,dc=example,dc=com", "*")).isEqualTo(
+                getUser1Entry());
+    }
+
+    @Test
     public void testSearchBase() throws Exception {
         final Connection connection = getConnection();
         assertThat(connection.readEntry("dc=example,dc=com")).isEqualTo(
@@ -410,6 +467,8 @@
                         "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",
@@ -423,6 +482,8 @@
                         "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",

--
Gitblit v1.10.0