Spring-MVC: find mapping details by its name - java

Spring-MVC's #RequestMapping annotation has parameter "name" which can be used for identifying each resource.
For some circumstances I need access this information on fly: retrieve mapping details (e.g. path) by given name.
Sure I can scan for classes for this annotation and retrieve needed instances via:
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(RequestMapping.class));
// ... find classes ... go through its methods ...
But it is quite ugly. Is any more simple solution?

You can use RequestMappingHandlerMapping to get all your mappings and filter them based on name. Following is a code snippet which creates a rest api and returns the path details of a api/mapping.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
#RestController
public class EndpointController {
#Autowired
private RequestMappingHandlerMapping handlerMapping;
#GetMapping("endpoints/{name}")
public String show(#PathVariable("name") String name) {
String output = name + "Not Found";
Map<RequestMappingInfo, HandlerMethod> methods = this.handlerMapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : methods.entrySet()) {
if (entry.getKey().getName() != null && entry.getKey().getName().equals(name)) {
output = entry.getKey().getName() + " : " + entry.getKey();
break;
}
}
return output;
}
}
The above is just an example, You can use the RequestMappingHandlerMapping anyway you want until you can autowire it.

Related

How do I create a nested JSON using Jackson?

I am trying to add the BusinessInformation data in my existing JSON.
{
"Original": "Web Form",
"SubmitterNetworkName": "null",
"SourceName": "Contact Request Form",
"SourceKind": "Web Form",
** "BusinessInformation": {
"BusinessContactName": null,
“AccountNumber”:null,
},**
"EmployeeName": null,
"EmployeeDOB": null,
}
So I have a Spring Batch app that exports data from a database to another DB and during that run some JSON needs to be created mapping multiple columns. I am using lombok and jackson mainly for the creation of JSON.
The model sample that I have. I also tried creating BusinessInformation class.
package model
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
#Data
#AllArgsConstructor
#NoArgsConstructor
#Builder
#JsonIgnoreProperties(ignoreUnknown = true)
public class ColumnNameForTheDBImImportingTo implements Serializable {
#JsonProperty("Origin")
private String Origin;
and so on..
}
And then I have the Service for it.
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import twc.batch.extcbk.model.*;
import java.io.IOException;
#Component
#Slf4j
public class ColumnNameForTheDBImImportingToService {
private ColumnNameForTheDBImImportingTo columnNameForTheDBImImportingTo (some codes) {
ColumnNameForTheDBImImportingTo columnNameForTheDBImImportingTo = ColumnNameForTheDBImImportingTo .builder().build();
columnNameForTheDBImImportingTo.setOrigin("Web Form");
}
**Then I have a method for the object mapper.**
private String getColumnNameForTheDBImImportingTo (ColumnNameForTheDBImImportingTo columnNameForTheDBImImportingTo ) {
String columnNameForTheDBImImportingToStr = null;
try {
ObjectMapper mapper = new ObjectMapper();
columnNameForTheDBImImportingToStr = mapper.writeValueAsString(columnNameForTheDBImImportingTo );
log.debug("columnNameForTheDBImImportingToStr {}", columnNameForTheDBImImportingToStr );
} catch (IOException e) {
log.error("getColumnNameForTheDBImImportingTo " + e.getMessage());
}
return columnNameForTheDBImImportingToStr ;
}
**Then another class builds the db columns and sets the values.**
I've tried following the Baeldung but I don't quite understand it.
I tried creating a new class for the business information and i was thinking of inserting it in the ColumnNameForTheDBImImportingTo.
Please refer me to any useful information I can follow for this problem.
Thanks!
Data transformation is a typical use case for an item processor.
The way I would implement this with Spring Batch is configure a chunk-oriented step defined as follows:
A JsonItemReader to read the input file
An item processor that adds the business information
A JsonItemWriter to write the output file

How to get query parameter name from ConstraintViolationException

I have a service method:
#GetMapping(path = "/api/some/path", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<?> getWhatever(#RequestParam(value = "page-number", defaultValue = "0") #Min(0) Integer pageNumber, ...
If the caller of an API doesn't submit a proper value for page-number query parameter, javax.ConstraintViolationexception is being raised. The message of the exception would read smth like:
getWhatever.pageNumber must be equal or greater than 0
In the response body, I would like to have this message instead:
page-number must be equal or greater than 0
I want my message to have the name of a query parameter, not the name of the argument. IMHO, including the name of the argument is exposing the implementation details.
The problem is, I cannot find an object that is carrying query parameter name. Seems like the ConstraintViolationException doesn't have it.
I am running my app in spring-boot.
Any help would be appreciated.
P.S.: I have been to the other similar threads that claim to solve the problem, none of them actually do in reality.
Here is how I made it work in spring-boot 2.0.3:
I had to override and disable ValidationAutoConfiguration in spring-boot:
import org.springframework.boot.validation.MessageInterpolatorFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
import javax.validation.Validator;
#Configuration
public class ValidationConfiguration {
public ValidationConfiguration() {
}
#Bean
public static LocalValidatorFactoryBean validator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
factoryBean.setParameterNameDiscoverer(new CustomParamNamesDiscoverer());
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
#Bean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, #Lazy Validator validator) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
boolean proxyTargetClass = (Boolean) environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
processor.setValidator(validator);
return processor;
}
}
CustomParamNamesDiscoverer sits in the same package and it is a pretty much a copy-paste of DefaultParameterNameDiscoverer, spring-boot's default implementation of param name discoverer:
import org.springframework.core.*;
import org.springframework.util.ClassUtils;
public class CustomParameterNameDiscoverer extends PrioritizedParameterNameDiscoverer {
private static final boolean kotlinPresent = ClassUtils.isPresent("kotlin.Unit", CustomParameterNameDiscoverer.class.getClassLoader());
public CustomParameterNameDiscoverer() {
if (kotlinPresent) {
this.addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
}
this.addDiscoverer(new ReqParamNamesDiscoverer());
this.addDiscoverer(new StandardReflectionParameterNameDiscoverer());
this.addDiscoverer(new LocalVariableTableParameterNameDiscoverer());
}
}
I wanted it to remain pretty much intact (you can see even kotlin checks in there) with the only addition:
I am adding an instance of ReqParamNamesDiscoverer to the linked lists of discoverers. Note that the order of addition does matter here.
Here is the source code:
import com.google.common.base.Strings;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.RequestParam;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
public class ReqParamNamesDiscoverer implements ParameterNameDiscoverer {
public ReqParamNamesDiscoverer() {
}
#Override
#Nullable
public String[] getParameterNames(Method method) {
return doGetParameterNames(method);
}
#Override
#Nullable
public String[] getParameterNames(Constructor<?> constructor) {
return doGetParameterNames(constructor);
}
#Nullable
private static String[] doGetParameterNames(Executable executable) {
Parameter[] parameters = executable.getParameters();
String[] parameterNames = new String[parameters.length];
for (int i = 0; i < parameters.length; ++i) {
Parameter param = parameters[i];
if (!param.isNamePresent()) {
return null;
}
String paramName = param.getName();
if (param.isAnnotationPresent(RequestParam.class)) {
RequestParam requestParamAnnotation = param.getAnnotation(RequestParam.class);
if (!Strings.isNullOrEmpty(requestParamAnnotation.value())) {
paramName = requestParamAnnotation.value();
}
}
parameterNames[i] = paramName;
}
return parameterNames;
}
}
If parameter is annotated with RequestParam annotation, I am retrieving the value attribute and return it as a parameter name.
The next thing was disabling auto validation config, somehow, it doesn't work without it. This annotation does the trick though:
#SpringBootApplication(exclude = {ValidationAutoConfiguration.class})
Also, you need to have a custom handler for your ConstraintValidationException :
#ResponseStatus(HttpStatus.BAD_REQUEST)
#ExceptionHandler(ConstraintViolationException.class)
public ErrorDTO handleConstraintViolationException(ConstraintViolationException ex) {
Map<String, Collection<String>> errors = new LinkedHashMap<>();
ex.getConstraintViolations().forEach(constraintViolation -> {
String queryParamPath = constraintViolation.getPropertyPath().toString();
log.debug("queryParamPath = {}", queryParamPath);
String queryParam = queryParamPath.contains(".") ?
queryParamPath.substring(queryParamPath.indexOf(".") + 1) :
queryParamPath;
String errorMessage = constraintViolation.getMessage();
Collection<String> perQueryParamErrors = errors.getOrDefault(queryParam, new ArrayList<>());
perQueryParamErrors.add(errorMessage);
errors.put(queryParam, perQueryParamErrors);
});
return validationException(new ValidationException("queryParameter", errors));
}
ValidationException stuff is my custom way of dealing with validation errors, in a nutshell, it produces an error DTO, which will be serialized into JSON with all the validation error messages.
Add a custom message to #Min annotation like this
#Min(value=0, message="page-number must be equal or greater than {value}")
Right now, you cannot do it (well, except if you define a custom message for each annotation but I suppose that's not what you want).
Funnily enough, someone worked recently on something very similar: https://github.com/hibernate/hibernate-validator/pull/1029 .
This work has been merged to the master branch but I haven't released a new 6.1 alpha containing this work yet. It's a matter of days.
That being said, we had properties in mind and now that you ask that, we should probably generalize that to more things, method parameters included.
Now that we have the general idea, it shouldn't be too much work to generalize it, I think.
I'll discuss this with the contributor and the rest of the team and get back to you.
I don't think getting the name of the query parameter is possible but would like to be proven wrong if somebody knows a way.
As Dmitry Bogdanovich says, having a custom message is the easiest and only way I know how to do something close to what you need. If you say you don't want to clutter your code with these messages, you can just do this:
Add a ValidationMessages.properties file in your resources folder. Here you can just say:
page_number.min=page-number must be equal or greater than {value}
Now you can use the annotation and write:
#Min(value = 0, message = "{page_number.min}")
This way you have a single source to change anything about the message when needed.

Spring Boot REST Controller issues

I am having a very strange issue with a Rest Controller. I have a very basic rest controller.
package com.therealdanvega.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.therealdanvega.domain.Post;
import com.therealdanvega.service.PostService;
#RestController
public class PostController {
private PostService postService;
#Autowired
public PostController(PostService postService){
this.postService = postService;
}
#RequestMapping("posts/test")
public String test(){
return "test...";
}
#RequestMapping( name="/posts/", method=RequestMethod.GET )
public Iterable<Post> list(){
return postService.list();
}
}
That calls a service
package com.therealdanvega.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.therealdanvega.domain.Post;
import com.therealdanvega.repository.PostRepository;
#Service
public class PostService {
private PostRepository postRepository;
#Autowired
public PostService(PostRepository postRepository){
this.postRepository = postRepository;
}
public Iterable<Post> list(){
return postRepository.findAll();
}
}
That calls a repository to fetch the data.
package com.therealdanvega.repository;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.therealdanvega.domain.Post;
#Repository
public interface PostRepository extends CrudRepository<Post, Long> {
Post findFirstByOrderByPostedOnDesc();
List<Post> findAllByOrderByPostedOnDesc();
Post findBySlug(String slug);
}
I am using an H2 in memory database and I only have a single Post record in there and can confirm so by going to the H2 console and running a select again the Post table.
If I visit the /test URL I get exactly what I am expecting which is the string "test..." printed to the browser. If I try and list all of the posts (which again is only 1) the browser starts looping over and over and continue to print out a JSON representing of the 1 post so many times that the application crashes and I see this in the console
2015-11-07 17:58:42.959 ERROR 5546 --- [nio-8080-exec-1]
o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for
servlet dispatcherServlet threw exception
java.lang.IllegalStateException: getOutputStream() has already been
called for this response
This is what my browser looks like when I visit /posts which should only list 1
Post Domain Class
package com.therealdanvega.domain;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.springframework.data.annotation.CreatedDate;
#Entity
public class Post {
#Id #GeneratedValue
private Long id;
private String title;
#Column(columnDefinition = "TEXT")
private String body;
#Column(columnDefinition = "TEXT")
private String teaser;
private String slug;
#CreatedDate
#Temporal(TemporalType.TIMESTAMP)
private Date postedOn;
#ManyToOne
private Author author;
#SuppressWarnings("unused")
private Post(){
}
public Post(String title){
this.setTitle(title);
}
// getters & setters
}
Does anyone know what I am doing wrong or missing here? Why isn't it just display the 1 record in JSON format?
It seems that your Post object has a circular reference. The Author object in your Post object has a list of Posts objects and so on. Try putting the #JsonIgnore annotation on the author attribute of your post object.
You can also use the #JsonBackReference and #JsonManagedReference to solve the problem.
From the Jackson documentation :
Object references, identity
#JsonManagedReference, #JsonBackReference: pair of annotations used to
indicate and handle parent/child relationships expressed with pair of
matching properties #JsonIdentityInfo: class/property annotation used
to indicate that Object Identity is to be used when
serializing/deserializing values, such that multiple references to a
single Java Object can be properly deserialized. This can be used to
properly deal with cyclic object graphs and directed-acyclic graphs.
I believe your Posts domain object contains Author domain object, that in turn in it's posts field contains all the posts by that author, which in turn contains author that contains posts... you see where I'm going with this.
It's probably best that you use fetch or load graphs to optimize your query's fetch strategy.

How to post a list to Spring Data Rest?

I followed this example, which allows to post a unique Person object. I want a REST service where I can post a collection of Person at once, e.g. a list/any collection named Team with numerous Person objects in just one call.
I mean, my question is not exactly about the OneToMany relationship, where you send each person in a REST request. This topic is well answered.
I want to send a collection of Person objects taking advantage of #RepositoryRestResource or another feature from Spring Data Rest. Is this possible with Spring Data Rest or should I workaround by creating a controller, receive the list and parse the Team list to insert each Person?
I found this feature request, which seems to answer that nowadays Spring Rest Data is missing what I am looking for, but I am not sure.
In my business requirement, application A will post a list of orders to application B and I have to save it in database for future processing, so, after reading about Spring Data Rest and making some samples, I found its clean architecture amazing and very suitable for my requirement except for the fact that I didn't figure out how to post a list.
Well, AFAIK you can't do that with spring data rest, just read the docs and you will see, that there is no mention about posting a list to collection resource.
The reason for this is unclear to me, but for one thing - the REST itself doesn't really specify how you should do batch operations.
So it's unclear how one should approach that feature, like should you POST a list to collection resource? Or should you export resource like /someentity/batch that would be able to patch, remove and add entities in one batch? If you will add list how should you return ids? For single POST to collection spring-data-rest return id in Location header. For batch add this cannot be done.
That doesn't justify that spring-data-rest is missing batch operations. They should implement this IMHO, but at least it can help to understand why are they missing it maybe.
What I can say though is that you can always add your own Controller to the project that would handle /someentity/batch properly and you can even probably make a library out of that, so that you can use it in another projects. Or even fork spring-data-rest and add this feature. Although I tried to understand how it works and failed so far.
But you probably know all that, right?
There is a feature request for this.
Based on user1685095 answer, You can make custom Controller PersonRestController and expose post collection of Person as it seem not exposed yet by Spring-date-rest
#RepositoryRestController
#RequestMapping(value = "/persons")
public class PersonRestController {
private final PersonRepository repo;
#Autowired
public AppointmentRestController(PersonRepository repo) {
this.repo = repo;
}
#RequestMapping(method = RequestMethod.POST, value = "/batch", consumes = "application/json", produces = "application/json")
public #ResponseBody ResponseEntity<?> savePersonList(#RequestBody Resource<PersonWrapper<Person>> personWrapper,
PersistentEntityResourceAssembler assembler) {
Resources<Person> resources = new Resources<Person>(repo.save(personWrapper.getContent()));
//TODO add extra links `assembler`
return ResponseEntity.ok(resources);
}
}
PersonWrapper to fix:
Can not deserialize instance of org.springframework.hateoas.Resources out of START_ARRAY token\n at [Source: java.io.PushbackInputStream#3298b722; line: 1, column: 1]
Update
public class PersonWrapper{
private List<Person> content;
public List<Person> getContent(){
return content;
}
public void setContent(List<Person> content){
this.content = content;
}
}
public class Person{
private String name;
private String email;
// Other fields
// GETTER & SETTER
}
I tried to use #RequestBody List<Resource<MyPojo>>.
When the request body does not contain any links, it works well, but
if the element carries a link, the server could not deserialize the request body.
Then I tried to use #RequestBody Resources<MyPojo>, but I could not figure out the default name of a list.
Finally, I tried a wrapper which contained List<Resource<MyPojo>>, and it works.
Here is my solution:
First create a wrapper class for List<Resource<MyPojo>>:
public class Bulk<T> {
private List<Resource<T>> bulk;
// getter and setter
}
Then use #RequestBody Resource<Bulk<MyPojo>> for parameters.
Finally, example json with links for create bulk data in one request:
{
"bulk": [
{
"title": "Spring in Action",
"author": "http://localhost:8080/authors/1"
},
{
"title": "Spring Quick Start",
"author": "http://localhost:8080/authors/2"
}
]
}
#RequestMapping(method=RequestMethod.POST, value="/batchInsert", consumes = "application/json", produces = "application/json")
#ResponseBody
public ResponseEntity<?> batchInsert(#RequestBody Resources<Person> people, PersistentEntityResourceAssembler assembler) throws Exception {
Iterable<Person> s = repo.save( people.getContent() ); // save entities
List<PersistentEntityResource> list = new ArrayList<PersistentEntityResource>();
Iterator<Sample> itr = s.iterator();
while(itr.hasNext()) {
list.add( assembler.toFullResource( itr.next() ) );
}
return ResponseEntity.ok( new Resources<PersistentEntityResource>(list) );
}
Base the answer of totran, this is my code:
There are dependencies:
springBootVersion = '2.4.2'
springDependencyManagement = '1.0.10.RELEASE'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-rest'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
The codes:
import icu.kyakya.rest.jpa.model.Address;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.data.rest.core.annotation.RestResource;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
#RepositoryRestResource(collectionResourceRel = "address", path = "address")
public interface AddressRepository extends PagingAndSortingRepository<Address, Long> {
//...
}
import lombok.Data;
import java.util.List;
#Data
public class Bulk<T> {
private List<T> bulk;
}
import lombok.RequiredArgsConstructor;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.server.ExposesResourceFor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
#BasePathAwareController // if base url exists, it needs to be added
#RepositoryRestController
#RequiredArgsConstructor
#ExposesResourceFor(Address.class)
public class AddressController {
private final AddressRepository repo;
#PostMapping("/address/saveAll")
public ResponseEntity<Iterable<Address>> saveAll(#RequestBody EntityModel<Bulk<Address>> bulk) {
List<Address> addresses = Objects.requireNonNull(bulk.getContent()).getBulk();
Iterable<Address> resp = repo.saveAll(addresses);
return new ResponseEntity<>(resp,HttpStatus.CREATED);
}
}
The way more like Spring data rest:
import lombok.RequiredArgsConstructor;
import org.springframework.data.rest.webmvc.BasePathAwareController;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.server.ExposesResourceFor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
#BasePathAwareController // if base url exists, it needs to be added
#RepositoryRestController
#RequiredArgsConstructor
#ExposesResourceFor(Address.class)
public class AddressController {
private final AddressRepository repo;
private final RepositoryEntityLinks entityLinks; //get link
/**
* curl -i -X POST -H "Content-Type:application/json" -d '{ "bulk": [ {"country" : "Japan" , "city" : "Tokyo" }, {"country" : "Japan" , "city" : "Osaka" }]} ' http://localhost:8080/api/v1/address/saveAll
*
* #return 201 https://docs.spring.io/spring-data/rest/docs/current/reference/html/#repository-resources.default-status-codes
*/
#PostMapping("/address/saveAll")
public ResponseEntity<CollectionModel<EntityModel<Address>>> List<Address> data = Objects.requireNonNull(bulk.getContent()).getBulk();
Iterable<Address> addresses = repo.saveAll(data);
ArrayList<EntityModel<Address>> models = new ArrayList<>();
addresses.forEach(i->{
Link link = entityLinks.linkToItemResource(Address.class, i.getId()).withRel("self");
models.add(EntityModel.of(i).add(link));
});
return new ResponseEntity<>(CollectionModel.of(models),HttpStatus.CREATED);
}
}

