diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 22775b35c4606..da4086de81495 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1138,6 +1138,81 @@ Importing this module will allow HTTP message bodies to be read from XML and serialised to XML, for <>. +=== Web Links support + +[[links]] + +To enable Web Links support, add the `quarkus-resteasy-reactive-links` extension to your project. + +.Table Context object +|=== +|GAV|Usage + +|`io.quarkus:quarkus-resteasy-reactive-links` +|https://www.w3.org/wiki/LinkHeader[Web Links support] + +|=== + +Importing this module will allow injecting web links into the response HTTP headers by just annotating your endpoint resources with the `@InjectRestLinks` annotation. To declare the web links that will be returned, you need to use the `@RestLink` annotation in the linked methods. An example of this could look like: + +[source,java] +---- +@Path("/records") +public class RecordsResource { + + @GET + @RestLink(rel = "list") + @InjectRestLinks + public List getAll() { + // ... + } + + @GET + @Path("/{id}") + @RestLink(rel = "self") + @InjectRestLinks(RestLinkType.INSTANCE) + public TestRecord get(@PathParam("id") int id) { + // ... + } + + @PUT + @Path("/{id}") + @RestLink + @InjectRestLinks(RestLinkType.INSTANCE) + public TestRecord update(@PathParam("id") int id) { + // ... + } + + @DELETE + @Path("/{id}") + @RestLink + public TestRecord delete(@PathParam("id") int id) { + // ... + } +} +---- + +When calling the endpoint `/records` which is defined by the method `getAll` within the above resource using curl, you would get the web links header: + +[source,bash] +---- +& curl -i localhost:8080/records +Link: ; rel="list" +---- + +As this resource does not return a single instance of type `Record`, the links for the methods `get`, `update`, and `delete` are not injected. Now, when calling the endpoint `/records/1`, you would get the following web links: + +[source,bash] +---- +& curl -i localhost:8080/records/1 +Link: ; rel="list" +Link: ; rel="self" +Link: ; rel="update" +Link: ; rel="delete" +---- + +Finally, when calling the delete resource, you should not see any web links as the method `delete` is not annotated with the `@InjectRestLinks` annotation. + == CORS filter link:https://en.wikipedia.org/wiki/Cross-origin_resource_sharing[Cross-origin resource sharing] (CORS) is a mechanism that diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java index 997faa96df525..142f178677ed9 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksProcessor.java @@ -1,6 +1,7 @@ package io.quarkus.resteasy.reactive.links.deployment; import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.OBJECT_NAME; import java.util.HashSet; import java.util.List; @@ -97,7 +98,7 @@ private RuntimeValue implementPathParameterValueGetter String entityType = linkInfo.getEntityType(); for (String parameterName : linkInfo.getPathParameters()) { // We implement a getter inside a class that has the required field. - // We later map that getter's accessor with a entity type. + // We later map that getter's accessor with an entity type. // If a field is inside a parent class, the getter accessor will be mapped to each subclass which // has REST links that need access to that field. FieldInfo fieldInfo = getFieldInfo(index, DotName.createSimple(entityType), parameterName); @@ -140,7 +141,7 @@ private FieldInfo getFieldInfo(IndexView index, DotName className, String fieldN if (fieldInfo != null) { return fieldInfo; } - if (classInfo.superName() != null) { + if (classInfo.superName() != null && !classInfo.superName().equals(OBJECT_NAME)) { return getFieldInfo(index, classInfo.superName(), fieldName); } throw new RuntimeException(String.format("Class '%s' field '%s' was not found", className, fieldName)); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/InjectRestLinks.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/InjectRestLinks.java index ca8ca0f0ed289..b2f23a560d4df 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/InjectRestLinks.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/InjectRestLinks.java @@ -5,9 +5,23 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Inject web links into the response HTTP headers with the "Link" header field. + * Only the response of the REST methods annotated with {@link RestLink} will include the "Link" headers. + *

+ * The InjectRestLinks annotation can be used at either class or method levels. + *

