Zero length part of URL in Spring controller RequestMapping PathVariable breaks resolution - java

I'm trying to make an app's REST API more RESTful and it feels like I'm not using the Spring RequestMappings in the way intended.
I have a single GET end point for doing reads:
#RequestMapping(value = "thing/{thingName}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public String getThing(
#PathVariable(value = "thingName", required = false)
String thingName,
#RequestParam(value = "findByComponent", required = false)
String findByComponentQuery,
#AuthenticationPrincipal User user) {
...
To be more restful, I want this endpoint to do both:
GET /thing/{thingName} returns a single thing having that name
GET /thing or /thing/ with query params returns lists of things
So in my controller, I can test whether {thingName} is null or zero-length and if so, check the query params for known query names.
However calling this with http://localhost:8080/thing/?findByComponent=component123 returns a 404 from Spring with this logging:
12:45:18.485 PageNotFound : No mapping found for HTTP request with URI [/thing/] in DispatcherServlet with name 'dispatcher' : WARN : XNIO-1 task-3 : org.springframework.web.servlet.DispatcherServlet

Spring does not allow path variables ({thingName}) to be mapped to an empty String. In practice, this means that the URL /thing/?findByComponent=component123 does not map to thing/{thingName} with an empty {thingName}, but rather, it expects there to be some mapping for thing. Since there is no endpoint that maps to the path thing (without the path variable), a 404 error is returned.
To solve this issue, you can break this single endpoint into two separate endpoints:
#RequestMapping(value = "thing/{thingName}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public String getThing(
#PathVariable(value = "thingName") String thingName,
#AuthenticationPrincipal User user) {
// ...
}
#RequestMapping(value = "thing",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public String getThings(,
#RequestParam(value = "findByComponent", required = false) String findByComponentQuery,
#AuthenticationPrincipal User user) {
// ...
}
For more information, see With Spring 3.0, can I make an optional path variable?.
The required=false flag allows for two types of requests:
/thing
/thing/<some_value>
Strictly speaking, including a trailing slash at the end of the URL (i.e. /thing/) means that a decision was made to include a value for the path variable, but none was provided. In the context of REST APIs, /thing and /thing/ are two different endpoints, where the latter means that a value after the trailing slash was expected.
A workaround for not having to create three separate request mappings (one for each case above) is to set the #RequestMapping value for the controller to the base path and then have a "" and "/{thingName} request mapping for the two endpoints:
#RestController
#RequestMapping("thing")
public class ThingController {
#RequestMapping(value = "/{thingName}",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public String getThing(
#PathVariable(value = "thingName") String thingName) {
return "foo";
}
#RequestMapping(value = "",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public String getThings(
#RequestParam(value = "findByComponent", required = false) String findByComponentQuery) {
return "bar";
}
}
In this case, the following mappings will occur:
/thing: getThings
/thing/: getThings
/thing/foo: getThing
An example of this workaround, including test cases can be found here.

Related

Difference between path and value attributes in #RequestMapping annotation

What is the difference between below two attributes and which one to use when?
#GetMapping(path = "/usr/{userId}")
public String findDBUserGetMapping(#PathVariable("userId") String userId) {
return "Test User";
}
#RequestMapping(value = "/usr/{userId}", method = RequestMethod.GET)
public String findDBUserReqMapping(#PathVariable("userId") String userId) {
return "Test User";
}
As mentioned in the comments (and the documentation), value is an alias to path. Spring often declares the value element as an alias to a commonly used element. In the case of #RequestMapping (and #GetMapping, ...) this is the path property:
This is an alias for path(). For example #RequestMapping("/foo") is equivalent to #RequestMapping(path="/foo").
The reasoning behind this is that the value element is the default when it comes to annotations, so it allows you to write code in a more concise way.
Other examples of this are:
#RequestParam (value → name)
#PathVariable (value → name)
...
However, aliases aren't limited to annotation elements only, because as you demonstrated in your example, #GetMapping is an alias for #RequestMapping(method = RequestMethod.GET).
Just looking for references of AliasFor in their code allows you to see that they do this quite often.
#GetMapping is a shorthand for #RequestMapping(method = RequestMethod.GET).
In your case.
#GetMapping(path = "/usr/{userId}") is a shorthand for #RequestMapping(value = "/usr/{userId}", method = RequestMethod.GET).
Both are equivalent. Prefer using shorthand #GetMapping over the more verbose alternative. One thing that you can do with #RequestMapping which you can't with #GetMapping is to provide multiple request methods.
#RequestMapping(value = "/path", method = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT)
public void handleRequet() {
}
Use #RequestMapping when you need to provide multiple Http verbs.
Another usage of #RequestMapping is when you need to provide a top level path for a controller. For e.g.
#RestController
#RequestMapping("/users")
public class UserController {
#PostMapping
public void createUser(Request request) {
// POST /users
// create a user
}
#GetMapping
public Users getUsers(Request request) {
// GET /users
// get users
}
#GetMapping("/{id}")
public Users getUserById(#PathVariable long id) {
// GET /users/1
// get user by id
}
}
#GetMapping is an alias for #RequestMapping
#GetMapping is a composed annotation that acts as a shortcut for #RequestMapping(method = RequestMethod.GET).
value method is an alias for path method.
This is an alias for path(). For example #RequestMapping("/foo") is equivalent to #RequestMapping(path="/foo").
So both methods are similar in that sense.

