How to change Hateoas output format in spring application? - spring

I am currently working on a spring application that offers a REST interface with which CRUD operations on entities of various kinds can be performed. These entities are stored in repositories and thus a major part of the REST interface is automatically generated by spring. When I execute a GET request on such an entity type (e.g. /devices), the result looks as following:
{
"_embedded":{
"devices":[
{
"macAddress": "...",
"ipAddress": "...",
"name": "Device_1",
"id":"5c866db2f8ea1203bc3518e8",
"_links":{
"self":{
...
},
"device":{
...
}
}, ...
]
},
"_links":{
...
},
"page":{
"size":20,
"totalElements":11,
"totalPages":1,
"number":0
}
}
Now I need to implement a similar interface manually, because additional checks are required. I have made use of the spring-hateoas features for this purpose. However, I am unable to achieve the same output structure like the one automatically generated by spring. The corresponding code in my controller class (annotated with RestController) looks as follows:
#GetMapping("/devices")
public Resources<Device> getDevices() {
List<Device> deviceList = getDeviceListFromRepository();
Link selfRelLink = ControllerLinkBuilder.linkTo(
ControllerLinkBuilder.methodOn(RestDeviceController.class)
.getDevices())
.withSelfRel();
Resources<Device> resources = new Resources<>(deviceList);
resources.add(selfRelLink);
return resources;
}
The configuration (excerpt) looks as follows:
#Configuration
#EnableWebMvc
#EnableSpringDataWebSupport
#EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL)
public class WebServletConfiguration extends WebMvcConfigurerAdapter implements ApplicationContextAware {
...
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer c) {
c.defaultContentType(MediaTypes.HAL_JSON);
}
...
}
However, this is the output of a request:
{
"links":[
{
"rel":"self",
"href":"..."
}
],
"content":[
{
"id":"5c866db2f8ea1203bc3518e8",
"name":"Device_1",
"macAddress": "...",
"ipAddress":"...",
}
]
}
As you can see, instead of an _embedded key there is a content key and the links key misses the leading underscore. These are the main issues I have with this output, more detailed differences compared to the output above are not that important to me. I would like to unify the ouput generated by my application, but I am unable to achieve the output format of the mappings that are automatically generated by spring. I also tried to wrap the resources object into another resource object (like return new Resource<...>(resources)), this did not work as well though.
Do you have any hints for me about what I am doing wrong here? I am quite new to Spring & Co, so please tell me if you need more information about a certain thing. Any help is highly appreciated. Thanks in advance!

Finally I was able to find a solution: The strange output format as showed in the question was generated due to the accept header application/json that was sent by the client. After adding
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.ignoreAcceptHeader(true);
configurer.defaultContentType(MediaTypes.HAL_JSON);
}
to class WebServletConfiguration which extends WebMvcConfigurerAdapter everything works as exptected and the output format is now HAL-like. A quite easy fix, but it took me weeks to figure this out. Maybe this answer will help somebody else in the future.

Related

Why does Spring Boot ignore my CustomErrorController?

I have a custom ErrorController like this:
#Controller
public class CustomErrorController implements ErrorController {
#RequestMapping("/error42")
public String handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
System.err.println(status);
if (Objects.isNull(status)) return "error";
int statusCode = Integer.parseInt(status.toString());
String view = switch (statusCode) {
case 403 -> "errors/403";
case 404 -> "errors/404";
case 500 -> "errors/500";
default -> "error";
};
return view;
}
}
And then I've set the server.error.path property like this:
server.error.path=/error42
So far, so good. Everything works fine. All the errors go through my CustomErrorController.
But when I set the error path to server.error.path=/error - and of course I change the request mapping annotation to #RequestMapping("/error") - this won't work anymore.
Spring Boot now completely ignores my CustomErrorController. I know, I've set the path to the one Spring Boot usually defines as standard, but is there no way to override this?
Many thanks for any information clearing up this weird behavior.
I found the error, and it was solely my own fault. Since especially at the beginning of a Spring Boot career, the setting options quickly become overhelming, and one can lose sight of one or the other adjustment made, I would still like to leave this question and answer it myself.
The culprit was a self-configured view that i did weeks ago and completely lost track of:
#Configuration
#EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
/* FYI: will map URIs to views without the need of a Controller */
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login")
.setViewName("/login");
registry.addViewController("/error") // <--- Take this out !!!
.setViewName("/error");
registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
}
}
May this help others facing the same mystery, why once again nothing runs quite as desired...

Use CRNK without repository

