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.Arrays.asList;
019import static org.forgerock.api.enums.CountPolicy.*;
020import static org.forgerock.api.enums.PagingMode.*;
021import static org.forgerock.api.enums.ParameterSource.*;
022import static org.forgerock.api.enums.PatchOperation.*;
023import static org.forgerock.api.enums.Stability.*;
024import static org.forgerock.api.models.VersionedPath.*;
025import static org.forgerock.json.JsonValue.*;
026import static org.forgerock.json.resource.ResourceException.*;
027import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_ABSTRACT_TYPE_IN_CREATE;
028import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_MISSING_TYPE_PROPERTY_IN_CREATE;
029import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE;
030import static org.forgerock.opendj.rest2ldap.Rest2ldapMessages.ERR_UNRECOGNIZED_TYPE_IN_CREATE;
031import static org.forgerock.opendj.rest2ldap.Utils.newBadRequestException;
032import static org.forgerock.util.Utils.joinAsString;
033
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Collection;
037import java.util.HashSet;
038import java.util.LinkedHashMap;
039import java.util.LinkedHashSet;
040import java.util.List;
041import java.util.Map;
042import java.util.Set;
043
044import org.forgerock.api.commons.CommonsApi;
045import org.forgerock.api.enums.CreateMode;
046import org.forgerock.api.enums.QueryType;
047import org.forgerock.api.models.ApiDescription;
048import org.forgerock.api.models.ApiError;
049import org.forgerock.api.models.Create;
050import org.forgerock.api.models.Definitions;
051import org.forgerock.api.models.Delete;
052import org.forgerock.api.models.Errors;
053import org.forgerock.api.models.Items;
054import org.forgerock.api.models.Parameter;
055import org.forgerock.api.models.Patch;
056import org.forgerock.api.models.Paths;
057import org.forgerock.api.models.Query;
058import org.forgerock.api.models.Read;
059import org.forgerock.api.models.Reference;
060import org.forgerock.api.models.Schema;
061import org.forgerock.api.models.Services;
062import org.forgerock.api.models.Update;
063import org.forgerock.http.ApiProducer;
064import org.forgerock.i18n.LocalizableMessage;
065import org.forgerock.i18n.LocalizedIllegalArgumentException;
066import org.forgerock.json.JsonPointer;
067import org.forgerock.json.JsonValue;
068import org.forgerock.json.resource.RequestHandler;
069import org.forgerock.json.resource.ResourceException;
070import org.forgerock.json.resource.Router;
071import org.forgerock.opendj.ldap.Attribute;
072import org.forgerock.opendj.ldap.Entry;
073import org.forgerock.opendj.ldap.LinkedAttribute;
074import org.forgerock.util.i18n.LocalizableString;
075
076/**
077 * Defines the characteristics of a resource, including its properties, inheritance, and sub-resources.
078 */
079public final class Resource {
080    // errors
081    private static final String ERROR_ADMIN_LIMIT_EXCEEDED = "#/errors/adminLimitExceeded";
082    private static final String ERROR_READ_FOUND_MULTIPLE_ENTRIES = "#/errors/readFoundMultipleEntries";
083    private static final String ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS = "#/errors/passwordModifyRequiresHttps";
084    private static final String ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION = "#/errors/passwordModifyRequiresAuthn";
085
086    private static final String ERROR_BAD_REQUEST = CommonsApi.Errors.BAD_REQUEST.getReference();
087    private static final String ERROR_FORBIDDEN = CommonsApi.Errors.FORBIDDEN.getReference();
088    private static final String ERROR_INTERNAL_SERVER_ERROR = CommonsApi.Errors.INTERNAL_SERVER_ERROR.getReference();
089    private static final String ERROR_NOT_FOUND = CommonsApi.Errors.NOT_FOUND.getReference();
090    private static final String ERROR_REQUEST_ENTITY_TOO_LARGE =
091        CommonsApi.Errors.REQUEST_ENTITY_TOO_LARGE.getReference();
092    private static final String ERROR_REQUEST_TIMEOUT = CommonsApi.Errors.REQUEST_TIMEOUT.getReference();
093    private static final String ERROR_UNAUTHORIZED = CommonsApi.Errors.UNAUTHORIZED.getReference();
094    private static final String ERROR_UNAVAILABLE = CommonsApi.Errors.UNAVAILABLE.getReference();
095    private static final String ERROR_VERSION_MISMATCH = CommonsApi.Errors.VERSION_MISMATCH.getReference();
096
097    /** All fields are queryable, but the directory server may reject some requests (unindexed?). */
098    private static final String ALL_FIELDS = "*";
099
100
101    /** The resource ID. */
102    private final String id;
103    /** {@code true} if only sub-types of this resource can be created. */
104    private boolean isAbstract;
105    /** The ID of the super-type of this resource, may be {@code null}. */
106    private String superTypeId;
107    /** The LDAP object classes associated with this resource. */
108    private final Attribute objectClasses = new LinkedAttribute("objectClass");
109    /** The possibly empty set of sub-resources. */
110    private final Set<SubResource> subResources = new LinkedHashSet<>();
111    /** The set of property mappers associated with this resource, excluding inherited properties. */
112    private final Map<String, PropertyMapper> declaredProperties = new LinkedHashMap<>();
113    /** The set of property mappers associated with this resource, including inherited properties. */
114    private final Map<String, PropertyMapper> allProperties = new LinkedHashMap<>();
115    /**
116     * A JSON pointer to the primitive JSON property that will be used to convey type information. May be {@code
117     * null} if the type property is defined in a super type or if this resource does not have any sub-types.
118     */
119    private JsonPointer resourceTypeProperty;
120    /** Set to {@code true} once this Resource has been built. */
121    private boolean isBuilt = false;
122    /** The resolved super-type. */
123    private Resource superType;
124    /** The resolved sub-resources (only immediate children). */
125    private final Set<Resource> subTypes = new LinkedHashSet<>();
126    /** The property mapper which will map all properties for this resource including inherited properties. */
127    private final ObjectPropertyMapper propertyMapper = new ObjectPropertyMapper();
128    /** Routes requests to sub-resources. */
129    private final Router subResourceRouter = new Router();
130    private volatile Boolean hasSubTypesWithSubResources = null;
131    /** The set of actions supported by this resource and its sub-types. */
132    private final Set<Action> supportedActions = new HashSet<>();
133    private LocalizableMessage description;
134
135    Resource(final String id) {
136        this.id = id;
137    }
138
139    /**
140     * Sets the description of this resource.
141     *
142     * @param description
143     *          the description of this resource
144     */
145    public void description(LocalizableMessage description) {
146        this.description = description;
147    }
148
149    /**
150     * Returns the resource ID of this resource.
151     *
152     * @return The resource ID of this resource.
153     */
154    @Override
155    public String toString() {
156        return id;
157    }
158
159    /**
160     * Returns {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this
161     * resource.
162     *
163     * @param o
164     *         The object to compare.
165     * @return {@code true} if the provided parameter is a {@code Resource} having the same resource ID as this
166     * resource.
167     */
168    @Override
169    public boolean equals(final Object o) {
170        return this == o || (o instanceof Resource && id.equals(((Resource) o).id));
171    }
172
173    @Override
174    public int hashCode() {
175        return id.hashCode();
176    }
177
178    /**
179     * Specifies the resource ID of the resource which is a super-type of this resource. This resource will inherit
180     * the properties and sub-resources of the super-type, and may optionally override them.
181     *
182     * @param resourceId
183     *         The resource ID of the resource which is a super-type of this resource, or {@code null} if there is no
184     *         super-type.
185     * @return A reference to this object.
186     */
187    public Resource superType(final String resourceId) {
188        this.superTypeId = resourceId;
189        return this;
190    }
191
192    /**
193     * Specifies whether this resource is an abstract type and therefore cannot be created. Only non-abstract
194     * sub-types can be created.
195     *
196     * @param isAbstract
197     *         {@code true} if this resource is abstract.
198     * @return A reference to this object.
199     */
200    public Resource isAbstract(final boolean isAbstract) {
201        this.isAbstract = isAbstract;
202        return this;
203    }
204
205    /**
206     * Specifies a mapping for a property contained in this JSON resource. Properties are inherited and sub-types may
207     * override them. Properties are optional: a resource that does not have any properties cannot be created, read,
208     * or modified, and may only be used for accessing sub-resources. These resources usually represent API
209     * "endpoints".
210     *
211     * @param name
212     *         The name of the JSON property to be mapped.
213     * @param mapper
214     *         The property mapper responsible for mapping the JSON property to LDAP attribute(s).
215     * @return A reference to this object.
216     */
217    public Resource property(final String name, final PropertyMapper mapper) {
218        declaredProperties.put(name, mapper);
219        return this;
220    }
221
222    /**
223     * Specifies whether all LDAP user attributes should be mapped by default using the default schema based mapping
224     * rules. Individual attributes can be excluded using {@link #excludedDefaultUserAttributes} in order to prevent
225     * attributes with explicit mappings being mapped twice.
226     *
227     * @param include {@code true} if all LDAP user attributes be mapped by default.
228     * @return A reference to this object.
229     */
230    public Resource includeAllUserAttributesByDefault(final boolean include) {
231        propertyMapper.includeAllUserAttributesByDefault(include);
232        return this;
233    }
234
235    /**
236     * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
237     * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
238     * excluded in order to prevent duplication.
239     *
240     * @param attributeNames The list of attributes to be excluded.
241     * @return A reference to this object.
242     */
243    public Resource excludedDefaultUserAttributes(final String... attributeNames) {
244        return excludedDefaultUserAttributes(Arrays.asList(attributeNames));
245    }
246
247    /**
248     * Specifies zero or more user attributes which will be excluded from the default user attribute mappings when
249     * enabled using {@link #includeAllUserAttributesByDefault}. Attributes which have explicit mappings should be
250     * excluded in order to prevent duplication.
251     *
252     * @param attributeNames The list of attributes to be excluded.
253     * @return A reference to this object.
254     */
255    public Resource excludedDefaultUserAttributes(final Collection<String> attributeNames) {
256        propertyMapper.excludedDefaultUserAttributes(attributeNames);
257        return this;
258    }
259
260    /**
261     * Specifies the name of the JSON property which contains the resource's type, whose value is the
262     * resource ID. The resource type property is inherited by sub-types and must be available to any resources
263     * referenced from {@link SubResource sub-resources}.
264     *
265     * @param resourceTypeProperty
266     *         The name of the JSON property which contains the resource's type, or {@code null} if this resource does
267     *         not have a resource type property or if it should be inherited from a super-type.
268     * @return A reference to this object.
269     */
270    public Resource resourceTypeProperty(final JsonPointer resourceTypeProperty) {
271        this.resourceTypeProperty = resourceTypeProperty;
272        return this;
273    }
274
275    /**
276     * Specifies an LDAP object class which is to be associated with this resource. Multiple object classes may be
277     * specified. The object classes are used for determining the type of resource being accessed during all requests
278     * other than create. Object classes are inherited by sub-types and must be defined for any resources that are
279     * non-abstract and which can be created.
280     *
281     * @param objectClass
282     *         An LDAP object class associated with this resource's LDAP representation.
283     * @return A reference to this object.
284     */
285    public Resource objectClass(final String objectClass) {
286        this.objectClasses.add(objectClass);
287        return this;
288    }
289
290    /**
291     * Specifies LDAP object classes which are to be associated with this resource. Multiple object classes may be
292     * specified. The object classes are used for determining the type of resource being accessed during all requests
293     * other than create. Object classes are inherited by sub-types and must be defined for any resources that are
294     * non-abstract and which can be created.
295     *
296     * @param objectClasses
297     *         The LDAP object classes associated with this resource's LDAP representation.
298     * @return A reference to this object.
299     */
300    public Resource objectClasses(final String... objectClasses) {
301        this.objectClasses.add((Object[]) objectClasses);
302        return this;
303    }
304
305    /**
306     * Registers an action which should be supported by this resource. By default, no actions are supported.
307     *
308     * @param action
309     *         The action supported by this resource.
310     * @return A reference to this object.
311     */
312    public Resource supportedAction(final Action action) {
313        this.supportedActions.add(action);
314        return this;
315    }
316
317    /**
318     * Registers zero or more actions which should be supported by this resource. By default, no actions are supported.
319     *
320     * @param actions
321     *         The actions supported by this resource.
322     * @return A reference to this object.
323     */
324    public Resource supportedActions(final Action... actions) {
325        this.supportedActions.addAll(Arrays.asList(actions));
326        return this;
327    }
328
329    /**
330     * Specifies a parent-child relationship with another resource. Sub-resources are inherited by sub-types and may
331     * be overridden.
332     *
333     * @param subResource
334     *         The sub-resource definition.
335     * @return A reference to this object.
336     */
337    public Resource subResource(final SubResource subResource) {
338        this.subResources.add(subResource);
339        return this;
340    }
341
342    /**
343     * Specifies a parent-child relationship with zero or more resources. Sub-resources are inherited by sub-types and
344     * may be overridden.
345     *
346     * @param subResources
347     *         The sub-resource definitions.
348     * @return A reference to this object.
349     */
350    public Resource subResources(final SubResource... subResources) {
351        this.subResources.addAll(asList(subResources));
352        return this;
353    }
354
355    boolean hasSupportedAction(final Action action) {
356        return supportedActions.contains(action);
357    }
358
359    boolean hasSubTypes() {
360        return !subTypes.isEmpty();
361    }
362
363    boolean mayHaveSubResources() {
364        return !subResources.isEmpty() || hasSubTypesWithSubResources();
365    }
366
367    boolean hasSubTypesWithSubResources() {
368        if (hasSubTypesWithSubResources == null) {
369            for (final Resource subType : subTypes) {
370                if (!subType.subResources.isEmpty() || subType.hasSubTypesWithSubResources()) {
371                    hasSubTypesWithSubResources = true;
372                    return true;
373                }
374            }
375            hasSubTypesWithSubResources = false;
376        }
377        return hasSubTypesWithSubResources;
378    }
379
380    Set<Resource> getSubTypes() {
381        return subTypes;
382    }
383
384    Resource resolveSubTypeFromJson(final JsonValue content) throws ResourceException {
385        if (!hasSubTypes()) {
386            // The resource type is implied because this resource does not have sub-types. In particular, resources
387            // are not required to have type information if they don't have sub-types.
388            return this;
389        }
390        final JsonValue jsonType = content.get(resourceTypeProperty);
391        if (jsonType == null || !jsonType.isString()) {
392            throw newBadRequestException(ERR_MISSING_TYPE_PROPERTY_IN_CREATE.get(resourceTypeProperty));
393        }
394        final String type = jsonType.asString();
395        final Resource subType = resolveSubTypeFromString(type);
396        if (subType == null) {
397            throw newBadRequestException(ERR_UNRECOGNIZED_TYPE_IN_CREATE.get(type, getAllowedResourceTypes()));
398        }
399        if (subType.isAbstract) {
400            throw newBadRequestException(ERR_ABSTRACT_TYPE_IN_CREATE.get(type, getAllowedResourceTypes()));
401        }
402        return subType;
403    }
404
405    private String getAllowedResourceTypes() {
406        final List<String> allowedTypes = new ArrayList<>();
407        getAllowedResourceTypes(allowedTypes);
408        return joinAsString(", ", allowedTypes);
409    }
410
411    private void getAllowedResourceTypes(final List<String> allowedTypes) {
412        if (!isAbstract) {
413            allowedTypes.add(id);
414        }
415        for (final Resource subType : subTypes) {
416            subType.getAllowedResourceTypes(allowedTypes);
417        }
418    }
419
420    Resource resolveSubTypeFromString(final String type) {
421        if (id.equalsIgnoreCase(type)) {
422            return this;
423        }
424        for (final Resource subType : subTypes) {
425            final Resource resolvedSubType = subType.resolveSubTypeFromString(type);
426            if (resolvedSubType != null) {
427                return resolvedSubType;
428            }
429        }
430        return null;
431    }
432
433    Resource resolveSubTypeFromObjectClasses(final Entry entry) {
434        if (!hasSubTypes()) {
435            // This resource does not have sub-types.
436            return this;
437        }
438        final Attribute objectClassesFromEntry = entry.getAttribute("objectClass");
439        final Resource subType = resolveSubTypeFromObjectClasses(objectClassesFromEntry);
440        if (subType == null) {
441            // Best effort.
442            return this;
443        }
444        return subType;
445    }
446
447    private Resource resolveSubTypeFromObjectClasses(final Attribute objectClassesFromEntry) {
448        if (!objectClassesFromEntry.containsAll(objectClasses)) {
449            return null;
450        }
451        // This resource is a potential match, but sub-types may be better.
452        for (final Resource subType : subTypes) {
453            final Resource resolvedSubType = subType.resolveSubTypeFromObjectClasses(objectClassesFromEntry);
454            if (resolvedSubType != null) {
455                return resolvedSubType;
456            }
457        }
458        return this;
459    }
460
461    Attribute getObjectClassAttribute() {
462        return objectClasses;
463    }
464
465    RequestHandler getSubResourceRouter() {
466        return subResourceRouter;
467    }
468
469    String getResourceId() {
470        return id;
471    }
472
473    void build(final Rest2Ldap rest2Ldap) {
474        // Prevent re-entrant calls.
475        if (isBuilt) {
476            return;
477        }
478        isBuilt = true;
479
480        if (superTypeId != null) {
481            superType = rest2Ldap.getResource(superTypeId);
482            if (superType == null) {
483                throw new LocalizedIllegalArgumentException(ERR_UNRECOGNIZED_RESOURCE_SUPER_TYPE.get(id, superTypeId));
484            }
485            // Inherit content from super-type.
486            superType.build(rest2Ldap);
487            superType.subTypes.add(this);
488            if (resourceTypeProperty == null) {
489                resourceTypeProperty = superType.resourceTypeProperty;
490            }
491            objectClasses.addAll(superType.objectClasses);
492            subResourceRouter.addAllRoutes(superType.subResourceRouter);
493            allProperties.putAll(superType.allProperties);
494        }
495        allProperties.putAll(declaredProperties);
496        for (final Map.Entry<String, PropertyMapper> property : allProperties.entrySet()) {
497            propertyMapper.property(property.getKey(), property.getValue());
498        }
499        for (final SubResource subResource : subResources) {
500            subResource.build(rest2Ldap, id);
501            subResource.addRoutes(subResourceRouter);
502        }
503    }
504
505    PropertyMapper getPropertyMapper() {
506        return propertyMapper;
507    }
508
509    /**
510     * Returns the api description that describes a single instance resource.
511     *
512     * @param isReadOnly
513     *          whether the associated resource is read only
514     * @return a new api description that describes a single instance resource.
515     */
516    ApiDescription instanceApi(boolean isReadOnly) {
517        if (allProperties.isEmpty() && superType == null && subTypes.isEmpty()) {
518            // It is not used in the api description
519            // so do not generate anything for this resource
520            return null;
521        }
522
523        org.forgerock.api.models.Resource.Builder resource = org.forgerock.api.models.Resource.
524            resource()
525            .title(id)
526            .description(toLS(description))
527            .resourceSchema(schemaRef("#/definitions/" + id))
528            .mvccSupported(isMvccSupported());
529
530        resource.read(readOperation());
531        if (!isReadOnly) {
532            resource.update(updateOperation());
533            resource.patch(patchOperation());
534            for (Action action : supportedActions) {
535                resource.action(actions(action));
536            }
537        }
538
539        return ApiDescription.apiDescription()
540                      .id("unused").version("unused")
541                      .definitions(definitions())
542                      .services(services(resource))
543                      .paths(paths())
544                      .errors(errors())
545                      .build();
546    }
547
548    /**
549     * Returns the api description that describes a collection resource.
550     *
551     * @param isReadOnly
552     *          whether the associated resource is read only
553     * @return a new api description that describes a collection resource.
554     */
555    ApiDescription collectionApi(boolean isReadOnly) {
556        org.forgerock.api.models.Resource.Builder resource = org.forgerock.api.models.Resource.
557            resource()
558            .title(id)
559            .description(toLS(description))
560            .resourceSchema(schemaRef("#/definitions/" + id))
561            .mvccSupported(isMvccSupported());
562
563        resource.items(buildItems(isReadOnly));
564        resource.create(createOperation(CreateMode.ID_FROM_SERVER));
565        resource.query(Query.query()
566                           .stability(EVOLVING)
567                           .type(QueryType.FILTER)
568                           .queryableFields(ALL_FIELDS)
569                           .pagingModes(COOKIE, OFFSET)
570                           .countPolicies(NONE)
571                           .error(errorRef(ERROR_BAD_REQUEST))
572                           .error(errorRef(ERROR_UNAUTHORIZED))
573                           .error(errorRef(ERROR_FORBIDDEN))
574                           .error(errorRef(ERROR_REQUEST_TIMEOUT))
575                           .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
576                           .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
577                           .error(errorRef(ERROR_UNAVAILABLE))
578                           .build());
579
580        return ApiDescription.apiDescription()
581                             .id("unused").version("unused")
582                             .definitions(definitions())
583                             .services(services(resource))
584                             .paths(paths())
585                             .errors(errors())
586                             .build();
587    }
588
589    private Services services(org.forgerock.api.models.Resource.Builder resource) {
590        return Services.services()
591                       .put(id, resource.build())
592                       .build();
593    }
594
595    private Paths paths() {
596        return Paths.paths()
597                     // do not put anything in the path to avoid unfortunate string concatenation
598                     // also use UNVERSIONED and rely on the router to stamp the version
599                     .put("", versionedPath().put(UNVERSIONED, resourceRef("#/services/" + id)).build())
600                     .build();
601    }
602
603    private Definitions definitions() {
604        final Definitions.Builder definitions = Definitions.definitions();
605        for (Resource res : collectTypeHierarchy(this)) {
606            definitions.put(res.id, res.buildJsonSchema());
607        }
608        return definitions.build();
609    }
610
611    private static Iterable<Resource> collectTypeHierarchy(Resource currentType) {
612        final List<Resource> typeHierarchy = new ArrayList<>();
613
614        Resource ancestorType = currentType;
615        while (ancestorType.superType != null) {
616            ancestorType = ancestorType.superType;
617            typeHierarchy.add(ancestorType);
618        }
619
620        typeHierarchy.add(currentType);
621
622        addSubTypes(typeHierarchy, currentType);
623        return typeHierarchy;
624    }
625
626    private static void addSubTypes(final List<Resource> typeHierarchy, Resource res) {
627        for (Resource subType : res.subTypes) {
628            typeHierarchy.add(subType);
629            addSubTypes(typeHierarchy, subType);
630        }
631    }
632
633    private LocalizableString toLS(LocalizableMessage msg) {
634        if (msg != null) {
635            // FIXME this code does cannot work today because LocalizableMessage.getKey() does not exist
636            //if (msg.resourceName() != null) {
637            //    return new LocalizableString("i18n:" + msg.resourceName() + "#" + msg.getKey());
638            //}
639            return new LocalizableString(msg.toString());
640        }
641        return null;
642    }
643
644    /**
645     * Returns the api description that describes a resource with sub resources.
646     *
647     * @param producer
648     *          the api producer
649     * @return a new api description that describes a resource with sub resources.
650     */
651    ApiDescription subResourcesApi(ApiProducer<ApiDescription> producer) {
652        return subResourceRouter.api(producer);
653    }
654
655    private boolean isMvccSupported() {
656        return allProperties.containsKey("_rev");
657    }
658
659    private Items buildItems(boolean isReadOnly) {
660        final Items.Builder builder = Items.items();
661        builder.pathParameter(Parameter
662                              .parameter()
663                              .name("id")
664                              .type("string")
665                              .source(PATH)
666                              .required(true)
667                              .build())
668               .read(readOperation());
669        if (!isReadOnly) {
670            builder.create(createOperation(CreateMode.ID_FROM_CLIENT));
671            builder.update(updateOperation());
672            builder.delete(deleteOperation());
673            builder.patch(patchOperation());
674            for (Action action : supportedActions) {
675                builder.action(actions(action));
676            }
677        }
678        return builder.build();
679    }
680
681    private org.forgerock.api.models.Action actions(Action action) {
682        switch (action) {
683        case MODIFY_PASSWORD:
684            return modifyPasswordAction();
685        case RESET_PASSWORD:
686            return resetPasswordAction();
687        default:
688            throw new RuntimeException("Not implemented for action " + action);
689        }
690    }
691
692    private static Create createOperation(CreateMode createMode) {
693        return Create.create()
694                     .stability(EVOLVING)
695                     .mode(createMode)
696                     .error(errorRef(ERROR_BAD_REQUEST))
697                     .error(errorRef(ERROR_UNAUTHORIZED))
698                     .error(errorRef(ERROR_FORBIDDEN))
699                     .error(errorRef(ERROR_NOT_FOUND))
700                     .error(errorRef(ERROR_REQUEST_TIMEOUT))
701                     .error(errorRef(ERROR_VERSION_MISMATCH))
702                     .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
703                     .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
704                     .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
705                     .error(errorRef(ERROR_UNAVAILABLE))
706                     .build();
707    }
708
709    private static Delete deleteOperation() {
710        return Delete.delete()
711                     .stability(EVOLVING)
712                     .error(errorRef(ERROR_BAD_REQUEST))
713                     .error(errorRef(ERROR_UNAUTHORIZED))
714                     .error(errorRef(ERROR_FORBIDDEN))
715                     .error(errorRef(ERROR_NOT_FOUND))
716                     .error(errorRef(ERROR_REQUEST_TIMEOUT))
717                     .error(errorRef(ERROR_VERSION_MISMATCH))
718                     .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
719                     .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
720                     .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
721                     .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
722                     .error(errorRef(ERROR_UNAVAILABLE))
723                     .build();
724    }
725
726    private static Patch patchOperation() {
727        return Patch.patch()
728                    .stability(EVOLVING)
729                    .operations(ADD, REMOVE, REPLACE, INCREMENT)
730                    .error(errorRef(ERROR_BAD_REQUEST))
731                    .error(errorRef(ERROR_UNAUTHORIZED))
732                    .error(errorRef(ERROR_FORBIDDEN))
733                    .error(errorRef(ERROR_NOT_FOUND))
734                    .error(errorRef(ERROR_REQUEST_TIMEOUT))
735                    .error(errorRef(ERROR_VERSION_MISMATCH))
736                    .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
737                    .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
738                    .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
739                    .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
740                    .error(errorRef(ERROR_UNAVAILABLE))
741                    .build();
742    }
743
744    private static Read readOperation() {
745        return Read.read()
746                   .stability(EVOLVING)
747                   .error(errorRef(ERROR_BAD_REQUEST))
748                   .error(errorRef(ERROR_UNAUTHORIZED))
749                   .error(errorRef(ERROR_FORBIDDEN))
750                   .error(errorRef(ERROR_NOT_FOUND))
751                   .error(errorRef(ERROR_REQUEST_TIMEOUT))
752                   .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
753                   .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
754                   .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
755                   .error(errorRef(ERROR_UNAVAILABLE))
756                   .build();
757    }
758
759    private static Update updateOperation() {
760        return Update.update()
761                     .stability(EVOLVING)
762                     .error(errorRef(ERROR_BAD_REQUEST))
763                     .error(errorRef(ERROR_UNAUTHORIZED))
764                     .error(errorRef(ERROR_FORBIDDEN))
765                     .error(errorRef(ERROR_NOT_FOUND))
766                     .error(errorRef(ERROR_REQUEST_TIMEOUT))
767                     .error(errorRef(ERROR_VERSION_MISMATCH))
768                     .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
769                     .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
770                     .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
771                     .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
772                     .error(errorRef(ERROR_UNAVAILABLE))
773                     .build();
774    }
775
776    private static org.forgerock.api.models.Action modifyPasswordAction() {
777        return org.forgerock.api.models.Action.action()
778               .stability(EVOLVING)
779               .name("modifyPassword")
780               .request(passwordModifyRequest())
781               .description("Modify a user password. This action requires HTTPS.")
782               .error(errorRef(ERROR_BAD_REQUEST))
783               .error(errorRef(ERROR_UNAUTHORIZED))
784               .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS))
785               .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION))
786               .error(errorRef(ERROR_FORBIDDEN))
787               .error(errorRef(ERROR_NOT_FOUND))
788               .error(errorRef(ERROR_REQUEST_TIMEOUT))
789               .error(errorRef(ERROR_VERSION_MISMATCH))
790               .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
791               .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
792               .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
793               .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
794               .error(errorRef(ERROR_UNAVAILABLE))
795               .build();
796    }
797
798    private static org.forgerock.api.models.Schema passwordModifyRequest() {
799        final JsonValue jsonSchema = json(object(
800            field("type", "object"),
801            field("description", "Supply the old password and new password."),
802            field("required", array("oldPassword", "newPassword")),
803            field("properties", object(
804                field("oldPassword", object(
805                    field("type", "string"),
806                    field("name", "Old Password"),
807                    field("description", "Current password as a UTF-8 string."),
808                    field("format", "password"))),
809                field("newPassword", object(
810                    field("type", "string"),
811                    field("name", "New Password"),
812                    field("description", "New password as a UTF-8 string."),
813                    field("format", "password")))))));
814        return schema(jsonSchema);
815    }
816
817    private static org.forgerock.api.models.Action resetPasswordAction() {
818        return org.forgerock.api.models.Action.action()
819               .stability(EVOLVING)
820               .name("resetPassword")
821               .response(resetPasswordResponse())
822               .description("Reset a user password to a generated value. This action requires HTTPS.")
823               .error(errorRef(ERROR_BAD_REQUEST))
824               .error(errorRef(ERROR_UNAUTHORIZED))
825               .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_HTTPS))
826               .error(errorRef(ERROR_PASSWORD_MODIFY_REQUIRES_AUTHENTICATION))
827               .error(errorRef(ERROR_FORBIDDEN))
828               .error(errorRef(ERROR_NOT_FOUND))
829               .error(errorRef(ERROR_REQUEST_TIMEOUT))
830               .error(errorRef(ERROR_VERSION_MISMATCH))
831               .error(errorRef(ERROR_REQUEST_ENTITY_TOO_LARGE))
832               .error(errorRef(ERROR_READ_FOUND_MULTIPLE_ENTRIES))
833               .error(errorRef(ERROR_ADMIN_LIMIT_EXCEEDED))
834               .error(errorRef(ERROR_INTERNAL_SERVER_ERROR))
835               .error(errorRef(ERROR_UNAVAILABLE))
836               .build();
837    }
838
839    private static org.forgerock.api.models.Schema resetPasswordResponse() {
840        final JsonValue jsonSchema = json(object(
841            field("type", "object"),
842            field("properties", object(
843                field("generatedPassword", object(
844                    field("type", "string"),
845                    field("description", "Generated password to communicate to the user.")))))));
846        return schema(jsonSchema);
847    }
848
849    private Schema buildJsonSchema() {
850        final List<String> requiredFields = new ArrayList<>();
851        JsonValue properties = json(JsonValue.object());
852        for (Map.Entry<String, PropertyMapper> prop : declaredProperties.entrySet()) {
853            final String propertyName = prop.getKey();
854            final PropertyMapper mapper = prop.getValue();
855            if (mapper.isRequired()) {
856                requiredFields.add(propertyName);
857            }
858            final JsonValue jsonSchema = mapper.toJsonSchema();
859            if (jsonSchema != null) {
860                properties.put(propertyName, jsonSchema.getObject());
861            }
862        }
863
864        final JsonValue jsonSchema = json(object(field("type", "object")));
865        final String discriminator = getDiscriminator();
866        if (discriminator != null) {
867            jsonSchema.put("discriminator", discriminator);
868        }
869        if (!requiredFields.isEmpty()) {
870            jsonSchema.put("required", requiredFields);
871        }
872        if (properties.size() > 0) {
873            jsonSchema.put("properties", properties.getObject());
874        }
875
876        if (superType != null) {
877            return schema(json(object(
878                field("allOf", array(
879                    object(field("$ref", "#/definitions/" + superType.id)),
880                    jsonSchema.getObject())))));
881        }
882        return schema(jsonSchema);
883    }
884
885    private String getDiscriminator() {
886        if (resourceTypeProperty != null) {
887            // Subtypes inherit the resourceTypeProperty from their parent.
888            // The discriminator must only be output for the type that defined it.
889            final String propertyName = resourceTypeProperty.leaf();
890            return declaredProperties.containsKey(propertyName) ? propertyName : null;
891        }
892        return null;
893    }
894
895    private Errors errors() {
896        return Errors
897            .errors()
898            .put("passwordModifyRequiresHttps",
899                error(FORBIDDEN, "Password modify requires a secure connection."))
900            .put("passwordModifyRequiresAuthn",
901                error(FORBIDDEN, "Password modify requires user to be authenticated."))
902            .put("readFoundMultipleEntries",
903                error(INTERNAL_ERROR, "Multiple entries where found when trying to read a single entry."))
904            .put("adminLimitExceeded",
905                error(INTERNAL_ERROR, "The request exceeded an administrative limit."))
906            .build();
907    }
908
909    static ApiError error(int code, String description) {
910        return ApiError.apiError().code(code).description(description).build();
911    }
912
913    static ApiError error(int code, LocalizableString description) {
914        return ApiError.apiError().code(code).description(description).build();
915    }
916
917    static ApiError errorRef(String referenceValue) {
918        return ApiError.apiError().reference(ref(referenceValue)).build();
919    }
920
921    static org.forgerock.api.models.Resource resourceRef(String referenceValue) {
922        return org.forgerock.api.models.Resource.resource().reference(ref(referenceValue)).build();
923    }
924
925    static org.forgerock.api.models.Schema schemaRef(String referenceValue) {
926        return Schema.schema().reference(ref(referenceValue)).build();
927    }
928
929    private static org.forgerock.api.models.Schema schema(JsonValue jsonSchema) {
930        return Schema.schema().schema(jsonSchema).build();
931    }
932
933    static Reference ref(String referenceValue) {
934        return Reference.reference().value(referenceValue).build();
935    }
936}