How to send Java collections containing subclasses to spring controller - spring

I'm trying to send collections to my spring MVC controller:
#RequestMapping("/postUsers.do")
public #ResponseBody ResponseDTO postUsers(#ModelAttribute("mapperList") MapperList mapperList) {
//prints {"users":null}
System.out.println(new ObjectMapper().writeValueAsString(mapperList));
return new ResponseDTO();
}
this is the code posting my users :
public ResponseDTO postUsers(ArrayList<User> users) {
ResponseDTO serverResponse = null;
URL url = new URL(urlString);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
connection.setRequestMethod("POST");
// prints {"users":[{"property1":"x","property1":y}]}
System.out.println(objectMapper.writeValueAsString(new MapperList(users)));
objectMapper.writeValue(connection.getOutputStream(), objectMapper.writeValueAsString(new MapperList(users)));
//blabla ...
}
and this is the object containing my list :
public class MapperList implements Serializable {
private static final long serialVersionUID = 8561295813487706798L;
private ArrayList<User> users;
public MapperList() {}
public MapperList(ArrayList<User> users) {
this.setUsers(users);
}
public ArrayList<User> getUsers() {
return users;
}
public void setUsers(ArrayList<User> users) {
this.users = users;
}
}
and this is the users type to post:
public abstract class User implements Serializable {
private static final long serialVersionUID = -1811485256250922102L;
private String property1;
private String property2;
public User() {}
public User(String prop1, String prop2) {
// set properties
}
// getters and setters
}
the problem is, when I output the value of the users's array before to post it to the controller, I got the following json value :
{"users":[{"property1":"x","property1":y}]}
but in the controller, when I print what I get from the request body, I only get :
{"users":null}
I also tryed with the annotation #RequestBody instead of #ModelAttribute("mapperList") and a JSONException is displayed :
*A JSONObject text must begin with '{' at 1 [character 2 line 1]\r\n*
My array list of users contains only one user that should be displayed. I don't understand why this doesn't work...
Thanks for any help !

You can chnage your MapperList class definition as public class MapperList extends ArrayList<User>{ ..} you dont need to define any instance variable like private ArrayList users inside MapperList class. Use #Requestbody annotation. You will be able to use MapperList as a ArrayList

Try to use:
public class MapperList{
private List<User> users;
//setter and getter
//toString
}
public class User{
private String property1;
private String property2;
//getter + setter
}
json:
{"users":[{"property1":"x", "property2":"y"}]}
in controller use #RequestBody. In that case Jackson will map your json to ArrayList of users.
#ResponseStatus(HttpStatus.OK)
#RequestMapping("/postUsers.do")
public #ResponseBody ResponseDTO postUsers(#RequestBody MapperList users) {
System.out.println(users);
return null;
}
no need to get objectMapper in that case. Don't forget to set content-type in request header to application/json. It required by Spring to handle #RequestBody processing.
If not working try to change MapperList:
List<User> users = new ArrayList<User>();

On the server side keep the #RequestBody annotation:
public #ResponseBody ResponseDTO postUsers(#RequestBody MapperList mapperList)
...
But this line causes problems:
objectMapper.writeValue(
connection.getOutputStream(),
objectMapper.writeValueAsString(new MapperList(users))
);
First it converts the object to JSON and then again uses objectMapper to JSON-encode the string into output stream. Try the following instead:
connection.getOutputStream().write(
objectMapper.writeValueAsString(new MapperList(users))
.getBytes("UTF-8")
);
or directly output to stream:
objectMapper.writeValue(
connection.getOutputStream(),
new MapperList(users))
);

Zbynek gave me part of the answer. Indeed
objectMapper.writeValue(
connection.getOutputStream(),
objectMapper.writeValueAsString(new MapperList(users))
);
doesn't work properly in my case
But moreover, my User class was an abstract class, with many type of User as subclasses. so the #RequestBody annotation couldn't work without specified the object type in the Json.
I used the following annotations on User class to make it working :
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = SubClassA.class, name = "a"),
#JsonSubTypes.Type(value = SubClassB.class, name = "b")
})
Thanks a lot for all your answers.

Related

Is Spring #Component annotation used correctly?

