All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.mice.voila.runtime.util.ResourceBundleMessageResolver Maven / Gradle / Ivy

package it.mice.voila.runtime.util;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.support.AbstractMessageSource;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.Assert;
import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.PropertiesPersister;
import org.springframework.util.StringUtils;

public class ResourceBundleMessageResolver extends
		AbstractMessageSource implements ResourceLoaderAware {

	private static final String PROPERTIES_SUFFIX = ".properties";
	private static final String XML_SUFFIX = ".xml";
	private String[] basenames = new String[0];
	private String defaultEncoding;
	private Properties fileEncodings;
	private boolean fallbackToSystemLocale = true;
	private long cacheMillis = -1;
	private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
	private ResourceLoader resourceLoader = new DefaultResourceLoader();
	/**
	 * Cache to hold filename lists per Locale
	 */
	private final Map>> cachedFilenames = new HashMap>>();
	/**
	 * Cache to hold already loaded properties per filename
	 */
	private final Map cachedProperties = new HashMap();
	/**
	 * Cache to hold merged loaded properties per locale
	 */
	private final Map cachedMergedProperties = new HashMap();

	/**
	 * Set a single basename, following the basic ResourceBundle convention of
	 * not specifying file extension or language codes, but in contrast to
	 * {@link ResourceBundleMessageSource} referring to a Spring resource
	 * location: e.g. "WEB-INF/messages" for "WEB-INF/messages.properties",
	 * "WEB-INF/messages_en.properties", etc.
	 * 

* XML properties files are also supported: .g. "WEB-INF/messages" will find * and load "WEB-INF/messages.xml", "WEB-INF/messages_en.xml", etc as well. * * @param basename * the single basename * @see #setBasenames * @see org.springframework.core.io.ResourceEditor * @see java.util.ResourceBundle */ public void setBasename(String basename) { setBasenames(new String[] { basename }); } /** * Set an array of basenames, each following the basic ResourceBundle * convention of not specifying file extension or language codes, but in * contrast to {@link ResourceBundleMessageSource} referring to a Spring * resource location: e.g. "WEB-INF/messages" for * "WEB-INF/messages.properties", "WEB-INF/messages_en.properties", etc. *

* XML properties files are also supported: .g. "WEB-INF/messages" will find * and load "WEB-INF/messages.xml", "WEB-INF/messages_en.xml", etc as well. *

* The associated resource bundles will be checked sequentially when * resolving a message code. Note that message definitions in a * previous resource bundle will override ones in a later bundle, due * to the sequential lookup. * * @param basenames * an array of basenames * @see #setBasename * @see java.util.ResourceBundle */ public void setBasenames(String[] basenames) { if (basenames != null) { this.basenames = new String[basenames.length]; for (int i = 0; i < basenames.length; i++) { String basename = basenames[i]; Assert.hasText(basename, "Basename must not be empty"); this.basenames[i] = basename.trim(); } } else { this.basenames = new String[0]; } } /** * Set the default charset to use for parsing properties files. Used if no * file-specific charset is specified for a file. *

* Default is none, using the java.util.Properties default * encoding. *

* Only applies to classic properties files, not to XML files. * * @param defaultEncoding * the default charset * @see #setFileEncodings * @see org.springframework.util.PropertiesPersister#load */ public void setDefaultEncoding(String defaultEncoding) { this.defaultEncoding = defaultEncoding; } /** * Set per-file charsets to use for parsing properties files. *

* Only applies to classic properties files, not to XML files. * * @param fileEncodings * Properties with filenames as keys and charset names as values. * Filenames have to match the basename syntax, with optional * locale-specific appendices: e.g. "WEB-INF/messages" or * "WEB-INF/messages_en". * @see #setBasenames * @see org.springframework.util.PropertiesPersister#load */ public void setFileEncodings(Properties fileEncodings) { this.fileEncodings = fileEncodings; } /** * Set whether to fall back to the system Locale if no files for a specific * Locale have been found. Default is "true"; if this is turned off, the * only fallback will be the default file (e.g. "messages.properties" for * basename "messages"). *

* Falling back to the system Locale is the default behavior of * java.util.ResourceBundle. However, this is often not * desirable in an application server environment, where the system Locale * is not relevant to the application at all: Set this flag to "false" in * such a scenario. * @param fallbackToSystemLocale true if this is turned off, otherwise false. */ public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) { this.fallbackToSystemLocale = fallbackToSystemLocale; } /** * Set the number of seconds to cache loaded properties files. *

    *
  • Default is "-1", indicating to cache forever (just like * java.util.ResourceBundle). *
  • A positive number will cache loaded properties files for the given * number of seconds. This is essentially the interval between refresh * checks. Note that a refresh attempt will first check the last-modified * timestamp of the file before actually reloading it; so if files don't * change, this interval can be set rather low, as refresh attempts will not * actually reload. *
  • A value of "0" will check the last-modified timestamp of the file on * every message access. Do not use this in a production environment! *
* * @param cacheSeconds set cache in seconds. */ public void setCacheSeconds(int cacheSeconds) { this.cacheMillis = (cacheSeconds * 1000); } /** * Set the PropertiesPersister to use for parsing properties files. *

* The default is a DefaultPropertiesPersister. * * @see org.springframework.util.DefaultPropertiesPersister * * @param propertiesPersister set the persist of properties */ public void setPropertiesPersister(PropertiesPersister propertiesPersister) { this.propertiesPersister = (propertiesPersister != null ? propertiesPersister : new DefaultPropertiesPersister()); } /** * Set the ResourceLoader to use for loading bundle properties files. *

* The default is a DefaultResourceLoader. Will get overridden by the * ApplicationContext if running in a context, as it implements the * ResourceLoaderAware interface. Can be manually overridden when running * outside of an ApplicationContext. * * @see org.springframework.core.io.DefaultResourceLoader * @see org.springframework.context.ResourceLoaderAware */ public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); } /** * Resolves the given message code as key in the retrieved bundle files, * returning the value found in the bundle as-is (without MessageFormat * parsing). */ @Override protected String resolveCodeWithoutArguments(String code, Locale locale) { if (this.cacheMillis < 0) { ResourceBundleMessageResolver.PropertiesHolder propHolder = getMergedProperties(locale); String result = propHolder.getProperty(code); if (result != null) { return result; } } else { for (String basename : this.basenames) { List filenames = calculateAllFilenames(basename, locale); for (String filename : filenames) { ResourceBundleMessageResolver.PropertiesHolder propHolder = getProperties(filename); String result = propHolder.getProperty(code); if (result != null) { return result; } } } } return null; } /** * Resolves the given message code as key in the retrieved bundle files, * using a cached MessageFormat instance per message code. */ @Override protected MessageFormat resolveCode(String code, Locale locale) { if (this.cacheMillis < 0) { ResourceBundleMessageResolver.PropertiesHolder propHolder = getMergedProperties(locale); MessageFormat result = propHolder.getMessageFormat(code, locale); if (result != null) { return result; } } else { for (String basename : this.basenames) { List filenames = calculateAllFilenames(basename, locale); for (String filename : filenames) { ResourceBundleMessageResolver.PropertiesHolder propHolder = getProperties(filename); MessageFormat result = propHolder.getMessageFormat(code, locale); if (result != null) { return result; } } } } return null; } /** * Get a PropertiesHolder that contains the actually visible properties for * a Locale, after merging all specified resource bundles. Either fetches * the holder from the cache or freshly loads it. *

* Only used when caching resource bundle contents forever, i.e. with * cacheSeconds < 0. Therefore, merged properties are always cached forever. * * @param locale the locale. * @return the mergedHolder. */ protected ResourceBundleMessageResolver.PropertiesHolder getMergedProperties( Locale locale) { synchronized (this.cachedMergedProperties) { ResourceBundleMessageResolver.PropertiesHolder mergedHolder = this.cachedMergedProperties .get(locale); if (mergedHolder != null) { return mergedHolder; } Properties mergedProps = new Properties(); mergedHolder = new ResourceBundleMessageResolver.PropertiesHolder( mergedProps, -1); for (int i = this.basenames.length - 1; i >= 0; i--) { List filenames = calculateAllFilenames(this.basenames[i], locale); for (int j = filenames.size() - 1; j >= 0; j--) { String filename = (String) filenames.get(j); ResourceBundleMessageResolver.PropertiesHolder propHolder = getProperties(filename); if (propHolder.getProperties() != null) { mergedProps.putAll(propHolder.getProperties()); } } } this.cachedMergedProperties.put(locale, mergedHolder); return mergedHolder; } } /** * Calculate all filenames for the given bundle basename and Locale. Will * calculate filenames for the given Locale, the system Locale (if * applicable), and the default file. * * @param basename * the basename of the bundle * @param locale * the locale * @return the List of filenames to check * @see #setFallbackToSystemLocale * @see #calculateFilenamesForLocale */ protected List calculateAllFilenames(String basename, Locale locale) { synchronized (this.cachedFilenames) { Map> localeMap = this.cachedFilenames .get(basename); if (localeMap != null) { List filenames = localeMap.get(locale); if (filenames != null) { return filenames; } } List filenames = new ArrayList(7); filenames.addAll(calculateFilenamesForLocale(basename, locale)); if (this.fallbackToSystemLocale && !locale.equals(Locale.getDefault())) { List fallbackFilenames = calculateFilenamesForLocale( basename, Locale.getDefault()); for (String fallbackFilename : fallbackFilenames) { if (!filenames.contains(fallbackFilename)) { // Entry for fallback locale that isn't already in // filenames list. filenames.add(fallbackFilename); } } } filenames.add(basename); if (localeMap != null) { localeMap.put(locale, filenames); } else { localeMap = new HashMap>(); localeMap.put(locale, filenames); this.cachedFilenames.put(basename, localeMap); } return filenames; } } /** * Calculate the filenames for the given bundle basename and Locale, * appending language code, country code, and variant code. E.g.: basename * "messages", Locale "de_AT_oo" -> "messages_de_AT_OO", "messages_de_AT", * "messages_de". *

* Follows the rules defined by {@link java.util.Locale#toString()}. * * @param basename * the basename of the bundle * @param locale * the locale * @return the List of filenames to check */ protected List calculateFilenamesForLocale(String basename, Locale locale) { List result = new ArrayList(3); String language = locale.getLanguage(); String country = locale.getCountry(); String variant = locale.getVariant(); StringBuilder temp = new StringBuilder(basename); temp.append('_'); if (language.length() > 0) { temp.append(language); result.add(0, temp.toString()); } temp.append('_'); if (country.length() > 0) { temp.append(country); result.add(0, temp.toString()); } if (variant.length() > 0 && (language.length() > 0 || country.length() > 0)) { temp.append('_').append(variant); result.add(0, temp.toString()); } return result; } /** * Get a PropertiesHolder for the given filename, either from the cache or * freshly loaded. * * @param filename * the bundle filename (basename + Locale) * @return the current PropertiesHolder for the bundle */ protected ResourceBundleMessageResolver.PropertiesHolder getProperties( String filename) { synchronized (this.cachedProperties) { ResourceBundleMessageResolver.PropertiesHolder propHolder = this.cachedProperties .get(filename); if (propHolder != null && (propHolder.getRefreshTimestamp() < 0 || propHolder .getRefreshTimestamp() > System.currentTimeMillis() - this.cacheMillis)) { // up to date return propHolder; } return refreshProperties(filename, propHolder); } } /** * Refresh the PropertiesHolder for the given bundle filename. The holder * can be null if not cached before, or a timed-out cache entry * (potentially getting re-validated against the current last-modified * timestamp). * * @param filename * the bundle filename (basename + Locale) * @param propHolder * the current PropertiesHolder for the bundle * * @return the propHolder. */ protected ResourceBundleMessageResolver.PropertiesHolder refreshProperties( String filename, ResourceBundleMessageResolver.PropertiesHolder propHolder) { long refreshTimestamp = (this.cacheMillis < 0) ? -1 : System .currentTimeMillis(); Resource[] resources = null; try { if (this.resourceLoader instanceof ResourcePatternResolver) { resources = ((ResourcePatternResolver) this.resourceLoader) .getResources(filename + PROPERTIES_SUFFIX); if (resources == null || resources.length == 0) { resources = ((ResourcePatternResolver) this.resourceLoader) .getResources(filename + XML_SUFFIX); } } else { Resource resource = this.resourceLoader.getResource(filename + PROPERTIES_SUFFIX); if (!resource.exists()) { resource = this.resourceLoader.getResource(filename + XML_SUFFIX); } resources = new Resource[1]; resources[0] = resource; } if (resources != null && resources.length > 0) { propHolder = new ResourceBundleMessageResolver.PropertiesHolder(); for (Resource resource : resources) { if (resource.exists()) { long fileTimestamp = -1; if (this.cacheMillis >= 0) { // Last-modified timestamp of file will just be read // if caching with timeout. try { fileTimestamp = resource.lastModified(); if (propHolder != null && propHolder.getFileTimestamp() == fileTimestamp) { if (logger.isDebugEnabled()) { logger.debug("Re-caching properties for filename [" + filename + "] - file hasn't been modified"); } propHolder .setRefreshTimestamp(refreshTimestamp); return propHolder; } } catch (IOException ex) { // Probably a class path resource: cache it // forever. if (logger.isDebugEnabled()) { logger.debug( resource + " could not be resolved in the file system - assuming that is hasn't changed", ex); } fileTimestamp = -1; } } try { Properties props = loadProperties(resource, filename); if (propHolder.getProperties() != null) { propHolder.getProperties().putAll(props); } else { propHolder.properties = props; } propHolder.fileTimestamp = fileTimestamp; } catch (IOException ex) { if (logger.isWarnEnabled()) { logger.warn("Could not parse properties file [" + resource.getFilename() + "]", ex); } } } else { // Resource does not exist. if (logger.isDebugEnabled()) { logger.debug("No properties file found for [" + resource.getFilename() + "] - neither plain properties nor XML"); } } } } else { // Resource does not exist. if (logger.isDebugEnabled()) { logger.debug("No properties files found for [" + filename + "] - neither plain properties nor XML"); } // Empty holder representing "not found". propHolder = new ResourceBundleMessageResolver.PropertiesHolder(); } } catch (IOException iOException) { if (logger.isWarnEnabled()) { logger.warn("Could not match pattern [" + filename + "]", iOException); } // Empty holder representing "not valid". propHolder = new ResourceBundleMessageResolver.PropertiesHolder(); } propHolder.setRefreshTimestamp(refreshTimestamp); this.cachedProperties.put(filename, propHolder); return propHolder; } /** * Load the properties from the given resource. * * @param resource * the resource to load from * @param filename * the original bundle filename (basename + Locale) * @return the populated Properties instance * @throws IOException * if properties loading failed */ protected Properties loadProperties(Resource resource, String filename) throws IOException { InputStream is = resource.getInputStream(); Properties props = new Properties(); try { if (resource.getFilename().endsWith(XML_SUFFIX)) { if (logger.isDebugEnabled()) { logger.debug("Loading properties [" + resource.getFilename() + "]"); } this.propertiesPersister.loadFromXml(props, is); } else { String encoding = null; if (this.fileEncodings != null) { encoding = this.fileEncodings.getProperty(filename); } if (encoding == null) { encoding = this.defaultEncoding; } if (encoding != null) { if (logger.isDebugEnabled()) { logger.debug("Loading properties [" + resource.getFilename() + "] with encoding '" + encoding + "'"); } this.propertiesPersister.load(props, new InputStreamReader( is, encoding)); } else { if (logger.isDebugEnabled()) { logger.debug("Loading properties [" + resource.getFilename() + "]"); } this.propertiesPersister.load(props, is); } } return props; } finally { is.close(); } } /** * Clear the resource bundle cache. Subsequent resolve calls will lead to * reloading of the properties files. */ public void clearCache() { logger.debug("Clearing entire resource bundle cache"); synchronized (this.cachedProperties) { this.cachedProperties.clear(); } synchronized (this.cachedMergedProperties) { this.cachedMergedProperties.clear(); } } /** * Clear the resource bundle caches of this MessageSource and all its * ancestors. * * @see #clearCache */ public void clearCacheIncludingAncestors() { clearCache(); if (getParentMessageSource() instanceof ResourceBundleMessageResolver) { ((ResourceBundleMessageResolver) getParentMessageSource()) .clearCacheIncludingAncestors(); } } @Override public String toString() { return getClass().getName() + ": basenames=[" + StringUtils.arrayToCommaDelimitedString(this.basenames) + "]"; } public String getMessage(String key) { return getMessage(key, null, null); } /** * PropertiesHolder for caching. Stores the last-modified timestamp of the * source file for efficient change detection, and the timestamp of the last * refresh attempt (updated every time the cache entry gets re-validated). */ protected class PropertiesHolder { private Properties properties; private long fileTimestamp = -1; private long refreshTimestamp = -1; /** * Cache to hold already generated MessageFormats per message code */ private final Map> cachedMessageFormats = new HashMap>(); public PropertiesHolder(Properties properties, long fileTimestamp) { this.properties = properties; this.fileTimestamp = fileTimestamp; } public PropertiesHolder() { } public Properties getProperties() { return properties; } public long getFileTimestamp() { return fileTimestamp; } public void setRefreshTimestamp(long refreshTimestamp) { this.refreshTimestamp = refreshTimestamp; } public long getRefreshTimestamp() { return refreshTimestamp; } public String getProperty(String code) { if (this.properties == null) { return null; } return this.properties.getProperty(code); } public MessageFormat getMessageFormat(String code, Locale locale) { if (this.properties == null) { return null; } synchronized (this.cachedMessageFormats) { Map localeMap = this.cachedMessageFormats .get(code); if (localeMap != null) { MessageFormat result = localeMap.get(locale); if (result != null) { return result; } } String msg = this.properties.getProperty(code); if (msg != null) { if (localeMap == null) { localeMap = new HashMap(); this.cachedMessageFormats.put(code, localeMap); } MessageFormat result = createMessageFormat(msg, locale); localeMap.put(locale, result); return result; } return null; } } } public String getMessage(UserMessage userMessage) { UserMessages userMessages = new UserMessages(); userMessages.add(userMessage); return getMessage(userMessages); } public String getMessage(UserMessages userMessages) { StringBuilder retVal = null; for (UserMessage userMessage : userMessages) { if (retVal == null) { retVal = new StringBuilder(); } else { retVal.append(System.getProperty("line.separator")); } retVal.append(getMessage(userMessage.getMessageKey(), userMessage.getMessageParams(), null)); } return retVal.toString(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy