Skip to content

Commit

Permalink
Support title and type fields when generating HAL links
Browse files Browse the repository at this point in the history
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"
    }
  }
}
```
  • Loading branch information
Sgitario committed Nov 14, 2024
1 parent 20dc22a commit 679c1ed
Show file tree
Hide file tree
Showing 12 changed files with 117 additions and 13 deletions.
14 changes: 13 additions & 1 deletion extensions/hal/runtime/src/main/java/io/quarkus/hal/HalLink.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,24 @@
public class HalLink {

private final String href;
private final String title;
private final String type;

public HalLink(String href) {
public HalLink(String href, String title, String type) {
this.href = href;
this.title = title;
this.type = type;
}

public String getHref() {
return href;
}

public String getTitle() {
return title;
}

public String getType() {
return type;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ public class HalLinkJacksonSerializer extends JsonSerializer<HalLink> {
public void serialize(HalLink value, JsonGenerator generator, SerializerProvider serializers) throws IOException {
generator.writeStartObject();
generator.writeObjectField("href", value.getHref());
if (value.getTitle() != null) {
generator.writeObjectField("title", value.getTitle());
}

if (value.getType() != null) {
generator.writeObjectField("type", value.getType());
}

generator.writeEndObject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ public class HalLinkJsonbSerializer implements JsonbSerializer<HalLink> {
public void serialize(HalLink value, JsonGenerator generator, SerializationContext context) {
generator.writeStartObject();
generator.write("href", value.getHref());
if (value.getTitle() != null) {
generator.write("title", value.getTitle());
}

if (value.getType() != null) {
generator.write("type", value.getType());
}

generator.writeEnd();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ public Map<String, HalLink> getLinks() {
@SuppressWarnings("unused")
public void addLinks(Link... links) {
for (Link link : links) {
this.links.put(link.getRel(), new HalLink(link.getUri().toString()));
this.links.put(link.getRel(), new HalLink(link.getUri().toString(),
link.getTitle(),
link.getType()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ protected Map<String, HalLink> getInstanceLinks(Object entity) {
private Map<String, HalLink> linksToMap(RESTServiceDiscovery serviceDiscovery) {
Map<String, HalLink> links = new HashMap<>(serviceDiscovery.size());
for (RESTServiceDiscovery.AtomLink atomLink : serviceDiscovery) {
links.put(atomLink.getRel(), new HalLink(atomLink.getHref()));
links.put(atomLink.getRel(), new HalLink(atomLink.getHref(), atomLink.getTitle(), atomLink.getType()));
}
return links;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,16 @@ private LinkInfo getLinkInfo(ResourceMethod resourceMethod, MethodInfo resourceM
AnnotationInstance restLinkAnnotation, String resourceClassPath, IndexView index) {
Type returnType = getNonAsyncReturnType(resourceMethodInfo.returnType());
String rel = getAnnotationValue(restLinkAnnotation, "rel", deductRel(resourceMethod, returnType, index));
String title = getAnnotationValue(restLinkAnnotation, "title", null);
String type = getAnnotationValue(restLinkAnnotation, "type", null);
String entityType = getAnnotationValue(restLinkAnnotation, "entityType", deductEntityType(returnType));
String path = UriBuilder.fromPath(resourceClassPath).path(resourceMethod.getPath()).toTemplate();
while (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
Set<String> pathParameters = getPathParameters(path);

return new LinkInfo(rel, entityType, path, pathParameters);
return new LinkInfo(rel, title, type, entityType, path, pathParameters);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import static io.restassured.RestAssured.given;
import static org.assertj.core.api.Assertions.assertThat;

import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;

import org.jboss.resteasy.reactive.common.util.RestMediaType;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -89,5 +92,19 @@ void shouldGetHalLinksForRestLinkId() {
.thenReturn();

assertThat(response.body().jsonPath().getString("_links.self.href")).endsWith("/records/with-rest-link-id/100");
assertThat(response.body().jsonPath().getString("_links.self.title")).isEqualTo("The with rest link title");
assertThat(response.body().jsonPath().getString("_links.self.type")).isEqualTo(MediaType.APPLICATION_JSON);
}

@Test
void shouldIncludeAllFieldsFromLink() {
Response response = given()
.header(HttpHeaders.ACCEPT, RestMediaType.APPLICATION_HAL_JSON)
.get("/records/with-rest-link-with-all-fields")
.thenReturn();

assertThat(response.body().jsonPath().getString("_links.all.href")).endsWith("/records/with-rest-link-id/100");
assertThat(response.body().jsonPath().getString("_links.all.title")).isEqualTo("The title link");
assertThat(response.body().jsonPath().getString("_links.all.type")).isEqualTo(MediaType.APPLICATION_JSON);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.resteasy.reactive.links.deployment;

import java.net.URI;
import java.time.Duration;
import java.util.Arrays;
import java.util.LinkedList;
Expand All @@ -11,10 +12,12 @@
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Link;
import jakarta.ws.rs.core.MediaType;

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

import io.quarkus.hal.HalEntityWrapper;
import io.quarkus.resteasy.reactive.links.InjectRestLinks;
import io.quarkus.resteasy.reactive.links.RestLink;
import io.quarkus.resteasy.reactive.links.RestLinkType;
Expand Down Expand Up @@ -172,7 +175,7 @@ public TestRecordWithPersistenceId getWithPersistenceId(@PathParam("id") int id)
@GET
@Path("/with-rest-link-id/{id}")
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
@RestLink(entityType = TestRecordWithRestLinkId.class)
@RestLink(entityType = TestRecordWithRestLinkId.class, title = "The with rest link title", type = MediaType.APPLICATION_JSON)
@InjectRestLinks
public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) {
return REST_LINK_ID_RECORDS.stream()
Expand All @@ -181,4 +184,18 @@ public TestRecordWithRestLinkId getWithRestLinkId(@PathParam("id") int id) {
.orElseThrow(NotFoundException::new);
}

@GET
@Path("/with-rest-link-with-all-fields")
@Produces({ MediaType.APPLICATION_JSON, RestMediaType.APPLICATION_HAL_JSON })
public HalEntityWrapper<TestRecordWithIdAndPersistenceIdAndRestLinkId> getAllFieldsFromLink() {

var entity = new TestRecordWithIdAndPersistenceIdAndRestLinkId(1, 10, 100, "one");
return new HalEntityWrapper<>(entity,
Link.fromUri(URI.create("/records/with-rest-link-id/100"))
.rel("all")
.title("The title link")
.type(MediaType.APPLICATION_JSON)
.build());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,20 @@
*/
String rel() default "";

/**
* Intended for labelling the link with a human-readable identifier.
*
* @return the link title.
*/
String title() default "";

/**
* Hint to indicate the media type expected when dereferencing the target resource.
*
* @return the link expected media type.
*/
String type() default "";

/**
* Declares a link for the given type of resources.
* If not set, it will default to the returning type of the annotated method.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@ public final class LinkInfo {

private final String rel;

private final String title;

private final String type;

private final String entityType;

private final String path;

private final Set<String> pathParameters;

public LinkInfo(String rel, String entityType, String path, Set<String> pathParameters) {
public LinkInfo(String rel, String title, String type, String entityType, String path, Set<String> pathParameters) {
this.rel = rel;
this.title = title;
this.type = type;
this.entityType = entityType;
this.path = path;
this.pathParameters = pathParameters;
Expand All @@ -23,6 +29,14 @@ public String getRel() {
return rel;
}

public String getTitle() {
return title;
}

public String getType() {
return type;
}

public String getEntityType() {
return entityType;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ public Collection<Link> getTypeLinks(Class<?> elementType) {
List<Link> links = new ArrayList<>(linkInfoList.size());
for (LinkInfo linkInfo : linkInfoList) {
if (linkInfo.getPathParameters().size() == 0) {
links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath()))
.rel(linkInfo.getRel())
.build());
links.add(linkBuilderFor(linkInfo).build());
}
}
return links;
Expand All @@ -52,13 +50,25 @@ public <T> Collection<Link> getInstanceLinks(T instance) {
List<LinkInfo> linkInfoList = linksContainer.getForClass(instance.getClass());
List<Link> links = new ArrayList<>(linkInfoList.size());
for (LinkInfo linkInfo : linkInfoList) {
links.add(Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath()))
.rel(linkInfo.getRel())
.build(getPathParameterValues(linkInfo, instance)));
links.add(linkBuilderFor(linkInfo).build(getPathParameterValues(linkInfo, instance)));
}
return links;
}

private Link.Builder linkBuilderFor(LinkInfo linkInfo) {
Link.Builder builder = Link.fromUriBuilder(uriInfo.getBaseUriBuilder().path(linkInfo.getPath()))
.rel(linkInfo.getRel());
if (linkInfo.getTitle() != null) {
builder.title(linkInfo.getTitle());
}

if (linkInfo.getType() != null) {
builder.type(linkInfo.getType());
}

return builder;
}

private Object[] getPathParameterValues(LinkInfo linkInfo, Object instance) {
List<Object> values = new ArrayList<>(linkInfo.getPathParameters().size());
for (String name : linkInfo.getPathParameters()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ protected Map<String, HalLink> getInstanceLinks(Object entity) {
private Map<String, HalLink> linksToMap(Collection<Link> refLinks) {
Map<String, HalLink> links = new HashMap<>();
for (Link link : refLinks) {
links.put(link.getRel(), new HalLink(link.getUri().toString()));
links.put(link.getRel(), new HalLink(link.getUri().toString(), link.getTitle(), link.getType()));
}

return links;
Expand Down

0 comments on commit 679c1ed

Please sign in to comment.