mirror of
https://github.com/apache/struts.git
synced 2026-08-31 19:35:40 +00:00
WW-4762 Extracts base abstract class
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
package com.opensymphony.xwork2.util;
|
||||
|
||||
import com.opensymphony.xwork2.ActionContext;
|
||||
import com.opensymphony.xwork2.LocalizedTextProvider;
|
||||
import com.opensymphony.xwork2.inject.Inject;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Collections;
|
||||
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;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
public abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(AbstractLocalizedTextProvider.class);
|
||||
|
||||
public static final String XWORK_MESSAGES_BUNDLE = "com/opensymphony/xwork2/xwork-messages";
|
||||
public static final String STRUTS_MESSAGES_BUNDLE = "org/apache/struts2/struts-messages";
|
||||
|
||||
private static final String TOMCAT_RESOURCE_ENTRIES_FIELD = "resourceEntries";
|
||||
private final String RELOADED = "com.opensymphony.xwork2.util.LocalizedTextProvider.reloaded";
|
||||
|
||||
protected final ConcurrentMap<String, ResourceBundle> bundlesMap = new ConcurrentHashMap<>();
|
||||
protected boolean devMode = false;
|
||||
protected boolean reloadBundles = false;
|
||||
|
||||
private final ConcurrentMap<MessageFormatKey, MessageFormat> messageFormats = new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<Integer, List<String>> classLoaderMap = new ConcurrentHashMap<>();
|
||||
private final Set<String> missingBundles = Collections.synchronizedSet(new HashSet<String>());
|
||||
private final ConcurrentMap<Integer, ClassLoader> delegatedClassLoaderMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Add's the bundle to the internal list of default bundles.
|
||||
* If the bundle already exists in the list it will be re-added.
|
||||
*
|
||||
* @param resourceBundleName the name of the bundle to add.
|
||||
*/
|
||||
@Override
|
||||
public void addDefaultResourceBundle(String resourceBundleName) {
|
||||
//make sure this doesn't get added more than once
|
||||
final ClassLoader ccl = getCurrentThreadContextClassLoader();
|
||||
synchronized (XWORK_MESSAGES_BUNDLE) {
|
||||
List<String> bundles = classLoaderMap.get(ccl.hashCode());
|
||||
if (bundles == null) {
|
||||
bundles = new CopyOnWriteArrayList<>();
|
||||
classLoaderMap.put(ccl.hashCode(), bundles);
|
||||
}
|
||||
bundles.remove(resourceBundleName);
|
||||
bundles.add(0, resourceBundleName);
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Added default resource bundle '{}' to default resource bundles for the following classloader '{}'", resourceBundleName, ccl.toString());
|
||||
}
|
||||
}
|
||||
|
||||
protected List<String> getCurrentBundleNames() {
|
||||
return classLoaderMap.get(getCurrentThreadContextClassLoader().hashCode());
|
||||
}
|
||||
|
||||
protected ClassLoader getCurrentThreadContextClassLoader() {
|
||||
return Thread.currentThread().getContextClassLoader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a localized message for the specified key, aTextName. Neither the key nor the
|
||||
* message is evaluated.
|
||||
*
|
||||
* @param aTextName the message key
|
||||
* @param locale the locale the message should be for
|
||||
* @return a localized message based on the specified key, or null if no localized message can be found for it
|
||||
*/
|
||||
@Override
|
||||
public String findDefaultText(String aTextName, Locale locale) {
|
||||
List<String> localList = getCurrentBundleNames();
|
||||
|
||||
for (String bundleName : localList) {
|
||||
ResourceBundle bundle = findResourceBundle(bundleName, locale);
|
||||
if (bundle != null) {
|
||||
reloadBundles();
|
||||
try {
|
||||
return bundle.getString(aTextName);
|
||||
} catch (MissingResourceException e) {
|
||||
// will be logged when not found in any bundle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (devMode) {
|
||||
LOG.warn("Missing key [{}] in bundles [{}]!", aTextName, localList);
|
||||
} else {
|
||||
LOG.debug("Missing key [{}] in bundles [{}]!", aTextName, localList);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a localized message for the specified key, aTextName, substituting variables from the
|
||||
* array of params into the message. Neither the key nor the message is evaluated.
|
||||
*
|
||||
* @param aTextName the message key
|
||||
* @param locale the locale the message should be for
|
||||
* @param params an array of objects to be substituted into the message text
|
||||
* @return A formatted message based on the specified key, or null if no localized message can be found for it
|
||||
*/
|
||||
@Override
|
||||
public String findDefaultText(String aTextName, Locale locale, Object[] params) {
|
||||
String defaultText = findDefaultText(aTextName, locale);
|
||||
if (defaultText != null) {
|
||||
MessageFormat mf = buildMessageFormat(defaultText, locale);
|
||||
return formatWithNullDetection(mf, params);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param classLoader a {@link ClassLoader} to look up the bundle from if none can be found on the current thread's classloader
|
||||
*/
|
||||
public void setDelegatedClassLoader(final ClassLoader classLoader) {
|
||||
synchronized (bundlesMap) {
|
||||
delegatedClassLoaderMap.put(getCurrentThreadContextClassLoader().hashCode(), classLoader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bundleName Removes the bundle from any cached "misses"
|
||||
*/
|
||||
public void clearBundle(final String bundleName) {
|
||||
bundlesMap.remove(getCurrentThreadContextClassLoader().hashCode() + bundleName);
|
||||
}
|
||||
|
||||
protected void reloadBundles() {
|
||||
reloadBundles(ActionContext.getContext() != null ? ActionContext.getContext().getContextMap() : null);
|
||||
}
|
||||
|
||||
protected void reloadBundles(Map<String, Object> context) {
|
||||
if (reloadBundles) {
|
||||
try {
|
||||
Boolean reloaded;
|
||||
if (context != null) {
|
||||
reloaded = (Boolean) ObjectUtils.defaultIfNull(context.get(RELOADED), Boolean.FALSE);
|
||||
} else {
|
||||
reloaded = Boolean.FALSE;
|
||||
}
|
||||
if (!reloaded) {
|
||||
bundlesMap.clear();
|
||||
try {
|
||||
clearMap(ResourceBundle.class, null, "cacheList");
|
||||
} catch (NoSuchFieldException e) {
|
||||
// happens in IBM JVM, that has a different ResourceBundle impl
|
||||
// it has a 'cache' member
|
||||
clearMap(ResourceBundle.class, null, "cache");
|
||||
}
|
||||
|
||||
// now, for the true and utter hack, if we're running in tomcat, clear
|
||||
// it's class loader resource cache as well.
|
||||
clearTomcatCache();
|
||||
if (context != null) {
|
||||
context.put(RELOADED, true);
|
||||
}
|
||||
LOG.debug("Resource bundles reloaded");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Could not reload resource bundles", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearTomcatCache() {
|
||||
ClassLoader loader = getCurrentThreadContextClassLoader();
|
||||
// no need for compilation here.
|
||||
Class cl = loader.getClass();
|
||||
|
||||
try {
|
||||
if ("org.apache.catalina.loader.WebappClassLoader".equals(cl.getName())) {
|
||||
clearMap(cl, loader, TOMCAT_RESOURCE_ENTRIES_FIELD);
|
||||
} else {
|
||||
LOG.debug("Class loader {} is not tomcat loader.", cl.getName());
|
||||
}
|
||||
} catch (NoSuchFieldException nsfe) {
|
||||
if ("org.apache.catalina.loader.WebappClassLoaderBase".equals(cl.getSuperclass().getName())) {
|
||||
LOG.debug("Base class {} doesn't contain '{}' field, trying with parent!", cl.getName(), TOMCAT_RESOURCE_ENTRIES_FIELD, nsfe);
|
||||
try {
|
||||
clearMap(cl.getSuperclass(), loader, TOMCAT_RESOURCE_ENTRIES_FIELD);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't clear tomcat cache using {}", cl.getSuperclass().getName(), e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't clear tomcat cache", cl.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void clearMap(Class cl, Object obj, String name)
|
||||
throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
|
||||
|
||||
Field field = cl.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
|
||||
Object cache = field.get(obj);
|
||||
|
||||
synchronized (cache) {
|
||||
Class ccl = cache.getClass();
|
||||
Method clearMethod = ccl.getMethod("clear");
|
||||
clearMethod.invoke(cache);
|
||||
}
|
||||
}
|
||||
|
||||
protected MessageFormat buildMessageFormat(String pattern, Locale locale) {
|
||||
MessageFormatKey key = new MessageFormatKey(pattern, locale);
|
||||
MessageFormat format = messageFormats.get(key);
|
||||
if (format == null) {
|
||||
format = new MessageFormat(pattern);
|
||||
format.setLocale(locale);
|
||||
format.applyPattern(pattern);
|
||||
messageFormats.put(key, format);
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
protected String formatWithNullDetection(MessageFormat mf, Object[] args) {
|
||||
String message = mf.format(args);
|
||||
if ("null".equals(message)) {
|
||||
return null;
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(value = StrutsConstants.STRUTS_I18N_RELOAD, required = false)
|
||||
public void setReloadBundles(String reloadBundles) {
|
||||
this.reloadBundles = Boolean.parseBoolean(reloadBundles);
|
||||
}
|
||||
|
||||
@Inject(value = StrutsConstants.STRUTS_DEVMODE, required = false)
|
||||
public void setDevMode(String devMode) {
|
||||
this.devMode = Boolean.parseBoolean(devMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the given resource bundle by it's name.
|
||||
* <p>
|
||||
* Will use <code>Thread.currentThread().getContextClassLoader()</code> as the classloader.
|
||||
* </p>
|
||||
*
|
||||
* @param aBundleName the name of the bundle (usually it's FQN classname).
|
||||
* @param locale the locale.
|
||||
* @return the bundle, <tt>null</tt> if not found.
|
||||
*/
|
||||
@Override
|
||||
public ResourceBundle findResourceBundle(String aBundleName, Locale locale) {
|
||||
ClassLoader classLoader = getCurrentThreadContextClassLoader();
|
||||
String key = createMissesKey(String.valueOf(classLoader.hashCode()), aBundleName, locale);
|
||||
|
||||
if (missingBundles.contains(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ResourceBundle bundle = null;
|
||||
try {
|
||||
if (bundlesMap.containsKey(key)) {
|
||||
bundle = bundlesMap.get(key);
|
||||
} else {
|
||||
bundle = ResourceBundle.getBundle(aBundleName, locale, classLoader);
|
||||
bundlesMap.putIfAbsent(key, bundle);
|
||||
}
|
||||
} catch (MissingResourceException ex) {
|
||||
if (delegatedClassLoaderMap.containsKey(classLoader.hashCode())) {
|
||||
try {
|
||||
if (bundlesMap.containsKey(key)) {
|
||||
bundle = bundlesMap.get(key);
|
||||
} else {
|
||||
bundle = ResourceBundle.getBundle(aBundleName, locale, delegatedClassLoaderMap.get(classLoader.hashCode()));
|
||||
bundlesMap.putIfAbsent(key, bundle);
|
||||
}
|
||||
} catch (MissingResourceException e) {
|
||||
LOG.debug("Missing resource bundle [{}]!", aBundleName, e);
|
||||
missingBundles.add(key);
|
||||
}
|
||||
} else {
|
||||
LOG.debug("Missing resource bundle [{}]!", aBundleName);
|
||||
missingBundles.add(key);
|
||||
}
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a key to used for lookup/storing in the bundle misses cache.
|
||||
*
|
||||
* @param prefix the prefix for the returning String - it is supposed to be the ClassLoader hash code.
|
||||
* @param aBundleName the name of the bundle (usually it's FQN classname).
|
||||
* @param locale the locale.
|
||||
* @return the key to use for lookup/storing in the bundle misses cache.
|
||||
*/
|
||||
private String createMissesKey(String prefix, String aBundleName, Locale locale) {
|
||||
return prefix + aBundleName + "_" + locale.toString();
|
||||
}
|
||||
|
||||
static class MessageFormatKey {
|
||||
String pattern;
|
||||
Locale locale;
|
||||
|
||||
MessageFormatKey(String pattern, Locale locale) {
|
||||
this.pattern = pattern;
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
MessageFormatKey that = (MessageFormatKey) o;
|
||||
|
||||
if (pattern != null ? !pattern.equals(that.pattern) : that.pattern != null) return false;
|
||||
return locale != null ? locale.equals(that.locale) : that.locale == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = pattern != null ? pattern.hashCode() : 0;
|
||||
result = 31 * result + (locale != null ? locale.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
static class GetDefaultMessageReturnArg {
|
||||
String message;
|
||||
boolean foundInBundle;
|
||||
|
||||
public GetDefaultMessageReturnArg(String message, boolean foundInBundle) {
|
||||
this.message = message;
|
||||
this.foundInBundle = foundInBundle;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,52 +23,32 @@ package com.opensymphony.xwork2.util;
|
||||
|
||||
import com.opensymphony.xwork2.ActionContext;
|
||||
import com.opensymphony.xwork2.ActionInvocation;
|
||||
import com.opensymphony.xwork2.LocalizedTextProvider;
|
||||
import com.opensymphony.xwork2.ModelDriven;
|
||||
import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
|
||||
import com.opensymphony.xwork2.inject.Inject;
|
||||
import com.opensymphony.xwork2.util.reflection.ReflectionProviderFactory;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.message.ParameterizedMessage;
|
||||
import org.apache.struts2.StrutsConstants;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.text.MessageFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.MissingResourceException;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.Set;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Provides support for localization in the framework, it can be used to read only default bundles,
|
||||
* or it can search the class hierarchy to find proper bundles.
|
||||
*/
|
||||
public class StrutsLocalizedTextProvider implements LocalizedTextProvider {
|
||||
public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider {
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(StrutsLocalizedTextProvider.class);
|
||||
|
||||
public static final String XWORK_MESSAGES_BUNDLE = "com/opensymphony/xwork2/xwork-messages";
|
||||
public static final String STRUTS_MESSAGES_BUNDLE = "org/apache/struts2/struts-messages";
|
||||
|
||||
private static final String TOMCAT_RESOURCE_ENTRIES_FIELD = "resourceEntries";
|
||||
|
||||
private final ConcurrentMap<Integer, List<String>> classLoaderMap = new ConcurrentHashMap<>();
|
||||
|
||||
private boolean reloadBundles = false;
|
||||
private boolean devMode = false;
|
||||
|
||||
private final ConcurrentMap<String, ResourceBundle> bundlesMap = new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<MessageFormatKey, MessageFormat> messageFormats = new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<Integer, ClassLoader> delegatedClassLoaderMap = new ConcurrentHashMap<>();
|
||||
private final Set<String> missingBundles = Collections.synchronizedSet(new HashSet<String>());
|
||||
|
||||
private final String RELOADED = "com.opensymphony.xwork2.util.LocalizedTextUtil.reloaded";
|
||||
|
||||
/**
|
||||
* Clears the internal list of resource bundles.
|
||||
*
|
||||
@@ -80,26 +60,10 @@ public class StrutsLocalizedTextProvider implements LocalizedTextProvider {
|
||||
}
|
||||
|
||||
public StrutsLocalizedTextProvider() {
|
||||
|
||||
addDefaultResourceBundle(XWORK_MESSAGES_BUNDLE);
|
||||
addDefaultResourceBundle(STRUTS_MESSAGES_BUNDLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Should resorce bundles be reloaded.
|
||||
*
|
||||
* @param reloadBundles reload bundles?
|
||||
*/
|
||||
@Inject(value = StrutsConstants.STRUTS_I18N_RELOAD, required = false)
|
||||
public void setReloadBundles(String reloadBundles) {
|
||||
this.reloadBundles = Boolean.parseBoolean(reloadBundles);
|
||||
}
|
||||
|
||||
@Inject(value = StrutsConstants.STRUTS_DEVMODE, required = false)
|
||||
public void setDevMode(String devMode) {
|
||||
this.devMode = Boolean.parseBoolean(devMode);
|
||||
}
|
||||
|
||||
@Inject(value = StrutsConstants.STRUTS_CUSTOM_I18N_RESOURCES, required = false)
|
||||
public void setCustomI18NResources(String bundles) {
|
||||
if (bundles != null && bundles.length() > 0) {
|
||||
@@ -117,33 +81,6 @@ public class StrutsLocalizedTextProvider implements LocalizedTextProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add's the bundle to the internal list of default bundles.
|
||||
* <p>
|
||||
* If the bundle already exists in the list it will be readded.
|
||||
* </p>
|
||||
*
|
||||
* @param resourceBundleName the name of the bundle to add.
|
||||
*/
|
||||
@Override
|
||||
public void addDefaultResourceBundle(String resourceBundleName) {
|
||||
//make sure this doesn't get added more than once
|
||||
final ClassLoader ccl = getCurrentThreadContextClassLoader();
|
||||
synchronized (XWORK_MESSAGES_BUNDLE) {
|
||||
List<String> bundles = classLoaderMap.get(ccl.hashCode());
|
||||
if (bundles == null) {
|
||||
bundles = new CopyOnWriteArrayList<>();
|
||||
classLoaderMap.put(ccl.hashCode(), bundles);
|
||||
}
|
||||
bundles.remove(resourceBundleName);
|
||||
bundles.add(0, resourceBundleName);
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Added default resource bundle '{}' to default resource bundles for the following classloader '{}'", resourceBundleName, ccl.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a {@link java.util.Locale} from a String of the form en_US_foo into a Locale
|
||||
* with language "en", country "US" and variant "foo". This will parse the output of
|
||||
@@ -189,135 +126,6 @@ public class StrutsLocalizedTextProvider implements LocalizedTextProvider {
|
||||
return new Locale(language, country, localeStr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a localized message for the specified key, aTextName. Neither the key nor the
|
||||
* message is evaluated.
|
||||
*
|
||||
* @param aTextName the message key
|
||||
* @param locale the locale the message should be for
|
||||
* @return a localized message based on the specified key, or null if no localized message can be found for it
|
||||
*/
|
||||
@Override
|
||||
public String findDefaultText(String aTextName, Locale locale) {
|
||||
List<String> localList = classLoaderMap.get(Thread.currentThread().getContextClassLoader().hashCode());
|
||||
|
||||
for (String bundleName : localList) {
|
||||
ResourceBundle bundle = findResourceBundle(bundleName, locale);
|
||||
if (bundle != null) {
|
||||
reloadBundles();
|
||||
try {
|
||||
return bundle.getString(aTextName);
|
||||
} catch (MissingResourceException e) {
|
||||
// will be logged when not found in any bundle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (devMode) {
|
||||
LOG.warn("Missing key [{}] in bundles [{}]!", aTextName, localList);
|
||||
} else {
|
||||
LOG.debug("Missing key [{}] in bundles [{}]!", aTextName, localList);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a localized message for the specified key, aTextName, substituting variables from the
|
||||
* array of params into the message. Neither the key nor the message is evaluated.
|
||||
*
|
||||
* @param aTextName the message key
|
||||
* @param locale the locale the message should be for
|
||||
* @param params an array of objects to be substituted into the message text
|
||||
* @return A formatted message based on the specified key, or null if no localized message can be found for it
|
||||
*/
|
||||
@Override
|
||||
public String findDefaultText(String aTextName, Locale locale, Object[] params) {
|
||||
String defaultText = findDefaultText(aTextName, locale);
|
||||
if (defaultText != null) {
|
||||
MessageFormat mf = buildMessageFormat(defaultText, locale);
|
||||
return formatWithNullDetection(mf, params);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the given resource bundle by it's name.
|
||||
* <p>
|
||||
* Will use <code>Thread.currentThread().getContextClassLoader()</code> as the classloader.
|
||||
* </p>
|
||||
*
|
||||
* @param aBundleName the name of the bundle (usually it's FQN classname).
|
||||
* @param locale the locale.
|
||||
* @return the bundle, <tt>null</tt> if not found.
|
||||
*/
|
||||
@Override
|
||||
public ResourceBundle findResourceBundle(String aBundleName, Locale locale) {
|
||||
ClassLoader classLoader = getCurrentThreadContextClassLoader();
|
||||
String key = createMissesKey(String.valueOf(classLoader.hashCode()), aBundleName, locale);
|
||||
|
||||
if (missingBundles.contains(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ResourceBundle bundle = null;
|
||||
try {
|
||||
if (bundlesMap.containsKey(key)) {
|
||||
bundle = bundlesMap.get(key);
|
||||
} else {
|
||||
bundle = ResourceBundle.getBundle(aBundleName, locale, classLoader);
|
||||
bundlesMap.putIfAbsent(key, bundle);
|
||||
}
|
||||
} catch (MissingResourceException ex) {
|
||||
if (delegatedClassLoaderMap.containsKey(classLoader.hashCode())) {
|
||||
try {
|
||||
if (bundlesMap.containsKey(key)) {
|
||||
bundle = bundlesMap.get(key);
|
||||
} else {
|
||||
bundle = ResourceBundle.getBundle(aBundleName, locale, delegatedClassLoaderMap.get(classLoader.hashCode()));
|
||||
bundlesMap.putIfAbsent(key, bundle);
|
||||
}
|
||||
} catch (MissingResourceException e) {
|
||||
LOG.debug("Missing resource bundle [{}]!", aBundleName, e);
|
||||
missingBundles.add(key);
|
||||
}
|
||||
} else {
|
||||
LOG.debug("Missing resource bundle [{}]!", aBundleName);
|
||||
missingBundles.add(key);
|
||||
}
|
||||
}
|
||||
return bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param classLoader a {@link ClassLoader} to look up the bundle from if none can be found on the current thread's classloader
|
||||
*/
|
||||
public void setDelegatedClassLoader(final ClassLoader classLoader) {
|
||||
synchronized (bundlesMap) {
|
||||
delegatedClassLoaderMap.put(getCurrentThreadContextClassLoader().hashCode(), classLoader);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bundleName Removes the bundle from any cached "misses"
|
||||
*/
|
||||
public void clearBundle(final String bundleName) {
|
||||
bundlesMap.remove(getCurrentThreadContextClassLoader().hashCode() + bundleName);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a key to used for lookup/storing in the bundle misses cache.
|
||||
*
|
||||
* @param prefix the prefix for the returning String - it is supposed to be the ClassLoader hash code.
|
||||
* @param aBundleName the name of the bundle (usually it's FQN classname).
|
||||
* @param locale the locale.
|
||||
* @return the key to use for lookup/storing in the bundle misses cache.
|
||||
*/
|
||||
private String createMissesKey(String prefix, String aBundleName, Locale locale) {
|
||||
return prefix + aBundleName + "_" + locale.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link #findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args)}
|
||||
* with aTextName as the default message.
|
||||
@@ -759,28 +567,6 @@ public class StrutsLocalizedTextProvider implements LocalizedTextProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private String formatWithNullDetection(MessageFormat mf, Object[] args) {
|
||||
String message = mf.format(args);
|
||||
if ("null".equals(message)) {
|
||||
return null;
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
private MessageFormat buildMessageFormat(String pattern, Locale locale) {
|
||||
MessageFormatKey key = new MessageFormatKey(pattern, locale);
|
||||
MessageFormat format = messageFormats.get(key);
|
||||
if (format == null) {
|
||||
format = new MessageFormat(pattern);
|
||||
format.setLocale(locale);
|
||||
format.applyPattern(pattern);
|
||||
messageFormats.put(key, format);
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse up class hierarchy looking for message. Looks at class, then implemented interface,
|
||||
* before going up hierarchy.
|
||||
@@ -790,7 +576,7 @@ public class StrutsLocalizedTextProvider implements LocalizedTextProvider {
|
||||
private String findMessage(Class clazz, String key, String indexedKey, Locale locale, Object[] args, Set<String> checked,
|
||||
ValueStack valueStack) {
|
||||
if (checked == null) {
|
||||
checked = new TreeSet<String>();
|
||||
checked = new TreeSet<>();
|
||||
} else if (checked.contains(clazz.getName())) {
|
||||
return null;
|
||||
}
|
||||
@@ -849,85 +635,6 @@ public class StrutsLocalizedTextProvider implements LocalizedTextProvider {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void reloadBundles() {
|
||||
reloadBundles(ActionContext.getContext() != null ? ActionContext.getContext().getContextMap() : null);
|
||||
}
|
||||
|
||||
private void reloadBundles(Map<String, Object> context) {
|
||||
if (reloadBundles) {
|
||||
try {
|
||||
Boolean reloaded;
|
||||
if (context != null) {
|
||||
reloaded = (Boolean) ObjectUtils.defaultIfNull(context.get(RELOADED), Boolean.FALSE);
|
||||
}else {
|
||||
reloaded = Boolean.FALSE;
|
||||
}
|
||||
if (!reloaded) {
|
||||
bundlesMap.clear();
|
||||
try {
|
||||
clearMap(ResourceBundle.class, null, "cacheList");
|
||||
} catch (NoSuchFieldException e) {
|
||||
// happens in IBM JVM, that has a different ResourceBundle impl
|
||||
// it has a 'cache' member
|
||||
clearMap(ResourceBundle.class, null, "cache");
|
||||
}
|
||||
|
||||
// now, for the true and utter hack, if we're running in tomcat, clear
|
||||
// it's class loader resource cache as well.
|
||||
clearTomcatCache();
|
||||
if(context!=null) {
|
||||
context.put(RELOADED, true);
|
||||
}
|
||||
LOG.debug("Resource bundles reloaded");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Could not reload resource bundles", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void clearTomcatCache() {
|
||||
ClassLoader loader = getCurrentThreadContextClassLoader();
|
||||
// no need for compilation here.
|
||||
Class cl = loader.getClass();
|
||||
|
||||
try {
|
||||
if ("org.apache.catalina.loader.WebappClassLoader".equals(cl.getName())) {
|
||||
clearMap(cl, loader, TOMCAT_RESOURCE_ENTRIES_FIELD);
|
||||
} else {
|
||||
LOG.debug("Class loader {} is not tomcat loader.", cl.getName());
|
||||
}
|
||||
} catch (NoSuchFieldException nsfe) {
|
||||
if ("org.apache.catalina.loader.WebappClassLoaderBase".equals(cl.getSuperclass().getName())) {
|
||||
LOG.debug("Base class {} doesn't contain '{}' field, trying with parent!", cl.getName(), TOMCAT_RESOURCE_ENTRIES_FIELD, nsfe);
|
||||
try {
|
||||
clearMap(cl.getSuperclass(), loader, TOMCAT_RESOURCE_ENTRIES_FIELD);
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't clear tomcat cache using {}", cl.getSuperclass().getName(), e);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Couldn't clear tomcat cache", cl.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void clearMap(Class cl, Object obj, String name)
|
||||
throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
|
||||
|
||||
Field field = cl.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
|
||||
Object cache = field.get(obj);
|
||||
|
||||
synchronized (cache) {
|
||||
Class ccl = cache.getClass();
|
||||
Method clearMethod = ccl.getMethod("clear");
|
||||
clearMethod.invoke(cache);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all the internal lists.
|
||||
*
|
||||
@@ -938,63 +645,4 @@ public class StrutsLocalizedTextProvider implements LocalizedTextProvider {
|
||||
// no-op
|
||||
}
|
||||
|
||||
static class MessageFormatKey {
|
||||
String pattern;
|
||||
Locale locale;
|
||||
|
||||
MessageFormatKey(String pattern, Locale locale) {
|
||||
this.pattern = pattern;
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof MessageFormatKey)) return false;
|
||||
|
||||
final MessageFormatKey messageFormatKey = (MessageFormatKey) o;
|
||||
|
||||
if (locale != null ? !locale.equals(messageFormatKey.locale) : messageFormatKey.locale != null)
|
||||
return false;
|
||||
if (pattern != null ? !pattern.equals(messageFormatKey.pattern) : messageFormatKey.pattern != null)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result;
|
||||
result = (pattern != null ? pattern.hashCode() : 0);
|
||||
result = 29 * result + (locale != null ? locale.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static ClassLoader getCurrentThreadContextClassLoader() {
|
||||
return Thread.currentThread().getContextClassLoader();
|
||||
}
|
||||
|
||||
static class GetDefaultMessageReturnArg {
|
||||
String message;
|
||||
boolean foundInBundle;
|
||||
|
||||
public GetDefaultMessageReturnArg(String message, boolean foundInBundle) {
|
||||
this.message = message;
|
||||
this.foundInBundle = foundInBundle;
|
||||
}
|
||||
}
|
||||
|
||||
private static class EmptyResourceBundle extends ResourceBundle {
|
||||
@Override
|
||||
public Enumeration<String> getKeys() {
|
||||
return null; // dummy
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object handleGetObject(String key) {
|
||||
return null; // dummy
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user