Resource.java

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2016 ForgeRock AS.
 */

package org.forgerock.api.models;

import static org.forgerock.api.models.Reference.reference;
import static org.forgerock.api.util.ValidationUtil.isEmpty;
import static org.forgerock.util.Reject.rejectStateIfTrue;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

import org.forgerock.api.ApiValidationException;
import org.forgerock.api.annotations.Actions;
import org.forgerock.api.annotations.CollectionProvider;
import org.forgerock.api.annotations.Handler;
import org.forgerock.api.annotations.Queries;
import org.forgerock.api.annotations.RequestHandler;
import org.forgerock.api.annotations.SingletonProvider;
import org.forgerock.util.Reject;
import org.forgerock.util.i18n.LocalizableString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;

/**
 * Class that represents the Resource type in API descriptor.
 * <p>
 *     {@code Resource}s may be either a reference to another {@code Resource} that will be defined elsewhere in the
 *     API Descriptor, or a described resource. If a {@link Reference} is provided, then none of the other fields may
 *     be used, and if any of the other fields are used, a reference may not be provided.
 * </p>
 */
@JsonDeserialize(builder = Resource.Builder.class)
@JsonInclude(JsonInclude.Include.NON_NULL)
public final class Resource {
    private static final Logger LOGGER = LoggerFactory.getLogger(Resource.class);
    private static final String SERVICES_REFERENCE = "#/services/%s";

    @JsonProperty("$ref")
    private final Reference reference;
    private final Schema resourceSchema;
    private final LocalizableString title;
    private final LocalizableString description;
    private final Create create;
    private final Read read;
    private final Update update;
    private final Delete delete;
    private final Patch patch;
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private final Action[] actions;
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private final Query[] queries;
    private final SubResources subresources;
    private final Items items;
    private final Boolean mvccSupported;
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private final Parameter[] parameters;

    private Resource(Builder builder) {
        this.reference = builder.reference;
        this.resourceSchema = builder.resourceSchema;
        this.title = builder.title;
        this.description = builder.description;
        this.create = builder.create;
        this.read = builder.read;
        this.update = builder.update;
        this.delete = builder.delete;
        this.patch = builder.patch;
        this.subresources = builder.subresources;
        this.actions = builder.actions.toArray(new Action[builder.actions.size()]);
        this.queries = builder.queries.toArray(new Query[builder.queries.size()]);
        this.items = builder.items;
        this.mvccSupported = builder.mvccSupported;
        this.parameters = builder.parameters.toArray(new Parameter[builder.parameters.size()]);

        if ((create != null || read != null || update != null || delete != null || patch != null
                || !isEmpty(actions) || !isEmpty(queries)) && reference != null) {
            throw new ApiValidationException("Cannot have a reference as well as operations");
        }
        if (mvccSupported == null && reference == null) {
            throw new ApiValidationException("mvccSupported required for non-reference Resources");
        }
    }

    /**
     * Getter of resource schema.
     *
     * @return Resource schema
     */
    public Schema getResourceSchema() {
        return resourceSchema;
    }

    /**
     * Getter of title.
     *
     * @return Title
     */
    public LocalizableString getTitle() {
        return title;
    }

    /**
     * Getter of description.
     *
     * @return Description
     */
    public LocalizableString getDescription() {
        return description;
    }

    /**
     * Getter of Create.
     *
     * @return Create
     */
    public Create getCreate() {
        return create;
    }

    /**
     * Getter of Read.
     *
     * @return Read
     */
    public Read getRead() {
        return read;
    }

    /**
     * Getter of Update.
     *
     * @return Update
     */
    public Update getUpdate() {
        return update;
    }

    /**
     * Getter of Delete.
     *
     * @return Delete
     */
    public Delete getDelete() {
        return delete;
    }

    /**
     * Getter of Patch.
     *
     * @return Patch
     */
    public Patch getPatch() {
        return patch;
    }

    /**
     * Getter of actions.
     *
     * @return Actions
     */
    public Action[] getActions() {
        return actions;
    }

