Spring & Thymeleaf: changing locale and stay on the current page - spring

I have a traditional Spring4/Thymeleaf i18n application
I switch the locale easily with classic
org.springframework.web.servlet.i18n.LocaleChangeInterceptor
and
org.springframework.web.servlet.i18n.CookieLocaleResolver
When switching I always send to the server /home?lang=fr. It works fine. But I need a more complex behaviour.
What I need to do is to preserve the current page while switching the locale.
I found a half-working solution with this thymeleaf snippet:
th:with="currentUrl=(${#httpServletRequest.pathInfo + '?' + #strings.defaultString(#httpServletRequest.queryString, '')})
The problem is I need to implement myself many corner cases:
when there is already any query parameter
if there is a lang=en param,
etc.
Does anybody know how to manage this case with native Spring or Thymeleaf tools? Or I need to write my own processor for Thymeleaf?

The easiest solution is concatenating "requestURI" and "queryString".
The drawback of this method is that if you click multiple times on the same link the parameter just gets added over and over again.
A workaround is to write a function that "cleans" the url before adding the parameter
For code examples, take a look at this question: Thymeleaf: add parameter to current url

I use this in my projects & it works fine. I don't specifically redirect to any URL when locale change.
#Bean
public LocaleResolver localeResolver()
{
Locale defaultLocale = new Locale( env.getProperty( Constants.DEFAULT_LOCALE ) );
CookieLocaleResolver clr = new CookieLocaleResolver();
clr.setDefaultLocale( defaultLocale );
return clr;
}
#Bean
public LocaleChangeInterceptor localeChangeInterceptor()
{
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
lci.setParamName( "lang" );
return lci;
}

Related

MVC Core set application culture from controller action

