How to configure i18n in Spring boot 2 + Webflux + Thymeleaf? - spring-boot

I just start a new project based on Spring boot 2 + Webflux. On upgrading version of spring boot and replace spring-boot-starter-web with spring-boot-starter-webflux classes like
WebMvcConfigurerAdapter
LocaleResolver
LocaleChangeInterceptor
are missing. How now can I configure defaultLocale, and interceptor to change the language?

Just add a WebFilter that sets the Accept-Language header from the value of a query parameter. The following example gets the language from the language query parameter on URIs like http://localhost:8080/examples?language=es:
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.adapter.HttpWebHandlerAdapter;
import reactor.core.publisher.Mono;
import static org.springframework.util.StringUtils.isEmpty;
#Component
public class LanguageQueryParameterWebFilter implements WebFilter {
private final ApplicationContext applicationContext;
private HttpWebHandlerAdapter httpWebHandlerAdapter;
public LanguageQueryParameterWebFilter(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
#EventListener(ApplicationReadyEvent.class)
public void loadHttpHandler() {
this.httpWebHandlerAdapter = applicationContext.getBean(HttpWebHandlerAdapter.class);
}
#Override
public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
final ServerHttpRequest request = exchange.getRequest();
final MultiValueMap<String, String> queryParams = request.getQueryParams();
final String languageValue = queryParams.getFirst("language");
final ServerWebExchange localizedExchange = getServerWebExchange(languageValue, exchange);
return chain.filter(localizedExchange);
}
private ServerWebExchange getServerWebExchange(final String languageValue, final ServerWebExchange exchange) {
return isEmpty(languageValue)
? exchange
: getLocalizedServerWebExchange(languageValue, exchange);
}
private ServerWebExchange getLocalizedServerWebExchange(final String languageValue, final ServerWebExchange exchange) {
final ServerHttpRequest httpRequest = exchange.getRequest()
.mutate()
.headers(httpHeaders -> httpHeaders.set("Accept-Language", languageValue))
.build();
return new DefaultServerWebExchange(httpRequest, exchange.getResponse(),
httpWebHandlerAdapter.getSessionManager(), httpWebHandlerAdapter.getCodecConfigurer(),
httpWebHandlerAdapter.getLocaleContextResolver());
}
}
It uses #EventListener(ApplicationReadyEvent.class) in order to avoid cyclic dependencies.
Feel free to test it and provide feedback on this POC.

With spring-boot-starter-webflux, there are
DelegatingWebFluxConfiguration
LocaleContextResolver
For example, to use a query parameter "lang" to explicitly control the locale:
Implement LocaleContextResolver, so that
resolveLocaleContext() returns a SimpleLocaleContext determined by a GET parameter of "lang". I name this implementation QueryParamLocaleContextResolver. Note that the default LocaleContextResolver is an org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver.
Create a #Configuration class that extends DelegatingWebFluxConfiguration. Override DelegatingWebFluxConfiguration.localeContextResolver() to return QueryParamLocaleContextResolver that we just created in step 1. Name this configuration class WebConfig.
In WebConfig, override DelegatingWebFluxConfiguration.configureViewResolvers() and add the ThymeleafReactiveViewResolver bean as a view resolver. We do this because, for some reason, DelegatingWebFluxConfiguration will miss ThymeleafReactiveViewResolver after step 2.
Also, I have to mention that, to use i18n with the reactive stack, this bean is necessary:
#Bean
public MessageSource messageSource() {
final ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath:/messages");
messageSource.setUseCodeAsDefaultMessage(true);
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(5);
return messageSource;
}
After creating a natural template, some properties files, and a controller, you will see that:
localhost:8080/test?lang=zh gives you the Chinese version
localhost:8080/test?lang=en gives you the English version
Just don't forget <meta charset="UTF-8"> in <head>, otherwise you may see some nasty display of Chinese characters.

