001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2016 ForgeRock AS.
015 */
016package org.forgerock.opendj.rest2ldap;
017
018import static java.util.Collections.emptyList;
019import static java.util.Collections.singletonList;
020import static org.forgerock.json.JsonValue.field;
021import static org.forgerock.json.JsonValue.object;
022import static org.forgerock.opendj.rest2ldap.Rest2Ldap.asResourceException;
023import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ENCODING_VALUES_FOR_FIELD;
024import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_PATCH_JSON_INTERNAL_PROPERTY;
025import static org.forgerock.opendj.rest2ldap.Utils.jsonToAttribute;
026import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
027import static org.forgerock.opendj.rest2ldap.Utils.newNotSupportedException;
028import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.byteStringToJson;
029import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.jsonToByteString;
030import static org.forgerock.util.promise.Promises.newResultPromise;
031
032import java.util.ArrayList;
033import java.util.Collection;
034import java.util.List;
035import java.util.Set;
036
037import org.forgerock.json.JsonPointer;
038import org.forgerock.json.JsonValue;
039import org.forgerock.json.resource.PatchOperation;
040import org.forgerock.json.resource.ResourceException;
041import org.forgerock.opendj.ldap.Attribute;
042import org.forgerock.opendj.ldap.AttributeDescription;
043import org.forgerock.opendj.ldap.Entry;
044import org.forgerock.opendj.ldap.Filter;
045import org.forgerock.opendj.ldap.Modification;
046import org.forgerock.services.context.Context;
047import org.forgerock.util.promise.Promise;
048import org.forgerock.util.query.QueryFilter;
049
050/** A property mapper which provides a mapping from a JSON value to an LDAP attribute having the JSON syntax. */
051public final class JsonPropertyMapper extends AbstractLdapPropertyMapper<JsonPropertyMapper> {
052    /**
053     * The default JSON schema for this property. According to json-schema.org the {} schema allows anything.
054     * However, the OpenAPI transformer seems to expect at least a "type" field to be present.
055     */
056    private static final JsonValue ANY_SCHEMA = new JsonValue(object(field("type", "object")));
057    private JsonValue jsonSchema = ANY_SCHEMA;
058
059    JsonPropertyMapper(final AttributeDescription ldapAttributeName) {
060        super(ldapAttributeName);
061    }
062
063    /**
064     * Sets the default JSON value which should be substituted when the LDAP attribute is not found in the LDAP entry.
065     *
066     * @param defaultValue
067     *         The default JSON value.
068     * @return This property mapper.
069     */
070    public JsonPropertyMapper defaultJsonValue(final Object defaultValue) {
071        this.defaultJsonValues = defaultValue != null ? singletonList(defaultValue) : emptyList();
072        return this;
073    }
074
075    /**
076     * Sets the default JSON values which should be substituted when the LDAP attribute is not found in the LDAP entry.
077     *
078     * @param defaultValues
079     *         The default JSON values.
080     * @return This property mapper.
081     */
082    public JsonPropertyMapper defaultJsonValues(final Collection<?> defaultValues) {
083        this.defaultJsonValues = defaultValues != null ? new ArrayList<>(defaultValues) : emptyList();
084        return this;
085    }
086
087    /**
088     * Sets the JSON schema corresponding to this simple property mapper. If not {@code null},
089     * it will be returned by {@link #toJsonSchema()}, otherwise a default JSON schema will be
090     * automatically generated with the information available in this property mapper.
091     *
092     * @param jsonSchema
093     *         the JSON schema corresponding to this simple property mapper. Can be {@code null}
094     * @return This property mapper.
095     */
096    public JsonPropertyMapper jsonSchema(JsonValue jsonSchema) {
097        this.jsonSchema = jsonSchema != null ? jsonSchema : ANY_SCHEMA;
098        return this;
099    }
100
101    @Override
102    public String toString() {
103        return "json(" + ldapAttributeName + ")";
104    }
105
106    @Override
107    Promise<Filter, ResourceException> getLdapFilter(final Context context, final Resource resource,
108                                                     final JsonPointer path, final JsonPointer subPath,
109                                                     final FilterType type, final String operator,
110                                                     final Object valueAssertion) {
111        final QueryFilter<JsonPointer> queryFilter = toQueryFilter(type, subPath, operator, valueAssertion);
112        return newResultPromise(Filter.equality(ldapAttributeName.toString(), queryFilter));
113    }
114
115    private QueryFilter<JsonPointer> toQueryFilter(final FilterType type, final JsonPointer subPath,
116                                                   final String operator, final Object valueAssertion) {
117        switch (type) {
118        case CONTAINS:
119            return QueryFilter.contains(subPath, valueAssertion);
120        case STARTS_WITH:
121            return QueryFilter.startsWith(subPath, valueAssertion);
122        case EQUAL_TO:
123            return QueryFilter.equalTo(subPath, valueAssertion);
124        case GREATER_THAN:
125            return QueryFilter.greaterThan(subPath, valueAssertion);
126        case GREATER_THAN_OR_EQUAL_TO:
127            return QueryFilter.greaterThanOrEqualTo(subPath, valueAssertion);
128        case LESS_THAN:
129            return QueryFilter.lessThan(subPath, valueAssertion);
130        case LESS_THAN_OR_EQUAL_TO:
131            return QueryFilter.lessThanOrEqualTo(subPath, valueAssertion);
132        case PRESENT:
133            return QueryFilter.present(subPath);
134        case EXTENDED:
135            return QueryFilter.extendedMatch(subPath, operator, valueAssertion);
136        default:
137            return QueryFilter.alwaysFalse();
138        }
139    }
140
141    @Override
142    Promise<Attribute, ResourceException> getNewLdapAttributes(final Context context, final Resource resource,
143                                                               final JsonPointer path, final List<Object> newValues) {
144        try {
145            return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, jsonToByteString()));
146        } catch (final Exception e) {
147            return newBadRequestException(ERR_ENCODING_VALUES_FOR_FIELD.get(path, e.getMessage())).asPromise();
148        }
149    }
150
151    @Override
152    JsonPropertyMapper getThis() {
153        return this;
154    }
155
156    /** Intercept attempts to patch internal fields and reject these as unsupported rather than unrecognized. */
157    @Override
158    Promise<List<Modification>, ResourceException> patch(final Context context, final Resource resource,
159                                                         final JsonPointer path, final PatchOperation operation) {
160        final JsonPointer field = operation.getField();
161        if (field.isEmpty() || field.size() == 1 && field.get(0).equals("-")) {
162            return super.patch(context, resource, path, operation);
163        }
164        return newNotSupportedException(ERR_PATCH_JSON_INTERNAL_PROPERTY.get(field, path, path)).asPromise();
165    }
166
167    @SuppressWarnings("fallthrough")
168    @Override
169    Promise<JsonValue, ResourceException> read(final Context context, final Resource resource, final JsonPointer path,
170                                               final Entry e) {
171        try {
172            final Set<Object> s = e.parseAttribute(ldapAttributeName).asSetOf(byteStringToJson(), defaultJsonValues);
173            switch (s.size()) {
174            case 0:
175                return newResultPromise(null);
176            case 1:
177                if (attributeIsSingleValued()) {
178                    return newResultPromise(new JsonValue(s.iterator().next()));
179                }
180                // Fall-though: unexpectedly got multiple values. It's probably best to just return them.
181            default:
182                return newResultPromise(new JsonValue(new ArrayList<>(s)));
183            }
184        } catch (final Exception ex) {
185            // The LDAP attribute could not be decoded.
186            return asResourceException(ex).asPromise();
187        }
188    }
189
190    @Override
191    JsonValue toJsonSchema() {
192        return jsonSchema;
193    }
194}