diff --git a/bom/application/pom.xml b/bom/application/pom.xml index b4a837c21fecd..95cf2dbef1a3d 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -537,6 +537,16 @@ quarkus-jsonp-deployment ${project.version} + + io.quarkus + quarkus-hal + ${project.version} + + + io.quarkus + quarkus-hal-deployment + ${project.version} + io.quarkus quarkus-resteasy-reactive-kotlin-serialization diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java index 79b8cafffe276..5af46ef4d0b2f 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Capability.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Capability.java @@ -35,6 +35,8 @@ public interface Capability { String JSONB = QUARKUS_PREFIX + "jsonb"; + String HAL = QUARKUS_PREFIX + "hal"; + String REST = QUARKUS_PREFIX + "rest"; String REST_CLIENT = REST + ".client"; String REST_JACKSON = REST + ".jackson"; diff --git a/devtools/bom-descriptor-json/pom.xml b/devtools/bom-descriptor-json/pom.xml index 0a391d1d2d568..3c749aff0120f 100644 --- a/devtools/bom-descriptor-json/pom.xml +++ b/devtools/bom-descriptor-json/pom.xml @@ -656,6 +656,19 @@ + + io.quarkus + quarkus-hal + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-hibernate-envers diff --git a/docs/pom.xml b/docs/pom.xml index 835c8bab9fc95..ed900eb7658a8 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -616,6 +616,19 @@ + + io.quarkus + quarkus-hal-deployment + ${project.version} + pom + test + + + * + * + + + io.quarkus quarkus-hibernate-envers-deployment diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index 6d0705a184766..24fcdfca77192 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1231,6 +1231,147 @@ public class RecordsResource { Using this injected bean of type `RestLinksProvider`, you can get the links by type using the method `RestLinksProvider.getTypeLinks` or get the links by a concrete instance using the method `RestLinksProvider.getInstanceLinks`. +==== JSON Hypertext Application Language (HAL) support + +The https://tools.ietf.org/id/draft-kelly-json-hal-01.html[HAL] standard is a simple format to represent web links. + +To enable the HAL support, add the `quarkus-hal` extension to your project. Also, as HAL needs JSON support, you need to add either the `quarkus-resteasy-reactive-jsonb` or the `quarkus-resteasy-reactive-jackson` extension. + +.Table Context object +|=== +|GAV|Usage + +|`io.quarkus:quarkus-hal` +|https://tools.ietf.org/id/draft-kelly-json-hal-01.html[HAL] + +|=== + +After adding the extensions, we can now annotate the REST resources to produce the media type `application/hal+json` (or use RestMediaType.APPLICATION_HAL_JSON). For example: + +[source,java] +---- +@Path("/records") +public class RecordsResource { + + @GET + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(rel = "list") + @InjectRestLinks + public List getAll() { + // ... + } + + @GET + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @Path("/{id}") + @RestLink(rel = "self") + @InjectRestLinks(RestLinkType.INSTANCE) + public TestRecord get(@PathParam("id") int id) { + // ... + } +} +---- + +Now, the endpoints `/records` and `/records/{id}` will accept the media type both `json` and `hal+json` to print the records in Hal format. + +For example, if we invoke the `/records` endpoint using curl to return a list of records, the HAL format will look like as follows: + +[source,bash] +---- +& curl -H "Accept:application/hal+json" -i localhost:8080/records +{ + "_embedded": { + "items": [ + { + "id": 1, + "slug": "first", + "value": "First value", + "_links": { + "self": { + "href": "http://localhost:8081/records/1" + }, + "list": { + "href": "http://localhost:8081/records" + } + } + }, + { + "id": 2, + "slug": "second", + "value": "Second value", + "_links": { + "self": { + "href": "http://localhost:8081/records/2" + }, + "list": { + "href": "http://localhost:8081/records" + } + } + } + ] + }, + "_links": { + "list": { + "href": "http://localhost:8081/records" + } + } +} +---- + +When we call a resource `/records/1` that returns only one instance, then the output is: + +[source,bash] +---- +& curl -H "Accept:application/hal+json" -i localhost:8080/records/1 +{ + "id": 1, + "slug": "first", + "value": "First value", + "_links": { + "self": { + "href": "http://localhost:8081/records/1" + }, + "list": { + "href": "http://localhost:8081/records" + } + } +} +---- + +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] +---- +@Path("/records") +public class RecordsResource { + + @Inject + RestLinksProvider linksProvider; + + @GET + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @RestLink(rel = "list") + public HalCollectionWrapper getAll() { + List list = // ... + HalCollectionWrapper halCollection = new HalCollectionWrapper(list, "collectionName", linksProvider.getTypeLinks(Record.class)); + halCollection.addLinks(Link.fromPath("/records/1").rel("first-record").build()); + return halCollection; + } + + @GET + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) + @Path("/{id}") + @RestLink(rel = "self") + @InjectRestLinks(RestLinkType.INSTANCE) + public HalEntityWrapper get(@PathParam("id") int id) { + Record entity = // ... + HalEntityWrapper halEntity = new HalEntityWrapper(entity, linksProvider.getInstanceLinks(entity)); + halEntity.addLinks(Link.fromPath("/records/1/parent").rel("parent-record").build()); + return halEntity; + } +} +---- + == CORS filter link:https://en.wikipedia.org/wiki/Cross-origin_resource_sharing[Cross-origin resource sharing] (CORS) is a mechanism that diff --git a/docs/src/main/asciidoc/resteasy.adoc b/docs/src/main/asciidoc/resteasy.adoc index 8fb011682fa01..1ffc2601f4e24 100644 --- a/docs/src/main/asciidoc/resteasy.adoc +++ b/docs/src/main/asciidoc/resteasy.adoc @@ -290,6 +290,111 @@ public class CustomJsonbConfig { } ---- +[[links]] +=== JSON Hypertext Application Language (HAL) support + +The https://tools.ietf.org/id/draft-kelly-json-hal-01.html[HAL] standard is a simple format to represent web links. + +To enable the HAL support, add the `quarkus-hal` extension to your project. Also, as HAL needs JSON support, you need to add either the `quarkus-resteasy-jsonb` or the `quarkus-resteasy-jackson` extension. + +.Table Context object +|=== +|GAV|Usage + +|`io.quarkus:quarkus-hal` +|https://tools.ietf.org/id/draft-kelly-json-hal-01.html[HAL] + +|=== + +After adding the extensions, we can now annotate the REST resources to produce the media type `application/hal+json` (or use RestMediaType.APPLICATION_HAL_JSON). For example: + +[source,java] +---- +@Path("/records") +public class RecordsResource { + + @GET + @Produces({ MediaType.APPLICATION_JSON, "application/hal+json" }) + @LinkResource(entityClassName = "org.acme.Record", rel = "list") + public List getAll() { + // ... + } + + @GET + @Path("/first") + @Produces({ MediaType.APPLICATION_JSON, "application/hal+json" }) + @LinkResource(rel = "first") + public TestRecord getFirst() { + // ... + } +} +---- + +Now, the endpoints `/records` and `/records/first` will accept the media type both `json` and `hal+json` to print the records in Hal format. + +For example, if we invoke the `/records` endpoint using curl to return a list of records, the HAL format will look like as follows: + +[source,bash] +---- +& curl -H "Accept:application/hal+json" -i localhost:8080/records +{ + "_embedded": { + "items": [ + { + "id": 1, + "slug": "first", + "value": "First value", + "_links": { + "list": { + "href": "http://localhost:8081/records" + }, + "first": { + "href": "http://localhost:8081/records/first" + } + } + }, + { + "id": 2, + "slug": "second", + "value": "Second value", + "_links": { + "list": { + "href": "http://localhost:8081/records" + }, + "first": { + "href": "http://localhost:8081/records/first" + } + } + } + ] + }, + "_links": { + "list": { + "href": "http://localhost:8081/records" + } + } +} +---- + +When we call a resource `/records/first` that returns only one instance, then the output is: + +[source,bash] +---- +& curl -H "Accept:application/hal+json" -i localhost:8080/records/first +{ + "id": 1, + "slug": "first", + "value": "First value", + "_links": { + "list": { + "href": "http://localhost:8081/records" + }, + "first": { + "href": "http://localhost:8081/records/first" + } + } +} +---- == Creating a frontend diff --git a/extensions/hal/deployment/pom.xml b/extensions/hal/deployment/pom.xml new file mode 100644 index 0000000000000..db8ffa13a6051 --- /dev/null +++ b/extensions/hal/deployment/pom.xml @@ -0,0 +1,60 @@ + + + + quarkus-hal-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-hal-deployment + Quarkus - HAL - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-hal + + + io.quarkus + quarkus-jackson-spi + + + io.quarkus + quarkus-jsonb-spi + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + diff --git a/extensions/hal/deployment/src/main/java/io/quarkus/hal/deployment/HalProcessor.java b/extensions/hal/deployment/src/main/java/io/quarkus/hal/deployment/HalProcessor.java new file mode 100755 index 0000000000000..d8988b7c78d2e --- /dev/null +++ b/extensions/hal/deployment/src/main/java/io/quarkus/hal/deployment/HalProcessor.java @@ -0,0 +1,43 @@ +package io.quarkus.hal.deployment; + +import java.util.Arrays; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.hal.HalCollectionWrapper; +import io.quarkus.hal.HalCollectionWrapperJacksonSerializer; +import io.quarkus.hal.HalCollectionWrapperJsonbSerializer; +import io.quarkus.hal.HalEntityWrapper; +import io.quarkus.hal.HalEntityWrapperJacksonSerializer; +import io.quarkus.hal.HalEntityWrapperJsonbSerializer; +import io.quarkus.hal.HalLink; +import io.quarkus.hal.HalLinkJacksonSerializer; +import io.quarkus.hal.HalLinkJsonbSerializer; +import io.quarkus.jackson.spi.JacksonModuleBuildItem; +import io.quarkus.jsonb.spi.JsonbSerializerBuildItem; + +public class HalProcessor { + + @BuildStep + ReflectiveClassBuildItem registerReflection() { + return new ReflectiveClassBuildItem(true, true, HalLink.class); + } + + @BuildStep + JacksonModuleBuildItem registerJacksonSerializers() { + return new JacksonModuleBuildItem.Builder("hal-wrappers") + .addSerializer(HalEntityWrapperJacksonSerializer.class.getName(), HalEntityWrapper.class.getName()) + .addSerializer(HalCollectionWrapperJacksonSerializer.class.getName(), HalCollectionWrapper.class.getName()) + .addSerializer(HalLinkJacksonSerializer.class.getName(), HalLink.class.getName()) + .build(); + } + + @BuildStep + JsonbSerializerBuildItem registerJsonbSerializers() { + return new JsonbSerializerBuildItem(Arrays.asList( + HalEntityWrapperJsonbSerializer.class.getName(), + HalCollectionWrapperJsonbSerializer.class.getName(), + HalLinkJsonbSerializer.class.getName())); + } + +} diff --git a/extensions/hal/pom.xml b/extensions/hal/pom.xml new file mode 100644 index 0000000000000..567cb6c9fc0c3 --- /dev/null +++ b/extensions/hal/pom.xml @@ -0,0 +1,20 @@ + + + + quarkus-extensions-parent + io.quarkus + 999-SNAPSHOT + ../pom.xml + + 4.0.0 + + quarkus-hal-parent + Quarkus - HAL + pom + + deployment + runtime + + diff --git a/extensions/hal/runtime/pom.xml b/extensions/hal/runtime/pom.xml new file mode 100644 index 0000000000000..85bd6f92679fe --- /dev/null +++ b/extensions/hal/runtime/pom.xml @@ -0,0 +1,70 @@ + + + + quarkus-hal-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-hal + Quarkus - HAL - Runtime + Hypertext Application Language (HAL) support + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-arc + + + org.jboss.spec.javax.ws.rs + jboss-jaxrs-api_2.1_spec + + + io.quarkus + quarkus-jackson + true + + + io.quarkus + quarkus-jsonb + true + + + org.junit.jupiter + junit-jupiter + test + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + io.quarkus.hal + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + 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 new file mode 100644 index 0000000000000..6b1bcb8f64100 --- /dev/null +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapper.java @@ -0,0 +1,43 @@ +package io.quarkus.hal; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.Link; + +/** + * The Hal collection wrapper that includes the list of Hal entities {@link HalEntityWrapper}, the collection name and the Hal + * links. + * + * This type is serialized into Json using: + * - the JSON-B serializer: {@link HalCollectionWrapperJsonbSerializer} + * - the Jackson serializer: {@link HalCollectionWrapperJacksonSerializer} + */ +public class HalCollectionWrapper extends HalWrapper { + + private final Collection collection; + private final String collectionName; + + public HalCollectionWrapper(Collection collection, String collectionName, Link... links) { + this(collection, collectionName, new HashMap<>()); + + addLinks(links); + } + + public HalCollectionWrapper(Collection collection, String collectionName, Map links) { + super(links); + + this.collection = collection; + this.collectionName = collectionName; + } + + public Collection getCollection() { + return collection; + } + + public String getCollectionName() { + return collectionName; + } + +} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJacksonSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJacksonSerializer.java similarity index 62% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJacksonSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJacksonSerializer.java index 28148fc8899a4..a922f9cec1c0b 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJacksonSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJacksonSerializer.java @@ -1,7 +1,6 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; import java.io.IOException; -import java.util.Map; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; @@ -9,16 +8,6 @@ public class HalCollectionWrapperJacksonSerializer extends JsonSerializer { - private final HalLinksProvider linksExtractor; - - public HalCollectionWrapperJacksonSerializer() { - this.linksExtractor = new RestEasyHalLinksProvider(); - } - - HalCollectionWrapperJacksonSerializer(HalLinksProvider linksExtractor) { - this.linksExtractor = linksExtractor; - } - @Override public void serialize(HalCollectionWrapper wrapper, JsonGenerator generator, SerializerProvider serializers) throws IOException { @@ -35,18 +24,16 @@ private void writeEmbedded(HalCollectionWrapper wrapper, JsonGenerator generator generator.writeFieldName("_embedded"); generator.writeStartObject(); generator.writeFieldName(wrapper.getCollectionName()); - generator.writeStartArray(wrapper.getCollection().size()); - for (Object entity : wrapper.getCollection()) { - entitySerializer.serialize(new HalEntityWrapper(entity), generator, serializers); + generator.writeStartArray(); + for (HalEntityWrapper entity : wrapper.getCollection()) { + entitySerializer.serialize(entity, generator, serializers); } generator.writeEndArray(); generator.writeEndObject(); } private void writeLinks(HalCollectionWrapper wrapper, JsonGenerator generator) throws IOException { - Map links = linksExtractor.getLinks(wrapper.getElementType()); - links.putAll(wrapper.getLinks()); generator.writeFieldName("_links"); - generator.writeObject(links); + generator.writeObject(wrapper.getLinks()); } } diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJsonbSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJsonbSerializer.java similarity index 60% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJsonbSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJsonbSerializer.java index 76e9f32016604..c12e137b805d0 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapperJsonbSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalCollectionWrapperJsonbSerializer.java @@ -1,6 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.util.Map; +package io.quarkus.hal; import javax.json.bind.serializer.JsonbSerializer; import javax.json.bind.serializer.SerializationContext; @@ -8,16 +6,6 @@ public class HalCollectionWrapperJsonbSerializer implements JsonbSerializer { - private final HalLinksProvider linksExtractor; - - public HalCollectionWrapperJsonbSerializer() { - this.linksExtractor = new RestEasyHalLinksProvider(); - } - - HalCollectionWrapperJsonbSerializer(HalLinksProvider linksExtractor) { - this.linksExtractor = linksExtractor; - } - @Override public void serialize(HalCollectionWrapper wrapper, JsonGenerator generator, SerializationContext context) { generator.writeStartObject(); @@ -31,16 +19,14 @@ private void writeEmbedded(HalCollectionWrapper wrapper, JsonGenerator generator generator.writeStartObject(); generator.writeKey(wrapper.getCollectionName()); generator.writeStartArray(); - for (Object entity : wrapper.getCollection()) { - context.serialize(new HalEntityWrapper(entity), generator); + for (HalEntityWrapper entity : wrapper.getCollection()) { + context.serialize(entity, generator); } generator.writeEnd(); generator.writeEnd(); } private void writeLinks(HalCollectionWrapper wrapper, JsonGenerator generator, SerializationContext context) { - Map links = linksExtractor.getLinks(wrapper.getElementType()); - links.putAll(wrapper.getLinks()); - context.serialize("_links", links, generator); + 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 new file mode 100644 index 0000000000000..24b3a1e6b0852 --- /dev/null +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapper.java @@ -0,0 +1,34 @@ +package io.quarkus.hal; + +import java.util.HashMap; +import java.util.Map; + +import javax.ws.rs.core.Link; + +/** + * The Hal entity wrapper that includes the entity and the Hal links. + * + * This type is serialized into Json using: + * - the JSON-B serializer: {@link HalEntityWrapperJsonbSerializer} + * - the Jackson serializer: {@link HalEntityWrapperJacksonSerializer} + */ +public class HalEntityWrapper extends HalWrapper { + + private final Object entity; + + public HalEntityWrapper(Object entity, Link... links) { + this(entity, new HashMap<>()); + + addLinks(links); + } + + public HalEntityWrapper(Object entity, Map links) { + super(links); + + this.entity = entity; + } + + public Object getEntity() { + return entity; + } +} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJacksonSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJacksonSerializer.java similarity index 72% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJacksonSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJacksonSerializer.java index 8efc74b8b5d83..58e27950e82eb 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJacksonSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJacksonSerializer.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; import java.io.IOException; import java.util.List; @@ -14,25 +14,16 @@ public class HalEntityWrapperJacksonSerializer extends JsonSerializer { - private final HalLinksProvider linksExtractor; - - public HalEntityWrapperJacksonSerializer() { - this.linksExtractor = new RestEasyHalLinksProvider(); - } - - HalEntityWrapperJacksonSerializer(HalLinksProvider linksExtractor) { - this.linksExtractor = linksExtractor; - } - @Override public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, SerializerProvider serializers) throws IOException { + Object entity = wrapper.getEntity(); generator.writeStartObject(); - for (BeanPropertyDefinition property : getPropertyDefinitions(serializers, wrapper.getEntity().getClass())) { + for (BeanPropertyDefinition property : getPropertyDefinitions(serializers, entity.getClass())) { AnnotatedMember accessor = property.getAccessor(); if (accessor != null) { - Object value = accessor.getValue(wrapper.getEntity()); + Object value = accessor.getValue(entity); generator.writeFieldName(property.getName()); if (value == null) { generator.writeNull(); @@ -41,12 +32,11 @@ public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, Seriali } } } - writeLinks(wrapper.getEntity(), generator); + writeLinks(wrapper.getLinks(), generator); generator.writeEndObject(); } - private void writeLinks(Object entity, JsonGenerator generator) throws IOException { - Map links = linksExtractor.getLinks(entity); + private void writeLinks(Map links, JsonGenerator generator) throws IOException { generator.writeFieldName("_links"); generator.writeObject(links); } diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJsonbSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java similarity index 74% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJsonbSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java index b860fbbcbe92e..a1d83f4bb3353 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapperJsonbSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalEntityWrapperJsonbSerializer.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; import java.util.Map; @@ -12,16 +12,6 @@ public class HalEntityWrapperJsonbSerializer implements JsonbSerializer { - private final HalLinksProvider linksExtractor; - - public HalEntityWrapperJsonbSerializer() { - this.linksExtractor = new RestEasyHalLinksProvider(); - } - - HalEntityWrapperJsonbSerializer(HalLinksProvider linksExtractor) { - this.linksExtractor = linksExtractor; - } - @Override public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, SerializationContext context) { Marshaller marshaller = (Marshaller) context; @@ -41,7 +31,7 @@ public void serialize(HalEntityWrapper wrapper, JsonGenerator generator, Seriali } } - writeLinks(entity, generator, context); + writeLinks(wrapper.getLinks(), generator, context); generator.writeEnd(); } finally { marshaller.removeProcessedObject(entity); @@ -56,8 +46,7 @@ private void writeValue(String name, Object value, JsonGenerator generator, Seri } } - private void writeLinks(Object entity, JsonGenerator generator, SerializationContext context) { - Map links = linksExtractor.getLinks(entity); + private void writeLinks(Map links, JsonGenerator generator, SerializationContext context) { context.serialize("_links", links, generator); } } diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLink.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java similarity index 78% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLink.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java index 6b4b24a0a4c0a..accf86b916e7e 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLink.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; public class HalLink { diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJacksonSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java similarity index 91% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJacksonSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java index 4ff10038509b4..01873dba72aa5 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJacksonSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; import java.io.IOException; diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJsonbSerializer.java b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java similarity index 90% rename from extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJsonbSerializer.java rename to extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java index 3f5b0c407883f..f4759eb53bf99 100644 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinkJsonbSerializer.java +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java @@ -1,4 +1,4 @@ -package io.quarkus.rest.data.panache.runtime.hal; +package io.quarkus.hal; import javax.json.bind.serializer.JsonbSerializer; import javax.json.bind.serializer.SerializationContext; 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 new file mode 100644 index 0000000000000..a4fc08cf0bcc9 --- /dev/null +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalService.java @@ -0,0 +1,82 @@ +package io.quarkus.hal; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Service with Hal utilities. This service is used by the Resteasy Links, Resteasy Reactive Links and the + * Rest Data Panache extensions. + */ +@SuppressWarnings("unused") +public abstract class HalService { + + private static final String SELF_REF = "self"; + + /** + * Wrap a collection of objects into a Hal collection wrapper by resolving the Hal links. + * The Hal collection wrapper is then serialized by either json or jackson. + * + * @param collection The collection of objects to wrap. + * @param collectionName The name that will include the collection of objects within the `_embedded` Hal object. + * @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, + Class entityClass) { + List items = new ArrayList<>(); + for (Object entity : collection) { + items.add(toHalWrapper(entity)); + } + + Map classLinks = Collections.emptyMap(); + if (entityClass != null) { + classLinks = getClassLinks(entityClass); + } + + return new HalCollectionWrapper(items, collectionName, classLinks); + } + + /** + * Wrap an entity into a Hal instance by including the entity itself and the Hal links. + * + * @param entity The entity to wrap. + * @return The Hal entity wrapper. + */ + public HalEntityWrapper toHalWrapper(Object entity) { + return new HalEntityWrapper(entity, getInstanceLinks(entity)); + } + + /** + * Get the HREF link with reference `self` from the Hal links of the entity instance. + * + * @param entity The entity instance where to get the Hal links. + * @return the HREF link with rel `self`. + */ + public String getSelfLink(Object entity) { + HalLink halLink = getInstanceLinks(entity).get(SELF_REF); + if (halLink != null) { + return halLink.getHref(); + } + + return null; + } + + /** + * Get the Hal links using the entity type class. + * + * @param entityClass The entity class to get the Hal links. + * @return a map with the Hal links which keys are the rel attributes, and the values are the href attributes. + */ + protected abstract Map getClassLinks(Class entityClass); + + /** + * Get the Hal links using the entity instance. + * + * @param entity the Object instance. + * @return a map with the Hal links which keys are the rel attributes, and the values are the href attributes. + */ + protected abstract Map getInstanceLinks(Object entity); +} 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 new file mode 100644 index 0000000000000..80d5953b21f2b --- /dev/null +++ b/extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java @@ -0,0 +1,30 @@ +package io.quarkus.hal; + +import java.util.Map; + +import javax.ws.rs.core.Link; + +public abstract class HalWrapper { + + private final Map links; + + public HalWrapper(Map links) { + this.links = links; + } + + public Map getLinks() { + return links; + } + + /** + * This method is used by Rest Data Panache to programmatically add links to the Hal wrapper. + * + * @param links The links to add into the Hal wrapper. + */ + @SuppressWarnings("unused") + public void addLinks(Link... links) { + for (Link link : links) { + this.links.put(link.getRel(), new HalLink(link.getUri().toString())); + } + } +} diff --git a/extensions/hal/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/hal/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..67954d3344abf --- /dev/null +++ b/extensions/hal/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,15 @@ +--- +artifact: ${project.groupId}:${project.artifactId}:${project.version} +name: "Hypertext Application Language (HAL)" +metadata: + keywords: + - "jsonb" + - "json-b" + - "jackson" + - "hal" + - "rest" + - "jaxrs" + - "links" + categories: + - "web" + status: "experimental" \ No newline at end of file diff --git a/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml b/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml index 0a23562bece62..7da5feefe7a9b 100644 --- a/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml +++ b/extensions/panache/hibernate-orm-rest-data-panache/deployment/pom.xml @@ -40,6 +40,11 @@ quarkus-resteasy-jsonb-deployment test + + io.quarkus + quarkus-resteasy-links-deployment + test + io.rest-assured rest-assured diff --git a/extensions/panache/rest-data-panache/deployment/pom.xml b/extensions/panache/rest-data-panache/deployment/pom.xml index 66c1b0e79bc43..65c66c9a3ad3b 100644 --- a/extensions/panache/rest-data-panache/deployment/pom.xml +++ b/extensions/panache/rest-data-panache/deployment/pom.xml @@ -29,14 +29,9 @@ io.quarkus quarkus-resteasy-common-spi - - - io.quarkus - quarkus-jackson-spi - io.quarkus - quarkus-jsonb-spi + quarkus-hal-deployment diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java index 2e46e264a0063..0691e13bec031 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/JaxRsResourceImplementor.java @@ -19,10 +19,7 @@ import io.quarkus.rest.data.panache.deployment.methods.ListMethodImplementor; import io.quarkus.rest.data.panache.deployment.methods.MethodImplementor; import io.quarkus.rest.data.panache.deployment.methods.UpdateMethodImplementor; -import io.quarkus.rest.data.panache.deployment.methods.hal.AddHalMethodImplementor; -import io.quarkus.rest.data.panache.deployment.methods.hal.GetHalMethodImplementor; import io.quarkus.rest.data.panache.deployment.methods.hal.ListHalMethodImplementor; -import io.quarkus.rest.data.panache.deployment.methods.hal.UpdateHalMethodImplementor; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; import io.quarkus.runtime.util.HashUtil; @@ -36,16 +33,14 @@ class JaxRsResourceImplementor { private final List methodImplementors; JaxRsResourceImplementor(boolean withValidation, boolean isResteasyClassic, boolean isReactivePanache) { - this.methodImplementors = Arrays.asList( - new GetMethodImplementor(isResteasyClassic, isReactivePanache), - new GetHalMethodImplementor(isResteasyClassic, isReactivePanache), + this.methodImplementors = Arrays.asList(new GetMethodImplementor(isResteasyClassic, isReactivePanache), new ListMethodImplementor(isResteasyClassic, isReactivePanache), - new ListHalMethodImplementor(isResteasyClassic, isReactivePanache), new AddMethodImplementor(withValidation, isResteasyClassic, isReactivePanache), - new AddHalMethodImplementor(withValidation, isResteasyClassic, isReactivePanache), new UpdateMethodImplementor(withValidation, isResteasyClassic, isReactivePanache), - new UpdateHalMethodImplementor(withValidation, isResteasyClassic, isReactivePanache), - new DeleteMethodImplementor(isResteasyClassic, isReactivePanache)); + new DeleteMethodImplementor(isResteasyClassic, isReactivePanache), + // The list hal endpoint needs to be added for both resteasy classic and resteasy reactive + // because the pagination links are programmatically added. + new ListHalMethodImplementor(isResteasyClassic, isReactivePanache)); } /** diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java index a9fe647b86743..c1843ab0e6a7f 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/RestDataProcessor.java @@ -1,10 +1,8 @@ package io.quarkus.rest.data.panache.deployment; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.deployment.Capabilities; @@ -12,25 +10,11 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; -import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.gizmo.ClassOutput; -import io.quarkus.jackson.spi.JacksonModuleBuildItem; -import io.quarkus.jsonb.spi.JsonbSerializerBuildItem; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; import io.quarkus.rest.data.panache.deployment.properties.ResourcePropertiesBuildItem; import io.quarkus.rest.data.panache.deployment.properties.ResourcePropertiesProvider; -import io.quarkus.rest.data.panache.runtime.hal.HalCollectionWrapper; -import io.quarkus.rest.data.panache.runtime.hal.HalCollectionWrapperJacksonSerializer; -import io.quarkus.rest.data.panache.runtime.hal.HalCollectionWrapperJsonbSerializer; -import io.quarkus.rest.data.panache.runtime.hal.HalEntityWrapper; -import io.quarkus.rest.data.panache.runtime.hal.HalEntityWrapperJacksonSerializer; -import io.quarkus.rest.data.panache.runtime.hal.HalEntityWrapperJsonbSerializer; -import io.quarkus.rest.data.panache.runtime.hal.HalLink; -import io.quarkus.rest.data.panache.runtime.hal.HalLinkJacksonSerializer; -import io.quarkus.rest.data.panache.runtime.hal.HalLinkJsonbSerializer; -import io.quarkus.rest.data.panache.runtime.resource.RESTEasyClassicResourceLinksProvider; -import io.quarkus.rest.data.panache.runtime.resource.RESTEasyReactiveResourceLinksProvider; import io.quarkus.rest.data.panache.runtime.sort.SortQueryParamFilter; import io.quarkus.rest.data.panache.runtime.sort.SortQueryParamValidator; import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; @@ -40,14 +24,8 @@ public class RestDataProcessor { - @BuildStep - ReflectiveClassBuildItem registerReflection() { - return new ReflectiveClassBuildItem(true, true, HalLink.class); - } - @BuildStep void supportingBuildItems(Capabilities capabilities, - BuildProducer additionalBeanBuildItemBuildProducer, BuildProducer runtimeInitializedClassBuildItemBuildProducer, BuildProducer resteasyJaxrsProviderBuildItemBuildProducer, BuildProducer containerRequestFilterBuildItemBuildProducer) { @@ -56,14 +34,9 @@ void supportingBuildItems(Capabilities capabilities, if (!isResteasyClassicAvailable && !isResteasyReactiveAvailable) { throw new IllegalStateException( - "REST Data Panache can only work if 'quarkus-resteasy' or 'quarkus-resteasy-reactive-links' is present"); + "REST Data Panache can only work if 'quarkus-resteasy' or 'quarkus-resteasy-reactive' is present"); } - String className = isResteasyClassicAvailable ? RESTEasyClassicResourceLinksProvider.class.getName() - : RESTEasyReactiveResourceLinksProvider.class.getName(); - additionalBeanBuildItemBuildProducer - .produce(AdditionalBeanBuildItem.builder().addBeanClass(className).setUnremovable().build()); - if (isResteasyClassicAvailable) { runtimeInitializedClassBuildItemBuildProducer .produce(new RuntimeInitializedClassBuildItem("org.jboss.resteasy.links.impl.EL")); @@ -100,9 +73,15 @@ void implementResources(CombinedIndexBuildItem index, List resourcePropertiesBuildItems) { for (ResourcePropertiesBuildItem resourcePropertiesBuildItem : resourcePropertiesBuildItems) { @@ -143,10 +104,13 @@ private boolean hasValidatorCapability(Capabilities capabilities) { return capabilities.isPresent(Capability.HIBERNATE_VALIDATOR); } - private boolean hasHalCapability(Capabilities capabilities) { + private boolean hasAnyJsonCapabilityForResteasyClassic(Capabilities capabilities) { return capabilities.isPresent(Capability.RESTEASY_JSON_JSONB) - || capabilities.isPresent(Capability.RESTEASY_JSON_JACKSON) - || capabilities.isPresent(Capability.RESTEASY_REACTIVE_JSON_JSONB) + || capabilities.isPresent(Capability.RESTEASY_JSON_JACKSON); + } + + private boolean hasAnyJsonCapabilityForResteasyReactive(Capabilities capabilities) { + return capabilities.isPresent(Capability.RESTEASY_REACTIVE_JSON_JSONB) || capabilities.isPresent(Capability.RESTEASY_REACTIVE_JSON_JACKSON); } } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/AddMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/AddMethodImplementor.java index 19836c02c87d6..8ffddc7a573dc 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/AddMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/AddMethodImplementor.java @@ -13,7 +13,6 @@ import io.quarkus.rest.data.panache.RestDataResource; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; import io.smallrye.mutiny.Uni; @@ -109,7 +108,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res addPathAnnotation(methodCreator, resourceProperties.getPath(RESOURCE_METHOD_NAME)); addPostAnnotation(methodCreator); addConsumesAnnotation(methodCreator, APPLICATION_JSON); - addProducesAnnotation(methodCreator, APPLICATION_JSON); + addProducesJsonAnnotation(methodCreator, resourceProperties); addLinksAnnotation(methodCreator, resourceMetadata.getEntityType(), REL); // Add parameter annotations if (withValidation) { @@ -124,7 +123,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res ResultHandle entity = tryBlock.invokeVirtualMethod( ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, Object.class, Object.class), resource, entityToSave); - tryBlock.returnValue(ResponseImplementor.created(tryBlock, entity)); + tryBlock.returnValue(responseImplementor.created(tryBlock, entity)); tryBlock.close(); } else { ResultHandle uniEntity = methodCreator.invokeVirtualMethod( @@ -132,7 +131,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res resource, entityToSave); methodCreator.returnValue(UniImplementor.map(methodCreator, uniEntity, EXCEPTION_MESSAGE, - (body, item) -> body.returnValue(ResponseImplementor.created(body, item)))); + (body, item) -> body.returnValue(responseImplementor.created(body, item)))); } methodCreator.close(); diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/DeleteMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/DeleteMethodImplementor.java index 93265c9bec692..a312b8d7ece56 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/DeleteMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/DeleteMethodImplementor.java @@ -13,7 +13,6 @@ import io.quarkus.rest.data.panache.RestDataResource; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; import io.smallrye.mutiny.Uni; @@ -105,8 +104,8 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res // Return response BranchResult entityWasDeleted = tryBlock.ifNonZero(deleted); - entityWasDeleted.trueBranch().returnValue(ResponseImplementor.noContent(entityWasDeleted.trueBranch())); - entityWasDeleted.falseBranch().returnValue(ResponseImplementor.notFound(entityWasDeleted.falseBranch())); + entityWasDeleted.trueBranch().returnValue(responseImplementor.noContent(entityWasDeleted.trueBranch())); + entityWasDeleted.falseBranch().returnValue(responseImplementor.notFound(entityWasDeleted.falseBranch())); tryBlock.close(); } else { @@ -124,9 +123,10 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res ofMethod(Boolean.class, "compareTo", int.class, Boolean.class), deleted, falseDefault); BranchResult entityWasDeleted = body.ifNonZero(deletedAsInt); - entityWasDeleted.trueBranch().returnValue(ResponseImplementor.noContent(entityWasDeleted.trueBranch())); + entityWasDeleted.trueBranch() + .returnValue(responseImplementor.noContent(entityWasDeleted.trueBranch())); entityWasDeleted.falseBranch() - .returnValue(ResponseImplementor.notFound(entityWasDeleted.falseBranch())); + .returnValue(responseImplementor.notFound(entityWasDeleted.falseBranch())); })); } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/GetMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/GetMethodImplementor.java index d495d467d9bcf..0d3b4a1a5121f 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/GetMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/GetMethodImplementor.java @@ -13,7 +13,6 @@ import io.quarkus.rest.data.panache.RestDataResource; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; import io.smallrye.mutiny.Uni; @@ -93,7 +92,8 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res // Add method annotations addPathAnnotation(methodCreator, appendToPath(resourceProperties.getPath(RESOURCE_METHOD_NAME), "{id}")); addGetAnnotation(methodCreator); - addProducesAnnotation(methodCreator, APPLICATION_JSON); + addProducesJsonAnnotation(methodCreator, resourceProperties); + addPathParamAnnotation(methodCreator.getParameterAnnotations(0), "id"); addLinksAnnotation(methodCreator, resourceMetadata.getEntityType(), REL); @@ -108,8 +108,8 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res // Return response BranchResult wasNotFound = tryBlock.ifNull(entity); - wasNotFound.trueBranch().returnValue(ResponseImplementor.notFound(wasNotFound.trueBranch())); - wasNotFound.falseBranch().returnValue(ResponseImplementor.ok(wasNotFound.falseBranch(), entity)); + wasNotFound.trueBranch().returnValue(responseImplementor.notFound(wasNotFound.trueBranch())); + wasNotFound.falseBranch().returnValue(responseImplementor.ok(wasNotFound.falseBranch(), entity)); tryBlock.close(); } else { @@ -121,9 +121,9 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res (body, entity) -> { BranchResult entityWasNotFound = body.ifNull(entity); entityWasNotFound.trueBranch() - .returnValue(ResponseImplementor.notFound(entityWasNotFound.trueBranch())); + .returnValue(responseImplementor.notFound(entityWasNotFound.trueBranch())); entityWasNotFound.falseBranch() - .returnValue(ResponseImplementor.ok(entityWasNotFound.falseBranch(), entity)); + .returnValue(responseImplementor.ok(entityWasNotFound.falseBranch(), entity)); })); } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java index 6bd29edd5d6a3..bfd1544b8c988 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/ListMethodImplementor.java @@ -21,7 +21,6 @@ import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; import io.quarkus.rest.data.panache.deployment.utils.PaginationImplementor; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.SortImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; import io.smallrye.mutiny.Uni; @@ -171,7 +170,7 @@ private void implementPaged(ClassCreator classCreator, ResourceMetadata resource resource, page, sort); // Return response - tryBlock.returnValue(ResponseImplementor.ok(tryBlock, entities, links)); + tryBlock.returnValue(responseImplementor.ok(tryBlock, entities, links)); tryBlock.close(); } else { ResultHandle uniPageCount = methodCreator.invokeVirtualMethod( @@ -188,7 +187,7 @@ private void implementPaged(ClassCreator classCreator, ResourceMetadata resource Sort.class), resource, page, sort); body.returnValue(UniImplementor.map(body, uniEntities, EXCEPTION_MESSAGE, - (listBody, list) -> listBody.returnValue(ResponseImplementor.ok(listBody, list, links)))); + (listBody, list) -> listBody.returnValue(responseImplementor.ok(listBody, list, links)))); })); } @@ -218,7 +217,7 @@ private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resou ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, List.class, Page.class, Sort.class), resource, tryBlock.loadNull(), sort); - tryBlock.returnValue(ResponseImplementor.ok(tryBlock, entities)); + tryBlock.returnValue(responseImplementor.ok(tryBlock, entities)); tryBlock.close(); } else { ResultHandle uniEntities = methodCreator.invokeVirtualMethod( @@ -227,7 +226,7 @@ private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resou resource, methodCreator.loadNull(), sort); methodCreator.returnValue(UniImplementor.map(methodCreator, uniEntities, EXCEPTION_MESSAGE, - (body, entities) -> body.returnValue(ResponseImplementor.ok(body, entities)))); + (body, entities) -> body.returnValue(responseImplementor.ok(body, entities)))); } methodCreator.close(); diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java index cf90e6b3b0e35..1cb56a8484253 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/StandardMethodImplementor.java @@ -24,6 +24,7 @@ import io.quarkus.rest.data.panache.RestDataPanacheException; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; +import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.runtime.sort.SortQueryParamValidator; /** @@ -33,12 +34,14 @@ public abstract class StandardMethodImplementor implements MethodImplementor { private static final Logger LOGGER = Logger.getLogger(StandardMethodImplementor.class); + protected final ResponseImplementor responseImplementor; private final boolean isResteasyClassic; private final boolean isReactivePanache; protected StandardMethodImplementor(boolean isResteasyClassic, boolean isReactivePanache) { this.isResteasyClassic = isResteasyClassic; this.isReactivePanache = isReactivePanache; + this.responseImplementor = new ResponseImplementor(isResteasyClassic); } /** @@ -121,6 +124,14 @@ protected void addDefaultValueAnnotation(AnnotatedElement element, String value) element.addAnnotation(DefaultValue.class).addValue("value", value); } + protected void addProducesJsonAnnotation(AnnotatedElement element, ResourceProperties properties) { + if (properties.isHal()) { + addProducesAnnotation(element, APPLICATION_JSON, APPLICATION_HAL_JSON); + } else { + addProducesAnnotation(element, APPLICATION_JSON); + } + } + protected void addProducesAnnotation(AnnotatedElement element, String... mediaTypes) { element.addAnnotation(Produces.class).addValue("value", mediaTypes); } @@ -147,6 +158,10 @@ protected String appendToPath(String path, String suffix) { return String.join("/", path, suffix); } + protected boolean isResteasyClassic() { + return isResteasyClassic; + } + protected boolean isNotReactivePanache() { return !isReactivePanache; } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/UpdateMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/UpdateMethodImplementor.java index c5b64ff2129b4..6924d7ab17bab 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/UpdateMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/UpdateMethodImplementor.java @@ -23,7 +23,6 @@ import io.quarkus.rest.data.panache.RestDataResource; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; import io.quarkus.rest.data.panache.runtime.UpdateExecutor; import io.smallrye.mutiny.Uni; @@ -142,7 +141,7 @@ protected void implementInternal(ClassCreator classCreator, ResourceMetadata res addPutAnnotation(methodCreator); addPathParamAnnotation(methodCreator.getParameterAnnotations(0), "id"); addConsumesAnnotation(methodCreator, APPLICATION_JSON); - addProducesAnnotation(methodCreator, APPLICATION_JSON); + addProducesJsonAnnotation(methodCreator, resourceProperties); addLinksAnnotation(methodCreator, resourceMetadata.getEntityType(), REL); // Add parameter annotations if (withValidation) { @@ -185,9 +184,9 @@ private void implementReactiveVersion(MethodCreator methodCreator, ResourceMetad (updateBody, itemUpdated) -> { BranchResult ifEntityIsNew = updateBody.ifNull(itemWasFound); ifEntityIsNew.trueBranch() - .returnValue(ResponseImplementor.created(ifEntityIsNew.trueBranch(), itemUpdated)); + .returnValue(responseImplementor.created(ifEntityIsNew.trueBranch(), itemUpdated)); ifEntityIsNew.falseBranch() - .returnValue(ResponseImplementor.noContent(ifEntityIsNew.falseBranch())); + .returnValue(responseImplementor.noContent(ifEntityIsNew.falseBranch())); })); })); } @@ -206,8 +205,8 @@ private void implementClassicVersion(MethodCreator methodCreator, ResourceMetada updateExecutor, updateFunction); BranchResult createdNewEntity = tryBlock.ifNotNull(newEntity); - createdNewEntity.trueBranch().returnValue(ResponseImplementor.created(createdNewEntity.trueBranch(), newEntity)); - createdNewEntity.falseBranch().returnValue(ResponseImplementor.noContent(createdNewEntity.falseBranch())); + createdNewEntity.trueBranch().returnValue(responseImplementor.created(createdNewEntity.trueBranch(), newEntity)); + createdNewEntity.falseBranch().returnValue(responseImplementor.noContent(createdNewEntity.falseBranch())); } private ResultHandle getUpdateFunction(BytecodeCreator creator, String resourceClass, ResultHandle resource, diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/AddHalMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/AddHalMethodImplementor.java deleted file mode 100644 index 36323f7824366..0000000000000 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/AddHalMethodImplementor.java +++ /dev/null @@ -1,143 +0,0 @@ -package io.quarkus.rest.data.panache.deployment.methods.hal; - -import static io.quarkus.gizmo.MethodDescriptor.ofMethod; - -import javax.validation.Valid; -import javax.ws.rs.core.Response; - -import io.quarkus.gizmo.ClassCreator; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.gizmo.TryBlock; -import io.quarkus.rest.data.panache.RestDataResource; -import io.quarkus.rest.data.panache.deployment.ResourceMetadata; -import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; -import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; -import io.smallrye.mutiny.Uni; - -public final class AddHalMethodImplementor extends HalMethodImplementor { - - private static final String METHOD_NAME = "addHal"; - - private static final String RESOURCE_METHOD_NAME = "add"; - - private static final String EXCEPTION_MESSAGE = "Failed to add an entity"; - - private final boolean withValidation; - - public AddHalMethodImplementor(boolean withValidation, boolean isResteasyClassic, boolean isReactivePanache) { - super(isResteasyClassic, isReactivePanache); - this.withValidation = withValidation; - } - - /** - * Generate HAL JAX-RS POST method. - * - * The RESTEasy Classic version exposes {@link RestDataResource#add(Object)} via HAL JAX-RS method. - * Generated code looks more or less like this: - * - *
-     * {@code
-     *     @POST
-     *     @Path("")
-     *     @Consumes({"application/json"})
-     *     @Produces({"application/hal+json"})
-     *     public Response addHal(Entity entityToSave) {
-     *         try {
-     *             Entity entity = resource.add(entityToSave);
-     *             HalEntityWrapper wrapper = new HalEntityWrapper(entity);
-     *             String location = new ResourceLinksProvider().getSelfLink(entity);
-     *             if (location != null) {
-     *                 ResponseBuilder responseBuilder = Response.status(201);
-     *                 responseBuilder.entity(wrapper);
-     *                 responseBuilder.location(URI.create(location));
-     *                 return responseBuilder.build();
-     *             } else {
-     *                 throw new RuntimeException("Could not extract a new entity URL");
-     *             }
-     *         } catch (Throwable t) {
-     *             throw new RestDataPanacheException(t);
-     *         }
-     *     }
-     * }
-     * 
- * - * The RESTEasy Reactive version exposes {@link io.quarkus.rest.data.panache.ReactiveRestDataResource#add(Object)} - * and the generated code looks more or less like this: - * - *
-     * {@code
-     *     @POST
-     *     @Path("")
-     *     @Consumes({"application/json"})
-     *     @Produces({"application/hal+json"})
-     *     public Uni addHal(Entity entityToSave) {
-     *
-     *         return resource.add(entityToSave).map(entity -> {
-     *             HalEntityWrapper wrapper = new HalEntityWrapper(entity);
-     *             String location = new ResourceLinksProvider().getSelfLink(entity);
-     *             if (location != null) {
-     *                 ResponseBuilder responseBuilder = Response.status(201);
-     *                 responseBuilder.entity(wrapper);
-     *                 responseBuilder.location(URI.create(location));
-     *                 return responseBuilder.build();
-     *             } else {
-     *                 throw new RuntimeException("Could not extract a new entity URL");
-     *             }
-     *         }).onFailure().invoke(t -> throw new RestDataPanacheException(t));
-     *     }
-     * }
-     * 
- * - */ - @Override - protected void implementInternal(ClassCreator classCreator, ResourceMetadata resourceMetadata, - ResourceProperties resourceProperties, FieldDescriptor resourceField) { - MethodCreator methodCreator = classCreator.getMethodCreator(METHOD_NAME, - isNotReactivePanache() ? Response.class : Uni.class, - resourceMetadata.getEntityType()); - - // Add method annotations - addPathAnnotation(methodCreator, resourceProperties.getPath(RESOURCE_METHOD_NAME)); - addPostAnnotation(methodCreator); - addConsumesAnnotation(methodCreator, APPLICATION_JSON); - addProducesAnnotation(methodCreator, APPLICATION_HAL_JSON); - // Add parameter annotations - if (withValidation) { - methodCreator.getParameterAnnotations(0).addAnnotation(Valid.class); - } - - ResultHandle resource = methodCreator.readInstanceField(resourceField, methodCreator.getThis()); - ResultHandle entityToSave = methodCreator.getMethodParam(0); - - if (isNotReactivePanache()) { - TryBlock tryBlock = implementTryBlock(methodCreator, EXCEPTION_MESSAGE); - ResultHandle entity = tryBlock.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, Object.class, Object.class), - resource, entityToSave); - - // Wrap and return response - tryBlock.returnValue(ResponseImplementor.created(tryBlock, wrapHalEntity(tryBlock, entity), - ResponseImplementor.getEntityUrl(tryBlock, entity))); - - tryBlock.close(); - } else { - ResultHandle uniEntity = methodCreator.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, Uni.class, Object.class), - resource, entityToSave); - - methodCreator.returnValue(UniImplementor.map(methodCreator, uniEntity, EXCEPTION_MESSAGE, - (body, item) -> body.returnValue(ResponseImplementor.created(body, wrapHalEntity(body, item), - ResponseImplementor.getEntityUrl(body, item))))); - } - - methodCreator.close(); - } - - @Override - protected String getResourceMethodName() { - return RESOURCE_METHOD_NAME; - } -} diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/GetHalMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/GetHalMethodImplementor.java deleted file mode 100644 index ba632e8f9063e..0000000000000 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/GetHalMethodImplementor.java +++ /dev/null @@ -1,128 +0,0 @@ -package io.quarkus.rest.data.panache.deployment.methods.hal; - -import static io.quarkus.gizmo.MethodDescriptor.ofMethod; - -import javax.ws.rs.core.Response; - -import io.quarkus.gizmo.BranchResult; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.ClassCreator; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.gizmo.TryBlock; -import io.quarkus.rest.data.panache.RestDataResource; -import io.quarkus.rest.data.panache.deployment.ResourceMetadata; -import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; -import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; -import io.smallrye.mutiny.Uni; - -public final class GetHalMethodImplementor extends HalMethodImplementor { - - private static final String METHOD_NAME = "getHal"; - - private static final String RESOURCE_METHOD_NAME = "get"; - - private static final String EXCEPTION_MESSAGE = "Failed to get an entity"; - - public GetHalMethodImplementor(boolean isResteasyClassic, boolean isReactivePanache) { - super(isResteasyClassic, isReactivePanache); - } - - /** - * Generate HAL JAX-RS GET method. - * - * The RESTEasy Classic version exposes {@link RestDataResource#get(Object)} via HAL JAX-RS method. - * Generated code looks more or less like this: - * - *
-     * {@code
-     *     @GET
-     *     @Produces({"application/hal+json"})
-     *     @Path("{id}")
-     *     public Response getHal(@PathParam("id") ID id) {
-     *         try {
-     *             Entity entity = resource.get(id);
-     *             if (entity != null) {
-     *                 return Response.ok(new HalEntityWrapper(entity)).build();
-     *             } else {
-     *                 return Response.status(404).build();
-     *             }
-     *         } catch (Throwable t) {
-     *             throw new RestDataPanacheException(t);
-     *         }
-     *     }
-     * }
-     * 
- * - * The RESTEasy Reactive version exposes {@link io.quarkus.rest.data.panache.ReactiveRestDataResource#get(Object)} - * and the generated code looks more or less like this: - * - *
-     * {@code
-     *     @GET
-     *     @Produces({"application/hal+json"})
-     *     @Path("{id}")
-     *     public Uni getHal(@PathParam("id") ID id) {
-     *         return resource.get(id).map(entity -> {
-     *             if (entity != null) {
-     *                 return Response.ok(new HalEntityWrapper(entity)).build();
-     *             } else {
-     *                 return Response.status(404).build();
-     *             }
-     *         }).onFailure().invoke(t -> throw new RestDataPanacheException(t));
-     *     }
-     * }
-     * 
- */ - @Override - protected void implementInternal(ClassCreator classCreator, ResourceMetadata resourceMetadata, - ResourceProperties resourceProperties, FieldDescriptor resourceField) { - MethodCreator methodCreator = classCreator.getMethodCreator(METHOD_NAME, - isNotReactivePanache() ? Response.class : Uni.class, - resourceMetadata.getIdType()); - - // Add method annotations - addPathAnnotation(methodCreator, appendToPath(resourceProperties.getPath(RESOURCE_METHOD_NAME), "{id}")); - addGetAnnotation(methodCreator); - addProducesAnnotation(methodCreator, APPLICATION_HAL_JSON); - addPathParamAnnotation(methodCreator.getParameterAnnotations(0), "id"); - - ResultHandle resource = methodCreator.readInstanceField(resourceField, methodCreator.getThis()); - ResultHandle id = methodCreator.getMethodParam(0); - - if (isNotReactivePanache()) { - TryBlock tryBlock = implementTryBlock(methodCreator, EXCEPTION_MESSAGE); - ResultHandle entity = tryBlock.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, Object.class, Object.class), - resource, id); - - // Wrap and return response - ifNullReturnNotFound(tryBlock, entity); - - tryBlock.close(); - } else { - ResultHandle uniEntity = methodCreator.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_METHOD_NAME, Uni.class, Object.class), - resource, id); - - methodCreator.returnValue(UniImplementor.map(methodCreator, uniEntity, EXCEPTION_MESSAGE, - (body, entity) -> ifNullReturnNotFound(body, entity))); - } - - methodCreator.close(); - } - - @Override - protected String getResourceMethodName() { - return RESOURCE_METHOD_NAME; - } - - private void ifNullReturnNotFound(BytecodeCreator body, ResultHandle entity) { - BranchResult wasNotFound = body.ifNull(entity); - wasNotFound.trueBranch().returnValue(ResponseImplementor.notFound(wasNotFound.trueBranch())); - wasNotFound.falseBranch().returnValue( - ResponseImplementor.ok(wasNotFound.falseBranch(), wrapHalEntity(wasNotFound.falseBranch(), entity))); - } -} diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/HalMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/HalMethodImplementor.java index 73f27d5ec1496..4a880938bd89e 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/HalMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/HalMethodImplementor.java @@ -1,17 +1,25 @@ package io.quarkus.rest.data.panache.deployment.methods.hal; +import static io.quarkus.gizmo.MethodDescriptor.ofMethod; + +import java.lang.annotation.Annotation; import java.util.Collection; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InstanceHandle; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; +import io.quarkus.hal.HalCollectionWrapper; +import io.quarkus.hal.HalService; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.methods.StandardMethodImplementor; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.runtime.hal.HalCollectionWrapper; -import io.quarkus.rest.data.panache.runtime.hal.HalEntityWrapper; +import io.quarkus.resteasy.links.runtime.hal.ResteasyHalService; +import io.quarkus.resteasy.reactive.links.runtime.hal.ResteasyReactiveHalService; /** * HAL JAX-RS method implementor. @@ -33,15 +41,19 @@ public void implement(ClassCreator classCreator, ResourceMetadata resourceMetada } } - protected ResultHandle wrapHalEntity(BytecodeCreator creator, ResultHandle entity) { - return creator.newInstance(MethodDescriptor.ofConstructor(HalEntityWrapper.class, Object.class), entity); - } - protected ResultHandle wrapHalEntities(BytecodeCreator creator, ResultHandle entities, String entityType, String collectionName) { - return creator.newInstance( - MethodDescriptor.ofConstructor(HalCollectionWrapper.class, Collection.class, Class.class, String.class), - entities, creator.loadClassFromTCCL(entityType), - creator.load(collectionName)); + ResultHandle arcContainer = creator.invokeStaticMethod(ofMethod(Arc.class, "container", ArcContainer.class)); + ResultHandle instanceHandle = creator.invokeInterfaceMethod( + ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class, Annotation[].class), + arcContainer, + creator.loadClassFromTCCL(isResteasyClassic() ? ResteasyHalService.class : ResteasyReactiveHalService.class), + creator.newArray(Annotation.class, 0)); + ResultHandle halService = creator.invokeInterfaceMethod( + ofMethod(InstanceHandle.class, "get", Object.class), instanceHandle); + + return creator.invokeVirtualMethod(MethodDescriptor.ofMethod(HalService.class, "toHalCollectionWrapper", + HalCollectionWrapper.class, Collection.class, String.class, Class.class), + halService, entities, creator.load(collectionName), creator.loadClassFromTCCL(entityType)); } } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/ListHalMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/ListHalMethodImplementor.java index 2c963d25be481..cd5f899397b15 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/ListHalMethodImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/ListHalMethodImplementor.java @@ -16,6 +16,7 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.ResultHandle; import io.quarkus.gizmo.TryBlock; +import io.quarkus.hal.HalCollectionWrapper; import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Sort; import io.quarkus.rest.data.panache.RestDataResource; @@ -23,10 +24,8 @@ import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; import io.quarkus.rest.data.panache.deployment.utils.PaginationImplementor; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; import io.quarkus.rest.data.panache.deployment.utils.SortImplementor; import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; -import io.quarkus.rest.data.panache.runtime.hal.HalCollectionWrapper; import io.smallrye.mutiny.Uni; public final class ListHalMethodImplementor extends HalMethodImplementor { @@ -190,9 +189,10 @@ private void returnWrappedHalEntitiesWithLinks(BytecodeCreator body, ResourceMet ResultHandle wrapper = wrapHalEntities(body, entities, resourceMetadata.getEntityType(), resourceProperties.getHalCollectionName()); + body.invokeVirtualMethod( ofMethod(HalCollectionWrapper.class, "addLinks", void.class, Link[].class), wrapper, links); - body.returnValue(ResponseImplementor.ok(body, wrapper, links)); + body.returnValue(responseImplementor.ok(body, wrapper, links)); } private void implementNotPaged(ClassCreator classCreator, ResourceMetadata resourceMetadata, @@ -237,6 +237,6 @@ private void returnWrappedHalEntities(BytecodeCreator body, ResourceMetadata res ResultHandle entities) { ResultHandle wrapper = wrapHalEntities(body, entities, resourceMetadata.getEntityType(), resourceProperties.getHalCollectionName()); - body.returnValue(ResponseImplementor.ok(body, wrapper)); + body.returnValue(responseImplementor.ok(body, wrapper)); } } diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/UpdateHalMethodImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/UpdateHalMethodImplementor.java deleted file mode 100644 index 457338aeb1017..0000000000000 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/methods/hal/UpdateHalMethodImplementor.java +++ /dev/null @@ -1,259 +0,0 @@ -package io.quarkus.rest.data.panache.deployment.methods.hal; - -import static io.quarkus.gizmo.MethodDescriptor.ofMethod; - -import java.lang.annotation.Annotation; -import java.util.function.Supplier; - -import javax.validation.Valid; -import javax.ws.rs.core.Response; - -import io.quarkus.arc.Arc; -import io.quarkus.arc.ArcContainer; -import io.quarkus.arc.InstanceHandle; -import io.quarkus.gizmo.AssignableResultHandle; -import io.quarkus.gizmo.BranchResult; -import io.quarkus.gizmo.BytecodeCreator; -import io.quarkus.gizmo.ClassCreator; -import io.quarkus.gizmo.FieldDescriptor; -import io.quarkus.gizmo.FunctionCreator; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.ResultHandle; -import io.quarkus.gizmo.TryBlock; -import io.quarkus.rest.data.panache.RestDataResource; -import io.quarkus.rest.data.panache.deployment.ResourceMetadata; -import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; -import io.quarkus.rest.data.panache.deployment.utils.ResponseImplementor; -import io.quarkus.rest.data.panache.deployment.utils.UniImplementor; -import io.quarkus.rest.data.panache.runtime.UpdateExecutor; -import io.smallrye.mutiny.Uni; - -public final class UpdateHalMethodImplementor extends HalMethodImplementor { - - private static final String METHOD_NAME = "updateHal"; - - private static final String RESOURCE_UPDATE_METHOD_NAME = "update"; - - private static final String RESOURCE_GET_METHOD_NAME = "get"; - - private static final String EXCEPTION_MESSAGE = "Failed to update an entity"; - - private final boolean withValidation; - - public UpdateHalMethodImplementor(boolean withValidation, boolean isResteasyClassic, boolean isReactivePanache) { - super(isResteasyClassic, isReactivePanache); - this.withValidation = withValidation; - } - - /** - * Generate HAL JAX-RS PUT method. - * - * The RESTEasy Classic version exposes {@link RestDataResource#update(Object, Object)} via HAL JAX-RS method. - * Generated code looks more or less like this: - * - *
-     * {@code
-     *     @PUT
-     *     @Path("{id}")
-     *     @Consumes({"application/json"})
-     *     @Produces({"application/hal+json"})
-     *     public Response updateHal(@PathParam("id") ID id, Entity entityToSave) {
-     *         try {
-     *             Object newEntity = updateExecutor.execute(() -> {
-     *                 if (resource.get(id) == null) {
-     *                     return resource.update(id, entityToSave);
-     *                 } else {
-     *                     resource.update(id, entityToSave);
-     *                     return null;
-     *                 }
-     *             });
-     *
-     *             if (newEntity == null) {
-     *                 return Response.status(204).build();
-     *             } else {
-     *                 String location = new ResourceLinksProvider().getSelfLink(newEntity);
-     *                 if (location != null) {
-     *                     ResponseBuilder responseBuilder = Response.status(201);
-     *                     responseBuilder.entity(new HalEntityWrapper(newEntity));
-     *                     responseBuilder.location(URI.create(location));
-     *                     return responseBuilder.build();
-     *                 } else {
-     *                     throw new RuntimeException("Could not extract a new entity URL")
-     *                 }
-     *             }
-     *         } catch (Throwable t) {
-     *             throw new RestDataPanacheException(t);
-     *         }
-     *     }
-     * }
-     * 
- * - * The RESTEasy Reactive version exposes - * {@link io.quarkus.rest.data.panache.ReactiveRestDataResource#update(Object, Object)} - * and the generated code looks more or less like this: - * - *
-     * {@code
-     *     @PUT
-     *     @Path("{id}")
-     *     @Consumes({"application/json"})
-     *     @Produces({"application/json"})
-     *     @LinkResource(
-     *         rel = "update",
-     *         entityClassName = "com.example.Entity"
-     *     )
-     *     public Uni update(@PathParam("id") ID id, Entity entityToSave) {
-     *         return resource.get(id).flatMap(entity -> {
-     *             if (entity == null) {
-     *                 return Uni.createFrom().item(Response.status(204).build());
-     *             } else {
-     *                 return resource.update(id, entityToSave).map(savedEntity -> {
-     *                     String location = new ResourceLinksProvider().getSelfLink(savedEntity);
-     *                     if (location != null) {
-     *                         ResponseBuilder responseBuilder = Response.status(201);
-     *                         responseBuilder.entity(new HalEntityWrapper(savedEntity));
-     *                         responseBuilder.location(URI.create(location));
-     *                         return responseBuilder.build();
-     *                     } else {
-     *                         throw new RuntimeException("Could not extract a new entity URL")
-     *                     }
-     *                 });
-     *             }
-     *         }).onFailure().invoke(t -> throw new RestDataPanacheException(t));
-     *     }
-     * }
-     * 
- */ - @Override - protected void implementInternal(ClassCreator classCreator, ResourceMetadata resourceMetadata, - ResourceProperties resourceProperties, FieldDescriptor resourceField) { - MethodCreator methodCreator = classCreator.getMethodCreator(METHOD_NAME, - isNotReactivePanache() ? Response.class : Uni.class, - resourceMetadata.getIdType(), resourceMetadata.getEntityType()); - - // Add method annotations - addPathAnnotation(methodCreator, - appendToPath(resourceProperties.getPath(RESOURCE_UPDATE_METHOD_NAME), "{id}")); - addPutAnnotation(methodCreator); - addPathParamAnnotation(methodCreator.getParameterAnnotations(0), "id"); - addConsumesAnnotation(methodCreator, APPLICATION_JSON); - addProducesAnnotation(methodCreator, APPLICATION_HAL_JSON); - // Add parameter annotations - if (withValidation) { - methodCreator.getParameterAnnotations(1).addAnnotation(Valid.class); - } - - ResultHandle resource = methodCreator.readInstanceField(resourceField, methodCreator.getThis()); - ResultHandle id = methodCreator.getMethodParam(0); - ResultHandle entityToSave = methodCreator.getMethodParam(1); - - if (isNotReactivePanache()) { - // Invoke resource methods inside a supplier function which will be given to an update executor. - // For ORM, this update executor will have the @Transactional annotation to make - // sure that all database operations are executed in a single transaction. - TryBlock tryBlock = implementTryBlock(methodCreator, EXCEPTION_MESSAGE); - ResultHandle updateExecutor = getUpdateExecutor(tryBlock); - ResultHandle updateFunction = getUpdateFunction(tryBlock, resourceMetadata.getResourceClass(), resource, id, - entityToSave); - ResultHandle newEntity = tryBlock.invokeInterfaceMethod( - ofMethod(UpdateExecutor.class, "execute", Object.class, Supplier.class), - updateExecutor, updateFunction); - - BranchResult createdNewEntity = tryBlock.ifNotNull(newEntity); - ResultHandle wrappedNewEntity = wrapHalEntity(createdNewEntity.trueBranch(), newEntity); - ResultHandle newEntityUrl = ResponseImplementor.getEntityUrl(createdNewEntity.trueBranch(), newEntity); - createdNewEntity.trueBranch().returnValue( - ResponseImplementor.created(createdNewEntity.trueBranch(), wrappedNewEntity, newEntityUrl)); - createdNewEntity.falseBranch().returnValue(ResponseImplementor.noContent(createdNewEntity.falseBranch())); - } else { - ResultHandle uniResponse = methodCreator.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_GET_METHOD_NAME, Uni.class, Object.class), - resource, id); - - methodCreator - .returnValue( - UniImplementor.flatMap(methodCreator, uniResponse, EXCEPTION_MESSAGE, (getBody, itemWasFound) -> { - ResultHandle uniUpdateEntity = getBody.invokeVirtualMethod( - ofMethod(resourceMetadata.getResourceClass(), RESOURCE_UPDATE_METHOD_NAME, Uni.class, - Object.class, - Object.class), - resource, id, entityToSave); - - getBody.returnValue(UniImplementor.map(getBody, uniUpdateEntity, EXCEPTION_MESSAGE, - (updateBody, itemUpdated) -> { - ResultHandle wrappedNewEntity = wrapHalEntity(updateBody, itemUpdated); - ResultHandle newEntityUrl = ResponseImplementor.getEntityUrl(updateBody, - itemUpdated); - - BranchResult ifEntityIsNew = updateBody.ifNull(itemWasFound); - ifEntityIsNew.trueBranch().returnValue(ResponseImplementor - .created(ifEntityIsNew.trueBranch(), wrappedNewEntity, newEntityUrl)); - ifEntityIsNew.falseBranch() - .returnValue(ResponseImplementor.noContent(ifEntityIsNew.falseBranch())); - })); - })); - } - - methodCreator.close(); - } - - @Override - protected String getResourceMethodName() { - return RESOURCE_UPDATE_METHOD_NAME; - } - - private ResultHandle getUpdateFunction(BytecodeCreator creator, String resourceClass, ResultHandle resource, - ResultHandle id, ResultHandle entity) { - FunctionCreator functionCreator = creator.createFunction(Supplier.class); - BytecodeCreator functionBytecodeCreator = functionCreator.getBytecode(); - - AssignableResultHandle entityToSave = functionBytecodeCreator.createVariable(Object.class); - functionBytecodeCreator.assign(entityToSave, entity); - - BranchResult shouldUpdate = entityExists(functionBytecodeCreator, resourceClass, resource, id); - // Update and return null - updateAndReturn(shouldUpdate.trueBranch(), resourceClass, resource, id, entityToSave); - // Update and return new entity - createAndReturn(shouldUpdate.falseBranch(), resourceClass, resource, id, entityToSave); - - return functionCreator.getInstance(); - } - - private BranchResult entityExists(BytecodeCreator creator, String resourceClass, ResultHandle resource, - ResultHandle id) { - return creator.ifNotNull(creator.invokeVirtualMethod( - ofMethod(resourceClass, RESOURCE_GET_METHOD_NAME, Object.class, Object.class), resource, id)); - } - - private void createAndReturn(BytecodeCreator creator, String resourceClass, ResultHandle resource, - ResultHandle id, ResultHandle entityToSave) { - ResultHandle newEntity = creator.invokeVirtualMethod( - ofMethod(resourceClass, RESOURCE_UPDATE_METHOD_NAME, Object.class, Object.class, Object.class), - resource, id, entityToSave); - creator.returnValue(newEntity); - } - - private void updateAndReturn(BytecodeCreator creator, String resourceClass, ResultHandle resource, - ResultHandle id, ResultHandle entityToSave) { - creator.invokeVirtualMethod( - ofMethod(resourceClass, RESOURCE_UPDATE_METHOD_NAME, Object.class, Object.class, Object.class), - resource, id, entityToSave); - creator.returnValue(creator.loadNull()); - } - - private ResultHandle getUpdateExecutor(BytecodeCreator creator) { - ResultHandle arcContainer = creator.invokeStaticMethod(ofMethod(Arc.class, "container", ArcContainer.class)); - ResultHandle instanceHandle = creator.invokeInterfaceMethod( - ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class, Annotation[].class), - arcContainer, creator.loadClassFromTCCL(UpdateExecutor.class), creator.newArray(Annotation.class, 0)); - ResultHandle instance = creator.invokeInterfaceMethod( - ofMethod(InstanceHandle.class, "get", Object.class), instanceHandle); - - creator.ifNull(instance) - .trueBranch() - .throwException(RuntimeException.class, - UpdateExecutor.class.getSimpleName() + " instance was not found"); - - return instance; - } -} diff --git a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/utils/ResponseImplementor.java b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/utils/ResponseImplementor.java index 71543ab82934d..ab14843701de0 100644 --- a/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/utils/ResponseImplementor.java +++ b/extensions/panache/rest-data-panache/deployment/src/main/java/io/quarkus/rest/data/panache/deployment/utils/ResponseImplementor.java @@ -16,17 +16,25 @@ import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; -import io.quarkus.rest.data.panache.runtime.resource.ResourceLinksProvider; +import io.quarkus.hal.HalService; +import io.quarkus.resteasy.links.runtime.hal.ResteasyHalService; +import io.quarkus.resteasy.reactive.links.runtime.hal.ResteasyReactiveHalService; public final class ResponseImplementor { - public static ResultHandle ok(BytecodeCreator creator, ResultHandle entity) { + private final boolean isResteasyClassic; + + public ResponseImplementor(boolean isResteasyClassic) { + this.isResteasyClassic = isResteasyClassic; + } + + public ResultHandle ok(BytecodeCreator creator, ResultHandle entity) { ResultHandle builder = creator.invokeStaticMethod( ofMethod(Response.class, "ok", ResponseBuilder.class, Object.class), entity); return creator.invokeVirtualMethod(ofMethod(ResponseBuilder.class, "build", Response.class), builder); } - public static ResultHandle ok(BytecodeCreator creator, ResultHandle entity, ResultHandle links) { + public ResultHandle ok(BytecodeCreator creator, ResultHandle entity, ResultHandle links) { ResultHandle builder = creator.invokeStaticMethod( ofMethod(Response.class, "ok", ResponseBuilder.class, Object.class), entity); creator.invokeVirtualMethod( @@ -34,11 +42,11 @@ public static ResultHandle ok(BytecodeCreator creator, ResultHandle entity, Resu return creator.invokeVirtualMethod(ofMethod(ResponseBuilder.class, "build", Response.class), builder); } - public static ResultHandle created(BytecodeCreator creator, ResultHandle entity) { + public ResultHandle created(BytecodeCreator creator, ResultHandle entity) { return created(creator, entity, getEntityUrl(creator, entity)); } - public static ResultHandle created(BytecodeCreator creator, ResultHandle entity, ResultHandle location) { + public ResultHandle created(BytecodeCreator creator, ResultHandle entity, ResultHandle location) { ResultHandle builder = getResponseBuilder(creator, Response.Status.CREATED.getStatusCode()); creator.invokeVirtualMethod( ofMethod(ResponseBuilder.class, "entity", ResponseBuilder.class, Object.class), builder, entity); @@ -47,42 +55,44 @@ public static ResultHandle created(BytecodeCreator creator, ResultHandle entity, return creator.invokeVirtualMethod(ofMethod(ResponseBuilder.class, "build", Response.class), builder); } - public static ResultHandle getEntityUrl(BytecodeCreator creator, ResultHandle entity) { + public ResultHandle getEntityUrl(BytecodeCreator creator, ResultHandle entity) { ResultHandle arcContainer = creator .invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class)); ResultHandle instance = creator.invokeInterfaceMethod( MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class, Annotation[].class), - arcContainer, creator.loadClassFromTCCL(ResourceLinksProvider.class), creator.loadNull()); - ResultHandle linksProvider = creator.invokeInterfaceMethod( + arcContainer, + creator.loadClassFromTCCL(isResteasyClassic ? ResteasyHalService.class : ResteasyReactiveHalService.class), + creator.loadNull()); + ResultHandle halService = creator.invokeInterfaceMethod( MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class), instance); - ResultHandle link = creator.invokeInterfaceMethod( - ofMethod(ResourceLinksProvider.class, "getSelfLink", String.class, Object.class), linksProvider, + ResultHandle link = creator.invokeVirtualMethod( + ofMethod(HalService.class, "getSelfLink", String.class, Object.class), halService, entity); creator.ifNull(link).trueBranch().throwException(RuntimeException.class, "Could not extract a new entity URL"); return creator.invokeStaticMethod(ofMethod(URI.class, "create", URI.class, String.class), link); } - public static ResultHandle noContent(BytecodeCreator creator) { + public ResultHandle noContent(BytecodeCreator creator) { return status(creator, Response.Status.NO_CONTENT.getStatusCode()); } - public static ResultHandle notFound(BytecodeCreator creator) { + public ResultHandle notFound(BytecodeCreator creator) { return status(creator, Response.Status.NOT_FOUND.getStatusCode()); } - public static ResultHandle notFoundException(BytecodeCreator creator) { + public ResultHandle notFoundException(BytecodeCreator creator) { return creator.newInstance(MethodDescriptor.ofConstructor(WebApplicationException.class, int.class), creator.load(Response.Status.NOT_FOUND.getStatusCode())); } - private static ResultHandle status(BytecodeCreator creator, int status) { + private ResultHandle status(BytecodeCreator creator, int status) { ResultHandle builder = getResponseBuilder(creator, status); return creator.invokeVirtualMethod(ofMethod(ResponseBuilder.class, "build", Response.class), builder); } - private static ResultHandle getResponseBuilder(BytecodeCreator creator, int status) { + private ResultHandle getResponseBuilder(BytecodeCreator creator, int status) { return creator.invokeStaticMethod( ofMethod(Response.class, "status", ResponseBuilder.class, int.class), creator.load(status)); } diff --git a/extensions/panache/rest-data-panache/runtime/pom.xml b/extensions/panache/rest-data-panache/runtime/pom.xml index d0fa6e33a4859..7a0e30f04692d 100644 --- a/extensions/panache/rest-data-panache/runtime/pom.xml +++ b/extensions/panache/rest-data-panache/runtime/pom.xml @@ -17,6 +17,10 @@ io.quarkus quarkus-panache-common
+ + io.quarkus + quarkus-hal + org.jboss.spec.javax.ws.rs jboss-jaxrs-api_2.1_spec diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapper.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapper.java deleted file mode 100644 index 9862d03e99426..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalCollectionWrapper.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import javax.ws.rs.core.Link; - -public class HalCollectionWrapper { - - private final Collection collection; - - private final Class elementType; - - private final String collectionName; - - private final Map links = new HashMap<>(); - - public HalCollectionWrapper(Collection collection, Class elementType, String collectionName) { - this.collection = collection; - this.elementType = elementType; - this.collectionName = collectionName; - } - - public Collection getCollection() { - return collection; - } - - public Class getElementType() { - return elementType; - } - - public String getCollectionName() { - return collectionName; - } - - public Map getLinks() { - return links; - } - - public void addLinks(Link... links) { - for (Link link : links) { - this.links.put(link.getRel(), new HalLink(link.getUri().toString())); - } - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapper.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapper.java deleted file mode 100644 index 8edcee713e639..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalEntityWrapper.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -public class HalEntityWrapper { - - private final Object entity; - - public HalEntityWrapper(Object entity) { - this.entity = entity; - } - - public Object getEntity() { - return entity; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinksProvider.java deleted file mode 100644 index 6ab614175e68a..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/HalLinksProvider.java +++ /dev/null @@ -1,10 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.util.Map; - -public interface HalLinksProvider { - - Map getLinks(Class entityClass); - - Map getLinks(Object entity); -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/RestEasyHalLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/RestEasyHalLinksProvider.java deleted file mode 100644 index ae87166774682..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/hal/RestEasyHalLinksProvider.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.util.HashMap; -import java.util.Map; - -import io.quarkus.arc.Arc; -import io.quarkus.arc.InstanceHandle; -import io.quarkus.rest.data.panache.runtime.resource.ResourceLinksProvider; - -final class RestEasyHalLinksProvider implements HalLinksProvider { - - @Override - public Map getLinks(Class entityClass) { - return toHalLinkMap(restLinksProvider().getClassLinks(entityClass)); - } - - @Override - public Map getLinks(Object entity) { - return toHalLinkMap(restLinksProvider().getInstanceLinks(entity)); - } - - private Map toHalLinkMap(Map links) { - Map halLinks = new HashMap<>(links.size()); - for (Map.Entry entry : links.entrySet()) { - halLinks.put(entry.getKey(), new HalLink(entry.getValue())); - } - return halLinks; - } - - private ResourceLinksProvider restLinksProvider() { - InstanceHandle instance = Arc.container().instance(ResourceLinksProvider.class); - if (instance.isAvailable()) { - return instance.get(); - } - throw new IllegalStateException("No bean of type '" + ResourceLinksProvider.class.getName() + "' found."); - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyClassicResourceLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyClassicResourceLinksProvider.java deleted file mode 100644 index a47204b88c291..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyClassicResourceLinksProvider.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.resource; - -import java.util.HashMap; -import java.util.Map; - -import org.jboss.resteasy.links.LinksProvider; -import org.jboss.resteasy.links.RESTServiceDiscovery; - -public final class RESTEasyClassicResourceLinksProvider implements ResourceLinksProvider { - - private static final String SELF_REF = "self"; - - public Map getClassLinks(Class className) { - RESTServiceDiscovery links = LinksProvider - .getClassLinksProvider() - .getLinks(className, Thread.currentThread().getContextClassLoader()); - return linksToMap(links); - } - - public Map getInstanceLinks(Object instance) { - RESTServiceDiscovery links = LinksProvider - .getObjectLinksProvider() - .getLinks(instance, Thread.currentThread().getContextClassLoader()); - return linksToMap(links); - } - - public String getSelfLink(Object instance) { - RESTServiceDiscovery.AtomLink link = LinksProvider.getObjectLinksProvider() - .getLinks(instance, Thread.currentThread().getContextClassLoader()) - .getLinkForRel(SELF_REF); - return link == null ? null : link.getHref(); - } - - private Map linksToMap(RESTServiceDiscovery serviceDiscovery) { - Map links = new HashMap<>(serviceDiscovery.size()); - for (RESTServiceDiscovery.AtomLink atomLink : serviceDiscovery) { - links.put(atomLink.getRel(), atomLink.getHref()); - } - return links; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyReactiveResourceLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyReactiveResourceLinksProvider.java deleted file mode 100644 index 0186bf1fb1e0f..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/RESTEasyReactiveResourceLinksProvider.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.resource; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; - -import javax.ws.rs.core.Link; - -import io.quarkus.arc.Arc; -import io.quarkus.arc.InstanceHandle; -import io.quarkus.resteasy.reactive.links.RestLinksProvider; - -public class RESTEasyReactiveResourceLinksProvider implements ResourceLinksProvider { - - private static final String SELF_REF = "self"; - - public Map getClassLinks(Class className) { - return linksToMap(restLinksProvider().getTypeLinks(className)); - } - - public Map getInstanceLinks(Object instance) { - return linksToMap(restLinksProvider().getInstanceLinks(instance)); - } - - public String getSelfLink(Object instance) { - Collection links = restLinksProvider().getInstanceLinks(instance); - for (Link link : links) { - if (SELF_REF.equals(link.getRel())) { - return link.getUri().toString(); - } - } - return null; - } - - private RestLinksProvider restLinksProvider() { - InstanceHandle instance = Arc.container().instance(RestLinksProvider.class); - if (instance.isAvailable()) { - return instance.get(); - } - throw new IllegalStateException("Invalid use of '" + this.getClass().getName() - + "'. No request scope bean found for type '" + RESTEasyReactiveResourceLinksProvider.class.getName() + "'"); - } - - private Map linksToMap(Collection links) { - Map result = new HashMap<>(); - for (Link link : links) { - result.put(link.getRel(), link.getUri().toString()); - } - return result; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/ResourceLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/ResourceLinksProvider.java deleted file mode 100644 index 8683c24cfef78..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/main/java/io/quarkus/rest/data/panache/runtime/resource/ResourceLinksProvider.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.resource; - -import java.util.Map; - -public interface ResourceLinksProvider { - - Map getClassLinks(Class className); - - Map getInstanceLinks(Object instance); - - @SuppressWarnings("unused") - String getSelfLink(Object instance); -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/AbstractSerializersTest.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/AbstractSerializersTest.java deleted file mode 100644 index 88ae8efea84d2..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/AbstractSerializersTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.StringReader; -import java.util.Collections; - -import javax.json.Json; -import javax.json.JsonObject; -import javax.json.JsonReader; - -import org.junit.jupiter.api.Test; - -abstract class AbstractSerializersTest { - - abstract String toJson(Object object); - - @Test - void shouldSerializeOneBook() { - int id = 1; - String title = "Black Swan"; - Book book = usePublishedBook() ? new PublishedBook(id, title) : new Book(id, title); - JsonReader jsonReader = Json.createReader(new StringReader(toJson(new HalEntityWrapper(book)))); - - assertBook(book, jsonReader.readObject()); - } - - @Test - void shouldSerializeOneBookWithNullName() { - int id = 1; - String title = null; - Book book = usePublishedBook() ? new PublishedBook(id, title) : new Book(id, title); - JsonReader jsonReader = Json.createReader(new StringReader(toJson(new HalEntityWrapper(book)))); - - assertBook(book, jsonReader.readObject()); - } - - @Test - void shouldSerializeCollectionOfBooks() { - int id = 1; - String title = "Black Swan"; - Book book = usePublishedBook() ? new PublishedBook(id, title) : new Book(id, title); - HalCollectionWrapper wrapper = new HalCollectionWrapper(Collections.singleton(book), Book.class, "books"); - JsonReader jsonReader = Json.createReader(new StringReader(toJson(wrapper))); - JsonObject collectionJson = jsonReader.readObject(); - - assertBook(book, collectionJson.getJsonObject("_embedded").getJsonArray("books").getJsonObject(0)); - - JsonObject collectionLinksJson = collectionJson.getJsonObject("_links"); - assertThat(collectionLinksJson.getJsonObject("list").getString("href")).isEqualTo("/books"); - assertThat(collectionLinksJson.getJsonObject("add").getString("href")).isEqualTo("/books"); - } - - private void assertBook(Book book, JsonObject bookJson) { - assertThat(bookJson.getInt("id")).isEqualTo(book.id); - if (bookJson.isNull("book-name")) { - assertThat(book.getName()).isNull(); - } else { - assertThat(bookJson.getString("book-name")).isEqualTo(book.getName()); - } - assertThat(bookJson.containsKey("ignored")).isFalse(); - - JsonObject bookLinksJson = bookJson.getJsonObject("_links"); - assertThat(bookLinksJson.getJsonObject("self").getString("href")).isEqualTo("/books/" + book.id); - assertThat(bookLinksJson.getJsonObject("update").getString("href")).isEqualTo("/books/" + book.id); - assertThat(bookLinksJson.getJsonObject("list").getString("href")).isEqualTo("/books"); - assertThat(bookLinksJson.getJsonObject("add").getString("href")).isEqualTo("/books"); - } - - protected abstract boolean usePublishedBook(); -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/Book.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/Book.java deleted file mode 100644 index fbf8190b2fd76..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/Book.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import javax.json.bind.annotation.JsonbProperty; -import javax.json.bind.annotation.JsonbTransient; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class Book { - - public final long id; - - @JsonProperty("book-name") - @JsonbProperty("book-name") - private final String name; - - @JsonIgnore - @JsonbTransient - public final String ignored = "ignore me"; - - public Book(long id, String name) { - this.id = id; - this.name = name; - } - - public String getName() { - return name; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/BookHalLinksProvider.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/BookHalLinksProvider.java deleted file mode 100644 index d6a0c9d613d8b..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/BookHalLinksProvider.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.util.HashMap; -import java.util.Map; - -public class BookHalLinksProvider implements HalLinksProvider { - - @Override - public Map getLinks(Class entityClass) { - Map links = new HashMap<>(2); - links.put("list", new HalLink("/books")); - links.put("add", new HalLink("/books")); - - return links; - } - - @Override - public Map getLinks(Object entity) { - Book book = (Book) entity; - Map links = new HashMap<>(4); - links.put("list", new HalLink("/books")); - links.put("add", new HalLink("/books")); - links.put("self", new HalLink("/books/" + book.id)); - links.put("update", new HalLink("/books/" + book.id)); - - return links; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JacksonSerializersTest.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JacksonSerializersTest.java deleted file mode 100644 index 2b444463d25a5..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JacksonSerializersTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import org.junit.jupiter.api.BeforeEach; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; - -class JacksonSerializersTest extends AbstractSerializersTest { - - private ObjectMapper objectMapper; - - @BeforeEach - void setup() { - objectMapper = new ObjectMapper(); - SimpleModule module = new SimpleModule(); - module.addSerializer(HalEntityWrapper.class, new HalEntityWrapperJacksonSerializer(new BookHalLinksProvider())); - module.addSerializer(HalCollectionWrapper.class, - new HalCollectionWrapperJacksonSerializer(new BookHalLinksProvider())); - objectMapper.registerModule(module); - objectMapper.registerModule(new JavaTimeModule()); - } - - @Override - String toJson(Object object) { - try { - return objectMapper.writeValueAsString(object); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - @Override - protected boolean usePublishedBook() { - return true; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JsonbSerializersTest.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JsonbSerializersTest.java deleted file mode 100644 index 5884f3b5894c7..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/JsonbSerializersTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import javax.json.bind.Jsonb; -import javax.json.bind.JsonbBuilder; -import javax.json.bind.JsonbConfig; - -import org.junit.jupiter.api.BeforeEach; - -class JsonbSerializersTest extends AbstractSerializersTest { - - private Jsonb jsonb; - - @BeforeEach - void setup() { - JsonbConfig config = new JsonbConfig(); - config.withSerializers(new HalEntityWrapperJsonbSerializer(new BookHalLinksProvider())); - config.withSerializers(new HalCollectionWrapperJsonbSerializer(new BookHalLinksProvider())); - jsonb = JsonbBuilder.create(config); - } - - @Override - String toJson(Object object) { - return jsonb.toJson(object); - } - - @Override - protected boolean usePublishedBook() { - return false; - } -} diff --git a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/PublishedBook.java b/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/PublishedBook.java deleted file mode 100644 index 38ff69b5463d1..0000000000000 --- a/extensions/panache/rest-data-panache/runtime/src/test/java/io/quarkus/rest/data/panache/runtime/hal/PublishedBook.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.quarkus.rest.data.panache.runtime.hal; - -import java.time.LocalDate; -import java.time.Month; - -public class PublishedBook extends Book { - - public LocalDate publicationDate = LocalDate.of(2021, Month.AUGUST, 31); - - public PublishedBook(long id, String name) { - super(id, name); - } -} diff --git a/extensions/pom.xml b/extensions/pom.xml index f403dd08ba5cc..a76c20e914d69 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -55,6 +55,7 @@ smallrye-openapi-common smallrye-openapi swagger-ui + hal mutiny diff --git a/extensions/resteasy-classic/resteasy-links/deployment/pom.xml b/extensions/resteasy-classic/resteasy-links/deployment/pom.xml index 4c55f33c1fe16..6287cd5765d9f 100644 --- a/extensions/resteasy-classic/resteasy-links/deployment/pom.xml +++ b/extensions/resteasy-classic/resteasy-links/deployment/pom.xml @@ -22,6 +22,26 @@ io.quarkus quarkus-resteasy-links + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + org.glassfish + jakarta.el + test + @@ -40,6 +60,4 @@ - - diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/main/java/io/quarkus/resteasy/links/deployment/LinksProcessor.java b/extensions/resteasy-classic/resteasy-links/deployment/src/main/java/io/quarkus/resteasy/links/deployment/LinksProcessor.java new file mode 100644 index 0000000000000..f972525e283e3 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/main/java/io/quarkus/resteasy/links/deployment/LinksProcessor.java @@ -0,0 +1,31 @@ +package io.quarkus.resteasy.links.deployment; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.links.runtime.hal.HalServerResponseFilter; +import io.quarkus.resteasy.links.runtime.hal.ResteasyHalService; + +final class LinksProcessor { + @BuildStep + void addHalSupport(Capabilities capabilities, BuildProducer jaxRsProviders, + BuildProducer additionalBeans) { + boolean isHalSupported = capabilities.isPresent(Capability.HAL); + if (isHalSupported) { + if (!capabilities.isPresent(Capability.RESTEASY_JSON_JSONB) + && !capabilities.isPresent(Capability.RESTEASY_JSON_JACKSON)) { + throw new IllegalStateException("Cannot generate HAL endpoints without " + + "either 'quarkus-resteasy-jsonb' or 'quarkus-resteasy-jackson'"); + } + + jaxRsProviders.produce( + new ResteasyJaxrsProviderBuildItem(HalServerResponseFilter.class.getName())); + + additionalBeans.produce(AdditionalBeanBuildItem.builder() + .addBeanClass(ResteasyHalService.class).setUnremovable().build()); + } + } +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractEntity.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractEntity.java new file mode 100644 index 0000000000000..f06f27dc4f8ed --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractEntity.java @@ -0,0 +1,32 @@ +package io.quarkus.resteasy.links.deployment; + +public abstract class AbstractEntity { + + private int id; + + private String slug; + + public AbstractEntity() { + } + + protected AbstractEntity(int id, String slug) { + this.id = id; + this.slug = slug; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getSlug() { + return slug; + } + + public void setSlug(String slug) { + this.slug = slug; + } +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractHalLinksTest.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractHalLinksTest.java new file mode 100644 index 0000000000000..0ccca18469c63 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/AbstractHalLinksTest.java @@ -0,0 +1,32 @@ +package io.quarkus.resteasy.links.deployment; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.restassured.response.Response; + +public abstract class AbstractHalLinksTest { + + private static final String APPLICATION_HAL_JSON = "application/hal+json"; + + @Test + void shouldGetHalLinksForCollections() { + Response response = given().accept(APPLICATION_HAL_JSON) + .get("/records") + .thenReturn(); + + assertThat(response.body().jsonPath().getList("_embedded.items.id")).containsOnly(1, 2); + assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/records"); + } + + @Test + void shouldGetHalLinksForInstance() { + Response response = given().accept(APPLICATION_HAL_JSON) + .get("/records/first") + .thenReturn(); + + assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/records"); + } +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJacksonTest.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJacksonTest.java new file mode 100644 index 0000000000000..8f70aa32c5d81 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJacksonTest.java @@ -0,0 +1,21 @@ +package io.quarkus.resteasy.links.deployment; + +import java.util.Arrays; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.test.QuarkusProdModeTest; + +public class HalLinksWithJacksonTest extends AbstractHalLinksTest { + @RegisterExtension + static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)) + .setForcedDependencies( + Arrays.asList( + new AppArtifact("io.quarkus", "quarkus-resteasy-jackson", "999-SNAPSHOT"), + new AppArtifact("io.quarkus", "quarkus-hal", "999-SNAPSHOT"))) + .setLogFileName("app.log") + .setRun(true); +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJsonbTest.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJsonbTest.java new file mode 100644 index 0000000000000..a888fdd4da55a --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/HalLinksWithJsonbTest.java @@ -0,0 +1,21 @@ +package io.quarkus.resteasy.links.deployment; + +import java.util.Arrays; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.test.QuarkusProdModeTest; + +public class HalLinksWithJsonbTest extends AbstractHalLinksTest { + @RegisterExtension + static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)) + .setForcedDependencies( + Arrays.asList( + new AppArtifact("io.quarkus", "quarkus-resteasy-jsonb", "999-SNAPSHOT"), + new AppArtifact("io.quarkus", "quarkus-hal", "999-SNAPSHOT"))) + .setRun(true); + +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestRecord.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestRecord.java new file mode 100644 index 0000000000000..f449fea6407e2 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestRecord.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.links.deployment; + +public class TestRecord extends AbstractEntity { + + private String value; + + public TestRecord() { + } + + public TestRecord(int id, String slug, String value) { + super(id, slug); + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestResource.java b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestResource.java new file mode 100644 index 0000000000000..a2b417099ef07 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/deployment/src/test/java/io/quarkus/resteasy/links/deployment/TestResource.java @@ -0,0 +1,38 @@ +package io.quarkus.resteasy.links.deployment; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.links.LinkResource; + +@Path("/records") +public class TestResource { + + private static final AtomicInteger ID_COUNTER = new AtomicInteger(0); + + private static final List RECORDS = new LinkedList<>(Arrays.asList( + new TestRecord(ID_COUNTER.incrementAndGet(), "first", "First value"), + new TestRecord(ID_COUNTER.incrementAndGet(), "second", "Second value"))); + + @GET + @Produces({ MediaType.APPLICATION_JSON, "application/hal+json" }) + @LinkResource(entityClassName = "io.quarkus.resteasy.links.deployment.TestRecord", rel = "list") + public List getAll() { + return RECORDS; + } + + @GET + @Path("/first") + @Produces({ MediaType.APPLICATION_JSON, "application/hal+json" }) + @LinkResource(rel = "first") + public TestRecord getFirst() { + return RECORDS.get(0); + } +} diff --git a/extensions/resteasy-classic/resteasy-links/runtime/pom.xml b/extensions/resteasy-classic/resteasy-links/runtime/pom.xml index e6b18a4191bf4..4440152caf48e 100644 --- a/extensions/resteasy-classic/resteasy-links/runtime/pom.xml +++ b/extensions/resteasy-classic/resteasy-links/runtime/pom.xml @@ -33,6 +33,11 @@ + + io.quarkus + quarkus-hal + true + diff --git a/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/HalServerResponseFilter.java b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/HalServerResponseFilter.java new file mode 100644 index 0000000000000..b2711330b5da7 --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/HalServerResponseFilter.java @@ -0,0 +1,66 @@ +package io.quarkus.resteasy.links.runtime.hal; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +import io.quarkus.arc.Arc; +import io.quarkus.hal.HalCollectionWrapper; +import io.quarkus.hal.HalEntityWrapper; + +@Provider +public class HalServerResponseFilter implements ContainerResponseFilter { + + private static final String APPLICATION_HAL_JSON = "application/hal+json"; + private static final String COLLECTION_NAME = "items"; + + @Override + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) { + Object entity = responseContext.getEntity(); + if (isHttpStatusSuccessful(responseContext.getStatusInfo()) + && acceptsHalMediaType(requestContext) + && canEntityBeProcessed(entity)) { + ResteasyHalService service = Arc.container().instance(ResteasyHalService.class).get(); + if (entity instanceof Collection) { + responseContext.setEntity(service.toHalCollectionWrapper((Collection) entity, + COLLECTION_NAME, findEntityClass(requestContext, responseContext.getEntityType()))); + } else { + responseContext.setEntity(service.toHalWrapper(entity)); + } + } + } + + private boolean canEntityBeProcessed(Object entity) { + return entity != null + && !(entity instanceof String) + && !(entity instanceof HalEntityWrapper || entity instanceof HalCollectionWrapper); + } + + private boolean isHttpStatusSuccessful(Response.StatusType statusInfo) { + return Response.Status.Family.SUCCESSFUL.equals(statusInfo.getFamily()); + } + + private boolean acceptsHalMediaType(ContainerRequestContext requestContext) { + List acceptMediaType = requestContext.getAcceptableMediaTypes().stream().map(MediaType::toString).collect( + Collectors.toList()); + return acceptMediaType.contains(APPLICATION_HAL_JSON); + } + + private Class findEntityClass(ContainerRequestContext requestContext, Type entityType) { + if (entityType instanceof ParameterizedType) { + // we can resolve the entity class from the param type + return (Class) ((ParameterizedType) entityType).getActualTypeArguments()[0]; + } + + return null; + } +} 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 new file mode 100644 index 0000000000000..6b5161282cb2f --- /dev/null +++ b/extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java @@ -0,0 +1,36 @@ +package io.quarkus.resteasy.links.runtime.hal; + +import java.util.HashMap; +import java.util.Map; + +import javax.enterprise.context.RequestScoped; + +import org.jboss.resteasy.links.LinksProvider; +import org.jboss.resteasy.links.RESTServiceDiscovery; + +import io.quarkus.hal.HalLink; +import io.quarkus.hal.HalService; + +@RequestScoped +public class ResteasyHalService extends HalService { + + @Override + protected Map getClassLinks(Class entityClass) { + return linksToMap(LinksProvider.getClassLinksProvider().getLinks(entityClass, + Thread.currentThread().getContextClassLoader())); + } + + @Override + protected Map getInstanceLinks(Object entity) { + return linksToMap(LinksProvider.getObjectLinksProvider().getLinks(entity, + Thread.currentThread().getContextClassLoader())); + } + + private Map linksToMap(RESTServiceDiscovery serviceDiscovery) { + Map links = new HashMap<>(serviceDiscovery.size()); + for (RESTServiceDiscovery.AtomLink atomLink : serviceDiscovery) { + links.put(atomLink.getRel(), new HalLink(atomLink.getHref())); + } + return links; + } +} 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 142f178677ed9..35a8647ce554c 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 @@ -13,6 +13,8 @@ import org.jboss.jandex.IndexView; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; import io.quarkus.deployment.annotations.BuildProducer; @@ -29,9 +31,12 @@ import io.quarkus.resteasy.reactive.links.runtime.LinksContainer; import io.quarkus.resteasy.reactive.links.runtime.LinksProviderRecorder; import io.quarkus.resteasy.reactive.links.runtime.RestLinksProviderProducer; +import io.quarkus.resteasy.reactive.links.runtime.hal.HalServerResponseFilter; +import io.quarkus.resteasy.reactive.links.runtime.hal.ResteasyReactiveHalService; import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveDeploymentInfoBuildItem; import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveResourceMethodEntriesBuildItem; import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; +import io.quarkus.resteasy.reactive.spi.CustomContainerResponseFilterBuildItem; import io.quarkus.runtime.RuntimeValue; final class LinksProcessor { @@ -75,6 +80,24 @@ AdditionalBeanBuildItem registerRestLinksProviderProducer() { return AdditionalBeanBuildItem.unremovableOf(RestLinksProviderProducer.class); } + @BuildStep + void addHalSupport(Capabilities capabilities, BuildProducer customResponseFilters, + BuildProducer additionalBeans) { + boolean isHalSupported = capabilities.isPresent(Capability.HAL); + if (isHalSupported) { + if (!capabilities.isPresent(Capability.RESTEASY_REACTIVE_JSON_JSONB) && !capabilities.isPresent( + Capability.RESTEASY_REACTIVE_JSON_JACKSON)) { + throw new IllegalStateException("Cannot generate HAL endpoints without " + + "either 'quarkus-resteasy-reactive-jsonb' or 'quarkus-resteasy-reactive-jackson'"); + } + + customResponseFilters.produce( + new CustomContainerResponseFilterBuildItem(HalServerResponseFilter.class.getName())); + + additionalBeans.produce(AdditionalBeanBuildItem.unremovableOf(ResteasyReactiveHalService.class)); + } + } + private LinksContainer getLinksContainer(ResteasyReactiveDeploymentInfoBuildItem deploymentInfoBuildItem, ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntriesBuildItem) { LinksContainerFactory linksContainerFactory = new LinksContainerFactory(); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java new file mode 100644 index 0000000000000..5245b1fcec275 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java @@ -0,0 +1,31 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; + +import org.jboss.resteasy.reactive.common.util.RestMediaType; +import org.junit.jupiter.api.Test; + +import io.restassured.response.Response; + +public abstract class AbstractHalLinksTest { + + @Test + void shouldGetHalLinksForCollections() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records") + .thenReturn(); + + assertThat(response.body().jsonPath().getList("_embedded.items.id")).containsOnly(1, 2); + assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/records"); + } + + @Test + void shouldGetHalLinksForInstance() { + Response response = given().accept(RestMediaType.APPLICATION_HAL_JSON) + .get("/records/1") + .thenReturn(); + + assertThat(response.body().jsonPath().getString("_links.list.href")).endsWith("/records"); + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java new file mode 100644 index 0000000000000..c000ec6c2d698 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJacksonTest.java @@ -0,0 +1,20 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import java.util.Arrays; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.test.QuarkusProdModeTest; + +public class HalLinksWithJacksonTest extends AbstractHalLinksTest { + @RegisterExtension + static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)) + .setForcedDependencies( + Arrays.asList( + new AppArtifact("io.quarkus", "quarkus-resteasy-reactive-jackson", "999-SNAPSHOT"), + new AppArtifact("io.quarkus", "quarkus-hal", "999-SNAPSHOT"))) + .setRun(true); +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java new file mode 100644 index 0000000000000..18bac4145e9a7 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/HalLinksWithJsonbTest.java @@ -0,0 +1,21 @@ +package io.quarkus.resteasy.reactive.links.deployment; + +import java.util.Arrays; + +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.bootstrap.model.AppArtifact; +import io.quarkus.test.QuarkusProdModeTest; + +public class HalLinksWithJsonbTest extends AbstractHalLinksTest { + @RegisterExtension + static final QuarkusProdModeTest TEST = new QuarkusProdModeTest() + .withApplicationRoot((jar) -> jar + .addClasses(AbstractEntity.class, TestRecord.class, TestResource.class)) + .setForcedDependencies( + Arrays.asList( + new AppArtifact("io.quarkus", "quarkus-resteasy-reactive-jsonb", "999-SNAPSHOT"), + new AppArtifact("io.quarkus", "quarkus-hal", "999-SNAPSHOT"))) + .setRun(true); + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java index f0231eb501670..2baa751f3bb16 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java @@ -13,6 +13,8 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; +import org.jboss.resteasy.reactive.common.util.RestMediaType; + import io.quarkus.resteasy.reactive.links.InjectRestLinks; import io.quarkus.resteasy.reactive.links.RestLink; import io.quarkus.resteasy.reactive.links.RestLinkType; @@ -28,7 +30,7 @@ public class TestResource { new TestRecord(ID_COUNTER.incrementAndGet(), "second", "Second value"))); @GET - @Produces(MediaType.APPLICATION_JSON) + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) @RestLink(entityType = TestRecord.class, rel = "list") @InjectRestLinks public Uni> getAll() { @@ -45,7 +47,7 @@ public List getAllWithoutLinks() { @GET @Path("/{id: \\d+}") - @Produces(MediaType.APPLICATION_JSON) + @Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) @RestLink(entityType = TestRecord.class, rel = "self") @InjectRestLinks(RestLinkType.INSTANCE) public TestRecord getById(@PathParam("id") int id) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/pom.xml b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/pom.xml index e42ff8e8c38b1..fdfb35a79cecb 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/pom.xml +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/pom.xml @@ -18,6 +18,12 @@ io.quarkus quarkus-resteasy-reactive + + + io.quarkus + quarkus-hal + true + diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/HalServerResponseFilter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/HalServerResponseFilter.java new file mode 100644 index 0000000000000..d537eb3741843 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/HalServerResponseFilter.java @@ -0,0 +1,80 @@ +package io.quarkus.resteasy.reactive.links.runtime.hal; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import javax.inject.Inject; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.common.util.RestMediaType; +import org.jboss.resteasy.reactive.server.ServerResponseFilter; +import org.jboss.resteasy.reactive.server.core.CurrentRequestManager; + +import io.quarkus.hal.HalCollectionWrapper; +import io.quarkus.hal.HalEntityWrapper; +import io.quarkus.hal.HalService; + +public class HalServerResponseFilter { + + private static final String COLLECTION_NAME = "items"; + + private final HalService service; + + @Inject + public HalServerResponseFilter(HalService service) { + this.service = service; + } + + @ServerResponseFilter + public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext, Throwable t) { + if (t == null) { + Object entity = responseContext.getEntity(); + if (isHttpStatusSuccessful(responseContext.getStatusInfo()) + && acceptsHalMediaType(requestContext) + && canEntityBeProcessed(entity)) { + if (entity instanceof Collection) { + responseContext.setEntity(service.toHalCollectionWrapper((Collection) entity, COLLECTION_NAME, + findEntityClass())); + } else { + responseContext.setEntity(service.toHalWrapper(entity)); + } + } + } + } + + private boolean canEntityBeProcessed(Object entity) { + return entity != null + && !(entity instanceof String) + && !(entity instanceof HalEntityWrapper || entity instanceof HalCollectionWrapper); + } + + private boolean isHttpStatusSuccessful(Response.StatusType statusInfo) { + return Response.Status.Family.SUCCESSFUL.equals(statusInfo.getFamily()); + } + + private boolean acceptsHalMediaType(ContainerRequestContext requestContext) { + List acceptMediaType = requestContext.getAcceptableMediaTypes().stream().map(MediaType::toString).collect( + Collectors.toList()); + return acceptMediaType.contains(RestMediaType.APPLICATION_HAL_JSON); + } + + private Class findEntityClass() { + Type entityType = CurrentRequestManager.get().getTarget().getReturnType(); + if (entityType instanceof ParameterizedType) { + // we can resolve the entity class from the param type + Type itemEntityType = ((ParameterizedType) entityType).getActualTypeArguments()[0]; + if (itemEntityType instanceof Class) { + return (Class) itemEntityType; + } + } + + return null; + } + +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java new file mode 100644 index 0000000000000..7840131160a9a --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/hal/ResteasyReactiveHalService.java @@ -0,0 +1,42 @@ +package io.quarkus.resteasy.reactive.links.runtime.hal; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.ws.rs.core.Link; + +import io.quarkus.hal.HalLink; +import io.quarkus.hal.HalService; +import io.quarkus.resteasy.reactive.links.RestLinksProvider; + +@RequestScoped +public class ResteasyReactiveHalService extends HalService { + private final RestLinksProvider linksProvider; + + @Inject + public ResteasyReactiveHalService(RestLinksProvider linksProvider) { + this.linksProvider = linksProvider; + } + + @Override + protected Map getClassLinks(Class entityType) { + return linksToMap(linksProvider.getTypeLinks(entityType)); + } + + @Override + protected Map getInstanceLinks(Object entity) { + return linksToMap(linksProvider.getInstanceLinks(entity)); + } + + private Map linksToMap(Collection refLinks) { + Map links = new HashMap<>(); + for (Link link : refLinks) { + links.put(link.getRel(), new HalLink(link.getUri().toString())); + } + + return links; + } +} diff --git a/extensions/spring-data-rest/deployment/pom.xml b/extensions/spring-data-rest/deployment/pom.xml index 41f083e166eed..9a7f1082a4298 100644 --- a/extensions/spring-data-rest/deployment/pom.xml +++ b/extensions/spring-data-rest/deployment/pom.xml @@ -41,6 +41,11 @@ quarkus-resteasy-jsonb-deployment test + + io.quarkus + quarkus-resteasy-links-deployment + test + io.rest-assured rest-assured diff --git a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java index e3b67da1513f7..a54899eeb4fef 100644 --- a/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java +++ b/extensions/spring-data-rest/deployment/src/main/java/io/quarkus/spring/data/rest/deployment/SpringDataRestProcessor.java @@ -21,6 +21,7 @@ import io.quarkus.arc.deployment.GeneratedBeanBuildItem; import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor; import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; @@ -29,6 +30,7 @@ import io.quarkus.rest.data.panache.RestDataPanacheException; import io.quarkus.rest.data.panache.deployment.ResourceMetadata; import io.quarkus.rest.data.panache.deployment.RestDataResourceBuildItem; +import io.quarkus.rest.data.panache.deployment.properties.ResourceProperties; import io.quarkus.rest.data.panache.deployment.properties.ResourcePropertiesBuildItem; import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; import io.quarkus.resteasy.reactive.spi.ExceptionMapperBuildItem; @@ -75,27 +77,27 @@ AdditionalBeanBuildItem registerTransactionalExecutor() { } @BuildStep - void registerCrudRepositories(CombinedIndexBuildItem indexBuildItem, + void registerCrudRepositories(CombinedIndexBuildItem indexBuildItem, Capabilities capabilities, BuildProducer implementationsProducer, BuildProducer restDataResourceProducer, BuildProducer resourcePropertiesProducer, BuildProducer unremovableBeansProducer) { IndexView index = indexBuildItem.getIndex(); - implementResources(implementationsProducer, restDataResourceProducer, resourcePropertiesProducer, + implementResources(capabilities, implementationsProducer, restDataResourceProducer, resourcePropertiesProducer, unremovableBeansProducer, new CrudMethodsImplementor(index), new CrudPropertiesProvider(index), getRepositoriesToImplement(index, CRUD_REPOSITORY_INTERFACE)); } @BuildStep - void registerPagingAndSortingRepositories(CombinedIndexBuildItem indexBuildItem, + void registerPagingAndSortingRepositories(CombinedIndexBuildItem indexBuildItem, Capabilities capabilities, BuildProducer implementationsProducer, BuildProducer restDataResourceProducer, BuildProducer resourcePropertiesProducer, BuildProducer unremovableBeansProducer) { IndexView index = indexBuildItem.getIndex(); - implementResources(implementationsProducer, restDataResourceProducer, resourcePropertiesProducer, + implementResources(capabilities, implementationsProducer, restDataResourceProducer, resourcePropertiesProducer, unremovableBeansProducer, new PagingAndSortingMethodsImplementor(index), new PagingAndSortingPropertiesProvider(index), getRepositoriesToImplement(index, PAGING_AND_SORTING_REPOSITORY_INTERFACE, JPA_REPOSITORY_INTERFACE)); @@ -105,7 +107,8 @@ unremovableBeansProducer, new PagingAndSortingMethodsImplementor(index), * Implement the {@link io.quarkus.rest.data.panache.RestDataResource} interface for each given Spring Data * repository and register its metadata and properties to be later picked up by the `rest-data-panache` extension. */ - private void implementResources(BuildProducer implementationsProducer, + private void implementResources(Capabilities capabilities, + BuildProducer implementationsProducer, BuildProducer restDataResourceProducer, BuildProducer resourcePropertiesProducer, BuildProducer unremovableBeansProducer, @@ -127,8 +130,8 @@ private void implementResources(BuildProducer implementa new ResourceMetadata(resourceClass, repositoryInterface, entityType, idType))); // Spring Data repositories use different annotations for configuration and we translate them for // the rest-data-panache here. - resourcePropertiesProducer.produce(new ResourcePropertiesBuildItem(resourceClass, - propertiesProvider.getResourceProperties(repositoryInterface))); + ResourceProperties resourceProperties = propertiesProvider.getResourceProperties(repositoryInterface); + resourcePropertiesProducer.produce(new ResourcePropertiesBuildItem(resourceClass, resourceProperties)); // Make sure that repository bean is not removed and will be injected to the generated resource unremovableBeansProducer.produce(new UnremovableBeanBuildItem( new UnremovableBeanBuildItem.BeanTypeExclusion(DotName.createSimple(repositoryInterface)))); diff --git a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/RestMediaType.java b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/RestMediaType.java index 57bb4fa6dd0b8..215fa1b28aad7 100644 --- a/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/RestMediaType.java +++ b/independent-projects/resteasy-reactive/common/runtime/src/main/java/org/jboss/resteasy/reactive/common/util/RestMediaType.java @@ -9,6 +9,8 @@ public class RestMediaType extends MediaType { public static final String APPLICATION_NDJSON = "application/x-ndjson"; public static final RestMediaType APPLICATION_NDJSON_TYPE = new RestMediaType("application", "x-ndjson"); + public static final String APPLICATION_HAL_JSON = "application/hal+json"; + public static final RestMediaType APPLICATION_HAL_JSON_TYPE = new RestMediaType("application", "hal+json"); public static final String APPLICATION_STREAM_JSON = "application/stream+json"; public static final RestMediaType APPLICATION_STREAM_JSON_TYPE = new RestMediaType("application", "stream+json");