Arbitrary name for RESTEasy QueryParam? - java

Is there a way to get any query parameter without explicitly declaring its name using the #QueryParam annotation?
I have a string which may have tokens like {animal}, which are then replaced by the query parameter posted, e.g. ?animal=fox, but I want the name of the token to be configurable as well. Ideally I would like to be able to do something like this:
for (QueryParam param : queryParams) {
text = text.replaceAll("{" + param.key + "}", param.value);
}

(I'm guessing you are talking about writing a resource to accept a query string correct?)
In which case, in your method, you can use #Context to get a UriInfo object which you an use to obtain a MultivaluedMap of all of your query string parameters.

Related

How to check REST invalid query parameter name with RESTEasy?

How to fail on invalid query parameter name with RESTEasy?
Consider a valid REST request like this one: /list?sort-by=date
Then, user makes this request: /list?sort_by=date
See that user replaced hyphen with underscore. It works, but it will ignore parameter and use default sorting (param not mandatory).
With Jackson, if a JSON with invalid member is sent it throws an Exception. I would like a similar behavior to query params (header params would be awesome too). Tested with #BeanParam, but apparently it doesn't use Jackson in this case.
RESTEasy version 3.15.1.
You have to check that in your code. Query params are not in json in standard, you can do that with a class with string constructor.
In fact "sort_by" is not bind to a method parameter, so it's ignored.
If you want that "sort-by" to be mandatory you have to do that in your code :
Required #QueryParam in JAX-RS (and what to do in their absence)
Currently since RESTEasy is built on top of a Servlet, it does not distinguish between URI query strings or url form encoded parameters. Like PathParam, your parameter type can be an String, primitive, or class that has a String constructor or static valueOf() method.
https://docs.jboss.org/resteasy/docs/3.15.1.Final/userguide/html_single/#_QueryParam

Parsing a request body containing a quoted string as JSON in Spring Boot 2

I have an application which exposes an endpoint accepting PUT requests as a JSON-formatted string, e.g.:
PUT /my/endpoint
"some string"
My endpoint method signature is something like:
#RequestMapping(
path = "/my/endpoint",
consumes = "application/vnd.mycompany.myservice-v1.hal+json"
)
public ResponseEntity<MyResponse> myEndpoint(
#RequestBody final String myString
) {
...
}
Using Spring Boot 1 (1.5.22.RELEASE), the value of myString given the PUT example above would be the literal text some string, but under Spring Boot 2 (2.3.6.RELEASE), it's now the literal text "some string" - i.e. it seems that the input isn't strictly being parsed as JSON because the quotes are not removed.
I believe that quoted strings are valid JSON (and that unquoted strings are not), just as an object (in { and }) and a list (in [ and ]) would be.
I've taken out some extraneous detail that I don't think matters for the problem at hand (e.g. we're using CompletableFuture as a return value, I've got a #PathVariable in there as well and there's some annotation-driven validation going on), but I've left in that we're using a custom media-type in case that has something to do with it.
Any ideas how I can convince Spring Boot 2 to treat my request body as JSON properly? Unfortunately, I can't redefine the API because we already have live customers using it.
This might not be the best option but if nothing else helps at start. Instead of String let Spring handle RequestBody as an Object. So like:
public ResponseEntity<String> myEndpoint(#RequestBody final Object myString)
When using String Spring might not even use Jackson for parsing but handle the body as a String that should have all the characters in the body even content type is set to JSON.
If you do the following:
String myString2 = new ObjectMapper().readValue(myString, String.class);
you can see it resulting into myString2 having not those surrounding double quotes.
For Object Spring seems to handle it differently. It makes it to a String but seems to consider it as a JSON value (and as a String value because having surrounding double quotes) it should not have surrounding double quotes.
If you use Object and then log myString.getClass() you will see it is actually a java.lang.String
You Can create the request object
public class MyObject{
private String myStr;
}
Use the MyObject class as RequestBody object with mediaType="application/json"
In your controller
#RequestMapping(
value= "/my/endpoint", method= RequestMethod.PUT
)
public ResponseEntity<MyResponse> myEndpoint(
#RequestBody final MyObject myObj
)

How to have DeleteMapping in Spring that deletes based on different types of path variables?

I am trying to build a REST application in Spring where I have a requirement to delete resources on the basis of certain path variables.
For example, I want to delete resource(s) by id
#DeleteMapping("resources/{id}")
or by name
#DeleteMapping("resources/{name}")
But when I do the above, I get the error
java.lang.IllegalStateException: Ambiguous handler methods
As I understand, Servlets can't tell whether 123 in path /resources/123 represents an ID or a name and hence the ambiguity.
How shall then I design my REST endpoint where a DELETE happens based on some parameter or maybe a combination of parameters?
Spring can't distinguish between the two request as your mapping is ambiguous.
You could try using a query parameter for the second request. So it could look like following :
#DeleteMapping("/hello/{id}")
public String deleteById(#PathVariable("id") Long id) {
return "Delete by id called";
}
#DeleteMapping("/hello")
public String deleteByName(#RequestParam(value = "name") String name) {
return "Delete by name called";
}
Requests like DELETE http://localhost:8080/hello/1 will be handled by deleteById
Requests like DELETE http://localhost:8080/hello?name=deleteMe will be handled by deleteByName.
Or you could add name query param to the same method and if your query param is not null you could delete by name.
For delete by id mapping is fine using path variable
#DeleteMapping("resources/{id}")
For delete by name you can take name as a query param
#DeleteMapping("resources")
public ResponseEntity<?> delete(#RequestParam(value = "name") String name) {
Or you can take both id and name as query param if you want to map both operations in one method in the controller.
Seems like you have two options:
Use only one mapping and disambiguate the request in your controller code. For example, try to parse the path param as an integer or match it to some pattern.
Use regex in the path pattern like #DeleteMapping("resources/{id:[0-9]+}") See Spring Docs - Request Mapping URI Patterns for details. This assumes you can tell the difference between an ID and a name with a pattern though.

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).

Categories

Resources