Missing functionality between Accept-Language header and ResourceBundle - java

Google foo failed me. I want to find out if there is a standard "by the book" way of transforming the input locales from Accept-Language header to correct ResourceBundle.
ResourceBundle::getBundle() method(s) accepts a single locale but Accept-Language can have multiple locales weighted by index, eg: de;q=1.0, sl;q=0.9.
Current code:
#Context
private HttpServletRequest request;
public String getString(String key) {
ResourceBundle i18n = ResourceBundle.getBundle("locale/strings", this.request.getLocale());
return i18n.getString(key);
}
The problem is that getLocale() returns the preferred locale, in this case de. If available resource bundles are sl and en, this will try to find de and then fallback to en, but the actual expected result by the client is sl!
My question is basically, do I have to implement a custom fallback code that iterates over HttpServletRequest.getLocales() (I don't want to reinvent the wheel..) or is there a more standard and straightforward way of doing this? I'd also settle for some 3rd party lib that fills this gap.
Custom solution so far:
#RequestScoped
public class Localization {
#Context
private HttpServletRequest request;
private ResourceBundle i18n;
#PostConstruct
void postConstruct() {
//List of locales from Accept-Language header
List<Locale> locales = Collections.list(request.getLocales());
if (locales.isEmpty()) {
//Fall back to default locale
locales.add(request.getLocale());
}
for (Locale locale : locales) {
try {
i18n = ResourceBundle.getBundle("bundles/translations", locale);
if (!languageEquals(i18n.getLocale(), locale)) {
//Default fallback detected
//The resource bundle that was returned has different language than the one requested, continue
//Only language tag is checked, no support for detecting different regions in this sample
continue;
}
break;
}
catch (MissingResourceException ignore) {
}
}
}
private boolean languageEquals(Locale first, Locale second) {
return getISO2Language(first).equalsIgnoreCase(getISO2Language(second));
}
private String languageGetISO2(Locale locale) {
String[] localeStrings = (locale.getLanguage().split("[-_]+"));
return localeStrings[0];
}
public ResourceBundle i18n() {
return this.i18n;
}
}

I would write an Interceptor, there you can set the language you want and apply the logic you want into a ThreadLocal or pass it down.
i.e you check against the available languages and define an order or set a default.
If you use Spring, you could then set LocaleContextHolder manualy or use the LocaleContextResolver instead of writing a own interceptor.

Related

How to send json with GET params via Spring Boot / Tomcat?

so currently I'm working on a project where we have product objects which in turn contain "Origin" objects (containing region: String and country: String).
What I'm trying to do is a RestController which takes in an optional Origin object and does something with it (e.g. logs it).
This is what I have right now:
#GetMapping("search")
public Page<Wine> getProductByStuff(
#RequestParam(required = false) Origin origin,
/* other attributes */) {
log.info(origin); // it has a proper toString method.
}
There are two problem with this approach. First of all, when I send a request like:
http://[...]/search?origin={"region":"blah","country":"UK"}
or even the html converted string like:
http://[...]/search?origin={%22region%22:%22blah%22%44%22country%22:%22UK%22}
... it says
Invalid character found in the request target [/api/products/search?origin={%22region%22:%22blah%22%44%22country%22:%22DE%22}]. The valid characters are defined in RFC 7230 and RFC 3986.
Afaik the only valid characters Tomcat has that I need are {}. All others I've replaced with the html encoded chars and it still doesn't work.
What I did to prevent this:
#Component
public class TomcatWebServerCustomizer
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
#Override
public void customize(TomcatServletWebServerFactory factory) {
TomcatConnectorCustomizer a = null;
factory.addConnectorCustomizers(connector -> {
connector.setAttribute("relaxedPathChars", "<>[\\]^`{|},\"");
connector.setAttribute("relaxedQueryChars", "<>[\\]^`{|},\"");
});
}
}
(See this, which is, by the way, deprecated (at least connector.setAttribute).)
This produced:
MethodArgumentConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type '[censored].backend.model.Origin'.
My questions are:
(How) is it possible to configure Tomcat/Spring so that they can actually accept json in the url params?
How would I format it in e.g. Postman so that it would work? Currently I'm just converting special characters by hand in the params tab of Postman.
Here is what you need to do if you want to send it as json query param.
#RestController
public class OriginController {
#GetMapping("/search")
public void getOrigin(#RequestParam(value = "origin", required = false)
Origin origin) {
System.out.println(origin);
}
}
Register a converter
#Component
public class StringToOriginConverter implements
Converter<String, Origin> {
ObjectMapper objectMapper = new ObjectMapper();
#Override
public Origin convert(String source) {
try {
return objectMapper.readValue(source, Origin.class);
} catch (JsonProcessingException e) {
//You could throw some exception here instead for custom error
return null;
}
}
}
Sending from postman
Note
My answer is not debating whether you should use POST or GET as it is not what you have asked. It is just providing one option if you want to send some payload as query param
As mentioned, don't use JSON as a path parameter.
Directly use path parameters, and convert to Origin object.
#GetMapping("search")
public Page<Wine> getProductByStuff(
#RequestParam(required = false) String region,
#RequestParam(required = false) String country, /* other attributes */) {
Origin origin = new Origin(region, country);
log.info(origin); // it has a proper toString method.
}

