How to map dynamic query parameters in Spring Boot RestController - java

Is it possible to map query parameters with dynamic names using Spring Boot? I would like to map parameters such as these:
/products?filter[name]=foo
/products?filter[length]=10
/products?filter[width]=5
I could do something like this, but it would involve having to know every possible filter, and I would like it to be dynamic:
#RestController
public class ProductsController {
#GetMapping("/products")
public String products(
#RequestParam(name = "filter[name]") String name,
#RequestParam(name = "filter[length]") String length,
#RequestParam(name = "filter[width]") String width
) {
//
}
}
If possible, I'm looking for something that will allow the user to define any number of possible filter values, and for those to be mapped as a HashMap by Spring Boot.
#RestController
public class ProductsController {
#GetMapping("/products")
public String products(
#RequestParam(name = "filter[*]") HashMap<String, String> filters
) {
filters.get("name");
filters.get("length");
filters.get("width");
}
}
An answer posted on this question suggests using #RequestParam Map<String, String> parameters, however this will capture all query parameters, not only those matching filter[*].

You can map multiple parameters without defining their names in #RequestParam using a map:
#GetMapping("/api/lala")
public String searchByQueryParams(#RequestParam Map<String,String> searchParams) {
...
}

Does matrix variables work for you? If I understand you correctly, can be like this:
// GET /products/filters;name=foo;length=100
#GetMapping("/products/filters")
public void products(
#MatrixVariable MultiValueMap matrixVars) {
// matrixVars: ["name" : "foo", "length" : 100]
}