    /**
     * Getter of queries.
     *
     * @return Queries
     */
    public Query[] getQueries() {
        return queries;
    }

    /**
     * Getter of sub-resources.
     *
     * @return Sub-resources
     */
    public SubResources getSubresources() {
        return subresources;
    }

    /**
     * Gets the reference.
     * @return The reference.
     */
    public Reference getReference() {
        return reference;
    }

    /**
     * Getter of items.
     *
     * @return Items
     */
    public Items getItems() {
        return items;
    }

    /**
     * Informs if MVCC is supported.
     *
     * @return {@code true} if MVCC is supported and {@code false} otherwise
     */
    public Boolean isMvccSupported() {
        return mvccSupported;
    }

    /**
     * Getter of the parameters array.
     *
     * @return Parameters
     */
    public Parameter[] getParameters() {
        return parameters;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Resource resource = (Resource) o;
        return Objects.equals(reference, resource.reference)
                && Objects.equals(resourceSchema, resource.resourceSchema)
                && Objects.equals(title, resource.title)
                && Objects.equals(description, resource.description)
                && Objects.equals(create, resource.create)
                && Objects.equals(read, resource.read)
                && Objects.equals(update, resource.update)
                && Objects.equals(delete, resource.delete)
                && Objects.equals(patch, resource.patch)
                && Arrays.equals(actions, resource.actions)
                && Arrays.equals(queries, resource.queries)
                && Objects.equals(subresources, resource.subresources)
                && Objects.equals(items, resource.items)
                && Objects.equals(mvccSupported, resource.mvccSupported)
                && Arrays.equals(parameters, resource.parameters);
    }

    @Override
    public int hashCode() {
        return Objects.hash(reference, resourceSchema, title, description, create, read, update, delete, patch, actions,
                queries, subresources, items, mvccSupported, parameters);
    }

    /**
     * Create a new Builder for Resoruce.
     *
     * @return Builder
     */
    public static Builder resource() {
        return new Builder();
    }

    /**
     * Build a {@code Resource} from an annotated request handler.
     * @param type The annotated type.
     * @param variant The annotated type variant.
     * @param descriptor The root descriptor to add definitions to.
     * @return The built {@code Resource} object.
     */
    public static Resource fromAnnotatedType(Class<?> type, AnnotatedTypeVariant variant, ApiDescription descriptor) {
        return fromAnnotatedType(type, variant, null, null, descriptor);
    }

    /**
     * Build a {@code Resource} from an annotated request handler.
     * @param type The annotated type.
     * @param variant The annotated type variant.
     * @param subResources The sub resources object to be included, if any sub-resources exist, or null.
     * @param descriptor The root descriptor to add definitions to.
     * @param extraParameters Extra parameters not from the resource annotation.
     * @return The built {@code Resource} object.
     */
    public static Resource fromAnnotatedType(Class<?> type, AnnotatedTypeVariant variant, SubResources subResources,
            ApiDescription descriptor, Parameter... extraParameters) {
        return fromAnnotatedType(type, variant, subResources, null, descriptor, extraParameters);
    }

    /**
     * Build a {@code Resource} from an annotated request handler.
     * @param type The annotated type.
     * @param variant The annotated type variant.
     * @param items The items definition for a collection variant, or null.
     * @param descriptor The root descriptor to add definitions to.
     * @param extraParameters Extra parameters not from the resource annotation.
     * @return The built {@code Resource} object.
     */
    public static Resource fromAnnotatedType(Class<?> type, AnnotatedTypeVariant variant,
            Items items, ApiDescription descriptor, Parameter... extraParameters) {
        return fromAnnotatedType(type, variant, null, items, descriptor, extraParameters);
    }

