From 679c1ed522eae8cd304d4639d7fac5ed8a6edcad Mon Sep 17 00:00:00 2001 From: Jose Date: Thu, 14 Nov 2024 11:39:50 +0100 Subject: [PATCH] Support title and type fields when generating HAL links Before these changes, we were only considering the "href" field for the HAL links. Example: ```json { "_links": { "subject": { "href": "/subject" } } } ``` After these changes, users that populate also the title and/or the type like: ```java @GET @Path("/with-rest-link-with-all-fields") @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) public HalEntityWrapper get() { var entity = // ... return new HalEntityWrapper<>(entity, Link.fromUri(URI.create("/path/to/100")) .rel("all") .title("The title link") // the link title .type(MediaType.APPLICATION_JSON) // the link type .build()); } ``` Or using the annotation like: ```java @GET @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) @RestLink(entityType = TestRecordWithRestLinkId.class, title = "The with rest link title", type = MediaType.APPLICATION_JSON) @InjectRestLinks public TestRecordWithRestLinkId get() { return // ... } ``` Then, the links will have the title and/or type fields populated: ```json { "_links": { "subject": { "href": "/subject", "title": "The with rest link title", "type": "application/json" } } } ``` --- .../src/main/java/io/quarkus/hal/HalLink.java | 14 +++++++++++- .../quarkus/hal/HalLinkJacksonSerializer.java | 8 +++++++ .../quarkus/hal/HalLinkJsonbSerializer.java | 8 +++++++ .../main/java/io/quarkus/hal/HalWrapper.java | 4 +++- .../links/runtime/hal/ResteasyHalService.java | 2 +- .../deployment/LinksContainerFactory.java | 4 +++- .../deployment/AbstractHalLinksTest.java | 17 ++++++++++++++ .../links/deployment/TestResource.java | 19 +++++++++++++++- .../resteasy/reactive/links/RestLink.java | 14 ++++++++++++ .../reactive/links/runtime/LinkInfo.java | 16 +++++++++++++- .../links/runtime/RestLinksProviderImpl.java | 22 ++++++++++++++----- .../hal/ResteasyReactiveHalService.java | 2 +- 12 files changed, 117 insertions(+), 13 deletions(-) diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java index accf86b916e7e..dd9ed6a3bfe60 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java @@ -3,12 +3,24 @@ public class HalLink { private final String href; + private final String title; + private final String type; - public HalLink(String href) { + public HalLink(String href, String title, String type) { this.href = href; + this.title = title; + this.type = type; } public String getHref() { return href; } + + public String getTitle() { + return title; + } + + public String getType() { + return type; + } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java index 01873dba72aa5..7bcf0ab6cc227 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java @@ -12,6 +12,14 @@ public class HalLinkJacksonSerializer extends JsonSerializer { public void serialize(HalLink value, JsonGenerator generator, SerializerProvider serializers) throws IOException { generator.writeStartObject(); generator.writeObjectField("href", value.getHref()); + if (value.getTitle() != null) { + generator.writeObjectField("title", value.getTitle()); + } + + if (value.getType() != null) { + generator.writeObjectField("type", value.getType()); + } + generator.writeEndObject(); } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java index 60fd49d43e2a5..44fb1354b24d0 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java @@ -10,6 +10,14 @@ public class HalLinkJsonbSerializer implements JsonbSerializer { public void serialize(HalLink value, JsonGenerator generator, SerializationContext context) { generator.writeStartObject(); generator.write("href", value.getHref()); + if (value.getTitle() != null) { + generator.write("title", value.getTitle()); + } + + if (value.getType() != null) { + generator.write("type", value.getType()); + } + generator.writeEnd(); } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java index 16338be4d3aea..1e3fbc97deb20 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java @@ -24,7 +24,9 @@ public Map getLinks() { @SuppressWarnings("unused") public void addLinks(Link... links) { for (Link link : links) { - this.links.put(link.getRel(), new HalLink(link.getUri().toString())); + this.links.put(link.getRel(), new HalLink(link.getUri().toString(), + link.getTitle(), + link.getType())); } } } diff --git a/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java index 6da3709d42b2f..73e5ae4beabc0 100644 --- a/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java +++ b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java @@ -29,7 +29,7 @@ protected Map getInstanceLinks(Object entity) { private Map linksToMap(RESTServiceDiscovery serviceDiscovery) { Map links = new HashMap<>(serviceDiscovery.size()); for (RESTServiceDiscovery.AtomLink atomLink : serviceDiscovery) { - links.put(atomLink.getRel(), new HalLink(atomLink.getHref())); + links.put(atomLink.getRel(), new HalLink(atomLink.getHref(), atomLink.getTitle(), atomLink.getType())); } return links; } diff --git a/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java b/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java index 2a991448e49df..7c19b66d00eae 100644 --- a/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java +++ b/extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java @@ -62,6 +62,8 @@ private LinkInfo getLinkInfo(ResourceMethod resourceMethod, MethodInfo resourceM AnnotationInstance restLinkAnnotation, String resourceClassPath, IndexView index) { Type returnType = getNonAsyncReturnType(resourceMethodInfo.returnType()); String rel = getAnnotationValue(restLinkAnnotation, "rel", deductRel(resourceMethod, returnType, index)); + String title = getAnnotationValue(restLinkAnnotation, "title", null); + String type = getAnnotationValue(restLinkAnnotation, "type", null); String entityType = getAnnotationValue(restLinkAnnotation, "entityType", deductEntityType(returnType)); String path = UriBuilder.fromPath(resourceClassPath).path(resourceMethod.getPath()).toTemplate(); while (path.endsWith("/")) { @@ -69,7 +71,7 @@ private LinkInfo getLinkInfo(ResourceMethod resourceMethod, MethodInfo resourceM } Set pathParameters = getPathParameters(path); - return new LinkInfo(rel, entityType, path, pathParameters); + return new LinkInfo(rel, title, type, entityType, path, pathParameters); } /** diff --git a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java index 99f71ebaac2b0..4bb1d51d03d86 100644 --- a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java +++ b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java @@ -3,6 +3,9 @@ import static io.restassured.RestAssured.given; import static org.assertj.core.api.Assertions.assertThat; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; + import org.jboss.resteasy.reactive.common.util.RestMediaType; import org.junit.jupiter.api.Test; @@ -89,5 +92,19 @@ void shouldGetHalLinksForRestLinkId() { .thenReturn(); assertThat(response.body().jsonPath().getString("_links.self.href")).endsWith("/records/with-rest-link-id/100"); + assertThat(response.body().jsonPath().getString("_links.self.title")).isEqualTo("The with rest link title"); + assertThat(response.body().jsonPath().getString("_links.self.type")).isEqualTo(MediaType.APPLICATION_JSON); + } + + @Test + void shouldIncludeAllFieldsFromLink() { + Response response = given() + .header(HttpHeaders.ACCEPT, RestMediaType.APPLICATION_HAL_JSON) + .get("/records/with-rest-link-with-all-fields") + .thenReturn(); + + assertThat(response.body().jsonPath().getString("_links.all.href")).endsWith("/records/with-rest-link-id/100"); + assertThat(response.body().jsonPath().getString("_links.all.title")).isEqualTo("The title link"); + assertThat(response.body().jsonPath().getString("_links.all.type")).isEqualTo(MediaType.APPLICATION_JSON); } } diff --git a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java index 50255cd5f39cb..42750f5090a57 100644 --- a/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java +++ b/extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.reactive.links.deployment; +import java.net.URI; import java.time.Duration; import java.util.Arrays; import java.util.LinkedList; @@ -11,10 +12,12 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Link; import jakarta.ws.rs.core.MediaType; import org.jboss.resteasy.reactive.common.util.RestMediaType; +import io.quarkus.hal.HalEntityWrapper; import io.quarkus.resteasy.reactive.links.InjectRestLinks; import io.quarkus.resteasy.reactive.links.RestLink; import io.quarkus.resteasy.reactive.links.RestLinkType; @@ -172,7 +175,7 @@ public TestRecordWithPersistenceId getWithPersistenceId(@PathParam("id") int id) @GET @Path("/with-rest-link-id/{id}") @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) - @RestLink(entityType = TestRecordWithRestLinkId.class) + @RestLink(entityType = TestRecordWithRestLinkId.class, title = "The with rest link title", type = MediaType.APPLICATION_JSON) @InjectRestLinks public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) { return REST_LINK_ID_RECORDS.stream() @@ -181,4 +184,18 @@ public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) { .orElseThrow(NotFoundException::new); } + @GET + @Path("/with-rest-link-with-all-fields") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + public HalEntityWrapper getAllFieldsFromLink() { + + var entity = new TestRecordWithIdAndPersistenceIdAndRestLinkId(1, 10, 100, "one"); + return new HalEntityWrapper<>(entity, + Link.fromUri(URI.create("/records/with-rest-link-id/100")) + .rel("all") + .title("The title link") + .type(MediaType.APPLICATION_JSON) + .build()); + } + } diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java index cfa5316efb180..d083abcf59e61 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java @@ -25,6 +25,20 @@ */ String rel() default ""; + /** + * Intended for labelling the link with a human-readable identifier. + * + * @return the link title. + */ + String title() default ""; + + /** + * Hint to indicate the media type expected when dereferencing the target resource. + * + * @return the link expected media type. + */ + String type() default ""; + /** * Declares a link for the given type of resources. * If not set, it will default to the returning type of the annotated method. diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java index 45851358c6e3c..fda5c0f2e7dea 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java @@ -6,14 +6,20 @@ public final class LinkInfo { private final String rel; + private final String title; + + private final String type; + private final String entityType; private final String path; private final Set pathParameters; - public LinkInfo(String rel, String entityType, String path, Set pathParameters) { + public LinkInfo(String rel, String title, String type, String entityType, String path, Set pathParameters) { this.rel = rel; + this.title = title; + this.type = type; this.entityType = entityType; this.path = path; this.pathParameters = pathParameters; @@ -23,6 +29,14 @@ public String getRel() { return rel; } + public String getTitle() { + return title; + } + + public String getType() { + return type; + } + public String getEntityType() { return entityType; } diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java index f29ccc56c1710..787b4bb90254f 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/RestLinksProviderImpl.java @@ -37,9 +37,7 @@ public Collection getTypeLinks(Class elementType) { List links = new ArrayList<>(linkInfoList.size()); for (LinkInfo linkInfo : linkInfoList) { if (linkInfo.getPathParameters().size() == 0) { - links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) - .rel(linkInfo.getRel()) - .build()); + links.add(linkBuilderFor(linkInfo).build()); } } return links; @@ -52,13 +50,25 @@ public Collection getInstanceLinks(T instance) { List linkInfoList = linksContainer.getForClass(instance.getClass()); List links = new ArrayList<>(linkInfoList.size()); for (LinkInfo linkInfo : linkInfoList) { - links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) - .rel(linkInfo.getRel()) - .build(getPathParameterValues(linkInfo, instance))); + links.add(linkBuilderFor(linkInfo).build(getPathParameterValues(linkInfo, instance))); } return links; } + private Link.Builder linkBuilderFor(LinkInfo linkInfo) { + Link.Builder builder = Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath())) + .rel(linkInfo.getRel()); + if (linkInfo.getTitle() != null) { + builder.title(linkInfo.getTitle()); + } + + if (linkInfo.getType() != null) { + builder.type(linkInfo.getType()); + } + + return builder; + } + private Object[] getPathParameterValues(LinkInfo linkInfo, Object instance) { List values = new ArrayList<>(linkInfo.getPathParameters().size()); for (String name : linkInfo.getPathParameters()) { diff --git a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java index c7a58fa36e1d0..a3d66cc7f172f 100644 --- a/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java +++ b/extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java @@ -34,7 +34,7 @@ protected Map getInstanceLinks(Object entity) { private Map linksToMap(Collection refLinks) { Map links = new HashMap<>(); for (Link link : refLinks) { - links.put(link.getRel(), new HalLink(link.getUri().toString())); + links.put(link.getRel(), new HalLink(link.getUri().toString(), link.getTitle(), link.getType())); } return links;