Reputation: 211
I have defined custom controllers for my repositories. For example, one looks like this
@RequestMapping(method = RequestMethod.GET, value = "/myEntities")
public ResponseEntity<?> get() {
...TO SOME STUFF
MyEntity myEntity= myEntityRepository.findById(1);
return ResponseEntity.ok(new Resource<>(myEntity));
}
This returns a JSON format data which includes a _links section, where I can get the href to the entity. Now if I want to return an array of entities which are all resources, I get stuck.
What I have tried so far:
1.
@RequestMapping(method = RequestMethod.GET, value = "/myEntities")
public ResponseEntity<?> get() {
...TO SOME STUFF
List<MyEntity> myEntityList= myEntityRepository.findAll(1);
return ResponseEntity.ok(new Resources<>(myEntityList));
}
2.
@RequestMapping(method = RequestMethod.GET, value = "/myEntities")
public ResponseEntity<?> get() {
...TO SOME STUFF
List<MyEntity> myEntityList= myEntityRepository.findAll();
List<Resource<MyEntity>> resources = new ArrayList<>();
myEntityList.forEach(me -> {
resources.add(new Resource<>(me));
})
return ResponseEntity.ok(resources);
}
Option 1. and 2. don't add _links to the result and I don't understand why. I have googled it a lot and you can add links manually but this seems to be a much clearer way. Can anybody understand, what I'm doing wrong?
Upvotes: 1
Views: 1770
Reputation: 21
There are answers at "adding association links to spring data rest custom exposed method" and "Enable HAL serialization in Spring Boot for custom controller method" for similar questions. I tried something a little different from those answers, for solving a similar situation, and it worked fine. I wish you get your problem solved.
It follows how I did solve my specific situation:
@PostMapping(value = "/myEntities/searchWithParams"
, consumes = MediaType.APPLICATION_JSON_VALUE
, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> searchWithParams(@RequestBody SearchParamsDtoClass params, PersistentEntityResourceAssembler assembler)
{
List<MyEntity> entities = myEntityService.searchWithParams(params);
List<PersistentEntityResource> resourcesList = new ArrayList<PersistentEntityResource>();
for (MyEntity entity: entities) {
PersistentEntityResource resource = assembler.toResource(entity);
resourcesList.add(resource);
}
Resources<PersistentEntityResource> resources = new Resources(resourcesList);
return ResponseEntity.ok(resources);
}
Upvotes: 2
Reputation: 1393
The Resources constructor accepts a collection of embedded content, which is not the same as a collection of links. You have to add the links manually.
Resources resources = new Resources();
resources.add(myEntityRepository
.findAll()
.stream()
.map(entry -> convertToLink(entry)
.collect(Collectors.toList()));
return ResponseEntity.ok(resources);
Here is an example that includes embedded content and pagination in a HAL compliant format.
import static java.util.Objects.*;
import static java.util.Optional.*;
import static org.elasticsearch.index.query.QueryBuilders.*;
import static org.springframework.hateoas.MediaTypes.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.http.MediaType.*;
import static org.springframework.web.bind.annotation.RequestMethod.*;
import java.util.ArrayList;
import javax.inject.Inject;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import org.elasticsearch.index.query.QueryBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.hateoas.Link;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.codahale.metrics.annotation.Timed;
@RestController
@RequestMapping("/catalog/users")
public class UserResource {
private final @NotNull UserLocator locator;
private final @NotNull ElasticsearchOperations elasticsearchTemplate;
@Inject
public UserResource(
final @NotNull UserLocator locator,
final @NotNull ElasticsearchOperations elasticsearchTemplate
) {
this.locator = requireNonNull(locator, "locator cannot be null");
this.elasticsearchTemplate = requireNonNull(elasticsearchTemplate, "elasticsearchTemplate cannot be null");
}
/**
* GET /users : get all the users.
*
* @return the ResponseEntity with status 200 (OK) and the list of users in body
*/
@Timed
@RequestMapping(method = GET, produces = { HAL_JSON_VALUE, APPLICATION_JSON_VALUE })
public ResponseEntity<Representations<User>> allUsers(
@RequestParam(name = "page", required = false, defaultValue = "0") @Min(0) int page,
@RequestParam(name = "size", required = false, defaultValue = "25") @Min(1) int size,
@RequestParam(name = "like", required = false) String like
) {
final PageRequest pageRequest = new PageRequest(page, size, Sort.Direction.ASC, "surname.raw", "givenName.raw");
final Page<User> entries = elasticsearchTemplate.queryForPage(
new NativeSearchQueryBuilder()
.withQuery(startingWith(like))
.withPageable(pageRequest)
.build(),
User.class
);
final ArrayList<Link> links = new ArrayList<>();
links.add(linkTo(UserResource.class).withSelfRel());
if (!entries.isFirst()) {
links.add(linkTo(methodOn(UserResource.class).allUsers(0, size, like)).withRel("first"));
}
if (!entries.isLast()) {
links.add(linkTo(methodOn(UserResource.class).allUsers(entries.getTotalPages() - 1, size, like)).withRel("last"));
}
if (entries.hasNext()) {
links.add(linkTo(methodOn(UserResource.class).allUsers(entries.nextPageable().getPageNumber(), size, like)).withRel("next"));
}
if (entries.hasPrevious()) {
links.add(linkTo(methodOn(UserResource.class).allUsers(entries.previousPageable().getPageNumber(), size, like)).withRel("prev"));
}
final Representations<User> resourceList = new Representations<>(entries, Representations.extractMetadata(entries), links);
return ResponseEntity.ok(resourceList);
}
private QueryBuilder startingWith(String like) {
return isNull(like) ? null : matchPhrasePrefixQuery("_all", like);
}
/**
* GET /users/:identifier : get the "identifier" user.
*
* @param identifier the identifier of the role to retrieve
* @return the ResponseEntity with status 200 (OK) and with body the role, or with status 404 (Not Found) or with 410 (Gone)
*/
@Timed
@RequestMapping(value = "/{identifier}", method = GET, produces = APPLICATION_JSON_VALUE)
public ResponseEntity<UserRepresentation> aUser(
@PathVariable("identifier") @NotNull String identifier
) {
return ofNullable(this.locator.findOne(identifier))
.map(user -> toRepresentation(user))
.map(ResponseEntity::ok)
.orElse(notFound())
;
}
private @NotNull UserRepresentation toRepresentation(final @NotNull User role) {
final String id = role.getIdentifier();
final Link self = linkTo(methodOn(UserResource.class).aUser(id)).withSelfRel().expand(id);
final Link collection = linkTo(UserResource.class).withRel("collection");
return new UserRepresentation(role, self, collection);
}
protected final @NotNull <U> ResponseEntity<U> notFound() {
return new ResponseEntity<>(NOT_FOUND);
}
}
import java.util.Optional;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.core.Relation;
@Relation(value = "item", collectionRelation = "items")
public class UserRepresentation extends Representation<User> {
public UserRepresentation(User content, Iterable<Optional<Link>> links) {
super(content, links);
}
public UserRepresentation(User content, Link... links) {
super(content, links);
}
@SafeVarargs
public UserRepresentation(User content, Optional<Link>... links) {
super(content, links);
}
public UserRepresentation(User content) {
super(content);
}
}
import static java.util.Arrays.*;
import static java.util.Collections.*;
import static java.util.Objects.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
public class Representation<T> extends Resource<T> {
private final Map<String, ResourceSupport> embedded = new HashMap<>();
public Representation(final @NotNull T content) {
super(content, emptyList());
}
public Representation(final @NotNull T content, final @NotNull Link... links) {
super(content, links);
}
@SafeVarargs
public Representation(final @NotNull T content, final @NotNull Optional<Link>... links) {
this(content, (Iterable<Optional<Link>>) asList(links));
}
public Representation(final @NotNull T content, final @NotNull Iterable<Optional<Link>> links) {
super(content, emptyList());
asStream(links).forEach(this::add);
}
public void add(final @NotNull Optional<Link> link) {
if (null != link && link.isPresent()) super.add(link.get());
}
@JsonProperty("_embedded")
@JsonInclude(Include.NON_EMPTY)
public final @NotNull Map<String, ResourceSupport> getEmbedded() {
return this.embedded;
}
/**
* @param rel must not be {@literal null} or empty.
* @param resource the resource to embed
*/
public final void embed(final @NotNull @Size(min=1) String rel, final ResourceSupport resource) {
requireNonNull(rel, "rel cannot be null");
if (rel.trim().isEmpty()) {
throw new IllegalArgumentException("rel cannot be empty");
}
if (null != resource) {
this.embedded.put(rel, resource);
}
}
}
import javax.validation.constraints.NotNull;
import org.springframework.data.domain.Page;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.PagedResources.PageMetadata;
import org.springframework.hateoas.core.Relation;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
@Relation(collectionRelation = "items")
public class Representations<T> extends Resources<T> {
@JsonUnwrapped
@JsonInclude(JsonInclude.Include.NON_NULL)
private final PageMetadata metadata;
public Representations(Iterable<T> content) {
super(content);
this.metadata = null;
}
public Representations(Iterable<T> content, PageMetadata metadata) {
super(content);
this.metadata = metadata;
}
public Representations(Iterable<T> content, Iterable<Link> links) {
super(content, links);
this.metadata = null;
}
public Representations(Iterable<T> content, PageMetadata metadata, Iterable<Link> links) {
super(content, links);
this.metadata = metadata;
}
public Representations(Iterable<T> content, Link... links) {
super(content, links);
this.metadata = null;
}
public Representations(Iterable<T> content, PageMetadata metadata, Link... links) {
super(content, links);
this.metadata = metadata;
}
/**
* Returns the pagination metadata.
*
* @return the metadata
*/
@JsonProperty("page")
public PageMetadata getMetadata() {
return metadata;
}
public static <U> PageMetadata extractMetadata(final @NotNull Page<U> page) {
return new PageMetadata(page.getSize(), page.getNumber(), page.getTotalElements(), page.getTotalPages());
}
}
Upvotes: 0