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}