Michael Hegner Michael Hegner - 3 months ago 24
Java Question

Consuming Spring Hateoas Pageable

I have a Rest-Service using HAteoas, what worked before without pageing. Now I am producing pageable Json. I did it with out-of-the box features from Spring-Hateoas. But now I am stucking consuming it and I guess it is really not well documented, if it is.

My JSON looks like follows:

{
"_embedded": {
"vertragResourceList": [
{
"identifier": 728,
"auszubildender": "Rumm",
"beruf": "Landwirt/in",
"betrieb": "Mitterbauer Johann",
"betriebsNummer": "e12d0949-67ae-4134-9dc2-fb67758b6b16",
"zustaendigeStelle": "Irgendwo",
"beginn": 529887600000,
"status": "RECENT",
"fachrichtung": null,
"schwerpunkt": "Gr├╝nland oder Ackergras",
"ende": 623113200000,
"_links": {
"self": {
"href": "http://localhost:8080/bbsng-app-rest/vertrag/728"
}
}
},
{
"identifier": 803,
"auszubildender": "Gossen",
"beruf": "Landwirt/in",
"betrieb": "Beer Johann",
"betriebsNummer": "d5a20cb9-7273-4b75-85bd-f8e7d6a843c4",
"zustaendigeStelle": "Woanders",
"beginn": 278118000000,
"status": "RECENT",
"fachrichtung": null,
"schwerpunkt": "Ackerfutterbau",
"ende": 339116400000,
"_links": {
"self": {
"href": "http://localhost:8080/bbsng-app-rest/vertrag/803"
}
}
}
]
},
"page": {
"size": 2,
"totalElements": 1000,
"totalPages": 500,
"number": 5
}
}


====

But now my list is "_embedded", so how can I consume it in most convenient way. I would prefer out-of-the-box soltions by Spring-Hateoas or similar.

My code before worked like follows (Json was not wrapped in _embedded/vertragResourceList before!!!).

@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public VertragsListe findeAlleVertraege(final Integer firstDataSet, final Integer lastDataSet, final VertragDTFilter vertragsFilter,
final VertragDTSorting vertragSorting) {
final VertragsListe vertragsListe = new VertragsListe();
final String url = LinkUtils.findeVertrag(firstDataSet, lastDataSet, vertragsFilter, vertragSorting);
final ResponseEntity<List> entity = template.getForEntity(url, List.class);

if (OK.equals(entity.getStatusCode())) {
final List<LinkedHashMap> body = entity.getBody();
for (final LinkedHashMap map : body) {
vertragsListe.add(getPopulatedVertrag(vertragsListe, map));
}
}

return vertragsListe;
}


Stacktrace:

org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON: Can not deserialize instance of java.util.ArrayList out of START_OBJECT token
at [Source: sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@e89d61c; line: 1, column: 1]; nested exception is com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.util.ArrayList out of START_OBJECT token
at [Source: sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@e89d61c; line: 1, column: 1]


=====

EDIT:

Corresponding Resourceclass looks like this (Serverside and Clientside!!!):

public class VertragPagedResources extends PagedResources<VertragResource> {

@SuppressWarnings("unchecked")
public VertragPagedResources(final Collection<VertragResource> content, final PageMetadata metadata) {
super(content, metadata, CollectionUtils.EMPTY_COLLECTION);
}

public VertragPagedResources() {
super();
}

}


On Clientside I changed now followed:

@Autowired private RestTemplate template;

@Override
public VertragPagedResources findeAlleVertraege(final Integer firstDataSet, final Integer lastDataSet, final VertragDTFilter vertragsFilter,
final VertragDTSorting vertragSorting) {
final String url = LinkUtils.findeVertrag(firstDataSet, lastDataSet, vertragsFilter, vertragSorting);
final ResponseEntity<VertragPagedResources> entity = template.getForEntity(url, VertragPagedResources.class);

if (OK.equals(entity.getStatusCode())) {
return entity.getBody();
}

return new VertragPagedResources();
}


Now I don't get any exceptions, but content is empty. The only thing what is filled correctly are the information from pageable (numberOfReturned Datasets, pageSize and so on). The content is empty List!!!
When debugging and I try out the given URL in browser, then JSON looks like above mentioned.

