Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support title and type fields when generating HAL links #44501

Merged
merged 1 commit into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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