Map url with language identifier - spring-mvc

Is there a nice way to resolve locale based on the URL and in the other hand map requests without any additional requirement ?
For example
http://example.com/ru/news
http://example.com/iw/news
and in the controller still use the standard mappings
#Controller
#RequestMapping(value = "/news")
public class NewsController {
// Controller methods ...
}

You could write a custom interceptor that works like LocaleChangeInterceptor
Here's a sample implementation that uses a regex pattern (most of the code is copied from LocaleChangeInterceptor):
public class CustomLocaleChangeInterceptor extends HandlerInterceptorAdapter {
private Pattern localePattern;
public void setLocalePattern(final String localePattern) {
Assert.isTrue(localePattern.matches(".*\\(.*\\).*"), "Your pattern needs to define a match group");
this.localePattern = Pattern.compile(localePattern);
}
#Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler)
throws ServletException {
final String pathTranslated = request.getPathTranslated();
if (pathTranslated != null) {
final Matcher matcher = localePattern.matcher(pathTranslated);
if (matcher.find()) {
final String newLocale = matcher.group(1);
if (newLocale != null) {
final LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
if (localeResolver == null) {
throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
}
final LocaleEditor localeEditor = new LocaleEditor();
localeEditor.setAsText(newLocale);
localeResolver.setLocale(request, response, (Locale) localeEditor.getValue());
}
}
}
// Proceed in any case.
return true;
}
}
Wire it like this:
<bean id="localeChangeInterceptor"
class="foo.bar.CustomLocaleChangeInterceptor">
<property name="localePattern" value="\b([a-z]{2})\b"/>
</bean

I'm not aware of an out-of-the-box solution for this, but it's easy enough to implement using a custom interceptor and some wisely chosen mappings.
Write an implementation of HandlerInterceptor which implements preHandle so that the locale string is extracted from the request URI, and then tag the request with that locale (see the source code for the similar LocalChangeInterceptor, which does a similar thing to what you need, but uses a request parameter instead of a path variable).
Then wire it up using <mvc:interceptor> e.g.
<mvc:interceptors>
<mvc:interceptor>
<mapping path="/*"/>
<bean class="x.y.MyInterceptor" />
</mvc:interceptor>
</mvc:interceptors>
You can then loosen up the request mapping on your controller to tolerate (and ignore) the locale part of the URI:
#Controller
#RequestMapping(value = "*/news")
public class NewsController {
// Controller methods ...
}

Take a look at http://lrkwz.github.com/URLManagedLocale/ you can simply drop the dependency in your pom file and configure the interceptor and localeresolver.

Related

Serve static content in Spring Boot despite using #RequestMapping("**")

