| | |
| | | */ |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | * 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. |
| | | */ |
| | |
| | | |
| | | 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=*)}. |
| | | * |
| | |
| | | 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) { |
| | |
| | | } |
| | | |
| | | @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) { |
| | |
| | | } |
| | | |
| | | @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; |
| | | } |
| | | |
| | |
| | | ResourceException re; |
| | | try { |
| | | throw error; |
| | | } catch (EntryNotFoundException e) { |
| | | } catch (final EntryNotFoundException e) { |
| | | // FIXME: improve error message. |
| | | re = |
| | | new BadRequestException( |
| | |
| | | + primaryKeyValue |
| | | .toString() |
| | | + "' does not exist"); |
| | | } catch (MultipleEntriesFoundException e) { |
| | | } catch (final MultipleEntriesFoundException e) { |
| | | // FIXME: improve error message. |
| | | re = |
| | | new BadRequestException( |
| | |
| | | + primaryKeyValue |
| | | .toString() |
| | | + "' is ambiguous"); |
| | | } catch (ErrorResultException e) { |
| | | } catch (final ErrorResultException e) { |
| | | re = asResourceException(e); |
| | | } |
| | | exception.compareAndSet(null, re); |
| | |
| | | @Override |
| | | public void handleResult( |
| | | final SearchResultEntry result) { |
| | | synchronized (reference) { |
| | | reference.add(result.getName()); |
| | | synchronized (newLDAPAttribute) { |
| | | newLDAPAttribute.add(result.getName()); |
| | | } |
| | | completeIfNecessary(); |
| | | } |
| | |
| | | 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>() { |
| | | |
| | |
| | | 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); |
| | | } |
| | | }); |
| | | } |