View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.api.models;
18  
19  import static org.forgerock.api.models.Reference.reference;
20  import static org.forgerock.api.util.ValidationUtil.isEmpty;
21  import static org.forgerock.util.Reject.rejectStateIfTrue;
22  
23  import java.lang.reflect.Method;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.List;
27  import java.util.Objects;
28  import java.util.Set;
29  import java.util.TreeSet;
30  
31  import org.forgerock.api.ApiValidationException;
32  import org.forgerock.api.annotations.Actions;
33  import org.forgerock.api.annotations.CollectionProvider;
34  import org.forgerock.api.annotations.Handler;
35  import org.forgerock.api.annotations.Queries;
36  import org.forgerock.api.annotations.RequestHandler;
37  import org.forgerock.api.annotations.SingletonProvider;
38  import org.forgerock.util.Reject;
39  import org.forgerock.util.i18n.LocalizableString;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  import com.fasterxml.jackson.annotation.JsonInclude;
44  import com.fasterxml.jackson.annotation.JsonProperty;
45  import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
46  
47  /**
48   * Class that represents the Resource type in API descriptor.
49   * <p>
50   *     {@code Resource}s may be either a reference to another {@code Resource} that will be defined elsewhere in the
51   *     API Descriptor, or a described resource. If a {@link Reference} is provided, then none of the other fields may
52   *     be used, and if any of the other fields are used, a reference may not be provided.
53   * </p>
54   */
55  @JsonDeserialize(builder = Resource.Builder.class)
56  @JsonInclude(JsonInclude.Include.NON_NULL)
57  public final class Resource {
58      private static final Logger LOGGER = LoggerFactory.getLogger(Resource.class);
59      private static final String SERVICES_REFERENCE = "#/services/%s";
60  
61      @JsonProperty("$ref")
62      private final Reference reference;
63      private final Schema resourceSchema;
64      private final LocalizableString title;
65      private final LocalizableString description;
66      private final Create create;
67      private final Read read;
68      private final Update update;
69      private final Delete delete;
70      private final Patch patch;
71      @JsonInclude(JsonInclude.Include.NON_EMPTY)
72      private final Action[] actions;
73      @JsonInclude(JsonInclude.Include.NON_EMPTY)
74      private final Query[] queries;
75      private final SubResources subresources;
76      private final Items items;
77      private final Boolean mvccSupported;
78      @JsonInclude(JsonInclude.Include.NON_EMPTY)
79      private final Parameter[] parameters;
80  
81      private Resource(Builder builder) {
82          this.reference = builder.reference;
83          this.resourceSchema = builder.resourceSchema;
84          this.title = builder.title;
85          this.description = builder.description;
86          this.create = builder.create;
87          this.read = builder.read;
88          this.update = builder.update;
89          this.delete = builder.delete;
90          this.patch = builder.patch;
91          this.subresources = builder.subresources;
92          this.actions = builder.actions.toArray(new Action[builder.actions.size()]);
93          this.queries = builder.queries.toArray(new Query[builder.queries.size()]);
94          this.items = builder.items;
95          this.mvccSupported = builder.mvccSupported;
96          this.parameters = builder.parameters.toArray(new Parameter[builder.parameters.size()]);
97  
98          if ((create != null || read != null || update != null || delete != null || patch != null
99                  || !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 }