ThreadLocal get() gives unexpected result when set() method is not invoked

I have base class which contains ThreadLocal :
#Singleton
public class BaseView extends HttpServlet {
protected ThreadLocal<Locale> locale = new ThreadLocal<Locale>();
private Locale getLocale() {
return (Locale) ObjectUtils.defaultIfNull(locale.get(), Locale.ENGLISH);
}
...
}
And it is extended in EmailValidatedView:
#Singleton
public class EmailValidatedView extends BaseView {
#Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String token = req.getParameter("token");
if (token != null) {
try {
User user = userService.validateEmail(token);
locale.set(user.parseLocale());
} catch (ServiceException e) {
e.printStackTrace();
}
}
sendResponse("validatedEmail.vm", resp.getWriter(), $());
}
}
When token is invalid, I get ServiceException and locale is not set. In this case sendResponse() method should use default locale - English. However, if I make refresh for the same page in browser with invalid token, I may get different/non related languages every time. Why does it happen?
Most HTTP servers reuse threads from a pool. Keeping a thread-safe config is a good thing, and ThreadLocal can help, but your logic is structured such that your locale will not be reset every request. Therefore, it's unsurprising that the thread's old locales from past requests will bleed through.
You'll need to ensure your locale is set every single request that reads it, and that the default locale is set (or equivalently that the ThreadLocal value is cleared) for every new request.

Something basic about Java pointers and changing locale in JSF application

I just tried to implement JSF Internationalization based on this article - "Internationalization in JSF with UTF-8 encoded properties files" and found something weird. Is it right way to change locale by using code in this bean?
#ManagedBean
#SessionScoped
public class LocaleBean {
private Locale locale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
public Locale getLocale() {
return locale;
}
public String getLanguage() {
return locale.getLanguage();
}
public void setLanguage(String language) {
this.locale = new Locale(language);
}
}
As I understand Java private Locale locale must be pointer to actual Locale object from viewRoot object but this method didn't work at me. Instead, when I changed setLanguage(String language) method to this
public void setLanguage(String language) {
FacesContext.getCurrentInstance().getViewRoot().setLocale(new Locale(language));
}
it began to work. Now I wonder where is mistake? What's wrong with #BulusC code? Maybe I did something wrong, maybe I forget something?
When I debugged I seen that private Locale locale and locale object from viewRoot are different objects.
Indeed, the code was missing the line you've posted. From this question: Localization in JSF, how to remember selected locale per session instead of per request/view, answered by BalusC, you can check the code for setLanguage (code taken from BalusC's answer, not mine):
public void setLanguage(String language) {
locale = new Locale(language);
//this is the line you added
FacesContext.getCurrentInstance().getViewRoot().setLocale(locale);
}

Unifying enum.values() across multiple human languages

