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

Matthew Swift
10.31.2013 507e00fb190713b1654579123d284bcd3d750abe
opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReferenceAttributeMapper.java
@@ -15,16 +15,15 @@
 */
package org.forgerock.opendj.rest2ldap;
import static java.util.Collections.singletonList;
import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest;
import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException;
import static org.forgerock.opendj.rest2ldap.Utils.accumulate;
import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull;
import static org.forgerock.opendj.rest2ldap.Utils.i18n;
import static org.forgerock.opendj.rest2ldap.Utils.transform;
import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
@@ -39,6 +38,7 @@
import org.forgerock.json.resource.ResultHandler;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.Attributes;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
@@ -47,11 +47,11 @@
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.Function;
import org.forgerock.opendj.ldap.LinkedAttribute;
import org.forgerock.opendj.ldap.Modification;
import org.forgerock.opendj.ldap.MultipleEntriesFoundException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchResultHandler;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
@@ -61,7 +61,8 @@
 * An attribute mapper which provides a mapping from a JSON value to a single DN
 * valued LDAP attribute.
 */
public final class ReferenceAttributeMapper extends AttributeMapper {
public final class ReferenceAttributeMapper extends
        AbstractLDAPAttributeMapper<ReferenceAttributeMapper> {
    /**
     * The maximum number of candidate references to allow in search filters.
     */
@@ -69,45 +70,19 @@
    private final DN baseDN;
    private Filter filter = null;
    private boolean isRequired = false;
    private boolean isSingleValued = false;
    private final AttributeDescription ldapAttributeName;
    private final AttributeMapper mapper;
    private final AttributeDescription primaryKey;
    private SearchScope scope = SearchScope.WHOLE_SUBTREE;
    private WritabilityPolicy writabilityPolicy = READ_WRITE;
    ReferenceAttributeMapper(final AttributeDescription ldapAttributeName, final DN baseDN,
            final AttributeDescription primaryKey, final AttributeMapper mapper) {
        this.ldapAttributeName = ldapAttributeName;
        super(ldapAttributeName);
        this.baseDN = baseDN;
        this.primaryKey = primaryKey;
        this.mapper = mapper;
    }
    /**
     * Indicates that the LDAP attribute is mandatory and must be provided
     * during create requests.
     *
     * @return This attribute mapper.
     */
    public ReferenceAttributeMapper isRequired() {
        this.isRequired = true;
        return this;
    }
    /**
     * Indicates that multi-valued LDAP attribute should be represented as a
     * single-valued JSON value, rather than an array of values.
     *
     * @return This attribute mapper.
     */
    public ReferenceAttributeMapper isSingleValued() {
        this.isSingleValued = true;
        return this;
    }
    /**
     * Sets the filter which should be used when searching for referenced LDAP
     * entries. The default is {@code (objectClass=*)}.
     *
@@ -148,30 +123,12 @@
        return this;
    }
    /**
     * Indicates whether or not the LDAP attribute supports updates. The default
     * is {@link WritabilityPolicy#READ_WRITE}.
     *
     * @param policy
     *            The writability policy.
     * @return This attribute mapper.
     */
    public ReferenceAttributeMapper writability(final WritabilityPolicy policy) {
        this.writabilityPolicy = policy;
        return this;
    }
    @Override
    void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute,
            final Set<String> ldapAttributes) {
        ldapAttributes.add(ldapAttributeName.toString());
    }
    @Override
    void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute,
            final String operator, final Object valueAssertion, final ResultHandler<Filter> h) {
    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
            final FilterType type, final String operator, final Object valueAssertion,
            final ResultHandler<Filter> h) {
        // Construct a filter which can be used to find referenced resources.
        mapper.getLDAPFilter(c, type, jsonAttribute, operator, valueAssertion,
        mapper.getLDAPFilter(c, path, subPath, type, operator, valueAssertion,
                new ResultHandler<Filter>() {
                    @Override
                    public void handleError(final ResourceException error) {
@@ -224,86 +181,27 @@
    }
    @Override
    void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) {
        final Attribute attribute = e.getAttribute(ldapAttributeName);
        if (attribute == null || attribute.isEmpty()) {
            h.handleResult(null);
        } else if (attributeIsSingleValued()) {
            try {
                final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
                readEntry(c, dn, h);
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                h.handleError(asResourceException(ex));
            }
        } else {
            try {
                final Set<DN> dns =
                        attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
                final ResultHandler<JsonValue> handler =
                        accumulate(dns.size(), transform(
                                new Function<List<JsonValue>, JsonValue, Void>() {
                                    @Override
                                    public JsonValue apply(final List<JsonValue> value, final Void p) {
                                        if (value.isEmpty()) {
                                            // No values, so omit the entire JSON object from the resource.
                                            return null;
                                        } else {
                                            // Combine values into a single JSON array.
                                            final List<Object> result =
                                                    new ArrayList<Object>(value.size());
                                            for (final JsonValue e : value) {
                                                result.add(e.getObject());
                                            }
                                            return new JsonValue(result);
                                        }
                                    }
                                }, h));
                for (final DN dn : dns) {
                    readEntry(c, dn, handler);
                }
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                h.handleError(asResourceException(ex));
            }
    void getNewLDAPAttributes(final Context c, final JsonPointer path,
            final List<Object> newValues, final ResultHandler<Attribute> h) {
        // No need to do anything if there are no values.
        if (newValues.isEmpty()) {
            h.handleResult(Attributes.emptyAttribute(ldapAttributeName));
            return;
        }
    }
    @Override
    void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) {
        try {
            if (v == null || v.isNull()) {
                if (attributeIsRequired()) {
                    // FIXME: improve error message.
                    throw new BadRequestException("no value provided");
                } else {
                    h.handleResult(Collections.<Attribute> emptyList());
                }
            } else if (v.isList() && attributeIsSingleValued()) {
                // FIXME: improve error message.
                throw new BadRequestException("expected single value, but got multiple values");
            } else if (!writabilityPolicy.canCreate(ldapAttributeName)) {
                if (writabilityPolicy.discardWrites()) {
                    h.handleResult(Collections.<Attribute> emptyList());
                } else {
                    // FIXME: improve error message.
                    throw new BadRequestException("attempted to create a read-only value");
                }
            } else {
                /*
                 * For each value use the subordinate mapper to obtain the LDAP
                 * primary key, the perform a search for each one to find the
                 * corresponding entries.
                 */
                final JsonValue valueList =
                        v.isList() ? v : new JsonValue(singletonList(v.getObject()));
                final Attribute reference = new LinkedAttribute(ldapAttributeName);
                final AtomicInteger pendingSearches = new AtomicInteger(valueList.size());
                final AtomicReference<ResourceException> exception =
                        new AtomicReference<ResourceException>();
        /*
         * For each value use the subordinate mapper to obtain the LDAP primary
         * key, the perform a search for each one to find the corresponding
         * entries.
         */
        final Attribute newLDAPAttribute = new LinkedAttribute(ldapAttributeName);
        final AtomicInteger pendingSearches = new AtomicInteger(newValues.size());
        final AtomicReference<ResourceException> exception =
                new AtomicReference<ResourceException>();
                for (final JsonValue value : valueList) {
                    mapper.toLDAP(c, value, new ResultHandler<List<Attribute>>() {
        for (final Object value : newValues) {
            mapper.toLDAP(c, path, null /* force create */, new JsonValue(value),
                    new ResultHandler<List<Modification>>() {
                        @Override
                        public void handleError(final ResourceException error) {
@@ -311,32 +209,29 @@
                        }
                        @Override
                        public void handleResult(final List<Attribute> result) {
                        public void handleResult(final List<Modification> result) {
                            Attribute primaryKeyAttribute = null;
                            for (final Attribute attribute : result) {
                                if (attribute.getAttributeDescription().equals(primaryKey)) {
                                    primaryKeyAttribute = attribute;
                            for (final Modification modification : result) {
                                if (modification.getAttribute().getAttributeDescription().equals(
                                        primaryKey)) {
                                    primaryKeyAttribute = modification.getAttribute();
                                    break;
                                }
                            }
                            if (primaryKeyAttribute == null) {
                                // FIXME: improve error message.
                                h.handleError(new BadRequestException(
                                        "reference primary key attribute is missing"));
                                return;
                            }
                            if (primaryKeyAttribute.isEmpty()) {
                                // FIXME: improve error message.
                            if (primaryKeyAttribute == null || primaryKeyAttribute.isEmpty()) {
                                h.handleError(new BadRequestException(
                                        "reference primary key attribute is empty"));
                                        i18n("The request cannot be processed because the reference "
                                                + "field '%s' contains a value which does not contain "
                                                + "a primary key", path)));
                                return;
                            }
                            if (primaryKeyAttribute.size() > 1) {
                                // FIXME: improve error message.
                                h.handleError(new BadRequestException(
                                        "reference primary key attribute contains multiple values"));
                                        i18n("The request cannot be processed because the reference "
                                                + "field '%s' contains a value which contains multiple "
                                                + "primary keys", path)));
                                return;
                            }
@@ -356,7 +251,7 @@
                                                    ResourceException re;
                                                    try {
                                                        throw error;
                                                    } catch (EntryNotFoundException e) {
                                                    } catch (final EntryNotFoundException e) {
                                                        // FIXME: improve error message.
                                                        re =
                                                                new BadRequestException(
@@ -364,7 +259,7 @@
                                                                                + primaryKeyValue
                                                                                        .toString()
                                                                                + "' does not exist");
                                                    } catch (MultipleEntriesFoundException e) {
                                                    } catch (final MultipleEntriesFoundException e) {
                                                        // FIXME: improve error message.
                                                        re =
                                                                new BadRequestException(
@@ -372,7 +267,7 @@
                                                                                + primaryKeyValue
                                                                                        .toString()
                                                                                + "' is ambiguous");
                                                    } catch (ErrorResultException e) {
                                                    } catch (final ErrorResultException e) {
                                                        re = asResourceException(e);
                                                    }
                                                    exception.compareAndSet(null, re);
@@ -382,8 +277,8 @@
                                                @Override
                                                public void handleResult(
                                                        final SearchResultEntry result) {
                                                    synchronized (reference) {
                                                        reference.add(result.getName());
                                                    synchronized (newLDAPAttribute) {
                                                        newLDAPAttribute.add(result.getName());
                                                    }
                                                    completeIfNecessary();
                                                }
@@ -393,40 +288,80 @@
                        private void completeIfNecessary() {
                            if (pendingSearches.decrementAndGet() == 0) {
                                if (exception.get() == null) {
                                    h.handleResult(singletonList(reference));
                                    h.handleResult(newLDAPAttribute);
                                } else {
                                    h.handleError(exception.get());
                                }
                            }
                        }
                    });
                }
            }
        } catch (final ResourceException e) {
            h.handleError(e);
        } catch (final Exception e) {
            // FIXME: improve error message.
            h.handleError(new BadRequestException(e.getMessage()));
        }
    }
    private boolean attributeIsRequired() {
        return isRequired;
    @Override
    ReferenceAttributeMapper getThis() {
        return this;
    }
    private boolean attributeIsSingleValued() {
        return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue();
    @Override
    void toJSON(final Context c, final JsonPointer path, final Entry e,
            final ResultHandler<JsonValue> h) {
        final Attribute attribute = e.getAttribute(ldapAttributeName);
        if (attribute == null || attribute.isEmpty()) {
            h.handleResult(null);
        } else if (attributeIsSingleValued()) {
            try {
                final DN dn = attribute.parse().usingSchema(c.getConfig().schema()).asDN();
                readEntry(c, path, dn, h);
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                h.handleError(asResourceException(ex));
            }
        } else {
            try {
                final Set<DN> dns =
                        attribute.parse().usingSchema(c.getConfig().schema()).asSetOfDN();
                final ResultHandler<JsonValue> handler =
                        accumulate(dns.size(), transform(
                                new Function<List<JsonValue>, JsonValue, Void>() {
                                    @Override
                                    public JsonValue apply(final List<JsonValue> value, final Void p) {
                                        if (value.isEmpty()) {
                                            /*
                                             * No values, so omit the entire
                                             * JSON object from the resource.
                                             */
                                            return null;
                                        } else {
                                            // Combine values into a single JSON array.
                                            final List<Object> result =
                                                    new ArrayList<Object>(value.size());
                                            for (final JsonValue e : value) {
                                                result.add(e.getObject());
                                            }
                                            return new JsonValue(result);
                                        }
                                    }
                                }, h));
                for (final DN dn : dns) {
                    readEntry(c, path, dn, handler);
                }
            } catch (final Exception ex) {
                // The LDAP attribute could not be decoded.
                h.handleError(asResourceException(ex));
            }
        }
    }
    private SearchRequest createSearchRequest(final Filter result) {
        final Filter searchFilter = filter != null ? Filter.and(filter, result) : result;
        final SearchRequest request = Requests.newSearchRequest(baseDN, scope, searchFilter, "1.1");
        return request;
        return newSearchRequest(baseDN, scope, searchFilter, "1.1");
    }
    private void readEntry(final Context c, final DN dn, final ResultHandler<JsonValue> handler) {
    private void readEntry(final Context c, final JsonPointer path, final DN dn,
            final ResultHandler<JsonValue> handler) {
        final Set<String> requestedLDAPAttributes = new LinkedHashSet<String>();
        mapper.getLDAPAttributes(c, new JsonPointer(), requestedLDAPAttributes);
        mapper.getLDAPAttributes(c, path, new JsonPointer(), requestedLDAPAttributes);
        c.getConnection().readEntryAsync(dn, requestedLDAPAttributes,
                new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() {
@@ -435,14 +370,17 @@
                        if (!(error instanceof EntryNotFoundException)) {
                            handler.handleError(asResourceException(error));
                        } else {
                            // The referenced entry does not exist so ignore it since it cannot be mapped.
                            /*
                             * The referenced entry does not exist so ignore it
                             * since it cannot be mapped.
                             */
                            handler.handleResult(null);
                        }
                    }
                    @Override
                    public void handleResult(final SearchResultEntry result) {
                        mapper.toJSON(c, result, handler);
                        mapper.toJSON(c, path, result, handler);
                    }
                });
    }