org.opencms.i18n.CmsResourceBundleLoader Maven / Gradle / Ivy
Show all versions of opencms-test Show documentation
/*
* This library is part of OpenCms -
* the Open Source Content Management System
*
* Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* For further information about Alkacon Software, please see the
* company website: http://www.alkacon.com
*
* For further information about OpenCms, please see the
* project website: http://www.opencms.org
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.opencms.i18n;
import org.opencms.util.CmsFileUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.AccessControlException;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Resource bundle loader for property based resource bundles from OpenCms that has a flushable cache.
*
* The main reason for implementing this is that the Java default resource bundle loading mechanism
* provided by {@link java.util.ResourceBundle#getBundle(java.lang.String, java.util.Locale)} uses a
* cache that can NOT be flushed by any standard means. This means for every simple change in a resource
* bundle, the Java VM (and the webapp container that runs OpenCms) must be restarted.
* This non-standard resource bundle loader avoids this by providing a flushable cache.
*
* In case the requested bundle can not be found, a fallback mechanism to
* {@link java.util.ResourceBundle#getBundle(java.lang.String, java.util.Locale)} is used to look up
* the resource bundle with the Java default resource bundle loading mechanism.
*
* @see java.util.ResourceBundle
* @see java.util.PropertyResourceBundle
* @see org.opencms.i18n.CmsPropertyResourceBundle
*
* @since 6.2.0
*/
public final class CmsResourceBundleLoader {
/**
* Cache key for the ResourceBundle cache.
*
* Resource bundles are keyed by the combination of bundle name, locale, and class loader.
*/
private static class BundleKey {
/** The base bundle name. */
private String m_baseName;
/** The hash code. */
private int m_hashcode;
/** The locale. */
private Locale m_locale;
/**
* Create an initialized bundle key.
*
* @param s the base name
* @param l the locale
*/
BundleKey(String s, Locale l) {
set(s, l);
}
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof BundleKey)) {
return false;
}
BundleKey key = (BundleKey)o;
return (m_hashcode == key.m_hashcode) && m_baseName.equals(key.m_baseName) && m_locale.equals(key.m_locale);
}
/**
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return m_hashcode;
}
/**
* Checks if the given base name is identical to the base name of this bundle key.
*
* @param baseName the base name to compare
*
* @return true
if the given base name is identical to the base name of this bundle key
*/
public boolean isSameBase(String baseName) {
return m_baseName.equals(baseName);
}
/**
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return m_baseName + "_" + m_locale;
}
/**
* Initialize this bundle key.
*
* @param s the base name
* @param l the locale
*/
void set(String s, Locale l) {
m_baseName = s;
m_locale = l;
m_hashcode = m_baseName.hashCode() ^ m_locale.hashCode();
}
}
/** The resource bundle cache. */
private static Map m_bundleCache;
/** The last default Locale we saw, if this ever changes then we have to reset our caches. */
private static Locale m_lastDefaultLocale;
/** Cache lookup key to avoid having to a new one for every getBundle() call. */
// private static BundleKey m_lookupKey = new BundleKey();
/** The permanent list resource bundle cache. */
private static Map m_permanentCache;
/** Singleton cache entry to represent previous failed lookups. */
private static final ResourceBundle NULL_ENTRY = new CmsListResourceBundle();
/**
* Hides the public constructor.
*/
private CmsResourceBundleLoader() {
// noop
}
static {
m_bundleCache = new ConcurrentHashMap();
m_lastDefaultLocale = Locale.getDefault();
m_permanentCache = new ConcurrentHashMap();
}
/**
* Adds the specified resource bundle to the permanent cache.
*
* @param baseName the raw bundle name, without locale qualifiers
* @param locale the locale
* @param bundle the bundle to cache
*/
public static void addBundleToCache(String baseName, Locale locale, I_CmsResourceBundle bundle) {
String key = baseName;
if (locale != null) {
key += "_" + locale;
}
m_permanentCache.put(key, bundle);
}
/**
* Flushes the complete resource bundle cache.
*/
public static void flushBundleCache() {
synchronized (m_bundleCache) {
m_bundleCache.clear();
}
// We are not flushing the permanent cache on clear!
// Reason: It's not 100% clear if the cache would be filled correctly from the XML after a flush.
// For example if a reference to an XML content object is held, than after a clear cache, this
// object would not have a working localization since the schema and handler would not be initialized again.
// For XML contents that are unmarshalled after the clear cache the localization would work, but it
// seems likely that old references are held.
// On the other hand, if something is changed in the XML, the cache is updated anyway, so we won't be
// stuck with "old" resource bundles that require a server restart.
// m_permanentCache.clear();
}
/**
* Flushes all variations for the provided bundle from the cache.
*
* @param baseName the bundle base name to flush the variations for
* @param flushPermanent if true, the cache for additional message bundles will be flushed, too
*/
public static void flushBundleCache(String baseName, boolean flushPermanent) {
if (baseName != null) {
synchronized (m_bundleCache) {
// first check and clear the bundle cache
Map bundleCacheNew = new ConcurrentHashMap(
m_bundleCache.size());
for (Map.Entry entry : m_bundleCache.entrySet()) {
if (!entry.getKey().isSameBase(baseName)) {
// entry has a different base name, keep it
bundleCacheNew.put(entry.getKey(), entry.getValue());
}
}
if (bundleCacheNew.size() < m_bundleCache.size()) {
// switch caches if only if at least one entry was removed
m_bundleCache = bundleCacheNew;
}
if (flushPermanent) {
Set keys = new HashSet(m_permanentCache.keySet());
for (String key : keys) {
if ((key.startsWith(baseName)
&& ((key.length() == baseName.length()) || (key.charAt(baseName.length()) == '_')))) {
// entry has a the same base name, remove it
m_permanentCache.remove(key);
}
}
}
}
}
}
/**
* Get the appropriate ResourceBundle for the given locale. The following
* strategy is used:
*
* A sequence of candidate bundle names are generated, and tested in
* this order, where the suffix 1 means the string from the specified
* locale, and the suffix 2 means the string from the default locale:
*
*
* - baseName + "_" + language1 + "_" + country1 + "_" + variant1
* - baseName + "_" + language1 + "_" + country1
* - baseName + "_" + language1
* - baseName + "_" + language2 + "_" + country2 + "_" + variant2
* - baseName + "_" + language2 + "_" + country2
* - baseName + "_" + language2
* - baseName
*
*
* In the sequence, entries with an empty string are ignored. Next,
* getBundle
tries to instantiate the resource bundle:
*
*
* - This implementation only resolves property based resource bundles.
* Class based resource bundles are nor found.
* - A search is made for a property resource file, by replacing
* '.' with '/' and appending ".properties", and using
* ClassLoader.getResource(). If a file is found, then a
* PropertyResourceBundle is created from the file's contents.
*
*
* If no resource bundle was found, the default resource bundle loader
* is used to look for the resource bundle. Class based resource bundles
* will be found now.
*
* @param baseName the name of the ResourceBundle
* @param locale A locale
* @return the desired resource bundle
*/
// This method is synchronized so that the cache is properly
// handled.
public static ResourceBundle getBundle(String baseName, Locale locale) {
// If the default locale changed since the last time we were called,
// all cache entries are invalidated.
Locale defaultLocale = Locale.getDefault();
if (defaultLocale != m_lastDefaultLocale) {
synchronized (m_bundleCache) {
if (defaultLocale != m_lastDefaultLocale) {
m_bundleCache = new ConcurrentHashMap();
m_lastDefaultLocale = defaultLocale;
}
}
}
// This will throw NullPointerException if any arguments are null.
BundleKey m_lookupKey = new BundleKey(baseName, locale);
Object obj = m_bundleCache.get(m_lookupKey);
if (obj instanceof ResourceBundle) {
return (ResourceBundle)obj;
} else if (obj == NULL_ENTRY) {
// Lookup has failed previously. Fall through.
} else {
synchronized (m_bundleCache) {
obj = m_bundleCache.get(m_lookupKey);
if (obj instanceof ResourceBundle) {
// check the bundle again
return (ResourceBundle)obj;
}
// First, look for a bundle for the specified locale. We don't want
// the base bundle this time.
boolean wantBase = locale.equals(m_lastDefaultLocale);
ResourceBundle bundle = tryBundle(baseName, locale, wantBase);
// Try the default locale if necessary
if ((bundle == null) && !locale.equals(m_lastDefaultLocale)) {
bundle = tryBundle(baseName, m_lastDefaultLocale, true);
}
BundleKey key = new BundleKey(baseName, locale);
if (bundle != null) {
// Cache the result and return it.
m_bundleCache.put(key, bundle);
return bundle;
}
}
}
// unable to find the resource bundle with this implementation
// use default Java mechanism to look up the bundle again
return ResourceBundle.getBundle(baseName, locale);
}
/**
* Tries to load a property file with the specified name.
*
* @param localizedName the name
* @return the resource bundle if it was loaded, otherwise the backup
*/
private static I_CmsResourceBundle tryBundle(String localizedName) {
I_CmsResourceBundle result = null;
try {
String resourceName = localizedName.replace('.', '/') + ".properties";
URL url = CmsResourceBundleLoader.class.getClassLoader().getResource(resourceName);
if (url != null) {
// the resource was found on the file system
InputStream is = null;
String path = CmsFileUtil.normalizePath(url);
File file = new File(path);
try {
// try to load the resource bundle from a file, NOT with the resource loader first
// this is important since using #getResourceAsStream() may return cached results,
// for example Tomcat by default does cache all resources loaded by the class loader
// this means a changed resource bundle file is not loaded
is = new FileInputStream(file);
} catch (IOException ex) {
// this will happen if the resource is contained for example in a .jar file
is = CmsResourceBundleLoader.class.getClassLoader().getResourceAsStream(resourceName);
} catch (AccessControlException acex) {
// fixed bug #1550
// this will happen if the resource is contained for example in a .jar file
// and security manager is turned on.
is = CmsResourceBundleLoader.class.getClassLoader().getResourceAsStream(resourceName);
}
if (is != null) {
result = new CmsPropertyResourceBundle(is);
}
} else {
// no found with class loader, so try the injected list cache
I_CmsResourceBundle additionalBundle = m_permanentCache.get(localizedName);
if (additionalBundle != null) {
result = additionalBundle.getClone();
}
}
} catch (IOException ex) {
// can't localized these message since this may lead to a chicken-egg problem
MissingResourceException mre = new MissingResourceException(
"Failed to load bundle '" + localizedName + "'",
localizedName,
"");
mre.initCause(ex);
throw mre;
}
return result;
}
/**
* Tries to load a the bundle for a given locale, also loads the backup
* locales with the same language.
*
* @param baseName the raw bundle name, without locale qualifiers
* @param locale the locale
* @param wantBase whether a resource bundle made only from the base name
* (with no locale information attached) should be returned.
* @return the resource bundle if it was loaded, otherwise the backup
*/
private static ResourceBundle tryBundle(String baseName, Locale locale, boolean wantBase) {
I_CmsResourceBundle first = null; // The most specialized bundle.
I_CmsResourceBundle last = null; // The least specialized bundle.
List bundleNames = CmsLocaleManager.getLocaleVariants(baseName, locale, true, true);
for (String bundleName : bundleNames) {
// break if we would try the base bundle, but we do not want it directly
if (bundleName.equals(baseName) && !wantBase && (first == null)) {
break;
}
I_CmsResourceBundle foundBundle = tryBundle(bundleName);
if (foundBundle != null) {
if (first == null) {
first = foundBundle;
}
if (last != null) {
last.setParent((ResourceBundle)foundBundle);
}
foundBundle.setLocale(locale);
last = foundBundle;
}
}
return (ResourceBundle)first;
}
}