/* * 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 2016 ForgeRock AS. */ package org.forgerock.opendj.rest2ldap; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.forgerock.json.JsonValue.field; import static org.forgerock.json.JsonValue.object; import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException; import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ENCODING_VALUES_FOR_FIELD; import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_PATCH_JSON_INTERNAL_PROPERTY; import static org.forgerock.opendj.rest2ldap.Utils.jsonToAttribute; import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException; import static org.forgerock.opendj.rest2ldap.Utils.newNotSupportedException; import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.byteStringToJson; import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.jsonToByteString; import static org.forgerock.util.promise.Promises.newResultPromise; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.json.resource.PatchOperation; import org.forgerock.json.resource.ResourceException; 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.Modification; import org.forgerock.services.context.Context; import org.forgerock.util.promise.Promise; import org.forgerock.util.query.QueryFilter; /** A property mapper which provides a mapping from a JSON value to an LDAP attribute having the JSON syntax. */ public final class JsonPropertyMapper extends AbstractLdapPropertyMapper { /** * The default JSON schema for this property. According to json-schema.org the {} schema allows anything. * However, the OpenAPI transformer seems to expect at least a "type" field to be present. */ private static final JsonValue ANY_SCHEMA = new JsonValue(object(field("type", "object"))); private JsonValue jsonSchema = ANY_SCHEMA; JsonPropertyMapper(final AttributeDescription ldapAttributeName) { super(ldapAttributeName); } /** * Sets the default JSON value which should be substituted when the LDAP attribute is not found in the LDAP entry. * * @param defaultValue * The default JSON value. * @return This property mapper. */ public JsonPropertyMapper defaultJsonValue(final Object defaultValue) { this.defaultJsonValues = defaultValue != null ? singletonList(defaultValue) : emptyList(); return this; } /** * Sets the default JSON values which should be substituted when the LDAP attribute is not found in the LDAP entry. * * @param defaultValues * The default JSON values. * @return This property mapper. */ public JsonPropertyMapper defaultJsonValues(final Collection defaultValues) { this.defaultJsonValues = defaultValues != null ? new ArrayList<>(defaultValues) : emptyList(); return this; } /** * Sets the JSON schema corresponding to this simple property mapper. If not {@code null}, * it will be returned by {@link #toJsonSchema()}, otherwise a default JSON schema will be * automatically generated with the information available in this property mapper. * * @param jsonSchema * the JSON schema corresponding to this simple property mapper. Can be {@code null} * @return This property mapper. */ public JsonPropertyMapper jsonSchema(JsonValue jsonSchema) { this.jsonSchema = jsonSchema != null ? jsonSchema : ANY_SCHEMA; return this; } @Override public String toString() { return "json(" + ldapAttributeName + ")"; } @Override Promise getLdapFilter(final Context context, final Resource resource, final JsonPointer path, final JsonPointer subPath, final FilterType type, final String operator, final Object valueAssertion) { final QueryFilter queryFilter = toQueryFilter(type, subPath, operator, valueAssertion); return newResultPromise(Filter.equality(ldapAttributeName.toString(), queryFilter)); } private QueryFilter toQueryFilter(final FilterType type, final JsonPointer subPath, final String operator, final Object valueAssertion) { switch (type) { case CONTAINS: return QueryFilter.contains(subPath, valueAssertion); case STARTS_WITH: return QueryFilter.startsWith(subPath, valueAssertion); case EQUAL_TO: return QueryFilter.equalTo(subPath, valueAssertion); case GREATER_THAN: return QueryFilter.greaterThan(subPath, valueAssertion); case GREATER_THAN_OR_EQUAL_TO: return QueryFilter.greaterThanOrEqualTo(subPath, valueAssertion); case LESS_THAN: return QueryFilter.lessThan(subPath, valueAssertion); case LESS_THAN_OR_EQUAL_TO: return QueryFilter.lessThanOrEqualTo(subPath, valueAssertion); case PRESENT: return QueryFilter.present(subPath); case EXTENDED: return QueryFilter.extendedMatch(subPath, operator, valueAssertion); default: return QueryFilter.alwaysFalse(); } } @Override Promise getNewLdapAttributes(final Context context, final Resource resource, final JsonPointer path, final List newValues) { try { return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, jsonToByteString())); } catch (final Exception e) { return newBadRequestException(ERR_ENCODING_VALUES_FOR_FIELD.get(path, e.getMessage())).asPromise(); } } @Override JsonPropertyMapper getThis() { return this; } /** Intercept attempts to patch internal fields and reject these as unsupported rather than unrecognized. */ @Override Promise, ResourceException> patch(final Context context, final Resource resource, final JsonPointer path, final PatchOperation operation) { final JsonPointer field = operation.getField(); if (field.isEmpty() || field.size() == 1 && field.get(0).equals("-")) { return super.patch(context, resource, path, operation); } return newNotSupportedException(ERR_PATCH_JSON_INTERNAL_PROPERTY.get(field, path, path)).asPromise(); } @SuppressWarnings("fallthrough") @Override Promise read(final Context context, final Resource resource, final JsonPointer path, final Entry e) { try { final Set s = e.parseAttribute(ldapAttributeName).asSetOf(byteStringToJson(), defaultJsonValues); switch (s.size()) { case 0: return newResultPromise(null); case 1: if (attributeIsSingleValued()) { return newResultPromise(new JsonValue(s.iterator().next())); } // Fall-though: unexpectedly got multiple values. It's probably best to just return them. default: return newResultPromise(new JsonValue(new ArrayList<>(s))); } } catch (final Exception ex) { // The LDAP attribute could not be decoded. return asResourceException(ex).asPromise(); } } @Override JsonValue toJsonSchema() { return jsonSchema; } }