I'm working on adding localization to my web application. I have configured the IStringLocalizer and it is correctly reading string resources from two different resx files, depending on the browser setting. It then maps those string resources to ViewData, from which my View is getting text in correct language (not sure if that is the best approach, but for now I don't want to spent more time on this).
The thing is that I also have a drop down list in my UI, that allows users to manually switch language. I'm reading the value set by user in my controller action and adding it to cookies, but now I'd also like to set my applications' culture to the one matching the string in cookie.
Is it possible to set application culture from the controller action in MVC Core? If yes, then how to do this correctly?
EDIT:
I have just learned that I can do something like this:
<a class="nav-item nav-link" asp-route-culture="en-US">English</a>
and it will add ?culture=en-US to my route, which will set culture for the page for me. Is there any way to do the same without having to keep it in an address bar?
EDIT 2:
Regarding answer by Adam Simon:
CookieRequestCultureProvider is what I'd like to use in my app, but the problem is that it is not producing any cookie. Documentation says that .net core will resolve which provider to use by checking which will give a working solution, starting from QueryStringRequestCultureProvider, then going to CookieRequestCultureProvider, then other providers.
My current Startup looks like this:
public class Startup
{
private const string defaultCulutreName = "en-US";
public void ConfigureServices(IServiceCollection services)
{
services.AddLocalization();
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo(defaultCulutreName),
new CultureInfo("pl-PL")
};
options.DefaultRequestCulture = new RequestCulture(defaultCulutreName, defaultCulutreName);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
//TRIED PLACING IT BEFORE AND AFTER UseRequestLocalization
//CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("pl-PL", "pl-PL"));
app.UseRequestLocalization(app.ApplicationServices
.GetService<IOptions<RequestLocalizationOptions>>().Value);
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("pl-PL", "pl-PL"));
app.UseMvc(ConfigureRoutes);
}
private void ConfigureRoutes(IRouteBuilder routeBuilder)
{
routeBuilder.MapRoute("Default", "{controller=About}/{action=About}");
}
}
Regarding CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("pl-PL", "pl-PL")) I have tried putting it in RequestLocalizationOptions in ConfigureServices, in Configure before UseRequestLocalization and after that. All with the same result.
The following "problems" appear with this solution:
MakeCookieValue method is not producing any .AspNetCore.Culture cookie
Chrome browser with language set to PL is using pl-PL culture
correctly, yet Firefox is using en-US culture with language set to PL
in options (despite commenting out options.DefaultRequestCulture =
new RequestCulture(defaultCulutreName, defaultCulutreName) line)
Somehow my localization is working by default without using query
strings nor cookies to provide culture for application, but this is
not how I'd like it to work, as I do not have any control over it
Somehow you must tie the selected culture to the user so if you don't want to carry it around in the URL, you must find another way to retain this piece of information between requests. Your options:
cookie
session
database
HTTP header
hidden input
Under normal circumstances using a cookie to store the language preference is a perfect choice.
In ASP.NET Core the best place to retrieve and set the culture for the current request is a middleware. Luckily, the framework includes one, which can be placed in the request pipeline by calling app.UseRequestLocalization(...) in your Startup.Configure method. By default this middleware will try to pick up the current culture from the request URL, cookies and Accept-Language HTTP header, in this order.
So, to summarize: you need to utilize the request localization middleware, store the user's culture preference in a cookie formatted like c=%LANGCODE%|uic=%LANGCODE% (e.g. c=en-US|uic=en-US) and you are done.
You find all the details in this MSDN article.
Bonus:
It then maps those string resources to ViewData, from which my View is
getting text in correct language (not sure if that is the best
approach, but for now I don't want to spent more time on this).
Passing localized texts to views in ViewData is cumbersome and error-prone. In ASP.NET Core we have view localization for this purpose. You just need to inject the IViewLocalizer component into your views to get a nice and convenient way to access your localized text resources. (Under the hood IViewLocalizer uses IStringLocalizer.)
About EDIT 2
MakeCookieValue method is not producing any .AspNetCore.Culture cookie
CookieRequestCultureProvider.MakeCookieValue method is just a helper to generate a cookie value in the correct format. It just returns a string and that's all. But even if it were meant to add the cookie to the response, calling it in Startup.Configure would be completely wrong as you configure the request pipeline there. (It seems to me you're a bit confused about request handling and middlewares in ASP.NET Core so I suggest studying this topic.)
So the correct setup of the request pipeline is something like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
#region Localization
// REMARK: you may refactor this into a separate method as it's better to avoid long methods with regions
var supportedCultures = new[]
{
new CultureInfo(defaultCultureName),
new CultureInfo("pl-PL")
};
var localizationOptions = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(defaultCultureName, defaultCultureName),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures,
// you can change the list of providers, if you don't want the default behavior
// e.g. the following line enables to pick up culture ONLY from cookies
RequestCultureProviders = new[] { new CookieRequestCultureProvider() }
};
app.UseRequestLocalization(localizationOptions);
#endregion
app.UseStaticFiles();
app.UseMvc(ConfigureRoutes);
}
(A remark on the above: it's unnecessary to register RequestLocalizationOptions in the DI container.)
Then you can have some controller action setting the culture cookie:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult SetCulture(string culture, string returnUrl)
{
HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
// making cookie valid for the actual app root path (which is not necessarily "/" e.g. if we're behind a reverse proxy)
new CookieOptions { Path = Url.Content("~/") });
return Redirect(returnUrl);
}
Finally, an example how to invoke this from a view:
#using Microsoft.AspNetCore.Localization
#using Microsoft.AspNetCore.Http.Extensions
#{
var httpContext = ViewContext.HttpContext;
var currentCulture = httpContext.Features.Get<IRequestCultureFeature>().RequestCulture.UICulture;
var currentUrl = UriHelper.BuildRelative(httpContext.Request.PathBase, httpContext.Request.Path, httpContext.Request.QueryString);
}
<form asp-action="SetCulture" method="post">
Culture: <input type="text" name="culture" value="#currentCulture">
<input type="hidden" name="returnUrl" value="#currentUrl">
<input type="submit" value="Submit">
</form>
Chrome browser with language set to PL is using pl-PL culture correctly, yet Firefox is using en-US culture with language set to PL in options (despite commenting out options.DefaultRequestCulture =
new RequestCulture(defaultCulutreName, defaultCulutreName) line)
I suspect Chrome browser sends the language preference in the Accept-Language header while FF not.
Somehow my localization is working by default without using query strings nor cookies to provide culture for application, but this is not how I'd like it to work, as I do not have any control over it
I repeat:
By default this middleware will try to pick up the current culture from the request URL, cookies and Accept-Language HTTP header, in this order.
You can configure this behavior by changing or replacing the RequestLocalizationOptions.RequestCultureProviders list.