We've standardized on using JSON:API for our REST endpoints, however; not all of our data revolves around repositories and it seems that CRNK requires repositories in order to work.
Is that correct?
Example
I wrote a very simple Spring Boot 2.1.9 example that has a single controller and included CRNK in it, but when I get into my controller I do not get the expected JSON:API output.
Please keep in mind, I am just starting to look at CRNK and this is just a simple "hello world" type of application that I am testing with
Here is my example
package com.example.crnkdemo;
import org.springframework.web.bind.annotation.*;
#RestController
#RequestMapping("/test/v1.0")
public class Controller {
#GetMapping(value = "/{country}", produces = "application/vnd.api+json")
public Country test1(#PathVariable String country, #RequestParam(name = "filter[region]", required = false) String filter) {
return new Country(country, filter);
}
}
Country is just a dummy class I created which is:
package com.example.crnkdemo;
import io.crnk.core.resource.annotations.JsonApiId;
import io.crnk.core.resource.annotations.JsonApiResource;
#JsonApiResource(type = "country")
#AllArgsConstructor
#Data
public class Country {
#JsonApiId
private String country;
private String region;
Results
But when I use the following URL http://localhost:8080/test/v1.0/US?filter[region]=northeast I get
{
"country": "US",
"region":"northeast"
}
I would have expected the JSON API type of result
{
"data": {
"type": "country",
"id": "US",
"attributes": {
"region": "northeast"
}
}
Thanks!
I ran into similar issue and the problem was that I got io.crnk:crnk-format-plain-json in my dependencies (simply copied from an example app) which changes the way how the responses look like (non-JSON-API). So first have a look into your maven/gradle configuration.
"not all of our data revolves around repositories"
you may also have a look at http://www.crnk.io/releases/stable/documentation/#_architecture where the architecture of resource-oriented framework like Crnk and JSON:API are discussed in more detail. In principle one can model everything as repository. Applications usually follow two kinds of patterns: CRUD ones and "actions". CRUD is simple: GET, POST, PATCH, DELETE objects. A repository is a perfect match for that. In contrast, people have a harder time when it comes to "actions". But this can be modelled as well as CRUD. For example, POSTing an AddressChange resource may trigger a server to start modifying the address(es) of some objects. This may happend immediately or take a longer time. Subsequent GET requests for the POSTed resources will reveal the current status of the action. And a DELETE request can cancel the request.
Crnk itself is not in need for Controllers as Spring MVC is. This kind of "lower-level plumbing" is taken care by Crnk itself because JSON:API specifies how a REST layer is supposed to look like. So there is no need to write custom code to specify urls patterns, parameters, etc. as in the MVC example above. Instead one can implement a repository:
public class TaskRepositoryImpl extends ResourceRepositoryBase<Task, Long> {
private ConcurrentHashMap<Long, Task> tasks = new Concurrent...
public TaskRepositoryImpl() {
super(Task.class);
}
#Override
public <S extends Task> S create(S entity) {
map.put(entity.getId(), entity);
return entity;
}
#Override
public ResourceList<Task> findAll(QuerySpec querySpec) {
return querySpec.apply(tasks.values());
}
...
}
There are also many built-in defult repository implementatons like for in-memory, JPA, security to cover the most frequent use cases.
with crnk, no need of writing controllers, manager classes. By default the controllers are defined.
Once we define the resources, we can access it by http://server_name:portno/crnk-path-prefix-property/defined_resourcename & the method type
Eg. In our case, resource is country, let's say server is running in localhost:8081 and crnk-path-prefix is /api/v1, then the url is http://localhost:8081/api/v1/country & set method type is GET, it will give the desired output. Remember to set content-type as application/vnd.api+json.
For POST, same url and set method type as POST, pass the data object
For PATCH, same url along with id attribute appended to the url and set method type as PATCH & pass the data object

How to get #TypeAlias working when reading document after upgrading to Spring Boot 2.1.3 when it was working in 1.4.5

I am currently using spring data mongodb and the Configuration file extends AbstractMongoConfiguration:
#Configuration
#EnableMongoRepositories(basePackages = "com.mycompany")
#EnableMongoAuditing
public class MongoConfig extends AbstractMongoConfiguration
{
I override the getMappingBasePackage() method to set the package to scan like this:
#Override
protected String getMappingBasePackage()
{
return "com.mycompany";
}
I have been debugging through the code and noticed some interesting things:
There are two place where I get a java.lang.InstantiationError. Both cases occur when I am trying to read in document from mongo that has a reference to an abstract class (ParentClass). It is trying to instantiate the abstract class instead of finding the #TypeAlias annotation which I have added to the child classes.
This is what my ParentClass looks like:
#Document
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.EXISTING_PROPERTY, visible=true, property="type")
#JsonSubTypes({
#Type(value=Child1.class, name="JSON_TYPE_CHILD1"),
#Type(value=Child2.class, name="JSON_TYPE_CHILD2"),
#Type(value=Child3.class, name="JSON_TYPE_CHILD3")
})
public abstract class ParentClass
{
...
My child classes look like this:
#Document
#JsonTypeName("JSON_TYPE_CHILD1")
#TypeAlias("ALIAS_TYPE_CHILD1")
public class Child1 extends ParentClass
{
...
This is what the json looks like (simplified) that I am trying to read in:
{
"_id" : ObjectId("5c86d31388f13344f4098c64"),
"listOfWrapperClass" : [
{
"parentClass" : {
"type" : "JSON_TYPE_CHILD1",
"prop1" : 50.0,
"prop2" : 50.0,
"_class" : "ALIAS_TYPE_CHILD1"
},
"isReportOutOfDate" : false,
}
],
"_class" : "com.mycompany.domain.job.Job"
}
When I debug through spring data the problem occurs in DefaultTypeMapper:
private TypeInformation<?> getFromCacheOrCreate(Alias alias) {
Optional<TypeInformation<?>> typeInformation = typeCache.get(alias);
if (typeInformation == null) {
typeInformation = typeCache.computeIfAbsent(alias, getAlias);
}
return typeInformation.orElse(null);
}
It load the wrapper class fine, but when it gets to child class the alias is set to "ALIAS_TYPE_CHILD1" as it should but the following values are in the typeCache:
{
NONE=Optional.empty,
ALIAS_TYPE_CHILD1=Optional.empty,
com.mycompany.domain.job.Job=Optional[com.mycompany.domain.job.Job]
}
Because the key "ALIAS_TYPE_CHILD1" has an Optional.empty as a value the code doesn't get the correct target type to load and it therefore uses the rawType which is the ParentClass. Which blows up because it can't instantiate an abstract class. Here is the stacktrace:
Caused by: java.lang.InstantiationError: com.mycompany.domain.job.base.ParentClass
at com.mycompany.domain.job.base.ParentClass_Instantiator_q3kytg.newInstance(Unknown Source)
at org.springframework.data.convert.ClassGeneratingEntityInstantiator$EntityInstantiatorAdapter.createInstance(ClassGeneratingEntityInstantiator.java:226)
at org.springframework.data.convert.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:84)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:272)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:245)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readValue(MappingMongoConverter.java:1491)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$MongoDbPropertyValueProvider.getPropertyValue(MappingMongoConverter.java:1389)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readProperties(MappingMongoConverter.java:378)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.populateProperties(MappingMongoConverter.java:295)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:275)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:245)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readCollectionOrArray(MappingMongoConverter.java:1038)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readValue(MappingMongoConverter.java:1489)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter$MongoDbPropertyValueProvider.getPropertyValue(MappingMongoConverter.java:1389)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.readProperties(MappingMongoConverter.java:378)
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.populateProperties(MappingMongoConverter.java:295)
at ...
The weird thing is if I insert a new document that has #TypeAlias("ALIAS_TYPE_CHILD1") first, the typeCache mentioned above is populated correctly like so:
{
NONE=Optional.empty,
ALIAS_TYPE_CHILD1=Optional[com.mycompany.domain.job.base.Child1],
com.mycompany.domain.job.Job=Optional[com.mycompany.domain.job.Job]
}
When I do findOne right after the insert, I can read in the document without error because it uses Child1 to instantiate the pojo instead of the ParentClass. If I try to read first then it doesn't matter if I insert after that or not because the typeCace gets the wrong value in there and it uses that until you restart the server.
My guess is there was a change in configuration or in a default setting. I was able to work through all the other upgrade issues, but this one has me baffled. I would be shocked if there is an actual issue in spring data because I am sure somebody would have run into this issue by now because I can't be the only one trying to use #TypeAlias with spring-data-mongodb. Not to mention this all works great with the previous version of spring boot that I used (1.4.5 which uses spring-data-mongodb 1.9.8.RELEASE).
Any thoughts or advice on what to try next are welcome. I am simply at a loss of what to do next.
The problem was the fact that the typeCache wasn't getting populated in the first place on server startup. This is because protected String getMappingBasePackage() is now deprecated. You should use protected Collection<String> getMappingBasePackages() instead and then everything works great.
Overriding this method solves the issue:
#Override
protected Collection<String> getMappingBasePackages()
{
return Arrays.asList("com.mycompany");
}