    private static Resource fromAnnotatedType(Class<?> type, AnnotatedTypeVariant variant,
            SubResources subResources, Items items, ApiDescription descriptor, Parameter... extraParameters) {
        Builder builder = resource();
        Handler handler = findHandlerAnnotation(variant, type);
        if (handler == null) {
            return null;
        }
        boolean foundCrudpq = false;
        for (Method m : type.getMethods()) {
            boolean instanceMethod = Arrays.asList(m.getParameterTypes()).indexOf(String.class) > -1;
            org.forgerock.api.annotations.Action action = m.getAnnotation(org.forgerock.api.annotations.Action.class);
            if (action != null && instanceMethod == variant.actionRequiresId) {
                builder.actions.add(Action.fromAnnotation(action, m, descriptor, type));
            }
            Actions actions = m.getAnnotation(Actions.class);
            if (actions != null && instanceMethod == variant.actionRequiresId) {
                for (org.forgerock.api.annotations.Action a : actions.value()) {
                    builder.actions.add(Action.fromAnnotation(a, null, descriptor, type));
                }
            }
            org.forgerock.api.annotations.Create create = m.getAnnotation(org.forgerock.api.annotations.Create.class);
            if (create != null) {
                builder.create = Create.fromAnnotation(create, variant.instanceCreate, descriptor, type);
                foundCrudpq = true;
            }
            if (variant.rudpOperations) {
                org.forgerock.api.annotations.Read read = m.getAnnotation(org.forgerock.api.annotations.Read.class);
                if (read != null) {
                    builder.read = Read.fromAnnotation(read, descriptor, type);
                    foundCrudpq = true;
                }
                org.forgerock.api.annotations.Update update =
                        m.getAnnotation(org.forgerock.api.annotations.Update.class);
                if (update != null) {
                    builder.update = Update.fromAnnotation(update, descriptor, type);
                    foundCrudpq = true;
                }
                org.forgerock.api.annotations.Delete delete =
                        m.getAnnotation(org.forgerock.api.annotations.Delete.class);
                if (delete != null) {
                    builder.delete = Delete.fromAnnotation(delete, descriptor, type);
                    foundCrudpq = true;
                }
                org.forgerock.api.annotations.Patch patch = m.getAnnotation(org.forgerock.api.annotations.Patch.class);
                if (patch != null) {
                    builder.patch = Patch.fromAnnotation(patch, descriptor, type);
                    foundCrudpq = true;
                }
            }
            if (variant.queryOperations) {
                org.forgerock.api.annotations.Query query = m.getAnnotation(org.forgerock.api.annotations.Query.class);
                if (query != null) {
                    builder.queries.add(Query.fromAnnotation(query, m, descriptor, type));
                    foundCrudpq = true;
                }
                Queries queries = m.getAnnotation(Queries.class);
                if (queries != null) {
                    for (org.forgerock.api.annotations.Query q : queries.value()) {
                        builder.queries.add(Query.fromAnnotation(q, null, descriptor, type));
                        foundCrudpq = true;
                    }
                }
            }
        }
        Schema resourceSchema = Schema.fromAnnotation(handler.resourceSchema(), descriptor, type);
        if (foundCrudpq && resourceSchema == null) {
            throw new IllegalArgumentException("CRUDPQ operation(s) defined, but no resource schema declared");
        }

        for (org.forgerock.api.annotations.Parameter parameter : handler.parameters()) {
            builder.parameter(Parameter.fromAnnotation(type, parameter));
        }
        for (Parameter param : extraParameters) {
            builder.parameter(param);
        }

        Resource resource = builder.resourceSchema(resourceSchema)
                .mvccSupported(handler.mvccSupported())
                .title(new LocalizableString(handler.title(), type))
                .description(new LocalizableString(handler.description(), type))
                .subresources(subResources)
                .items(items)
                .build();

        if (!handler.id().isEmpty()) {
            descriptor.addService(handler.id(), resource);
            Reference reference = reference().value(String.format(SERVICES_REFERENCE, handler.id())).build();
            resource = resource().reference(reference).build();
        }
        return resource;
    }

