Spring request parameter parsing - java

Can someone please explain the following behaviour:
Given the following HTTP GET request
/test?q=queryString&map[one]=val&map[two]=val2&map[three]=val3
why does this Controller method
#RequestMapping(value = "/test")
public ResponseEntity<Void> test(String q, Map<String, String> map) {
log.info("Q: " + q);
log.info("Map: " + map);
return ResponseEntity.noContent().build();
}
result in
Q: queryString
Map: {}
whereas if I wrap both parameters in a so-aptly named Wrapper class
public class Wrapper {
private String q;
private Map<String, String> map;
//getters & setters
}
and refactor the Controller method so
#RequestMapping(value = "/test")
public ResponseEntity<Void> test(Wrapper wrapper) {
log.info("Q: " + wrapper.getQ());
log.info("Map: " + wrapper.getMap());
return ResponseEntity.noContent().build();
}
the result is as follows:
Q: queryString
Map: {one=val, two=val2, three=val3}
Why does Spring only bind the map variable values if it's contained inside a wrapper object?
EDIT
Looking at #RequestParam JavaDoc it states
If the method parameter is Map<String, String> or MultiValueMap<String, String> and a parameter name is not specified, then the map parameter is populated with all request parameter names and values.
But adding #RequestParam(name = "map") to the map variable doesn't change anything. It's still empty.

Related

typeMismatch.java.util.List when trying to set a list

I am trying to set a List<Long> to an Java object.
The set function is:
ResponseEntity<String> response = bcInsertService.addNewClip(new PrmBcClipInsert()
.setTags(Arrays.asList(new Long[]{5L, 3L}))
);
And the object is
public class PrmBcClipInsert implements Serializable {
#ApiModelProperty(required = true)
private List<Long> tags;
public List<Long> getTags() {
return tags;
}
public PrmBcClipInsert setTags(List<Long> tags) {
this.tags = tags;
return this;
}
}
This is BcInsertService:
public class BcInsertService extends RestTemplate {
private static final Logger log = LoggerFactory.getLogger(BcInsertService.class);
public ResponseEntity<String> addNewClip(PrmBcClipInsert prmBcClipInsert) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
MultiValueMap<String, Object> map= new LinkedMultiValueMap<String, Object>();
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<MultiValueMap<String, Object>>(prmBcClipInsert.getParameters(), headers);
ParameterizedTypeReference<StandardResponse> typeRef = new ParameterizedTypeReference<StandardResponse>() {};
ResponseEntity<String> response = this.postForEntity( "http://localhost:8080/bc/add-clip", request , String.class );
log.info(response.toString());
return response;
}
}
And it returns an error:
Field error in object 'prmBcClipInsert' on field 'tags': rejected
value [[5,3]]; codes
[typeMismatch.prmBcClipInsert.tags,typeMismatch.tags,typeMismatch.java.util.List,typeMismatch];
arguments
[org.springframework.context.support.DefaultMessageSourceResolvable:
codes [prmBcClipInsert.tags,tags]; arguments []; default message
[tags]]; default message [Failed to convert property value of type
'java.lang.String' to required type 'java.util.List' for property
'tags'; nested exception is java.lang.NumberFormatException: For input
string: "[5,3]"]
Why the method doesn't accept a list even though it says that it accepts a list?
I was able to recreate your error case using a form validation. You are probably trying to pass a form data that is [5, 3] for the tags variable with type List<Long>, but passing with brackets break that structure, the value ought to be 5, 3...
So what I've done is;
Create a dummy controller using your input;
#Controller
public class TestController {
#PostMapping
public ModelAndView test(#Validated #ModelAttribute final PrmBcClipInsert prmBcClipInsert, final BindingResult bindingResult) {
final ModelAndView modelAndView = new ModelAndView();
System.out.println(prmBcClipInsert.getTags());
modelAndView.setViewName("test");
return modelAndView;
}
}
Pass the form with tags=[5,3], and get the following error in BindingResult;
org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'prmBcClipInsert' on field 'tags': rejected
value [[5, 3]]; codes
[typeMismatch.prmBcClipInsert.tags,typeMismatch.tags,typeMismatch.java.util.List,typeMismatch];
arguments
[org.springframework.context.support.DefaultMessageSourceResolvable:
codes [prmBcClipInsert.tags,tags]; arguments []; default message
[tags]]; default message [Failed to convert property value of type
'java.lang.String' to required type 'java.util.List' for property
'tags'; nested exception is java.lang.NumberFormatException: For input
string: "[5,3]"]
Which is the identical error that you were getting... So I presume either you get this PrmBcClipInsert as a form input like in my example, or you are trying to do a similar binding in some other part of your code...
Pass the form with tags=5,3, no error...
There can be a custom converter to support for passing said array input with brackets in binding logic with something like;
#Component
public class LongListConverter implements Converter<String, List<Long>> {
#Override
public List<Long> convert(String source) {
return Arrays.stream(StringUtils.strip(source, "[]").split(","))
.map(StringUtils::strip)
.map(Long::new)
.collect(Collectors.toList());
}
}
With this, both 5, 3 & [5, 3] can be supplied as value of tags variable.
All you need is a converter here. Create a List<>String converter like below (refactor the below example in your code):
#Converter
public class StringListConverter implements AttributeConverter<List<String>, String> {
private static final String SPLIT_CHAR = ";";
// Go nuts on List to string here...
#Override
public String convertToDatabaseColumn(List<String> stringList) {
return String.join(SPLIT_CHAR, stringList.toString());
}
#Override
public List<String> convertToEntityAttribute(String string) {
return Arrays.asList(string.split(SPLIT_CHAR));
}
}
Try it and share the outcome.