Add additional info in authetication reply

Working on a REST API application using Grails 3.3.5 and Spring-security-rest 2.0.0-RC1.
When I log in using /api/login I got correctly this as result:
{
"username": "name.surname#acme.com",
"roles": [
"ROLE_USER"
],
"access_token": "qgsbpk05jfrbf33xx08m8r4govkg53d1"
}
I'd like to add other information in the response, like name and surname of the user. Thanks
UPDATE
Thanks to Evgeny that points me in the right direction and reading the manual I implement also a UserDetailsService.
From the manual:
If you want to render additional information in your JSON response,
you have to:
Configure an alternative userDetailsService bean that retrieves the additional information you want, and put it in a principal object.
Configure an alternative accessTokenJsonRenderer that reads that information from the restAuthenticationToken.principal object.
You can override accessTokenJsonRenderer bean
Create class:
class MyRestAuthTokenJsonRenderer implements AccessTokenJsonRenderer {
#Override
String generateJson(AccessToken accessToken){
// create response, see DefaultAccessTokenJsonRenderer.groovy from https://github.com/alvarosanchez/grails-spring-security-rest
return your_formatted_json_response
}
}
override bean in resources.groovy
beans = {
accessTokenJsonRenderer(MyRestAuthTokenJsonRenderer)
}