    private static Handler findHandlerAnnotation(AnnotatedTypeVariant variant, Class<?> type) {
        switch (variant) {
        case SINGLETON_RESOURCE:
            if (type.getAnnotation(SingletonProvider.class) != null) {
                return type.getAnnotation(SingletonProvider.class).value();
            }
            break;
        case REQUEST_HANDLER:
            if (type.getAnnotation(RequestHandler.class) != null) {
                return type.getAnnotation(RequestHandler.class).value();
            }
            break;
        default:
            if (type.getAnnotation(CollectionProvider.class) != null) {
                return type.getAnnotation(CollectionProvider.class).details();
            }
        }
        LOGGER.info("Asked for Resource for annotated type, but type does not have required RequestHandler"
                + " annotation. No api descriptor will be available for " + type);
        return null;
    }

    /**
     * The variant of the annotated type. Allows the annotation processing to make assumptions about what type of
     * operations are expected from this context of the type.
     */
    public enum AnnotatedTypeVariant {
        /** A singleton resource handler. (expect RUDPA operations). */
        SINGLETON_RESOURCE(true, true, false, false),
        /** A collection resource handler, collection endpoint (expect CAQ opererations). */
        COLLECTION_RESOURCE_COLLECTION(false, false, false, true),
        /** A collection resource handler, instance endpoint (expect CRUDPA operations). */
        COLLECTION_RESOURCE_INSTANCE(true, true, true, false),
        /** A plain request handler (expects all operations). */
        REQUEST_HANDLER(false, true, false, true);

        private final boolean instanceCreate;
        private final boolean rudpOperations;
        private final boolean actionRequiresId;
        private final boolean queryOperations;

        AnnotatedTypeVariant(boolean instanceCreate, boolean rudpOperations, boolean actionRequiresId,
                boolean queryOperations) {
            this.instanceCreate = instanceCreate;
            this.rudpOperations = rudpOperations;
            this.actionRequiresId = actionRequiresId;
            this.queryOperations = queryOperations;
        }
    }

    /**
     * Builder to help construct the Resource.
     */
    public final static class Builder {
        private Schema resourceSchema;
        private LocalizableString title;
        private LocalizableString description;
        private Create create;
        private Read read;
        private Update update;
        private Delete delete;
        private Patch patch;
        private SubResources subresources;
        private final Set<Action> actions;
        private final Set<Query> queries;
        private Items items;
        private Boolean mvccSupported;
        private Reference reference;
        private final List<Parameter> parameters;
        private boolean built = false;

        /**
         * Private default constructor.
         */
        protected Builder() {
            actions = new TreeSet<>();
            queries = new TreeSet<>();
            parameters = new ArrayList<>();
        }

        /**
         * Set a reference.
         * @param reference The reference.
         * @return This builder.
         */
        @JsonProperty("$ref")
        public Builder reference(Reference reference) {
            checkState();
            this.reference = reference;
            return this;
        }

        /**
         * Set the resource schema.
         *
         * @param resourceSchema The schema of the resource for this path.
         * Required when any of create, read, update, delete, patch are supported
         * @return Builder
         */
        @JsonProperty("resourceSchema")
        public Builder resourceSchema(Schema resourceSchema) {
            checkState();
            this.resourceSchema = resourceSchema;
            return this;
        }

        /**
         * Set the title.
         *
         * @param title Title of the endpoint
         * @return Builder
         */
        public Builder title(LocalizableString title) {
            this.title = title;
            return this;
        }

        /**
         * Set the title.
         *
         * @param title Title of the endpoint
         * @return Builder
         */
        @JsonProperty("title")
        public Builder title(String title) {
            return title(new LocalizableString(title));
        }

        /**
         * Set the description.
         *
         * @param description A description of the endpoint
         * @return Builder
         */
        public Builder description(LocalizableString description) {
            checkState();
            this.description = description;
            return this;
        }

        /**
         * Set the description.
         *
         * @param description A description of the endpoint
         * @return Builder
         */
        @JsonProperty("description")
        public Builder description(String description) {
            checkState();
            return description(new LocalizableString(description));
        }

        /**
         * Set create.
         *
         * @param create The create operation description, if supported
         * @return Builder
         */
        @JsonProperty("create")
        public Builder create(Create create) {
            checkState();
            this.create = create;
            return this;
        }