The purpose of this question is to find out if the codes are written with the right approach. Let's do CRUD operations on categories and posts in the blog website project. To keep the question short, I shared just create and update side.
(Technologies used in the project: spring-boot, mongodb)
Let's start to model Category:
#Document("category")
public class Category{
#Id
private String id;
#Indexed(unique = true, background = true)
private String name;
#Indexed(unique = true, background = true)
private String slug;
// getter and setter
Abstract BaseController class and IController Interface is created for fundamental level save, delete and update operations. I shared below controller side:
public interface IController<T>{
#PostMapping("/save")
ResponseEntity<BlogResponse> save(T object);
#GetMapping(value = "/find-all")
ResponseEntity<BlogResponse> findAll();
#GetMapping(value = "/delete-all")
ResponseEntity<BlogResponse> deleteAll();
}
public abstract class BaseController<T extends MongoRepository<S,String>, S> implements IController<S> {
#Autowired
private T repository;
#Autowired
private BlogResponse blogResponse;
#PostMapping(value = "/save", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public #ResponseBody ResponseEntity<BlogResponse> save(S object) {
try {
S model = (S) repository.save(object);
String modelName = object.getClass().getSimpleName().toLowerCase();
blogResponse.setMessage(modelName + " is saved successfully").putData(modelName, object);
} catch (DuplicateKeyException dke) {
return new ResponseEntity<BlogResponse>(blogResponse.setMessage("This data is already existing!!!"), HttpStatus.BAD_REQUEST);
} catch (Exception e) {
return new ResponseEntity<BlogResponse>(blogResponse.setMessage(e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<BlogResponse>(blogResponse, HttpStatus.OK);
}
// delete, findAll and other controllers
#RestController
#RequestMapping(value = "category")
#RequestScope
public class CategoryController extends BaseController<ICategoryRepository, Category>{
// More specific opretions like findSlug() can be write here.
}
And finally BlogResponce component is shared below;
#Component
#Scope("prototype")
public class BlogResponse{
private String message;
private Map<String, Object> data;
public String getMessage() {
return message;
}
public BlogResponse setMessage(String message) {
this.message = message;
return this;
}
public BlogResponse putData(String key, Object object){
if(data == null)
data = new HashMap<String,Object>();
data.put(key,object);
return this;
}
public Map<String,Object> getData(){
return data;
}
#Override
public String toString() {
return "BlogResponse{" +
"message='" + message + '\'' +
", data=" + data +
'}';
}
}
Question: I am new spring boot and I want to move forward by doing it right. BlogResponse is set bean by using #Component annotation. This doc said that other annotations like #Controller, #Service are specializations of #Component for more specific use cases. So I think, I cant use them. BlogResponse is set prototype scope for create new object at each injection. Also it's life end after response because of #RequestScope. Are this annotations using correcty? Maybe there is more effective way or approach. You can remark about other roughness if it existing.

How to send model attribute to spring controller using Postman

I want to test my controller using postman but don't know how to send a model attribute using postman.
I tried to send all attributes in row json fornamt and x-www-form-urlencoded in body but it is not working for me, I didn't understand where i'm getting wrong
My controller class looks like :
#RestController
public class DemoController {
#Autowired
private DemoService demoService;
#RequestMapping(value = "/userDetail", method = { RequestMethod.GET }, produces = { MediaType.APPLICATION_JSON })
public String testme(
ModelMap model,
#ModelAttribute("inputParameter") InputParameter inputParameter,
BindingResult result) {
return demoService.getDetail(inputParameter);
}
}
Model Class :
public class InputParameter {
private String id;
private String name;
private String number;
private String address;
private String pass;
}

SPRING JPA Lazy loading data to use in other class

I'm using Spring Boot for a project, I'm stuck with lazy loading.
What I want to do is load data in my controller, then send to presentable object, that will extract needed information and the JSON serializer do the bad work to create my custom HTTP response.
the problem occurs when the UserPresentation class calls the folder getter, the error is the well known: could not initialize proxy - no Session.
Of course the default fetch is LAZY for the folder and I want this, but I don't know how to prepare the object to be usable in the Presentation.
I copy-pasted only Folder set to be clear and short, but I've more collection inside User class, all of them give me the same problem.
I know that I could call getter in controller just to initialize Collections, but I find this like an hardcoding, in fact if I want add something to presentable I need to do in controller too.
I've tried too with #Transactional but not works.
Here are my class:
#Entity
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "USER_ID")
private Integer id;
#Column(unique = true)
private String email;
private String password;
#Enumerated(EnumType.STRING)
private Authority userAuthority;
#OneToMany(mappedBy = "owner", cascade = CascadeType.ALL)
private Set<Folder> ownFolders = new HashSet<>();
... getter setter
}
#RestController
public class UserController {
#GetMapping(value = "/api/user", produces = APPLICATION_JSON_VALUE)
public CustomResponseEntity userInfo() {
User currentUser = loginService.getCurrentUser();
UserPresentation userPresentation = new UserPresentation(currentUser);
return ResponseManager.respondData(userPresentation);
}
}
public class UserPresentation implements Presentable {
private User user;
public UserPresentation(User user) {
this.user = user;
}
public Integer getId() {
return user.getId();
}
public String getEmail() {
return user.getUsername();
}
public String getAuthority() {
return user.getUserAuthority().name();
}
public boolean isEnabled() {
return user.isEnabled();
}
public Integer getOwnFolders() {
Set<Folder> folderList = user.getOwnFolders();
if (folderList == null)
return 0;
return folderList.size();
}
}
Last two just to be clear
public class ResponseManager {
// DATA
public static ResponseEntity respondData(Presentable presentable, String token) {
CustomResponse response = new DataResponse<>(presentable);
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
public class DataResponse<T extends Presentable> extends CustomResponse {
private T data;
public T getData() {
return data;
}
private void setData(T data) {
this.data = data;
}
public DataResponse(T data) {
this.setData(data);
}
#Override
public String getType() {
return DATA;
}
}
I suppose you load the current user form the database with:
User currentUser = loginService.getCurrentUser();
and the getCurrentUser() method is transactional. You can either:
Use JPQL like this:
"select u from User u join fetch u.ownFolders where ... " to load the user's info (this way ownFolders relation is eagerly fetched)
or
Simply call user.getOwnFolders() inside getCurrentUser() to trigger
the fetch.
I found a way, even is a little bit dirty it allows me to do what I want without big change at the code.
Practically the problem occurs during the JSON serialization, that run outside of my control (somewhere inside Spring classes just before send HTTP response), so I manually serialized every Presentable object inside a #Transactional block just after its creation.
These are the changed classes:
public class UserPresentation implements Presentable {
private User user;
public UserPresentation(User user) {
this.user = user;
this.initialize() //ADDED (called here and in every other class that implements Presentable)
}
...getter and setter (which I want as JSON fields)
}
#RestController
public class UserController {
#Transactional //ADDED
#GetMapping(value = "/api/user", produces = APPLICATION_JSON_VALUE)
public CustomResponseEntity userInfo() {
User currentUser = loginService.getCurrentUser();
UserPresentation userPresentation = new UserPresentation(currentUser);
return ResponseManager.respondData(userPresentation);
}
}
Before this fix, the interface was used only to use Polymorfism inside ResponseManager, so was empty
public interface Presentable {
default void initialize() {
try {
new ObjectMapper().writeValueAsString(this);
} catch (JsonProcessingException e) {
throw new RuntimeJsonMappingException(e.getMessage());
}
}
}
I would suggest you use https://github.com/FasterXML/jackson-datatype-hibernate
The module supports datatypes of Hibernate versions 3.x , 4.x and 5.x; as well as some of the associated behavior such as lazy-loading and detection of transiency (#Transient annotation).
It knows how to handle Lazy loading after the session is closed , it will skip the json conversion for objects marked as Lazy fetch when outside session
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-hibernate5</artifactId>
<version>2.9.8</version>
</dependency>
ObjectMapper mapper = new ObjectMapper();
// for Hibernate 4.x:
mapper.registerModule(new Hibernate4Module());
// or, for Hibernate 5.x
mapper.registerModule(new Hibernate5Module());
// or, for Hibernate 3.6
mapper.registerModule(new Hibernate3Module());
#Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/*
* Here we register the Hibernate4Module into an ObjectMapper, then set this * custom-configured ObjectMapper to the MessageConverter and return it to be * added to the HttpMessageConverters of our application
*/
public MappingJackson2HttpMessageConverter jacksonMessageConverter() {
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper hibernateAwareObjectMapper = new ObjectMapper();
hibernateAwareObjectMapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
hibernateAwareObjectMapper.enable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// Registering Hibernate5Module to support lazy objects
hibernateAwareObjectMapper.registerModule(new Hibernate5Module());
messageConverter.setObjectMapper(hibernateAwareObjectMapper);
return messageConverter;
}
}
XML config
<mvc:annotation-driven>
<mvc:message-converters>
<!-- Use the HibernateAware mapper instead of the default -->
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper">
<bean class="path.to.your.HibernateAwareObjectMapper" />
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>

Spring #RequestMapping method without #RequestBody

In a Spring Boot Controller method, how do I get the body of a POST? All of the examples I have seen use #RequestBody. How do I get the body without using #RequestBody?
I am writing a method to handle Slack Events. When Slack POSTs an event, the body is in JSON and often contains a "user" key. Depending on the type of event, the value of "user" can either be a string or an object. Because of this, I cannot create a single Class and write
#RequestMapping(path = "/slackRequest", method = RequestMethod.POST)
public String handleSlackRequest(#RequestBody final SlackRequest slackRequest)
Answer: Implementing the approach suggested by #ChiDov, the solution is to keep the #RequestBody, import
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
define the user field (and a new field to store the 'user' if it is a simple String value) as
#OneToOne
private SlackEventUser user;
private String slackUserId;
and define its Setter method as
#JsonSetter("user")
public void setUser(JsonNode userNode) {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
if (userNode.isObject()) {
SlackEventUser slackEventUser = mapper.convertValue(userNode, SlackEventUser.class);
this.user = slackEventUser;
} else {
String userString = mapper.convertValue(userNode, String.class);
this.slackUserId = userString;
this.user = null;
}
}
Updated: I would make your DTO like :
Class SlackRequest{
...
private String eventType;
private JsonNode user;
...
public String getUser(){
return user.asText();
}
}
and in controller:
#RequestMapping(path = "/slackRequest", method = RequestMethod.POST)
public String handleSlackRequest(#RequestBody final SlackRequest slackRequest){
if(slackRequest.getEventType == "objectEvent"){
SomeObject user = mapper.convertValue(slackRequest.getUser(), SomeObject.class);
// do something with the object
}else{
// do something with the user string
}
}
Get Inspiration from : How to deserialize dynamic field using Jackson?

JAXB Error while using in SpringREST to return a ArrayList of a domain object

I am trying to use JAXB in Spring RESTful webservice.
My code is as follows:
#RequestMapping(value = "/countries",
method = RequestMethod.GET,
headers="Accept=application/xml, application/json")
public #ResponseBody CountryList getCountry() {
logger.debug("Provider has received request to get all persons");
// Call service here
CountryList result = new CountryList();
result.setData(countryService.getAll());
return result;
}
The CountryList.java class looks like:
#XmlRootElement(name="countries")
public class CountryList {
#XmlElement(required = true)
public List<Country> data;
#XmlElement(required = false)
public List<Country> getData() {
return data;
}
public void setData(List<Country> data) {
this.data = data;
}
}
The Country.java looks like:
#XmlRootElement(name="country")
public class Country {
private Calendar createdDt;
private String updatedBy;
private String createdBy;
private Long id;
private String countryName;
private Calendar updatedDt;
// getters and setters for all attributes goes here
}
Now, when I access the method getCountry(), I am getting the following exception
Caused by: com.sun.xml.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions
Class has two properties of the same name "data"
this problem is related to the following location:
at public java.util.List com.cisco.bic.services.model.CountryList.getData()
at com.cisco.bic.services.model.CountryList
this problem is related to the following location:
at public java.util.List com.cisco.bic.services.model.CountryList.data
at com.cisco.bic.services.model.CountryList
Would anyone has any idea why is this error coming. Am I doing anything wrong in the annotaion part ??
Please help.
Regards
Saroj
You can't annotate both the getter/setter and the field, you need to decide on one of them.

Resources