Spring RequestParam Map<String, String> - java

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}

Related

Spring ArgumentResolver to get pathvariable map and

this my method signature
#RequestMapping(value = {"/article", "/article/{id}", "/article/{name}"}, method = RequestMethod.GET,
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<JsonNode> get(#PathVariable Map<String, String> pathVarsMap, #RequestParam(value="test") MultiValueMap<String, String> test, #RequestBody(required=false) JsonNode requestBody )
I want to make this into
public ResponseEntity<JsonNode> get( MyStructure mystr)
where MyStructure will have #PathVariable Map<String, String> pathVarsMap, #RequestParam(value="test") MultiValueMap<String, String> test, #RequestBody(required=false) JsonNode requestBody inside of it.
I know that I have to use custom resolvers and implement resolveArgument. One of the examples i saw did (Map<String, String>) httpServletRequest.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE). But im not sure how to get it to work. Can i create MultiValueMap and RequestBody inside MyString ?
In another place, I see that the recommendation is to use
#Nonnull
protected final Map<String, String> getUriTemplateVariables(NativeWebRequest request) {
#SuppressWarnings("unchecked")
Map<String, String> variables =
(Map<String, String>) request.getAttribute(
URI_TEMPLATE_VARIABLES_ATTRIBUTE, SCOPE_REQUEST);
return (variables != null) ? variables : Collections.<String, String>emptyMap();
}
so im a bit confused on how should i be implementing this
All #PathVariable , #RequestParam and #RequestBody can only be annotated on the method parameters , so there are no ways for you to annotate them on the object fields.
The codes of the existing HandlerMethodArgumentResolver that resolve the values for these annotations also assume these annotation are annotated on the method parameters ,that means you also cannot simply delegate to them to resolve the value for your request object.
Your best bet is to simply reference the corresponding HandlerMethodArgumentResolver for each annotation and copy the related codes to your implementation.
For #PathVariable , it is resolved by PathVariableMapMethodArgumentResolver
For #RequestParam on MultiValueMap , it is resolved by RequestParamMapMethodArgumentResolver
For #RequestBody , it is resolved by RequestResponseBodyMethodProcessor . Internally , it works with a list of HttpMessageConverter to read the HTTP request body. As you are now using Jackson to read the request body , you only need to focus on MappingJackson2HttpMessageConverter for simplicity.
It is easier than I expected. There following implementation should be a good starting point for you.
First define MyStructure class :
public class MyStructure {
public Map<String, String> pathVariables;
public MultiValueMap<String, String> queryParameters;
public JsonNode requestBody;
}
And implement MyStructureArgumentResolver :
public class MyStructureArgumentResolver implements HandlerMethodArgumentResolver {
private MappingJackson2HttpMessageConverter messageConverter;
public MyStructureArgumentResolver(MappingJackson2HttpMessageConverter messageConverter) {
super();
this.messageConverter = messageConverter;
}
#Override
public boolean supportsParameter(MethodParameter parameter) {
return MyStructure.class.isAssignableFrom(parameter.getParameterType());
}
#Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
MyStructure request = new MyStructure();
request.queryParameters = resolveQueryParameters(webRequest);
request.pathVariables = resolvePathVariables(webRequest);
request.requestBody = resolveRequestBody(webRequest, parameter);
return request;
}
private MultiValueMap<String, String> resolveQueryParameters(NativeWebRequest webRequest) {
// resolve all query parameter into MultiValueMap
Map<String, String[]> parameterMap = webRequest.getParameterMap();
MultiValueMap<String, String> result = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
for (String value : values) {
result.add(key, value);
}
});
return result;
}
private Map<String, String> resolvePathVariables(NativeWebRequest webRequest) {
Map<String, String> uriTemplateVars = (Map<String, String>) webRequest.getAttribute(
HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (!CollectionUtils.isEmpty(uriTemplateVars)) {
return new LinkedHashMap<>(uriTemplateVars);
} else {
return Collections.emptyMap();
}
}
private JsonNode resolveRequestBody(NativeWebRequest webRequest, MethodParameter parameter)
throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
HttpInputMessage inputMessage = new ServletServerHttpRequest(servletRequest);
MediaType contentType;
try {
contentType = inputMessage.getHeaders().getContentType();
} catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
Class<?> contextClass = parameter.getContainingClass();
JsonNode body = JsonNodeFactory.instance.objectNode();
if (messageConverter.canRead(JsonNode.class, contextClass, contentType)) {
body = (JsonNode) messageConverter.read(JsonNode.class, inputMessage);
}
return body;
}
}
Then register MyStructureArgumentResolver :
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Autowired
private MappingJackson2HttpMessageConverter messageConverter;
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new MyStructureArgumentResolver(messageConverter));
}
}
And use it in the controller method :
#RequestMapping(value = { "/test/{name}" }, method = RequestMethod.GET)
public ResponseEntity<String> test(MyStructure request) {
}
#PostMapping("/get")
public ResponseEntity<JsonNode> get( #RequestBody MyStructure mystr){...}
When call this api, fill in params in request body, send body as application/json. Refer to this sample: sample project

Map as parameter in RestAPI Post request

I have created an API with a Map<String, Integer> parameter, like this:
#RequestMapping(value = "upload", method = RequestMethod.POST)
public ResponseEntity<String> handleContactsFileUpload(#RequestParam("file") MultipartFile file,
#RequestParam("name") String name,
#RequestParam("campaignAppItemId") Long campaignAppItemId,
#RequestParam("fileColumnHeaders") Map<String,Integer> fileColumnHeaders) throws Exception {
if (file == null)
return new ResponseEntity<>("No file uploaded", HttpStatus.BAD_REQUEST);
contactService.handleContactsFile(file, name, campaignAppItemId,fileColumnHeaders);
return new ResponseEntity<>("File uploaded successfully", HttpStatus.OK);
}
I am trying to call this via Postman:
I passed the fileColumnHeaders inside Body->Form Data as in the screenshot.
Then I got a message like this in Postman:
Failed to convert value of type 'java.lang.String' to required type 'java.util.Map'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.util.Map': no matching editors or conversion strategy found.
Anybody know why this message came ?
How can we pass a map as a parameter in Rest API request?
How can we pass a map through Postman?
You could use #RequestBody instead of #RequestParam for Maps and other non trivial data types and objects - this way spring will map the JSON representing your map parameter to a domain object, which is then serializable and can be converted to a java object.
... Or simply create a converter:
#Component
#RequiredArgsConstructor
public class StringToMapConverter implements Converter<String, Map<String, Object>> {
private final ObjectMapper objectMapper;
#Override
public Map<String, Object> convert(String source) {
try {
return objectMapper.readValue(source, new TypeReference<Map<String, String>>() {
});
} catch (final IOException e) {
return null;
}
}
}
Firstly, you create DTO object to get all data from your request.
public class FormDataDTO {
private MultipartFile file;
private String name;
private Long campaignAppItemId;
private Map<String,Integer> fileColumnHeaders;
// getters, setters
}
Secondly, you can map FormDataDTO from your request without any annotation:
#RequestMapping(value = "upload", method = RequestMethod.POST)
public ResponseEntity<String> handleContactsFileUpload(FormDataDTO formDataDTO){
// your logic code here
}
Finally, form-data in your request will be:
I think this could work:
#RequestMapping(value = "upload/{fileColumnHeaders}", method = RequestMethod.POST)
public ResponseEntity<String> handleContactsFileUpload(#RequestParam("file") MultipartFile file,
#RequestParam("name") String name,
#RequestParam("campaignAppItemId") Long campaignAppItemId,
#MatrixVariable Map<String,Integer> fileColumnHeaders) throws Exception {
if (file == null)
return new ResponseEntity<>("No file uploaded", HttpStatus.BAD_REQUEST);
contactService.handleContactsFile(file, name, campaignAppItemId,fileColumnHeaders);
return new ResponseEntity<>("File uploaded successfully", HttpStatus.OK);
}
Put all other parameters into the body, but add the fileColumnHeaders to the URL like this:
/upload/firstName=1;lastName=2;address=3;phone=4
You will also need this extra configuration:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}

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]}

