Spring MVC #PathVariable gives 404 error - spring

I have been searching for the answer and many answers didn't solve my problem even though they solved very similar problems.
So My problem is this : I have a path variable which may contain character "/". The same value also contains other special characters such as "." "+" "=" etc .basically all valid Base64 characters.
But Spring MVC throws 404 with logs saying no handler found. I tried using regular expressions in path variable as well but to no avail. so below id my code snippets :
http://localhost:8080/sale/public/viewSaleDetails/b91a03730a746a2b27e1c7bbbd94ddf6a9df593301cd96c606348df5eed235da.FkJJbOqEM8Xvhffe6FwUdQ8/mMCD4+fxpY7w5L9kbJ8=
is my URL. If you see it has / in path variable value. along with "." and "+" and "=". Spring maps this correctly if I remove / between character "m" and "8". but with / in value it just doesnt work. I tried a lot of things including character encoding filter,regex in pathvariable etc. Please help.
Also I dont want to use request parameters as far as possible.
#RequestMapping(value = "/public/viewSaleDetails/{id}", method = RequestMethod.GET)
is my mapping. Also the url is hit from the browser as it is without any URL encoding. I tracked it on browser network bar and it doesnt encode it as expected. I am using Spring 4.2.8 RELEASE version with java 8 and tomcat 8

There is open issue in spring Jira according matching slashes in path. And due to discussion it is not reasonable to change mathing strategy on framework level. The issue was created due to this stackoverflow post and I suggest creating value resolver according to the answer.
Here is example code for such resolver:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.List;
#SpringBootApplication
public class SampleSpringApp {
public static void main(String[] args) {
SpringApplication.run(SampleSpringApp.class, args);
}
}
#RestController
class SampleController {
#RequestMapping("/records/**")
public String getId(Id id) {
return id.id;
}
}
#Configuration
#EnableWebMvc
class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new IdResolver());
}
}
class IdResolver implements HandlerMethodArgumentResolver {
#Override
public boolean supportsParameter(MethodParameter parameter) {
return Id.class.isAssignableFrom(parameter.getParameterType());
}
#Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
String basePath = ((String) webRequest.getAttribute(
HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE,
RequestAttributes.SCOPE_REQUEST
)).replace("**", "");
String id = ((String) webRequest.getAttribute(
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE,
RequestAttributes.SCOPE_REQUEST
)).replace(basePath, "");
return new Id(id);
}
}
class Id {
public final String id;
Id(String id) {
this.id = id;
}
}

Related

Is it possible to set the default date format in JSON-B (Yasson) globally, instead of adding an annotation on every property?

I have using Jersey so far and I am doing my first implementation with JSON-B.
I am using Payara, so I working with Jersey and Yasson. I had an issue, because the serialized dates would always contain the "[UTC]" suffix.
I have managed to use an annotation on my date property, in my DTO. But I would like to configure that globally (in the JAX-RS application config?), instead of repeating myself on every date property. Is that possible? I haven't found anything so far...
Side question: I assume that it is possible to get rid of this "[UTC]" suffix, since it breaks all clients trying to parse the date. Any idea?
Thanks to this Github issue, I was able to solve my problem. Here is what I ended up writing in my code:
JSONConfigurator.java:
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbConfig;
import javax.json.bind.config.PropertyNamingStrategy;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
#Provider
public class JSONConfigurator implements ContextResolver<Jsonb> {
#Override
public Jsonb getContext(Class<?> type) {
JsonbConfig config = getJsonbConfig();
return JsonbBuilder
.newBuilder()
.withConfig(config)
.build();
}
private JsonbConfig getJsonbConfig() {
return new JsonbConfig()
.withDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", null);
}
}
And:
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
import java.util.HashSet;
import java.util.Set;
#ApplicationPath("/api")
public class ApplicationConfig extends Application {
#Override
public Set<Class<?>> getClasses() {
Set<Class<?>> resources = new HashSet<Class<?>>();
addRestResourceClasses(resources);
resources.add(JSONConfigurator.class);
return resources;
}
private void addRestResourceClasses(Set<Class<?>> resources) {
...
}
}

