Partial fix for OPENDJ-693: Implement modify/update support
* add support for all attribute mappers
2 files added
12 files modified
| | |
| | | // The REST APIs and their LDAP attribute mappings. |
| | | "mappings" : { |
| | | "/users" : { |
| | | "baseDN" : "ou=people,dc=example,dc=com", |
| | | "readOnUpdatePolicy" : "controls", |
| | | "useSubtreeDelete" : true, |
| | | "etagAttribute" : "etag", |
| | | "namingStrategy" : { |
| | | "strategy" : "clientDNNaming", |
| | | "baseDN" : "ou=people,dc=example,dc=com", |
| | | "readOnUpdatePolicy" : "controls", |
| | | "useSubtreeDelete" : true, |
| | | "usePermissiveModify" : true, |
| | | "etagAttribute" : "etag", |
| | | "namingStrategy" : { |
| | | "strategy" : "clientDNNaming", |
| | | "dnAttribute" : "uid" |
| | | }, |
| | | "additionalLDAPAttributes" : [ |
| | |
| | | } |
| | | }, |
| | | "/groups" : { |
| | | "baseDN" : "ou=groups,dc=example,dc=com", |
| | | "readOnUpdatePolicy" : "controls", |
| | | "useSubtreeDelete" : true, |
| | | "etagAttribute" : "etag", |
| | | "namingStrategy" : { |
| | | "strategy" : "clientDNNaming", |
| | | "baseDN" : "ou=groups,dc=example,dc=com", |
| | | "readOnUpdatePolicy" : "controls", |
| | | "useSubtreeDelete" : true, |
| | | "usePermissiveModify" : true, |
| | | "etagAttribute" : "etag", |
| | | "namingStrategy" : { |
| | | "strategy" : "clientDNNaming", |
| | | "dnAttribute" : "cn" |
| | | }, |
| | | "additionalLDAPAttributes" : [ |
| New file |
| | |
| | | /* |
| | | * The contents of this file are subject to the terms of the Common Development and |
| | | * Distribution License (the License). You may not use this file except in compliance with the |
| | | * License. |
| | | * |
| | | * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the |
| | | * specific language governing permission and limitations under the License. |
| | | * |
| | | * When distributing Covered Software, include this CDDL Header Notice in each file and include |
| | | * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013 ForgeRock AS. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static java.util.Collections.emptyList; |
| | | import static java.util.Collections.singletonList; |
| | | import static org.forgerock.opendj.ldap.Attributes.emptyAttribute; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.AttributeDescription; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.LinkedAttribute; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | import org.forgerock.opendj.ldap.ModificationType; |
| | | |
| | | /** |
| | | * An abstract LDAP attribute mapper which provides a simple mapping from a JSON |
| | | * value to a single LDAP attribute. |
| | | */ |
| | | abstract class AbstractLDAPAttributeMapper<T extends AbstractLDAPAttributeMapper<T>> extends |
| | | AttributeMapper { |
| | | Object defaultJSONValue = null; |
| | | List<Object> defaultJSONValues = emptyList(); |
| | | final AttributeDescription ldapAttributeName; |
| | | private boolean isRequired = false; |
| | | private boolean isSingleValued = false; |
| | | private WritabilityPolicy writabilityPolicy = READ_WRITE; |
| | | |
| | | AbstractLDAPAttributeMapper(final AttributeDescription ldapAttributeName) { |
| | | this.ldapAttributeName = ldapAttributeName; |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the LDAP attribute is mandatory and must be provided |
| | | * during create requests. |
| | | * |
| | | * @return This attribute mapper. |
| | | */ |
| | | public final T isRequired() { |
| | | this.isRequired = true; |
| | | return getThis(); |
| | | } |
| | | |
| | | /** |
| | | * 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 final T isSingleValued() { |
| | | this.isSingleValued = true; |
| | | return getThis(); |
| | | } |
| | | |
| | | /** |
| | | * 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 final T writability(final WritabilityPolicy policy) { |
| | | this.writabilityPolicy = policy; |
| | | return getThis(); |
| | | } |
| | | |
| | | boolean attributeIsRequired() { |
| | | return isRequired && defaultJSONValue == null; |
| | | } |
| | | |
| | | boolean attributeIsSingleValued() { |
| | | return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue(); |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath, |
| | | final Set<String> ldapAttributes) { |
| | | ldapAttributes.add(ldapAttributeName.toString()); |
| | | } |
| | | |
| | | abstract void getNewLDAPAttributes(Context c, JsonPointer path, List<Object> newValues, |
| | | ResultHandler<Attribute> h); |
| | | |
| | | abstract T getThis(); |
| | | |
| | | @Override |
| | | void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v, |
| | | final ResultHandler<List<Modification>> h) { |
| | | // Make following code readable. |
| | | final boolean isUpdateRequest = e != null; |
| | | final boolean isCreateRequest = !isUpdateRequest; |
| | | |
| | | // Get the existing LDAP attribute. |
| | | final Attribute oldLDAPAttribute; |
| | | if (isCreateRequest) { |
| | | oldLDAPAttribute = emptyAttribute(ldapAttributeName); |
| | | } else { |
| | | final Attribute tmp = e.getAttribute(ldapAttributeName); |
| | | oldLDAPAttribute = tmp != null ? tmp : emptyAttribute(ldapAttributeName); |
| | | } |
| | | |
| | | if (v != null && v.isList() && attributeIsSingleValued()) { |
| | | // Single-valued field violation. |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because an array of values was " |
| | | + "provided for the single valued field '%s'", path))); |
| | | } else { |
| | | getNewLDAPAttributes(c, path, asList(v), new ResultHandler<Attribute>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final Attribute newLDAPAttribute) { |
| | | /* |
| | | * If the attribute is read-only then handle the following |
| | | * cases: |
| | | * |
| | | * 1) new values are provided and they are the same as the |
| | | * existing values |
| | | * |
| | | * 2) no new values are provided. |
| | | */ |
| | | if (isCreateRequest && !writabilityPolicy.canCreate(ldapAttributeName) |
| | | || isUpdateRequest && !writabilityPolicy.canWrite(ldapAttributeName)) { |
| | | if (newLDAPAttribute.isEmpty() |
| | | || (isUpdateRequest && newLDAPAttribute.equals(oldLDAPAttribute)) |
| | | || writabilityPolicy.discardWrites()) { |
| | | // No change. |
| | | h.handleResult(Collections.<Modification> emptyList()); |
| | | } else { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because it attempts to modify " |
| | | + "the read-only field '%s'", path))); |
| | | } |
| | | } else { |
| | | // Compute the changes to the attribute. |
| | | final List<Modification> modifications; |
| | | if (oldLDAPAttribute.isEmpty() && newLDAPAttribute.isEmpty()) { |
| | | // No change. |
| | | modifications = Collections.<Modification> emptyList(); |
| | | } else if (oldLDAPAttribute.isEmpty() || newLDAPAttribute.isEmpty()) { |
| | | // Delete or add. |
| | | modifications = |
| | | singletonList(new Modification(ModificationType.REPLACE, |
| | | newLDAPAttribute)); |
| | | } else { |
| | | /* |
| | | * We could do a replace, but try to save bandwidth |
| | | * and send diffs instead. Perform deletes first in |
| | | * case we don't have an appropriate normalizer: |
| | | * permissive add(x) followed by delete(x) is |
| | | * destructive, whereas delete(x) followed by add(x) |
| | | * is idempotent when adding/removing the same |
| | | * value. |
| | | */ |
| | | modifications = new ArrayList<Modification>(2); |
| | | |
| | | final Attribute deletedValues = new LinkedAttribute(oldLDAPAttribute); |
| | | deletedValues.removeAll(newLDAPAttribute); |
| | | if (!deletedValues.isEmpty()) { |
| | | modifications.add(new Modification(ModificationType.DELETE, |
| | | deletedValues)); |
| | | } |
| | | |
| | | final Attribute addedValues = new LinkedAttribute(newLDAPAttribute); |
| | | addedValues.removeAll(oldLDAPAttribute); |
| | | if (!addedValues.isEmpty()) { |
| | | modifications.add(new Modification(ModificationType.ADD, |
| | | addedValues)); |
| | | } |
| | | } |
| | | h.handleResult(modifications); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | |
| | | private List<Object> asList(final JsonValue v) { |
| | | if (v == null || v.isNull() || (v.isList() && v.size() == 0)) { |
| | | return defaultJSONValues; |
| | | } else if (v.isList()) { |
| | | return v.asList(); |
| | | } else { |
| | | return singletonList(v.getObject()); |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | |
| | | /** |
| | | * An attribute mapper is responsible for converting JSON values to and from |
| | |
| | | * The set into which the required LDAP attribute names should be |
| | | * put. |
| | | */ |
| | | abstract void getLDAPAttributes(Context c, JsonPointer jsonAttribute, Set<String> ldapAttributes); |
| | | abstract void getLDAPAttributes(Context c, JsonPointer path, JsonPointer subPath, |
| | | Set<String> ldapAttributes); |
| | | |
| | | /** |
| | | * Transforms the provided REST comparison filter parameters to an LDAP |
| | |
| | | * @param h |
| | | * The result handler. |
| | | */ |
| | | abstract void getLDAPFilter(Context c, FilterType type, JsonPointer jsonAttribute, |
| | | abstract void getLDAPFilter(Context c, JsonPointer path, JsonPointer subPath, FilterType type, |
| | | String operator, Object valueAssertion, ResultHandler<Filter> h); |
| | | |
| | | /** |
| | |
| | | * @param h |
| | | * The result handler. |
| | | */ |
| | | abstract void toJSON(Context c, Entry e, ResultHandler<JsonValue> h); |
| | | abstract void toJSON(Context c, JsonPointer path, Entry e, ResultHandler<JsonValue> h); |
| | | |
| | | /** |
| | | * Maps a JSON value to one or more LDAP attributes, invoking a completion |
| | |
| | | * @param h |
| | | * The result handler. |
| | | */ |
| | | abstract void toLDAP(Context c, JsonValue v, ResultHandler<List<Attribute>> h); |
| | | abstract void toLDAP(Context c, JsonPointer path, Entry e, JsonValue v, |
| | | ResultHandler<List<Modification>> h); |
| | | |
| | | // TODO: methods for obtaining schema information (e.g. name, description, |
| | | // type information). |
| | |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.Context; |
| | |
| | | AuthenticatedConnectionContext(final JsonValue savedContext, final PersistenceConfig config) |
| | | throws ResourceException { |
| | | super(savedContext, config); |
| | | throw new InternalServerErrorException("Cached LDAP connections cannot be restored"); |
| | | throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be restored")); |
| | | } |
| | | |
| | | /** |
| | |
| | | protected void saveToJson(final JsonValue savedContext, final PersistenceConfig config) |
| | | throws ResourceException { |
| | | super.saveToJson(savedContext, config); |
| | | throw new InternalServerErrorException("Cached LDAP connections cannot be persisted"); |
| | | throw new InternalServerErrorException(i18n("Cached LDAP connections cannot be persisted")); |
| | | } |
| | | |
| | | /** |
| | |
| | | |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.isJSONPrimitive; |
| | | |
| | | import java.util.ArrayList; |
| | |
| | | DN.valueOf(authzId.substring(3), schema); |
| | | } catch (final IllegalArgumentException e) { |
| | | throw new ForbiddenException( |
| | | "The request could not be authorized because the required security principal " |
| | | + " was not a valid LDAP DN"); |
| | | i18n("The request could not be authorized because the required " |
| | | + "security principal was not a valid LDAP DN")); |
| | | } |
| | | return authzId; |
| | | } |
| | |
| | | if (isJSONPrimitive(value)) { |
| | | values[i] = String.valueOf(value); |
| | | } else if (value == null) { |
| | | // FIXME: i18n. |
| | | throw new ForbiddenException( |
| | | "The request could not be authorized because the required security principal " |
| | | + key + " could not be determined"); |
| | | throw new ForbiddenException(i18n( |
| | | "The request could not be authorized because the required " |
| | | + "security principal '%s' could not be determined", key)); |
| | | } else { |
| | | // FIXME: i18n. |
| | | throw new ForbiddenException( |
| | | "The request could not be authorized because the required security principal " |
| | | + key + " had an invalid data type"); |
| | | throw new ForbiddenException(i18n( |
| | | "The request could not be authorized because the required " |
| | | + "security principal '%s' had an invalid data type", key)); |
| | | } |
| | | } |
| | | return values; |
| | |
| | | private final ReadOnUpdatePolicy readOnUpdatePolicy; |
| | | private final Schema schema; |
| | | private final boolean useSubtreeDelete; |
| | | private final boolean usePermissiveModify; |
| | | |
| | | Config(final ConnectionFactory factory, final ReadOnUpdatePolicy readOnUpdatePolicy, |
| | | final AuthorizationPolicy authzPolicy, final AuthzIdTemplate proxiedAuthzTemplate, |
| | | final boolean useSubtreeDelete, final Schema schema) { |
| | | final boolean useSubtreeDelete, final boolean usePermissiveModify, final Schema schema) { |
| | | this.factory = factory; |
| | | this.readOnUpdatePolicy = readOnUpdatePolicy; |
| | | this.authzPolicy = authzPolicy; |
| | | this.proxiedAuthzTemplate = proxiedAuthzTemplate; |
| | | this.useSubtreeDelete = useSubtreeDelete; |
| | | this.usePermissiveModify = usePermissiveModify; |
| | | this.schema = schema; |
| | | this.options = new DecodeOptions().setSchema(schema); |
| | | } |
| | |
| | | } |
| | | |
| | | /** |
| | | * Returns {@code true} if modify requests should include the permissive |
| | | * modify control. |
| | | * |
| | | * @return {@code true} if modify requests should include the permissive |
| | | * modify control. |
| | | */ |
| | | boolean usePermissiveModify() { |
| | | return usePermissiveModify; |
| | | } |
| | | |
| | | /** |
| | | * Returns {@code true} if delete requests should include the subtree delete |
| | | * control. |
| | | * |
| | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | |
| | | /** |
| | | * An attribute mapper which maps a single JSON attribute to a fixed value. |
| | |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute, |
| | | void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath, |
| | | final Set<String> ldapAttributes) { |
| | | // Nothing to do. |
| | | } |
| | | |
| | | @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) { |
| | | final Filter filter; |
| | | final JsonValue subValue = value.get(jsonAttribute); |
| | | final JsonValue subValue = value.get(subPath); |
| | | if (subValue == null) { |
| | | filter = alwaysFalse(); |
| | | } else if (type == FilterType.PRESENT) { |
| | |
| | | } |
| | | |
| | | @Override |
| | | void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) { |
| | | void toJSON(final Context c, final JsonPointer path, final Entry e, |
| | | final ResultHandler<JsonValue> h) { |
| | | h.handleResult(value.copy()); |
| | | } |
| | | |
| | | @Override |
| | | void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) { |
| | | void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v, |
| | | final ResultHandler<List<Modification>> h) { |
| | | // FIXME: should we check if the provided value matches the constant? |
| | | h.handleResult(Collections.<Attribute> emptyList()); |
| | | h.handleResult(Collections.<Modification> emptyList()); |
| | | } |
| | | |
| | | private <T extends Comparable<T>> Filter compare(final Context c, final FilterType type, |
| | |
| | | |
| | | import static org.forgerock.opendj.ldap.Filter.alwaysFalse; |
| | | import static org.forgerock.opendj.ldap.Filter.alwaysTrue; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newDeleteRequest; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newModifyRequest; |
| | | import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; |
| | | import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.CONTROLS; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.asResourceException; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.accumulate; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.toFilter; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.transform; |
| | | |
| | |
| | | import org.forgerock.json.resource.CollectionResourceProvider; |
| | | import org.forgerock.json.resource.CreateRequest; |
| | | import org.forgerock.json.resource.DeleteRequest; |
| | | import org.forgerock.json.resource.InternalServerErrorException; |
| | | import org.forgerock.json.resource.NotSupportedException; |
| | | import org.forgerock.json.resource.PatchRequest; |
| | | import org.forgerock.json.resource.PreconditionFailedException; |
| | | import org.forgerock.json.resource.QueryFilter; |
| | | import org.forgerock.json.resource.QueryFilterVisitor; |
| | | import org.forgerock.json.resource.QueryRequest; |
| | |
| | | import org.forgerock.opendj.ldap.ErrorResultException; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Function; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | import org.forgerock.opendj.ldap.ModificationType; |
| | | import org.forgerock.opendj.ldap.SearchResultHandler; |
| | | import org.forgerock.opendj.ldap.SearchScope; |
| | | import org.forgerock.opendj.ldap.controls.AssertionRequestControl; |
| | | import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl; |
| | | import org.forgerock.opendj.ldap.controls.PostReadRequestControl; |
| | | import org.forgerock.opendj.ldap.controls.PostReadResponseControl; |
| | | import org.forgerock.opendj.ldap.controls.PreReadRequestControl; |
| | | import org.forgerock.opendj.ldap.controls.PreReadResponseControl; |
| | | import org.forgerock.opendj.ldap.controls.SubtreeDeleteRequestControl; |
| | | import org.forgerock.opendj.ldap.requests.AddRequest; |
| | | import org.forgerock.opendj.ldap.requests.Requests; |
| | | import org.forgerock.opendj.ldap.requests.ModifyRequest; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.opendj.ldap.responses.Result; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | |
| | | @Override |
| | | public void run() { |
| | | // Calculate entry content. |
| | | attributeMapper.toLDAP(c, request.getContent(), |
| | | new ResultHandler<List<Attribute>>() { |
| | | attributeMapper.toLDAP(c, new JsonPointer(), null, request.getContent(), |
| | | new ResultHandler<List<Modification>>() { |
| | | @Override |
| | | public void handleError(final ResourceException error) { |
| | | h.handleError(error); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final List<Attribute> result) { |
| | | public void handleResult(final List<Modification> result) { |
| | | // Perform add operation. |
| | | final AddRequest addRequest = Requests.newAddRequest(DN.rootDN()); |
| | | final AddRequest addRequest = newAddRequest(DN.rootDN()); |
| | | for (final Attribute attribute : additionalLDAPAttributes) { |
| | | addRequest.addAttribute(attribute); |
| | | } |
| | | for (final Attribute attribute : result) { |
| | | addRequest.addAttribute(attribute); |
| | | for (final Modification modification : result) { |
| | | if (modification.getModificationType() == ModificationType.ADD |
| | | || modification.getModificationType() == ModificationType.REPLACE) { |
| | | addRequest.addAttribute(modification.getAttribute()); |
| | | } else { |
| | | // Attribute mappers must return add/replace updates. |
| | | h.handleError(new InternalServerErrorException( |
| | | i18n("Attribute mapper returned a modification which " |
| | | + "does not add an attribute"))); |
| | | return; |
| | | } |
| | | } |
| | | try { |
| | | nameStrategy.setResourceId(c, getBaseDN(c), request |
| | |
| | | @Override |
| | | public void run() { |
| | | // Find the entry and then delete it. |
| | | final String ldapAttribute = |
| | | (etagAttribute != null && request.getRevision() != null) ? etagAttribute |
| | | .toString() : "1.1"; |
| | | final SearchRequest searchRequest = |
| | | nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute( |
| | | "1.1"); |
| | | ldapAttribute); |
| | | c.getConnection().searchSingleEntryAsync(searchRequest, |
| | | new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() { |
| | | @Override |
| | |
| | | @Override |
| | | public void handleResult(final SearchResultEntry entry) { |
| | | try { |
| | | // Fail-fast if there is a version mismatch. |
| | | ensureMVCCVersionMatches(entry, request.getRevision()); |
| | | |
| | | // Perform delete operation. |
| | | final ChangeRecord deleteRequest = |
| | | Requests.newDeleteRequest(entry.getName()); |
| | | newDeleteRequest(entry.getName()); |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | | final String[] attributes = |
| | | getLDAPAttributes(c, request.getFields()); |
| | |
| | | final Context c = wrap(context); |
| | | final QueryResultHandler h = wrap(c, handler); |
| | | |
| | | // Get the connection, then calculate the search filter, then perform the search. |
| | | /* |
| | | * Get the connection, then calculate the search filter, then perform |
| | | * the search. |
| | | */ |
| | | c.run(h, new Runnable() { |
| | | @Override |
| | | public void run() { |
| | |
| | | |
| | | @Override |
| | | public void handleResult(final Filter ldapFilter) { |
| | | // Avoid performing a search if the filter could not be mapped or if it will never match. |
| | | /* |
| | | * Avoid performing a search if the filter could not be |
| | | * mapped or if it will never match. |
| | | */ |
| | | if (ldapFilter == null || ldapFilter == alwaysFalse()) { |
| | | h.handleResult(new QueryResult()); |
| | | } else { |
| | | // Perform the search. |
| | | final String[] attributes = getLDAPAttributes(c, request.getFields()); |
| | | final SearchRequest request = |
| | | Requests.newSearchRequest(getBaseDN(c), |
| | | SearchScope.SINGLE_LEVEL, ldapFilter == Filter |
| | | .alwaysTrue() ? Filter.objectClassPresent() |
| | | : ldapFilter, attributes); |
| | | newSearchRequest(getBaseDN(c), SearchScope.SINGLE_LEVEL, |
| | | ldapFilter == Filter.alwaysTrue() ? Filter |
| | | .objectClassPresent() : ldapFilter, attributes); |
| | | c.getConnection().searchAsync(request, null, new SearchResultHandler() { |
| | | private final AtomicInteger pendingResourceCount = |
| | | new AtomicInteger(); |
| | |
| | | }; |
| | | |
| | | pendingResourceCount.incrementAndGet(); |
| | | attributeMapper.toJSON(c, entry, mapHandler); |
| | | attributeMapper.toJSON(c, new JsonPointer(), entry, mapHandler); |
| | | return true; |
| | | } |
| | | |
| | |
| | | @Override |
| | | public void updateInstance(final ServerContext context, final String resourceId, |
| | | final UpdateRequest request, final ResultHandler<Resource> handler) { |
| | | handler.handleError(new NotSupportedException("Not yet implemented")); |
| | | /* |
| | | * Update operations are a bit awkward because there is not direct |
| | | * mapping to LDAP. We need to convert the update request into an LDAP |
| | | * modify operation which means reading the current LDAP entry, |
| | | * generating the new entry content, then comparing the two in order to |
| | | * obtain a set of changes. We also need to handle read-only fields |
| | | * correctly: if a read-only field is included with the new resource |
| | | * then it must match exactly the value of the existing field. |
| | | */ |
| | | final Context c = wrap(context); |
| | | final ResultHandler<Resource> h = wrap(c, handler); |
| | | |
| | | // Get connection then perform the search. |
| | | c.run(h, new Runnable() { |
| | | @Override |
| | | public void run() { |
| | | // First of all read the existing entry. |
| | | final String[] attributes = |
| | | getLDAPAttributes(c, Collections.<JsonPointer> emptyList()); |
| | | final SearchRequest searchRequest = |
| | | nameStrategy.createSearchRequest(c, getBaseDN(c), resourceId).addAttribute( |
| | | attributes); |
| | | c.getConnection().searchSingleEntryAsync(searchRequest, |
| | | new org.forgerock.opendj.ldap.ResultHandler<SearchResultEntry>() { |
| | | @Override |
| | | public void handleErrorResult(final ErrorResultException error) { |
| | | h.handleError(asResourceException(error)); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult(final SearchResultEntry entry) { |
| | | try { |
| | | // Fail-fast if there is a version mismatch. |
| | | ensureMVCCVersionMatches(entry, request.getRevision()); |
| | | |
| | | // Create the modify request. |
| | | final ModifyRequest modifyRequest = |
| | | newModifyRequest(entry.getName()); |
| | | if (config.readOnUpdatePolicy() == CONTROLS) { |
| | | final String[] attributes = |
| | | getLDAPAttributes(c, request.getFields()); |
| | | modifyRequest.addControl(PostReadRequestControl.newControl( |
| | | false, attributes)); |
| | | } |
| | | if (config.usePermissiveModify()) { |
| | | modifyRequest.addControl(PermissiveModifyRequestControl |
| | | .newControl(true)); |
| | | } |
| | | addAssertionControl(modifyRequest, request.getRevision()); |
| | | |
| | | /* |
| | | * Determine the set of changes that need to |
| | | * be performed. |
| | | */ |
| | | attributeMapper.toLDAP(c, new JsonPointer(), entry, request |
| | | .getNewContent(), |
| | | new ResultHandler<List<Modification>>() { |
| | | @Override |
| | | public void handleError( |
| | | final ResourceException error) { |
| | | h.handleError(error); |
| | | } |
| | | |
| | | @Override |
| | | public void handleResult( |
| | | final List<Modification> result) { |
| | | // Perform the modify operation. |
| | | modifyRequest.getModifications().addAll(result); |
| | | c.getConnection().applyChangeAsync( |
| | | modifyRequest, null, |
| | | postUpdateHandler(c, h)); |
| | | } |
| | | }); |
| | | } catch (final Exception e) { |
| | | h.handleError(asResourceException(e)); |
| | | } |
| | | } |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private void adaptEntry(final Context c, final Entry entry, |
| | | final ResultHandler<Resource> handler) { |
| | | final String actualResourceId = nameStrategy.getResourceId(c, entry); |
| | | final String revision = getRevisionFromEntry(entry); |
| | | attributeMapper.toJSON(c, entry, transform(new Function<JsonValue, Resource, Void>() { |
| | | @Override |
| | | public Resource apply(final JsonValue value, final Void p) { |
| | | return new Resource(actualResourceId, revision, new JsonValue(value)); |
| | | } |
| | | }, handler)); |
| | | attributeMapper.toJSON(c, new JsonPointer(), entry, transform( |
| | | new Function<JsonValue, Resource, Void>() { |
| | | @Override |
| | | public Resource apply(final JsonValue value, final Void p) { |
| | | return new Resource(actualResourceId, revision, new JsonValue(value)); |
| | | } |
| | | }, handler)); |
| | | } |
| | | |
| | | private void addAssertionControl(final ChangeRecord request, final String revision) |
| | | throws NotSupportedException { |
| | | if (revision != null) { |
| | | if (etagAttribute != null) { |
| | | request.addControl(AssertionRequestControl.newControl(true, Filter.equality( |
| | | etagAttribute.toString(), revision))); |
| | | } else { |
| | | // FIXME: i18n |
| | | throw new NotSupportedException( |
| | | "Multi-version concurrency control is not supported by this resource"); |
| | | private void addAssertionControl(final ChangeRecord request, final String expectedRevision) |
| | | throws ResourceException { |
| | | if (expectedRevision != null) { |
| | | ensureMVCCSupported(); |
| | | request.addControl(AssertionRequestControl.newControl(true, Filter.equality( |
| | | etagAttribute.toString(), expectedRevision))); |
| | | } |
| | | } |
| | | |
| | | private void ensureMVCCSupported() throws NotSupportedException { |
| | | if (etagAttribute == null) { |
| | | throw new NotSupportedException( |
| | | i18n("Multi-version concurrency control is not supported by this resource")); |
| | | } |
| | | } |
| | | |
| | | private void ensureMVCCVersionMatches(final Entry entry, final String expectedRevision) |
| | | throws ResourceException { |
| | | if (expectedRevision != null) { |
| | | ensureMVCCSupported(); |
| | | final String actualRevision = entry.parseAttribute(etagAttribute).asString(); |
| | | if (actualRevision == null) { |
| | | throw new PreconditionFailedException(i18n( |
| | | "The resource could not be accessed because it did not contain any " |
| | | + "version information, when the version '%s' was expected", |
| | | expectedRevision)); |
| | | } else if (!expectedRevision.equals(actualRevision)) { |
| | | throw new PreconditionFailedException(i18n( |
| | | "The resource could not be accessed because the expected version '%s' " |
| | | + "does not match the current version '%s'", expectedRevision, |
| | | actualRevision)); |
| | | } |
| | | } |
| | | } |
| | |
| | | if (requestedAttributes.isEmpty()) { |
| | | // Full read. |
| | | requestedLDAPAttributes = new LinkedHashSet<String>(); |
| | | attributeMapper.getLDAPAttributes(c, new JsonPointer(), requestedLDAPAttributes); |
| | | attributeMapper.getLDAPAttributes(c, new JsonPointer(), new JsonPointer(), |
| | | requestedLDAPAttributes); |
| | | } else { |
| | | // Partial read. |
| | | requestedLDAPAttributes = new LinkedHashSet<String>(requestedAttributes.size()); |
| | | for (final JsonPointer requestedAttribute : requestedAttributes) { |
| | | attributeMapper.getLDAPAttributes(c, requestedAttribute, requestedLDAPAttributes); |
| | | attributeMapper.getLDAPAttributes(c, new JsonPointer(), requestedAttribute, |
| | | requestedLDAPAttributes); |
| | | } |
| | | } |
| | | |
| | |
| | | @Override |
| | | public Void visitContainsFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, FilterType.CONTAINS, field, null, |
| | | valueAssertion, p); |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.CONTAINS, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitEqualsFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, FilterType.EQUAL_TO, field, null, |
| | | valueAssertion, p); |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.EQUAL_TO, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | |
| | | public Void visitExtendedMatchFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final String operator, |
| | | final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, FilterType.EXTENDED, field, operator, |
| | | valueAssertion, p); |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.EXTENDED, operator, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitGreaterThanFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, FilterType.GREATER_THAN, field, null, |
| | | valueAssertion, p); |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.GREATER_THAN, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitGreaterThanOrEqualToFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, FilterType.GREATER_THAN_OR_EQUAL_TO, |
| | | field, null, valueAssertion, p); |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.GREATER_THAN_OR_EQUAL_TO, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitLessThanFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, FilterType.LESS_THAN, field, null, |
| | | valueAssertion, p); |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.LESS_THAN, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitLessThanOrEqualToFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, FilterType.LESS_THAN_OR_EQUAL_TO, field, |
| | | null, valueAssertion, p); |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.LESS_THAN_OR_EQUAL_TO, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | |
| | | @Override |
| | | public Void visitPresentFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field) { |
| | | attributeMapper.getLDAPFilter(c, FilterType.PRESENT, field, null, null, p); |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.PRESENT, null, null, p); |
| | | return null; |
| | | } |
| | | |
| | | @Override |
| | | public Void visitStartsWithFilter(final ResultHandler<Filter> p, |
| | | final JsonPointer field, final Object valueAssertion) { |
| | | attributeMapper.getLDAPFilter(c, FilterType.STARTS_WITH, field, null, |
| | | valueAssertion, p); |
| | | attributeMapper.getLDAPFilter(c, new JsonPointer(), field, |
| | | FilterType.STARTS_WITH, null, valueAssertion, p); |
| | | return null; |
| | | } |
| | | |
| | | }; |
| | | // Note that the returned LDAP filter may be null if it could not be mapped by any attribute mappers. |
| | | /* |
| | | * Note that the returned LDAP filter may be null if it could not be |
| | | * mapped by any attribute mappers. |
| | | */ |
| | | queryFilter.accept(visitor, h); |
| | | } |
| | | |
| | |
| | | |
| | | import static org.forgerock.opendj.ldap.Filter.alwaysFalse; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.accumulate; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.transform; |
| | | |
| | |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Function; |
| | | import org.forgerock.opendj.ldap.Modification; |
| | | |
| | | /** |
| | | * An attribute mapper which maps JSON objects to LDAP attributes. |
| | |
| | | this.name = name; |
| | | this.mapper = mapper; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | return name + " -> " + mapper; |
| | | } |
| | | } |
| | | |
| | | private final Map<String, Mapping> mappings = new LinkedHashMap<String, Mapping>(); |
| | |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute, |
| | | void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath, |
| | | final Set<String> ldapAttributes) { |
| | | if (jsonAttribute.isEmpty()) { |
| | | if (subPath.isEmpty()) { |
| | | // Request all subordinate mappings. |
| | | for (final Mapping mapping : mappings.values()) { |
| | | mapping.mapper.getLDAPAttributes(c, jsonAttribute, ldapAttributes); |
| | | mapping.mapper.getLDAPAttributes(c, path.child(mapping.name), subPath, |
| | | ldapAttributes); |
| | | } |
| | | } else { |
| | | // Request single subordinate mapping. |
| | | final Mapping mapping = getMapping(jsonAttribute); |
| | | final Mapping mapping = getMapping(subPath); |
| | | if (mapping != null) { |
| | | final JsonPointer relativePointer = jsonAttribute.relativePointer(); |
| | | mapping.mapper.getLDAPAttributes(c, relativePointer, ldapAttributes); |
| | | mapping.mapper.getLDAPAttributes(c, path.child(subPath.get(0)), subPath |
| | | .relativePointer(), ldapAttributes); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void getLDAPFilter(final Context c, final FilterType type, final JsonPointer jsonAttribute, |
| | | final String operator, final Object valueAssertion, final ResultHandler<Filter> h) { |
| | | final Mapping mapping = getMapping(jsonAttribute); |
| | | void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath, |
| | | final FilterType type, final String operator, final Object valueAssertion, |
| | | final ResultHandler<Filter> h) { |
| | | final Mapping mapping = getMapping(subPath); |
| | | if (mapping != null) { |
| | | final JsonPointer relativePointer = jsonAttribute.relativePointer(); |
| | | mapping.mapper.getLDAPFilter(c, type, relativePointer, operator, valueAssertion, h); |
| | | mapping.mapper.getLDAPFilter(c, path.child(subPath.get(0)), subPath.relativePointer(), |
| | | type, operator, valueAssertion, h); |
| | | } else { |
| | | // Either the filter targeted the entire object (i.e. it was "/"), or it targeted |
| | | // an unrecognized attribute within the object. Either way, the filter will |
| | | // never match. |
| | | /* |
| | | * Either the filter targeted the entire object (i.e. it was "/"), |
| | | * or it targeted an unrecognized attribute within the object. |
| | | * Either way, the filter will never match. |
| | | */ |
| | | h.handleResult(alwaysFalse()); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) { |
| | | // Use an accumulator which will aggregate the results from the subordinate mappers into |
| | | // a single list. On completion, the accumulator combines the results into a single JSON |
| | | // map object. |
| | | void toJSON(final Context c, final JsonPointer path, final Entry e, |
| | | final ResultHandler<JsonValue> h) { |
| | | /* |
| | | * Use an accumulator which will aggregate the results from the |
| | | * subordinate mappers into a single list. On completion, the |
| | | * accumulator combines the results into a single JSON map object. |
| | | */ |
| | | final ResultHandler<Map.Entry<String, JsonValue>> handler = |
| | | accumulate(mappings.size(), transform( |
| | | new Function<List<Map.Entry<String, JsonValue>>, JsonValue, Void>() { |
| | |
| | | public JsonValue apply(final List<Map.Entry<String, JsonValue>> value, |
| | | final Void p) { |
| | | if (value.isEmpty()) { |
| | | // No subordinate attributes, so omit the entire JSON object |
| | | // from the resource. |
| | | /* |
| | | * No subordinate attributes, so omit the |
| | | * entire JSON object from the resource. |
| | | */ |
| | | return null; |
| | | } else { |
| | | // Combine the sub-attributes into a single JSON object. |
| | | /* |
| | | * Combine the sub-attributes into a single |
| | | * JSON object. |
| | | */ |
| | | final Map<String, Object> result = |
| | | new LinkedHashMap<String, Object>(value.size()); |
| | | for (final Map.Entry<String, JsonValue> e : value) { |
| | |
| | | }, h)); |
| | | |
| | | for (final Mapping mapping : mappings.values()) { |
| | | mapping.mapper.toJSON(c, e, transform( |
| | | mapping.mapper.toJSON(c, path.child(mapping.name), e, transform( |
| | | new Function<JsonValue, Map.Entry<String, JsonValue>, Void>() { |
| | | @Override |
| | | public Map.Entry<String, JsonValue> apply(final JsonValue value, |
| | |
| | | } |
| | | |
| | | @Override |
| | | void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) { |
| | | // Fail immediately if the JSON value has the wrong type or contains unknown attributes. |
| | | void toLDAP(final Context c, final JsonPointer path, final Entry e, final JsonValue v, |
| | | final ResultHandler<List<Modification>> h) { |
| | | /* |
| | | * Fail immediately if the JSON value has the wrong type or contains |
| | | * unknown attributes. |
| | | */ |
| | | final Map<String, Mapping> missingMappings = new LinkedHashMap<String, Mapping>(mappings); |
| | | if (v != null && !v.isNull()) { |
| | | if (v.isMap()) { |
| | | for (final String attribute : v.asMap().keySet()) { |
| | | if (missingMappings.remove(toLowerCase(attribute)) == null) { |
| | | h.handleError(new BadRequestException("unrecognized attribute '" |
| | | + attribute + "'")); |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because the JSON resource " |
| | | + "contains an unrecognized field '%s'", path |
| | | .child(attribute)))); |
| | | return; |
| | | } |
| | | } |
| | | } else { |
| | | h.handleError(new BadRequestException("JSON object expected")); |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because the JSON resource " |
| | | + "contains the field '%s' whose value is the wrong type: " |
| | | + "an object is expected", path))); |
| | | return; |
| | | } |
| | | } |
| | | |
| | | // Accumulate the results of the subordinate mappings. |
| | | final ResultHandler<List<Attribute>> handler = |
| | | final ResultHandler<List<Modification>> handler = |
| | | accumulate(mappings.size(), transform( |
| | | new Function<List<List<Attribute>>, List<Attribute>, Void>() { |
| | | new Function<List<List<Modification>>, List<Modification>, Void>() { |
| | | @Override |
| | | public List<Attribute> apply(final List<List<Attribute>> value, |
| | | public List<Modification> apply(final List<List<Modification>> value, |
| | | final Void p) { |
| | | switch (value.size()) { |
| | | case 0: |
| | |
| | | case 1: |
| | | return value.get(0); |
| | | default: |
| | | final List<Attribute> attributes = |
| | | new ArrayList<Attribute>(value.size()); |
| | | for (final List<Attribute> a : value) { |
| | | final List<Modification> attributes = |
| | | new ArrayList<Modification>(value.size()); |
| | | for (final List<Modification> a : value) { |
| | | attributes.addAll(a); |
| | | } |
| | | return attributes; |
| | |
| | | |
| | | // Invoke mappings for which there are values provided. |
| | | if (v != null && !v.isNull()) { |
| | | for (final Map.Entry<String, Object> e : v.asMap().entrySet()) { |
| | | final Mapping mapping = getMapping(e.getKey()); |
| | | final JsonValue subValue = new JsonValue(e.getValue()); |
| | | mapping.mapper.toLDAP(c, subValue, handler); |
| | | for (final Map.Entry<String, Object> me : v.asMap().entrySet()) { |
| | | final Mapping mapping = getMapping(me.getKey()); |
| | | final JsonValue subValue = new JsonValue(me.getValue()); |
| | | mapping.mapper.toLDAP(c, path.child(me.getKey()), e, subValue, handler); |
| | | } |
| | | } |
| | | |
| | | // Invoke mappings for which there were no values provided. |
| | | for (final Mapping mapping : missingMappings.values()) { |
| | | mapping.mapper.toLDAP(c, null, handler); |
| | | mapping.mapper.toLDAP(c, path.child(mapping.name), e, null, handler); |
| | | } |
| | | } |
| | | |
| | |
| | | */ |
| | | 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); |
| | | } |
| | | }); |
| | | } |
| | |
| | | import org.forgerock.opendj.ldap.ConnectionException; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.Connections; |
| | | import org.forgerock.opendj.ldap.ConstraintViolationException; |
| | | import org.forgerock.opendj.ldap.DN; |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.EntryNotFoundException; |
| | |
| | | private AttributeMapper rootMapper; |
| | | private Schema schema = Schema.getDefaultSchema(); |
| | | private boolean useSubtreeDelete; |
| | | private boolean usePermissiveModify; |
| | | |
| | | Builder() { |
| | | useEtagAttribute(); |
| | |
| | | } |
| | | return new LDAPCollectionResourceProvider(baseDN, rootMapper, nameStrategy, |
| | | etagAttribute, new Config(factory, readOnUpdatePolicy, authzPolicy, |
| | | proxiedAuthzTemplate, useSubtreeDelete, schema), |
| | | proxiedAuthzTemplate, useSubtreeDelete, usePermissiveModify, schema), |
| | | additionalLDAPAttributes); |
| | | } |
| | | |
| | |
| | | useEtagAttribute(etagAttribute.asString()); |
| | | } |
| | | |
| | | if (configuration.get("useSubtreeDelete").required().asBoolean()) { |
| | | if (configuration.get("useSubtreeDelete").defaultTo(false).asBoolean()) { |
| | | useSubtreeDelete(); |
| | | } |
| | | |
| | | if (configuration.get("usePermissiveModify").defaultTo(false).asBoolean()) { |
| | | usePermissiveModify(); |
| | | } |
| | | |
| | | mapper(configureObjectMapper(configuration.get("attributes").required())); |
| | | |
| | | return this; |
| | |
| | | return this; |
| | | } |
| | | |
| | | public Builder usePermissiveModify() { |
| | | this.usePermissiveModify = true; |
| | | return this; |
| | | } |
| | | |
| | | private AttributeDescription ad(final String attribute) { |
| | | return AttributeDescription.valueOf(attribute, schema); |
| | | } |
| | |
| | | if (config.isDefined("defaultJSONValue")) { |
| | | s.defaultJSONValue(config.get("defaultJSONValue").getObject()); |
| | | } |
| | | if (config.isDefined("defaultLDAPValue")) { |
| | | s.defaultLDAPValue(config.get("defaultLDAPValue").getObject()); |
| | | } |
| | | if (config.get("isBinary").defaultTo(false).asBoolean()) { |
| | | s.isBinary(); |
| | | } |
| | |
| | | return e; |
| | | } catch (final AssertionFailureException e) { |
| | | resourceResultCode = ResourceException.VERSION_MISMATCH; |
| | | } catch (final ConstraintViolationException e) { |
| | | final ResultCode rc = e.getResult().getResultCode(); |
| | | if (rc.equals(ResultCode.ENTRY_ALREADY_EXISTS)) { |
| | | resourceResultCode = ResourceException.VERSION_MISMATCH; // Consistent with MVCC. |
| | | } else { |
| | | // Schema violation, etc. |
| | | resourceResultCode = ResourceException.BAD_REQUEST; |
| | | } |
| | | } catch (final AuthenticationException e) { |
| | | resourceResultCode = 401; |
| | | } catch (final AuthorizationException e) { |
| | |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import static java.util.Collections.emptyList; |
| | | import static java.util.Collections.emptySet; |
| | | import static java.util.Collections.singleton; |
| | | import static java.util.Collections.singletonList; |
| | | import static org.forgerock.opendj.ldap.Filter.alwaysFalse; |
| | | import static org.forgerock.opendj.ldap.Functions.fixedFunction; |
| | |
| | | import static org.forgerock.opendj.rest2ldap.Utils.base64ToByteString; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.byteStringToBase64; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.byteStringToJson; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.i18n; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.jsonToAttribute; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.jsonToByteString; |
| | | import static org.forgerock.opendj.rest2ldap.Utils.toFilter; |
| | | import static org.forgerock.opendj.rest2ldap.WritabilityPolicy.READ_WRITE; |
| | | |
| | | import java.util.Collection; |
| | | import java.util.Collections; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.ResourceException; |
| | | import org.forgerock.json.resource.ResultHandler; |
| | | import org.forgerock.opendj.ldap.Attribute; |
| | | import org.forgerock.opendj.ldap.AttributeDescription; |
| | |
| | | import org.forgerock.opendj.ldap.Entry; |
| | | import org.forgerock.opendj.ldap.Filter; |
| | | import org.forgerock.opendj.ldap.Function; |
| | | import org.forgerock.opendj.ldap.LinkedAttribute; |
| | | |
| | | /** |
| | | * An attribute mapper which provides a simple mapping from a JSON value to a |
| | | * single LDAP attribute. |
| | | */ |
| | | public final class SimpleAttributeMapper extends AttributeMapper { |
| | | public final class SimpleAttributeMapper extends AbstractLDAPAttributeMapper<SimpleAttributeMapper> { |
| | | private Function<ByteString, ?, Void> decoder = null; |
| | | private Object defaultJSONValue = null; |
| | | private Collection<Object> defaultJSONValues = Collections.emptySet(); |
| | | private ByteString defaultLDAPValue = null; |
| | | |
| | | private Function<Object, ByteString, Void> encoder = null; |
| | | private boolean isRequired = false; |
| | | private boolean isSingleValued = false; |
| | | private final AttributeDescription ldapAttributeName; |
| | | private WritabilityPolicy writabilityPolicy = READ_WRITE; |
| | | |
| | | SimpleAttributeMapper(final AttributeDescription ldapAttributeName) { |
| | | this.ldapAttributeName = ldapAttributeName; |
| | | super(ldapAttributeName); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | public SimpleAttributeMapper defaultJSONValue(final Object defaultValue) { |
| | | this.defaultJSONValue = defaultValue; |
| | | this.defaultJSONValues = defaultValue != null ? singleton(defaultValue) : emptySet(); |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Sets the default LDAP value which should be substituted when the JSON |
| | | * attribute is not found in the JSON value. |
| | | * |
| | | * @param defaultValue |
| | | * The default LDAP value. |
| | | * @return This attribute mapper. |
| | | */ |
| | | public SimpleAttributeMapper defaultLDAPValue(final Object defaultValue) { |
| | | this.defaultLDAPValue = defaultValue != null ? ByteString.valueOf(defaultValue) : null; |
| | | this.defaultJSONValues = defaultValue != null ? singletonList(defaultValue) : emptyList(); |
| | | return this; |
| | | } |
| | | |
| | |
| | | return this; |
| | | } |
| | | |
| | | /** |
| | | * Indicates that the LDAP attribute is mandatory and must be provided |
| | | * during create requests. |
| | | * |
| | | * @return This attribute mapper. |
| | | */ |
| | | public SimpleAttributeMapper 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 SimpleAttributeMapper isSingleValued() { |
| | | this.isSingleValued = true; |
| | | 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 SimpleAttributeMapper 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) { |
| | | if (jsonAttribute.isEmpty()) { |
| | | void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath, |
| | | final FilterType type, final String operator, final Object valueAssertion, |
| | | final ResultHandler<Filter> h) { |
| | | if (subPath.isEmpty()) { |
| | | try { |
| | | final ByteString va = |
| | | valueAssertion != null ? encoder().apply(valueAssertion, null) : null; |
| | | h.handleResult(toFilter(c, type, ldapAttributeName.toString(), va)); |
| | | } catch (Exception e) { |
| | | } catch (final Exception e) { |
| | | // Invalid assertion value - bad request. |
| | | |
| | | // FIXME: improve error message. |
| | | h.handleError(new BadRequestException("Invalid filter assertion value '" |
| | | + String.valueOf(valueAssertion) + "'", e)); |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because it contained an " |
| | | + "illegal filter assertion value '%s' for field '%s'", String |
| | | .valueOf(valueAssertion), path), e)); |
| | | } |
| | | } else { |
| | | // This attribute mapper does not support partial filtering. |
| | |
| | | } |
| | | |
| | | @Override |
| | | void toJSON(final Context c, final Entry e, final ResultHandler<JsonValue> h) { |
| | | void getNewLDAPAttributes(final Context c, final JsonPointer path, |
| | | final List<Object> newValues, final ResultHandler<Attribute> h) { |
| | | try { |
| | | h.handleResult(jsonToAttribute(newValues, ldapAttributeName, encoder())); |
| | | } catch (final Exception ex) { |
| | | h.handleError(new BadRequestException(i18n( |
| | | "The request cannot be processed because an error occurred while " |
| | | + "encoding the values for the field '%s': %s", path, ex.getMessage()))); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | SimpleAttributeMapper getThis() { |
| | | return this; |
| | | } |
| | | |
| | | @Override |
| | | void toJSON(final Context c, final JsonPointer path, final Entry e, |
| | | final ResultHandler<JsonValue> h) { |
| | | try { |
| | | final Object value; |
| | | if (attributeIsSingleValued()) { |
| | |
| | | value = s.isEmpty() ? null : s; |
| | | } |
| | | h.handleResult(value != null ? new JsonValue(value) : null); |
| | | } catch (Exception ex) { |
| | | } catch (final Exception ex) { |
| | | // The LDAP attribute could not be decoded. |
| | | h.handleError(asResourceException(ex)); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) { |
| | | try { |
| | | final List<Attribute> result; |
| | | if (v == null || v.isNull()) { |
| | | if (attributeIsRequired()) { |
| | | // FIXME: improve error message. |
| | | throw new BadRequestException("no value provided"); |
| | | } else if (defaultLDAPValue != null) { |
| | | result = |
| | | singletonList((Attribute) new LinkedAttribute(ldapAttributeName, |
| | | defaultLDAPValue)); |
| | | } else { |
| | | result = 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()) { |
| | | result = emptyList(); |
| | | } else { |
| | | // FIXME: improve error message. |
| | | throw new BadRequestException("attempted to create a read-only value"); |
| | | } |
| | | } else { |
| | | final Object value = v.getObject(); |
| | | if (value != null) { |
| | | result = singletonList(jsonToAttribute(value, ldapAttributeName, encoder())); |
| | | } else if (defaultLDAPValue != null) { |
| | | result = |
| | | singletonList((Attribute) new LinkedAttribute(ldapAttributeName, |
| | | defaultLDAPValue)); |
| | | } else { |
| | | result = emptyList(); |
| | | } |
| | | } |
| | | h.handleResult(result); |
| | | } 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 && defaultJSONValue == null; |
| | | } |
| | | |
| | | private boolean attributeIsSingleValued() { |
| | | return isSingleValued || ldapAttributeName.getAttributeType().isSingleValue(); |
| | | } |
| | | |
| | | private Function<ByteString, ? extends Object, Void> decoder() { |
| | | return decoder == null ? fixedFunction(byteStringToJson(), ldapAttributeName) : decoder; |
| | | } |
| | |
| | | import static org.fest.assertions.Fail.fail; |
| | | import static org.forgerock.json.resource.Requests.newDeleteRequest; |
| | | import static org.forgerock.json.resource.Requests.newReadRequest; |
| | | import static org.forgerock.json.resource.Requests.newUpdateRequest; |
| | | import static org.forgerock.json.resource.Resources.newCollection; |
| | | import static org.forgerock.json.resource.Resources.newInternalConnection; |
| | | import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.object; |
| | | import static org.forgerock.opendj.rest2ldap.Rest2LDAP.simple; |
| | | import static org.forgerock.opendj.rest2ldap.TestUtils.asResource; |
| | | import static org.forgerock.opendj.rest2ldap.TestUtils.content; |
| | | import static org.forgerock.opendj.rest2ldap.TestUtils.ctx; |
| | | import static org.forgerock.opendj.rest2ldap.TestUtils.field; |
| | | import static org.forgerock.opendj.rest2ldap.TestUtils.object; |
| | | |
| | | import java.io.IOException; |
| | | |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.BadRequestException; |
| | | import org.forgerock.json.resource.Connection; |
| | | import org.forgerock.json.resource.NotFoundException; |
| | | import org.forgerock.json.resource.PreconditionFailedException; |
| | | import org.forgerock.json.resource.RequestHandler; |
| | | import org.forgerock.json.resource.Resource; |
| | | import org.forgerock.json.resource.RootContext; |
| | | import org.forgerock.opendj.ldap.ConnectionFactory; |
| | | import org.forgerock.opendj.ldap.MemoryBackend; |
| | | import org.forgerock.opendj.ldif.LDIFEntryReader; |
| | |
| | | public void testDelete() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Connection connection = newInternalConnection(handler); |
| | | final Resource resource = connection.delete(c(), newDeleteRequest("/test1")); |
| | | checkTestUser1(resource); |
| | | final Resource resource = connection.delete(ctx(), newDeleteRequest("/test1")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | try { |
| | | connection.read(c(), newReadRequest("/test1")); |
| | | connection.read(ctx(), newReadRequest("/test1")); |
| | | fail("Read succeeded unexpectedly"); |
| | | } catch (final NotFoundException e) { |
| | | // Expected. |
| | |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Connection connection = newInternalConnection(handler); |
| | | final Resource resource = |
| | | connection.delete(c(), newDeleteRequest("/test1").setRevision("12345")); |
| | | checkTestUser1(resource); |
| | | connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | try { |
| | | connection.read(c(), newReadRequest("/test1")); |
| | | connection.read(ctx(), newReadRequest("/test1")); |
| | | fail("Read succeeded unexpectedly"); |
| | | } catch (final NotFoundException e) { |
| | | // Expected. |
| | |
| | | public void testDeleteMVCCNoMatch() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Connection connection = newInternalConnection(handler); |
| | | connection.delete(c(), newDeleteRequest("/test1").setRevision("12346")); |
| | | connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12346")); |
| | | } |
| | | |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testDeleteNotFound() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Connection connection = newInternalConnection(handler); |
| | | connection.delete(c(), newDeleteRequest("/missing")); |
| | | connection.delete(ctx(), newDeleteRequest("/missing")); |
| | | } |
| | | |
| | | @Test |
| | | public void testRead() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Resource resource = |
| | | newInternalConnection(handler).read(c(), newReadRequest("/test1")); |
| | | checkTestUser1(resource); |
| | | newInternalConnection(handler).read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | } |
| | | |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testReadNotFound() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | newInternalConnection(handler).read(c(), newReadRequest("/missing")); |
| | | newInternalConnection(handler).read(ctx(), newReadRequest("/missing")); |
| | | } |
| | | |
| | | @Test |
| | | public void testReadSelectAllFields() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Resource resource = |
| | | newInternalConnection(handler).read(c(), newReadRequest("/test1").addField("/")); |
| | | checkTestUser1(resource); |
| | | newInternalConnection(handler).read(ctx(), newReadRequest("/test1").addField("/")); |
| | | checkResourcesAreEqual(resource, getTestUser1(12345)); |
| | | } |
| | | |
| | | @Test |
| | | public void testReadSelectPartial() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Resource resource = |
| | | newInternalConnection(handler).read(c(), |
| | | newInternalConnection(handler).read(ctx(), |
| | | newReadRequest("/test1").addField("surname")); |
| | | assertThat(resource.getId()).isEqualTo("test1"); |
| | | assertThat(resource.getRevision()).isEqualTo("12345"); |
| | | assertThat(resource.getContent().get("id").asString()).isNull(); |
| | | assertThat(resource.getContent().get("_id").asString()).isNull(); |
| | | assertThat(resource.getContent().get("displayName").asString()).isNull(); |
| | | assertThat(resource.getContent().get("surname").asString()).isEqualTo("user 1"); |
| | | assertThat(resource.getContent().get("rev").asString()).isNull(); |
| | | assertThat(resource.getContent().get("_rev").asString()).isNull(); |
| | | } |
| | | |
| | | // Disabled - see CREST-86 (Should JSON resource fields be case insensitive?) |
| | |
| | | public void testReadSelectPartialInsensitive() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Resource resource = |
| | | newInternalConnection(handler).read(c(), |
| | | newInternalConnection(handler).read(ctx(), |
| | | newReadRequest("/test1").addField("SURNAME")); |
| | | assertThat(resource.getId()).isEqualTo("test1"); |
| | | assertThat(resource.getRevision()).isEqualTo("12345"); |
| | | assertThat(resource.getContent().get("id").asString()).isNull(); |
| | | assertThat(resource.getContent().get("_id").asString()).isNull(); |
| | | assertThat(resource.getContent().get("displayName").asString()).isNull(); |
| | | assertThat(resource.getContent().get("surname").asString()).isEqualTo("user 1"); |
| | | assertThat(resource.getContent().get("rev").asString()).isNull(); |
| | | assertThat(resource.getContent().get("_rev").asString()).isNull(); |
| | | } |
| | | |
| | | @Test |
| | | public void testUpdate() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Connection connection = newInternalConnection(handler); |
| | | final Resource resource1 = |
| | | connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))); |
| | | checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); |
| | | } |
| | | |
| | | @Test |
| | | public void testUpdateMVCCMatch() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Connection connection = newInternalConnection(handler); |
| | | final Resource resource1 = |
| | | connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)) |
| | | .setRevision("12345")); |
| | | checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); |
| | | final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); |
| | | checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); |
| | | } |
| | | |
| | | @Test(expectedExceptions = PreconditionFailedException.class) |
| | | public void testUpdateMVCCNoMatch() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Connection connection = newInternalConnection(handler); |
| | | connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)) |
| | | .setRevision("12346")); |
| | | } |
| | | |
| | | @Test(expectedExceptions = NotFoundException.class) |
| | | public void testUpdateNotFound() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Connection connection = newInternalConnection(handler); |
| | | connection.update(ctx(), newUpdateRequest("/missing", getTestUser1Updated(12345))); |
| | | } |
| | | |
| | | @Test(expectedExceptions = BadRequestException.class) |
| | | public void testUpdateReadOnlyAttribute() throws Exception { |
| | | final RequestHandler handler = newCollection(builder().build()); |
| | | final Connection connection = newInternalConnection(handler); |
| | | // Etag is read-only. |
| | | connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(99999))); |
| | | } |
| | | |
| | | private Builder builder() throws IOException { |
| | |
| | | .useEtagAttribute().useClientDNNaming("uid").readOnUpdatePolicy( |
| | | ReadOnUpdatePolicy.CONTROLS).authorizationPolicy(AuthorizationPolicy.NONE) |
| | | .additionalLDAPAttribute("objectClass", "top", "person").mapper( |
| | | object().attribute("id", simple("uid").isSingleValued().isRequired()) |
| | | .attribute("displayName", |
| | | simple("cn").isSingleValued().isRequired()).attribute( |
| | | "surname", simple("sn").isSingleValued().isRequired()) |
| | | .attribute("rev", simple("etag").isSingleValued().isRequired())); |
| | | object().attribute( |
| | | "_id", |
| | | simple("uid").isSingleValued().isRequired().writability( |
| | | WritabilityPolicy.CREATE_ONLY)).attribute("displayName", |
| | | simple("cn").isSingleValued().isRequired()).attribute("surname", |
| | | simple("sn").isSingleValued().isRequired()).attribute( |
| | | "_rev", |
| | | simple("etag").isSingleValued().isRequired().writability( |
| | | WritabilityPolicy.READ_ONLY))); |
| | | } |
| | | |
| | | private RootContext c() { |
| | | return new RootContext(); |
| | | } |
| | | |
| | | private void checkTestUser1(final Resource resource) { |
| | | assertThat(resource.getId()).isEqualTo("test1"); |
| | | assertThat(resource.getRevision()).isEqualTo("12345"); |
| | | assertThat(resource.getContent().get("id").asString()).isEqualTo("test1"); |
| | | assertThat(resource.getContent().get("displayName").asString()).isEqualTo("test user 1"); |
| | | assertThat(resource.getContent().get("surname").asString()).isEqualTo("user 1"); |
| | | assertThat(resource.getContent().get("rev").asString()).isEqualTo("12345"); |
| | | private void checkResourcesAreEqual(final Resource actual, final JsonValue expected) { |
| | | final Resource expectedResource = asResource(expected); |
| | | assertThat(actual.getId()).isEqualTo(expectedResource.getId()); |
| | | assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision()); |
| | | assertThat(actual.getContent().getObject()).isEqualTo( |
| | | expectedResource.getContent().getObject()); |
| | | } |
| | | |
| | | private ConnectionFactory getConnectionFactory() throws IOException { |
| | |
| | | |
| | | return newInternalConnectionFactory(backend); |
| | | } |
| | | |
| | | private JsonValue getTestUser1(final int rev) { |
| | | return content(object(field("_id", "test1"), field("_rev", String.valueOf(rev)), field( |
| | | "displayName", "test user 1"), field("surname", "user 1"))); |
| | | } |
| | | |
| | | private JsonValue getTestUser1Updated(final int rev) { |
| | | return content(object(field("_id", "test1"), field("_rev", String.valueOf(rev)), field( |
| | | "displayName", "changed"), field("surname", "user 1"))); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * The contents of this file are subject to the terms of the Common Development and |
| | | * Distribution License (the License). You may not use this file except in compliance with the |
| | | * License. |
| | | * |
| | | * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the |
| | | * specific language governing permission and limitations under the License. |
| | | * |
| | | * When distributing Covered Software, include this CDDL Header Notice in each file and include |
| | | * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2013 ForgeRock Inc. |
| | | */ |
| | | package org.forgerock.opendj.rest2ldap; |
| | | |
| | | import java.util.AbstractMap; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | import org.forgerock.json.fluent.JsonPointer; |
| | | import org.forgerock.json.fluent.JsonValue; |
| | | import org.forgerock.json.resource.Resource; |
| | | import org.forgerock.json.resource.RootContext; |
| | | |
| | | /** |
| | | * Unit test utility methods, including fluent methods for creating JSON |
| | | * objects. |
| | | */ |
| | | public final class TestUtils { |
| | | |
| | | /** |
| | | * Creates a JSON array object. |
| | | * |
| | | * @param objects |
| | | * The array elements. |
| | | * @return A JSON array. |
| | | */ |
| | | public static Object array(final Object... objects) { |
| | | return Arrays.asList(objects); |
| | | } |
| | | |
| | | /** |
| | | * Returns a {@code Resource} containing the provided JSON content. The ID |
| | | * and revision will be taken from the "_id" and "_rev" fields respectively. |
| | | * |
| | | * @param content |
| | | * The JSON content. |
| | | * @return A {@code Resource} containing the provided JSON content. |
| | | */ |
| | | public static Resource asResource(final JsonValue content) { |
| | | return new Resource(content.get("_id").asString(), content.get("_rev").asString(), content); |
| | | } |
| | | |
| | | /** |
| | | * Creates a JSON value for the provided object. |
| | | * |
| | | * @param object |
| | | * The object. |
| | | * @return The JSON value. |
| | | */ |
| | | public static JsonValue content(final Object object) { |
| | | return new JsonValue(object); |
| | | } |
| | | |
| | | /** |
| | | * Creates a root context to be passed in with client requests. |
| | | * |
| | | * @return The root context. |
| | | */ |
| | | public static RootContext ctx() { |
| | | return new RootContext(); |
| | | } |
| | | |
| | | /** |
| | | * Creates a JSON value for the provided object. This is the same as |
| | | * {@link #content(Object)} but can yield more readable test data in data |
| | | * providers. |
| | | * |
| | | * @param object |
| | | * The object. |
| | | * @return The JSON value. |
| | | */ |
| | | public static JsonValue expected(final Object object) { |
| | | return content(object); |
| | | } |
| | | |
| | | /** |
| | | * Creates a JSON field for inclusion in a JSON object using |
| | | * {@link #object(java.util.Map.Entry...)}. |
| | | * |
| | | * @param key |
| | | * The JSON field name. |
| | | * @param value |
| | | * The JSON field value. |
| | | * @return The JSON field for inclusion in a JSON object. |
| | | */ |
| | | public static Map.Entry<String, Object> field(final String key, final Object value) { |
| | | return new AbstractMap.SimpleImmutableEntry<String, Object>(key, value); |
| | | } |
| | | |
| | | /** |
| | | * Creates a list of JSON pointers from the provided string representations. |
| | | * |
| | | * @param fields |
| | | * The list of JSON pointer strings. |
| | | * @return The list of parsed JSON pointers. |
| | | */ |
| | | public static List<JsonPointer> filter(final String... fields) { |
| | | final List<JsonPointer> result = new ArrayList<JsonPointer>(fields.length); |
| | | for (final String field : fields) { |
| | | result.add(new JsonPointer(field)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * Creates a JSON object comprised of the provided JSON |
| | | * {@link #field(String, Object) fields}. |
| | | * |
| | | * @param fields |
| | | * The list of {@link #field(String, Object) fields} to include |
| | | * in the JSON object. |
| | | * @return The JSON object. |
| | | */ |
| | | @SuppressWarnings({ "unchecked", "rawtypes" }) |
| | | public static Object object(final Map.Entry... fields) { |
| | | final Map<String, Object> object = new LinkedHashMap<String, Object>(fields.length); |
| | | for (final Map.Entry<String, Object> field : fields) { |
| | | object.put(field.getKey(), field.getValue()); |
| | | } |
| | | return object; |
| | | } |
| | | |
| | | private TestUtils() { |
| | | // Prevent instantiation. |
| | | } |
| | | |
| | | } |