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 *
016 */
017package org.forgerock.opendj.rest2ldap;
018
019import static java.util.Arrays.asList;
020import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ABSTRACT_TYPE_IN_CREATE;
021import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_MISSING_TYPE_PROPERTY_IN_CREATE;
022import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE;
023import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_TYPE_IN_CREATE;
024import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
025import static org.forgerock.util.Utils.joinAsString;
026
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.Collection;
030import java.util.HashSet;
031import java.util.LinkedHashMap;
032import java.util.LinkedHashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Set;
036
037import org.forgerock.i18n.LocalizedIllegalArgumentException;
038import org.forgerock.json.JsonPointer;
039import org.forgerock.json.JsonValue;
040import org.forgerock.json.resource.RequestHandler;
041import org.forgerock.json.resource.ResourceException;
042import org.forgerock.json.resource.Router;
043import org.forgerock.opendj.ldap.Attribute;
044import org.forgerock.opendj.ldap.Entry;
045import org.forgerock.opendj.ldap.LinkedAttribute;
046
047/**
048 * Defines the characteristics of a resource, including its properties, inheritance, and sub-resources.
049 */
050public final class Resource {
051    /** The resource ID. */
052    private final String id;
053    /** {@code true} if only sub-types of this resource can be created. */
054    private boolean isAbstract;
055    /** The ID of the super-type of this resource, may be {@code null}. */
056    private String superTypeId;
057    /** The LDAP object classes associated with this resource. */
058    private final Attribute objectClasses = new LinkedAttribute("objectClass");
059    /** The possibly empty set of sub-resources. */
060    private final Set<SubResource> subResources = new LinkedHashSet<>();
061    /** The set of property mappers associated with this resource, excluding inherited properties. */
062    private final Map<String, PropertyMapper> declaredProperties = new LinkedHashMap<>();
063    /** The set of property mappers associated with this resource, including inherited properties. */
064    private final Map<String, PropertyMapper> allProperties = new LinkedHashMap<>();
065    /**
066     * A JSON pointer to the primitive JSON property that will be used to convey type information. May be {@code
067     * null} if the type property is defined in a super type or if this resource does not have any sub-types.
068     */
069    private JsonPointer resourceTypeProperty;
070    /** Set to {@code true} once this Resource has been built. */
071    private boolean isBuilt = false;
072    /** The resolved super-type. */
073    private Resource superType;
074    /** The resolved sub-resources (only immediate children). */
075    private final Set<Resource> subTypes = new LinkedHashSet<>();
076    /** The property mapper which will map all properties for this resource including inherited properties. */
077    private final ObjectPropertyMapper propertyMapper = new ObjectPropertyMapper();
078    /** Routes requests to sub-resources. */
079    private final Router subResourceRouter = new Router();
080    private volatile Boolean hasSubTypesWithSubResources = null;
081    /** The set of actions supported by this resource and its sub-types. */
082    private final Set<Action> supportedActions = new HashSet<>();
083
084    Resource(final String id) {
085        this.id = id;
086    }
087
088    /**
089     * Returns the resource ID of this resource.
090     *
091     * @return The resource ID of this resource.
092     */
093    @Override
094    public String toString() {
095        return id;
096    }
097
098    /**
099     * Returns {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this
100     * resource.
101     *
102     * @param o
103     *         The object to compare.
104     * @return {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this
105     * resource.
106     */
107    @Override
108    public boolean equals(final Object o) {
109        return this == o || (o instanceof Resource && id.equals(((Resource) o).id));
110    }
111
112    @Override
113    public int hashCode() {
114        return id.hashCode();
115    }
116
117    /**
118     * Specifies the resource ID of the resource which is a super-type of this resource. This resource will inherit
119     * the properties and sub-resources of the super-type, and may optionally override them.
120     *
121     * @param resourceId
122     *         The resource ID of the resource which is a super-type of this resource, or {@code null} if there is no
123     *         super-type.
124     * @return A reference to this object.
125     */
126    public Resource superType(final String resourceId) {
127        this.superTypeId = resourceId;
128        return this;
129    }
130
131    /**
132     * Specifies whether this resource is an abstract type and therefore cannot be created. Only non-abstract
133     * sub-types can be created.
134     *
135     * @param isAbstract
136     *         {@code true} if this resource is abstract.
137     * @return A reference to this object.
138     */
139    public Resource isAbstract(final boolean isAbstract) {
140        this.isAbstract = isAbstract;
141        return this;
142    }
143
144    /**
145     * Specifies a mapping for a property contained in this JSON resource. Properties are inherited and sub-types may
146     * override them. Properties are optional: a resource that does not have any properties cannot be created, read,
147     * or modified, and may only be used for accessing sub-resources. These resources usually represent API
148     * "endpoints".
149     *
150     * @param name
151     *         The name of the JSON property to be mapped.
152     * @param mapper
153     *         The property mapper responsible for mapping the JSON property to LDAP attribute(s).
154     * @return A reference to this object.
155     */
156    public Resource property(final String name, final PropertyMapper mapper) {
157        declaredProperties.put(name, mapper);
158        return this;
159    }
160
161    /**
162     * Specifies whether all LDAP user attributes should be mapped by default using the default schema based mapping
163     * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes} in order to prevent
164     * attributes with explicit mappings being mapped twice.
165     *
166     * @param include {@code true} if all LDAP user attributes be mapped by default.
167     * @return A reference to this object.
168     */
169    public Resource includeAllUserAttributesByDefault(final boolean include) {
170        propertyMapper.includeAllUserAttributesByDefault(include);
171        return this;
172    }
173
174    /**
175     * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
176     * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
177     * excluded in order to prevent duplication.
178     *
179     * @param attributeNames The list of attributes to be excluded.
180     * @return A reference to this object.
181     */
182    public Resource excludedDefaultUserAttributes(final String... attributeNames) {
183        return excludedDefaultUserAttributes(Arrays.asList(attributeNames));
184    }
185
186    /**
187     * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
188     * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
189     * excluded in order to prevent duplication.
190     *
191     * @param attributeNames The list of attributes to be excluded.
192     * @return A reference to this object.
193     */
194    public Resource excludedDefaultUserAttributes(final Collection<String> attributeNames) {
195        propertyMapper.excludedDefaultUserAttributes(attributeNames);
196        return this;
197    }
198
199    /**
200     * Specifies the name of the JSON property which contains the resource's type, whose value is the
201     * resource ID. The resource type property is inherited by sub-types and must be available to any resources
202     * referenced from {@link SubResource sub-resources}.
203     *
204     * @param resourceTypeProperty
205     *         The name of the JSON property which contains the resource's type, or {@code null} if this resource does
206     *         not have a resource type property or if it should be inherited from a super-type.
207     * @return A reference to this object.
208     */
209    public Resource resourceTypeProperty(final JsonPointer resourceTypeProperty) {
210        this.resourceTypeProperty = resourceTypeProperty;
211        return this;
212    }
213
214    /**
215     * Specifies an LDAP object class which is to be associated with this resource. Multiple object classes may be
216     * specified. The object classes are used for determining the type of resource being accessed during all requests
217     * other than create. Object classes are inherited by sub-types and must be defined for any resources that are
218     * non-abstract and which can be created.
219     *
220     * @param objectClass
221     *         An LDAP object class associated with this resource's LDAP representation.
222     * @return A reference to this object.
223     */
224    public Resource objectClass(final String objectClass) {
225        this.objectClasses.add(objectClass);
226        return this;
227    }
228
229    /**
230     * Specifies LDAP object classes which are to be associated with this resource. Multiple object classes may be
231     * specified. The object classes are used for determining the type of resource being accessed during all requests
232     * other than create. Object classes are inherited by sub-types and must be defined for any resources that are
233     * non-abstract and which can be created.
234     *
235     * @param objectClasses
236     *         The LDAP object classes associated with this resource's LDAP representation.
237     * @return A reference to this object.
238     */
239    public Resource objectClasses(final String... objectClasses) {
240        this.objectClasses.add((Object[]) objectClasses);
241        return this;
242    }
243
244    /**
245     * Registers an action which should be supported by this resource. By default, no actions are supported.
246     *
247     * @param action
248     *         The action supported by this resource.
249     * @return A reference to this object.
250     */
251    public Resource supportedAction(final Action action) {
252        this.supportedActions.add(action);
253        return this;
254    }
255
256    /**
257     * Registers zero or more actions which should be supported by this resource. By default, no actions are supported.
258     *
259     * @param actions
260     *         The actions supported by this resource.
261     * @return A reference to this object.
262     */
263    public Resource supportedActions(final Action... actions) {
264        this.supportedActions.addAll(Arrays.asList(actions));
265        return this;
266    }
267
268    /**
269     * Specifies a parent-child relationship with another resource. Sub-resources are inherited by sub-types and may
270     * be overridden.
271     *
272     * @param subResource
273     *         The sub-resource definition.
274     * @return A reference to this object.
275     */
276    public Resource subResource(final SubResource subResource) {
277        this.subResources.add(subResource);
278        return this;
279    }
280
281    /**
282     * Specifies a parent-child relationship with zero or more resources. Sub-resources are inherited by sub-types and
283     * may be overridden.
284     *
285     * @param subResources
286     *         The sub-resource definitions.
287     * @return A reference to this object.
288     */
289    public Resource subResources(final SubResource... subResources) {
290        this.subResources.addAll(asList(subResources));
291        return this;
292    }
293
294    boolean hasSupportedAction(final Action action) {
295        return supportedActions.contains(action);
296    }
297
298    boolean hasSubTypes() {
299        return !subTypes.isEmpty();
300    }
301
302    boolean mayHaveSubResources() {
303        return !subResources.isEmpty() || hasSubTypesWithSubResources();
304    }
305
306    boolean hasSubTypesWithSubResources() {
307        if (hasSubTypesWithSubResources == null) {
308            for (final Resource subType : subTypes) {
309                if (!subType.subResources.isEmpty() || subType.hasSubTypesWithSubResources()) {
310                    hasSubTypesWithSubResources = true;
311                    return true;
312                }
313            }
314            hasSubTypesWithSubResources = false;
315        }
316        return hasSubTypesWithSubResources;
317    }
318
319    Set<Resource> getSubTypes() {
320        return subTypes;
321    }
322
323    Resource resolveSubTypeFromJson(final JsonValue content) throws ResourceException {
324        if (!hasSubTypes()) {
325            // The resource type is implied because this resource does not have sub-types. In particular, resources
326            // are not required to have type information if they don't have sub-types.
327            return this;
328        }
329        final JsonValue jsonType = content.get(resourceTypeProperty);
330        if (jsonType == null || !jsonType.isString()) {
331            throw newBadRequestException(ERR_MISSING_TYPE_PROPERTY_IN_CREATE.get(resourceTypeProperty));
332        }
333        final String type = jsonType.asString();
334        final Resource subType = resolveSubTypeFromString(type);
335        if (subType == null) {
336            throw newBadRequestException(ERR_UNRECOGNIZED_TYPE_IN_CREATE.get(type, getAllowedResourceTypes()));
337        }
338        if (subType.isAbstract) {
339            throw newBadRequestException(ERR_ABSTRACT_TYPE_IN_CREATE.get(type, getAllowedResourceTypes()));
340        }
341        return subType;
342    }
343
344    private String getAllowedResourceTypes() {
345        final List<String> allowedTypes = new ArrayList<>();
346        getAllowedResourceTypes(allowedTypes);
347        return joinAsString(", ", allowedTypes);
348    }
349
350    private void getAllowedResourceTypes(final List<String> allowedTypes) {
351        if (!isAbstract) {
352            allowedTypes.add(id);
353        }
354        for (final Resource subType : subTypes) {
355            subType.getAllowedResourceTypes(allowedTypes);
356        }
357    }
358
359    Resource resolveSubTypeFromString(final String type) {
360        if (id.equalsIgnoreCase(type)) {
361            return this;
362        }
363        for (final Resource subType : subTypes) {
364            final Resource resolvedSubType = subType.resolveSubTypeFromString(type);
365            if (resolvedSubType != null) {
366                return resolvedSubType;
367            }
368        }
369        return null;
370    }
371
372    Resource resolveSubTypeFromObjectClasses(final Entry entry) {
373        if (!hasSubTypes()) {
374            // This resource does not have sub-types.
375            return this;
376        }
377        final Attribute objectClassesFromEntry = entry.getAttribute("objectClass");
378        final Resource subType = resolveSubTypeFromObjectClasses(objectClassesFromEntry);
379        if (subType == null) {
380            // Best effort.
381            return this;
382        }
383        return subType;
384    }
385
386    private Resource resolveSubTypeFromObjectClasses(final Attribute objectClassesFromEntry) {
387        if (!objectClassesFromEntry.containsAll(objectClasses)) {
388            return null;
389        }
390        // This resource is a potential match, but sub-types may be better.
391        for (final Resource subType : subTypes) {
392            final Resource resolvedSubType = subType.resolveSubTypeFromObjectClasses(objectClassesFromEntry);
393            if (resolvedSubType != null) {
394                return resolvedSubType;
395            }
396        }
397        return this;
398    }
399
400    Attribute getObjectClassAttribute() {
401        return objectClasses;
402    }
403
404    RequestHandler getSubResourceRouter() {
405        return subResourceRouter;
406    }
407
408    String getResourceId() {
409        return id;
410    }
411
412    void build(final Rest2Ldap rest2Ldap) {
413        // Prevent re-entrant calls.
414        if (isBuilt) {
415            return;
416        }
417        isBuilt = true;
418
419        if (superTypeId != null) {
420            superType = rest2Ldap.getResource(superTypeId);
421            if (superType == null) {
422                throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE.get(id, superTypeId));
423            }
424            // Inherit content from super-type.
425            superType.build(rest2Ldap);
426            superType.subTypes.add(this);
427            if (resourceTypeProperty == null) {
428                resourceTypeProperty = superType.resourceTypeProperty;
429            }
430            objectClasses.addAll(superType.objectClasses);
431            subResourceRouter.addAllRoutes(superType.subResourceRouter);
432            allProperties.putAll(superType.allProperties);
433        }
434        allProperties.putAll(declaredProperties);
435        for (final Map.Entry<String, PropertyMapper> property : allProperties.entrySet()) {
436            propertyMapper.property(property.getKey(), property.getValue());
437        }
438        for (final SubResource subResource : subResources) {
439            subResource.build(rest2Ldap, id);
440            subResource.addRoutes(subResourceRouter);
441        }
442    }
443
444    PropertyMapper getPropertyMapper() {
445        return propertyMapper;
446    }
447}