The context
I am currently working on an educational project. This implies two Spring Boot REST servers. One is an actual server, which does some processing.
The one I'm interested in is the other. It is a proxy which will redirect all calls to the first one. So that when I call http://localhost:8080/foo, my proxy server will in turn call http://localhost:8090/foo. And if the first server returns A, the proxy will return {"proxied": A, "someInformationAboutThisCall": B}.
I managed to get to this point with some probably inelegant but functioning code of which I give an excerpt below. The key here is that I use #RequestMapping("**") to achieve this. The next step is to design an interface that will make my additional information immediately legible, which is basically the point of this project. If I remove all #RequestMapping("**"), it works just fine.
The question
Now my problem is the following: having used #RequestMapping("**"), I cannot serve static content (the calls get redirect to the other REST server, which does not serve static content). How could I configure Spring Boot/Spring MVC to ignore resources available as static content when mapping the requests, or make the PathResourceResolver prioritary over my controller?` Or should I serve my static content from yet another JVM/server?
Thanks in advance for your help!
Edit of interest: while doing some tests, I discovered that the static content is served, with some restrictions, if I use #RequestMapping("*").
/index.html generates an error page (as does more generally any static content directly in public)
/itf/index.html works (as does more generally any file in public/itf or any other subdirectory of public)
/itf does not work: Spring Boot seems unaware of an index file in it. I must specify a full URI, down to the specific file I want to display.
This however does not work at all with #RequestMapping("**"), which I need.
The tentatives
I tried using a WebMvcConfigurerAdapter with an HandlerInterceptorAdapter (found on SO, SO again and many other places on the Internet), but could not start my project anymore because Spring boot then does not find the InterceptorRegistry bean (has there been recent changes in Spring Boot? I'm using the version 1.5.3.RELEASE).
I also tried some anti-matching but not only does it not work, it also feels very very dirty (and this whole project is probably not optimal, so that's saying a lot).
The code samples for the curious
My "proxy" controller
Note: you can suggest better ways to realize this in comments. Please keep in mind that, though I'm always open to enhancement suggestions, this was not my question.
#RestController
public class ProxyController {
#Value("${monitored.url.base}") // "http://localhost:8090"
private String redirectBase;
#RequestMapping(value = "**", method = {RequestMethod.POST, RequestMethod.PUT})
public ProxiedResponse proxifyRequestsWithBody(HttpServletRequest request, #RequestHeader HttpHeaders headers, #RequestBody Object body) throws URISyntaxException {
return proxifyRequest(request, headers, body);
}
#RequestMapping(value = "**")
public ProxiedResponse proxifyRequestsWithoutBody(HttpServletRequest request, #RequestHeader HttpHeaders headers) throws URISyntaxException {
return proxifyRequest(request, headers, null);
}
private ProxiedResponse proxifyRequest(HttpServletRequest request, #RequestHeader HttpHeaders headers, #RequestBody Object body) throws URISyntaxException {
final RequestEntity<Object> requestEntity = convertToRequestEntity(request, headers, body);
// call remote service
final ResponseEntity<Object> proxied = restTemplate.exchange(requestEntity, Object.class);
// Return service result + monitoring information
final ProxiedResponse response = new ProxiedResponse();
response.setProxied(proxied.getBody());
// set additional information
return response;
}
// Won't work properly for POST yet
private <T> RequestEntity<T> convertToRequestEntity(HttpServletRequest request, HttpHeaders headers, T body) throws URISyntaxException {
// Build proxied URL
final StringBuilder redirectUrl = new StringBuilder(redirectBase).append(request.getRequestURI());
final String queryString = request.getQueryString();
if (queryString != null) {
redirectUrl.append("?").append(queryString);
}
// TODO enhancement: transmit headers and request body to make this a real proxy
final HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod());
return new RequestEntity<>(body, headers, httpMethod, new URI(redirectUrl.toString()));
}
}
My dirty attempt at excluding static resources URLs
#Configuration // adding #EnableWebMvc did not solve the problem
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
private static class StaticResourcesHandlerInterceptor extends HandlerInterceptorAdapter {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
final String requestURI = request.getRequestURI();
if (requestURI == null || "/".equals(requestURI) || "/index.html".equals(requestURI) || requestURI.startsWith("/assets")) {
return super.preHandle(request, response, null);
}
return super.preHandle(request, response, handler);
}
}
#Autowired
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new StaticResourcesHandlerInterceptor()).addPathPatterns("/**");
}
}
You can split the path into a wild-card, and a named path variable which must match a negative lookahead regular expression.
#RequestMapping("/{variable:(?!static).*}/**")
You can then use #PathVariable String variable as an argument of your controller method to obtain the value of variable if you need to pass it.
(Would rather have written a comment but I have insufficient reputation)
Try to add the #EnableWebMvc annotation to your configuration:
#Configuration
#EnableWebMvc
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
...
}

How to target specific handlers with a #ControllerAdvice #ModelAttribute?

I'd like to display a warning message on specific pages 5 minutes prior to a system shutdown. Rather than add it manually to each these pages I created a #ControllerAdvice class with a #ModelAttribute method that adds the message to the Model parameter, but from what I understand reading the documentation and SO and some initial testing this model attribute will be added to every method with a #RequestMapping.
I realize I could refactor my code so that the targeted methods are all in one controller and limit the #ControllerAdvice to that one controller, but I would end up with a collection of otherwise non-related methods in that controller which muddies up the overall structure of my controllers.
So, is there a way to indicate which specific methods in multiple controllers the #ModelAttribute is applied to? Would a custom annotation be a solution (not sure how that would work)? I'd like to do this via annotations if possible.
Edit:
The #ControllerAdvice code is pretty basic:
#ControllerAdvice
public class GlobalModelController {
private final Logger logger = LoggerFactory.getLogger(getClass());
#Autowired
private MaintenanceInterceptor maintInterceptor;
#ModelAttribute()
public void globalAttributes(Model model, Locale locale) {
if (maintInterceptor.isMaintenanceWindowSet() && !maintInterceptor.isMaintenanceInEffect()) {
String msg = maintInterceptor.getImminentMaint(locale);
model.addAttribute("warningMaint", msg);
logger.debug("maint msg= " + msg);
}
}
}
A controller advice can be limited to certain controllers (not methods) by using one of the values of the #ControllerAdvice annotation, e.g.
#ControllerAdvice(assignableTypes = {MyController1.class, MyController2.class})
If you need to do it on a method level I suggest to take a look at Interceptors.
Thanks to #zeroflagL for pointing me to the interceptor solution. I ditched the #ControllerAdvice approach and ended up with this:
Custom annotation:
#Target({ElementType.METHOD, ElementType.TYPE})
#Retention(RetentionPolicy.RUNTIME)
#Qualifier
public #interface MaintAware {
String name() default "MaintAware";
}
Interceptor:
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
MaintAware maintAware = method.getAnnotation(MaintAware.class);
if (maintAware != null) {
Locale locale = request.getLocale();
if (isMaintenanceWindowSet() && !isMaintenanceInEffect()) {
String msg = getImminentMaint(locale);
if (!msg.isEmpty())
modelAndView.addObject("warningMaint", msg);
}
}
super.postHandle(request, response, handler, modelAndView);
}
Now I can annotate the specific methods that require the maintenance notification. Easy peasy. :)