Spring's LdapTemplate search: PartialResultException: Unprocessed Continuation Reference(s); remaining name '/'

I add users through LDAP for a certain application, made with spring.
While this works for most of the cases, in some cases, it does not work...
The retrieve the users I use:
public class LdapUserServiceImpl implements ILdapUserService {
#Override
public List<LdapUserVO> getUserNamesByQuery(String query) {
return ldapTemplate.search(
query().countLimit(15)
.where("objectClass").is("user")
.and("sAMAccountName").isPresent()
.and(query()
.where("sAMAccountName").like("*" + query + "*")
.or("sAMAccountName").is(query)
.or("displayName").like("*" + query + "*")
.or("displayName").is(query))
,
new AttributesMapper<LdapUserVO>() {
public LdapUserVO mapFromAttributes(Attributes attrs) throws NamingException {
LdapUserVO ldapUser = new LdapUserVO();
Attribute attr = attrs.get(ldapUserSearch);
if (attr != null && attr.get() != null) {
ldapUser.setUserName(attr.get().toString());
}
attr = attrs.get("displayName");
if (attr != null && attr.get() != null) {
ldapUser.setDisplayName(attr.get().toString());
}
return ldapUser;
}
});
}
}
So this works in most of the cases, but sometimes I get the following error:
unprocessed continuation reference(s); remaining name "/"
I've searched a lot about this, and I explicitly set
DefaultSpringSecurityContextSource ctxSrc = new DefaultSpringSecurityContextSource(ldapUrl);
ctxSrc.setReferral("follow");
Some more info:
Search-query "admin_a" works, but "admin_ah" does not
Spring version is 4.2.5.RELEASE
Spring ldap-core version is 2.0.2.RELEASE
I think it strange that the remaining name is the root directory... Does someone has any ideas how to fix this, or even where to start looking?
Thanks in advance!
This may be related with the Active Directory being unable to handle referrals automatically. Please take a look at the LdapTemplate javadoc.
If this is the case, set the ignorePartialResultException property to true in your ldapTemplate configuration.
The reason for this error in my case was that the structure of the new AD had changed (userPrincipleName was now the emailaddress instead of login). Because of this the authentication to the AD worked fine, but no entry could be found that matched the filter, and as such didn't return any result.
So the PartialResultException was only an indication, not the reason. the reason is the lack of any result in the method searchForSingleEntryInternal of the SpringSecurityLdapTemplate class.
In my case, I had to make sure I used the correct userPrincipleName and configure the correct domain and baseDN in my ActiveDirectoryLdapAuthenticationProvider.

Grails change locale

How to change locale in grails application and force the browser to use the same. I tried this solution.
def locale = new Locale("de","DE")
RequestContextUtils.getLocaleResolver(request).setLocale(request, response, locale)
Also tried to change browser location as well nothing worked.
Any suggestions?
You could make use of params.lang. I use a grails filter to set the language on every request, e.g.
languageFilter(controller: '*', action: '*')
{
before = {
params.lang = "de"
return true;
}
}

Storing a (possibly large) file between requests in Spring

