opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ComplexAttributeMapper.java
@@ -99,7 +99,11 @@ @Override void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) { // TODO Auto-generated method stub if (v.isDefined(jsonAttributeName)) { mapper.toLDAP(c, v.get(jsonAttributeName), h); } else { mapper.toLDAP(c, new JsonValue(Collections.emptyMap()), h); } } private boolean matches(final JsonPointer jsonAttribute) { opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/CompositeAttributeMapper.java
@@ -121,7 +121,31 @@ @Override void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) { // TODO Auto-generated method stub final ResultHandler<List<Attribute>> handler = accumulate(attributeMappers.size(), transform( new Function<List<List<Attribute>>, List<Attribute>, Void>() { @Override public List<Attribute> apply(final List<List<Attribute>> value, final Void p) { switch (value.size()) { case 0: return Collections.emptyList(); case 1: return value.get(0) != null ? value.get(0) : Collections .<Attribute> emptyList(); default: List<Attribute> attributes = new ArrayList<Attribute>(value.size()); for (List<Attribute> a : value) { attributes.addAll(a); } return attributes; } } }, h)); for (final AttributeMapper mapper : attributeMappers) { mapper.toLDAP(c, v, handler); } } @SuppressWarnings("unchecked") opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Config.java
@@ -15,165 +15,50 @@ */ package org.forgerock.opendj.rest2ldap; import static org.forgerock.opendj.rest2ldap.Config.ReadOnUpdatePolicy.USE_READ_ENTRY_CONTROLS; import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull; import org.forgerock.opendj.ldap.DecodeOptions; import org.forgerock.opendj.ldap.Filter; import org.forgerock.opendj.ldap.schema.Schema; /** * Common configuration options. */ public final class Config { /** * An interface for incrementally constructing common configuration options. */ public static final class Builder { private Filter falseFilter; private ReadOnUpdatePolicy readOnUpdatePolicy; private Filter trueFilter; private Builder() { // Nothing to do. } /** * Returns a new configuration based on the current state of this * builder. * * @return A new configuration based on the current state of this * builder. */ public Config build() { return new Config(trueFilter, falseFilter, readOnUpdatePolicy); } /** * Sets the absolute false filter which should be used when querying the * LDAP server. * * @param filter * The absolute false filter. * @return A reference to this builder. */ public Builder falseFilter(final Filter filter) { this.trueFilter = ensureNotNull(filter); return this; } /** * Sets the policy which should be used in order to read an entry before * it is deleted, or after it is added or modified. * * @param policy * The policy which should be used in order to read an entry * before it is deleted, or after it is added or modified. * @return A reference to this builder. */ public Builder readOnUpdatePolicy(final ReadOnUpdatePolicy policy) { this.readOnUpdatePolicy = ensureNotNull(policy); return this; } /** * Sets the absolute true filter which should be used when querying the * LDAP server. * * @param filter * The absolute true filter. * @return A reference to this builder. */ public Builder trueFilter(final Filter filter) { this.trueFilter = ensureNotNull(filter); return this; } }; /** * The policy which should be used in order to read an entry before it is * deleted, or after it is added or modified. */ public static enum ReadOnUpdatePolicy { /** * The LDAP entry will not be read when an update is performed. More * specifically, the REST resource will not be returned as part of a * create, delete, patch, or update request. */ DISABLED, /** * The LDAP entry will be read atomically using the RFC 4527 read-entry * controls. More specifically, the REST resource will be returned as * part of a create, delete, patch, or update request, and it will * reflect the state of the resource at the time the update was * performed. This policy requires that the LDAP server supports RFC * 4527. */ USE_READ_ENTRY_CONTROLS, /** * The LDAP entry will be read non-atomically using an LDAP search when * an update is performed. More specifically, the REST resource will be * returned as part of a create, delete, patch, or update request, but * it may not reflect the state of the resource at the time the update * was performed. */ USE_SEARCH; } private static final Config DEFAULT = new Builder().trueFilter(Filter.objectClassPresent()) .falseFilter(Filter.present("1.1")).readOnUpdatePolicy(USE_READ_ENTRY_CONTROLS).build(); /** * Returns a new builder which can be used for incrementally constructing * common configuration options. The builder will initially have * {@link #defaultConfig() default} settings. * * @return The new builder. */ public static Builder builder() { return builder(DEFAULT); } /** * Returns a new builder which can be used for incrementally constructing * common configuration options. The builder will initially have the same * settings as the provided configuration. * * @param config * The initial settings. * @return The new builder. */ public static Builder builder(final Config config) { return new Builder().trueFilter(config.trueFilter()).falseFilter(config.falseFilter()) .readOnUpdatePolicy(config.readOnUpdatePolicy()); } /** * Returns the default configuration having the following settings: * <ul> * <li>the absolute true filter {@code (objectClass=*)} * <li>the absolute false filter {@code (1.1=*)} * <li>the read on update policy * {@link ReadOnUpdatePolicy#USE_READ_ENTRY_CONTROLS}. * </ul> * * @return The default configuration. */ public static Config defaultConfig() { return DEFAULT; } final class Config { private final Filter falseFilter; private final Schema schema; private final ReadOnUpdatePolicy readOnUpdatePolicy; private final Filter trueFilter; private final DecodeOptions options; private Config(final Filter trueFilter, final Filter falseFilter, final ReadOnUpdatePolicy readOnUpdatePolicy) { Config(final Filter trueFilter, final Filter falseFilter, final ReadOnUpdatePolicy readOnUpdatePolicy, final Schema schema) { this.trueFilter = trueFilter; this.falseFilter = falseFilter; this.readOnUpdatePolicy = readOnUpdatePolicy; this.schema = schema; this.options = new DecodeOptions().setSchema(schema); } /** * Returns the schema which should be used when attribute types and * controls. * * @return The schema which should be used when attribute types and * controls. */ public Schema schema() { return schema; } /** * Returns the decoding options which should be used when decoding controls * in responses. * * @return The decoding options which should be used when decoding controls * in responses. */ public DecodeOptions decodeOptions() { return options; } /** opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/DefaultAttributeMapper.java
@@ -20,6 +20,9 @@ import static org.forgerock.opendj.rest2ldap.Utils.toFilter; import static org.forgerock.opendj.rest2ldap.Utils.toLowerCase; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -27,8 +30,11 @@ import org.forgerock.json.fluent.JsonPointer; 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.AttributeDescription; import org.forgerock.opendj.ldap.Attributes; import org.forgerock.opendj.ldap.Entry; import org.forgerock.opendj.ldap.Filter; @@ -126,7 +132,50 @@ @Override void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) { // TODO: if (v.isMap()) { List<Attribute> result = new ArrayList<Attribute>(v.size()); for (Map.Entry<String, Object> field : v.asMap().entrySet()) { final AttributeDescription ad; try { ad = AttributeDescription.valueOf(field.getKey(), c.getConfig().schema()); } catch (Exception e) { // FIXME: improve error message. h.handleError(new BadRequestException("The field " + field.getKey() + " is invalid")); return; } Object value = field.getValue(); if (isJSONPrimitive(value)) { result.add(Attributes.singletonAttribute(ad, value)); } else if (value instanceof Collection<?>) { Attribute a = c.getConfig().decodeOptions().getAttributeFactory().newAttribute(ad); for (Object o : (Collection<?>) value) { if (isJSONPrimitive(o)) { a.add(o); } else { // FIXME: improve error message. h.handleError(new BadRequestException("The field " + field.getKey() + " is invalid")); return; } } result.add(a); } else { // FIXME: improve error message. h.handleError(new BadRequestException("The field " + field.getKey() + " is invalid")); return; } } h.handleResult(result); } else { h.handleResult(Collections.<Attribute> emptyList()); } } private boolean isJSONPrimitive(Object value) { return value instanceof String || value instanceof Boolean || value instanceof Number; } private boolean isIncludedAttribute(final String name) { opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/JSONConstantAttributeMapper.java
File was renamed from opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ConstantAttributeMapper.java @@ -32,20 +32,11 @@ /** * An attribute mapper which maps a single JSON attribute to a fixed value. */ final class ConstantAttributeMapper extends AttributeMapper { final class JSONConstantAttributeMapper extends AttributeMapper { private final String jsonAttributeName; private final Object jsonAttributeValue; /** * Creates a new constant attribute mapper which maps a single JSON * attribute to a fixed value. * * @param attributeName * The name of the simple JSON attribute. * @param attributeValue * The value of the simple JSON attribute. */ ConstantAttributeMapper(final String attributeName, final Object attributeValue) { JSONConstantAttributeMapper(final String attributeName, final Object attributeValue) { this.jsonAttributeName = attributeName; this.jsonAttributeValue = attributeValue; } @@ -109,7 +100,7 @@ @Override void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) { // TODO Auto-generated method stub h.handleResult(Collections.<Attribute>emptyList()); } private <T extends Comparable<T>> Filter compare(final Context c, final FilterType type, opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPCollectionResourceProvider.java
@@ -15,10 +15,12 @@ */ package org.forgerock.opendj.rest2ldap; import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.USE_READ_ENTRY_CONTROLS; import static org.forgerock.opendj.rest2ldap.Utils.accumulate; import static org.forgerock.opendj.rest2ldap.Utils.transform; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; @@ -56,6 +58,8 @@ import org.forgerock.opendj.ldap.ConnectionException; import org.forgerock.opendj.ldap.ConnectionFactory; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.DecodeException; import org.forgerock.opendj.ldap.Entry; import org.forgerock.opendj.ldap.EntryNotFoundException; import org.forgerock.opendj.ldap.ErrorResultException; import org.forgerock.opendj.ldap.Filter; @@ -64,7 +68,12 @@ import org.forgerock.opendj.ldap.SearchResultHandler; import org.forgerock.opendj.ldap.SearchScope; import org.forgerock.opendj.ldap.TimeoutResultException; import org.forgerock.opendj.ldap.controls.PostReadRequestControl; import org.forgerock.opendj.ldap.controls.PostReadResponseControl; import org.forgerock.opendj.ldap.controls.PreReadResponseControl; import org.forgerock.opendj.ldap.requests.AddRequest; import org.forgerock.opendj.ldap.requests.ModifyRequest; import org.forgerock.opendj.ldap.requests.Request; import org.forgerock.opendj.ldap.requests.Requests; import org.forgerock.opendj.ldap.requests.SearchRequest; import org.forgerock.opendj.ldap.responses.Result; @@ -161,8 +170,8 @@ private final NameStrategy nameStrategy; LDAPCollectionResourceProvider(final DN baseDN, final AttributeMapper mapper, final ConnectionFactory factory, final Config config, final NameStrategy nameStrategy, final MVCCStrategy mvccStrategy) { final ConnectionFactory factory, final NameStrategy nameStrategy, final MVCCStrategy mvccStrategy, final Config config) { this.baseDN = baseDN; this.attributeMapper = mapper; this.factory = factory; @@ -186,25 +195,7 @@ @Override public void createInstance(final ServerContext context, final CreateRequest request, final ResultHandler<Resource> handler) { // We will support three use-cases: // // 1) client provided: the RDN is derived from the ID // 2) client provided: the RDN is derived from a JSON attribute, the ID maps to a user attribute // 3) server provided: the RDN is derived from a JSON attribute // // Procedure: // // 1) Generate LDAP attributes and create entry // 2) Apply ID mapper: create RDN from entry/ID, store ID in entry // 3) Create add request // 4) Add post read control if policy rfc // 5) Do add request // 6) If add failed then return error // 7) If policy is rfc then return entry // 8) If policy is search then read entry // final Context c = wrap(context); final AddRequest addRequest = Requests.newAddRequest(DN.rootDN()); attributeMapper.toLDAP(c, request.getContent(), new ResultHandler<List<Attribute>>() { @Override public void handleError(final ResourceException error) { @@ -213,10 +204,16 @@ @Override public void handleResult(final List<Attribute> result) { final AddRequest addRequest = Requests.newAddRequest(DN.rootDN()); for (final Attribute attribute : result) { addRequest.addAttribute(attribute); } nameStrategy.setResourceId(c, baseDN, request.getNewResourceId(), addRequest); nameStrategy.setResourceId(c, getBaseDN(c), request.getNewResourceId(), addRequest); if (config.readOnUpdatePolicy() == USE_READ_ENTRY_CONTROLS) { final String[] attributes = getLDAPAttributes(c, request.getFieldFilters()); addRequest.addControl(PostReadRequestControl.newControl(false, attributes)); } applyUpdate(c, addRequest, handler); } }); } @@ -374,24 +371,9 @@ @Override public void handleResult(final SearchResultEntry entry) { final String revision = mvccStrategy.getRevisionFromEntry(c, entry); final ResultHandler<Map<String, Object>> mapHandler = new ResultHandler<Map<String, Object>>() { @Override public void handleError(final ResourceException e) { handler.handleError(e); } @Override public void handleResult(final Map<String, Object> result) { final Resource resource = new Resource(resourceId, revision, new JsonValue( result)); handler.handleResult(resource); } }; attributeMapper.toJSON(c, entry, mapHandler); adaptEntry(c, entry, handler); } }; // The handler which will be invoked @@ -421,6 +403,27 @@ handler.handleError(new NotSupportedException("Not yet implemented")); } private void adaptEntry(final Context c, final Entry entry, final ResultHandler<Resource> handler) { final String actualResourceId = nameStrategy.getResourceId(c, entry); final String revision = mvccStrategy.getRevisionFromEntry(c, entry); final ResultHandler<Map<String, Object>> mapHandler = new ResultHandler<Map<String, Object>>() { @Override public void handleError(final ResourceException e) { handler.handleError(e); } @Override public void handleResult(final Map<String, Object> result) { final Resource resource = new Resource(actualResourceId, revision, new JsonValue(result)); handler.handleResult(resource); } }; attributeMapper.toJSON(c, entry, mapHandler); } /** * Adapts an LDAP result code to a resource exception. * @@ -676,4 +679,79 @@ private Context wrap(final ServerContext context) { return new Context(config, context); } private org.forgerock.opendj.ldap.ResultHandler<Result> postUpdateHandler(final Context c, final ResultHandler<Resource> handler) { // The handler which will be invoked for the LDAP add result. final org.forgerock.opendj.ldap.ResultHandler<Result> resultHandler = new org.forgerock.opendj.ldap.ResultHandler<Result>() { @Override public void handleErrorResult(final ErrorResultException error) { handler.handleError(adaptErrorResult(error)); } @Override public void handleResult(final Result result) { // FIXME: handle USE_SEARCH policy. Entry entry; try { PostReadResponseControl postReadControl = result.getControl(PostReadResponseControl.DECODER, config .decodeOptions()); if (postReadControl != null) { entry = postReadControl.getEntry(); } else { PreReadResponseControl preReadControl = result.getControl(PreReadResponseControl.DECODER, config .decodeOptions()); if (preReadControl != null) { entry = preReadControl.getEntry(); } else { entry = null; } } } catch (DecodeException e) { // FIXME: log something? entry = null; } if (entry != null) { adaptEntry(c, entry, handler); } else { final Resource resource = new Resource(null, null, new JsonValue(Collections.emptyMap())); handler.handleResult(resource); } } }; return resultHandler; } private void applyUpdate(final Context c, final Request request, final ResultHandler<Resource> handler) { final org.forgerock.opendj.ldap.ResultHandler<Result> resultHandler = postUpdateHandler(c, handler); final ConnectionCompletionHandler<Result> outerHandler = new ConnectionCompletionHandler<Result>(resultHandler) { @Override public void handleResult(final Connection connection) { final RequestCompletionHandler<Result> innerHandler = new RequestCompletionHandler<Result>(connection, resultHandler); // FIXME: simplify this once we have Connection#applyChange() if (request instanceof AddRequest) { connection.addAsync((AddRequest) request, null, innerHandler); } else if (request instanceof org.forgerock.opendj.ldap.requests.DeleteRequest) { connection.deleteAsync( (org.forgerock.opendj.ldap.requests.DeleteRequest) request, null, innerHandler); } else if (request instanceof ModifyRequest) { connection.modifyAsync((ModifyRequest) request, null, innerHandler); } else { throw new IllegalStateException(); } } }; factory.getConnectionAsync(outerHandler); } } opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/LDAPConstantAttributeMapper.java
New file @@ -0,0 +1,67 @@ /* * 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 2012-2013 ForgeRock AS. */ package org.forgerock.opendj.rest2ldap; import static org.forgerock.opendj.ldap.Attributes.singletonAttribute; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; 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.AttributeDescription; import org.forgerock.opendj.ldap.Entry; import org.forgerock.opendj.ldap.Filter; /** * An attribute mapper which maps a single LDAP attribute to a fixed value. */ final class LDAPConstantAttributeMapper extends AttributeMapper { private final List<Attribute> attributes; LDAPConstantAttributeMapper(final AttributeDescription attributeName, final Object attributeValue) { attributes = Collections.singletonList(singletonAttribute(attributeName, attributeValue)); } @Override void getLDAPAttributes(final Context c, final JsonPointer jsonAttribute, 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) { // This attribute mapper cannot handle the provided filter component. h.handleResult(null); } @Override void toJSON(final Context c, final Entry e, final ResultHandler<Map<String, Object>> h) { h.handleResult(Collections.<String, Object> emptyMap()); } @Override void toLDAP(final Context c, final JsonValue v, final ResultHandler<List<Attribute>> h) { h.handleResult(attributes); } } opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/ReadOnUpdatePolicy.java
New file @@ -0,0 +1,48 @@ /* * 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; /** * The policy which should be used in order to read an entry before it is * deleted, or after it is added or modified. */ public enum ReadOnUpdatePolicy { /** * The LDAP entry will not be read when an update is performed. More * specifically, the REST resource will not be returned as part of a create, * delete, patch, or update request. */ DISABLED, /** * The LDAP entry will be read atomically using the RFC 4527 read-entry * controls. More specifically, the REST resource will be returned as part * of a create, delete, patch, or update request, and it will reflect the * state of the resource at the time the update was performed. This policy * requires that the LDAP server supports RFC 4527. */ USE_READ_ENTRY_CONTROLS, /** * The LDAP entry will be read non-atomically using an LDAP search when an * update is performed. More specifically, the REST resource will be * returned as part of a create, delete, patch, or update request, but it * may not reflect the state of the resource at the time the update was * performed. */ USE_SEARCH; } opendj3/opendj-rest2ldap/src/main/java/org/forgerock/opendj/rest2ldap/Rest2LDAP.java
@@ -18,6 +18,7 @@ import static org.forgerock.opendj.ldap.requests.Requests.newSearchRequest; import static org.forgerock.opendj.ldap.schema.CoreSchema.getEntryUUIDAttributeType; import static org.forgerock.opendj.rest2ldap.ReadOnUpdatePolicy.USE_READ_ENTRY_CONTROLS; import static org.forgerock.opendj.rest2ldap.Utils.ensureNotNull; import java.util.Arrays; @@ -52,16 +53,73 @@ */ public static final class Builder { private DN baseDN; // TODO: support template variables. private Config config = Config.defaultConfig(); private ConnectionFactory factory; private final List<AttributeMapper> mappers = new LinkedList<AttributeMapper>(); private MVCCStrategy mvccStrategy = mvccUsingEtag(); private NameStrategy nameStrategy = nameByEntryUUID("uid"); private Filter falseFilter = Filter.present("1.1"); private Schema schema = Schema.getDefaultSchema(); private ReadOnUpdatePolicy readOnUpdatePolicy = USE_READ_ENTRY_CONTROLS; private Filter trueFilter = Filter.objectClassPresent(); Builder() { // No implementation required. } /** * Sets the schema which should be used when attribute types and * controls. * * @param schema * The schema which should be used when attribute types and * controls. * @return A reference to this builder. */ public Builder schema(final Schema schema) { this.schema = ensureNotNull(schema); return this; } /** * Sets the absolute false filter which should be used when querying the * LDAP server. * * @param filter * The absolute false filter. * @return A reference to this builder. */ public Builder falseFilter(final Filter filter) { this.trueFilter = ensureNotNull(filter); return this; } /** * Sets the policy which should be used in order to read an entry before * it is deleted, or after it is added or modified. * * @param policy * The policy which should be used in order to read an entry * before it is deleted, or after it is added or modified. * @return A reference to this builder. */ public Builder readOnUpdatePolicy(final ReadOnUpdatePolicy policy) { this.readOnUpdatePolicy = ensureNotNull(policy); return this; } /** * Sets the absolute true filter which should be used when querying the * LDAP server. * * @param filter * The absolute true filter. * @return A reference to this builder. */ public Builder trueFilter(final Filter filter) { this.trueFilter = ensureNotNull(filter); return this; } public Builder baseDN(final DN dn) { ensureNotNull(dn); this.baseDN = dn; @@ -80,14 +138,9 @@ if (mappers.isEmpty()) { throw new IllegalStateException("No mappings provided"); } return new LDAPCollectionResourceProvider(baseDN, mapOf(mappers), factory, config, nameStrategy, mvccStrategy); } public Builder config(final Config config) { ensureNotNull(config); this.config = config; return this; return new LDAPCollectionResourceProvider(baseDN, mapOf(mappers), factory, nameStrategy, mvccStrategy, new Config(trueFilter, falseFilter, readOnUpdatePolicy, schema)); } public Builder factory(final ConnectionFactory factory) { @@ -254,8 +307,19 @@ return new ComplexAttributeMapper(jsonAttribute, mapOf(mappers)); } public static AttributeMapper mapConstant(final String attribute, final Object attributeValue) { return new ConstantAttributeMapper(attribute, attributeValue); public static AttributeMapper mapJSONConstant(final String attribute, final Object attributeValue) { return new JSONConstantAttributeMapper(attribute, attributeValue); } public static AttributeMapper mapLDAPConstant(final String attribute, final Object attributeValue) { return mapLDAPConstant(AttributeDescription.valueOf(attribute), attributeValue); } public static AttributeMapper mapLDAPConstant(final AttributeDescription attribute, final Object attributeValue) { return new LDAPConstantAttributeMapper(attribute, attributeValue); } public static MVCCStrategy mvccUsingAttribute(final AttributeDescription attribute) {