        /**
         * Set Read.
         *
         * @param read The read operation description, if supported
         * @return Builder
         */
        @JsonProperty("read")
        public Builder read(Read read) {
            checkState();
            this.read = read;
            return this;
        }

        /**
         * Set Update.
         *
         * @param update The update operation description, if supported
         * @return Builder
         */
        @JsonProperty("update")
        public Builder update(Update update) {
            checkState();
            this.update = update;
            return this;
        }

        /**
         * Set Delete.
         *
         * @param delete The delete operation description, if supported
         * @return Builder
         */
        @JsonProperty("delete")
        public Builder delete(Delete delete) {
            checkState();
            this.delete = delete;
            return this;
        }

        /**
         * Set Patch.
         *
         * @param patch The patch operation description, if supported
         * @return Builder
         */
        @JsonProperty("patch")
        public Builder patch(Patch patch) {
            checkState();
            this.patch = patch;
            return this;
        }

        /**
         * Set Actions.
         *
         * @param actions The list of action operation descriptions, if supported
         * @return Builder
         */
        @JsonProperty("actions")
        public Builder actions(List<Action> actions) {
            checkState();
            this.actions.addAll(actions);
            return this;
        }

        /**
         * Adds one Action to the list of Actions.
         *
         * @param action Action operation description to be added to the list
         * @return Builder
         */
        public Builder action(Action action) {
            checkState();
            this.actions.add(action);
            return this;
        }

        /**
         * Set Queries.
         *
         * @param queries The list or query operation descriptions, if supported
         * @return Builder
         */
        @JsonProperty("queries")
        public Builder queries(List<Query> queries) {
            checkState();
            this.queries.addAll(queries);
            return this;
        }

        /**
         * Adds one Query to the list of queries.
         *
         * @param query Query operation description to be added to the list
         * @return Builder
         */
        public Builder query(Query query) {
            checkState();
            this.queries.add(query);
            return this;
        }

        /**
         * Sets the sub-resources for this resource.
         *
         * @param subresources The sub-reosurces definition.
         * @return Builder
         */
        @JsonProperty("subresources")
        public Builder subresources(SubResources subresources) {
            checkState();
            this.subresources = subresources;
            return this;
        }

        /**
         * Allocates the operations given in the parameter by their type.
         *
         * @param operations One or more Operations
         * @return Builder
         */
        @JsonProperty("operations")
        public Builder operations(Operation... operations) {
            checkState();
            Reject.ifNull(operations);
            for (Operation operation : operations) {
                operation.allocateToResource(this);
            }
            return this;
        }

        /**
         * Setter for MVCC-supported flag.
         *
         * @param mvccSupported Whether this resource supports MVCC
         * @return Builder
         */
        @JsonProperty("mvccSupported")
        public Builder mvccSupported(Boolean mvccSupported) {
            checkState();
            this.mvccSupported = mvccSupported;
            return this;
        }

        /**
         * Adds items-resource.
         *
         * @param items The definition of the collection items
         * @return Builder
         */
        @JsonProperty("items")
        public Builder items(Items items) {
            checkState();
            this.items = items;
            return this;
        }

        /**
         * Set multiple supported parameters.
         *
         * @param parameters Extra parameters supported by the resource
         * @return Builder
         */
        @JsonProperty("parameters")
        public Builder parameters(List<Parameter> parameters) {
            checkState();
            this.parameters.addAll(parameters);
            return this;
        }

        /**
         * Sets a single supported parameter.
         *
         * @param parameter Extra parameter supported by the resource
         * @return Builder
         */
        public Builder parameter(Parameter parameter) {
            this.parameters.add(parameter);
            return this;
        }

        /**
         * Construct a new instance of Resource.
         *
         * @return Resource instance
         */
        public Resource build() {
            checkState();
            this.built = true;
            if (create == null && read == null && update == null && delete == null && patch == null
                    && actions.isEmpty() && queries.isEmpty() && reference == null && items == null
                    && subresources == null) {
                return null;
            }

            return new Resource(this);
        }

        private void checkState() {
            rejectStateIfTrue(built, "Already built Resource");
        }

    }
}