+ * + * @see RFC 5988 Web Linking Standard + */ @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.TYPE, ElementType.METHOD }) public @interface InjectRestLinks { + /** + * Find all the types available in {@link RestLinkType}. + * + * @return what types of links will be injected. + */ RestLinkType value() default RestLinkType.TYPE; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java index 6f6b836a3d9ce..cfa5316efb180 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java @@ -5,11 +5,31 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Represents a Web link to be incorporated into the HTTP response. + * Only the response of methods or classes annotated with {@link InjectRestLinks} will include the "Link" headers. + *

+ * The RestLink annotation can be used at method level. + *

+ * + * @see RFC 5988 Web Linking Standard + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface RestLink { + /** + * If not set, it will default to the method name. + * + * @return the link relation. + */ String rel() default ""; + /** + * Declares a link for the given type of resources. + * If not set, it will default to the returning type of the annotated method. + * + * @return the type of returning method. + */ Class entityType() default Object.class; } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkType.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkType.java index 5a23ed33ca821..bd1e356b8ad46 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkType.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinkType.java @@ -1,6 +1,81 @@ package io.quarkus.resteasy.reactive.links; +/** + * Manage the link types to be injected in the Web links. + */ public enum RestLinkType { + /** + * It will inject the links that return the link type {@link RestLink#entityType()} without filtering or searching. + * For example: + * + *

+     *     @GET
+     *     @Path("/records")
+     *     @RestLink(rel = "list")
+     *     @InjectRestLinks(RestLinkType.TYPE)
+     *     public List getAll() { // ... }
+     *
+     *     @GET
+     *     @Path("/records/valid")
+     *     @RestLink
+     *     public List getValidRecords() { // ... }
+     *
+     *     @GET
+     *     @Path("/records/{id}")
+     *     @RestLink(rel = "self")
+     *     public TestRecord getById(@PathParam("id") int id) { // ... }
+     * 
+ *

+ * Note that the method `getAll` is annotated with `@InjectRestLinks(RestLinkType.TYPE)`, so when calling to the endpoint + * `/records`, it will inject the following links: + * + *

+     * Link: ; rel="list"
+     * Link: ; rel="getValidRecords"
+     * 
+ *

+ * The method `getById` is not injected because it's instance based (it depends on the field `id`). + */ TYPE, + + /** + * It will inject all the links that return the link type {@link RestLink#entityType()}. + * For example: + * + *

+     *
+     *     @GET
+     *     @RestLink(rel = "list")
+     *     public List getAll() { // ... }
+     *
+     *     @GET
+     *     @Path("/records/{id}")
+     *     @RestLink(rel = "self")
+     *     @InjectRestLinks(RestLinkType.INSTANCE)
+     *     public TestRecord getById(@PathParam("id") int id) { // ... }
+     *
+     *     @GET
+     *     @Path("/records/{slug}")
+     *     @RestLink
+     *     public TestRecord getBySlug(@PathParam("slug") String slug) { // ... }
+     *
+     *     @DELETE
+     *     @Path("/records/{id}")
+     *     @RestLink
+     *     public TestRecord delete(@PathParam("slug") String slug) { // ... }
+     * 
+ *

+ * Note that the method `getById` is annotated with `@InjectRestLinks(RestLinkType.INSTANCE)`, so when calling to the + * endpoint `/records/1`, it will inject the following links: + * + *

+     * Link: ; rel="list"
+     * Link: ; rel="self"
+     * Link: ; rel="getBySlug"
+     * Link: ; rel="delete"
+     * 
+ *

+ * Now, all the links have been injected. + */ INSTANCE } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksHandler.java index 10c2e24571c8e..5a1ad25289e84 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksHandler.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksHandler.java @@ -31,11 +31,13 @@ public void handle(ResteasyReactiveRequestContext context) { } private Collection getLinks(Response response) { + RestLinksProvider provider = getRestLinksProvider(); + if ((restLinkData.getRestLinkType() == RestLinkType.INSTANCE) && response.hasEntity()) { - return getTestLinksProvider().getInstanceLinks(response.getEntity()); + return provider.getInstanceLinks(response.getEntity()); } - return getTestLinksProvider() - .getTypeLinks(restLinkData.getEntityType() != null ? entityTypeClass() : response.getEntity().getClass()); + return provider.getTypeLinks( + restLinkData.getEntityType() != null ? entityTypeClass() : response.getEntity().getClass()); } private Class entityTypeClass() { @@ -46,7 +48,7 @@ private Class entityTypeClass() { } } - private RestLinksProvider getTestLinksProvider() { + private RestLinksProvider getRestLinksProvider() { return Arc.container().instance(RestLinksProvider.class).get(); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksProvider.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksProvider.java index ea0a90c331810..b2e7cc3f8d2f4 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksProvider.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLinksProvider.java @@ -4,9 +4,21 @@ import javax.ws.rs.core.Link; +/** + * An injectable bean that contains methods to get the web links at class and instance levels. + */ public interface RestLinksProvider { + /** + * @param elementType The resource type. + * @return the web links associated with the element type. + */ Collection getTypeLinks(Class elementType); + /** + * @param instance the resource instance. + * @param the resource generic type. + * @return the web links associated with the instance. + */ Collection getInstanceLinks(T instance); }