Aspect not getting called in Spring MVC

I have our aspect, annotation & MVC controller written as follows:
Aspect
#Aspect
public class AuditAspect {
#Around(value = "#annotation(com.test.Audit)")
public Object audit(ProceedingJoinPoint pjp) {
System.out.println("Inside the Audit aspect ...");
Object result = null;
try {
result = pjp.proceed();
} catch (Throwable t) {
}
return result;
}
}
The annotation:
#Retention(RetentionPolicy.RUNTIME)
#Target(ElementType.METHOD)
public #interface Audit
{
AuditType auditType();
}
The controller:
#RestController
#RequestMapping("/patients")
public class PatientController {
#Audit(auditType = AuditType.PATIENT_LIST)
#RequestMapping(value="", method=RequestMethod.GET)
public APIResponse getPatients(HttpServletRequest request, HttpServletResponse response, #RequestParam(required = false, value="audit") String sAudit) {
System.out.println("Inside getPatients ...");
return null;
}
}
However, the aspect's audit method is not getting called whenever I make rest requests.
Looked around for some help. Found few posts where there were mentions of AspectJ not working with Spring MVC controllers. However, I tried this same example with a simple spring MVC application, and the aspect was getting called properly, even if controller methods were annotated. Not sure what is going wrong here. Any pointers/suggestions here would be very helpful.
The sample application I tried didn't have use of spring transaction manager, or integration with hibernate etc... Would that make any difference?
Also, given below is the context file entries:
<aop:aspectj-autoproxy />
<context:component-scan base-package="com.test">
<context:include-filter type="aspectj" expression="com.test.AuditAspect" />
</context:component-scan>
<context:annotation-config />
In order to make Spring AOP work, both your aspect and the target object must be a Spring #Component.

Controller Inheritance and Ambiguous Mappings with URL Versioning in Spring MVC