Applying different Jackson filter for different Jersey REST service calls

I am using Jersey to implement JAX-RS REST-style services along with Jackson 2.0.2 for the JSON mapping. One of these REST services returns a List<EntityA> (let's call it indexA) where EntityA contains another List<EntityB> whereas another service just returns a List<EntityB> (let's call it indexB):
#Entity
#JsonAutoDetect
public class EntityA {
#Id
private String id;
#OneToMany
private List<EntityB> b;
...
}
#Entity
#JsonAutoDetect
#JsonFilter("bFilter")
public class EntityB {
#Id
private String id;
private String some;
private String other;
private String attributes;
...
}
#Path("/a")
public class AResource {
#GET
#Path("/")
public List<EntityA> indexA() {
...
}
}
#Path("/b")
public class BResource {
#GET
#Path("/")
public List<EntityB> indexB() {
...
}
}
What I'd like to achieve is to apply a Jackson filter to the indexA invocation so that not all attributes of the child EntityB elements are serialized. OTOH, indexB should return EntityB in its completeness.
I am aware of the existence of a ContextResolver<ObjectMapper>, which I am already using for other purposes. Unfortunately, for the ContextResolver it seems to be impossible to distinguish both service invocations as the Class supplied to ContextResolver.getContext(Class) is ArrayList in both cases (and thanks to type erasure I cannot figure out the generic type parameters).
Are there any hooks better suited at configuring an ObjectMapper/FilterProvider depending on the entity type that is being mapped?
I could use the approach proposed in How to return a partial JSON response using Java?: Manually mapping to a String, but that kills the whole beauty of a declarative annotation-based approach, so I'd like to avoid this.
I was in the same situation, after tons of research, I figured it out, the solution is to use #JsonView and Spring which can inject an ObjectMapper into the JSON Writer without killing the beauty of Jersey.
I am working on a set of REST APIs, I want to get a list of instances of SystemObject and the detail a specific instance of SystemObject, just like you I just want very limited of number of properties of each instance in the list and some additional properties in the detail, I just define Views for them, and add annotation in the SystemObject class. but by default, all properties with no #JsonView annotation will be output to the JSON, but there is a configuration item(SerializationConfig.Feature.DEFAULT_VIEW_INCLUSION) I can use to exclude them.
The problem is that I have to set it to true to meet my need. but I can not change the ObjectMapper which does the magic to convert the object to JSON, by reading the 3 articles below, I got the idea that the only way I can do is to inject a Modified ObjectMapper to Jersey.
Now I got what I want.
It is like you create multiple views against a database table.
These 3 links will help you in different ways:
How to create a ObjectMapperProvider which can be used by Spring to inject
Jersey, Jackson, Spring and JSON
Jersey + Spring integration example
REST resource:
package com.john.rest.resource;
import java.util.ArrayList;
import java.util.List;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.UriInfo;
import org.codehaus.jackson.map.annotate.JsonView;
import org.springframework.stereotype.Component;
import com.midtronics.esp.common.EspException;
import com.midtronics.esp.common.SystemObject;
import com.midtronics.esp.mobile.model.SystemObjectView;
import com.midtronics.esp.model.accesscontrol.AccessControlBean;
import com.midtronics.esp.model.site.SiteBean;
#Component
#Path("/hierarchy")
public class Hierarchy {
// Allows to insert contextual objects into the class,
// e.g. ServletContext, Request, Response, UriInfo
#Context
UriInfo uriInfo;
#Context
Request request;
// Return the list of sites
#GET
#Path("sites")
#Produces(MediaType.APPLICATION_JSON)
#JsonView({SystemObjectView.ObjectList.class})
public List<SystemObject> listSite(
#HeaderParam("userId") String userId,
#HeaderParam("password") String password) {
ArrayList<SystemObject> sites= new ArrayList<SystemObject>();
try{
if(!AccessControlBean.CheckUser(userId, password)){
throw new WebApplicationException(401);
}
SystemObject.GetSiteListByPage(sites, 2, 3);
return sites;
} catch(EspException e){
throw new WebApplicationException(401);
} catch (Exception e) {
throw new WebApplicationException(500);
}
}
// Return the number of sites
#GET
#Path("sites/total")
#Produces(MediaType.TEXT_PLAIN)
public String getSiteNumber(#HeaderParam("userId") String userId,
#HeaderParam("password") String password) {
try{
return Integer.toString(SiteBean.GetSiteTotal());
} catch(EspException e){
throw new WebApplicationException(401);
} catch (Exception e) {
throw new WebApplicationException(500);
}
}
}
REST model:
package com.john.rest.model;
import java.io.Serializable;
import java.util.ArrayList;
import javax.xml.bind.annotation.XmlRootElement;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.annotate.JsonView;
import com.midtronics.esp.mobile.model.SystemObjectView;
import com.midtronics.esp.model.common.ICommonDAO;
#XmlRootElement
public class SystemObject implements Serializable
{
private static final long serialVersionUID = 3989499187492868996L;
#JsonProperty("id")
#JsonView({SystemObjectView.ObjectList.class, SystemObjectView.ObjectDetail.class})
protected String objectID = "";
#JsonProperty("parentId")
protected String parentID = "";
#JsonProperty("name")
#JsonView({SystemObjectView.ObjectList.class, SystemObjectView.ObjectDetail.class})
protected String objectName = "";
//getters...
//setters...
}
REST model view:
package com.john.rest.model;
public class SystemObjectView {
public static class ObjectList { };
public static class ObjectDetail extends ObjectList { }
}

Categories

Resources