Spring REST response is different in a custom controller - java

I have several controllers that are automatically creating REST endpoints.
#RepositoryRestResource(collectionResourceRel = "books", path = "books")
public interface BooksRepository extends CrudRepository<Books, Integer> {
public Page<Books> findTopByNameOrderByFilenameDesc(String name);
}
When I visit: http://localhost:8080/Books
I get back:
{
"_embedded": {
"Books": [{
"id": ,
"filename": "Test123",
"name": "test123",
"_links": {
"self": {
"href": "http://localhost:8080/books/123"
},
"Books": {
"href": "http://localhost:8080/books/123"
}
}
}]
},
"_links": {
"self": {
"href": "http://localhost:8080/books"
},
"profile": {
"href": "http://localhost:8080/profile/books"
},
"search": {
"href": "http://localhost:8080/books/search"
},
"page": {
"size": 20,
"totalElements": 81,
"totalPages": 5,
"number": 0
}
}
}
When I create my own controller:
#Controller
#RequestMapping(value = "/CustomBooks")
public class CustomBooksController {
#Autowired
public CustomBookService customBookService;
#RequestMapping("/search")
#ResponseBody
public Page<Book> search(#RequestParam(value = "q", required = false) String query,
#PageableDefault(page = 0, size = 20) Pageable pageable) {
return customBookService.findAll();
}
}
I'll get a response back that looks nothing like the automatically generated controller response:
{
"content": [{
"filename": "Test123",
"name" : "test123"
}],
"totalPages": 5,
"totalElements": 81,
"size": 20,
"number": 0,
}
What do I need to do to make my response look like the automatically generated response? I want to keep it consistent, so I don't have to rewrite code for a different response. Should I be doing it a different way?
Edit: Found this: Enable HAL serialization in Spring Boot for custom controller method
But I don't understand what I need to change in my REST Controller to enable: PersistentEntityResourceAssembler. I've searched on Google for PersistentEntityResourceAssembler, but it keeps leading me back to similar pages without much of an example (or the example doesn't seem to work for me).

As #chrylis suggested you should replace your #Controller annotation with #RepositoryRestController for spring-data-rest to invoke it's ResourceProcessors for customizing the given resource.
For you resource to follow the HATEOAS specification (like your spring-data-rest BooksRepository) your method declaration return type should be like HttpEntity<PagedResources<Resource<Books>>>
For converting your Page object to PagedResources:
You need to autowire this object.
#Autowired
private PagedResourcesAssembler<Books> bookAssembler;
Your return statement should be like
return new ResponseEntity<>(bookAssembler.toResource(customBookService.findAll()), HttpStatus.OK);
These changes should help you to get a org.springframework.hateoas.Resources compliant response containing the "_embedded" and "_links" attribute.

Related

Reading JSON object with jackson

I would like to deserialize the following json object with jackson:
{
"_embedded": {
"endpoints": [
{
"name": "Tester",
"id": "48aba1b3-3585-4327-a20f-627a1749611b",
"componentId": "Darwin2",
"_links": {
"self": {
"href": "www.google.com"
},
"network": {
"href": "www.google.com"
},
"appWans": {
"href": "www.google.com"
},
"services": {
"href": "www.google.com"
},
"endpointGroups": {
"href": "www.google.com"
},
"geoRegion": {
"href": "www.google.com"
},
"dataCenter": {
"href": "www.google.com"
}
}
}
]
},
"_links": {
"first": {
"href": "www.google.com"
},
"last": {
"href": "www.google.com"
}
},
"page": {
"size": 2000,
"totalElements": 1,
"totalPages": 1,
"number": 1
}
}
My goal is to implement an Embedded object then within this object add another object called Endpoints. Ideally, I'd be able to access the id property off of the endpoints object. However, I keep getting deserialization errors. For the moment I am using this class:
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
#lombok.Value
public class Endpoints {
#JsonProperty("_embedded")
private Map<String, Object> embedded = new HashMap<>();
}
This at least affords me the opportunity to do the following:
Endpoints result = apiRequest.get();
if (result != null) System.out.println(result.getEmbedded().get("endpoints"));
Which prints out the array of endpoints, but I can't use this. I must implement a java object. Any help would be greatly appreciated with this issue.
So I have no idea what frameworks you use, or where this JSON data comes from, but you mentioned Spring HATEOS, so here is a solution using Jackson and Spring HATEOAS:
#Data
public class Endpoint extends RepresentationModel<Endpoint> {
private String name;
private String id; // Could be a UUID type instead??
private String componentId;
}
#Service
public class MyService {
#Autowired
private ObjectMapper objectMapper;
public void foo() {
String mysteryString = apiRequest.get();
PagedModel<Endpoint> endpointsPage = objectMapper.readValue(mysteryString, new TypeReference<PagedModel<Endpoint>>);
for (Endpoint e : endpointsPage) {
System.out.println(e.getName());
}
}
}
Spring HATEOAS docs and a guide. Also look at the Javadoc of the classes I've used.
If you want Endpoint to be a Lombok Value (i.e., all final), you need a constructor with appropriate ´#JsonCreator´ and ´#JsonProperty´ annotations, so Jackson knows how to build your Object (see 'guide' link).
And some more reading on Jackson.
Then again, if you are using Spring and you could just use Spring RestTemplate to fetch the Data from the remote API, you don't even need to manually use Jackson ObjectMapper:
PagedModel<Endpoint> endpointsPage =
restTemplate.exchange(apiUrl,
HttpMethod.GET,
null, // RequestEntity (body of request)
new ParameterizedTypeReference<PagedModel<Endpoint>>() {}).getBody();
The whole TypeReference and ParameterizedTypeReference business is only needed bc. we are dealing with generics.

HAL-based REST service client using Traverson

I am trying to write a Java program to consume the output from a REST-based web service I wrote utilizing the tutorials at spring.io. When I run SpringBoot bootRun the JSON output is built nicely with embedded data and links, and looks like this:
{
"_embedded": {
"invoiceList": [
{
"id": 4,
"seqNum": 1,
"fileId": null,
"fileName": null,
"invoiceNumber": "10080",
"invoiceDate": "2018-06-18T05:00:00.000+0000",
"invoiceTotal": 1000,
"sourceLastModified": "2018-11-30T16:22:23.000+0000",
"lastModified": "2018-11-30T16:22:23.000+0000",
"validFrom": "2018-11-30T16:22:23.000+0000",
"validTo": "9999-12-31T06:00:00.000+0000",
"_links": {
"self": {
"href": "http://localhost:8086/vadir/dental/invoices/4"
},
"invoices": {
"href": "http://localhost:8086/vadir/dental/invoices"
},
"claims": {
"href": "http://localhost:8086/vadir/dental/claims?invoice=10080"
}
}
},
{
"id": 5,
"seqNum": 1,
"fileId": null,
"fileName": null,
"invoiceNumber": "10080",
"invoiceDate": "2018-06-18T05:00:00.000+0000",
"invoiceTotal": 500,
"sourceLastModified": "2018-11-30T16:22:23.000+0000",
"lastModified": "2018-11-30T16:22:23.000+0000",
"validFrom": "2018-11-30T16:22:23.000+0000",
"validTo": "9999-12-31T06:00:00.000+0000",
"_links": {
"self": {
"href": "http://localhost:8086/vadir/dental/invoices/5"
},
"invoices": {
"href": "http://localhost:8086/vadir/dental/invoices"
},
"claims": {
"href": "http://localhost:8086/vadir/dental/claims?invoice=10080"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8086/vadir/dental/invoices/last"
}
}
}
I have found the documentation for Traverson and the SO question at Consuming HAL-based REST. However, when I use
ParameterizedTypeReference<Resources<Resource<DentalInvoice>>> parameterizedTypeReference =
new ParameterizedTypeReference<Resources<Resource<DentalInvoice>>> () {
};
Traverson traverson =
new Traverson (new URI ("http://localhost:8086/vadir/dental/invoices/last"), MediaTypes.HAL_JSON);
Resources<Resource<DentalInvoice>> invoiceResources = traverson
//.follow ((String) null)
.follow ("$._embedded.invoiceList")
.toObject (parameterizedTypeReference);
I get an error: Illegal character in scheme name at index 0: [{"id":4,...
It looks like Traverson is expecting the content of the follow reference to be a link rather than an embedded object. How can I get Traverson to parse the embedded object into a list of Resources of DentalInvoices?
Do I have to create a controller method that only outputs the links to the possible actions just so Traverson has a link to follow?

Spring embedded reading json+hal from JSON content-type

"_embedded": {
"employees": [
{
"id": "1",
"name": "Some Employee",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
}
}
},
{
"id": "2",
"name": "Some Employee",
"_links": {
"self": {
"href": "http://localhost:8080/employees/2"
}
}
},
{
"id": "3",
"name": "Some Employee",
"_links": {
"self": {
"href": "http://localhost:8080/employees/3"
}
}
}
]
},
{
"_links": {
"self": {
"href": "http://localhost:8080/employees{?page,size,sort}"
},
"search": {
"href": "http://localhost:8080/employees/search"
}
},
"page": {
"size": 20,
"totalElements": 3,
"totalPages": 1,
"number": 0
}
}
I'm reading a third-party Rest service that returns json content with the above structure or similar (content-type is application/json).
Currently, I created a custom json converter to a generic object that has custom objects for links,pages and a generic type for the embedded object. So that, I read it in some sort of generic way. However, I would like to do it via hateoas objects. I tried several approaches but any of them are working.
Following others questions I tried many things like configuring the RestTamplate
#Bean
public OAuth2RestTemplate getOAuth2Rest() {
[...]
OAuth2RestTemplate template = new OAuth2RestTemplate(resourceDetails, clientContext);
List<HttpMessageConverter<?>> existingConverters = template.getMessageConverters();
List<HttpMessageConverter<?>> newConverters = new ArrayList<>();
newConverters.add(getHalMessageConverter());
newConverters.addAll(existingConverters);
template.setMessageConverters(newConverters);
return template;
}
private HttpMessageConverter getHalMessageConverter() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new Jackson2HalModule());
MappingJackson2HttpMessageConverter halConverter =
new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class);
halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
halConverter.setObjectMapper(objectMapper);
return halConverter;
}
Then
ResponseEntity<Resources<MyDto>> entity = restTemplate.exchange(
myUri,
HttpMethod.GET, null,
new ParameterizedTypeReference<Resources<MyDto>>() {}, Collections.emptyMap());
There are similar questions like Consuming Spring Hateoas Pageable but they are consuming json+hal content. I think here is my problem because the deserializer uses the json converter and not the hal converter.
I tried
#EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
Didn't work and interferes with my REST configuration.
I tried this blog

Parsing _embedded items from JSON HAL response

We are trying to parse the below JSON to get a list of people.
JSON Response:
{
"_embedded": {
"people": [
{
"id": 35356,
"name": "Jon",
"description": "Test",
"type": "person",
"_links": {
"self": {
"href": "http://localhost/api/v1/50452/people/35356"
},
"items": {
"href": "http://localhost/api/v1/50452/items?person_id=35356"
},
"enabled_services": [
{
"title": "Water Company",
"href": "http://localhost/api/v1/50452/services/103890"
}
]
}
},
{
"id": 46363,
"name": "Kevin",
"description": "",
"type": "person",
"_links": {
"self": {
"href": "http://localhost/api/v1/50452/people/46363"
},
"items": {
"href": "http://localhost/api/v1/50452/items?person_id=46363"
},
"enabled_services": [
{
"title": "Water Company",
"href": "http://localhost/api/v1/50452/services/103890"
}
]
}
}
]
},
"_links": {
"self": {
"href": "http://localhost/api/v1/50452/people"
}
}
}
Our code:
ParameterizedTypeReference<Resources<Person>> resource = new ParameterizedTypeReference<Resources<Person>>() {};
Traverson traverson = new Traverson(new URI("http://localhost/api/v1/people"), MediaType.APPLICATION_JSON_UTF8);
// Create our own LinkDiscoverer as our service returns application/json instead of application/json+hal
List<LinkDiscoverer> linkDiscoverers = new ArrayList<>();
linkDiscoverers.add(new JsonPathLinkDiscoverer("$._links..['%s']..href", MediaType.APPLICATION_JSON_UTF8));
traverson.setLinkDiscoverers(linkDiscoverers);
HttpHeaders headers = new HttpHeaders();
headers.add("App-Key", Globals.Appkey);
headers.add("App-Id", Globals.AppId);
Resources<Person> personResources = traverson.follow("people").withHeaders(headers).toObject(resource);
However we are getting the following error:
java.lang.IllegalStateException: Expected to find link with rel 'people' in response {"_embedded":{"people":[{"id":31350,"name":"Jon Blue","description":"Developer","type":"person","deleted":false,"disabled":false,"company_id":50452,"order":31350,"phone_prefix":"44","_links":{"self":{"href":"http://localhost/api/v1/50452/people/31350"},"items":{"href":"http://localhost/api/v1/50452/items?person_id=31350"},"enabled_services":[{"title":"Water ...
Based on the (very limited) client docs this seems to be the correct way to do things. Does anyone know what we might be missing here?
Thanks
The Traverson is meant to be used to follow links:
Component to ease traversing hypermedia APIs by following links with
relation types. Highly inspired by the equally named JavaScript
library.
There is no link with rel people in your response, so it cannot be followed.
I could imagine you wanted to call the top-level resource that has a link to people:
Traverson traverson = new Traverson(new URI("http://localhost/api/v1/"), MediaType.APPLICATION_JSON_UTF8);
Otherwise I would suggest to use a RestTemplate to get the Resource:
restTemplate.exchange(
URI.create("http://localhost/api/v1/people"),
HttpMethod.GET,
new HttpEntity<Void>(headers),
resource).getBody();

Spring's MockMVC Responses Don't Match Browser Response

For some reason, my Spring controller is returning different responses if I access it via a browser or via my MockMVC test class. Could someone help me spot why?
First the controller method:
#RequestMapping(value = APPLICATIONS_ROOT, method = GET)
public HttpEntity<ApplicationsListResource> listApplications(#PageableDefault(page = DEFAULT_START,
size = DEFAULT_HITS_PER_PAGE) Pageable pageable) {
Page<Application> applications = applicationRepository.findAll(pageable);
ApplicationsListResource applicationListResource = new ApplicationsListResource(applications, pageable);
return new ResponseEntity<ApplicationsListResource>(applicationListResource, HttpStatus.OK);
}
Obviously there's a few unknown classes in there. ApplicationListResource extends ResourceSupport and contains a list of ApplicationResource called applications. This ApplicationResource also extends ResourceSupport.
When I access the code via the browser, I'll get something along the lines of:
{
"_links": {
"self": {
"href": "http://localhost:10000/applications{?page,size,sort}",
"templated": true
}
},
"_embedded": {
"applications": [{
"displayname": "One",
"description": "My Test Application!",
"locations": ["http://foo.com"],
"_links": {
"self": { "href": "http://localhost:10000/applications/one" }
}
}, {
...
}]
},
"page": {
"size": 20,
"totalElements": 7,
"totalPages": 1,
"number": 0
}
}
Looks HATEOAS compliant to me. But when I go via a MockMVC request...
getMockMvc().perform(get(APPLICATIONS_ROOT)).andExpect(status().isOk()).andExpect(content().contentType(MediaTypes.HAL_JSON)).andExpect(jsonPath("$._embedded.applcations", hasSize(5))).andReturn();
The responses have no HATEOAS compliant elements in them so my tests fail on the jsonPath check:
{
"page" : 0,
"size" : 10,
"sort" : null,
"total" : 5,
"applications" : [ {
"name" : "one",
"version" : "1.0",
...
I've tried changing the ContentType on the GET request for the MockMVC method but it makes no difference. In the browser, I'm not setting any specific content type, headers etc.
I know the MockMVC class makes it HTTP requests with certain differences from the usual RestTemplate so perhaps it's something like this? Can anyone see anything obvious I am missing?
I will add additional code if needs be but it would have made the question even more long winded than it is currently.
Spring HATEOAS adds additional configuration for rendering hal properly, check this for details: http://docs.spring.io/spring-hateoas/docs/0.19.0.RELEASE/reference/html/#configuration.
In a nutshell it adds proper MixIns added by Jackson2HalModule and HalHandlerInstantiator to the ObjectMapper. It's all configured in HypermediaSupportBeanDefinitionRegistrar.java (https://github.com/spring-projects/spring-hateoas/blob/master/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java)
If you're using standalone mockMvc configuration you have to configure the ObjectMapper manually to mimic spring's behaviour. I ran into same problem and ended up adding following configuration to my tests:
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setMessageConverters(
new MappingJackson2HttpMessageConverter(configureObjectMapper()))
.build();
and
private ObjectMapper configureObjectMapper() {
return Jackson2ObjectMapperBuilder.json()
.modules(new Jackson2HalModule())
.handlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(
new DelegatingRelProvider(
OrderAwarePluginRegistry.create(Arrays.asList(
new EvoInflectorRelProvider(),
new AnnotationRelProvider()))),
null))
.build();
}

Categories