I have this controller methods that depending on the parameters introduced by the user downloads a certain PDF file and shows a view with its different pages converted to PNG.
So the way I approached it works like this:
First I map a method to receive the post data sent by the user, then generate the URL of the actual PDF converter and pass it to the model:
#RequestMapping(method = RequestMethod.POST)
public String formPost(Model model, HttpServletRequest request) {
//Gather parameters and generate PDF url
Long idPdf = Long.parseLong(request.getParam("idPdf"));
//feed the jsp the url of the to-be-generated image
model.addAttribute("image", "getImage?idPdf=" + idPdf);
}
Then in getImageMethod I download the PDF and then generate a PNG out of it:
#RequestMapping("/getImage")
public HttpEntity<byte[]> getPdfToImage(#RequestParam Long idPdf) {
String url = "myPDFrepository?idPDF=" + idPdf;
URL urlUrl = new URL(url);
URLConnection urlConnection;
urlConnection = urlUrl.openConnection();
InputStream is = urlConnection.getInputStream();
return PDFtoPNGConverter.convert(is);
}
My JSP just has an img tag that refers to this url:
<img src="${image}" />
So far this work perfectly. But now I need to allow the possibility of viewing multi page PDFs, converted as PNGS, each of them in a different page. So I would add a page parameter, then feed my model with the image url including that page parameter, and in my getImage method I would convert only that page.
But the way it is implemented, I would be downloading the PDF again for each page, plus an additional time for the view, so it can find out whether this specific PDF has more pages and then show the "prev" and "next" buttons.
What would be a good way to preserve the same file during these requests, so I download it just once? I thought about using temp files but then managing its deletion might be a problem. So maybe storing the PDF in the session would be a good solution? I don't even know if this is good practice or not.
I am using Spring MVC by the way.
I think the simplest way would be using spring cache abstraction. Look at tutorial and will need to change your code a little: move logic that load pdf to separate class.
it will looks like:
interface PDFRepository {
byte[] getImage(long id);
}
#Repository
public class PDFRepositoryImpl implements PDFRepository {
#Cacheable
public byte[] getImage(long id) {
String url = "myPDFrepository?idPDF=" + idPdf;
URL urlUrl = new URL(url);
URLConnection urlConnection;
urlConnection = urlUrl.openConnection();
InputStream is = urlConnection.getInputStream();
return PDFtoPNGConverter.convert(is);
}
}
You will get pluggable cache implementation support and good cache expiration management.

Spring 3 RESTful return on POST (create)

I am new to RESTful services and their implementation on Spring 3. I would like your opinion on the best practices for returning type when a client creates a new resource in my server.
#RequestMapping(method = RequestMethod.POST,
value = "/organisation",
headers = "content-type=application/xml")
#ResponseStatus(HttpStatus.CREATED)
public ??? createOrganisation(#RequestBody String xml)
{
StreamSource source = new StreamSource(new StringReader(xml));
Organisation organisation = (Organisation) castorMarshaller.unmarshal(source);
// save
return ???;
}
A simple choice would be javax.ws.rs.core.Response, found in the Java EE's own restful services package. It - simply - tells what the web server should answer to the HTTP request.
For instance:
if (organisation != null)
return Response.ok().build();
else
return Response.serverError().build();
Custom response headers and other exotic things like that are possible with that return type too, but I don't think that would match with "best practices".
uh, I missed that #ResponseStatus(HttpStatus.CREATED)... I guess my answer was not much of help.
Maybe this will help instead: How to return generated ID in RESTful POST?
I would go for a ResponseEntity<byte[]> and you would have take care of the marshalling of your response on your controller method. Notice that you are basically scrapping the V in MVC, there is a MarshallingView on Spring but from experience I consider the previous solution much more flexible and easier to understand.
It is a good idea to return the newly created entity(with the generated id) wrapped in ResponseEntity. You can also set the HttpStatus in ResponseEntity based on the result of the operation.
#RequestMapping(method = RequestMethod.POST,
value = "/organization",
headers = "content-type=application/xml")
public ResponseEntity<Organization> createOrganisation(#RequestBody String xml) {
StreamSource source = new StreamSource(new StringReader(xml));
Organization organisation = (Organization) castorMarshaller.unmarshal(source);
// save
return new ResponseEntity<Organization>(organization, HttpStatus.OK);
}

Resources