Remove direct access to "/" on spring boot

I have a spring boot application where i have only 2 endpoints defined.
#RequestMapping(value = "/getAllFeatures", method = RequestMethod.GET)
#Cacheable("features")
public ResponseEntity<?> getAllFeatures() {
...
}
#RequestMapping(name = "/getFeatureStatus", method = RequestMethod.GET)
#Cacheable("features")
public ResponseEntity<?> getFeatureStatus(#RequestParam(value = "groupName", required = false) String groupName,
#RequestParam(value = "featureName", required = false) String featureName) {
...
}
And i have defined the context to be server.context-path=/abc
How the problem is when i do a GET call on /abc/ the application gives me a valid response. Where as i have never mapped "/" in my rest controller. Any ideas on how to block requests to "/". Also this application doesn't require any kind of spring security.
It should be
#RequestMapping(path= "/getFeatureStatus"....)
instead of
#RequestMapping(name = "/getFeatureStatus"....)
the name attribute just assigns a name to this mapping.
Sometimes "/" endpoint is mapped to index.html file of your resource folder.
Does this file exist ?
You also need to annotate your class with #RestController.

Spring path params with multiple slash

I have a Spring boot app where I have an API that takes other urls as path params. For example:
host:port/{eid} is my base path and after this I can have URLs like
host:port/{eid}/abc
host:port/{eid}/abc/pqr/
host:port/{eid}/abc/pqr/b=2
host:port/{eid}/abc/pqr/xyz
host:port/{eid}/abc/pqr/xyz?a=1
...and so on...
I would like to define a controller that I can map to all the above URLs and that should work something like
#RequestMapping(value = "/{eid}/{urlParts}", method = RequestMethod.GET)
public ResponseEntity<Object> share(
#PathVariable String eid,
#PathVariable String urlParts) {
......
}
I tried using #PathVariable Map<String, String> path and also #RequestMapping(value = "/{eid}/{urlParts:.+}"
but couldn't get the expected result.
Is there any solution to receive path slash(/) in path param.
Note: I can not URL encode the slash(/) in the URL. That's not an option for me.
I know the query is too old but still it's useful and this answer can help others.
You can get the full url parts using request attribute as below.
#RequestMapping(value = "/{eid}/**", method = RequestMethod.GET)
public ResponseEntity<Object> share(#PathVariable String eid, HttpServletRequest request) {
Object uriObject = request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
if (null != uriObject) {
String urlParts = uriObject.toString().replaceFirst("^/" eid + "/", "");
}
....
}
why don't you try #RequestParam to take url if you working with jsp or other stuff..
#PathVariable means that the annotated method argument should be extracted from the path of the invoked URL. #RequestParam means that the annotated method argument must be extracted from the request parameters. None of these annotations cause the annotated arguments to be put in the request, session or application scope.
so you use your map also...
${username} means "write the value of the username attribute (found in page, or request, or session, or application scope) in the response". Since you didn't include any username attribute in any of those scopes, it doesn't write anything.
The code would work if the method returned a ModelAndView object, and the model contained a username attribute and a studentid attribute.
you can refer below code and link :
First URL : localhost:8080/projectName/test?firstname=john
Second URL :localhost:8080/projectName/test?firstname=john&secondname=roy
#Controller
#RequestMapping("/test")
public class TestController {
#RequestMapping(value = { "/test/{firstname}/test" }, method = { RequestMethod.GET })
public String someMethod(#PathVariable("firstname") String firstname){
return someMethod(firstValue )
}
#RequestMapping(value = { "/test/{firstname}/{otherString}/test" }, method = { RequestMethod.GET })
public String someOtherMethod(#PathVariable("firstname") String firstname, #PathVariable("secondname") String secondValue) {
return someMethod(firstValue + "/" + secondValue)
}
}
so I am not sure if there is a direct spring implementation to doing this however, you could us a mixture of things.
#RequestParam - returns a map of the URL params (succeeding the ?)
#PathVariable - return the eid
HttpServletRequest - use the request to return the URI and strip host:port/{eid} and anything after ? , then use Arrays.asList(str.split("/")); (remember this is a wrapper of an array use new ArrayList<Sting>(Arrays.asList(str.split("/"))) )
#RequestMapping(value = "/{eid}", method = RequestMethod.GET)
public ResponseEntity<Object> share(
#PathVariable String eid,
#RequestParam Map<String,String> allRequestParams,
HttpServletRequest request) {
......
}