How to make Spring boot CSV message converter display CSV inline and not download when using a browser

I created a spring starter project in eclipse . Most of the code was from this link https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/csv-msg-converter.html.
I added content negotiation configuration to accept headers, path extension and parameters. It works great from postman.
But when I try in a browser http://localhost:8080/employeelist.csv. In all the cases CSV is getting downloaded in a file. I want it displayed inline on the browser. I tried to set content disposition as inline in Request mapping, http output message header but still CSV is always getting downloaded.
What should I be doing to get csv displayed inline? I had previously successfully displayed CSV inline in a browser by having separate request mapping method for CSV and make the method return void and accept httpservletresponse as parameter. But I want to use content negotiation and a single method for all formats - XML, CSV, json. Whatever format selected should be displayed inline in the browser.
Is that possible ?
Thanks a lot for your time.
Update : added portions of code which were edited
package ti.projects;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.List;
#SuppressWarnings("deprecation")
#EnableWebMvc
#Configuration
#ComponentScan("ti.projects")
public class AppConfig extends WebMvcConfigurerAdapter {
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new CsvHttpMessageConverter<>());
}
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(true).favorParameter(true).parameterName("mediaType").ignoreAcceptHeader(false)
.useJaf(false).mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("csv", new MediaType("text", "csv"));
}
}
package ti.projects;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.Arrays;
import java.util.List;
#Controller
public class ExampleController {
#RequestMapping(
value = "/newEmployee",
consumes = "text/csv",
produces = MediaType.TEXT_PLAIN_VALUE,
method = RequestMethod.POST)
#ResponseBody
#ResponseStatus(HttpStatus.OK)
public String handleRequest (#RequestBody EmployeeList employeeList) {
System.out.printf("In handleRequest method, employeeList: %s%n", employeeList.getList());
String s = String.format("size: " + employeeList.getList().size());
System.out.println(s);
return s;
}
#RequestMapping(
value = "/employeeList",
produces = {"text/csv", "application/json"},
method = RequestMethod.GET
)
#ResponseBody
#ResponseStatus(HttpStatus.OK)
public EmployeeList handleRequest2 () {
List<Employee> list = Arrays.asList(
new Employee("1", "Tina", "111-111-1111"),
new Employee("2", "John", "222-222-2222")
);
EmployeeList employeeList = new EmployeeList();
employeeList.setList(list);
return employeeList;
}
}
package ti.projects;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class ContentNegotiationApplication {
public static void main(String[] args) {
SpringApplication.run(ContentNegotiationApplication.class, args);
}
}
The browser (should) use the provided mime type to decide how to display or process the response. What should work is using a MIME of text/plain to let the browser render the received content as text.
You can set the MIME type of your response in your spring Controller like this:
#GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
public String renderCsv() {...}
If you want to offer different MIME types with one method you have three options:
Use query parameter (e.g. ...?contentType=json)
Use path parameter (e.g..../{contentType})
Use accept header of client (preferably?)
You can register different MessageConverter for each contentType and configure a ContentNegotiationConfigurer to automatically choose the correct converter depending on given MIME type and your preferences.
I'll try to attach an example tonight.

Binding snake_case request parameters to a Spring form

