Spring: Bind a collection on controller parameter - java

I've seen lots of questions similar to mine but, I couldn't find a solution to this problem so far.
I am implementing a grid filtering and pagination on Spring + Hibernate. The load() method must receive the specific parameters for pagination (page, start and limit) and a list of key-value parameter for filtering, which is being the problem.
The parameters are coming like that:
page:1
start:0
limit:23
filter:[{"operator":"like","value":"tes","property":"desc"},{"operator":"like","value":"teste","property":"model_desc"}]
or (encoded version):
page=1&start=0&limit=23&filter=%5B%7B%22operator%22%3A%22like%22%2C%22value%22%3A%22tes%22%2C%22property%22%3A%22desc%22%7D%2C%7B%22operator%22%3A%22like%22%2C%22value%22%3A%22teste%22%2C%22property%22%3A%22model_desc%22%7D%5D
The filter parameter is coming as a String and the problem is to make Spring parse that either as something like ArrayList<Map<String,String>> or ArrayList<SomeFilterClass>.
This is the signature of my controller method (the commented lines are all not working, they are here just to show what I've tried so far):
public Map<String, Object> loadData(#RequestParam(value = "page", required = true) int page,
#RequestParam(value = "start", required = true) int start,
#RequestParam(value = "limit", required = true) int limit,
// #ModelAttribute("filter") ArrayList<Map<String, String>> filter) {
// #RequestParam(value = "filter", required = false) Map<String, Object>[] filter) {
// #RequestParam(value = "filter", required = false) List<Map<String, String>> filter) {
#ModelAttribute("filter") RemoteFilter filter)
This class, RemoteFilter, is a wrapper class that I built, following a suggestion from other posts but, it didn't work also. Its structure is:
public class RemoteFilter {
private ArrayList<Filter> filter;
//Getters and Setters....
class Filter {
private String operator;
private String value;
private String property;
//Getters and Setters....
}
}
I will be very glad if anybody help me with that.
Thanks!

Try to POST the data instead of using GET, Spring only offers JSON to Java conversion when data is posted.
Post
{
page:1
start:0
limit:23
filter:[{"operator":"like","value":"tes","property":"desc"},{"operator":"like","value":"teste","property":"model_desc"}]
}
And have the controller use #RequestBody
#RequestMapping(method = RequestMethod.POST, value = "url",
produces = MimeTypeUtils.APPLICATION_JSON_VALUE,
consumes = MimeTypeUtils.APPLICATION_JSON_VALUE)
public Map<String, Object> loadData(#RequestBody RemoteFilter filter) {
}
The response uses Objectas the Map value type. This will work, but using an un-typped return value is a bad thing in general.

Related

Is there a better way to provide filtering feature through REST API?

#GetMapping
public ResponseEntity<Page<CsatSurveyModel>> getAllSurveys(
#RequestParam(required = false) String teamName,
#RequestParam(required = false) String customerName,
#RequestParam(required = false) Integer year,
#RequestParam(defaultValue = "id") String orderBy,
#RequestParam(defaultValue = "DESC") Direction direction,
#RequestParam(defaultValue = AppConstant.DEFAULT_PAGE) int page,
#RequestParam(defaultValue = AppConstant.DEFAULT_PAGE_SIZE) int size) {
Sort sort = Sort.by(direction, orderBy);
Pageable pageRequest = PageRequest.of(page, size, sort);
Specification<CsatSurvey> csatSurveySpecification = Specification.where(null);
if (Objects.nonNull(teamName)) {
csatSurveySpecification = csatSurveySpecification.and(CsatSurvey.teamNameSpec(teamName));
}
if (Objects.nonNull(customerName)) {
csatSurveySpecification =
csatSurveySpecification.and(CsatSurvey.customerNameSpec(customerName));
}
if (Objects.nonNull(year)) {
csatSurveySpecification = csatSurveySpecification.and(CsatSurvey.yearSpec(year));
}
UserModel loggedInUser = sessionUtils.getLoggedInUser();
List<Team> teams =
UserRole.ADMIN.equals(loggedInUser.getRole())
? Collections.emptyList()
: loggedInUser.getTeams();
Page<CsatSurveyModel> csatSurveyModels =
csatService.getAllSurveysForTeams(teams, csatSurveySpecification, pageRequest);
return ResponseEntity.ok(csatSurveyModels);
}
The first three parameters are used for filtering purposes with specifications. The rest is for page requests. I was wondering if there's a better way to do this. There's a lot of code in the controller, and even if I want to move the processing to the service layer, the method would have to accept a long list of parameters, which I don't want to do. Although this method only accepts seven parameters, there are other routes that accept more than ten parameters.
I understand that one way is to accept all these params as Map<String, String>, but isn't it a bit tedious to process that?
My way is using a request class.
The advantage is you can change the params without changing the method signature both for the controller and the service (assuming you pass that request object to the service as well).
Example of a Controller method:
public UserDataResponse getUserData(#RequestBody UserDataRequest userDataRequest)
Where UserDataRequest is a simple class with getters and setters.
class UserDataRequest {
private String paramA;
private String paramB;
public String getParamA() {
return paramA;
}
}
etc.
Spring MVC can take a Pageable (or a Sort) as a controller parameter directly, and Spring Data can accept one as a query parameter.

How would you handle a REST API with only optional query params with Spring Boot?

I want to build a simple endpoint that returns an Order object where I can search for this order by a single query parameter or a combination of several query parameters altogether. All of these query parameters are optional and the reason is that different people will access these orders based on the different Ids.
So for example:
/order/items?itemId={itemId}&orderId={orderId}&deliveryId={deliveryId}&packetId={packetId}
#GetMapping(path = "/order/items", produces = "application/json")
public Order getOrders(#RequestParam Optional<String> itemId,
#RequestParam Optional<String> orderId,
#RequestParam Optional<String> deliveryId,
#RequestParam Optional<String> packetId) { }
I could of course also skip the Java Optional and use #RequestParam(required = false), but the question here is rather how do I escape the if-else or .isPresent() nightmare of checking whether the query params are null? Or is there an elegant way, depending on the constellation of params, to pass further to my service and Spring Data JPA repository.
To minimize the amount of parameters in your method, you could define your query parameters as fields of a class:
#Data
public class SearchOrderCriteria {
private String itemId;
private String orderId;
private String deliveryId;
private String packetId;
}
Then receive an instance of such class in your controller method:
#GetMapping(path = "/order/items", produces = "application/json")
public ResponseEntity<OrderInfo> getOrder(SearchOrderCriteria searchCriteria) {
OrderInfo order = orderService.findOrder(searchCriteria)
return ResponseEntity.ok(order);
}
And, in your service, to avoid a bunch of if-else, you could use query by example:
public OrderInfo findOrder(SearchOrderCriteria searchCriteria) {
OrderInfo order = new OrderInfo();
order.setItemId(searchCriteria.getItemId());
order.setOrderId(searchCriteria.getOrderId());
order.setDeliveryId(searchCriteria.getDeliveryId());
order.setPacketId(searchCriteria.getPacketId());
Example<OrderInfo> example = Example.of(order);
return orderRepository.findOne(example);
}
My small suggestion is avoid to use too general API, for example you can split your mapping into several endpoints eg:/order/delivery/itemId/{itemId} and /order/delivery/deliveryId/{deliveryId} and /order/delivery/packetId/{packetId} and handle which you need to call on client side.

Spring MVC wrap a lot of #RequestParam to an Object [duplicate]

Suppose i have a page that lists the objects on a table and i need to put a form to filter the table. The filter is sent as an Ajax GET to an URL like that: http://foo.com/system/controller/action?page=1&prop1=x&prop2=y&prop3=z
And instead of having lots of parameters on my Controller like:
#RequestMapping(value = "/action")
public #ResponseBody List<MyObject> myAction(
#RequestParam(value = "page", required = false) int page,
#RequestParam(value = "prop1", required = false) String prop1,
#RequestParam(value = "prop2", required = false) String prop2,
#RequestParam(value = "prop3", required = false) String prop3) { ... }
And supposing i have MyObject as:
public class MyObject {
private String prop1;
private String prop2;
private String prop3;
//Getters and setters
...
}
I wanna do something like:
#RequestMapping(value = "/action")
public #ResponseBody List<MyObject> myAction(
#RequestParam(value = "page", required = false) int page,
#RequestParam(value = "myObject", required = false) MyObject myObject,) { ... }
Is it possible?
How can i do that?
You can absolutely do that, just remove the #RequestParam annotation, Spring will cleanly bind your request parameters to your class instance:
public #ResponseBody List<MyObject> myAction(
#RequestParam(value = "page", required = false) int page,
MyObject myObject)
I will add some short example from me.
The DTO class:
public class SearchDTO {
private Long id[];
public Long[] getId() {
return id;
}
public void setId(Long[] id) {
this.id = id;
}
// reflection toString from apache commons
#Override
public String toString() {
return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
}
}
Request mapping inside controller class:
#RequestMapping(value="/handle", method=RequestMethod.GET)
#ResponseBody
public String handleRequest(SearchDTO search) {
LOG.info("criteria: {}", search);
return "OK";
}
Query:
http://localhost:8080/app/handle?id=353,234
Result:
[http-apr-8080-exec-7] INFO c.g.g.r.f.w.ExampleController.handleRequest:59 - criteria: SearchDTO[id={353,234}]
I hope it helps :)
UPDATE / KOTLIN
Because currently I'm working a lot of with Kotlin if someone wants to define similar DTO the class in Kotlin should have the following form:
class SearchDTO {
var id: Array<Long>? = arrayOf()
override fun toString(): String {
// to string implementation
}
}
With the data class like this one:
data class SearchDTO(var id: Array<Long> = arrayOf())
the Spring (tested in Boot) returns the following error for request mentioned in answer:
"Failed to convert value of type 'java.lang.String[]' to required type
'java.lang.Long[]'; nested exception is
java.lang.NumberFormatException: For input string: \"353,234\""
The data class will work only for the following request params form:
http://localhost:8080/handle?id=353&id=234
Be aware of this!
Since the question on how to set fields mandatory pops up under each post, I wrote a small example on how to set fields as required:
public class ExampleDTO {
#NotNull
private String mandatoryParam;
private String optionalParam;
#DateTimeFormat(iso = ISO.DATE) //accept Dates only in YYYY-MM-DD
#NotNull
private LocalDate testDate;
public String getMandatoryParam() {
return mandatoryParam;
}
public void setMandatoryParam(String mandatoryParam) {
this.mandatoryParam = mandatoryParam;
}
public String getOptionalParam() {
return optionalParam;
}
public void setOptionalParam(String optionalParam) {
this.optionalParam = optionalParam;
}
public LocalDate getTestDate() {
return testDate;
}
public void setTestDate(LocalDate testDate) {
this.testDate = testDate;
}
}
//Add this to your rest controller class
#RequestMapping(value = "/test", method = RequestMethod.GET)
public String testComplexObject (#Valid ExampleDTO e){
System.out.println(e.getMandatoryParam() + " " + e.getTestDate());
return "Does this work?";
}
I have a very similar problem. Actually the problem is deeper as I thought. I am using jquery $.post which uses Content-Type:application/x-www-form-urlencoded; charset=UTF-8 as default. Unfortunately I based my system on that and when I needed a complex object as a #RequestParam I couldn't just make it happen.
In my case I am trying to send user preferences with something like;
$.post("/updatePreferences",
{id: 'pr', preferences: p},
function (response) {
...
On client side the actual raw data sent to the server is;
...
id=pr&preferences%5BuserId%5D=1005012365&preferences%5Baudio%5D=false&preferences%5Btooltip%5D=true&preferences%5Blanguage%5D=en
...
parsed as;
id:pr
preferences[userId]:1005012365
preferences[audio]:false
preferences[tooltip]:true
preferences[language]:en
and the server side is;
#RequestMapping(value = "/updatePreferences")
public
#ResponseBody
Object updatePreferences(#RequestParam("id") String id, #RequestParam("preferences") UserPreferences preferences) {
...
return someService.call(preferences);
...
}
I tried #ModelAttribute, added setter/getters, constructors with all possibilities to UserPreferences but no chance as it recognized the sent data as 5 parameters but in fact the mapped method has only 2 parameters. I also tried Biju's solution however what happens is that, spring creates an UserPreferences object with default constructor and doesn't fill in the data.
I solved the problem by sending JSon string of the preferences from the client side and handle it as if it is a String on the server side;
client:
$.post("/updatePreferences",
{id: 'pr', preferences: JSON.stringify(p)},
function (response) {
...
server:
#RequestMapping(value = "/updatePreferences")
public
#ResponseBody
Object updatePreferences(#RequestParam("id") String id, #RequestParam("preferences") String preferencesJSon) {
String ret = null;
ObjectMapper mapper = new ObjectMapper();
try {
UserPreferences userPreferences = mapper.readValue(preferencesJSon, UserPreferences.class);
return someService.call(userPreferences);
} catch (IOException e) {
e.printStackTrace();
}
}
to brief, I did the conversion manually inside the REST method. In my opinion the reason why spring doesn't recognize the sent data is the content-type.
While answers that refer to #ModelAttribute, #RequestParam, #PathParam and the likes are valid, there is a small gotcha I ran into. The resulting method parameter is a proxy that Spring wraps around your DTO. So, if you attempt to use it in a context that requires your own custom type, you may get some unexpected results.
The following will not work:
#GetMapping(produces = APPLICATION_JSON_VALUE)
public ResponseEntity<CustomDto> request(#ModelAttribute CustomDto dto) {
return ResponseEntity.ok(dto);
}
In my case, attempting to use it in Jackson binding resulted in a com.fasterxml.jackson.databind.exc.InvalidDefinitionException.
You will need to create a new object from the dto.
Yes, You can do it in a simple way. See below code of lines.
URL - http://localhost:8080/get/request/multiple/param/by/map?name='abc' & id='123'
#GetMapping(path = "/get/request/header/by/map")
public ResponseEntity<String> getRequestParamInMap(#RequestParam Map<String,String> map){
// Do your business here
return new ResponseEntity<String>(map.toString(),HttpStatus.OK);
}
Accepted answer works like a charm but if the object has a list of objects it won't work as expected so here is my solution after some digging.
Following this thread advice, here is how I've done.
Frontend: stringify your object than encode it in base64 for submission.
Backend: decode base64 string then convert the string json into desired object.
It isn't the best for debugging your API with postman but it is working as expected for me.
Original object: { page: 1, size: 5, filters: [{ field: "id", value: 1, comparison: "EQ" }
Encoded object: eyJwYWdlIjoxLCJzaXplIjo1LCJmaWx0ZXJzIjpbeyJmaWVsZCI6ImlkUGFyZW50IiwiY29tcGFyaXNvbiI6Ik5VTEwifV19
#GetMapping
fun list(#RequestParam search: String?): ResponseEntity<ListDTO> {
val filter: SearchFilterDTO = decodeSearchFieldDTO(search)
...
}
private fun decodeSearchFieldDTO(search: String?): SearchFilterDTO {
if (search.isNullOrEmpty()) return SearchFilterDTO()
return Gson().fromJson(String(Base64.getDecoder().decode(search)), SearchFilterDTO::class.java)
}
And here the SearchFilterDTO and FilterDTO
class SearchFilterDTO(
var currentPage: Int = 1,
var pageSize: Int = 10,
var sort: Sort? = null,
var column: String? = null,
var filters: List<FilterDTO> = ArrayList<FilterDTO>(),
var paged: Boolean = true
)
class FilterDTO(
var field: String,
var value: Any,
var comparison: Comparison
)

Spring Data Pagination & AJAX

I have the following Controller and I've just included pagination into my returned results
#RequestMapping(value = "/search/{person}", produces="application/json", method = RequestMethod.GET)
public Page<Person> findAllPersons(#PathVariable String person) {
Page<Person> list = personRepo.findAll(new PageRequest(1, PAGE_SIZE));
return list;
}
I'm now trying to figure out how to actually tab through these results - The search on the Person table has is it's own AJAX request, where as selecting "next" or "previous" on my UI tool can launch it's own GET
<a id="previous" href="onclick="setPageNumber(1)">
<a id="next" href="onclick="setPageNumber(2)">
function setPageNumber(num) { //relaunch request with page number value retrieved from previous or next}
Should I include a pageNumber as a #PathVariable like so:
#RequestMapping(value = "/search/{person}/{pageNumber}", produces="application/json", method = RequestMethod.GET)
public Page<Person> findAllPersons(#PathVariable String person, #PathVariable int pageNumber) {
Page<Person> list = personRepo.findAll(new PageRequest(pageNumber, PAGE_SIZE));
return list;
}
or should setting the pageNumber be a completely separate controller method that somehow invokes findAllPersons with the pageNumber argument? I may be confusing myself here - any input is welcome thanks!
For REST service I would put it to the parameters rather then to URI page_start=X&page_size=Y.
I know this post isn't active for a long time but for anyone still looking for a way to use Spring pagination with Ajax, here are some possible solutions:
1. If your repository is an instance of JpaRepository (or more precisely PagingAndSortingRepository) then you can simply pass a Pageable to it:
#Controller
public class FooController {
//...
#GetMapping("/foo/list")
public List<Foo> handleList(Pageable pageable) {
return fooRepository.findAll(pageable);
}
//...
}
2. Instead of Pageable, you can also retrieve your pagination parameters as #RequestParam and create a PageRequest yourself. This approach might be useful if the project does not use Spring Data and JPA:
#Controller
public class FooController {
//...
#GetMapping("/foo/list")
public List<Foo> handleList(
#RequestParam(value = "size", required = false) Optional<Integer> pageSize,
#RequestParam(value = "page", required = false) Optional<Integer> pageNumber,
#RequestParam(value = "sort", required = false) Sort sort,
) {
PageRequest pageable = new PageRequest(pageNumber.orElse(0), pageSize.orElse(10), sort);
return fooRepository.customfindAll(pageable);
}
//...
}
(e.g. this repository above might be a class extending a JDBCRepository such as this one )
And for the AJAX part of these two possible solutions, something like that can be used:
// ... handle pageNo, listSize etc.
var url = '/yourprj/foo/list?page=' + pageNo + '&size=' + listSize
$.ajax({
type: "GET",
contentType: "application/json",
url: url,
success: function(result) {
// handle Foo list...
}
});
3. Alternatively, if you are using Thymeleaf+Spring Data there is a dialect to automatically add pagination.

Spring MVC 3 - How come #ResponseBody method renders a JSTLView?

I have mapped one of my method in one Controller to return JSON object by #ResponseBody.
#RequestMapping("/{module}/get/{docId}")
public #ResponseBody Map<String, ? extends Object> get(#PathVariable String module,
#PathVariable String docId) {
Criteria criteria = new Criteria("_id", docId);
return genericDAO.getUniqueEntity(module, true, criteria);
}
However, it redirects me to the JSTLView instead. Say, if the {module} is product and {docId} is 2, then in the console I found:
DispatcherServlet with name 'xxx' processing POST request for [/xxx/product/get/2]
Rendering view [org.springframework.web.servlet.view.JstlView: name 'product/get/2'; URL [/WEB-INF/views/jsp/product/get/2.jsp]] in DispatcherServlet with name 'xxx'
How can that be happened? In the same Controller, I have another method similar to this but it's running fine:
#RequestMapping("/{module}/list")
public #ResponseBody Map<String, ? extends Object> list(#PathVariable String module,
#RequestParam MultiValueMap<String, String> params,
#RequestParam(value = "page", required = false) Integer pageNumber,
#RequestParam(value = "rows", required = false) Integer recordPerPage) {
...
return genericDAO.list(module, criterias, orders, pageNumber, recordPerPage);
}
Above do returns correctly providing me a list of objects I required.
Anyone to help me solve the mystery?
If a controller method returns null, Spring interprets that as saying that you want the framework to render the "default view".
It would be better, I think, that when the method is #RequestBody-annotated, this logic should not apply, but perhaps that's hard to implement - how would it handle a null return from a method that normally returns XML, for example?
Anyway, to stop this from happening, you need to make sure you return something, like an empty Map.

Categories