Request Mapping with Multiple Wildcards

I want to have two endpoints that have wild cards in the #RequestMapping
#RequestMapping(value="/**", method = { RequestMethod.GET}, produces = "application/json")
#RequestMapping(value="/**/versions/{versionId}", method = { RequestMethod.GET}, produces = "application/json")
When I perform a request that should go to /**/versions/{versionId} it is preferring the /** endpoint over the /**/versions/{versionId} endpoint even though the request should match.
I am using:
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-parent</artifactId>
<version>Brixton.SR2</version>
</parent>
I think you just have to change the order of your #RequestMapping methods.
For me works http://localhost:8080/versions/1 returns version 1.
And for any other request without version/{versionId} it returns index.
#Controller
public class DemoController {
#RequestMapping(value="/**/versions/{versionId}", method = RequestMethod.GET)
#ResponseBody
public String version(#PathVariable String versionId){
return "version " + versionId;
}
#RequestMapping(value="/**", method = RequestMethod.GET)
#ResponseBody
public String index(){
return "index";
}
}
If you want quite complicated request mapping try to overwrite handleRequest as it is here: How to define RequestMapping prioritization
Than you can:
if (urlPath.contains("/versions")) {
/* forward to method with #RequestMapping(value="/get/versions/{versionId}")
}
Instead of:
#RequestMapping(value="/**/versions/{versionId}", method = { RequestMethod.GET}, produces = "application/json")
use:
#RequestMapping(value="/response_for_versions/{versionId}", method = { RequestMethod.GET}, produces = "application/json")
Now all ".../versions/{versionId}" should be forwarded to "/response_for_versions/{versionId}" and all others would be handled by "/**"

Receive Collection/Iterable as arguments in spring boot controller

I am new to Spring and I want to write a controller which will take Collection/Iterable as arguments. Like this:
#RequestMapping(value = "friends", method = RequestMethod.POST)
public #ResponseBody Callable<Iterable<User>>
getFriendsOfUser(#RequestParam(required = true, name = "mobiles") Iterable<String> mobs) {
// return callable
}
There is no compilation error, but I cannot make it work. Can you say how will this work? And how shall be the request to this api be constructed?
public String getFriendsOfUser(#RequestParam(required = true, value = "mobiles") String[] mobiless){
....
}
and your mobile should be
mobiles=myValue1&mobiles=myValue2&mobiles=myValue3
or
mobiles=myvalue1,myValue2,myValue3
still if you have any doubt post your front-end code and Ajax call.
You've mapped a POST method so you might need #RequestBody instead of #RequestParam
#RequestParam is, as the name implies, for request parameters: [host]/endpoint?param=foo&secondParam=bar
whereas
#RequestBody is for JSON/XML or any other type content sent as the request's body.

Categories