I'm implementing a simple RESTful service using Spring Boot, with the interface defined by a .NET (I think) client. Their parameter names are snake_case, rather than camelCase, which obviously means I need to customise how they are mapped.
In the case of JSON input/output, that's fine, I've just customised the ObjectMapper, like so:
#Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
return objectMapper;
}
That works fine. Now my problem is form data. I have a Spring form like:
public class MyForm {
private String myValue;
public String getMyValue() {return myValue;}
public void setMyValue(String myValue) {this.myValue = myValue;}
}
But the requests I need to accept will look like:
POST /foo/bar HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
my_value=5
I feel like there must be some simple hook into Spring's binding, like the equivalent setting in Jackon's ObjectMapper, but I'm struggling to find one. The only somewhat-relevant post I can find on here is this one, about completely changing the parameter names, which has some suggestions that seem like overkill for my use case.
The simple solution is simply to use snake case for the fields in MyForm, which works fine, but is a bit ugly.
A final suggestion I've seen elsewhere is to use an interceptor to modify the request parameters on the way in, which seems like it would be straightforward but it feels like there are bound to be exceptions that make it non-trivial, and I'm concerned that having code hidden away in an interceptor makes it really hard to find when you hit the one obscure case where it doesn't work.
Is there some 'proper' Spring-y way of handling this that I'm missing, or do I just need to pick one of the above not-quite-perfect solutions?
probably you already have solved this issue, I was fighting with this today and answered a question on StackOverflow PT.
So here is the deal:
Create a filter to be executed before the request reach the controller, and format the parameters accordingly (from snake case to camel case on my scenario).
Talk is cheap, show me the code!
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.OncePerRequestFilter;
import com.google.common.base.CaseFormat;
#Configuration
public class AppConfig {
#Bean
public Filter snakeConverter() {
return new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final Map<String, String[]> formattedParams = new ConcurrentHashMap<>();
for (String param : request.getParameterMap().keySet()) {
String formattedParam = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, param);
formattedParams.put(formattedParam, request.getParameterValues(param));
}
filterChain.doFilter(new HttpServletRequestWrapper(request) {
#Override
public String getParameter(String name) {
return formattedParams.containsKey(name) ? formattedParams.get(name)[0] : null;
}
#Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(formattedParams.keySet());
}
#Override
public String[] getParameterValues(String name) {
return formattedParams.get(name);
}
#Override
public Map<String, String[]> getParameterMap() {
return formattedParams;
}
}, response);
}
};
}
}
The snakeConverter do the magic.
In there, the doFilterInternal is executed always before the request reach the controller, the parameters are stored in a new Map in their formatted form, and are forwarded to the controller through the filterChain.doFilter.
The HttpServletRequestWrapper do the job of provide our new parameters to the controller.
This code is completely based on the azhawkes filter.
Testing it using a simple controller in the the following URL: http://localhost:8080/snakecase?foo_bar=123

Override default dispatcherServlet when a custom REST controller has been created

