Binding snake_case request parameters to a Spring form - spring

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

Related

Convert JWT Authentication Principal to something more usable in spring

I have a spring boot microservice that validates a JWT (issued by a different service) for authentication. It is working nicely, and I can access the JWT details in my controller like so:
// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
// MyController.java
#RestController
#RequestMapping("/")
public class MyController {
#GetMapping()
public String someControllerMethod(#AuthenticationPrincipal Jwt jwt) {
int userId = Integer.parseInt(jwt.getClaim("userid"));
...
}
}
That works great. I can extract what I need from the JWT and go on to talk to my database with the correct userid etc.
However I find it a bit tedious to have to use the Jwt type to get these values in each controller. Is there a way I can inject a different type as the #AuthenticationPrincipal?
E.g. my own class which has already extracted what is needed from the JWT, and exposes something like .getUserId() that returns an int?
That would also let me centralise the logic of parsing the claims or throwing exceptions if they are not as expected etc.
UPDATE
After more google spelunking, it seems I have two options
Option1: #ControllerAdvice and #ModelAttribute
As explained in this answer. I can do something like:
import com.whatever.CustomPrincipal; // a basic "data" class with some properties, getters, setters and constructor
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
#ControllerAdvice
public class SecurityControllerAdvice {
#ModelAttribute
public CustomPrincipal customPrincipal(Authentication auth) throws Exception {
CustomPrincipal customPrincipal;
if (auth != null && auth.getPrincipal() instanceof Jwt) {
Jwt jwt = (Jwt) auth.getPrincipal();
String sessionId = jwt.getClaimAsString("sessionid");
int userId = Integer.parseInt(jwt.getClaimAsString("userid"));
customPrincipal = new CustomPrincipal(userId, sessionId);
} else {
// log an error and throw an exception?
}
return customPrincipal;
}
}
and then
import com.whatever.CustomPrincipal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController;
#RestController
#ControllerAdvice
public class HelloWorldController {
#GetMapping("/controlleradvice")
public String index(#ModelAttribute CustomPrincipal cp) {
log.info(cp.getUserId());
return "whatever";
}
}
This seems pretty succinct, and neat and tidy. 1 new class with #ControllerAdvice, and bob's your uncle!
Option2: Using jwtAuthenticationConverter()
This answer shows another way to do it, using a "converter", which seems to convert the default Principal from a JWT to a custom object (that extends AbstractAuthenticationToken) that contains the JWT (.getCredentials()) as well as a custom object like CustomPrincipal (or a User class or something).
#EnableWebSecurity
public class SecurityConfig {
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors().disable()
.csrf().disable()
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer().jwt(customizer -> customizer.jwtAuthenticationConverter((new MyPrincipalJwtConvertor())));
return http.build();
}
}
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jwt.Jwt;
public class MyPrincipalJwtConvertor implements Converter<Jwt, MyAuthenticationToken> {
#Override
public MyAuthenticationToken convert(Jwt jwt) {
var principal = new MyPrincipal(Integer.parseInt(jwt.getClaimAsString("userid")), jwt.getClaimAsString("sessionid"));
return new MyAuthenticationToken(jwt, principal);
}
}
#RestController
public class HelloWorldController {
#GetMapping("/converter")
public String converter(#AuthenticationPrincipal MyPrincipal myPrincipal) {
log.info("/converter triggered");
log.info("" + myPrincipal.getUserId());
return "woo";
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
#Data
#AllArgsConstructor
public class MyPrincipal {
private int userId;
private String sessionId;
}
Option 1 is much simpler it seems.
But Option 2 is nice, as, I have Filter's that run to do additional validation (like validate the session id in the JWT). When that filter runs, when it calls SecurityContext.getContext().getAuthentication().getPrincipal(), it will get the MyPrincipal object, and not have to call Jwt.getClaimAsString() and cast it etc.
I guess I am asking, are there pros and cons to these two approaches I have not considered? Is one of them perhaps bastardising/abusing something in a way it is not meant to be?
Or is it much the same and I should select whichever I prefer?

How to set same-site cookie flag in Spring Boot?

Is it possible to set Same-Site Cookie flag in Spring Boot?
My problem in Chrome:
A cookie associated with a cross-site resource at http://google.com/
was set without the SameSite attribute. A future release of Chrome
will only deliver cookies with cross-site requests if they are set
with SameSite=None and Secure. You can review cookies in developer
tools under Application>Storage>Cookies and see more details at
https://www.chromestatus.com/feature/5088147346030592 and
https://www.chromestatus.com/feature/5633521622188032.
How to solve this problem?
Spring Boot 2.6.0
Spring Boot 2.6.0 now supports configuration of SameSite cookie attribute:
Configuration via properties
server.servlet.session.cookie.same-site=strict
Configuration via code
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
#Configuration(proxyBeanMethods = false)
public class MySameSiteConfiguration {
#Bean
public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
return CookieSameSiteSupplier.ofStrict();
}
}
Spring Boot 2.5.0 and below
Spring Boot 2.5.0-SNAPSHOT doesn't support SameSite cookie attribute and there is no setting to enable it.
The Java Servlet 4.0 specification doesn't support the SameSite cookie attribute. You can see available attributes by opening javax.servlet.http.Cookie java class.
However, there are a couple of workarounds. You can override Set-Cookie attribute manually.
The first approach (using custom Spring HttpFirewall) and wrapper around request:
You need to wrap request and adjust cookies right after session is created. You can achieve it by defining the following classes:
one bean (You can define it inside SecurityConfig if you want to hold everything in one place. I just put #Component annotation on it for brevity)
package hello.approach1;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedException;
import org.springframework.stereotype.Component;
#Component
public class CustomHttpFirewall implements HttpFirewall {
#Override
public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
return new RequestWrapper(request);
}
#Override
public HttpServletResponse getFirewalledResponse(HttpServletResponse response) {
return new ResponseWrapper(response);
}
}
first wrapper class
package hello.approach1;
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.http.HttpHeaders;
import org.springframework.security.web.firewall.FirewalledRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* Wrapper around HttpServletRequest that overwrites Set-Cookie response header and adds SameSite=None portion.
*/
public class RequestWrapper extends FirewalledRequest {
/**
* Constructs a request object wrapping the given request.
*
* #param request The request to wrap
* #throws IllegalArgumentException if the request is null
*/
public RequestWrapper(HttpServletRequest request) {
super(request);
}
/**
* Must be empty by default in Spring Boot. See FirewalledRequest.
*/
#Override
public void reset() {
}
#Override
public HttpSession getSession(boolean create) {
HttpSession session = super.getSession(create);
if (create) {
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ra != null) {
overwriteSetCookie(ra.getResponse());
}
}
return session;
}
#Override
public String changeSessionId() {
String newSessionId = super.changeSessionId();
ServletRequestAttributes ra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (ra != null) {
overwriteSetCookie(ra.getResponse());
}
return newSessionId;
}
private void overwriteSetCookie(HttpServletResponse response) {
if (response != null) {
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // there can be multiple Set-Cookie attributes
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // set
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=None")); // add
}
}
}
}
second wrapper class
package hello.approach1;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* Dummy implementation.
* To be aligned with RequestWrapper.
*/
public class ResponseWrapper extends HttpServletResponseWrapper {
/**
* Constructs a response adaptor wrapping the given response.
*
* #param response The response to be wrapped
* #throws IllegalArgumentException if the response is null
*/
public ResponseWrapper(HttpServletResponse response) {
super(response);
}
}
The second approach (using Spring's AuthenticationSuccessHandler):
This approach doesn't work for basic authentication.
In case basic authentication, response is flushed/committed right after controller returns response object, before SameSiteFilter#addSameSiteCookieAttribute is called.
package hello.approach2;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
#Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
addSameSiteCookieAttribute(response); // add SameSite=strict to Set-Cookie attribute
response.sendRedirect("/hello"); // redirect to hello.html after success auth
}
private void addSameSiteCookieAttribute(HttpServletResponse response) {
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // there can be multiple Set-Cookie attributes
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
}
}
}
The third approach (using javax.servlet.Filter):
This approach doesn't work for basic authentication.
In case basic authentication, response is flushed/committed right after controller returns response object, before SameSiteFilter#addSameSiteCookieAttribute is called.
package hello.approach3;
import java.io.IOException;
import java.util.Collection;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
public class SameSiteFilter implements javax.servlet.Filter {
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
addSameSiteCookieAttribute((HttpServletResponse) response); // add SameSite=strict cookie attribute
}
private void addSameSiteCookieAttribute(HttpServletResponse response) {
Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE);
boolean firstHeader = true;
for (String header : headers) { // there can be multiple Set-Cookie attributes
if (firstHeader) {
response.setHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
firstHeader = false;
continue;
}
response.addHeader(HttpHeaders.SET_COOKIE, String.format("%s; %s", header, "SameSite=Strict"));
}
}
#Override
public void destroy() {
}
}
You can look at this demo project on the GitHub for more details on the configuration for org.springframework.security.web.authentication.AuthenticationSuccessHandler or javax.servlet.Filter.
The SecurityConfig contains all the necessary configuration.
Using addHeader is not guaranteed to work because basically the
Servlet container manages the creation of the Session and Cookie. For
example, the second and third approaches won't work in case you return JSON in
response body because application server will overwrite Set-Cookie
header during flushing of response. However, second and third approaches will
work in cases, when you redirect a user to another page after successful
authentication.
Pay attention that Postman doesn't render/support SameSite cookie attribute under Cookies section (at least at the time of writing). You can look at Set-Cookie response header or use curl to see if SameSite cookie attribute was added.
This is an open issue with Spring Security (https://github.com/spring-projects/spring-security/issues/7537)
As I inspected in Spring-Boot (2.1.7.RELEASE), By Default it uses DefaultCookieSerializer which carry a property sameSite defaulting to Lax.
You can modify this upon application boot, through the following code.
Note: This is a hack until a real fix (configuration) is exposed upon next spring release.
#Component
#AllArgsConstructor
public class SameSiteInjector {
private final ApplicationContext applicationContext;
#EventListener
public void onApplicationEvent(ContextRefreshedEvent event) {
DefaultCookieSerializer cookieSerializer = applicationContext.getBean(DefaultCookieSerializer.class);
log.info("Received DefaultCookieSerializer, Overriding SameSite Strict");
cookieSerializer.setSameSite("strict");
}
}
From spring boot version 2.6.+ you may specify your samesite cookie either programatically or via configuration file.
Spring boot 2.6.0 documentation
If you would like to set samesite to lax via configuration file then:
server.servlet.session.cookie.same-site=lax
Or programatically
#Configuration
public class MySameSiteConfiguration {
#Bean
public CookieSameSiteSupplier applicationCookieSameSiteSupplier() {
return CookieSameSiteSupplier.ofLax();
}
}
Ever since the last update, chrome started showing that message to me too. Not really an answer regarding spring, but you can add the cookie flag to the header of the session. In my case, since I'm using spring security, I intend to add it when the user logs in, since I'm already manipulating the session in order to add authentication data.
For more info, check this answer to a similar topic: https://stackoverflow.com/a/43250133
To add the session header right after the user logs in, you can base your code on this topic (by creating a spring component that implements AuthenticationSuccessHandler): Spring Security. Redirect to protected page after authentication
For me none of the above worked. My problem was, that after a login, the SameSite flag created with other methods mentioned in this post was simply ignored by redirect mechanizm.
In our spring boot 2.4.4 application I managed to get it done with custom SameSiteHeaderWriter:
import org.springframework.security.web.header.HeaderWriter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import static javax.ws.rs.core.HttpHeaders.SET_COOKIE;
/**
* This header writer just adds "SameSite=None;" to the Set-Cookie response header
*/
public class SameSiteHeaderWriter implements HeaderWriter {
private static final String SAME_SITE_NONE = "SameSite=None";
private static final String SECURE = "Secure";
#Override
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (response.containsHeader(SET_COOKIE)) {
var setCookie = response.getHeader(SET_COOKIE);
var toAdd = new ArrayList<String>();
toAdd.add(setCookie);
if (! setCookie.contains(SAME_SITE_NONE)) {
toAdd.add(SAME_SITE_NONE);
}
if (! setCookie.contains(SECURE)) {
toAdd.add(SECURE);
}
response.setHeader(SET_COOKIE, String.join("; ", toAdd));
}
}
}
then in my WebSecurityConfigurerAdapter#configure I just added this header writer to the list using:
if (corsEnabled) {
httpSecurity = httpSecurity
.cors()
.and()
.headers(configurer -> {
configurer.frameOptions().disable();
configurer.addHeaderWriter(new SameSiteHeaderWriter());
});
}
This feature have to be explicitly enabled in our app by user knowing the risks.
Just thought this might help someone in the future.
Starting from Spring Boot 2.6.0 this is now possible and easy:
import org.springframework.http.ResponseCookie;
ResponseCookie springCookie = ResponseCookie.from("refresh-token", "000")
.sameSite("Strict")
.build();
and return it in a ResponseEntity, could be like this :
ResponseEntity
.ok()
.header(HttpHeaders.SET_COOKIE, springCookie.toString())
.build();
If you use spring-redis-session, you can customize the Cookie (🍪) by creating a bean like the following:
#Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
serializer.setSameSite(null);
return serializer;
}
You can look here more detail information.
Follow the documentation to solve this issue:
https://github.com/GoogleChromeLabs/samesite-examples
It has examples with different languages

Spring MVC #PathVariable gives 404 error

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;
}
}

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