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.api.models;
018
019import static org.forgerock.api.models.Reference.reference;
020import static org.forgerock.api.util.ValidationUtil.isEmpty;
021import static org.forgerock.util.Reject.rejectStateIfTrue;
022
023import java.lang.reflect.Method;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.List;
027import java.util.Objects;
028import java.util.Set;
029import java.util.TreeSet;
030
031import org.forgerock.api.ApiValidationException;
032import org.forgerock.api.annotations.Actions;
033import org.forgerock.api.annotations.CollectionProvider;
034import org.forgerock.api.annotations.Handler;
035import org.forgerock.api.annotations.Queries;
036import org.forgerock.api.annotations.RequestHandler;
037import org.forgerock.api.annotations.SingletonProvider;
038import org.forgerock.util.Reject;
039import org.forgerock.util.i18n.LocalizableString;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043import com.fasterxml.jackson.annotation.JsonInclude;
044import com.fasterxml.jackson.annotation.JsonProperty;
045import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
046
047/**
048 * Class that represents the Resource type in API descriptor.
049 * <p>
050 *     {@code Resource}s may be either a reference to another {@code Resource} that will be defined elsewhere in the
051 *     API Descriptor, or a described resource. If a {@link Reference} is provided, then none of the other fields may
052 *     be used, and if any of the other fields are used, a reference may not be provided.
053 * </p>
054 */
055@JsonDeserialize(builder = Resource.Builder.class)
056@JsonInclude(JsonInclude.Include.NON_NULL)
057public final class Resource {
058    private static final Logger LOGGER = LoggerFactory.getLogger(Resource.class);
059    private static final String SERVICES_REFERENCE = "#/services/%s";
060
061    @JsonProperty("$ref")
062    private final Reference reference;
063    private final Schema resourceSchema;
064    private final LocalizableString title;
065    private final LocalizableString description;
066    private final Create create;
067    private final Read read;
068    private final Update update;
069    private final Delete delete;
070    private final Patch patch;
071    @JsonInclude(JsonInclude.Include.NON_EMPTY)
072    private final Action[] actions;
073    @JsonInclude(JsonInclude.Include.NON_EMPTY)
074    private final Query[] queries;
075    private final SubResources subresources;
076    private final Items items;
077    private final Boolean mvccSupported;
078    @JsonInclude(JsonInclude.Include.NON_EMPTY)
079    private final Parameter[] parameters;
080
081    private Resource(Builder builder) {
082        this.reference = builder.reference;
083        this.resourceSchema = builder.resourceSchema;
084        this.title = builder.title;
085        this.description = builder.description;
086        this.create = builder.create;
087        this.read = builder.read;
088        this.update = builder.update;
089        this.delete = builder.delete;
090        this.patch = builder.patch;
091        this.subresources = builder.subresources;
092        this.actions = builder.actions.toArray(new Action[builder.actions.size()]);
093        this.queries = builder.queries.toArray(new Query[builder.queries.size()]);
094        this.items = builder.items;
095        this.mvccSupported = builder.mvccSupported;
096        this.parameters = builder.parameters.toArray(new Parameter[builder.parameters.size()]);
097
098        if ((create != null || read != null || update != null || delete != null || patch != null
099                || !isEmpty(actions) || !isEmpty(queries)) && reference != null) {
100            throw new ApiValidationException("Cannot have a reference as well as operations");
101        }
102        if (mvccSupported == null && reference == null) {
103            throw new ApiValidationException("mvccSupported required for non-reference Resources");
104        }
105    }
106
107    /**
108     * Getter of resource schema.
109     *
110     * @return Resource schema
111     */
112    public Schema getResourceSchema() {
113        return resourceSchema;
114    }
115
116    /**
117     * Getter of title.
118     *
119     * @return Title
120     */
121    public LocalizableString getTitle() {
122        return title;
123    }
124
125    /**
126     * Getter of description.
127     *
128     * @return Description
129     */
130    public LocalizableString getDescription() {
131        return description;
132    }
133
134    /**
135     * Getter of Create.
136     *
137     * @return Create
138     */
139    public Create getCreate() {
140        return create;
141    }
142
143    /**
144     * Getter of Read.
145     *
146     * @return Read
147     */
148    public Read getRead() {
149        return read;
150    }
151
152    /**
153     * Getter of Update.
154     *
155     * @return Update
156     */
157    public Update getUpdate() {
158        return update;
159    }
160
161    /**
162     * Getter of Delete.
163     *
164     * @return Delete
165     */
166    public Delete getDelete() {
167        return delete;
168    }
169
170    /**
171     * Getter of Patch.
172     *
173     * @return Patch
174     */
175    public Patch getPatch() {
176        return patch;
177    }
178
179    /**
180     * Getter of actions.
181     *
182     * @return Actions
183     */
184    public Action[] getActions() {
185        return actions;
186    }
187
188    /**
189     * Getter of queries.
190     *
191     * @return Queries
192     */
193    public Query[] getQueries() {
194        return queries;
195    }
196
197    /**
198     * Getter of sub-resources.
199     *
200     * @return Sub-resources
201     */
202    public SubResources getSubresources() {
203        return subresources;
204    }
205
206    /**
207     * Gets the reference.
208     * @return The reference.
209     */
210    public Reference getReference() {
211        return reference;
212    }
213
214    /**
215     * Getter of items.
216     *
217     * @return Items
218     */
219    public Items getItems() {
220        return items;
221    }
222
223    /**
224     * Informs if MVCC is supported.
225     *
226     * @return {@code true} if MVCC is supported and {@code false} otherwise
227     */
228    public Boolean isMvccSupported() {
229        return mvccSupported;
230    }
231
232    /**
233     * Getter of the parameters array.
234     *
235     * @return Parameters
236     */
237    public Parameter[] getParameters() {
238        return parameters;
239    }
240
241    @Override
242    public boolean equals(Object o) {
243        if (this == o) {
244            return true;
245        }
246        if (o == null || getClass() != o.getClass()) {
247            return false;
248        }
249        Resource resource = (Resource) o;
250        return Objects.equals(reference, resource.reference)
251                && Objects.equals(resourceSchema, resource.resourceSchema)
252                && Objects.equals(title, resource.title)
253                && Objects.equals(description, resource.description)
254                && Objects.equals(create, resource.create)
255                && Objects.equals(read, resource.read)
256                && Objects.equals(update, resource.update)
257                && Objects.equals(delete, resource.delete)
258                && Objects.equals(patch, resource.patch)
259                && Arrays.equals(actions, resource.actions)
260                && Arrays.equals(queries, resource.queries)
261                && Objects.equals(subresources, resource.subresources)
262                && Objects.equals(items, resource.items)
263                && Objects.equals(mvccSupported, resource.mvccSupported)
264                && Arrays.equals(parameters, resource.parameters);
265    }
266
267    @Override
268    public int hashCode() {
269        return Objects.hash(reference, resourceSchema, title, description, create, read, update, delete, patch, actions,
270                queries, subresources, items, mvccSupported, parameters);
271    }
272
273    /**
274     * Create a new Builder for Resoruce.
275     *
276     * @return Builder
277     */
278    public static Builder resource() {
279        return new Builder();
280    }
281
282    /**
283     * Build a {@code Resource} from an annotated request handler.
284     * @param type The annotated type.
285     * @param variant The annotated type variant.
286     * @param descriptor The root descriptor to add definitions to.
287     * @return The built {@code Resource} object.
288     */
289    public static Resource fromAnnotatedType(Class<?> type, AnnotatedTypeVariant variant, ApiDescription descriptor) {
290        return fromAnnotatedType(type, variant, null, null, descriptor);
291    }
292
293    /**
294     * Build a {@code Resource} from an annotated request handler.
295     * @param type The annotated type.
296     * @param variant The annotated type variant.
297     * @param subResources The sub resources object to be included, if any sub-resources exist, or null.
298     * @param descriptor The root descriptor to add definitions to.
299     * @param extraParameters Extra parameters not from the resource annotation.
300     * @return The built {@code Resource} object.
301     */
302    public static Resource fromAnnotatedType(Class<?> type, AnnotatedTypeVariant variant, SubResources subResources,
303            ApiDescription descriptor, Parameter... extraParameters) {
304        return fromAnnotatedType(type, variant, subResources, null, descriptor, extraParameters);
305    }
306
307    /**
308     * Build a {@code Resource} from an annotated request handler.
309     * @param type The annotated type.
310     * @param variant The annotated type variant.
311     * @param items The items definition for a collection variant, or null.
312     * @param descriptor The root descriptor to add definitions to.
313     * @param extraParameters Extra parameters not from the resource annotation.
314     * @return The built {@code Resource} object.
315     */
316    public static Resource fromAnnotatedType(Class<?> type, AnnotatedTypeVariant variant,
317            Items items, ApiDescription descriptor, Parameter... extraParameters) {
318        return fromAnnotatedType(type, variant, null, items, descriptor, extraParameters);
319    }
320
321    private static Resource fromAnnotatedType(Class<?> type, AnnotatedTypeVariant variant,
322            SubResources subResources, Items items, ApiDescription descriptor, Parameter... extraParameters) {
323        Builder builder = resource();
324        Handler handler = findHandlerAnnotation(variant, type);
325        if (handler == null) {
326            return null;
327        }
328        boolean foundCrudpq = false;
329        for (Method m : type.getMethods()) {
330            boolean instanceMethod = Arrays.asList(m.getParameterTypes()).indexOf(String.class) > -1;
331            org.forgerock.api.annotations.Action action = m.getAnnotation(org.forgerock.api.annotations.Action.class);
332            if (action != null && instanceMethod == variant.actionRequiresId) {
333                builder.actions.add(Action.fromAnnotation(action, m, descriptor, type));
334            }
335            Actions actions = m.getAnnotation(Actions.class);
336            if (actions != null && instanceMethod == variant.actionRequiresId) {
337                for (org.forgerock.api.annotations.Action a : actions.value()) {
338                    builder.actions.add(Action.fromAnnotation(a, null, descriptor, type));
339                }
340            }
341            org.forgerock.api.annotations.Create create = m.getAnnotation(org.forgerock.api.annotations.Create.class);
342            if (create != null) {
343                builder.create = Create.fromAnnotation(create, variant.instanceCreate, descriptor, type);
344                foundCrudpq = true;
345            }
346            if (variant.rudpOperations) {
347                org.forgerock.api.annotations.Read read = m.getAnnotation(org.forgerock.api.annotations.Read.class);
348                if (read != null) {
349                    builder.read = Read.fromAnnotation(read, descriptor, type);
350                    foundCrudpq = true;
351                }
352                org.forgerock.api.annotations.Update update =
353                        m.getAnnotation(org.forgerock.api.annotations.Update.class);
354                if (update != null) {
355                    builder.update = Update.fromAnnotation(update, descriptor, type);
356                    foundCrudpq = true;
357                }
358                org.forgerock.api.annotations.Delete delete =
359                        m.getAnnotation(org.forgerock.api.annotations.Delete.class);
360                if (delete != null) {
361                    builder.delete = Delete.fromAnnotation(delete, descriptor, type);
362                    foundCrudpq = true;
363                }
364                org.forgerock.api.annotations.Patch patch = m.getAnnotation(org.forgerock.api.annotations.Patch.class);
365                if (patch != null) {
366                    builder.patch = Patch.fromAnnotation(patch, descriptor, type);
367                    foundCrudpq = true;
368                }
369            }
370            if (variant.queryOperations) {
371                org.forgerock.api.annotations.Query query = m.getAnnotation(org.forgerock.api.annotations.Query.class);
372                if (query != null) {
373                    builder.queries.add(Query.fromAnnotation(query, m, descriptor, type));
374                    foundCrudpq = true;
375                }
376                Queries queries = m.getAnnotation(Queries.class);
377                if (queries != null) {
378                    for (org.forgerock.api.annotations.Query q : queries.value()) {
379                        builder.queries.add(Query.fromAnnotation(q, null, descriptor, type));
380                        foundCrudpq = true;
381                    }
382                }
383            }
384        }
385        Schema resourceSchema = Schema.fromAnnotation(handler.resourceSchema(), descriptor, type);
386        if (foundCrudpq && resourceSchema == null) {
387            throw new IllegalArgumentException("CRUDPQ operation(s) defined, but no resource schema declared");
388        }
389
390        for (org.forgerock.api.annotations.Parameter parameter : handler.parameters()) {
391            builder.parameter(Parameter.fromAnnotation(type, parameter));
392        }
393        for (Parameter param : extraParameters) {
394            builder.parameter(param);
395        }
396
397        Resource resource = builder.resourceSchema(resourceSchema)
398                .mvccSupported(handler.mvccSupported())
399                .title(new LocalizableString(handler.title(), type))
400                .description(new LocalizableString(handler.description(), type))
401                .subresources(subResources)
402                .items(items)
403                .build();
404
405        if (!handler.id().isEmpty()) {
406            descriptor.addService(handler.id(), resource);
407            Reference reference = reference().value(String.format(SERVICES_REFERENCE, handler.id())).build();
408            resource = resource().reference(reference).build();
409        }
410        return resource;
411    }
412
413    private static Handler findHandlerAnnotation(AnnotatedTypeVariant variant, Class<?> type) {
414        switch (variant) {
415        case SINGLETON_RESOURCE:
416            if (type.getAnnotation(SingletonProvider.class) != null) {
417                return type.getAnnotation(SingletonProvider.class).value();
418            }
419            break;
420        case REQUEST_HANDLER:
421            if (type.getAnnotation(RequestHandler.class) != null) {
422                return type.getAnnotation(RequestHandler.class).value();
423            }
424            break;
425        default:
426            if (type.getAnnotation(CollectionProvider.class) != null) {
427                return type.getAnnotation(CollectionProvider.class).details();
428            }
429        }
430        LOGGER.info("Asked for Resource for annotated type, but type does not have required RequestHandler"
431                + " annotation. No api descriptor will be available for " + type);
432        return null;
433    }
434
435    /**
436     * The variant of the annotated type. Allows the annotation processing to make assumptions about what type of
437     * operations are expected from this context of the type.
438     */
439    public enum AnnotatedTypeVariant {
440        /** A singleton resource handler. (expect RUDPA operations). */
441        SINGLETON_RESOURCE(true, true, false, false),
442        /** A collection resource handler, collection endpoint (expect CAQ opererations). */
443        COLLECTION_RESOURCE_COLLECTION(false, false, false, true),
444        /** A collection resource handler, instance endpoint (expect CRUDPA operations). */
445        COLLECTION_RESOURCE_INSTANCE(true, true, true, false),
446        /** A plain request handler (expects all operations). */
447        REQUEST_HANDLER(false, true, false, true);
448
449        private final boolean instanceCreate;
450        private final boolean rudpOperations;
451        private final boolean actionRequiresId;
452        private final boolean queryOperations;
453
454        AnnotatedTypeVariant(boolean instanceCreate, boolean rudpOperations, boolean actionRequiresId,
455                boolean queryOperations) {
456            this.instanceCreate = instanceCreate;
457            this.rudpOperations = rudpOperations;
458            this.actionRequiresId = actionRequiresId;
459            this.queryOperations = queryOperations;
460        }
461    }
462
463    /**
464     * Builder to help construct the Resource.
465     */
466    public final static class Builder {
467        private Schema resourceSchema;
468        private LocalizableString title;
469        private LocalizableString description;
470        private Create create;
471        private Read read;
472        private Update update;
473        private Delete delete;
474        private Patch patch;
475        private SubResources subresources;
476        private final Set<Action> actions;
477        private final Set<Query> queries;
478        private Items items;
479        private Boolean mvccSupported;
480        private Reference reference;
481        private final List<Parameter> parameters;
482        private boolean built = false;
483
484        /**
485         * Private default constructor.
486         */
487        protected Builder() {
488            actions = new TreeSet<>();
489            queries = new TreeSet<>();
490            parameters = new ArrayList<>();
491        }
492
493        /**
494         * Set a reference.
495         * @param reference The reference.
496         * @return This builder.
497         */
498        @JsonProperty("$ref")
499        public Builder reference(Reference reference) {
500            checkState();
501            this.reference = reference;
502            return this;
503        }
504
505        /**
506         * Set the resource schema.
507         *
508         * @param resourceSchema The schema of the resource for this path.
509         * Required when any of create, read, update, delete, patch are supported
510         * @return Builder
511         */
512        @JsonProperty("resourceSchema")
513        public Builder resourceSchema(Schema resourceSchema) {
514            checkState();
515            this.resourceSchema = resourceSchema;
516            return this;
517        }
518
519        /**
520         * Set the title.
521         *
522         * @param title Title of the endpoint
523         * @return Builder
524         */
525        public Builder title(LocalizableString title) {
526            this.title = title;
527            return this;
528        }
529
530        /**
531         * Set the title.
532         *
533         * @param title Title of the endpoint
534         * @return Builder
535         */
536        @JsonProperty("title")
537        public Builder title(String title) {
538            return title(new LocalizableString(title));
539        }
540
541        /**
542         * Set the description.
543         *
544         * @param description A description of the endpoint
545         * @return Builder
546         */
547        public Builder description(LocalizableString description) {
548            checkState();
549            this.description = description;
550            return this;
551        }
552
553        /**
554         * Set the description.
555         *
556         * @param description A description of the endpoint
557         * @return Builder
558         */
559        @JsonProperty("description")
560        public Builder description(String description) {
561            checkState();
562            return description(new LocalizableString(description));
563        }
564
565        /**
566         * Set create.
567         *
568         * @param create The create operation description, if supported
569         * @return Builder
570         */
571        @JsonProperty("create")
572        public Builder create(Create create) {
573            checkState();
574            this.create = create;
575            return this;
576        }
577
578        /**
579         * Set Read.
580         *
581         * @param read The read operation description, if supported
582         * @return Builder
583         */
584        @JsonProperty("read")
585        public Builder read(Read read) {
586            checkState();
587            this.read = read;
588            return this;
589        }
590
591        /**
592         * Set Update.
593         *
594         * @param update The update operation description, if supported
595         * @return Builder
596         */
597        @JsonProperty("update")
598        public Builder update(Update update) {
599            checkState();
600            this.update = update;
601            return this;
602        }
603
604        /**
605         * Set Delete.
606         *
607         * @param delete The delete operation description, if supported
608         * @return Builder
609         */
610        @JsonProperty("delete")
611        public Builder delete(Delete delete) {
612            checkState();
613            this.delete = delete;
614            return this;
615        }
616
617        /**
618         * Set Patch.
619         *
620         * @param patch The patch operation description, if supported
621         * @return Builder
622         */
623        @JsonProperty("patch")
624        public Builder patch(Patch patch) {
625            checkState();
626            this.patch = patch;
627            return this;
628        }
629
630        /**
631         * Set Actions.
632         *
633         * @param actions The list of action operation descriptions, if supported
634         * @return Builder
635         */
636        @JsonProperty("actions")
637        public Builder actions(List<Action> actions) {
638            checkState();
639            this.actions.addAll(actions);
640            return this;
641        }
642
643        /**
644         * Adds one Action to the list of Actions.
645         *
646         * @param action Action operation description to be added to the list
647         * @return Builder
648         */
649        public Builder action(Action action) {
650            checkState();
651            this.actions.add(action);
652            return this;
653        }
654
655        /**
656         * Set Queries.
657         *
658         * @param queries The list or query operation descriptions, if supported
659         * @return Builder
660         */
661        @JsonProperty("queries")
662        public Builder queries(List<Query> queries) {
663            checkState();
664            this.queries.addAll(queries);
665            return this;
666        }
667
668        /**
669         * Adds one Query to the list of queries.
670         *
671         * @param query Query operation description to be added to the list
672         * @return Builder
673         */
674        public Builder query(Query query) {
675            checkState();
676            this.queries.add(query);
677            return this;
678        }
679
680        /**
681         * Sets the sub-resources for this resource.
682         *
683         * @param subresources The sub-reosurces definition.
684         * @return Builder
685         */
686        @JsonProperty("subresources")
687        public Builder subresources(SubResources subresources) {
688            checkState();
689            this.subresources = subresources;
690            return this;
691        }
692
693        /**
694         * Allocates the operations given in the parameter by their type.
695         *
696         * @param operations One or more Operations
697         * @return Builder
698         */
699        @JsonProperty("operations")
700        public Builder operations(Operation... operations) {
701            checkState();
702            Reject.ifNull(operations);
703            for (Operation operation : operations) {
704                operation.allocateToResource(this);
705            }
706            return this;
707        }
708
709        /**
710         * Setter for MVCC-supported flag.
711         *
712         * @param mvccSupported Whether this resource supports MVCC
713         * @return Builder
714         */
715        @JsonProperty("mvccSupported")
716        public Builder mvccSupported(Boolean mvccSupported) {
717            checkState();
718            this.mvccSupported = mvccSupported;
719            return this;
720        }
721
722        /**
723         * Adds items-resource.
724         *
725         * @param items The definition of the collection items
726         * @return Builder
727         */
728        @JsonProperty("items")
729        public Builder items(Items items) {
730            checkState();
731            this.items = items;
732            return this;
733        }
734
735        /**
736         * Set multiple supported parameters.
737         *
738         * @param parameters Extra parameters supported by the resource
739         * @return Builder
740         */
741        @JsonProperty("parameters")
742        public Builder parameters(List<Parameter> parameters) {
743            checkState();
744            this.parameters.addAll(parameters);
745            return this;
746        }
747
748        /**
749         * Sets a single supported parameter.
750         *
751         * @param parameter Extra parameter supported by the resource
752         * @return Builder
753         */
754        public Builder parameter(Parameter parameter) {
755            this.parameters.add(parameter);
756            return this;
757        }
758
759        /**
760         * Construct a new instance of Resource.
761         *
762         * @return Resource instance
763         */
764        public Resource build() {
765            checkState();
766            this.built = true;
767            if (create == null && read == null && update == null && delete == null && patch == null
768                    && actions.isEmpty() && queries.isEmpty() && reference == null && items == null
769                    && subresources == null) {
770                return null;
771            }
772
773            return new Resource(this);
774        }
775
776        private void checkState() {
777            rejectStateIfTrue(built, "Already built Resource");
778        }
779
780    }
781}