Following my question here, I have succeded in creating a custom REST controller to handle different kinds of requests to /api/urls and operate accordingly.
However, there is still a default controller handling requests at /urls which affects my application: When receiving a request that is not /api/something, it should fetch my database for the URL linked to said /whatever and redirect the user there. Moreover, under /api/urls I've developed certain validation rules to ensure integrity and optimization of the requests, which does not jhappen in /urls so anyone could insert any kind of data into my database.
What would be a possible way to disable this default handler? Seeing the logs I headed to register my own ServletRegistrationBean as instructed here but this is for having two isolated environments as far as I understand
My goal is to simply "disconnect" /urls URL from the default REST controller -which is no longer of any use to me now that I have my own one- and just use the custom one that I implemented in /api/urls (Or whatever other URL I may decide to use such as "/service/shortener* if possible)
Below are my Java classes:
Url.java (getters and setters omitted for brevity):
#Document
public class Url {
#Id private String id;
private String longURL;
private String hash;
private String originalUrl;
private String shortUri;
private Date creationDate;
}
UrlRepository.java
import org.springframework.data.mongodb.repository.MongoRepository;
public interface UrlRepository extends MongoRepository<Url, String> {
// Empty
}
UrlController.java:
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/api/urls")
public class UrlController {
#Autowired
private UrlRepository repo;
#RequestMapping(method=RequestMethod.GET)
public List<Url> getAll() {
System.out.println("Showing all stored links");
List<Url> results = repo.findAll();
return results;
}
#RequestMapping(method=RequestMethod.GET, value="{id}")
public Url getUrl(#PathVariable String id) {
System.out.println("Looking for URL " + id);
return null;
}
#RequestMapping(method=RequestMethod.POST)
public Url create(#RequestBody Url url) {
System.out.println("Received POST " + url);
return null;
}
#RequestMapping(method=RequestMethod.DELETE, value="{id}")
public void delete(#PathVariable String id) {
//TBD
}
#RequestMapping(method=RequestMethod.PUT, value="{id}")
public Url update(#PathVariable String id, #RequestBody Url url) {
//TBD
}
}
Application.java:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Instead of trying to hack your way around Spring Boot and Spring Data REST I strongly suggest to work WITH the frameworks instead of around them.
To change the default context-path from / to /api simply add a property to your application.properties file.
server.context-path=/api
Now you would need to change your controller mapping to /urls instead of /api/urls.
If you only want /api for Spring Data REST endpoints use the following property
spring.data.rest.base-uri=/api
This will make all Spring Data REST endpoints available under /api. You want to override the /urls so instead of using #Controller use #RepositoryRestController this will make your controller override the one registered by default.
#RepositoryRestController
#RequestMapping("/urls")
public class UrlController { ... }

Customizing PropertyEditorSupport in Spring

I'm working with Spring 3.2. In order to validate double values globally, I use CustomNumberEditor. The validation is indeed performed.
But when I input a number like 1234aaa, 123aa45 and so forth, I expect the NumberFormatException to be thrown but it doesn't. The docs says,
ParseException is caused, if the beginning of the specified string cannot be
parsed
Therefore, such values as mentioned above are parsed up to they are represented as numbers and the rest of the string is then omitted.
To avoid this, and to make it throw an exception, when such values are fed, I need to implement my own Property Editor by extending the PropertyEditorSupport class as mentioned in this question.
package numeric.format;
import java.beans.PropertyEditorSupport;
public final class StrictNumericFormat extends PropertyEditorSupport
{
#Override
public String getAsText()
{
System.out.println("value = "+this.getValue());
return ((Number)this.getValue()).toString();
}
#Override
public void setAsText(String text) throws IllegalArgumentException
{
System.out.println("value = "+text);
super.setValue(Double.parseDouble(text));
}
}
The editors I have specified inside a method annotated with the #InitBinder annotation are as follows.
package spring.databinder;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.Format;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.propertyeditors.CustomNumberEditor;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.context.request.WebRequest;
#ControllerAdvice
public final class GlobalDataBinder
{
#InitBinder
public void initBinder(WebDataBinder binder, WebRequest request)
{
DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
dateFormat.setLenient(false);
binder.setIgnoreInvalidFields(true);
binder.setIgnoreUnknownFields(true);
//binder.setAllowedFields("startDate");
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true));
//The following is the CustomNumberEditor
NumberFormat numberFormat = NumberFormat.getInstance();
numberFormat.setGroupingUsed(false);
binder.registerCustomEditor(Double.class, new CustomNumberEditor(Double.class, numberFormat, false));
}
}
Since I'm using Spring 3.2, I can take advantage of #ControllerAdvice
Out of curiosity, the overridden methods from the PropertyEditorSupport class in the StrictNumericFormat class are never invoked and the statements that redirect the output to the console as specified inside of those methods (getAsText() and setAsText()) don't print anything on the server console.
I have tried all the approaches described in all the answers of that question but none worked for me. What am I missing here? Is this required to configure in some xml file(s)?
Clearly you have nowhere passed the StrictNumericFormat reference. You should register your editor like:
binder.registerCustomEditor(Double.class, new StrictNumericFormat());
BTW Spring 3.X introduced a new way achieving conversion:Converters

Resources