Skip to content

Commit 679c1ed

Browse files
committed
Support title and type fields when generating HAL links
Before these changes, we were only considering the "href" field for the HAL links. Example: ```json { "_links": { "subject": { "href": "/subject" } } } ``` After these changes, users that populate also the title and/or the type like: ```java @get @path("/with-rest-link-with-all-fields") @produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) public HalEntityWrapper<TestRecordWithIdAndPersistenceIdAndRestLinkId> get() { var entity = // ... return new HalEntityWrapper<>(entity, Link.fromUri(URI.create("/path/to/100")) .rel("all") .title("The title link") // the link title .type(MediaType.APPLICATION_JSON) // the link type .build()); } ``` Or using the annotation like: ```java @get @produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON }) @RestLink(entityType = TestRecordWithRestLinkId.class, title = "The with rest link title", type = MediaType.APPLICATION_JSON) @InjectRestLinks public TestRecordWithRestLinkId get() { return // ... } ``` Then, the links will have the title and/or type fields populated: ```json { "_links": { "subject": { "href": "/subject", "title": "The with rest link title", "type": "application/json" } } } ```
1 parent 20dc22a commit 679c1ed

File tree

12 files changed

+117
-13
lines changed

12 files changed

+117
-13
lines changed

extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,24 @@
33
public class HalLink {
44

55
private final String href;
6+
private final String title;
7+
private final String type;
68

7-
public HalLink(String href) {
9+
public HalLink(String href, String title, String type) {
810
this.href = href;
11+
this.title = title;
12+
this.type = type;
913
}
1014

1115
public String getHref() {
1216
return href;
1317
}
18+
19+
public String getTitle() {
20+
return title;
21+
}
22+
23+
public String getType() {
24+
return type;
25+
}
1426
}

extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJacksonSerializer.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ public class HalLinkJacksonSerializer extends JsonSerializer<HalLink> {
1212
public void serialize(HalLink value, JsonGenerator generator, SerializerProvider serializers) throws IOException {
1313
generator.writeStartObject();
1414
generator.writeObjectField("href", value.getHref());
15+
if (value.getTitle() != null) {
16+
generator.writeObjectField("title", value.getTitle());
17+
}
18+
19+
if (value.getType() != null) {
20+
generator.writeObjectField("type", value.getType());
21+
}
22+
1523
generator.writeEndObject();
1624
}
1725
}

extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLinkJsonbSerializer.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ public class HalLinkJsonbSerializer implements JsonbSerializer<HalLink> {
1010
public void serialize(HalLink value, JsonGenerator generator, SerializationContext context) {
1111
generator.writeStartObject();
1212
generator.write("href", value.getHref());
13+
if (value.getTitle() != null) {
14+
generator.write("title", value.getTitle());
15+
}
16+
17+
if (value.getType() != null) {
18+
generator.write("type", value.getType());
19+
}
20+
1321
generator.writeEnd();
1422
}
1523
}

extensions/hal/runtime/src/main/java/io/quarkus/hal/HalWrapper.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ public Map<String, HalLink> getLinks() {
2424
@SuppressWarnings("unused")
2525
public void addLinks(Link... links) {
2626
for (Link link : links) {
27-
this.links.put(link.getRel(), new HalLink(link.getUri().toString()));
27+
this.links.put(link.getRel(), new HalLink(link.getUri().toString(),
28+
link.getTitle(),
29+
link.getType()));
2830
}
2931
}
3032
}

extensions/resteasy-classic/resteasy-links/runtime/src/main/java/io/quarkus/resteasy/links/runtime/hal/ResteasyHalService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ protected Map<String, HalLink> getInstanceLinks(Object entity) {
2929
private Map<String, HalLink> linksToMap(RESTServiceDiscovery serviceDiscovery) {
3030
Map<String, HalLink> links = new HashMap<>(serviceDiscovery.size());
3131
for (RESTServiceDiscovery.AtomLink atomLink : serviceDiscovery) {
32-
links.put(atomLink.getRel(), new HalLink(atomLink.getHref()));
32+
links.put(atomLink.getRel(), new HalLink(atomLink.getHref(), atomLink.getTitle(), atomLink.getType()));
3333
}
3434
return links;
3535
}

extensions/resteasy-reactive/rest-links/deployment/src/main/java/io/quarkus/resteasy/reactive/links/deployment/LinksContainerFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,16 @@ private LinkInfo getLinkInfo(ResourceMethod resourceMethod, MethodInfo resourceM
6262
AnnotationInstance restLinkAnnotation, String resourceClassPath, IndexView index) {
6363
Type returnType = getNonAsyncReturnType(resourceMethodInfo.returnType());
6464
String rel = getAnnotationValue(restLinkAnnotation, "rel", deductRel(resourceMethod, returnType, index));
65+
String title = getAnnotationValue(restLinkAnnotation, "title", null);
66+
String type = getAnnotationValue(restLinkAnnotation, "type", null);
6567
String entityType = getAnnotationValue(restLinkAnnotation, "entityType", deductEntityType(returnType));
6668
String path = UriBuilder.fromPath(resourceClassPath).path(resourceMethod.getPath()).toTemplate();
6769
while (path.endsWith("/")) {
6870
path = path.substring(0, path.length() - 1);
6971
}
7072
Set<String> pathParameters = getPathParameters(path);
7173

72-
return new LinkInfo(rel, entityType, path, pathParameters);
74+
return new LinkInfo(rel, title, type, entityType, path, pathParameters);
7375
}
7476

7577
/**

extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/AbstractHalLinksTest.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import static io.restassured.RestAssured.given;
44
import static org.assertj.core.api.Assertions.assertThat;
55

6+
import jakarta.ws.rs.core.HttpHeaders;
7+
import jakarta.ws.rs.core.MediaType;
8+
69
import org.jboss.resteasy.reactive.common.util.RestMediaType;
710
import org.junit.jupiter.api.Test;
811

@@ -89,5 +92,19 @@ void shouldGetHalLinksForRestLinkId() {
8992
.thenReturn();
9093

9194
assertThat(response.body().jsonPath().getString("_links.self.href")).endsWith("/records/with-rest-link-id/100");
95+
assertThat(response.body().jsonPath().getString("_links.self.title")).isEqualTo("The with rest link title");
96+
assertThat(response.body().jsonPath().getString("_links.self.type")).isEqualTo(MediaType.APPLICATION_JSON);
97+
}
98+
99+
@Test
100+
void shouldIncludeAllFieldsFromLink() {
101+
Response response = given()
102+
.header(HttpHeaders.ACCEPT, RestMediaType.APPLICATION_HAL_JSON)
103+
.get("/records/with-rest-link-with-all-fields")
104+
.thenReturn();
105+
106+
assertThat(response.body().jsonPath().getString("_links.all.href")).endsWith("/records/with-rest-link-id/100");
107+
assertThat(response.body().jsonPath().getString("_links.all.title")).isEqualTo("The title link");
108+
assertThat(response.body().jsonPath().getString("_links.all.type")).isEqualTo(MediaType.APPLICATION_JSON);
92109
}
93110
}

extensions/resteasy-reactive/rest-links/deployment/src/test/java/io/quarkus/resteasy/reactive/links/deployment/TestResource.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.quarkus.resteasy.reactive.links.deployment;
22

3+
import java.net.URI;
34
import java.time.Duration;
45
import java.util.Arrays;
56
import java.util.LinkedList;
@@ -11,10 +12,12 @@
1112
import jakarta.ws.rs.Path;
1213
import jakarta.ws.rs.PathParam;
1314
import jakarta.ws.rs.Produces;
15+
import jakarta.ws.rs.core.Link;
1416
import jakarta.ws.rs.core.MediaType;
1517

1618
import org.jboss.resteasy.reactive.common.util.RestMediaType;
1719

20+
import io.quarkus.hal.HalEntityWrapper;
1821
import io.quarkus.resteasy.reactive.links.InjectRestLinks;
1922
import io.quarkus.resteasy.reactive.links.RestLink;
2023
import io.quarkus.resteasy.reactive.links.RestLinkType;
@@ -172,7 +175,7 @@ public TestRecordWithPersistenceId getWithPersistenceId(@PathParam("id") int id)
172175
@GET
173176
@Path("/with-rest-link-id/{id}")
174177
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
175-
@RestLink(entityType = TestRecordWithRestLinkId.class)
178+
@RestLink(entityType = TestRecordWithRestLinkId.class, title = "The with rest link title", type = MediaType.APPLICATION_JSON)
176179
@InjectRestLinks
177180
public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) {
178181
return REST_LINK_ID_RECORDS.stream()
@@ -181,4 +184,18 @@ public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) {
181184
.orElseThrow(NotFoundException::new);
182185
}
183186

187+
@GET
188+
@Path("/with-rest-link-with-all-fields")
189+
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
190+
public HalEntityWrapper<TestRecordWithIdAndPersistenceIdAndRestLinkId> getAllFieldsFromLink() {
191+
192+
var entity = new TestRecordWithIdAndPersistenceIdAndRestLinkId(1, 10, 100, "one");
193+
return new HalEntityWrapper<>(entity,
194+
Link.fromUri(URI.create("/records/with-rest-link-id/100"))
195+
.rel("all")
196+
.title("The title link")
197+
.type(MediaType.APPLICATION_JSON)
198+
.build());
199+
}
200+
184201
}

extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/RestLink.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,20 @@
2525
*/
2626
String rel() default "";
2727

28+
/**
29+
* Intended for labelling the link with a human-readable identifier.
30+
*
31+
* @return the link title.
32+
*/
33+
String title() default "";
34+
35+
/**
36+
* Hint to indicate the media type expected when dereferencing the target resource.
37+
*
38+
* @return the link expected media type.
39+
*/
40+
String type() default "";
41+
2842
/**
2943
* Declares a link for the given type of resources.
3044
* If not set, it will default to the returning type of the annotated method.

extensions/resteasy-reactive/rest-links/runtime/src/main/java/io/quarkus/resteasy/reactive/links/runtime/LinkInfo.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,20 @@ public final class LinkInfo {
66

77
private final String rel;
88

9+
private final String title;
10+
11+
private final String type;
12+
913
private final String entityType;
1014

1115
private final String path;
1216

1317
private final Set<String> pathParameters;
1418

15-
public LinkInfo(String rel, String entityType, String path, Set<String> pathParameters) {
19+
public LinkInfo(String rel, String title, String type, String entityType, String path, Set<String> pathParameters) {
1620
this.rel = rel;
21+
this.title = title;
22+
this.type = type;
1723
this.entityType = entityType;
1824
this.path = path;
1925
this.pathParameters = pathParameters;
@@ -23,6 +29,14 @@ public String getRel() {
2329
return rel;
2430
}
2531

32+
public String getTitle() {
33+
return title;
34+
}
35+
36+
public String getType() {
37+
return type;
38+
}
39+
2640
public String getEntityType() {
2741
return entityType;
2842
}

0 commit comments

Comments
 (0)