How to use Map to bound a values on parameters of java method from URI in Rest API

#GET
#Path("/getResults/{names}/view")
#Produces("application/json")
public String getResults(#QueryParam("names") Map<String, String> names) {
System.out.println(names);
return "someValue";
}
Explanation: We are trying to bind a value to java.util.map parameter from URI, but not getting it.
Accept string as Query params.
Then convert the string to map using gson.
#GET
#Path("/getResults/{names}/view")
#Produces("application/json")
public String getResults(#QueryParam("names") String names) {
Map<String, String> map = new Gson().fromJson(names, Map.class)
return "someValue";
}

Spring MVC populate #RequestParam Map<String, String>

I have the following method in my Spring MVC #Controller :
#RequestMapping(method = RequestMethod.GET)
public String testUrl(#RequestParam(value="test") Map<String, String> test) {
(...)
}
I call it like this :
http://myUrl?test[A]=ABC&test[B]=DEF
However the "test" RequestParam variable is always null
What do I have to do in order to populate "test" variable ?
As detailed here
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestParam.html
If the method parameter is Map or MultiValueMap and a parameter name is not specified, then the map parameter is populated with all request parameter names and values.
So you would change your definition like this.
#RequestMapping(method = RequestMethod.GET)
public String testUrl(#RequestParam Map<String, String> parameters)
{
(...)
}
And in your parameters if you called the url http://myUrl?A=ABC&B=DEF
You would have in your method
parameters.get("A");
parameters.get("B");
You can create a new class that contains the map that should be populated by Spring and then use that class as a parameter of your #RequestMapping annotated method.
In your example create a new class
public static class Form {
private Map<String, String> test;
// getters and setters
}
Then you can use Form as a parameter in your method.
#RequestMapping(method = RequestMethod.GET)
public String testUrl(Form form) {
// use values from form.getTest()
}
Spring doesn't have default conversion strategy from multiple parameters with the same name to HashMap. It can, however, convert them easily to List, array or Set.
#RequestMapping(value = "/testset", method = RequestMethod.GET)
public String testSet(#RequestParam(value = "test") Set<String> test) {
return "success";
}
I tested with postman like http://localhost:8080/mappings/testset?test=ABC&test=DEF
You will see set having data, [ABC, DEF]
Your question needs to be considered from different points of view.
first part:
as is mentioned in the title of the question, is how to have Map<String, String> as #RequestParam.
Consider this endpoint:
#GetMapping(value = "/map")
public ResponseEntity getData(#RequestParam Map<String, String> allParams) {
String str = Optional.ofNullable(allParams.get("first")).orElse(null);
return ResponseEntity.ok(str);
}
you can call that via:
http://<ip>:<port>/child/map?first=data1&second=data2
then when you debug your code, you will get these values:
> allParams (size = 2)
> first = data1
> second = data2
and the response of the requested url will be data1.
second part:
as your requested url shows (you have also said that in other answers' comments) ,you need an array to be passed by url.
consider this endpoint:
public ResponseEntity<?> getData (#RequestParam("test") Long[] testId,
#RequestParam("notTest") Long notTestId)
to call this API and pass proper values, you need to pass parameters in this way:
?test=1&test=2&notTest=3
all test values are reachable via test[0] or test[1] in your code.
third part:
have another look on requested url parameters, like: test[B]
putting brackets (or [ ]) into url is not usually possible. you have to put equivalent ASCII code with % sign.
for example [ is equal to %5B and ] is equal to %5D.
as an example, test[0] would be test%5B0%5D.
more ASCII codes on: https://ascii.cl/
I faced a similar situation where the client sends two groups of variable parameters. Let's call these groups foo and bar. A request could look like:
GET /search?page=2&size=10&foo[x]=aaa&foo[y]=bbb&bar[z]=ccc
I wanted to map these parameters to two distinct maps. Something like:
#GetMapping("/search")
public Page<...> search(Pageable pageable,
#RequestParam(...) Map<String, String> foo,
#RequestParam(...) Map<String, String> bar) {
...
}
#RequestParam didn't work for me, too. Instead I created a Model class with two fields of type Map<> matching the query parameter names foo and bar (#Data is lombok.Data).
#Data
public static class FooBar {
Map<String, String> foo;
Map<String, String> bar;
}
My controller code has changed to:
#GetMapping("/search")
public Page<...> search(Pageable pageable, FooBar fooBar) {
...
}
When requesting GET /search?page=2&size=10&foo[x]=aaa&foo[y]=bbb&bar[z]=ccc Spring instantiated the Maps and filled fooBar.getFoo() with keys/values x/aaa and y/bbb and fooBar.getBar() with z/ccc.
you can use MultiValueMap
MultiValueMap<String, String>
#RequestMapping(method = RequestMethod.GET)
public String testUrl(#RequestParam(value="test") MultiValueMap<String, String> test) {
(...)
}
and while testing don't use test[A],test[B]. just use it as stated below.
http://myUrl?test=ABC&test=DEF
test result will be in below format when you print it.
test = {[ABC, DEF]}

How to get Form data as a Map in Spring MVC controller?

I have a complicated html form that dynamically created with java script.
I want to get the map of key-value pairs as a Map in java and store them.
here is my controller to get the submitted data.
#RequestMapping(value="/create", method=RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String createRole(Hashmap<String, Object) keyVals) {
....
}
but my map is empty.
How can i get form data as a map of name-value pairs in Spring mvc controller?
You can also use #RequestBody with MultiValueMap
e.g.
#RequestMapping(value="/create",
method=RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String createRole(#RequestBody MultiValueMap<String, String> formData){
// your code goes here
}
Now you can get parameter names and their values.
MultiValueMap is in Spring utils package
I,ve just found a solution
#RequestMapping(value="/create", method=RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String createRole(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
...
}
this way i have a map of submitted parameters.
Try this,
#RequestMapping(value = "/create", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String createRole(#RequestParam HashMap<String, String> formData) {
The answers above already correctly point out that #RequestParam annotation is missing. Just to add why thta is required,
A simple GET request would be soemthing like :
http://localhost:8080/api/foos?id=abc
In controller, we need to map the function paramter to the parameter in the GET request. So we write the controller as
#GetMapping("/api/foos")
#ResponseBody
public String getFoos(#RequestParam String id) {
return "ID: " + id;
}
and add #RequestParam for the mapping of the paramter "id".

Spring RequestParam Map<String, String>

I have this in my controller:
#RequestMapping(value = "/myUrl", method = RequestMethod.GET)
public String myUrl(#RequestParam(value = "test") Map<String, String> test)
{
return test.toString();
}
And I'm making this HTTP request:
GET http://localhost:8080/myUrl?test[a]=1&test[b]=2
But in the logs I'm getting this error:
org.springframework.web.bind.MissingServletRequestParameterException: Required Map parameter 'test' is not present
How can I pass Map<String, String> to Spring?
May be it's a bit late but this can be made to work by declaring an intermediate class:
public static class AttributeMap {
private Map<String, String> attrs;
public Map<String, String> getAttrs() {
return attrs;
}
public void setAttrs(Map<String, String> attrs) {
this.attrs = attrs;
}
}
And using it as parameter type in method declaration (w/o #RequestParam):
#RequestMapping(value = "/myUrl", method = RequestMethod.GET)
public String myUrl(AttributeMap test)
Then with a request URL like this:
http://localhost:8080/myUrl?attrs[1]=b&attrs[222]=aaa
In test.attrs map all the attributes will present as expected.
It's not immediately clear what you are trying to do since test[a] and test[b] are completely unrelated query string parameters.
You can simply remove the value attribute of #RequestParam to have your Map parameter contain two entries, like so
{test[b]=2, test[a]=1}

Categories

Resources