Skip to content

Commit

Permalink
Fixes quarkusio#38543 - LinksProcessor ID field error for native clas…
Browse files Browse the repository at this point in the history
…s HalCollectionWrapper
  • Loading branch information
Bas Passon committed Feb 7, 2024
1 parent 11bef66 commit 9855040
Show file tree
Hide file tree
Showing 11 changed files with 168 additions and 31 deletions.
12 changes: 6 additions & 6 deletions docs/src/main/asciidoc/resteasy-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1869,22 +1869,22 @@ 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<T>` (to return a list of entities) or `HalEntityWrapper<T>` (to return a single object) as described in the following example:

[source,java]
----
@Path("/records")
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<Record> getAll() {
List<Record> list = // ...
HalCollectionWrapper halCollection = new HalCollectionWrapper(list, "collectionName", linksProvider.getTypeLinks(Record.class));
HalCollectionWrapper<Record> halCollection = halService.toHalCollectionWrapper( list, "collectionName", Record.class);
halCollection.addLinks(Link.fromPath("/records/1").rel("first-record").build());
return halCollection;
}
Expand All @@ -1894,9 +1894,9 @@ public class RecordsResource {
@Path("/{id}")
@RestLink(rel = "self")
@InjectRestLinks(RestLinkType.INSTANCE)
public HalEntityWrapper get(@PathParam("id") int id) {
public HalEntityWrapper<Record> get(@PathParam("id") int id) {
Record entity = // ...
HalEntityWrapper halEntity = new HalEntityWrapper(entity, linksProvider.getInstanceLinks(entity));
HalEntityWrapper<Record> halEntity = halService.toHalWrapper(entity);
halEntity.addLinks(Link.fromPath("/records/1/parent").rel("parent-record").build());
return halEntity;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,25 @@
* - the JSON-B serializer: {@link HalCollectionWrapperJsonbSerializer}
* - the Jackson serializer: {@link HalCollectionWrapperJacksonSerializer}
*/
public class HalCollectionWrapper extends HalWrapper {
public class HalCollectionWrapper<T> extends HalWrapper {

private final Collection<HalEntityWrapper> collection;
private final Collection<HalEntityWrapper<T>> collection;
private final String collectionName;

public HalCollectionWrapper(Collection<HalEntityWrapper> collection, String collectionName, Link... links) {
public HalCollectionWrapper(Collection<HalEntityWrapper<T>> collection, String collectionName, Link... links) {
this(collection, collectionName, new HashMap<>());

addLinks(links);
}

public HalCollectionWrapper(Collection<HalEntityWrapper> collection, String collectionName, Map<String, HalLink> links) {
public HalCollectionWrapper(Collection<HalEntityWrapper<T>> collection, String collectionName, Map<String, HalLink> links) {
super(links);

this.collection = collection;
this.collectionName = collectionName;
}

public Collection<HalEntityWrapper> getCollection() {
public Collection<HalEntityWrapper<T>> getCollection() {
return collection;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,33 @@
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

public class HalCollectionWrapperJacksonSerializer extends JsonSerializer<HalCollectionWrapper> {
public class HalCollectionWrapperJacksonSerializer extends JsonSerializer<HalCollectionWrapper<?>> {

@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);
writeLinks(wrapper, generator);
generator.writeEndObject();
}

private void writeEmbedded(HalCollectionWrapper wrapper, JsonGenerator generator, SerializerProvider serializers)
private void writeEmbedded(HalCollectionWrapper<?> wrapper, JsonGenerator generator, SerializerProvider serializers)
throws IOException {
JsonSerializer<Object> entitySerializer = serializers.findValueSerializer(HalEntityWrapper.class);

generator.writeFieldName("_embedded");
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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HalCollectionWrapper> {

@Override
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@
* - the JSON-B serializer: {@link HalEntityWrapperJsonbSerializer}
* - the Jackson serializer: {@link HalEntityWrapperJacksonSerializer}
*/
public class HalEntityWrapper extends HalWrapper {
public class HalEntityWrapper<T> 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<String, HalLink> links) {
public HalEntityWrapper(T entity, Map<String, HalLink> links) {
super(links);

this.entity = entity;
}

public Object getEntity() {
public T getEntity() {
return entity;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;

public class HalEntityWrapperJacksonSerializer extends JsonSerializer<HalEntityWrapper> {
public class HalEntityWrapperJacksonSerializer extends JsonSerializer<HalEntityWrapper<?>> {

@Override
public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, SerializerProvider serializers)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HalEntityWrapper> {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object> collection, String collectionName,
public <T> HalCollectionWrapper<T> toHalCollectionWrapper(Collection<T> collection, String collectionName,
Class<?> entityClass) {
List<HalEntityWrapper> items = new ArrayList<>();
for (Object entity : collection) {
List<HalEntityWrapper<T>> items = new ArrayList<>();
for (T entity : collection) {
items.add(toHalWrapper(entity));
}

Expand All @@ -36,7 +36,7 @@ public HalCollectionWrapper toHalCollectionWrapper(Collection<Object> collection
classLinks = getClassLinks(entityClass);
}

return new HalCollectionWrapper(items, collectionName, classLinks);
return new HalCollectionWrapper<>(items, collectionName, classLinks);
}

/**
Expand All @@ -45,8 +45,8 @@ public HalCollectionWrapper toHalCollectionWrapper(Collection<Object> collection
* @param entity The entity to wrap.
* @return The Hal entity wrapper.
*/
public HalEntityWrapper toHalWrapper(Object entity) {
return new HalEntityWrapper(entity, getInstanceLinks(entity));
public <T> HalEntityWrapper<T> toHalWrapper(T entity) {
return new HalEntityWrapper<>(entity, getInstanceLinks(entity));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hal-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TestRecordWithIdAndPersistenceIdAndRestLinkId> getRecords(@Context UriInfo uriInfo) {
List<TestRecordWithIdAndPersistenceIdAndRestLinkId> items = List.of(
new TestRecordWithIdAndPersistenceIdAndRestLinkId(1, 10, 100, "one"),
new TestRecordWithIdAndPersistenceIdAndRestLinkId(2, 20, 200, "two"));

HalCollectionWrapper<TestRecordWithIdAndPersistenceIdAndRestLinkId> 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<TestRecordWithIdAndPersistenceIdAndRestLinkId> getRecord(@PathParam("id") int id,
@Context UriInfo uriInfo) {

HalEntityWrapper<TestRecordWithIdAndPersistenceIdAndRestLinkId> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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");

}
}

0 comments on commit 9855040

Please sign in to comment.