Starting with Spring Boot 2.4.0, the WebFluxAutoConfiguration contains a bean definition for the LocaleContextResolver, which allows us to inject custom LocaleContextResolver. For reference, the following is the default bean definition in Spring Boot 2.5.4 (the implementation may be different in earlier versions):
#Bean
#Override
#ConditionalOnMissingBean(name = WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME)
public LocaleContextResolver localeContextResolver() {
if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
return new FixedLocaleContextResolver(this.webProperties.getLocale());
}
AcceptHeaderLocaleContextResolver localeContextResolver = new AcceptHeaderLocaleContextResolver();
localeContextResolver.setDefaultLocale(this.webProperties.getLocale());
return localeContextResolver;
}
You can provide your own LocaleContextResolver implementation to get the locale from the query parameter by providing a custom bean definition:
//#Component("localeContextResolver")
#Component(WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME)
public class RequestParamLocaleContextResolver implements LocaleContextResolver {
#Override
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
List<String> lang = exchange.getRequest().getQueryParams().get("lang");
Locale targetLocale = null;
if (lang != null && !lang.isEmpty()) {
targetLocale = Locale.forLanguageTag(lang.get(0));
}
if (targetLocale == null) {
targetLocale = Locale.getDefault();
}
return new SimpleLocaleContext(targetLocale);
}
#Override
public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext) {
throw new UnsupportedOperationException(
"Cannot change lang query parameter - use a different locale context resolution strategy");
}
}
Note that the framework consumes the LocaleContextResolver with a specific name localeContextResolver (WebHttpHandlerBuilder.LOCALE_CONTEXT_RESOLVER_BEAN_NAME). You need to provide the bean with the given name. See #24209.

Another solution with spring boot starter web flux, which is much more cleaner, is to define your own HttpHandler using WebHttpHandlerBuilder in which you can set your LocaleContextResolver.
Documentation (see 1.2.2. WebHandler API) : https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-config-customize
MyLocaleContextResolver.java
public class MyLocaleContextResolver implements LocaleContextResolver {
#Override
public LocaleContext resolveLocaleContext(ServerWebExchange exchange) {
return new SimpleLocaleContext(Locale.FRENCH);
}
#Override
public void setLocaleContext(ServerWebExchange exchange, LocaleContext localeContext) {
throw new UnsupportedOperationException();
}
}
Then in a config file (annotated with #Configuration) or in your spring boot application file, defined your own HttpHandler bean.
Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public HttpHandler httpHandler(ApplicationContext context) {
MyLocaleContextResolver localeContextResolver = new MyLocaleContextResolver();
return WebHttpHandlerBuilder.applicationContext(context)
.localeContextResolver(localeContextResolver) // set your own locale resolver
.build();
}
}
That's it!

Based on Jonatan Mendoza's answer, but simpliefied and in kotlin:
/**
* Override Accept-Language header by "lang" query parameter.
*/
#Component
class LanguageQueryParameterWebFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val languageValue = exchange.request.queryParams.getFirst("lang") ?: ""
if (languageValue.isEmpty()) {
return chain.filter(exchange)
}
return chain.filter(
exchange.mutate().request(
exchange.request
.mutate()
.headers {
it[HttpHeaders.ACCEPT_LANGUAGE] = languageValue
}
.build(),
).build(),
)
}
}

Related

Is it possible to define a custom rest template?

I'm trying to define a common bean to be used for all my application so to add inside a logger and other logic. My idea would be:
public class MyRestTemplate extends RestTemplate{
Then:
#Configuration
public class RestTemplateConfig {
#Bean
public MyRestTemplate myRestTemplate(RestTemplateBuilder builder){
return (MyRestTemplate) builder.build(); //throws classcast exception!
}
}
What am I doing wrong? Is there another way? I want to be sure that people will have to use my customized class.
If you want some customizations in your restTemplate you could define a class that implements RestTemplateCustomizer and add a custom interceptor to it.
public class CustomRestTemplateCustomizer implements RestTemplateCustomizer {
#Override
public void customize(RestTemplate restTemplate) {
restTemplate.getInterceptors().add(new CustomClientHttpRequestInterceptor());
}
}
Then you have to define that custom interceptor for all the requests going out of this restTemplate with
public class CustomClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
// This is where you can do a lot of thing with this request like logging
return execution.execute(request, body);
}
}
And finally, just define a bean for the custom restTemplate you have written
#Bean
public CustomRestTemplateCustomizer customRestTemplateCustomizer() {
return new CustomRestTemplateCustomizer();
}
builder.build() returns a RestTemplate, not a MyRestTemplate.
If you change your code as shown below you would create a bean named myRestTemplate. Spring use the name of the method as bean name if you don't override it in the #Bean annotation.
#Configuration
public class RestTemplateConfig {
#Bean
public RestTemplate myRestTemplate(RestTemplateBuilder builder){
return builder.build(); //throws classcast exception!
}
}
Please also see https://docs.spring.io/spring-boot/docs/1.5.x/reference/html/boot-features-restclient.html