This seems like a solvable problem. The solutions are not ideal far as I know, but there are ways.
A previous attempt seemed bent on finding a perfect solution where the entire composition of the filter was known in-transit.
Spring MVC populate
The entirety of the dynamic criteria that user defines can be transmitted with some basic scheme you define, as one key=value parameter from the client, then decomposed into its elements once it is received.
You could also send two parameters: "fields" and "values", where the lists of each are encoded in there respectively, with some cautious delimiter of your choosing (could be an encoded special character that the user cannot physically type, perhaps).
You still need, as with everything other approach where the client side is submitting criteria (like filter criteria), full protection from any malicious use of the parameters, just as the client trying to embed SQL criteria in them (SQL Injection).
But so long as the client code follows the agreed syntax, you can receive any number of dynamic parameters from them in one shot.
Client:
/products?filter=field1--value1||field2--value2||field3--value3...
That is a simplified example showing delimiters that are too easy to "break", but the idea is some simple, even fully readable (no harm in doing so) scheme just for the purpose of packing your field names and values together for easy transit.
Server:
#RequestMapping(value = "/products", method = RequestMethod.GET)
public String doTheStuff(#RequestParam(value = "filter") String encodedFilter) {
.. decompose the filter here, filter everything they've sent for disallowed characters etc.

Related

How to keep nulls in a Spring GET servlet?

#RestController
public class MyController {
#GetMapping("/get)
public void get(Map<String, String> params) {
println(params.get("optional")); //"null"
}
#PostMapping("/post)
public void post(Map<String, String> params) {
println(params.get("optional")); //null
}
}
localhost:8080/get?key=value&optional=null
Result: the value of the key optional will be "null" written as String, not as null type.
Whereas a POST request would work as follows:
{
"key": "value",
"optional": null
}
Question: how can I make the GET request behave the same as POST? Means, how can I tell spring to interpret the null string in a GET as a real null?
You can't pass null via HTTP query parameter like you do it in JSON. Because null within an HTTP query has no special meaning and is treated like any other string.
Instead just don't pass optional parameter at all
localhost:8080/get?key=value
You could do something like this as well.
#RequestParam(name = "optional", required = false) String optional
Spring Docs
Like #Nikolai says, null has no special meaning in the query. The query is often called the Query String such as in AWS API Gateway, which is more descriptive that it tells you that it is a String, it isn't a Map, Strings only have chars encoded, there is no concept of a null in this context.
IMO it isn't good practice to use a Map<String,String> params if you can avoid it, rather prefer strong types and list all the possible query params with optional parameters for non-required inputs. If you want the users to specify a Map it should be in the BODY, but a GET with a body feels wrong to me so you might need to then change the HTTP method.
If you have many parameters, and that's why you are using a Map, remember some browsers limit the chars in a URL to 2048, so it can be dangerous and you may have a case whereby the user cannot specify all the parameters they need to because of this limit.
TL;DR: Map<String,String> should be in request body.

Deserializing List<Map<String, String>> QueryParam in jersey 1

I'm trying to implement a method in a dropwizard resource, that will service a call from a JS frontend (that uses DataTables).
The request has query parameters that look like this:
columns[0][data]=0&columns[0][name]=&columns[0][searchable]=false&columns[0][orderable]=false&columns[0][search][value]=&columns[0][search][regex]=false
columns[1][data]=iata&columns[1][name]=iata&columns[1][searchable]=true&columns[1][orderable]=true&columns[1][search][value]=&columns[1][search][regex]=false
The request comes from a JS frontend implemented with DataTables, and uses server-side processing. Info about how datatables sends the requests here:
https://datatables.net/manual/server-side
I'm having issues defining the data type for the above query parameters. With spring data, we can define it as:
List<Map<String, String>> columns
which can be wrapped in an object annotated with ModelAttribute and it will deserialize fine.
In my app I'm using an older version of dropwizard which depends on jersey 1.19.
I've tried annotating it as a QueryParam, but the app fails at startup.
Method:
#Path("/mappings")
#GET
#Timed
#Consumes(MediaType.APPLICATION_JSON)
#Produces(MediaType.APPLICATION_JSON)
public Response getMappings(#QueryParam("columns") List<Map<String, String>> columns) {
// processing here.
}
When I do this, I get:
ERROR [2016-11-07 14:16:13,061] com.sun.jersey.spi.inject.Errors: The
following errors and warnings have been detected with resource and/or
provider classes: SEVERE: Missing dependency for method public
javax.ws.rs.core.Response
com.ean.gds.proxy.ams.application.resource.gui.IataMappingGuiResource.getMappings(java.util.List)
at parameter at index 0 WARN [2016-11-07 14:16:13,070] /: unavailable
My question is: do I have any option other than writing a custom deserializer for it ?
Note: If I grab the request with #Context, I can see that the decodedQueryParams are a MultivaluedMap, which maps String keys like "columns[0][data]" to Lists of String values, which always have a single element, that is the value.
Update:
After some digging, I found the following JAX-RS specification (section 3.2) which explains why my approach isn't valid to begin with:
The following types are supported:
Primitive Types
Types that have a constructor that accepts a single String argument.
Types that have a static method named valueOf with a single String argument.
List, Set, or SortedSet where T satisfies 2 or 3 above.
Source: Handling Multiple Query Parameters in Jersey
So I've tried using just a List instead. This doesn't crash the app at startup, but when the request comes in, it deserializes into an empty list. So the question remains as to what approach is correct.
In fact, you're using such a very different structure from all the common ones we have mapped for Rest Web Services consummation. Also, because of this structural compliance problem, trying to use JSON to marshal/unmarshal the values won't suit, once we haven't object-based parameters being transported.
But, we have a couple of options to "work this situation around". Let's see:
Going with the #QueryParam strategy is not possible because of two main reasons:
As you noticed, there are some limitations on its use regarding Collections other than Lists, Sets, etc;
This annotation maps one (or a list) of param(s) by its(their) name(s), so you need every single parameter (separated by &) to have the same name. It's easier when we think about a form that submits (via GET) a list of checkboxes values: once they all have the same name property, they'll be sent in "name=value1&name=value2" format.
So, in order to get this requirement, you'd have to make something like:
#GET
public Response getMappings(#QueryParam("columns") List<String> columns) {
return Response.status(200).entity(columns).build();
}
// URL to be called (with same param names):
// /mappings?columns=columns[1][name]=0&columns=columns[0][searchable]=false
// Result: [columns[1][name]=0, columns[0][searchable]=false]
You can also try creating a Custom Java Type for Param Annotations, like you see here. That would avoid encoding problems, but in my tests it didn't work for the brackets issue. :(
You can use regex along with #Path annotation defining what is going to be accepted by a String parameter. Unfortunately, your URL would be composed by unvalid characteres (like the brackets []), which means your server is going to return a 500 error.
One alternative for this is if you "replace" this chars for valid ones (like underscore character, e.g.):
/mappings/columns_1_=0&columns_1__name_=
This way, the solution can be applied with no worries:
#GET
#Path("/{columns: .*}")
public Response getMappings(#PathParam("columns") String columns) {
return Response.status(200).entity(columns).build();
}
// Result: columns_1_=0&columns_1__name_=
A much better way to do this is through UriInfo object, as you may have tried. This is simpler because there's no need to change the URL and params. The object has a getQueryParameters() that returns a Map with the param values:
#GET
public Response getMappings(#Context UriInfo uriInfo) {
MultivaluedMap<String, String> queryParams = uriInfo.getQueryParameters();
// In case you want to get the whole generated string
String query = uriInfo.getRequestUri().getQuery();
String output = "QueryParams: " + queryParams
+ "<br> Keys: " + queryParams.keySet()
+ "<br> Values: " + queryParams.values()
+ "<br> Query: " + query;
return Response.status(200).entity(output).build();
}
// URL: /mappings?columns[1][name]=0&columns[0][searchable]=false
/* Result:
* QueryParams: {columns[0][searchable]=[false], columns[1][name]=[0]}
* Keys: [columns[0][searchable], columns[1][name]]
* Values: [[false], [0]]
* Query: columns[1][name]=0&columns[0][searchable]=false
*/
However, you must be aware that if you follow this approach (using a Map) you can't have duplicated keys, once the structure doesn't support it. That's why I include the getQuery() option where you get the whole string.
A last possibility is creating a InjectableProvider, but I can't see many diffs to the getQuery() strategy (since you can split it and create your own map of values).

Spring MVC: Dynamic Param Name or Param Name using Regex?

I'm trying out this grid component called jQuery Bootgrid. In AJAX mode, it POSTs parameters to the server and the one related to sorting is sent like this:
sort[colname]=desc
The colname part changes depending on how you sort the grid.
Is there any way in Spring MVC using #RequestParam to capture that sort param?
For example, something like:
#RequestParam("sort[{\\*}]") Map<String, String> sort
That's just a wild guess and I doubt there is any clean way to do it. Any suggestions on how to handle it would be great.
Update: Also tried this simpler version which I actually thought might work
#RequestParam("sort") Map<String, String> sort
See on bootgrid forum: https://github.com/rstaib/jquery-bootgrid/issues/111
It is really silly but because cannot parse dynamic parameter on server side, you need to create new request parameters from the sort parameter by defining requestHandler in your bootgrid configuration in the following way:
requestHandler: function (request) {
if (request.sort) {
request.sortBy = Object.keys(request.sort)[0]; //this only gets first sort param
request.sortDir = request.sort[request.sortBy];
delete request.sort
}
return request;
}
And in Spring Controller:
#RequestParam(value = "sortBy", required = false) final String sortBy,
#RequestParam(value = "sortDir", required = false) final String sortDir
Do not forget to mark these parameters as not required because sort is not always posted to server side.

How to pass #RequestParam and #ModelAttribute in one form

I would like to mix #RequestParam and #ModelAttribute in one spring form and controller.
Things that I do in my controller are as follows:
#RequestMapping("/user/edit/{userId}")
public ModelAndView editUser(#PathVariable String userId,
#ModelAttribute User user, BindingResult bindingResult,
#RequestParam Set<String> groups) {
if(bindingResults.hasErrors() {
//return back to form and correct errors
} else {
//save data and get out of form
}
}
There are simple User bean (id, firstName, lastName, etc) but without "groups" property.
There is also simple Group bean (id, name, description) but without any connection with User.
So on the logical level User and Group are totally separeted.
In my form, when editing User, there is a html SELECT element that contains ids of all groups. You can select multiple ids together with filling User data and send it to controller.
Until now, everything works pretty well. I get #ModelAttibute User properly filled. I also get #RequestParam Set[String] groups filled with selected ids (Strings).
Now I would like to do something more. I need to write something that instead of #RequestParam Set[String] groups will give me #RequestParam Set[Group] groups. Of course I can convert it directly in my controller method editUser(...) but it's not a nice solution. So I decided to write a custom #InitBinder method that will do it for me nicely and smoothly.
And tere the problem comes.
I wrote the initBinder as follows:
[...]
webDataBinder.registerCustomEditor(Set.class, "groups", new CustomCollectionEditor(Set.class) {
#Override
protected Object convertElement(Object element) {
if (element instanceof GroupName) {
return element;
}
if (element instanceof String) {
Group group = userCatalog.loadGroup((String) element);
return group.getGroupName();
}
return null;
}
});
[...]
The problem is that there is absolutely no way to call this method. I put inside that some "debugging" System.out.println(...) but it doesn't print anything.
My question is: how to write a correct InitBinder that will be invoked in response to Strings array from html OPTION element and convert it to my Set of Users.
I changed some brackets to this [ and ] because it wasn't displayed.
Thank you for help.
meanwhile I came across on this topic: Custom property editors do not work for request parameters in Spring MVC?
and I think this is a key to the solution. In fact, as javadocs says, "field" argument is necessary only when using ModelAttibute (or form backing object which is the same...), but not for RequestParam. So is there any solution to use InitBinder together with RequestParam? Or I need to write a direct method in controller that converts ids to Set of Group objects?
Try registering your custom editor just against the Set.class type and leaving out the field name to see if that works.
webDataBinder.registerCustomEditor(Set.class, new CustomCol...
I have a suspicion that the field parameter denotes the property name of a ModelAttribute and not the request parameter name.
Alternatively, you could use a wrapper view model and use a second ModelAttribute parameter,
class GroupsViewModel {
private List<Group> groups
...
}

Unprecise number of POST parameters to controller

I have a serach form of 2 types:
With pre-defined parameters count.
With unprecise parameters count, meaning that for each different entry type in my DB i will have different searchForm(which will consists of textfields mostly).
If the form number 1 is not so hard to do, then form number 2 is quite confusing
P.S. Currently I understand that this kind of form might be implemented with some kind of AJAX request, where JS transform data into JSON, but how to do it with regular post?
You can use the params map in the Controller to retrieve the values. See the documentation. For example:
public static void search() {
String name = params.get("name");
String[] tags = params.getAll("tags");
// ... etc ...
}

Categories

Resources