I wish to share with you my solution.
The validated response (with the two solutions) of the current question is really interresting.
The only problem on the first solution is to use a hard coded message key ("currentLanguage") that can disappear from the corresponding properties file.
The second one needs to hard code the basename ("fr-messages_") of the properties file. But the file name can be changed...
So, I followed the example of the validated response to extend my custom ResourceBundleMessageSource to do that.
Initialy, I needed to get the content of the Spring message properties files (messages_en.properties, messages_fr.properties, ...) because I have a full Javascript front end (using ExtJs). So, I needed to load all the (internationalized) labels of the application on a JS object.
But it doesn't exist... For this reason, I have developed a custom ReloadableResourceBundleMessageSource class. The corresponding methods are "getAllProperties()", "getAllPropertiesAsMap()" and "getAllPropertiesAsMessages()".
Later, I needed to get the available Locales on the application. And reading this stackoverflow page, I had the idea to extend my ReloadableResourceBundleMessageSource class to do that. You can see the "getAvailableLocales()" and "isAvailableLocale()" (to test just one Locale) methods.
package fr.ina.archibald.web.support;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.LocaleUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ReflectionUtils;
import fr.ina.archibald.commons.util.StringUtils;
import fr.ina.archibald.entity.MessageEntity;
/**
* Custom {@link org.springframework.context.support.ReloadableResourceBundleMessageSource}.
*
* @author srambeau
*/
public class ReloadableResourceBundleMessageSource extends org.springframework.context.support.ReloadableResourceBundleMessageSource {
private static final Logger LOGGER = LoggerFactory.getLogger(ReloadableResourceBundleMessageSource.class);
private static final String PROPERTIES_SUFFIX = ".properties";
private static final String XML_SUFFIX = ".xml";
private Set<Locale> cacheAvailableLocales;
private Set<Resource> cacheResources;
/**
* Returns all messages for the specified {@code Locale}.
*
* @param locale the {@code Locale}.
*
* @return a {@code Properties} containing all the expected messages or {@code null} if the {@code locale} argument is null or if the properties are empty.
*/
public Properties getAllProperties(final Locale locale) {
if(locale == null) {
LOGGER.debug("Cannot get all properties. 'locale' argument is null.");
return null;
}
return getMergedProperties(locale).getProperties();
}
/**
* Returns all messages for the specified {@code Locale}.
*
* @param locale the {@code Locale}.
*
* @return a {@code Map} containing all the expected messages or {@code null} if the {@code locale} argument is null or if the properties are empty.
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public Map<String, String> getAllPropertiesAsMap(final Locale locale) {
if(locale == null) {
LOGGER.debug("Cannot get all properties as Map. 'locale' argument is null.");
return null;
}
Properties props = getAllProperties(locale);
if(props == null) {
LOGGER.debug("Cannot get all properties as Map. The properties are missing.");
return null;
}
return new HashMap<String, String>((Map) props);
}
/**
* Returns all messages for the specified {@code Locale}.
*
* @param locale the {@code Locale}.
*
* @return a {@code List<MessageEntity>} containing all the expected messages or {@code null} if the {@code locale} argument is null or if the properties are empty.
*/
public List<MessageEntity> getAllPropertiesAsMessages(final Locale locale) {
if(locale == null) {
LOGGER.debug("Cannot get all properties as MessageEntity. 'locale' argument is null.");
return null;
}
Properties props = getAllProperties(locale);
if(props == null) {
LOGGER.debug("Cannot get all properties as MessageEntity. The properties are missing.");
return null;
}
Set<Entry<Object, Object>> propsSet = props.entrySet();
List<MessageEntity> messages = new ArrayList<MessageEntity>();
for(Entry<Object, Object> prop : propsSet) {
messages.add(new MessageEntity((String) prop.getKey(), (String) prop.getValue()));
}
return messages;
}
/**
* Returns the available {@code Locales} on the specified application context. Calculated from the Spring message files of the application context.
* <p>
* Example of Locales returned corresponding with the messages files defines on the application:
*
* <pre>
* messages_en.properties --> en
* messages_fr.properties --> fr
* messages_en.properties, messages_fr.properties --> en, fr
* </pre>
* </p>
*
* @return the set of {@code Locales} or null if an error occurs.
*/
public Set<Locale> getAvailableLocales() {
if(cacheAvailableLocales != null) {
return cacheAvailableLocales;
}
cacheAvailableLocales = getLocales(getAllFileNames(), getMessageFilePrefixes());
return cacheAvailableLocales;
}
/**
* Indicates if the specified {@code Locale} is available on the application.
* <p>
* Examples of results returned if the application contains the files "messages_en.properties" and "messages_fr.properties":
*
* <pre>
* en --> true
* fr --> true
* de --> false
* es --> false
* </pre>
*
* @param locale the {@code Locale}.
*
* @return {@code true} if the locale is available, {@code false} otherwise.
*/
public boolean isAvailableLocale(final Locale locale) {
Set<Locale> locales = getAvailableLocales();
if(locales == null) {
return false;
}
return locales.contains(locale);
}
// ********************** PRIVATE METHODES **********************
/**
* Returns the {@code Locales} specified on the file names.
*
* @param fileNames the file names.
* @param filePrefixes the basenames' prefixes of the resources bundles.
*
* @return the set of the {@code Locales}.
*/
private Set<Locale> getLocales(final List<String> fileNames, List<String> filePrefixes) {
if(fileNames == null || fileNames.isEmpty() || filePrefixes == null || filePrefixes.isEmpty()) {
LOGGER.debug("Cannot get available Locales. fileNames=[" + StringUtils.toString(fileNames) + "], filePrefixes=[" + StringUtils.toString(filePrefixes) + "]");
return null;
}
Set<Locale> locales = new HashSet<Locale>();
for(String fileName : fileNames) {
String fileNameWithoutExtension = FilenameUtils.getBaseName(fileName);
for(String filePrefixe : filePrefixes) {
String localeStr = fileNameWithoutExtension.substring(filePrefixe.length() + 1);
try {
locales.add(LocaleUtils.toLocale(localeStr));
} catch(IllegalArgumentException ex) {
continue;
}
}
}
return locales;
}
/**
* Returns all the file names of the resources bundles.
*
* @return the list of file names or {@code null} if the resources are missing.
*/
private List<String> getAllFileNames() {
Set<Resource> resources = getAllResources();
if(resources == null) {
LOGGER.debug("Missing resources bundles.");
return null;
}
List<String> filenames = new ArrayList<String>(resources.size());
for(Resource resource : resources) {
filenames.add(resource.getFilename());
}
return filenames;
}
/**
* Gets the array of the prefixes for messages files.
*
* <pre>
* "WEB-INF/messages" --> "messages"
* "classpath:config/i18n/messages" --> "messages"
* "messages" --> "messages"
* </pre>
*
* @return the array of the prefixes or null if an error occurs.
*/
private List<String> getMessageFilePrefixes() {
String[] basenames = getBasenames();
if(basenames == null) {
LOGGER.debug("Missing basenames of the resources bundles.");
return null;
}
List<String> prefixes = new ArrayList<String>(basenames.length);
for(int i = 0; i < basenames.length; ++i) {
prefixes.add(FilenameUtils.getName(basenames[i]));
}
return prefixes;
}
/**
* Returns all the resources bundles.
*
* @return the set of resources or null if {@code basenames} or the {@link ResourceLoader} is missing.
*/
private Set<Resource> getAllResources() {
if(cacheResources != null) {
return cacheResources;
}
String[] basenames = getBasenames();
if(basenames == null) {
LOGGER.debug("Missing basenames of the resources bundles.");
return null;
}
ResourceLoader resourceLoader = getResourceLoader();
if(resourceLoader == null) {
LOGGER.debug("Missing ResourceLoader.");
return null;
}
Set<Resource> resources = new HashSet<Resource>();
for(String basename : basenames) {
for(Locale locale : Locale.getAvailableLocales()) {
List<String> filenames = calculateFilenamesForLocale(basename, locale);
for(String filename : filenames) {
Resource resource = resourceLoader.getResource(filename + PROPERTIES_SUFFIX);
if( ! resource.exists()) {
resource = resourceLoader.getResource(filename + XML_SUFFIX);
}
if(resource.exists()) {
resources.add(resource);
}
}
}
}
cacheResources = resources;
return resources;
}
/**
* Gets the array of basenames, each following the basic ResourceBundle convention of not specifying file extension or language codes.
*
* @return the array of basenames or null if an error occurs.
*
* @see org.springframework.context.support.ReloadableResourceBundleMessageSource#setBasenames
*/
private String[] getBasenames() {
Field field = ReflectionUtils.findField(org.springframework.context.support.ReloadableResourceBundleMessageSource.class, "basenames");
if(field == null) {
LOGGER.debug("Missing field 'basenames' from 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
return null;
}
ReflectionUtils.makeAccessible(field);
try {
return (String[]) field.get(this);
} catch(Exception ex) {
LOGGER.debug("Unable to get the 'basenames' field value from the 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
return null;
}
}
/**
* Gets the resource loader.
*
* @return the resource loader.
*
* @see org.springframework.context.support.ReloadableResourceBundleMessageSource#setResourceLoader
*/
private ResourceLoader getResourceLoader() {
Field field = ReflectionUtils.findField(org.springframework.context.support.ReloadableResourceBundleMessageSource.class, "resourceLoader");
if(field == null) {
LOGGER.debug("Missing field 'resourceLoader' from 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
return null;
}
ReflectionUtils.makeAccessible(field);
try {
return (ResourceLoader) field.get(this);
} catch(Exception ex) {
LOGGER.debug("Unable to get the 'resourceLoader' field value from the 'org.springframework.context.support.ReloadableResourceBundleMessageSource' class.");
return null;
}
}
}
If you want to use the two functionnalities (get the available Locales and get all Spring messages from the properties files), so you need to get this complete class.
To use this ReloadableResourceBundleMessageSource, it is really simple.
You need to declare the resource bundle :
<!-- Custom message source. -->
<bean id="messageSource" class="fr.ina.archibald.web.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:config/i18n/messages" />
<property name="defaultEncoding" value="UTF-8" />
</bean>
Then, you just need to inject the resource bundle into the class where you want to get the available Locales:
@Inject
private ReloadableResourceBundleMessageSource resourceBundleMessageSource;
Here is a usage example to check if the Locale is available before automatically update the browsing Locale of the User on database when the Spring LocaleChangeInterceptor detect a change (via URL for example => 'http://your.domain?lang=en'):
package fr.ina.archibald.web.resolver;
import java.util.Locale;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import fr.ina.archibald.commons.annotation.Log;
import fr.ina.archibald.dao.entity.UserEntity;
import fr.ina.archibald.security.entity.CustomUserDetails;
import fr.ina.archibald.security.util.SecurityUtils;
import fr.ina.archibald.service.UserService;
import fr.ina.archibald.web.support.ReloadableResourceBundleMessageSource;
/**
* Custom SessionLocaleResolver.
*
* @author srambeau
*
* @see org.springframework.web.servlet.i18n.SessionLocaleResolver
*/
public class SessionLocaleResolver extends org.springframework.web.servlet.i18n.SessionLocaleResolver {
@Log
private Logger logger;
@Inject
private UserService userService;
@Inject
private ReloadableResourceBundleMessageSource resourceBundleMessageSource;
@Override
public void setLocale(HttpServletRequest req, HttpServletResponse res, Locale newLocale) {
super.setLocale(req, res, newLocale);
updateUserLocale(newLocale);
}
// /**
// * Returns the default Locale that this resolver is supposed to fall back to, if any.
// */
// @Override
// public Locale getDefaultLocale() {
// return super.getDefaultLocale();
// }
// ********************** PRIVATE METHODES **********************
/**
* Updates the locale of the currently logged in user with the new Locale.
* <p>
* The locale is not updated if the specified locale is {@code null} or the same as the previous, if the user is missing or if an error occurs.
* </p>
*
* @param newLocale the new locale.
*/
private void updateUserLocale(final Locale newLocale) {
if(newLocale == null) {
logger.debug("Cannot update the user's browsing locale. The new locale is null.");
return;
}
CustomUserDetails userDetails = SecurityUtils.getCurrentUser();
if(userDetails == null || userDetails.getUser() == null) {
logger.debug("Cannot update the user's browsing locale. The user is missing.");
return;
}
UserEntity user = userDetails.getUser();
// Updates the user locale if and only if the locale has changed and is available on the application.
if(newLocale.equals(user.getBrowsingLocale()) || ! resourceBundleMessageSource.isAvailableLocale(newLocale)) {
return;
}
user.setBrowsingLocale(newLocale);
try {
userService.update(user);
} catch(Exception ex) {
logger.error("The browsing locale of the user with identifier " + user.getUserId() + " cannot be updated.", ex);
}
}
}
The corresponding SessionLocaleResolver declaration:
<!-- This custom SessionLocaleResolver allows to update the user Locale when it change. -->
<bean id="localeResolver" class="fr.ina.archibald.web.resolver.SessionLocaleResolver">
<property name="defaultLocale" value="fr" />
</bean>
I hope this will be useful to you...
Enjoy! :-)