Spring Data Rest custom argument Resolver

So i am trying to add a custom argument resolver to my Spring-Data-Rest project.
I am devolping a multi-tenant application, and need to filter data based on a users tenant-id.
So i wrote a simple annotation and ArgumentResolver to query my tenant repository and inject a tenant Object as Parameter on some needed Methods:
Handler:
#AllArgsConstructor
public class TenantInjector implements HandlerMethodArgumentResolver {
private final TenantStore tenantStore;
private final TenantRepository tenantRepository;
#Override
public boolean supportsParameter(MethodParameter methodParameter) {
if(! methodParameter.hasParameterAnnotation(InjectTenant.class)) {
return false;
}
return true;
}
#Override
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer modelAndViewContainer,
NativeWebRequest nativeWebRequest,
WebDataBinderFactory webDataBinderFactory) throws Exception {
return tenantRepository.findById(tenantStore.getId()).get();
}
}
This handler queries the tenantRepository to find the current tenant by its Id, which is set when the incoming requests security token is parsed.
To register the handler, i do the following:
#Configuration
public class DispatcherContext implements WebMvcConfigurer {
private final TenantStore tenantStore;
private final TenantRepository tenantRepository;
#Autowired
public DispatcherContext(TenantStore tenantStore, TenantRepository tenantRepository) {
this.tenantStore = tenantStore;
this.tenantRepository= tenantRepository;
}
#Override
public void addArgumentResolvers(
List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new TenantInjector(tenantStore, tenantRepository));
}
}
This works nice as long as the corrensponding Controller is annotated with either #Controller or #RestController
As the #RepositoryRestController has an other context, this configuration is ignored. How can I add the same ArgumentResolver to the Spring-Data-Rest configuration?
It might be an option to just switch the annotations, but i would like to rather stick with this approche, as links get generated by spring-data-rest.
Has anyone stumble over this to?
Your issue could be that you registered your custom argument resolver in your WebMvcConfigurer. Spring Data Rest seems to work in a different context, so you have to register your custom argument resolver in your RepositoryRestMvcConfiguration.
#Configuration
public class RepositoryConfiguration extends RepositoryRestMvcConfiguration {
public RepositoryConfiguration(ApplicationContext context, ObjectFactory<ConversionService> conversionService)
{
super(context, conversionService);
}
#Override
protected List<HandlerMethodArgumentResolver> defaultMethodArgumentResolvers()
{
List<HandlerMethodArgumentResolver> resolvers =
new ArrayList<>(super.defaultMethodArgumentResolvers());
resolvers.add(new TenantInjector(tenantStore, tenantRepository));
return resolvers;
}
}
Answer inspired by: https://github.com/tkaczmarzyk/specification-arg-resolver/issues/6#issuecomment-111952898

How to retrieve WebSession using WebFlux and WebSocketSession in latest versions of spring?

I have been trying to retrieve the WebSession from WebSocketSessionHandler (namespace: org.springframework.web.reactive.socket) in order to use it in my send consumer (using FluxSink).
I have been digging through the whole spring source code and documentations which didn't seem up to date yet and I didn't find anything about the last versions.
Is it possible to retrieve the WebSession or are there ways to get only the id?
Here is sample code: WebSocketHandler.java
public class WebSocketHandler implements org.springframework.web.reactive.socket.WebSocketHandler {
/**
* {#inheritDoc}
*/
#Override
public Mono<Void> handle(final WebSocketSession session) {
// use session to receive message etc...
return (session.send(Flux.create((FluxSink<OutputMessageInterface> sink) -> {
// use sink
}).map(/* ... convert from string to WebSocketMessage ... */)));
}
}
And the configuration file: WebSocketConfiguration.java
#Configuration
public class WebSocketConfiguration {
#Bean
public WebSocketHandler webSocketHandler() {
return (new WebSocketHandler());
}
#Bean
public WebSocketHandlerAdapter handlerAdapter() {
return (new WebSocketHandlerAdapter());
}
#Bean
public HandlerMapping handlerMapping(#Autowired WebSocketHandler webSocketHandler) {
Map<String, WebSocketHandler> map = new HashMap<>();
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
map.put("/realtime", webSocketHandler);
mapping.setOrder(10);
mapping.setUrlMap(map);
return (mapping);
}
}
The project is based on spring-boot 2.0.0.M6, spring-boot-starter-webflux 2.0.0.M6 (spring-core 5.0.1.RELEASE)

