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-2015 ForgeRock AS.
015 */
016package org.forgerock.opendj.rest2ldap;
017
018import java.util.AbstractMap.SimpleImmutableEntry;
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.LinkedHashMap;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025
026import org.forgerock.json.fluent.JsonPointer;
027import org.forgerock.json.fluent.JsonValue;
028import org.forgerock.json.resource.BadRequestException;
029import org.forgerock.json.resource.PatchOperation;
030import org.forgerock.json.resource.ResourceException;
031import org.forgerock.json.resource.ResultHandler;
032import org.forgerock.opendj.ldap.Attribute;
033import org.forgerock.opendj.ldap.Entry;
034import org.forgerock.opendj.ldap.Filter;
035import org.forgerock.opendj.ldap.Modification;
036import org.forgerock.util.Function;
037import org.forgerock.util.promise.NeverThrowsException;
038
039import static org.forgerock.json.resource.PatchOperation.*;
040import static org.forgerock.opendj.ldap.Filter.*;
041import static org.forgerock.opendj.rest2ldap.Rest2LDAP.*;
042import static org.forgerock.opendj.rest2ldap.Utils.*;
043
044/**
045 * An attribute mapper which maps JSON objects to LDAP attributes.
046 */
047public final class ObjectAttributeMapper extends AttributeMapper {
048
049    private static final class Mapping {
050        private final AttributeMapper mapper;
051        private final String name;
052
053        private Mapping(final String name, final AttributeMapper mapper) {
054            this.name = name;
055            this.mapper = mapper;
056        }
057
058        @Override
059        public String toString() {
060            return name + " -> " + mapper;
061        }
062    }
063
064    private final Map<String, Mapping> mappings = new LinkedHashMap<>();
065
066    ObjectAttributeMapper() {
067        // Nothing to do.
068    }
069
070    /**
071     * Creates a mapping for an attribute contained in the JSON object.
072     *
073     * @param name
074     *            The name of the JSON attribute to be mapped.
075     * @param mapper
076     *            The attribute mapper responsible for mapping the JSON
077     *            attribute to LDAP attribute(s).
078     * @return A reference to this attribute mapper.
079     */
080    public ObjectAttributeMapper attribute(final String name, final AttributeMapper mapper) {
081        mappings.put(toLowerCase(name), new Mapping(name, mapper));
082        return this;
083    }
084
085    @Override
086    public String toString() {
087        return "object(" + mappings.values() + ")";
088    }
089
090    @Override
091    void create(final Context c, final JsonPointer path, final JsonValue v,
092            final ResultHandler<List<Attribute>> h) {
093        try {
094            /*
095             * First check that the JSON value is an object and that the fields
096             * it contains are known by this mapper.
097             */
098            final Map<String, Mapping> missingMappings = checkMapping(path, v);
099
100            // Accumulate the results of the subordinate mappings.
101            final ResultHandler<List<Attribute>> handler = accumulator(h);
102
103            // Invoke mappings for which there are values provided.
104            if (v != null && !v.isNull()) {
105                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
106                    final Mapping mapping = getMapping(me.getKey());
107                    final JsonValue subValue = new JsonValue(me.getValue());
108                    mapping.mapper.create(c, path.child(me.getKey()), subValue, handler);
109                }
110            }
111
112            // Invoke mappings for which there were no values provided.
113            for (final Mapping mapping : missingMappings.values()) {
114                mapping.mapper.create(c, path.child(mapping.name), null, handler);
115            }
116        } catch (final Exception e) {
117            h.handleError(asResourceException(e));
118        }
119    }
120
121    @Override
122    void getLDAPAttributes(final Context c, final JsonPointer path, final JsonPointer subPath,
123            final Set<String> ldapAttributes) {
124        if (subPath.isEmpty()) {
125            // Request all subordinate mappings.
126            for (final Mapping mapping : mappings.values()) {
127                mapping.mapper.getLDAPAttributes(c, path.child(mapping.name), subPath,
128                        ldapAttributes);
129            }
130        } else {
131            // Request single subordinate mapping.
132            final Mapping mapping = getMapping(subPath);
133            if (mapping != null) {
134                mapping.mapper.getLDAPAttributes(c, path.child(subPath.get(0)), subPath
135                        .relativePointer(), ldapAttributes);
136            }
137        }
138    }
139
140    @Override
141    void getLDAPFilter(final Context c, final JsonPointer path, final JsonPointer subPath,
142            final FilterType type, final String operator, final Object valueAssertion,
143            final ResultHandler<Filter> h) {
144        final Mapping mapping = getMapping(subPath);
145        if (mapping != null) {
146            mapping.mapper.getLDAPFilter(c, path.child(subPath.get(0)), subPath.relativePointer(),
147                    type, operator, valueAssertion, h);
148        } else {
149            /*
150             * Either the filter targeted the entire object (i.e. it was "/"),
151             * or it targeted an unrecognized attribute within the object.
152             * Either way, the filter will never match.
153             */
154            h.handleResult(alwaysFalse());
155        }
156    }
157
158    @Override
159    void patch(final Context c, final JsonPointer path, final PatchOperation operation,
160            final ResultHandler<List<Modification>> h) {
161        try {
162            final JsonPointer field = operation.getField();
163            final JsonValue v = operation.getValue();
164
165            if (field.isEmpty()) {
166                /*
167                 * The patch operation applies to this object. We'll handle this
168                 * by allowing the JSON value to be a partial object and
169                 * add/remove/replace only the provided values.
170                 */
171                final Map<String, Mapping> missingMappings = checkMapping(path, v);
172
173                // Accumulate the results of the subordinate mappings.
174                final ResultHandler<List<Modification>> handler =
175                        accumulator(mappings.size() - missingMappings.size(), h);
176
177                // Invoke mappings for which there are values provided.
178                if (!v.isNull()) {
179                    for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
180                        final Mapping mapping = getMapping(me.getKey());
181                        final JsonValue subValue = new JsonValue(me.getValue());
182                        final PatchOperation subOperation =
183                                operation(operation.getOperation(), field /* empty */, subValue);
184                        mapping.mapper.patch(c, path.child(me.getKey()), subOperation, handler);
185                    }
186                }
187            } else {
188                /*
189                 * The patch operation targets a subordinate field. Create a new
190                 * patch operation targeting the field and forward it to the
191                 * appropriate mapper.
192                 */
193                final String fieldName = field.get(0);
194                final Mapping mapping = getMapping(fieldName);
195                if (mapping == null) {
196                    throw new BadRequestException(i18n(
197                            "The request cannot be processed because it included "
198                                    + "an unrecognized field '%s'", path.child(fieldName)));
199                }
200                final PatchOperation subOperation =
201                        operation(operation.getOperation(), field.relativePointer(), v);
202                mapping.mapper.patch(c, path.child(fieldName), subOperation, h);
203            }
204        } catch (final Exception ex) {
205            h.handleError(asResourceException(ex));
206        }
207    }
208
209    @Override
210    void read(final Context c, final JsonPointer path, final Entry e,
211            final ResultHandler<JsonValue> h) {
212        /*
213         * Use an accumulator which will aggregate the results from the
214         * subordinate mappers into a single list. On completion, the
215         * accumulator combines the results into a single JSON map object.
216         */
217        final ResultHandler<Map.Entry<String, JsonValue>> handler =
218                accumulate(mappings.size(), transform(
219                        new Function<List<Map.Entry<String, JsonValue>>, JsonValue, NeverThrowsException>() {
220                            @Override
221                            public JsonValue apply(final List<Map.Entry<String, JsonValue>> value) {
222                                if (value.isEmpty()) {
223                                    /*
224                                     * No subordinate attributes, so omit the
225                                     * entire JSON object from the resource.
226                                     */
227                                    return null;
228                                } else {
229                                    // Combine the sub-attributes into a single JSON object.
230                                    final Map<String, Object> result = new LinkedHashMap<>(value.size());
231                                    for (final Map.Entry<String, JsonValue> e : value) {
232                                        result.put(e.getKey(), e.getValue().getObject());
233                                    }
234                                    return new JsonValue(result);
235                                }
236                            }
237                        }, h));
238
239        for (final Mapping mapping : mappings.values()) {
240            mapping.mapper.read(c, path.child(mapping.name), e, transform(
241                    new Function<JsonValue, Map.Entry<String, JsonValue>, NeverThrowsException>() {
242                        @Override
243                        public Map.Entry<String, JsonValue> apply(final JsonValue value) {
244                            return value != null ? new SimpleImmutableEntry<String, JsonValue>(
245                                    mapping.name, value) : null;
246                        }
247                    }, handler));
248        }
249    }
250
251    @Override
252    void update(final Context c, final JsonPointer path, final Entry e, final JsonValue v,
253            final ResultHandler<List<Modification>> h) {
254        try {
255            /*
256             * First check that the JSON value is an object and that the fields
257             * it contains are known by this mapper.
258             */
259            final Map<String, Mapping> missingMappings = checkMapping(path, v);
260
261            // Accumulate the results of the subordinate mappings.
262            final ResultHandler<List<Modification>> handler = accumulator(h);
263
264            // Invoke mappings for which there are values provided.
265            if (v != null && !v.isNull()) {
266                for (final Map.Entry<String, Object> me : v.asMap().entrySet()) {
267                    final Mapping mapping = getMapping(me.getKey());
268                    final JsonValue subValue = new JsonValue(me.getValue());
269                    mapping.mapper.update(c, path.child(me.getKey()), e, subValue, handler);
270                }
271            }
272
273            // Invoke mappings for which there were no values provided.
274            for (final Mapping mapping : missingMappings.values()) {
275                mapping.mapper.update(c, path.child(mapping.name), e, null, handler);
276            }
277        } catch (final Exception ex) {
278            h.handleError(asResourceException(ex));
279        }
280    }
281
282    private <T> ResultHandler<List<T>> accumulator(final ResultHandler<List<T>> h) {
283        return accumulator(mappings.size(), h);
284    }
285
286    private <T> ResultHandler<List<T>> accumulator(final int size, final ResultHandler<List<T>> h) {
287        return accumulate(size, transform(new Function<List<List<T>>, List<T>, NeverThrowsException>() {
288            @Override
289            public List<T> apply(final List<List<T>> value) {
290                switch (value.size()) {
291                case 0:
292                    return Collections.emptyList();
293                case 1:
294                    return value.get(0);
295                default:
296                    final List<T> attributes = new ArrayList<>(value.size());
297                    for (final List<T> a : value) {
298                        attributes.addAll(a);
299                    }
300                    return attributes;
301                }
302            }
303        }, h));
304    }
305
306    /**
307     * Fail immediately if the JSON value has the wrong type or contains unknown
308     * attributes.
309     */
310    private Map<String, Mapping> checkMapping(final JsonPointer path, final JsonValue v)
311            throws ResourceException {
312        final Map<String, Mapping> missingMappings = new LinkedHashMap<>(mappings);
313        if (v != null && !v.isNull()) {
314            if (v.isMap()) {
315                for (final String attribute : v.asMap().keySet()) {
316                    if (missingMappings.remove(toLowerCase(attribute)) == null) {
317                        throw new BadRequestException(i18n(
318                                "The request cannot be processed because it included "
319                                        + "an unrecognized field '%s'", path.child(attribute)));
320                    }
321                }
322            } else {
323                throw new BadRequestException(i18n(
324                        "The request cannot be processed because it included "
325                                + "the field '%s' whose value is the wrong type: "
326                                + "an object is expected", path));
327            }
328        }
329        return missingMappings;
330    }
331
332    private Mapping getMapping(final JsonPointer jsonAttribute) {
333        return jsonAttribute.isEmpty() ? null : getMapping(jsonAttribute.get(0));
334    }
335
336    private Mapping getMapping(final String jsonAttribute) {
337        return mappings.get(toLowerCase(jsonAttribute));
338    }
339
340}