My Android app uses an enum type to define certain API endpoints.
public static enum API_ENDPOINT{
MISSION, FEATURED_MEDIA
}
The enum type seems an appropriate argument for methods that are dependent on the API call type, but I'm unable to translate enums to consistent Strings (i.e for mapping to API endpoint urls) across devices configured with different languages.
In Turkish API_ENDPOINT.values() returns: mıssıon, featured_medıa
In English API_ENDPOINT.values() returns: mission, featured_media
An obvious solution is an additional data structure that maps API_ENDPOINT to hard-coded string endpoints, but I'm curious as to whether this behavior of enum.values() is intended and/or avoidable.
Solved: Thanks everyone for the insight. It turns out deeper in the logic to convert API_ENDPOINT to a URL string I used String.toLowerCase() without specifying a Locale, which resulted in the undesirable behavior. This has been replaced with String.toLowerCase(Locale.US)
You can hard-code the strings as part of the enum, without any additional data structure:
public static enum API_ENDPOINT{
MISSION("mission"), FEATURED_MEDIA("featured_media");
private final String value;
API_ENDPOINT(String value) { this.value = value; }
public String value() { return value; }
}
but it would be nice if there were just a way to control the representation that's automatically generated.
The JLS enum section doesn't speak directly to language differences like this, but strongly suggests that the output would exactly match the enum identifiers; I'm surprised that you'd even get lower-case strings with upper-case identifiers.
After further testing, this isn't reproducible, something else must be going on in your code.
This minimal program displays the enum identifiers exactly as typed regardless of locale:
public class MainActivity extends Activity {
public enum ENUM {
MISSION, FEATURED_MEDIA
}
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
TextView textView = (TextView) findViewById(R.id.text);
String enums = "";
for (ENUM e : ENUM.values()) {
enums += e + " ";
}
textView.setText(enums);
}
}
You can define two property-files. One for English and one for Turkish.The Enum could then look like this:
public static enum API_ENDPOINT{
MISSION("path.to.property.mission"), FEATURED_MEDIA("path.to.property.featured_media");
private String propertyName;
API_ENDPOINT(String propertyName){
this.propertyName = propertyName;
}
// language could also be an enum which defines the language to be taken
// and should contain the path to the file.
public String getTranslatedText(Language language){
Properties prop = new Properties();
try {
//load a properties file from class path
prop.load(API_ENDPOINT.class.getClassLoader().getResourceAsStream(language.getPropertyFileName()));
//get the translated value and raturn it.
return prop.getProperty(propertyName);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
The Property-File will look like this (English):
path.to.property.mission=Mission
path.to.property.featured_media=Featured Media
Same goes for Turkish.
Hope that helps.
EDIT: Due to you are using Android, this might be the solution for your problem:
Is there a sensible way to refer to application resources (R.string...) in static initializers
Make Enum.toString() localized

Make Enum.toString() localized

I'm developing an Android application and I want to know if I can set Enum.toString() multilanguage.
I'm going to use this Enum on a Spinner and I want to use multi language texts.
public class Types
{
public enum Stature
{
tall (0, "tall"),
average(1, "average"),
small(2, "small");
private final int stature;
private final String statureString;
Stature(int anStature, String anStatureString) { stature = anStature; statureString = anStatureString; }
public int getValue() { return stature; }
#Override
public String toString() { return statureString; }
}
}
I don't know how to use Context.getString() inside an Enum, and I have hardcoded "tall", "average" and "small" to test it. I have defined that enum inside on a helper class.
This how I use the enum on a Spinner:
mSpinStature.setAdapter(new ArrayAdapter<Stature>(mActivity, android.R.layout.simple_dropdown_item_1line, Stature.values()));
Do you know how can I do it?
I created a simple library which is a part of my big project (Xdroid):
compile 'com.shamanland:xdroid-enum-format:0.2.4'
Now you can avoid the same monkey-job (declaring field, constructor, etc) for all enumetations by using annotations:
public enum State {
#EnumString(R.string.state_idle)
IDLE,
#EnumString(R.string.state_pending)
PENDING,
#EnumString(R.string.state_in_progress)
IN_PROGRESS,
#EnumString(R.string.state_cancelled)
CANCELLED,
#EnumString(R.string.state_done)
DONE;
}
And then use the common Java approach - use extensions of class java.text.Format:
public void onStateChanged(State state) {
EnumFormat enumFormat = EnumFormat.getInstance();
toast(enumFormat.format(state));
}
strings.xml
<string name="state_idle">Idle</string>
<string name="state_pending">Pending</string>
<string name="state_in_progress">In progress</string>
<string name="state_cancelled">Cancelled</string>
<string name="state_done">Done</string>
Look here how to show Toast simply.
You can also compile a demo app from github.
Assume this resource path
String resourceBundlePath = "my.package.bundles.messages"
In package my.package.bundles you may have messages.properties, messages_en_US.properties etc.
Then, using
ResourceBundle resourceBundle = ResourceBundle.getBundle(resourceBundlePath);
String messageKey = "myFirstMessage";
String message = resourceBundle.getMessage(messageKey);
message will contain the value of the messageKey property defined on messages.properties. If the current Locale is actually en_US you will get the value from messages_en_US.properties. If the current locale is something you do not have a properties file for the value will be from the default messages.properties
You can also call
ResourceBundle.getBundle(resourceBundlePath, myLocale);
but it is generally better to use the platform locale (have a look at jvm arguments -Duser.language, -Duser.country)
You can have a ResourceBundle for each enum you want to translate with keys the enum element names and use it in the toString() implementation of your enum:
#Override
public String toString() {
return resourceBudle.getString(super.toString());
}
I would leave enum as is and use the standard ResourceBundle approach http://docs.oracle.com/javase/tutorial/i18n/resbundle/concept.html using Enum.toString as the key
#Override
public String toString()
{
//java
return ResourceBundle.getBundle().getString(id);
//android?
App.getContext().getString(id);
}

Categories