From 9855040131e54c46ed4d8ef1a0ee587a3461b595 Mon Sep 17 00:00:00 2001 From: Bas Passon Date: Wed, 7 Feb 2024 13:50:36 +0100 Subject: [PATCH] Fixes #38543 - LinksProcessor ID field error for native class HalCollectionWrapper --- docs/src/main/asciidoc/resteasy-reactive.adoc | 12 ++-- .../io/quarkus/hal/HalCollectionWrapper.java | 10 +-- ...HalCollectionWrapperJacksonSerializer.java | 10 +-- .../HalCollectionWrapperJsonbSerializer.java | 8 ++- .../java/io/quarkus/hal/HalEntityWrapper.java | 10 +-- .../HalEntityWrapperJacksonSerializer.java | 2 +- .../hal/HalEntityWrapperJsonbSerializer.java | 2 + .../main/java/io/quarkus/hal/HalService.java | 12 ++-- .../deployment/pom.xml | 5 ++ .../links/deployment/HalWrapperResource.java | 63 ++++++++++++++++++ .../deployment/HalWrapperResourceTest.java | 65 +++++++++++++++++++ 11 files changed, 168 insertions(+), 31 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalWrapperResource.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalWrapperResourceTest.java diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 1b456e8b94b5f..c5f77edcd6eb4 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1869,7 +1869,7 @@ When we call a resource `/records/1` that returns only one instance, then the ou } ---- -Finally, you can also provide additional HAL links programmatically in your resource just by returning either `HalCollectionWrapper` (to return a list of entities) or `HalEntityWrapper` (to return a single object) as described in the following example: +Finally, you can also provide additional HAL links programmatically in your resource just by returning either `HalCollectionWrapper` (to return a list of entities) or `HalEntityWrapper` (to return a single object) as described in the following example: [source,java] ---- @@ -1877,14 +1877,14 @@ Finally, you can also provide additional HAL links programmatically in your reso public class RecordsResource { @Inject - RestLinksProvider linksProvider; + HalService halService; @GET @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) @RestLink(rel = "list") - public HalCollectionWrapper getAll() { + public HalCollectionWrapper getAll() { List list = // ... - HalCollectionWrapper halCollection = new HalCollectionWrapper(list, "collectionName", linksProvider.getTypeLinks(Record.class)); + HalCollectionWrapper halCollection = halService.toHalCollectionWrapper( list, "collectionName", Record.class); halCollection.addLinks(Link.fromPath("/records/1").rel("first-record").build()); return halCollection; } @@ -1894,9 +1894,9 @@ public class RecordsResource { @Path("/{id}") @RestLink(rel = "self") @InjectRestLinks(RestLinkType.INSTANCE) - public HalEntityWrapper get(@PathParam("id") int id) { + public HalEntityWrapper get(@PathParam("id") int id) { Record entity = // ... - HalEntityWrapper halEntity = new HalEntityWrapper(entity, linksProvider.getInstanceLinks(entity)); + HalEntityWrapper halEntity = halService.toHalWrapper(entity); halEntity.addLinks(Link.fromPath("/records/1/parent").rel("parent-record").build()); return halEntity; } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapper.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapper.java index 1a0caa75f83b5..fc86a00e6e6ff 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapper.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapper.java @@ -14,25 +14,25 @@ * - the JSON-B serializer: {@link HalCollectionWrapperJsonbSerializer} * - the Jackson serializer: {@link HalCollectionWrapperJacksonSerializer} */ -public class HalCollectionWrapper extends HalWrapper { +public class HalCollectionWrapper extends HalWrapper { - private final Collection collection; + private final Collection> collection; private final String collectionName; - public HalCollectionWrapper(Collection collection, String collectionName, Link... links) { + public HalCollectionWrapper(Collection> collection, String collectionName, Link... links) { this(collection, collectionName, new HashMap<>()); addLinks(links); } - public HalCollectionWrapper(Collection collection, String collectionName, Map links) { + public HalCollectionWrapper(Collection> collection, String collectionName, Map links) { super(links); this.collection = collection; this.collectionName = collectionName; } - public Collection getCollection() { + public Collection> getCollection() { return collection; } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJacksonSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJacksonSerializer.java index a922f9cec1c0b..82694c1e45ac5 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJacksonSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJacksonSerializer.java @@ -6,10 +6,10 @@ import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -public class HalCollectionWrapperJacksonSerializer extends JsonSerializer { +public class HalCollectionWrapperJacksonSerializer extends JsonSerializer> { @Override - public void serialize(HalCollectionWrapper wrapper, JsonGenerator generator, SerializerProvider serializers) + public void serialize(HalCollectionWrapper wrapper, JsonGenerator generator, SerializerProvider serializers) throws IOException { generator.writeStartObject(); writeEmbedded(wrapper, generator, serializers); @@ -17,7 +17,7 @@ public void serialize(HalCollectionWrapper wrapper, JsonGenerator generator, Ser generator.writeEndObject(); } - private void writeEmbedded(HalCollectionWrapper wrapper, JsonGenerator generator, SerializerProvider serializers) + private void writeEmbedded(HalCollectionWrapper wrapper, JsonGenerator generator, SerializerProvider serializers) throws IOException { JsonSerializer entitySerializer = serializers.findValueSerializer(HalEntityWrapper.class); @@ -25,14 +25,14 @@ private void writeEmbedded(HalCollectionWrapper wrapper, JsonGenerator generator generator.writeStartObject(); generator.writeFieldName(wrapper.getCollectionName()); generator.writeStartArray(); - for (HalEntityWrapper entity : wrapper.getCollection()) { + for (HalEntityWrapper entity : wrapper.getCollection()) { entitySerializer.serialize(entity, generator, serializers); } generator.writeEndArray(); generator.writeEndObject(); } - private void writeLinks(HalCollectionWrapper wrapper, JsonGenerator generator) throws IOException { + private void writeLinks(HalCollectionWrapper wrapper, JsonGenerator generator) throws IOException { generator.writeFieldName("_links"); generator.writeObject(wrapper.getLinks()); } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJsonbSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJsonbSerializer.java index 16ab71e68eafd..bf9964d4d89ad 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJsonbSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJsonbSerializer.java @@ -4,6 +4,8 @@ import jakarta.json.bind.serializer.SerializationContext; import jakarta.json.stream.JsonGenerator; +// Using the raw type here as eclipse yasson doesn't like custom serializers for +// generic root types, see https://github.com/eclipse-ee4j/yasson/issues/639 public class HalCollectionWrapperJsonbSerializer implements JsonbSerializer { @Override @@ -14,19 +16,19 @@ public void serialize(HalCollectionWrapper wrapper, JsonGenerator generator, Ser generator.writeEnd(); } - private void writeEmbedded(HalCollectionWrapper wrapper, JsonGenerator generator, SerializationContext context) { + private void writeEmbedded(HalCollectionWrapper wrapper, JsonGenerator generator, SerializationContext context) { generator.writeKey("_embedded"); generator.writeStartObject(); generator.writeKey(wrapper.getCollectionName()); generator.writeStartArray(); - for (HalEntityWrapper entity : wrapper.getCollection()) { + for (HalEntityWrapper entity : wrapper.getCollection()) { context.serialize(entity, generator); } generator.writeEnd(); generator.writeEnd(); } - private void writeLinks(HalCollectionWrapper wrapper, JsonGenerator generator, SerializationContext context) { + private void writeLinks(HalCollectionWrapper wrapper, JsonGenerator generator, SerializationContext context) { context.serialize("_links", wrapper.getLinks(), generator); } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapper.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapper.java index c908213f4dab5..0c1b18f58f651 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapper.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapper.java @@ -12,23 +12,23 @@ * - the JSON-B serializer: {@link HalEntityWrapperJsonbSerializer} * - the Jackson serializer: {@link HalEntityWrapperJacksonSerializer} */ -public class HalEntityWrapper extends HalWrapper { +public class HalEntityWrapper extends HalWrapper { - private final Object entity; + private final T entity; - public HalEntityWrapper(Object entity, Link... links) { + public HalEntityWrapper(T entity, Link... links) { this(entity, new HashMap<>()); addLinks(links); } - public HalEntityWrapper(Object entity, Map links) { + public HalEntityWrapper(T entity, Map links) { super(links); this.entity = entity; } - public Object getEntity() { + public T getEntity() { return entity; } } diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJacksonSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJacksonSerializer.java index 58e27950e82eb..d64b30ff54c86 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJacksonSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJacksonSerializer.java @@ -12,7 +12,7 @@ import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; -public class HalEntityWrapperJacksonSerializer extends JsonSerializer { +public class HalEntityWrapperJacksonSerializer extends JsonSerializer> { @Override public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, SerializerProvider serializers) diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java index 990febd478606..dc19ead70ebf9 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java @@ -10,6 +10,8 @@ import org.eclipse.yasson.internal.model.ClassModel; import org.eclipse.yasson.internal.model.PropertyModel; +// Using the raw type here as eclipse yasson doesn't like custom serializers for +// generic root types, see https://github.com/eclipse-ee4j/yasson/issues/639 public class HalEntityWrapperJsonbSerializer implements JsonbSerializer { @Override diff --git a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalService.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalService.java index a4fc08cf0bcc9..0f779c7c856a6 100644 --- a/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalService.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalService.java @@ -24,10 +24,10 @@ public abstract class HalService { * @param entityClass The class of the objects in the collection. If null, it will not resolve the links for these objects. * @return The Hal collection wrapper instance. */ - public HalCollectionWrapper toHalCollectionWrapper(Collection collection, String collectionName, + public HalCollectionWrapper toHalCollectionWrapper(Collection collection, String collectionName, Class entityClass) { - List items = new ArrayList<>(); - for (Object entity : collection) { + List> items = new ArrayList<>(); + for (T entity : collection) { items.add(toHalWrapper(entity)); } @@ -36,7 +36,7 @@ public HalCollectionWrapper toHalCollectionWrapper(Collection collection classLinks = getClassLinks(entityClass); } - return new HalCollectionWrapper(items, collectionName, classLinks); + return new HalCollectionWrapper<>(items, collectionName, classLinks); } /** @@ -45,8 +45,8 @@ public HalCollectionWrapper toHalCollectionWrapper(Collection collection * @param entity The entity to wrap. * @return The Hal entity wrapper. */ - public HalEntityWrapper toHalWrapper(Object entity) { - return new HalEntityWrapper(entity, getInstanceLinks(entity)); + public HalEntityWrapper toHalWrapper(T entity) { + return new HalEntityWrapper<>(entity, getInstanceLinks(entity)); } /** diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/pom.xml index d22a41c9656d3..564f0202cfe37 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/pom.xml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/pom.xml @@ -21,6 +21,11 @@ io.quarkus quarkus-resteasy-reactive-deployment + + io.quarkus + quarkus-hal-deployment + test + io.quarkus quarkus-junit5-internal diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalWrapperResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalWrapperResource.java new file mode 100644 index 0000000000000..1d57b81db96c0 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalWrapperResource.java @@ -0,0 +1,63 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import java.util.List; + +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Link; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.UriInfo; + +import org.jboss.resteasy.reactive.common.util.RestMediaType; + +import io.quarkus.hal.HalCollectionWrapper; +import io.quarkus.hal.HalEntityWrapper; +import io.quarkus.hal.HalService; +import io.quarkus.resteasy.reactive.links.InjectRestLinks; +import io.quarkus.resteasy.reactive.links.RestLink; +import io.quarkus.resteasy.reactive.links.RestLinkType; + +@Path("/hal") +public class HalWrapperResource { + + @Inject + HalService halService; + + @GET + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(rel = "list") + @InjectRestLinks + public HalCollectionWrapper getRecords(@Context UriInfo uriInfo) { + List items = List.of( + new TestRecordWithIdAndPersistenceIdAndRestLinkId(1, 10, 100, "one"), + new TestRecordWithIdAndPersistenceIdAndRestLinkId(2, 20, 200, "two")); + + HalCollectionWrapper halCollection = halService.toHalCollectionWrapper( + items, + "collectionName", TestRecordWithIdAndPersistenceIdAndRestLinkId.class); + halCollection.addLinks( + Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(String.format("/hal/%d", 1))).rel("first-record").build()); + + return halCollection; + } + + @GET + @Path("/{id}") + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(rel = "self") + @InjectRestLinks(RestLinkType.INSTANCE) + public HalEntityWrapper getRecord(@PathParam("id") int id, + @Context UriInfo uriInfo) { + + HalEntityWrapper halEntity = halService.toHalWrapper( + new TestRecordWithIdAndPersistenceIdAndRestLinkId(1, 10, 100, "one")); + halEntity.addLinks(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(String.format("/hal/%d/parent", id))) + .rel("parent-record").build()); + + return halEntity; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalWrapperResourceTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalWrapperResourceTest.java new file mode 100644 index 0000000000000..5a2c426416f1c --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalWrapperResourceTest.java @@ -0,0 +1,65 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import jakarta.ws.rs.core.HttpHeaders; + +import org.jboss.resteasy.reactive.common.util.RestMediaType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.builder.Version; +import io.quarkus.maven.dependency.Dependency; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.restassured.response.Response; + +public class HalWrapperResourceTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(HalWrapperResource.class, TestRecordWithIdAndPersistenceIdAndRestLinkId.class)) + .setForcedDependencies(List.of( + Dependency.of("io.quarkus", "quarkus-resteasy-reactive-jackson", Version.getVersion()), + Dependency.of("io.quarkus", "quarkus-hal", Version.getVersion()))); + + @TestHTTPResource("hal") + String recordsUrl; + + @TestHTTPResource("hal/{id}") + String recordIdUrl; + + @Test + void shouldGetAllRecordsWithCustomHalMetadata() { + Response response = given() + .header(HttpHeaders.ACCEPT, RestMediaType.APPLICATION_HAL_JSON) + .get(recordsUrl).thenReturn(); + + assertThat(response.body().jsonPath().getString("_embedded['collectionName'][0].restLinkId")).isEqualTo("1"); + assertThat(response.body().jsonPath().getString("_embedded['collectionName'][0]._links.self.href")).endsWith("/hal/1"); + assertThat(response.body().jsonPath().getString("_embedded['collectionName'][0]._links.list.href")).endsWith("/hal"); + assertThat(response.body().jsonPath().getString("_embedded['collectionName'][1].restLinkId")).isEqualTo("2"); + assertThat(response.body().jsonPath().getString("_embedded['collectionName'][1]._links.self.href")).endsWith("/hal/2"); + assertThat(response.body().jsonPath().getString("_embedded['collectionName'][1]._links.list.href")).endsWith("/hal"); + assertThat(response.body().jsonPath().getString("_links.first-record.href")).endsWith("/hal/1"); + assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/hal"); + } + + @Test + void shouldGetSingleRecordWithCustomHalMetadata() { + Response response = given() + .header(HttpHeaders.ACCEPT, RestMediaType.APPLICATION_HAL_JSON) + .get(recordIdUrl, 1L) + .thenReturn(); + + assertThat(response.body().jsonPath().getString("restLinkId")).isEqualTo("1"); + assertThat(response.body().jsonPath().getString("_links.parent-record.href")).endsWith("/hal/1/parent"); + assertThat(response.body().jsonPath().getString("_links.self.href")).endsWith("/hal/1"); + assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/hal"); + + } +}