Spring Data Rest Secure HATEOAS link

Lets say I have an User entity with a ManyToMany mapping to UserGroup entity. If I create repositories for both entities and GET the URI /users/1, I get a response like this:
{
"enabled" : true,
"password" : "xxxxxx",
"username" : "xxxxxx",
"credentialsNonExpired" : true,
"accountNonLocked" : true,
"accountNonExpired" : true,
"_links" : {
"self" : {
"href" : "http://127.0.0.1:45950/users/1"
},
"user" : {
"href" : "http://127.0.0.1:45950/users/1"
},
"userGroups" : {
"href" : "http://127.0.0.1:45950/users/1/userGroups"
}
}
}
The userGroups link here is really useful.
I can list all UserGroups using the /userGroups endpoint.
I would like to protect the /userGroups endpoint and /users/1/userGroups endpoint using different spring-security expressions.
Using the reference here: http://docs.spring.io/spring-data/rest/docs/current/reference/html/#security I understand how to secure the first endpoint:
public interface UserGroupRepository extends PagingAndSortingRepository<UserGroup, Long> {
#PreAuthorize("hasRole('ROLE_ADMIN')")
#Override
Iterable<T> findAll();
}
But how do I secure the second endpoint? Is that even possible currently? Is there some work planned on such a feature. I would love to contribute.
I have also encountered this problem and not found any solution working with Spring's security annotations. As a workaround, I have added something along the line of:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
// Whatever config you already have.
.authorizeRequests()
.antMatchers("/users/*/userGroups").hasRole("ADMIN");
}
to my implementation of WebSecurityConfigurerAdapter. While this works, it duplicates the work done when securing the repositories and it is easy to forget adding both the java config and annotations when adding new repositories.
I realize this question is four years old, but this issue has continued to plague me, and I finally found something that works.
The problem essentially comes down to the fact that Spring HATEOAS has no real security controls; while you can configure the repositories with the appropriate method-level security annotations to prevent access via the REST API, you can't stop the HATEAOS representation model assembler from slurping up any objects it can see.
The solution I found was to expose a RepresentationModelProcessor bean for EntityModel<DomainObject>s. In your case, this may look something like
#Configuration
public class SecureHateoasConfig {
public static class UserGroupProcessor implements RepresentationModelProcessor<EntityModel<UserGroup>> {
#Override
public EntityModel<UserGroup> process(EntityModel<UserGroup> model) {
if(SecurityContextHolder.getContext()
.getAuthentication()
.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.anyMatch("ROLE_ADMIN"::equals)) {
return model;
} else {
return null;
}
}
}
#Bean
public UserGroupProcessor userGroupProcessor() {
return new UserGroupProcessor();
}
}
Objects replaced by null seem to get filtered out after the normal processing, so embedded entities simply don't include the objects which don't match your filter!
There's probably a much easier way to filter links if you're only doing role-based security, but I'm afraid I don't know it...
Of course, it's possible to extend this considerably, since your secure object processor need not only apply to EntityModel<UserGroup>! In my case I have an interface all my secure domain objects implement which can be used to make security decisions, so only one RepresentationModelProcessor implementation/bean is required.

Resources