<200 OK,PagedResource { content: [], metadata: Metadata { number: 1, total pages: 100, total elements: 1000, size: 10 }, links: [] },{Server=[Apache-Coyote/1.1], X-Application-Context=[application:custom:8080], totalNumber=[1000], Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Wed, 28 Jan 2015 16:58:16 GMT]}>


VertragResource (Client & Server):

public class VertragResource extends IdentifierResourceSupport {

@NotNull private String auszubildender;
@NotNull private String beruf;
@NotNull private String betrieb;
@NotNull private String betriebsNummer;
@NotNull private String zustaendigeStelle;
@NotNull private Calendar beginn;
@NotNull private String status;

private String fachrichtung;
private String schwerpunkt;
private Calendar ende;

// GETTER & SETTER ....


Controller Server-Side:

@RequestMapping(method = GET, produces = MediaType.APPLICATION_JSON_VALUE)
public HttpEntity<VertragPagedResources> showAll( /* PARAMS */ ) {

// FILTER ...
final VertragFilter filter = new VertragFilter();
// FILL FILTER

// SORTING ...
final VertragSorting sorting = new VertragSorting(/* BLA */)

// COMPUTE ...
final VertragResourceAssembler assembler = new VertragResourceAssembler();
final List<Vertrag> alleVertrage = service.findeAlleVertraege(/* BLA */);
final List<VertragResource> resources = assembler.toResources(alleVertrage);

//

final long totalElements = service.zaehleAlleVertraege(filter);
final long size = Math.min(displayLength, totalElements);
final long totalPages = totalElements / size;
final PageMetadata pageMetadata = new PageMetadata(displayLength, displayStart, totalElements, totalPages);
final VertragPagedResources pagedResources = new VertragPagedResources(resources, pageMetadata);
return new HttpEntity<VertragPagedResources>(pagedResources, headerTotalNumberOfData());
}


====

IdentifierResourceSupport :

public class IdentifierResourceSupport extends ResourceSupport {
private Long identifier;

public Long getIdentifier() {
return identifier;
}

public void setIdentifier(Long identifier) {
this.identifier = identifier;
}
}


=====

EDIT 2:

What I did now, I switched Spring-Boot from 1.2.1 back to 1.1.10. Now I get an exception when trying the same, it seems that Spring-Boot 1.2.1 hides the exception:

org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON: Unrecognized field "_embedded" (class at.compax.bbsng.client.mvc.client.resource.VertragPagedResources), not marked as ignorable (3 known properties: "links", "content", "page"])
at [Source: sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@62532c56; line: 1, column: 15] (through reference chain: at.compax.bbsng.client.mvc.client.resource.VertragPagedResources["_embedded"]); nested exception is com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "_embedded" (class at.compax.bbsng.client.mvc.client.resource.VertragPagedResources), not marked as ignorable (3 known properties: "links", "content", "page"])
at [Source: sun.net.www.protocol.http.HttpURLConnection$HttpInputStream@62532c56; line: 1, column: 15] (through reference chain: at.compax.bbsng.client.mvc.client.resource.VertragPagedResources["_embedded"])

Answer

Okay,

I got the solution now.

First of all, there is a BUG in Spring Boot 1.2.1. like mentioned in the question EDIT2. Spring-Boot 1.1.10 throws exception, because it cannot map the content in _embedded. Thats why my content stayed empty. Spring Boot 1.2.1 just leave it emptry without any hint as exception. Downgrading to 1.1.10 gave me the hint.

So what were the consequence to change:

Controller on Serverside:

@RequestMapping(method = GET, produces = "application/hal+json")
public HttpEntity<VertragPagedResources> showAll( /* PARAMS */  ) { 

    // LIKE CODE IN QUESTION ...

    return new HttpEntity<VertragPagedResources>(pagedResources);
}

====

RestTemplate Config:

Then you need to configure your Resttemplate to handle HAL-Format.

@Bean
public RestTemplate restTemplate() {
    final ObjectMapper mapper = new ObjectMapper();
    mapper.registerModule(new Jackson2HalModule());

    final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    converter.setSupportedMediaTypes(MediaType.parseMediaTypes("application/hal+json"));
    converter.setObjectMapper(mapper);

    return new RestTemplate(Collections.<HttpMessageConverter<?>> singletonList(converter));
}

The Client-Code stays same as first Edit in Question!