Spring: #PathVariable from multiple path elements like with jax-rs? - spring

I'm migrating some routes from a jax-rs based application to SpringBoot. In jax-rs I can use #Path to define a regex that contains multiple URL path elements:
#Path("{id:[^/]+/y[\d]{4}/m[\d]{1,2}/d[\d]{1,2}/h[\d]{1,2}}/")
The id variable in the method body will then be the matching segment of the URL and I can go about my day.
With #RequestMapping in Spring this doesn't work. As soon as you put a forward slash into the regex you get a PatternParseException.
PathContainer pathContainingSlash = PathContainer.parsePath("/api/test/y1978/m07/d15");
PathPatternParser parser = new PathPatternParser();
assertThrows(PatternParseException.class, () ->
parser.parse("/api/test/{ticketId:y[\\d]{4}/m[\\d]{1,2}/d[\\d]{1,2}}"));
This same problem appears to happen with AntPathMatcher.
AntPathMatcher antPathMatcher = new AntPathMatcher();
assertThrows(IllegalStateException.class, () ->
antPathMatcher.extractUriTemplateVariables(
"/api/test/{ticketId:y[\\d]{4}/m[\\d]{1,2}/d[\\d]{1,2}}",
"/api/test/y1978/m07/d15"));
This is a problem because I have about 78 of these URL patterns. I'm going to have to define each pattern individually with each path element being a separate variable. Then I'm going to have to use String concatenation to combine them back together in the format of a path.
#GetMapping("/{year:y[\\d]{4}}/{month:m[\\d]1,2}/{day:d[\\d]{1,2}")
public ResponseEntity<Void> foo(#PathVariable String year,
#PathVariable String month,
#PathVariable String day) {
String date = year + "/" + month + "/" + day;
}
Other than using Jax-rs in my SpringBoot app, is there accomplish this? It's possible to write them all like this but it seems sub-optimal.
For clarity, I really want a way to extract multiple path elements from a URL into a #PathVariable. I would like something like this:
#GetMapping("/api/test/{date:y[\\d]{4}/m[\\d]{1,2}/d[\\d]{1,2}}")
public ResponseEntity<Void> foo(#PathVariable String date) {}
So that date is now equal to y1978/m07/d15
Also, this is just one example pattern. There are 78 unique patterns, that have a varying number of a path elements and contents of the elements. In Jax-RS using #Path I can OR these regexes together and create one route and the path variable is accessible inside the method.

how about adding spring-boot-starter-validation for validation
requires addition of following jar
org.springframework.boot:spring-boot-starter-validation
add #org.springframework.validation.annotation.Validated on top of controller class
add #javax.validation.constraints.Pattern with regex attribute to the #PathVariable method params
#GetMapping("{year}/{month}/{day}")
public ResponseEntity<Void> foo(
#PathVariable #Pattern(regexp = "[\\d]{4}", message = "year must be ..") String year,
#PathVariable #Pattern(regexp = "[\\d]{1,2}", message = "month must ..") String month,
#PathVariable #Pattern(regexp = "[\\d]{1,2}", message= "day must be ..") String day) {
String date = year + "/" + month + "/" + day;
to return http 400 status, add a method to handle the ConstraintViolationException
#ExceptionHandler(value = { ConstraintViolationException.class })
protected ResponseEntity<List<String>> handleConstraintViolations(ConstraintViolationException ex, WebRequest request) {
List<String> errorMessages = ex.getConstraintViolations().stream()
.map(violation -> violation.getMessage()).collect(Collectors.toList());
return new ResponseEntity<List<String>>(errorMessages, HttpStatus.BAD_REQUEST);
}
more validation examples here:
https://reflectoring.io/bean-validation-with-spring-boot/
more exception handling options here:
https://www.baeldung.com/exception-handling-for-rest-with-spring

a possible option using path rewrite from this thread
Spring MVC Getting PathVariables containing dots and slashes
add
<dependency>
<groupId>org.tuckey</groupId>
<artifactId>urlrewritefilter</artifactId>
<version>4.0.3</version>
</dependency>
add rewrite rules to src/main/webapp/WEB-INF/urlrewrite.xml
<urlrewrite>
<rule>
<from>^/api/test/(y[\d]{4})/(m[\d]{2})/(d[\d]{2})$</from>
<to>/api/test?date=$1/$2/$3</to>
</rule>
</urlrewrite>
create controller methods matching the to path of the rewrite rule with query params
#GetMapping("/api/test")
public ResponseEntity<Void> foo(#RequestParam String date) {
System.out.println(date);
return new ResponseEntity<Void>(HttpStatus.OK);
}
add configuration class to register the rewrite filter with urlPatterns to filter
#Configuration
public class FiltersConfig {
#Bean
public FilterRegistrationBean<Filter> someFilterRegistration() {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<Filter>();
registration.setFilter(rewriteFilter());
// add paths to filter
registration.addUrlPatterns("/api/*");
registration.setName("urlRewriteFilter");
registration.setOrder(1);
return registration;
}
public Filter rewriteFilter() {
return new UrlRewriteFilter();
}
}

Related

REST API design - optional request parameters

I've written this request mapping to access a ticket by it's id:
#GetMapping(path = "/tickets/{ticketId}")
#ResponseStatus(value = HttpStatus.OK)
public ResponseEntity<List<TicketResponse>> getTicketsById(#PathVariable("ticketId") final Long ticketId
I'm planning to add multiple query parameters to support filtering such as ticketType, ticketStatus. REST API users should have options to filter on any or all of the query parameters.
What are the REST API design principles to achieve this ? Should I add new request parameters to support the filtering like below ? :
#GetMapping(path = "/tickets/{ticketId}")
#ResponseStatus(value = HttpStatus.OK)
public ResponseEntity<List<TicketResponse>> getTicketsById(#PathVariable("ticketId") final Long ticketId, #RequestParam("ticketType") final String ticketType, #RequestParam("ticketStatus") final String ticketStatus)
Is there a Spring design pattern for this scenario ? The Java builder pattern could be used where parameter an attribute of a QueryParameter object ?
You basically have two options:
Either you put all your RequestParams as method parameters, but with required=false, like #AmitKumar wrote. Example: #RequestParam(name="ticketType", required = false) String ticketType
Put all these parameters into a, let's say FilterDTO and have that as a parameter (FilterDTO filter). Spring will make sure to populate its fields with your RequestParams . Just put your ticketType and other parameters into the DTO as fields, and they will be optional. Example: public ResponseEntity<List<TicketResponse>> getTicketsById(#PathVariable("ticketId") ong ticketId, FilterDto filter){}
If you want to make parameter as an optional. you need to add required=false.
public ResponseEntity<List<TicketResponse>> getTicketsById(#PathVariable("ticketId") final Long ticketId, #RequestParam(name="ticketType", required=false) final String ticketType, #RequestParam(name="ticketStatus",required=false) final String ticketStatus)

Issue with Spring Rest #RequestMapping when negating params

I have two spring controller methods :
#RequestMapping(value="/requestotp",method = RequestMethod.POST,params = "!applicationId") //new customer
public OTPResponseDTO requestOTP( #RequestBody CustomerDTO customerDTO){
return customerService.requestOTP(customerDTO);
}
#RequestMapping(value="/requestotp",method = RequestMethod.POST,params = {"idNumber","applicationId"}) //existing customer
public String requestOTP( #RequestParam(value="idNumber") String idNumber , #RequestParam(value="applicationId") String applicationId) {
return customerService.requestOTP(idNumber, applicationId);
}
using "!applicationId" , I am expecting that when I call the url with applicationId parameter there that the second method will be called , but actually when I pass a request like this :
{"idNumber":"345","applicationId":"64536"}
The first method gets called
This is the part of the params paremeters documentation that I rely on :
Finally, "!myParam" style expressions indicate that the specified
parameter is not supposed to be present in the request.
Can't you just simply delete first request params?
#RequestMapping(value="/requestotp",method = RequestMethod.POST) //new customer
public OTPResponseDTO requestOTP( #RequestBody CustomerDTO customerDTO){
return customerService.requestOTP(customerDTO);
}
The issue actually wasn't with negating the parameter, the issue was that I was sending {"idNumber":"345","applicationId":"64536"} in the POST body and I was expecting the variables to be mapped to the method parameters annotated with #RequestParam ... this is not correct ... #RequestParam only map URL parameters .... so the controller was trying to find the best match so it was using the first method as it contained #RequestBody

spring mvc truncating special symbols

We have an application created in spring-boot. We have a rest controller in place as below
#RequestMapping(value = "getDataFromSpaceForTypeForSpaceId/{gridName}/{spaceName}/{dataType}/{spaceId}", method = GET, produces = "application/json")
public DetailedJsonView getDataFromSpaceForTypeForSpaceId(#PathVariable String spaceId,
#PathVariable String dataType,
#PathVariable String gridName,
#PathVariable String spaceName) throws Exception {
Object detailedObject = spaceAccessorService.getDetailedDataFromSpaceForTypeNameWithSpaceId(gridName, spaceName, dataType, spaceId);
String detailedXml = ObjectMarshallarService.marshal(detailedObject);
return new DetailedJsonView(detailedXml);
}
for the urls like this it works fine
http://localhost:8080/rest/query/getDataFromSpaceForTypeForSpaceId/COLO/gcmmprivateSpace/com.ambuj.ValuationSliceRun/InterestRates
for the urls like this the last variable spaceId
http://localhost:8080/rest/query/getDataFromSpaceForTypeForSpaceId/COLO/gcmmprivateSpace/com.ambuj.ValuationSliceRun/InterestRates#MUREX:RATES:EMEA:2015-01-01
Now what we see on the above url is that spaceId comes as InterestRates instead of InterestRates#MUREX:RATES:EMEA:2015-01-01.
I am not sure how i can resolve that

how to configure spring-data-rest search method path with #PathVariable

I want to customize my spring-data-rest search method path by passing parameter as a path variable like follows
http://localhost:8080/orders/search/customers/{customerId}
findByCustomer(#PathVariable("customerId") Integer customer);
The search resource listh the links as follows
http://localhost:8080/orders/search/customers/%7BcustomerId%7D
How to expose search url with path params?
You can use custom handler similar to this:
#RepositoryRestController
public class OrderController {
#Autowired
OrderRepository orderRepository;
#GetMapping("/orders/search/customers/{id}")
public #ResponseBody ResponseEntity<?> getByCustomers(#PathVariable Integer customer) {
Order order = orderRepository.findOne(id);
if(order == null) return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
Resource<Order> resource = new Resource<Order>(order);
return ResponseEntity.ok(resource);
}
}
More about this can be found here.
Use HttpServletRequest to get the request url:
findByCustomer(#PathVariable("customerId") Integer customer, HttpServletRequest request){
String request = request.getRequestURL().toString(); // StringBuffer, so use append if you want to...
[...]
}
also you can use request.getQueryString() to get the query part after ?.

RequestParam value in spring MVC to be case insensitive

Am sending data from JSP to controller using query string.
My controller is annotation driven.
The value of the the request parameter should be case-insensitive.
The method which i use for welcome page is
public String welcome(#RequestParam("orgID") String orgID, ModelMap model)
The request parameter "orgID" should be case insensitive. How to do this ?.
I should be able to give the query-string as "orgid" or "orgId". The parameter should be completely case-insensitive. Looking for your help friends.
Thanks in Advance :-)
Another approach would be to have two parameters "orgId" and "orgid" and have the optional.
public String welcome(#RequestParam(value="orgID", required = false) String org_ID, #RequestParam(value="orgid", required=false, String orgid, ModelMap model) {
final String orgId = orgid == null ? org_ID : orgid;
...
}
But if you have control over the parameters I would strongly prefer to just have one consistent way, say org-id and follow it both in the client and the server side.
It might be nice for Spring to support this, but I can also understand why they wouldn't since it could result in a weakening of an interface contract between a supplier and consumer. In the meantime, here's a fairly straightforward way to take the first value using the parameter name regardless of its case.
String myVal = null;
for (String pname : request.getParameterMap().keySet() ) {
if ( pname != null && pname.toLowerCase().equals("your_lc_pname") ) {
for (String v : request.getParameterValues(pname) ) {
// do something with your value(s)
}
}
}
in Spring 4.2+
#Configuration
#EnableWebMvc
#ComponentScan(basePackages="org.saat")
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void configurePathMatch(PathMatchConfigurer configurer) {
AntPathMatcher matcher = new AntPathMatcher();
matcher.setCaseSensitive(false);
configurer.setPathMatcher(matcher);
}
#Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new
InternalResourceViewResolver();
viewResolver.setPrefix("/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
You'll have to try changing the way Spring matches your urls . You could for one, create a filter (probably a DelegatingFilterProxyBean) to lower case your parameter before you pass it on to Spring or try to change the way the paths are matched .
An explanation to the second options is given at How can I have case insensitive URLS in Spring MVC with annotated mappings .
The simplest way I found was to change the controller method to take a Map with a single key value pair as a parameter and then convert the key to lowercase.
public String welcome(#RequestParam(required = true) Map<String, String> params, ModelMap model) {
String caseInsensitiveOrgId = params.keySet().toArray()[0].toString().toLowerCase();
String OrgValue = params.values().toArray()[0];
// Do whatever you want here
}
There is a simple workaround.You can operate directly on the HttpServletRequest and use method getParameter() and check all versions of parameter.
public String welcome(HttpServletRequest request, ModelMap model){
String orgID = extractOrgId(request);
//rest of your code
}
private String extractOrgId(HttpServletRequest request){
if(request.getParameter("orgId") != null){
return request.getParameter("orgId");
}
// and so on
}

Resources