1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
49
50
51
52
53
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
109
110
111
112 public Schema getResourceSchema() {
113 return resourceSchema;
114 }
115
116
117
118
119
120
121 public LocalizableString getTitle() {
122 return title;
123 }
124
125
126
127
128
129
130 public LocalizableString getDescription() {
131 return description;
132 }
133
134
135
136
137
138
139 public Create getCreate() {
140 return create;
141 }
142
143
144
145
146
147
148 public Read getRead() {
149 return read;
150 }
151
152
153
154
155
156
157 public Update getUpdate() {
158 return update;
159 }
160
161
162
163
164
165
166 public Delete getDelete() {
167 return delete;
168 }
169
170
171
172
173
174
175 public Patch getPatch() {
176 return patch;
177 }
178
179
180
181
182
183
184 public Action[] getActions() {
185 return actions;
186 }
187
188
189
190
191
192
193 public Query[] getQueries() {
194 return queries;
195 }
196
197
198
199
200
201
202 public SubResources getSubresources() {
203 return subresources;
204 }
205
206
207
208
209
210 public Reference getReference() {
211 return reference;
212 }
213
214
215
216
217
218
219 public Items getItems() {
220 return items;
221 }
222
223
224
225
226
227
228 public Boolean isMvccSupported() {
229 return mvccSupported;
230 }
231
232
233
234
235
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
275
276
277
278 public static Builder resource() {
279 return new Builder();
280 }
281
282
283
284
285
286
287
288
289 public static Resource fromAnnotatedType(Class<?> type, AnnotatedTypeVariant variant, ApiDescription descriptor) {
290 return fromAnnotatedType(type, variant, null, null, descriptor);
291 }
292
293
294
295
296
297
298
299
300
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
309
310
311
312
313
314
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
437
438
439 public enum AnnotatedTypeVariant {
440
441 SINGLETON_RESOURCE(true, true, false, false),
442
443 COLLECTION_RESOURCE_COLLECTION(false, false, false, true),
444
445 COLLECTION_RESOURCE_INSTANCE(true, true, true, false),
446
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
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
486
487 protected Builder() {
488 actions = new TreeSet<>();
489 queries = new TreeSet<>();
490 parameters = new ArrayList<>();
491 }
492
493
494
495
496
497
498 @JsonProperty("$ref")
499 public Builder reference(Reference reference) {
500 checkState();
501 this.reference = reference;
502 return this;
503 }
504
505
506
507
508
509
510
511
512 @JsonProperty("resourceSchema")
513 public Builder resourceSchema(Schema resourceSchema) {
514 checkState();
515 this.resourceSchema = resourceSchema;
516 return this;
517 }
518
519
520
521
522
523
524
525 public Builder title(LocalizableString title) {
526 this.title = title;
527 return this;
528 }
529
530
531
532
533
534
535
536 @JsonProperty("title")
537 public Builder title(String title) {
538 return title(new LocalizableString(title));
539 }
540
541
542
543
544
545
546
547 public Builder description(LocalizableString description) {
548 checkState();
549 this.description = description;
550 return this;
551 }
552
553
554
555
556
557
558
559 @JsonProperty("description")
560 public Builder description(String description) {
561 checkState();
562 return description(new LocalizableString(description));
563 }
564
565
566
567
568
569
570
571 @JsonProperty("create")
572 public Builder create(Create create) {
573 checkState();
574 this.create = create;
575 return this;
576 }
577
578
579
580
581
582
583
584 @JsonProperty("read")
585 public Builder read(Read read) {
586 checkState();
587 this.read = read;
588 return this;
589 }
590
591
592
593
594
595
596
597 @JsonProperty("update")
598 public Builder update(Update update) {
599 checkState();
600 this.update = update;
601 return this;
602 }
603
604
605
606
607
608
609
610 @JsonProperty("delete")
611 public Builder delete(Delete delete) {
612 checkState();
613 this.delete = delete;
614 return this;
615 }
616
617
618
619
620
621
622
623 @JsonProperty("patch")
624 public Builder patch(Patch patch) {
625 checkState();
626 this.patch = patch;
627 return this;
628 }
629
630
631
632
633
634
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
645
646
647
648
649 public Builder action(Action action) {
650 checkState();
651 this.actions.add(action);
652 return this;
653 }
654
655
656
657
658
659
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
670
671
672
673
674 public Builder query(Query query) {
675 checkState();
676 this.queries.add(query);
677 return this;
678 }
679
680
681
682
683
684
685
686 @JsonProperty("subresources")
687 public Builder subresources(SubResources subresources) {
688 checkState();
689 this.subresources = subresources;
690 return this;
691 }
692
693
694
695
696
697
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
711
712
713
714
715 @JsonProperty("mvccSupported")
716 public Builder mvccSupported(Boolean mvccSupported) {
717 checkState();
718 this.mvccSupported = mvccSupported;
719 return this;
720 }
721
722
723
724
725
726
727
728 @JsonProperty("items")
729 public Builder items(Items items) {
730 checkState();
731 this.items = items;
732 return this;
733 }
734
735
736
737
738
739
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
750
751
752
753
754 public Builder parameter(Parameter parameter) {
755 this.parameters.add(parameter);
756 return this;
757 }
758
759
760
761
762
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 }