I am trying to setup versioned services with Spring MVC, using inheritance to extend older controllers to avoid rewriting unchanged controller methods. I've based my solution on a previous question about versioning services, however I've run into a problem with ambiguous mappings.
#Controller
#RequestMapping({"/rest/v1/bookmark"})
public class BookmarkJsonController {
#ResponseBody
#RequestMapping(value = "/write", produces = "application/json", method = RequestMethod.POST)
public Map<String, String> writeBookmark(#RequestParam String parameter) {
// Perform some operations and return String
}
}
#Controller
#RequestMapping({"/rest/v2/bookmark"})
public class BookmarkJsonControllerV2 extends BookmarkJsonController {
#ResponseBody
#RequestMapping(value = "/write", produces = "application/json", method = RequestMethod.POST)
public BookmarkJsonModel writeBookmark(#RequestBody #Valid BookmarkJsonModel bookmark) {
// Perform some operations and return BookmarkJsonModel
}
}
With this setup I get IllegalStateException: Ambiguous mapping found. My thought regarding this is that because I have two methods with different return/argument types I have two methods in BookmarkJsonControllerV2 with the same mapping. As a workaround I attempted to override writeBookmark in BookmarkJsonControllerV2 without any request mapping:
#Override
public Map<String, String> writeBookmark(#RequestParam String parameter) {
return null; // Shouldn't actually be used
}
However, when I compiled and ran this code I still got the exception for an ambiguous mapping. However, when I hit the URL /rest/v2/bookmark/write I got back an empty/null response. Upon changing return null to:
return new HashMap<String, String>() {{
put("This is called from /rest/v2/bookmark/write", "?!");
}};
I would receive JSON with that map, indicating that despite not having any request mapping annotation, it is apparently "inheriting" the annotation from the super class. At this point, my only "solution" to future-proofing the extension of the controllers is to make every controller return Object and only have the HttpServletRequest and HttpServletResponse objects as arguments. This seems like a total hack and I would rather never do this.
So is there a better approach to achieve URL versioning using Spring MVC that allows me to only override updated methods in subsequent versions or is my only real option to completely rewrite each controller?
For whatever reason, using the #RequestMapping annotation was causing the ambiguous mapping exceptions. As a workaround I decided to try using springmvc-router for my REST services which would allow me to leverage inheritance on my controller classes so I would not have to reimplement endpoints that did not change between versions as desired. My solution also allowed me to continue using annotation mappings for my non-REST controllers.
Note: I am using Spring 3.1, which has different classes for the handler mappings than previous versions.
The springmvc-router project brings the router system from the Play framework over to Spring MVC. Inside of my application-context.xml, the relevant setup looks like:
<mvc:annotation-driven/>
<bean id="handlerAdapter" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter" />
<bean class="org.resthub.web.springmvc.router.RouterHandlerMapping">
<property name="routeFiles">
<list>
<value>routes/routes.conf</value>
</list>
</property>
<property name="order" value="0" />
</bean>
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
<property name="order" value="1" />
</bean>
This allows me to continue using my annotated controllers alongside the router. Spring uses a chain-of-responsibility system, so we can assign multiple mapping handlers. From here, I have a router configuration like so:
# Original Services
POST /rest/bookmark/write bookmarkJsonController.write
POST /rest/bookmark/delete bookmarkJsonController.delete
# Version 2 Services
POST /rest/v2/bookmark/write bookmarkJsonControllerV2.write
POST /rest/v2/bookmark/delete bookmarkJsonControllerV2.delete
Alongside controllers looking like:
#Controller
public class BookmarkJsonController {
#ResponseBody
public Map<String, Boolean> write(#RequestParam String param) { /* Actions go here */ }
#ResponseBody
public Map<String, Boolean> delete(#RequestParam String param) { /* Actions go here */ }
}
#Controller
public class BookmarkJsonControllerV2 extends BoomarkJsonController {
#ResponseBody
public Model write(#RequestBody Model model) { /* Actions go here */ }
}
With a configuration like this, the URL /rest/v2/bookmark/write will hit the method BookmarkJsonControllerV2.write(Model model) and the URL /rest/v2/bookmark/delete will hit the inherited method BookmarkJsonController.delete(String param).
The only disadvantage from this comes from having to redefine entire routes for new versions, as opposed to changing the #RequestMapping(value = "/rest/bookmark") to #RequestMapping(value = "/rest/v2/bookmark") on the class.

Return literal JSON strings in spring mvc #ResponseBody

I am storing objects in my database as JSON strings. I want to make a REST service that exposes these strings. When I write my methods however, the strings I get back have their quotes escaped. For example, I have included a method that returns a String,
#RequestMapping(value = "test", method = RequestMethod.GET)
public #ResponseBody
String getTest() {
return "{\"a\":1, \"b\":\"foo\"}";
}
but when I call this method in the browser I get a back "{\"a\":1, \"b\":\"foo\"}" when what I really want to happen is {"a": 1, "b": "foo"}. I think "String" as the return type is likely the problem, but what else can I do? A wrapper class does the same thing:
{
"value" : "{\"a\":1, \"b\":\"foo\"}"
}
I could serialize it and then return the object, but that seems a bit ridiculous.
Here is a possibly the relevant portion of my configuration file:
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
super.configureMessageConverters(converters);
converters.add(mappingJacksonHttpMessageConverter());
}
#Bean
MappingJacksonHttpMessageConverter mappingJacksonHttpMessageConverter() {
MappingJacksonHttpMessageConverter mappingJacksonHttpMessageConverter = new MappingJacksonHttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
mappingJacksonHttpMessageConverter.setObjectMapper(objectMapper);
mappingJacksonHttpMessageConverter.setPrettyPrint(true);
return mappingJacksonHttpMessageConverter;
}
Thanks
EDIT: as was suggested below, it seems the string is being double encoded. Commenting out the 2 classes in my configuration fixes this issue. However, I still have other places where I want to return Objects and would like to keep those running through that common serializing bean that I know where to configure. So I see my options as:
a) Do all the serializing myself. All methods return Strings, and those that are already JSON return themselves, and those that are objects all return JSONUtil.toJson(object). I don't love this approach, but I know it will work.
b) Use a wrapper class that looks kind of like:
public static class Wrapper {
#JsonRawValue
private final String value;
}
This leads to an awkward "value" at the front though that has no real meaning.
Basically what I want is #JsonRawValue, but to have it work on RequestMapping methods instead of properties.
Thoughts? Opinions? Other suggestions?
This works with Jackson 2 (at least):
#Controller
public class YourController {
#RequestMapping(..)
public #ResponseBody Json get() {
return new Json("{ \"attr\" : \"value\" }");
}
}
class Json {
private final String value;
public Json(String value) {
this.value = value;
}
#JsonValue
#JsonRawValue
public String value() {
return value;
}
}
Not particularly pretty but works. I only wish Spring supported this:
#RequestMapping(..)
public #JsonRawValue #ResponseBody String get() {
// ...
}
I guess what you want is producing a response with content-type application/json. In your case, when you have the json-data as a raw string, do the following:
In your controller add produces="application/json" to your #RequestMapping attribute:
#RequestMapping(value = "test", method = RequestMethod.GET, produces="application/json")
public #ResponseBody
String getTest() {
return "{\"a\":1, \"b\":\"foo\"}";
}
Then you have to configure the StringHttpMessageConverter to accept the application/json media-type.
With Java-config:
#Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> converters) {
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(
Charset.forName("UTF-8"));
stringConverter.setSupportedMediaTypes(Arrays.asList( //
MediaType.TEXT_PLAIN, //
MediaType.TEXT_HTML, //
MediaType.APPLICATION_JSON));
converters.add(stringConverter);
}
With XML-config:
<bean class = "org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<array>
<bean class = "org.springframework.http.converter.StringHttpMessageConverter">
<property name="supportedMediaTypes" value="application/json; charset=UTF-8" />
</bean>
</array>
</property>
</bean>
I used this:
#RequestMapping(..)
#ResponseBody
public JsonNode myGetRequest(){
...
//rawJsonString is the raw Json that we want to proxy back to the client
return objectMapper.readTree(rawJsonString);
}
And the Jackson converter knew how to transform the JsonNode into plain Json.
If you want to convert JSON String to JSON object in your browser, keep string convertor before Jackson convertor.
Follow this link for complete example. It works with custom converter configuration plus spring validation.
It Works
converters.add(stringConverter());
converters.add(mappingJackson2HttpMessageConverter());
super.configureMessageConverters(converters);
It Doesn't
converters.add(mappingJackson2HttpMessageConverter());
converters.add(stringConverter());
super.configureMessageConverters(converters);
In my case, I wanted the response type to be determined by a request parameter, so had to specify the content type in the code, e.g.:
#RequestMapping("/myurl")
public void radiusSearch(#RequestParam responseType, HttpServletResponse response) throws IOException {
String jsonResponse = makeSomeJson();
response.setContentType(responseType);
try {
response.getOutputStream().write(jsonResponse.getBytes());
} finally {
response.getOutputStream().close();
}
}
Today we had the same issue and solved it with multiple converters. Now every String will treated as a string and every other Object will get serialised by Jackson. This allows to serialise manually (by returning String) or automatically (by returning something else) in Spring controllers.
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(stringConverter());
converters.add(mappingJackson2HttpMessageConverter());
super.configureMessageConverters(converters);
}
#Bean
public StringHttpMessageConverter stringConverter() {
final StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(UTF_8);
stringConverter.setSupportedMediaTypes(Arrays.asList(
MediaType.TEXT_PLAIN,
MediaType.TEXT_HTML,
MediaType.APPLICATION_JSON));
return stringConverter;
}
#Bean
public GenericHttpMessageConverter<Object> mappingJackson2HttpMessageConverter() {
final ObjectMapper objectMapper = objectMapperBuilder().build();
final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper);
return converter;
}
The \" means the character " is being escaped, which is standard. If it's being printed like that, you're probably double-serializing the object.
I know this is an old question, but I was just dealing with the opposite problem myself (I was returning a String and WANTED it to get converted to JSON). In your case, it sounds like you simply want to have your String treated as a plain string and not have any sort of JSON conversion done on it as you already have JSON.
So in your case you don't want to use the MappingJacksonHttpMessageConverter (or the MappingJackson2HttpMessageConverter if you're now using Jackson2). You want no conversions done at all, and that converter converts Java objects to/from JSON. So instead you should just use the plain StringHttpMessageConverter. You can do that by changing your setup method like this:
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new StringHttpMessageConverter());
}
This converter is applicable to */* types (the document is incorrect which says text/*, I found out the hard way in the debugger). So whether your content type is application/json or not, either way Spring won't mess with your Strings if you use this converter.
The solution to your problem is, this works perfectly without changing any configurations
import com.fasterxml.jackson.databind.JsonNode;
import com.github.fge.jackson.JsonLoader;
JsonNode getTest() {
return JsonLoader.fromString("{\"a\":1, \"b\":\"foo\"}");
}

Resources