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 2012-2016 ForgeRock AS.
015 */
016package org.forgerock.opendj.rest2ldap;
017
018import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.*;
019
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.List;
023import java.util.Set;
024
025import org.forgerock.json.JsonPointer;
026import org.forgerock.json.JsonValue;
027import org.forgerock.json.resource.ResourceException;
028import org.forgerock.opendj.ldap.Attribute;
029import org.forgerock.opendj.ldap.AttributeDescription;
030import org.forgerock.opendj.ldap.ByteString;
031import org.forgerock.opendj.ldap.Entry;
032import org.forgerock.opendj.ldap.Filter;
033import org.forgerock.opendj.ldap.schema.AttributeType;
034import org.forgerock.opendj.ldap.schema.Syntax;
035import org.forgerock.services.context.Context;
036import org.forgerock.util.Function;
037import org.forgerock.util.promise.Promise;
038
039import static java.util.Collections.*;
040import static org.forgerock.json.JsonValue.*;
041import static org.forgerock.opendj.ldap.Filter.*;
042import static org.forgerock.opendj.ldap.schema.CoreSchema.*;
043import static org.forgerock.opendj.rest2ldap.Utils.*;
044import static org.forgerock.util.promise.Promises.newResultPromise;
045
046/** An property mapper which provides a simple mapping from a JSON value to a single LDAP attribute. */
047public final class SimplePropertyMapper extends AbstractLdapPropertyMapper<SimplePropertyMapper> {
048    private Function<ByteString, ?, ? extends Exception> decoder;
049    private Function<Object, ByteString, ? extends Exception> encoder;
050    private JsonValue jsonSchema;
051
052    SimplePropertyMapper(final AttributeDescription ldapAttributeName) {
053        super(ldapAttributeName);
054    }
055
056    /**
057     * Sets the decoder which will be used for converting LDAP attribute values
058     * to JSON values.
059     *
060     * @param f
061     *            The function to use for decoding LDAP attribute values.
062     * @return This property mapper.
063     */
064    public SimplePropertyMapper decoder(final Function<ByteString, ?, ? extends Exception> f) {
065        this.decoder = f;
066        return this;
067    }
068
069    /**
070     * Sets the default JSON value which should be substituted when the LDAP attribute is not found in the LDAP entry.
071     *
072     * @param defaultValue
073     *            The default JSON value.
074     * @return This property mapper.
075     */
076    public SimplePropertyMapper defaultJsonValue(final Object defaultValue) {
077        this.defaultJsonValues = defaultValue != null ? singletonList(defaultValue) : emptyList();
078        return this;
079    }
080
081    /**
082     * Sets the default JSON values which should be substituted when the LDAP attribute is not found in the LDAP entry.
083     *
084     * @param defaultValues
085     *            The default JSON values.
086     * @return This property mapper.
087     */
088    public SimplePropertyMapper defaultJsonValues(final Collection<?> defaultValues) {
089        this.defaultJsonValues = defaultValues != null ? new ArrayList<>(defaultValues) : emptyList();
090        return this;
091    }
092
093    /**
094     * Sets the encoder which will be used for converting JSON values to LDAP
095     * attribute values.
096     *
097     * @param f
098     *            The function to use for encoding LDAP attribute values.
099     * @return This property mapper.
100     */
101    public SimplePropertyMapper encoder(final Function<Object, ByteString, ? extends Exception> f) {
102        this.encoder = f;
103        return this;
104    }
105
106    /**
107     * Indicates that JSON values are base 64 encodings of binary data. Calling
108     * this method with the value {@code true} is equivalent to the following:
109     *
110     * <pre>
111     * mapper.decoder(...); // function that converts binary data to base 64
112     * mapper.encoder(...); // function that converts base 64 to binary data
113     * </pre>
114     *
115     * Passing in a value of {@code false} resets the encoding and decoding
116     * functions to the default.
117     *
118     * @param isBinary {@code true} if this property is binary.
119     * @return This property mapper.
120     */
121    public SimplePropertyMapper isBinary(final boolean isBinary) {
122        if (isBinary) {
123            decoder = byteStringToBase64();
124            encoder = base64ToByteString();
125        } else {
126            decoder = null;
127            encoder = null;
128        }
129        return this;
130    }
131
132    /**
133     * Sets the JSON schema corresponding to this simple property mapper. If not {@code null},
134     * it will be returned by {@link #toJsonSchema()}, otherwise a default JSON schema will be
135     * automatically generated with the information available in this property mapper.
136     *
137     * @param jsonSchema
138     *          the JSON schema corresponding to this simple property mapper. Can be {@code null}
139     * @return This property mapper.
140     */
141    public SimplePropertyMapper jsonSchema(JsonValue jsonSchema) {
142        this.jsonSchema = jsonSchema;
143        return this;
144    }
145
146    @Override
147    public String toString() {
148        return "simple(" + ldapAttributeName + ")";
149    }
150
151    @Override
152    Promise<Filter, ResourceException> getLdapFilter(final Context context, final Resource resource,
153                                                     final JsonPointer path, final JsonPointer subPath,
154                                                     final FilterType type, final String operator,
155                                                     final Object valueAssertion) {
156        if (subPath.isEmpty()) {
157            try {
158                final ByteString va = valueAssertion != null ? encoder().apply(valueAssertion) : null;
159                return newResultPromise(toFilter(type, ldapAttributeName.toString(), va));
160            } catch (final Exception e) {
161                // Invalid assertion value - bad request.
162                return newBadRequestException(
163                        ERR_ILLEGAL_FILTER_ASSERTION_VALUE.get(String.valueOf(valueAssertion), path), e).asPromise();
164            }
165        } else {
166            // This property mapper does not support partial filtering.
167            return newResultPromise(alwaysFalse());
168        }
169    }
170
171    @Override
172    Promise<Attribute, ResourceException> getNewLdapAttributes(final Context context, final Resource resource,
173                                                               final JsonPointer path, final List<Object> newValues) {
174        try {
175            return newResultPromise(jsonToAttribute(newValues, ldapAttributeName, encoder()));
176        } catch (final Exception ex) {
177            return newBadRequestException(ERR_ENCODING_VALUES_FOR_FIELD.get(path, ex.getMessage())).asPromise();
178        }
179    }
180
181    @Override
182    SimplePropertyMapper getThis() {
183        return this;
184    }
185
186    @SuppressWarnings("fallthrough")
187    @Override
188    Promise<JsonValue, ResourceException> read(final Context context, final Resource resource,
189                                               final JsonPointer path, final Entry e) {
190        try {
191            final Set<Object> s = e.parseAttribute(ldapAttributeName).asSetOf(decoder(), defaultJsonValues);
192            switch (s.size()) {
193            case 0:
194                return newResultPromise(null);
195            case 1:
196                if (attributeIsSingleValued()) {
197                    return newResultPromise(new JsonValue(s.iterator().next()));
198                }
199                // Fall-though: unexpectedly got multiple values. It's probably best to just return them.
200            default:
201                return newResultPromise(new JsonValue(new ArrayList<>(s)));
202            }
203        } catch (final Exception ex) {
204            // The LDAP attribute could not be decoded.
205            return Rest2Ldap.asResourceException(ex).asPromise();
206        }
207    }
208
209    private Function<ByteString, ?, ? extends Exception> decoder() {
210        return decoder == null ? byteStringToJson(ldapAttributeName) : decoder;
211    }
212
213    private Function<Object, ByteString, ? extends Exception> encoder() {
214        return encoder == null ? jsonToByteString(ldapAttributeName) : encoder;
215    }
216
217    @Override
218    JsonValue toJsonSchema() {
219        return this.jsonSchema != null ? this.jsonSchema : toJsonSchema0();
220    }
221
222    private JsonValue toJsonSchema0() {
223        final AttributeType attrType = ldapAttributeName.getAttributeType();
224
225        final JsonValue jsonSchema;
226        if (isMultiValued()) {
227            jsonSchema = json(object(
228                field("type", "array"),
229                // LDAP has set semantics => all items are unique
230                field("uniqueItems", true),
231                field("items", itemsSchema(attrType).getObject())));
232        } else {
233            jsonSchema = itemsSchema(attrType);
234        }
235
236        final String description = attrType.getDescription();
237        if (description != null && !"".equals(description)) {
238            jsonSchema.put("title", description);
239        }
240        putWritabilityProperties(jsonSchema);
241        return jsonSchema;
242    }
243
244    private JsonValue itemsSchema(final AttributeType attrType) {
245        final JsonValue itemsSchema = json(object());
246        putTypeAndFormat(itemsSchema, attrType);
247        return itemsSchema;
248    }
249
250    /**
251     * Puts the type and format corresponding to the provided attribute type on the provided JSON
252     * schema.
253     *
254     * @param jsonSchema
255     *          the JSON schema where to put the type and format
256     * @param attrType
257     *          the attribute type for which to infer JSON the type and format
258     * @see <a href=
259     *      "https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types">
260     *      OpenAPI Specification 2.0</a>
261     * @see <a href="https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-7.3">
262     *      draft-fge-json-schema-validation-00 - Semantic validation with "format" - Defined
263     *      attributes</a>
264     */
265    public static void putTypeAndFormat(JsonValue jsonSchema, AttributeType attrType) {
266        if (attrType.isPlaceHolder()) {
267            jsonSchema.put("type", "string");
268            return;
269        }
270
271        final Syntax syntax = attrType.getSyntax();
272        if (attrType.hasName("userPassword")) {
273            jsonSchema.put("type", "string");
274            jsonSchema.put("format", "password");
275        } else if (attrType.hasName("mail")) {
276            jsonSchema.put("type", "string");
277            jsonSchema.put("format", "email");
278        } else if (syntax.equals(getBooleanSyntax())) {
279            jsonSchema.put("type", "boolean");
280        } else if (syntax.equals(getNumericStringSyntax())) {
281            // credit card numbers are numeric strings whose leading zeros are significant
282            jsonSchema.put("type", "string");
283        } else if (syntax.equals(getIntegerSyntax())) {
284            jsonSchema.put("type", "integer");
285        } else if (syntax.equals(getGeneralizedTimeSyntax())) {
286            jsonSchema.put("type", "string");
287            jsonSchema.put("format", "date-time");
288        } else if (!syntax.isHumanReadable()) {
289            jsonSchema.put("type", "string");
290            jsonSchema.put("format", "byte");
291        } else {
292            jsonSchema.put("type", "string");
293        }
294    }
295}