Spring request parameter parsing

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.

Encoding A Java Object (POJO) into a JSON String in GWT

Objective
I'd like to use JSON-RPC to send this on the clientside:
{"jsonrpc":"2.0","method":"RegisterValues","params":[["Planets","Stars"]],"id":2}
My Attempt
Using Resty-GWT:
public interface testService extends RestService {
#Produces("application/json")
#POST
public void myCall(Params myparams, MethodCallback<Response> callback );
public class Params {
String jsonrpc = "2.0";
String method;
//String[] params;
Map<String, String> params;
int id;
public void setId(int id){
this.id = id;
}
public void setMethod(String method){
this.method = method;
}
}
public void setParams(Map<String, String> params){
this.params = params;
}
}
When I call upon the Params object in another class (to set up the call), doing something like this:
Map<Integer, String> myParams = new HashMap<Integer, String>();
myParams.put(0, "Planets");
myParams.setParams(myParams);
Obviously this then sends it like:
{"jsonrpc":"2.0", "method":"RegisterValues", "params":{"0":"Planets"}, "id":0}
I can't figure out how to get those magic: [["Planets"]]
I tried using a String[] but this gives me the result: ["Planets"]
String[][] gives the error that it isn't supported yet.
Horrible Hack
If I pass a String in my testService as:
String myparams = "{\"jsonrpc\":\"2.0\",\"method\":\"RegisterValues\",\"params\":[[\"FPlanets\"]],\"id\":2}";
It works but obviously this is horrible.
I tried using GSON to encode the POJO, but GWT doesn't allow reflections. I tried using thetransactioncompany to parse the object into a 'String' but couldn't figure out the <inherits> I needed in my gwt.xml file.
Any thoughts?
Try this one
List<List<String>> params;
instead of
Map<String, String> params;
in your Params POJO class.
Read my post here about How to create a POJO class structure from an JSON string.
below JSON string
"params":[["Planets","Stars"]]
represents a List<List<String>> where "params" is a variable name in your POJO.
POJO:
class Params implements Serializable {
private String jsonrpc;
private String method;
private int id;
private List<List<String>> params;
...
//getter/setter
//default constructor
}
Try this one at server side
String myparams = "{\"jsonrpc\":\"2.0\",\"method\":\"RegisterValues\",\"params\":[[\"FPlanets\"]],\"id\":2}";
Params params=gson.fromJson(myparams,Params.class);
System.out.println(params.params+"-"+params.id);
Send the Params object back to client.

Categories

Resources