vaadin + spring boot: Cannot forward to error page for request

By single view application vaadin 7.7.7, spring-boot 1.5 i check uri fragment https:/tld/#!category-name-1 from user and if the category exist show items and if not
VaadinService.getCurrentResponse().sendError(404, "page not found!");
but i got error after update spring-boot 1.5 and vaadin 7.7.7 (with embeded tomcat):
Cannot forward to error page for request [/vaadinServlet/UIDL/] as the response has already been committed. As a result, the response may have the wrong status code. If your application is running on WebSphere Application Server you may be able to resolve this problem by setting com.ibm.ws.webcontainer.invokeFlushAfterService to false
How can i send http error pages from vaadin to user?
ErrorPageCutomizer.java
#Component
public class ErrorPageCutomizer implements EmbeddedServletContainerCustomizer {
#Override
public void customize(ConfigurableEmbeddedServletContainer container) {
container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/error/404"));
container.addErrorPages(new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500"));
}
}
RestController.java
import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class ErrorHandlingController implements ErrorController {
private static final String PATH = "/error";
#RequestMapping(value = PATH + "/404")
public String error404() {
return "<div style='font-weight:bold; margin-top:200px; text-align:center; font-size:160%;'>Page not found...<br>to home</div>";
}
#RequestMapping(value = PATH + "/500")
public String error500() {
return "<div style='font-weight:bold; margin-top:200px; text-align:center; font-size:160%;'>500 Internal server error...</div>";
}
#Override
public String getErrorPath() {
return PATH;
}
}
Soliution was:
#Configuration
public class AppInitializer implements WebApplicationInitializer {
#Bean
public ErrorPageFilter errorPageFilter() {
return new ErrorPageFilter();
}
#Bean
public FilterRegistrationBean disableSpringBootErrorFilter(ErrorPageFilter filter) {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(filter);
filterRegistrationBean.setEnabled(false);
return filterRegistrationBean;
}
}
Have you tried the SystemMessagesProvider. In that provider you could define an errorUrl for a variety of errors:
public class YourServlet extends VaadinServlet
{
#Override
protected void servletInitialized() throws ServletException
{
super.servletInitialized();
getService().setSystemMessagesProvider(new SystemMessagesProvider()
{
#Override
public SystemMessages getSystemMessages(SystemMessagesInfo systemMessagesInfo)
{
final CustomizedSystemMessages c = new CustomizedSystemMessages();
final String errorUrl = "<url to errorpage>";
c.setSessionExpiredURL(errorUrl);
c.setSessionExpiredNotificationEnabled(false);
c.setAuthenticationErrorURL(errorUrl);
c.setAuthenticationErrorNotificationEnabled(false);
c.setCommunicationErrorURL(errorUrl);
c.setCommunicationErrorNotificationEnabled(false);
c.setCookiesDisabledURL(errorUrl);
c.setCookiesDisabledNotificationEnabled(false);
c.setInternalErrorURL(errorUrl);
c.setInternalErrorNotificationEnabled(false);
c.setSessionExpiredURL(errorUrl);
c.setSessionExpiredNotificationEnabled(false);
return c;
}
});
}

Getting No bean resolver registered

After upgrading today from Spring boot 1.2.5 to 1.3.0 BUILD-SNAPSHOT Calling
#PreAuthorize fails:
example:
#PreAuthorize("#defaultSecurityService.canDoSomething(authentication.principal.id, #objId)")
Result doSomething(#P("objId")String objId);
where defaultSecurityService is defined as:
#Service
public class DefaultSecurityService implements SecurityService {
...
public boolean canDoSomething(String userId, String objId){
return true; //
}
}
Stack trace
Caused by: java.lang.IllegalArgumentException: Failed to evaluate expression '#oauth2.throwOnError(defaultSecurityService.canDoSomething(authentication.principal.id, #objId))'
at org.springframework.security.access.expression.ExpressionUtils.evaluateAsBoolean(ExpressionUtils.java:14)
...
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1057E:(pos 8): No bean resolver registered in the context to resolve access to bean 'defaultSecurityService'
what i've tried:
make SecurityService extend [PermissionEvaluator][1] and register a bean
atApplication.java`
#Bean
#Lazy
public PermissionEvaluator permissionEvaluator(){
return securityService;
}`
But i'm still getting the same error
Reading the spring security 4.0.2 documentation didn't reveal any relevant material about breaking changes
This appears to be a bug in the newly added OAuth2AutoConfiguration. Specifically it brings in OAuth2MethodSecurityConfiguration which overrides the DefaultMethodSecurityExpressionHandler with a OAuth2MethodSecurityExpressionHandler that does not have a BeanResolver set.
If you are not using OAuth2, then the easiest solution is to remove Spring Security OAuth from your classpath.
Alternatively, you can exclude the OAuth2AutoConfiguration using the following if you use #SpringBootApplication:
#SpringBootApplication(exclude=OAuth2AutoConfiguration.class)
alternatively you can use the following if you leverage #AutoConfiguration directly:
#AutoConfiguration(exclude=OAuth2AutoConfiguration.class)
UPDATE
You can also use something like this:
public class DelegatingMethodSecurityExpressionHandler implements
MethodSecurityExpressionHandler {
private final MethodSecurityExpressionHandler delegate;
public DelegatingMethodSecurityExpressionHandler(
MethodSecurityExpressionHandler delegate) {
super();
this.delegate = delegate;
}
public Object filter(Object filterTarget, Expression filterExpression,
EvaluationContext ctx) {
return delegate.filter(filterTarget, filterExpression, ctx);
}
public ExpressionParser getExpressionParser() {
return delegate.getExpressionParser();
}
public EvaluationContext createEvaluationContext(
Authentication authentication, MethodInvocation invocation) {
return delegate.createEvaluationContext(authentication, invocation);
}
public void setReturnObject(Object returnObject, EvaluationContext ctx) {
delegate.setReturnObject(returnObject, ctx);
}
}
Then in your configuration use:
#Autowired(required = false)
List<AuthenticationTrustResolver> trustResolvers = new ArrayList<>();
#Autowired(required = false)
List<PermissionEvaluator> permissionEvaluators = new ArrayList<>();
#Bean
public MethodSecurityExpressionHandler securityExpressionHandler(ApplicationContext context) {
OAuth2MethodSecurityExpressionHandler delegate = new OAuth2MethodSecurityExpressionHandler();
delegate.setApplicationContext(context);
if(trustResolvers.size() == 1) {
delegate.setTrustResolver(trustResolvers.get(0));
}
if(permissionEvaluators.size() == 1) {
delegate.setPermissionEvaluator(permissionEvaluators.get(0));
}
return new DelegatingMethodSecurityExpressionHandler(delegate);
}
We have to wrap it in the DelegatingMethodSecurityExpressionHandler because Spring Boot's auto config will replace any subclass of DefaultMethodSecurityExpressionHandler with the broken configuration.
I had the same problem than you, my bean in charge of managing security on a REST controller wasn't found:
org.springframework.expression.spel.SpelEvaluationException: EL1057E:(pos 8): No bean resolver registered in the context to resolve access to bean 'communitySecurityAuthorizer
Rob's reply pointed me in the right direction (I thought I was doing it wrong, not that it was a bug in the standard Spring OAuth2).
I don't use springboot as I'm making a webapp and I found the answer that solved my problem here:
https://github.com/spring-projects/spring-security-oauth/issues/730#issuecomment-219480394
The problem comes in fact from the bean resolver which is null so here is the solution (retranscription of the link above):
Add a #Bean with OAuth2WebSecurityExpressionHandler that explicitly
sets the application context
#Bean
public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler(ApplicationContext applicationContext) {
OAuth2WebSecurityExpressionHandler expressionHandler = new OAuth2WebSecurityExpressionHandler();
expressionHandler.setApplicationContext(applicationContext);
return expressionHandler;
}
In the ResourceServerConfigurerAdapter, configure the resources and
pass in the Bean above.
#Autowired
private OAuth2WebSecurityExpressionHandler expressionHandler;
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.expressionHandler(expressionHandler);
}
Hope this'll others !
As Almiriad has said, generate the OAuth2MethodSecurityExpressionHandler instance as a bean.
Instead do that:
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig extends GlobalMethodSecurityConfiguration {
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
....
}
do this:
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuth2ResourceServerConfig extends GlobalMethodSecurityConfiguration {
#Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return getOAuth2MethodSecurityExpressionHandler();
}
#Bean
public OAuth2MethodSecurityExpressionHandler getOAuth2MethodSecurityExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler();
}
....
}
Hope this'll others !

Resources