mirror of
https://github.com/apache/struts.git
synced 2026-08-07 15:46:57 +00:00
WW-5668 Make the localized-text provider caches size-bounded and align request-locale resolution (6.x) (#1823)
* WW-5668 Add remove(key) to the OgnlCache abstraction Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * WW-5668 Bound the localized-text provider caches with configurable size Converts bundlesMap, messageFormats and missingBundles to the existing OgnlCache abstraction, configurable via struts.i18n.cacheType and struts.i18n.cacheMaxSize (wtlfu / 10000 by default). The caches are kept transient and rebuilt in readObject so the providers stay serializable, and bundlesMap-related synchronization moves to a dedicated monitor since the field is now reassignable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * WW-5668 Add opt-in request-locale resolution consistency to Dispatcher Adds struts.locale.validateRequestLocale (default false) so request-derived locales can be restricted to the JVM's available-locale set, matching what I18nInterceptor already applies to its own locale sources. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * WW-5668 Keep the localized-text providers deserializable across a version upgrade Pins serialVersionUID to the value implicitly computed for the pre-6.11.0 class shape instead of 1L, so a session serialized by a 6.10.0 node still loads on a 6.11.0 one during a rolling upgrade rather than failing with InvalidClassException. Such a stream carries no value for the new cache settings, and field initialisers do not run during deserialization, so readObject restores their defaults before rebuilding the caches - without that guard it failed with a NullPointerException. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,15 @@ public interface OgnlCache<Key, Value> {
|
||||
|
||||
void putIfAbsent(Key key, Value value);
|
||||
|
||||
/**
|
||||
* Removes the mapping for the given key, if present.
|
||||
*
|
||||
* @param key the key to remove
|
||||
* @return the previous value associated with the key, or {@code null} if none
|
||||
* @since 6.11.0
|
||||
*/
|
||||
Value remove(Key key);
|
||||
|
||||
int size();
|
||||
|
||||
void clear();
|
||||
|
||||
@@ -56,6 +56,11 @@ public class OgnlCaffeineCache<K, V> implements OgnlCache<K, V> {
|
||||
cache.asMap().putIfAbsent(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(K key) {
|
||||
return cache.asMap().remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return cache.asMap().size();
|
||||
|
||||
@@ -57,6 +57,11 @@ public class OgnlDefaultCache<K, V> implements OgnlCache<K, V> {
|
||||
this.clearIfEvictionLimitExceeded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(K key) {
|
||||
return ognlCache.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return ognlCache.size();
|
||||
|
||||
@@ -64,6 +64,11 @@ public class OgnlLRUCache<K, V> implements OgnlCache<K, V> {
|
||||
ognlLRUCache.putIfAbsent(key, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public V remove(K key) {
|
||||
return ognlLRUCache.remove(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return ognlLRUCache.size();
|
||||
|
||||
+102
-13
@@ -21,6 +21,10 @@ package com.opensymphony.xwork2.util;
|
||||
import com.opensymphony.xwork2.ActionContext;
|
||||
import com.opensymphony.xwork2.LocalizedTextProvider;
|
||||
import com.opensymphony.xwork2.inject.Inject;
|
||||
import com.opensymphony.xwork2.ognl.DefaultOgnlCacheFactory;
|
||||
import com.opensymphony.xwork2.ognl.OgnlCache;
|
||||
import com.opensymphony.xwork2.ognl.OgnlCacheFactory.CacheType;
|
||||
import org.apache.commons.lang3.EnumUtils;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -45,6 +49,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
|
||||
|
||||
// Pinned to the value implicitly computed for the pre-6.11.0 class shape, so sessions serialized by
|
||||
// an older node still deserialize here during a rolling upgrade. The caches this change made transient
|
||||
// are simply discarded from such a stream and rebuilt by readObject.
|
||||
private static final long serialVersionUID = -4563130226985473584L;
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(AbstractLocalizedTextProvider.class);
|
||||
|
||||
public static final String XWORK_MESSAGES_BUNDLE = "com/opensymphony/xwork2/xwork-messages";
|
||||
@@ -56,16 +65,37 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
|
||||
private static final String TOMCAT_WEBAPP_CLASSLOADER_BASE = "org.apache.catalina.loader.WebappClassLoaderBase";
|
||||
private static 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;
|
||||
protected boolean searchDefaultBundlesFirst = false; // Search default resource bundles first. Note: This flag may not be meaningful to all implementations.
|
||||
|
||||
private final ConcurrentMap<MessageFormatKey, MessageFormat> messageFormats = new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<Integer, List<String>> classLoaderMap = new ConcurrentHashMap<>();
|
||||
private final Set<String> missingBundles = ConcurrentHashMap.newKeySet();
|
||||
private final ConcurrentMap<Integer, ClassLoader> delegatedClassLoaderMap = new ConcurrentHashMap<>();
|
||||
|
||||
// Dedicated monitor for bundlesMap-related synchronization: bundlesMap is reassigned by
|
||||
// rebuildI18nCaches(), so locking on it directly would lock on a monitor that can change identity.
|
||||
// transient + reinitialised in readObject: a bare Object is not Serializable.
|
||||
private transient Object bundlesMapLock = new Object();
|
||||
|
||||
private static final int DEFAULT_I18N_CACHE_MAX_SIZE = 10000;
|
||||
|
||||
private volatile CacheType i18nCacheType = CacheType.WTLFU;
|
||||
private volatile int i18nCacheMaxSize = DEFAULT_I18N_CACHE_MAX_SIZE;
|
||||
|
||||
private <K, V> OgnlCache<K, V> buildI18nCache() {
|
||||
return new DefaultOgnlCacheFactory<K, V>(i18nCacheMaxSize, i18nCacheType).buildOgnlCache();
|
||||
}
|
||||
|
||||
// The OgnlCache implementations are themselves thread-safe; volatile only safely publishes the
|
||||
// reference when rebuildI18nCaches() replaces a cache (during injection / readObject), so S3077
|
||||
// ("volatile is not enough") does not apply here.
|
||||
@SuppressWarnings("java:S3077")
|
||||
protected transient volatile OgnlCache<String, ResourceBundle> bundlesMap = buildI18nCache();
|
||||
@SuppressWarnings("java:S3077")
|
||||
private transient volatile OgnlCache<MessageFormatKey, MessageFormat> messageFormats = buildI18nCache();
|
||||
@SuppressWarnings("java:S3077")
|
||||
private transient volatile OgnlCache<String, Boolean> missingBundles = buildI18nCache();
|
||||
|
||||
/**
|
||||
* Adds the bundle to the internal list of default bundles.
|
||||
* If the bundle already exists in the list it will be re-added.
|
||||
@@ -99,6 +129,21 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
|
||||
return Thread.currentThread().getContextClassLoader();
|
||||
}
|
||||
|
||||
/** Test-support accessor: current number of cached resource bundles. */
|
||||
protected int bundlesMapSize() {
|
||||
return bundlesMap.size();
|
||||
}
|
||||
|
||||
/** Test-support accessor: current number of cached missing-bundle markers. */
|
||||
protected int missingBundlesSize() {
|
||||
return missingBundles.size();
|
||||
}
|
||||
|
||||
/** Test-support accessor: current number of cached message formats. */
|
||||
protected int messageFormatsSize() {
|
||||
return messageFormats.size();
|
||||
}
|
||||
|
||||
@Inject(value = StrutsConstants.STRUTS_CUSTOM_I18N_RESOURCES, required = false)
|
||||
public void setCustomI18NResources(String bundles) {
|
||||
if (bundles != null && bundles.length() > 0) {
|
||||
@@ -221,7 +266,7 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
|
||||
* @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) {
|
||||
synchronized (bundlesMapLock) {
|
||||
delegatedClassLoaderMap.put(getCurrentThreadContextClassLoader().hashCode(), classLoader);
|
||||
}
|
||||
}
|
||||
@@ -443,6 +488,52 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
|
||||
this.searchDefaultBundlesFirst = Boolean.parseBoolean(searchDefaultBundlesFirst);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cacheType the type of cache to use for the localized-text caches
|
||||
*
|
||||
* @since 6.11.0
|
||||
*/
|
||||
@Inject(value = StrutsConstants.STRUTS_I18N_CACHE_TYPE, required = false)
|
||||
public void setI18nCacheType(String cacheType) {
|
||||
this.i18nCacheType = EnumUtils.getEnumIgnoreCase(CacheType.class, cacheType, CacheType.WTLFU);
|
||||
rebuildI18nCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cacheMaxSize the maximum size of each localized-text cache
|
||||
*
|
||||
* @since 6.11.0
|
||||
*/
|
||||
@Inject(value = StrutsConstants.STRUTS_I18N_CACHE_MAXSIZE, required = false)
|
||||
public void setI18nCacheMaxSize(String cacheMaxSize) {
|
||||
this.i18nCacheMaxSize = Integer.parseInt(cacheMaxSize);
|
||||
rebuildI18nCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuilds the localized-text caches from the current type/size. Called during dependency injection
|
||||
* (single-threaded startup, before the provider serves lookups); discards any warm-up entries.
|
||||
*/
|
||||
private void rebuildI18nCaches() {
|
||||
bundlesMap = buildI18nCache();
|
||||
messageFormats = buildI18nCache();
|
||||
missingBundles = buildI18nCache();
|
||||
}
|
||||
|
||||
private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, ClassNotFoundException {
|
||||
in.defaultReadObject();
|
||||
bundlesMapLock = new Object();
|
||||
// Field initialisers do not run during deserialization, so a stream written before these settings
|
||||
// existed (an older node in a rolling upgrade) leaves them at null/0. Restore the defaults.
|
||||
if (i18nCacheType == null) {
|
||||
i18nCacheType = CacheType.WTLFU;
|
||||
}
|
||||
if (i18nCacheMaxSize <= 0) {
|
||||
i18nCacheMaxSize = DEFAULT_I18N_CACHE_MAX_SIZE;
|
||||
}
|
||||
rebuildI18nCaches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the given resource bundle by it's name.
|
||||
* <p>
|
||||
@@ -458,34 +549,32 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
|
||||
ClassLoader classLoader = getCurrentThreadContextClassLoader();
|
||||
String key = createMissesKey(String.valueOf(classLoader.hashCode()), aBundleName, locale);
|
||||
|
||||
if (missingBundles.contains(key)) {
|
||||
if (missingBundles.get(key) != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ResourceBundle bundle = null;
|
||||
try {
|
||||
if (bundlesMap.containsKey(key)) {
|
||||
bundle = bundlesMap.get(key);
|
||||
} else {
|
||||
bundle = bundlesMap.get(key);
|
||||
if (bundle == null) {
|
||||
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 = bundlesMap.get(key);
|
||||
if (bundle == null) {
|
||||
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);
|
||||
missingBundles.put(key, Boolean.TRUE);
|
||||
}
|
||||
} else {
|
||||
LOG.debug("Missing resource bundle [{}]!", aBundleName);
|
||||
missingBundles.add(key);
|
||||
missingBundles.put(key, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
return bundle;
|
||||
|
||||
@@ -33,6 +33,9 @@ import java.util.ResourceBundle;
|
||||
*/
|
||||
public class GlobalLocalizedTextProvider extends AbstractLocalizedTextProvider {
|
||||
|
||||
// Pinned to the value implicitly computed for the pre-6.11.0 class shape, see AbstractLocalizedTextProvider.
|
||||
private static final long serialVersionUID = 7569216885652454296L;
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(GlobalLocalizedTextProvider.class);
|
||||
|
||||
public GlobalLocalizedTextProvider() {
|
||||
|
||||
@@ -36,6 +36,9 @@ import java.util.ResourceBundle;
|
||||
*/
|
||||
public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider {
|
||||
|
||||
// Pinned to the value implicitly computed for the pre-6.11.0 class shape, see AbstractLocalizedTextProvider.
|
||||
private static final long serialVersionUID = -4377984952850818176L;
|
||||
|
||||
private static final Logger LOG = LogManager.getLogger(StrutsLocalizedTextProvider.class);
|
||||
|
||||
/**
|
||||
|
||||
@@ -100,6 +100,14 @@ public final class StrutsConstants {
|
||||
/** The default locale for the Struts application */
|
||||
public static final String STRUTS_LOCALE = "struts.locale";
|
||||
|
||||
/**
|
||||
* When enabled, request-derived locales (from {@code Accept-Language}, used when {@code struts.locale} is
|
||||
* unset) are restricted to the JVM's available-locale set; unavailable values fall back to the default.
|
||||
*
|
||||
* @since 6.11.0
|
||||
*/
|
||||
public static final String STRUTS_LOCALE_VALIDATE_REQUEST = "struts.locale.validateRequestLocale";
|
||||
|
||||
/** Whether to use a Servlet request parameter workaround necessary for some versions of WebLogic */
|
||||
public static final String STRUTS_DISPATCHER_PARAMETERSWORKAROUND = "struts.dispatcher.parametersWorkaround";
|
||||
|
||||
@@ -288,6 +296,22 @@ public final class StrutsConstants {
|
||||
*/
|
||||
public static final String STRUTS_OGNL_BEANINFO_CACHE_FACTORY = "struts.ognl.beanInfoCacheFactory";
|
||||
|
||||
/**
|
||||
* Specifies the type of cache to use for the localized-text provider caches. Valid values defined in
|
||||
* {@link com.opensymphony.xwork2.ognl.OgnlCacheFactory.CacheType}.
|
||||
*
|
||||
* @since 6.11.0
|
||||
*/
|
||||
public static final String STRUTS_I18N_CACHE_TYPE = "struts.i18n.cacheType";
|
||||
|
||||
/**
|
||||
* Specifies the maximum size of each localized-text provider cache. Configure based on the cache type
|
||||
* chosen and application-specific needs.
|
||||
*
|
||||
* @since 6.11.0
|
||||
*/
|
||||
public static final String STRUTS_I18N_CACHE_MAXSIZE = "struts.i18n.cacheMaxSize";
|
||||
|
||||
/**
|
||||
* Specifies the type of cache to use for BeanInfo objects.
|
||||
* @since 6.4.0
|
||||
|
||||
@@ -152,6 +152,11 @@ public class Dispatcher {
|
||||
*/
|
||||
private String defaultLocale;
|
||||
|
||||
/**
|
||||
* Store state of {@link StrutsConstants#STRUTS_LOCALE_VALIDATE_REQUEST} setting.
|
||||
*/
|
||||
private boolean validateRequestLocale = false;
|
||||
|
||||
/**
|
||||
* Store state of StrutsConstants.STRUTS_MULTIPART_SAVEDIR setting.
|
||||
*/
|
||||
@@ -311,6 +316,18 @@ public class Dispatcher {
|
||||
defaultLocale = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify state of {@link StrutsConstants#STRUTS_LOCALE_VALIDATE_REQUEST} setting.
|
||||
*
|
||||
* @param val New setting
|
||||
*
|
||||
* @since 6.11.0
|
||||
*/
|
||||
@Inject(value = StrutsConstants.STRUTS_LOCALE_VALIDATE_REQUEST, required = false)
|
||||
public void setValidateRequestLocale(String val) {
|
||||
validateRequestLocale = Boolean.parseBoolean(val);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify state of StrutsConstants.STRUTS_I18N_ENCODING setting.
|
||||
*
|
||||
@@ -950,7 +967,7 @@ public class Dispatcher {
|
||||
locale = LocaleUtils.toLocale(defaultLocale);
|
||||
} catch (IllegalArgumentException e) {
|
||||
try {
|
||||
locale = request.getLocale();
|
||||
locale = resolveRequestLocale(request);
|
||||
LOG.warn(new ParameterizedMessage("Cannot convert 'struts.locale' = [{}] to proper locale, defaulting to request locale [{}]",
|
||||
defaultLocale, locale), e);
|
||||
} catch (RuntimeException rex) {
|
||||
@@ -961,7 +978,7 @@ public class Dispatcher {
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
locale = request.getLocale();
|
||||
locale = resolveRequestLocale(request);
|
||||
} catch (RuntimeException rex) {
|
||||
LOG.warn("Cannot get locale from HTTP Request, falling back to system default locale", rex);
|
||||
locale = Locale.getDefault();
|
||||
@@ -970,6 +987,33 @@ public class Dispatcher {
|
||||
return locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the request locale. When {@code struts.locale.validateRequestLocale} is enabled and the
|
||||
* request locale is not part of the JVM's available-locale set, falls back to the configured
|
||||
* {@code struts.locale} when set and parseable, otherwise the JVM default. When disabled (default),
|
||||
* returns the request locale unchanged.
|
||||
*
|
||||
* @param request the current request
|
||||
* @return the locale to use for this request
|
||||
*
|
||||
* @since 6.11.0
|
||||
*/
|
||||
protected Locale resolveRequestLocale(HttpServletRequest request) {
|
||||
Locale locale = request.getLocale();
|
||||
if (!validateRequestLocale || LocaleUtils.isAvailableLocale(locale)) {
|
||||
return locale;
|
||||
}
|
||||
if (defaultLocale != null) {
|
||||
try {
|
||||
return LocaleUtils.toLocale(defaultLocale);
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.debug("Configured 'struts.locale' = [{}] is not parseable; falling back to system default", defaultLocale);
|
||||
}
|
||||
}
|
||||
LOG.debug("Request locale [{}] is not available; falling back to system default locale", locale);
|
||||
return Locale.getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the path to save uploaded files to (this is configurable).
|
||||
*
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
|
||||
### This can be used to set your default locale and encoding scheme
|
||||
# struts.locale=en_US
|
||||
### When true, restrict request-derived locales (Accept-Language, used when struts.locale is unset) to the
|
||||
### JVM's available-locale set; unavailable values fall back to the default locale. Defaults to false.
|
||||
struts.locale.validateRequestLocale=false
|
||||
struts.i18n.encoding=UTF-8
|
||||
|
||||
### if specified, the default object factory can be overridden here
|
||||
@@ -240,6 +243,13 @@ struts.ognl.expressionCacheType=wtlfu
|
||||
### chosen and application-specific needs.
|
||||
struts.ognl.expressionCacheMaxSize=10000
|
||||
|
||||
### Specifies the type of cache to use for the localized-text provider caches. See StrutsConstants for details.
|
||||
struts.i18n.cacheType=wtlfu
|
||||
|
||||
### Specifies the maximum size of each localized-text provider cache. This should be configured based on the
|
||||
### cache type chosen and application-specific needs.
|
||||
struts.i18n.cacheMaxSize=10000
|
||||
|
||||
### Specifies the type of cache to use for BeanInfo objects. See StrutsConstants class for further information.
|
||||
struts.ognl.beanInfoCacheType=wtlfu
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package com.opensymphony.xwork2.ognl;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNull;
|
||||
|
||||
public class OgnlCacheRemoveTest {
|
||||
|
||||
private void assertRemoveContract(OgnlCache<String, String> cache) {
|
||||
cache.put("k", "v");
|
||||
assertEquals("v", cache.get("k"));
|
||||
assertEquals("remove returns previous value", "v", cache.remove("k"));
|
||||
assertNull("entry gone after remove", cache.get("k"));
|
||||
assertNull("remove of absent key returns null", cache.remove("absent"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void caffeineCacheRemove() {
|
||||
assertRemoveContract(new OgnlCaffeineCache<>(10, 16));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void defaultCacheRemove() {
|
||||
assertRemoveContract(new OgnlDefaultCache<>(10, 16, 0.75f));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lruCacheRemove() {
|
||||
assertRemoveContract(new OgnlLRUCache<>(10, 16, 0.75f));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package com.opensymphony.xwork2.util;
|
||||
|
||||
/**
|
||||
* Simple fixture whose class-associated bundle ({@code CacheFixture.properties}) backs the
|
||||
* localized-text caching tests.
|
||||
*
|
||||
* @since 6.11.0
|
||||
*/
|
||||
public class CacheFixture {
|
||||
}
|
||||
@@ -34,6 +34,11 @@ import com.opensymphony.xwork2.test.TestBean2;
|
||||
import org.apache.struts2.config.StrutsXmlConfigurationProvider;
|
||||
import org.apache.struts2.interceptor.parameter.StrutsParameter;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.lang.reflect.Field;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
@@ -563,6 +568,109 @@ public class StrutsLocalizedTextProviderTest extends XWorkTestCase {
|
||||
assertEquals("Result of bean2.name lookup not as expected ?", "Okay! You found Me!", messageResult);
|
||||
}
|
||||
|
||||
public void testCachesAreBoundedByConfiguredMaxSize() {
|
||||
TestStrutsLocalizedTextProvider provider = new TestStrutsLocalizedTextProvider();
|
||||
provider.setI18nCacheMaxSize("100");
|
||||
ValueStack valueStack = ActionContext.getContext().getValueStack();
|
||||
|
||||
for (int i = 0; i < 20000; i++) {
|
||||
Locale locale = Locale.forLanguageTag("en-US-x" + String.format("%05d", i));
|
||||
provider.findText(CacheFixture.class, "cache.missing", locale, "Fallback", null, valueStack);
|
||||
}
|
||||
|
||||
assertTrue("bundlesMap not bounded ?", provider.bundlesMapSize() <= 2000);
|
||||
assertTrue("missingBundles not bounded ?", provider.missingBundlesSize() <= 2000);
|
||||
assertTrue("messageFormats not bounded ?", provider.messageFormatsSize() <= 2000);
|
||||
}
|
||||
|
||||
public void testCorrectTextStillReturnedUnderEviction() {
|
||||
TestStrutsLocalizedTextProvider provider = new TestStrutsLocalizedTextProvider();
|
||||
provider.setI18nCacheMaxSize("50");
|
||||
ValueStack valueStack = ActionContext.getContext().getValueStack();
|
||||
|
||||
// Force heavy eviction with many distinct locales.
|
||||
for (int i = 0; i < 5000; i++) {
|
||||
Locale locale = Locale.forLanguageTag("en-US-x" + String.format("%05d", i));
|
||||
provider.findText(CacheFixture.class, "cache.missing", locale, "Fallback", null, valueStack);
|
||||
}
|
||||
|
||||
// A real key in a real locale still resolves correctly after eviction pressure.
|
||||
String result = provider.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
|
||||
assertEquals("Static cached value", result);
|
||||
}
|
||||
|
||||
public void testReloadClearsBoundedCaches() {
|
||||
TestStrutsLocalizedTextProvider provider = new TestStrutsLocalizedTextProvider();
|
||||
ValueStack valueStack = ActionContext.getContext().getValueStack();
|
||||
|
||||
provider.findText(CacheFixture.class, "cache.missing", Locale.ENGLISH, "Fallback", null, valueStack);
|
||||
assertTrue("missingBundles not populated ?", provider.missingBundlesSize() > 0);
|
||||
|
||||
provider.callReloadBundlesForceReload();
|
||||
assertEquals("reload did not clear bundlesMap ?", 0, provider.bundlesMapSize());
|
||||
}
|
||||
|
||||
public void testProviderIsUsableAfterDeserialization() throws Exception {
|
||||
StrutsLocalizedTextProvider provider = new StrutsLocalizedTextProvider();
|
||||
ValueStack valueStack = ActionContext.getContext().getValueStack();
|
||||
provider.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
|
||||
oos.writeObject(provider);
|
||||
}
|
||||
Object restored;
|
||||
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
|
||||
restored = ois.readObject();
|
||||
}
|
||||
StrutsLocalizedTextProvider deserialized = (StrutsLocalizedTextProvider) restored;
|
||||
// Caches were transient (null right after defaultReadObject) but readObject rebuilds them:
|
||||
assertEquals("Deserialized caches not rebuilt empty", 0, deserialized.bundlesMapSize());
|
||||
String result = deserialized.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
|
||||
assertEquals("Static cached value", result);
|
||||
}
|
||||
|
||||
/**
|
||||
* A stream written before the i18n cache settings existed carries no value for them, and field
|
||||
* initialisers do not run during deserialization, so they arrive as null/0. The provider must still
|
||||
* come back usable rather than failing while rebuilding its caches.
|
||||
*/
|
||||
public void testProviderIsUsableAfterDeserializingLegacyStream() throws Exception {
|
||||
StrutsLocalizedTextProvider provider = new StrutsLocalizedTextProvider();
|
||||
ValueStack valueStack = ActionContext.getContext().getValueStack();
|
||||
provider.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
|
||||
|
||||
// Simulate the absent-field state an older stream produces.
|
||||
Field cacheType = AbstractLocalizedTextProvider.class.getDeclaredField("i18nCacheType");
|
||||
cacheType.setAccessible(true);
|
||||
cacheType.set(provider, null);
|
||||
Field maxSize = AbstractLocalizedTextProvider.class.getDeclaredField("i18nCacheMaxSize");
|
||||
maxSize.setAccessible(true);
|
||||
maxSize.setInt(provider, 0);
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
|
||||
oos.writeObject(provider);
|
||||
}
|
||||
Object restored;
|
||||
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {
|
||||
restored = ois.readObject();
|
||||
}
|
||||
|
||||
StrutsLocalizedTextProvider deserialized = (StrutsLocalizedTextProvider) restored;
|
||||
String result = deserialized.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
|
||||
assertEquals("Static cached value", result);
|
||||
}
|
||||
|
||||
public void testCacheTypeSelectionKeepsProviderWorking() {
|
||||
TestStrutsLocalizedTextProvider provider = new TestStrutsLocalizedTextProvider();
|
||||
provider.setI18nCacheType("basic");
|
||||
ValueStack valueStack = ActionContext.getContext().getValueStack();
|
||||
String result = provider.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, null, null, valueStack);
|
||||
assertEquals("Static cached value", result);
|
||||
assertTrue("bundlesMap should populate", provider.bundlesMapSize() >= 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUp() throws Exception {
|
||||
super.setUp();
|
||||
|
||||
@@ -570,6 +570,46 @@ public class DispatcherTest extends StrutsJUnit4InternalTestCase {
|
||||
assertEquals(Locale.getDefault(), context.getLocale()); // Expect the system default value when Mock request access fails.
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateRequestLocaleOffPassesThrough() {
|
||||
initDispatcher(new HashMap<>());
|
||||
dispatcher.setDefaultLocale(null); // Force struts.locale unset; the test-config default would otherwise mask the request locale.
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
// A syntactically valid but not JVM-available locale.
|
||||
Locale exotic = new Locale("en", "US", "xzz99");
|
||||
when(request.getLocale()).thenReturn(exotic);
|
||||
|
||||
assertEquals("Default off must pass the request locale through unchanged",
|
||||
exotic, dispatcher.getLocale(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateRequestLocaleOnKeepsAvailableLocale() {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put(StrutsConstants.STRUTS_LOCALE_VALIDATE_REQUEST, "true");
|
||||
initDispatcher(params);
|
||||
dispatcher.setDefaultLocale(null); // Force struts.locale unset; the test-config default would otherwise mask the request locale.
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
when(request.getLocale()).thenReturn(Locale.UK);
|
||||
|
||||
assertEquals("Available request locale must be kept", Locale.UK, dispatcher.getLocale(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateRequestLocaleOnFallsBackForUnavailableLocale() {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put(StrutsConstants.STRUTS_LOCALE_VALIDATE_REQUEST, "true");
|
||||
initDispatcher(params);
|
||||
dispatcher.setDefaultLocale(null); // Force struts.locale unset; the test-config default would otherwise mask the request locale.
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
Locale exotic = new Locale("en", "US", "xzz99");
|
||||
when(request.getLocale()).thenReturn(exotic);
|
||||
|
||||
// struts.locale unset in this dispatcher -> fall back to the JVM default.
|
||||
assertEquals("Unavailable request locale must fall back to system default",
|
||||
Locale.getDefault(), dispatcher.getLocale(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dispatcherReinjectedAfterReload() {
|
||||
HttpServletRequest request = mock(HttpServletRequest.class);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
cache.static=Static cached value
|
||||
Reference in New Issue
Block a user