This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/master by this push: new f62384a TextProvider feature addition and cleanup: - Introduce (optional) control flag STRUTS_I18N_SEARCH_DEFAULTBUNDLES_FIRST to request TextProviders read from default resource bundles first, instead of their standard lookup ordering. Defaults to false. - Minor refactor of AbstractLocalizedTextProvider hierarchy to introduce getDefaultMessageWithAlternateKey() method that repackages existing logic used by the SrutsLocalizedTextProvider and GlobalLocalizedTextProvider. - Updat [...] new e870763 Merge pull request #467 from JCgH4164838Gh792C124B5/localS2_26_TextProvider_improvement_1 f62384a is described below commit f62384a471bdf93ce53d7bf9f6e86b4287f4e23a Author: JCgH4164838Gh792C124B5 <43964333+jcgh4164838gh792c12...@users.noreply.github.com> AuthorDate: Sun Jan 3 15:33:03 2021 -0500 TextProvider feature addition and cleanup: - Introduce (optional) control flag STRUTS_I18N_SEARCH_DEFAULTBUNDLES_FIRST to request TextProviders read from default resource bundles first, instead of their standard lookup ordering. Defaults to false. - Minor refactor of AbstractLocalizedTextProvider hierarchy to introduce getDefaultMessageWithAlternateKey() method that repackages existing logic used by the SrutsLocalizedTextProvider and GlobalLocalizedTextProvider. - Update GlobalLocalizedTextProvider method comments to properly reflect the actual logic of some of its methods. - New unit tests to confirm standard and default bundle first lookup ordering. - Cleanup of some unused imports, a few typos, and code formatting items. - Cleanup of default.properties comments to make them more consistent. --- .../xwork2/util/AbstractLocalizedTextProvider.java | 63 ++++++- .../xwork2/util/GlobalLocalizedTextProvider.java | 56 ++---- .../xwork2/util/StrutsLocalizedTextProvider.java | 43 +++-- .../java/org/apache/struts2/StrutsConstants.java | 11 ++ .../org/apache/struts2/default.properties | 24 +-- .../util/StrutsLocalizedTextProviderTest.java | 190 +++++++++++++++++++++ .../com/opensymphony/xwork2/util/Bar.properties | 2 + .../xwork2/util/LocalizedTextUtilTest.properties | 1 + 8 files changed, 313 insertions(+), 77 deletions(-) diff --git a/core/src/main/java/com/opensymphony/xwork2/util/AbstractLocalizedTextProvider.java b/core/src/main/java/com/opensymphony/xwork2/util/AbstractLocalizedTextProvider.java index 6ed6202..d197d05 100644 --- a/core/src/main/java/com/opensymphony/xwork2/util/AbstractLocalizedTextProvider.java +++ b/core/src/main/java/com/opensymphony/xwork2/util/AbstractLocalizedTextProvider.java @@ -61,6 +61,7 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { 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<>(); @@ -68,7 +69,7 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { private final ConcurrentMap<Integer, ClassLoader> delegatedClassLoaderMap = new ConcurrentHashMap<>(); /** - * Add's the bundle to the internal list of default bundles. + * Adds 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. @@ -431,6 +432,20 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { } /** + * Set the {@link #searchDefaultBundlesFirst} flag state. This flag may be used by descendant TextProvider + * implementations to determine if default bundles should be searched for messages first (before the standard + * flow of the {@link LocalizedTextProvider} implementation the descendant provides). + * + * @param searchDefaultBundlesFirst provide {@link String} "true" or "false" to set the flag state accordingly. + * + * @since 2.6 + */ + @Inject(value = StrutsConstants.STRUTS_I18N_SEARCH_DEFAULTBUNDLES_FIRST, required = false) + public void setSearchDefaultBundlesFirst(String searchDefaultBundlesFirst) { + this.searchDefaultBundlesFirst = Boolean.parseBoolean(searchDefaultBundlesFirst); + } + + /** * Finds the given resource bundle by it's name. * <p> * Will use <code>Thread.currentThread().getContextClassLoader()</code> as the classloader. @@ -549,6 +564,42 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { } /** + * A helper method that can be used by descendant classes to perform some common two-stage message lookup operations + * against the default resource bundles. The default resource bundles are searched for a value using key first, then + * alternateKey when the first search fails, then utilizing defaultMessage (which may be <code>null</code>) if <em>both</em> + * key lookup operations fail. + * + * <p> + * A known use case is when a key indexes a collection (e.g. user.phone[0]) for which some specific keys may exist, but not all, + * along with a general key (e.g. user.phone[*]). In such cases the specific key would be passed in the key parameter and the + * general key would be passed in the alternateKey parameter. + * </p> + * + * @param key the initial key to search for a value within the default resource bundles. + * @param alternateKey the alternate (fall-back) key to search for a value within the default resource bundles, if the initial key lookup fails. + * @param locale the {@link Locale} to be used for the default resource bundle lookup. + * @param valueStack the {@link ValueStack} associated with the operation. + * @param args the argument array for parameterized messages (may be <code>null</code>). + * @param defaultMessage the default message {@link String} to use if both key lookup operations fail. + * @return the {@link GetDefaultMessageReturnArg} result containing the processed message lookup (by key first, then alternateKey if key's lookup fails). + * If both key lookup operations fail, defaultMessage is used for processing. + * If defaultMessage is <code>null</code> then the return result may be <code>null</code>. + */ + protected GetDefaultMessageReturnArg getDefaultMessageWithAlternateKey(String key, String alternateKey, Locale locale, ValueStack valueStack, + Object[] args, String defaultMessage) { + GetDefaultMessageReturnArg result; + if (alternateKey == null || alternateKey.isEmpty()) { + result = getDefaultMessage(key, locale, valueStack, args, defaultMessage); + } else { + result = getDefaultMessage(key, locale, valueStack, args, null); + if (result == null || result.message == null) { + result = getDefaultMessage(alternateKey, locale, valueStack, args, defaultMessage); + } + } + return result; + } + + /** * @return the message from the named resource bundle. */ protected String getMessage(String bundleName, Locale locale, String key, ValueStack valueStack, Object[] args) { @@ -556,12 +607,14 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { if (bundle == null) { return null; } - if (valueStack != null) + if (valueStack != null) { reloadBundles(valueStack.getContext()); + } try { - String message = bundle.getString(key); - if (valueStack != null) - message = TextParseUtil.translateVariables(bundle.getString(key), valueStack); + String message = bundle.getString(key); + if (valueStack != null) { + message = TextParseUtil.translateVariables(bundle.getString(key), valueStack); + } MessageFormat mf = buildMessageFormat(message, locale); return formatWithNullDetection(mf, args); } catch (MissingResourceException e) { diff --git a/core/src/main/java/com/opensymphony/xwork2/util/GlobalLocalizedTextProvider.java b/core/src/main/java/com/opensymphony/xwork2/util/GlobalLocalizedTextProvider.java index fce1b99..d86682f 100644 --- a/core/src/main/java/com/opensymphony/xwork2/util/GlobalLocalizedTextProvider.java +++ b/core/src/main/java/com/opensymphony/xwork2/util/GlobalLocalizedTextProvider.java @@ -19,7 +19,6 @@ package com.opensymphony.xwork2.util; import com.opensymphony.xwork2.ActionContext; -import com.opensymphony.xwork2.ModelDriven; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -27,8 +26,10 @@ import java.util.Locale; import java.util.ResourceBundle; /** - * 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. + * Provides support for localization in the framework, it can be used to read only default bundles. + * + * Note that unlike {@link StrutsLocalizedTextProvider}, this class {@link GlobalLocalizedTextProvider} will + * <em>only</em> search the default bundles for localized text. */ public class GlobalLocalizedTextProvider extends AbstractLocalizedTextProvider { @@ -62,22 +63,8 @@ public class GlobalLocalizedTextProvider extends AbstractLocalizedTextProvider { * </p> * * <ol> - * <li>Look for message in aClass' class hierarchy. - * <ol> - * <li>Look for the message in a resource bundle for aClass</li> - * <li>If not found, look for the message in a resource bundle for any implemented interface</li> - * <li>If not found, traverse up the Class' hierarchy and repeat from the first sub-step</li> - * </ol></li> - * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in - * the model's class hierarchy (repeat sub-steps listed above).</li> - * <li>If not found, look for message in child property. This is determined by evaluating - * the message key as an OGNL expression. For example, if the key is - * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an - * object. If so, repeat the entire process fromthe beginning with the object's class as - * aClass and "address.state" as the message key.</li> - * <li>If not found, look for the message in aClass' package hierarchy.</li> - * <li>If still not found, look for the message in the default resource bundles.</li> - * <li>Return defaultMessage</li> + * <li>Look for the message in the default resource bundles.</li> + * <li>If not found, return defaultMessage</li> * </ol> * * <p> @@ -115,22 +102,8 @@ public class GlobalLocalizedTextProvider extends AbstractLocalizedTextProvider { * </p> * * <ol> - * <li>Look for message in aClass' class hierarchy. - * <ol> - * <li>Look for the message in a resource bundle for aClass</li> - * <li>If not found, look for the message in a resource bundle for any implemented interface</li> - * <li>If not found, traverse up the Class' hierarchy and repeat from the first sub-step</li> - * </ol></li> - * <li>If not found and aClass is a {@link ModelDriven} Action, then look for message in - * the model's class hierarchy (repeat sub-steps listed above).</li> - * <li>If not found, look for message in child property. This is determined by evaluating - * the message key as an OGNL expression. For example, if the key is - * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an - * object. If so, repeat the entire process fromthe beginning with the object's class as - * aClass and "address.state" as the message key.</li> - * <li>If not found, look for the message in aClass' package hierarchy.</li> - * <li>If still not found, look for the message in the default resource bundles.</li> - * <li>Return defaultMessage</li> + * <li>Look for the message in the default resource bundles.</li> + * <li>If not found, return defaultMessage</li> * </ol> * * <p> @@ -145,7 +118,7 @@ public class GlobalLocalizedTextProvider extends AbstractLocalizedTextProvider { * </p> * * <p> - * If a message is <b>not</b> found a WARN log will be logged. + * If a message is <b>not</b> found a DEBUG level log warning will be logged. * </p> * * @param aClass the class whose name to use as the start point for the search @@ -180,16 +153,7 @@ public class GlobalLocalizedTextProvider extends AbstractLocalizedTextProvider { } // get default - GetDefaultMessageReturnArg result; - if (indexedTextName == null) { - result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage); - } else { - result = getDefaultMessage(aTextName, locale, valueStack, args, null); - if (result != null && result.message != null) { - return result.message; - } - result = getDefaultMessage(indexedTextName, locale, valueStack, args, defaultMessage); - } + GetDefaultMessageReturnArg result = getDefaultMessageWithAlternateKey(aTextName, indexedTextName, locale, valueStack, args, defaultMessage); // could we find the text, if not log a warn if (unableToFindTextForKey(result) && LOG.isDebugEnabled()) { diff --git a/core/src/main/java/com/opensymphony/xwork2/util/StrutsLocalizedTextProvider.java b/core/src/main/java/com/opensymphony/xwork2/util/StrutsLocalizedTextProvider.java index 1dc74b7..48acc42 100644 --- a/core/src/main/java/com/opensymphony/xwork2/util/StrutsLocalizedTextProvider.java +++ b/core/src/main/java/com/opensymphony/xwork2/util/StrutsLocalizedTextProvider.java @@ -27,9 +27,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.beans.PropertyDescriptor; -import java.text.MessageFormat; import java.util.Locale; -import java.util.MissingResourceException; import java.util.ResourceBundle; /** @@ -122,6 +120,7 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { * </p> * * <ol> + * <li>If {@link #searchDefaultBundlesFirst} is <code>true</code>, look for the message in the default resource bundles first.</li> * <li>Look for message in aClass' class hierarchy. * <ol> * <li>Look for the message in a resource bundle for aClass</li> @@ -133,10 +132,11 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { * <li>If not found, look for message in child property. This is determined by evaluating * the message key as an OGNL expression. For example, if the key is * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an - * object. If so, repeat the entire process fromthe beginning with the object's class as + * object. If so, repeat the entire process from the beginning with the object's class as * aClass and "address.state" as the message key.</li> * <li>If not found, look for the message in aClass' package hierarchy.</li> - * <li>If still not found, look for the message in the default resource bundles.</li> + * <li>If still not found, look for the message in the default resource bundles + * (Note: the lookup is not repeated again if {@link #searchDefaultBundlesFirst} was <code>true</code>).</li> * <li>Return defaultMessage</li> * </ol> * @@ -175,6 +175,7 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { * </p> * * <ol> + * <li>If {@link #searchDefaultBundlesFirst} is <code>true</code>, look for the message in the default resource bundles first.</li> * <li>Look for message in aClass' class hierarchy. * <ol> * <li>Look for the message in a resource bundle for aClass</li> @@ -186,10 +187,11 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { * <li>If not found, look for message in child property. This is determined by evaluating * the message key as an OGNL expression. For example, if the key is * <i>user.address.state</i>, then it will attempt to see if "user" can be resolved into an - * object. If so, repeat the entire process fromthe beginning with the object's class as + * object. If so, repeat the entire process from the beginning with the object's class as * aClass and "address.state" as the message key.</li> * <li>If not found, look for the message in aClass' package hierarchy.</li> - * <li>If still not found, look for the message in the default resource bundles.</li> + * <li>If still not found, look for the message in the default resource bundles + * (Note: the lookup is not repeated again if {@link #searchDefaultBundlesFirst} was <code>true</code>).</li> * <li>Return defaultMessage</li> * </ol> * @@ -205,7 +207,7 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { * </p> * * <p> - * If a message is <b>not</b> found a WARN log will be logged. + * If a message is <b>not</b> found a DEBUG level log warning will be logged. * </p> * * @param aClass the class whose name to use as the start point for the search @@ -240,6 +242,20 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { } } + // Allow for and track an early lookup for the message in the default resource bundles first, before searching the class hierarchy. + // The early lookup is only performed when the text provider has been configured to do so, otherwise follow the standard processing order. + boolean performedInitialDefaultBundlesMessageLookup = false; + GetDefaultMessageReturnArg result = null; + + // If search default bundles first is set true, call alternative logic first. + if (searchDefaultBundlesFirst) { + result = getDefaultMessageWithAlternateKey(aTextName, indexedTextName, locale, valueStack, args, defaultMessage); + performedInitialDefaultBundlesMessageLookup = true; + if (!unableToFindTextForKey(result)) { + return result.message; // Found a message in the default resource bundles for aTextName or indexedTextName. + } + } + // search up class hierarchy String msg = findMessage(aClass, aTextName, indexedTextName, locale, args, null, valueStack); @@ -342,15 +358,10 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { } // get default - GetDefaultMessageReturnArg result; - if (indexedTextName == null) { - result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage); - } else { - result = getDefaultMessage(aTextName, locale, valueStack, args, null); - if (result != null && result.message != null) { - return result.message; - } - result = getDefaultMessage(indexedTextName, locale, valueStack, args, defaultMessage); + // Note: The default bundles lookup may already have been performed (via alternate early lookup), + // so we check first to avoid repeating the same operation twice. + if (!performedInitialDefaultBundlesMessageLookup) { + result = getDefaultMessageWithAlternateKey(aTextName, indexedTextName, locale, valueStack, args, defaultMessage); } // could we find the text, if not log a warn diff --git a/core/src/main/java/org/apache/struts2/StrutsConstants.java b/core/src/main/java/org/apache/struts2/StrutsConstants.java index 45b44a6..9e6ff62 100644 --- a/core/src/main/java/org/apache/struts2/StrutsConstants.java +++ b/core/src/main/java/org/apache/struts2/StrutsConstants.java @@ -35,6 +35,17 @@ public final class StrutsConstants { /** The encoding to use for localization messages */ public static final String STRUTS_I18N_ENCODING = "struts.i18n.encoding"; + /** + * Whether the default bundles should be searched for messages first. Can be used to modify the + * standard processing order for message lookup in TextProvider implementations. + * <p> + * Note: This control flag may not be meaningful to all provider implementations, and should be false by default. + * </p> + * + * @since 2.6 + */ + public static final String STRUTS_I18N_SEARCH_DEFAULTBUNDLES_FIRST = "struts.i18n.search.defaultbundles.first"; + /** Whether to reload the XML configuration or not */ public static final String STRUTS_CONFIGURATION_XML_RELOAD = "struts.configuration.xml.reload"; diff --git a/core/src/main/resources/org/apache/struts2/default.properties b/core/src/main/resources/org/apache/struts2/default.properties index 1bbe20c..c61c5d4 100644 --- a/core/src/main/resources/org/apache/struts2/default.properties +++ b/core/src/main/resources/org/apache/struts2/default.properties @@ -19,7 +19,7 @@ ### START SNIPPET: complete_file ### Struts default properties -###(can be overridden by a struts.properties file in the root of the classpath) +### (can be overridden by a struts.properties file in the root of the classpath) ### ### This can be used to set your default locale and encoding scheme @@ -57,15 +57,15 @@ struts.objectFactory.spring.enableAopSupport = false ### using generics. com.opensymphony.xwork2.util.GenericsObjectTypeDeterminer was deprecated since XWork 2, it's ### functions are integrated in DefaultObjectTypeDeterminer now. ### To disable tiger support use the "notiger" property value here. -#struts.objectTypeDeterminer = tiger -#struts.objectTypeDeterminer = notiger +# struts.objectTypeDeterminer = tiger +# struts.objectTypeDeterminer = notiger ### Parser to handle HTTP POST requests, encoded using the MIME-type multipart/form-data # struts.multipart.parser=cos # struts.multipart.parser=pell # struts.multipart.parser=jakarta-stream struts.multipart.parser=jakarta -# uses javax.servlet.context.tempdir by default +### Uses javax.servlet.context.tempdir by default struts.multipart.saveDir= struts.multipart.maxSize=2097152 @@ -74,7 +74,7 @@ struts.multipart.maxSize=2097152 ### How request URLs are mapped to and from actions ### Vy default 'struts' name is used which maps to DefaultActionMapper -#struts.mapper.class=restful +# struts.mapper.class=restful ### Used by the DefaultActionMapper ### You may provide a comma separated list, e.g. struts.action.extension=action,jnlp,do @@ -136,7 +136,7 @@ struts.devMode = false ### when set to true, resource bundles will be reloaded on _every_ request. ### this is good during development, but should never be used in production -### struts.i18n.reload=false +# struts.i18n.reload=false ### Standard UI theme ### Change this to reflect which path should be used for JSP control tag templates by default @@ -144,12 +144,12 @@ struts.ui.theme=xhtml struts.ui.templateDir=template ### Change this to use a different token to indicate template theme expansion struts.ui.theme.expansion.token=~~~ -#sets the default template type. Either ftl, vm, or jsp +### Sets the default template type. Either ftl, vm, or jsp struts.ui.templateSuffix=ftl ### Configuration reloading ### This will cause the configuration to reload struts.xml when it is changed -### struts.configuration.xml.reload=false +# struts.configuration.xml.reload=false ### Location of velocity.properties file. defaults to velocity.properties struts.velocity.configfile = velocity.properties @@ -169,6 +169,10 @@ struts.url.includeParams = none ### Load custom default resource bundles # struts.custom.i18n.resources=testmessages,testmessages2 +### Control whether to search the default resource bundes for messages first (if true) or not (if false). +### Default is false (when not set). +# struts.i18n.search.defaultbundles.first=false + ### workaround for some app servers that don't handle HttpServletRequest.getParameterMap() ### often used for WebLogic, Orion, and OC4J struts.dispatcher.parametersWorkaround = false @@ -176,11 +180,11 @@ struts.dispatcher.parametersWorkaround = false ### configure the Freemarker Manager class to be used ### Allows user to plug-in customised Freemarker Manager if necessary ### MUST extends off org.apache.struts2.views.freemarker.FreemarkerManager -#struts.freemarker.manager.classname=org.apache.struts2.views.freemarker.FreemarkerManager +# struts.freemarker.manager.classname=org.apache.struts2.views.freemarker.FreemarkerManager ### Enables caching of FreeMarker templates ### Has the same effect as copying the templates under WEB_APP/templates -### struts.freemarker.templatesCache=false +# struts.freemarker.templatesCache=false ### Enables caching of models on the BeanWrapper struts.freemarker.beanwrapperCache=false diff --git a/core/src/test/java/com/opensymphony/xwork2/util/StrutsLocalizedTextProviderTest.java b/core/src/test/java/com/opensymphony/xwork2/util/StrutsLocalizedTextProviderTest.java index 2a3038f..f7669b1 100644 --- a/core/src/test/java/com/opensymphony/xwork2/util/StrutsLocalizedTextProviderTest.java +++ b/core/src/test/java/com/opensymphony/xwork2/util/StrutsLocalizedTextProviderTest.java @@ -371,6 +371,196 @@ public class StrutsLocalizedTextProviderTest extends XWorkTestCase { 2, testStrutsLocalizedTextProvider.currentBundlesMapSize()); } + /** + * Test the {@link StrutsLocalizedTextProvider#searchDefaultBundlesFirst} flag behaviour for basic correctness. + */ + public void testSetSearchDefaultBundlesFirst() { + TestStrutsLocalizedTextProvider testStrutsLocalizedTextProvider = new TestStrutsLocalizedTextProvider(); + assertFalse("Default setSearchDefaultBundlesFirst state is not false ?", testStrutsLocalizedTextProvider.searchDefaultBundlesFirst); + testStrutsLocalizedTextProvider.setSearchDefaultBundlesFirst(Boolean.TRUE.toString()); + assertTrue("The setSearchDefaultBundlesFirst state is not true after explicit set ?", testStrutsLocalizedTextProvider.searchDefaultBundlesFirst); + testStrutsLocalizedTextProvider.setSearchDefaultBundlesFirst(Boolean.FALSE.toString()); + assertFalse("The setSearchDefaultBundlesFirst state is not false after explicit set ?", testStrutsLocalizedTextProvider.searchDefaultBundlesFirst); + testStrutsLocalizedTextProvider.setSearchDefaultBundlesFirst("invalidstring"); + assertFalse("The setSearchDefaultBundlesFirst state is not false after set with invalid value ?", testStrutsLocalizedTextProvider.searchDefaultBundlesFirst); + } + + /** + * Test the {@link StrutsLocalizedTextProvider#getDefaultMessageWithAlternateKey(java.lang.String, java.lang.String, java.util.Locale, com.opensymphony.xwork2.util.ValueStack, java.lang.Object[], java.lang.String)} + * method for basic correctness. + */ + public void testGetDefaultMessageWithAlternateKey() { + final String DEFAULT_MESSAGE = "This is the default message."; + final String DEFAULT_MESSAGE_WITH_PARAMS = DEFAULT_MESSAGE + " We provide a couple of parameter placeholders: -{0}- and -{1}- for fun."; + final String param1 = "param1_String"; + final String param2 = "param2_String"; + final String[] paramArray = { param1, param2 }; + TestStrutsLocalizedTextProvider testStrutsLocalizedTextProvider = new TestStrutsLocalizedTextProvider(); + + // Load some specific default bundles already provided and used by other tests within this module. + testStrutsLocalizedTextProvider.addDefaultResourceBundle("com/opensymphony/xwork2/util/LocalizedTextUtilTest"); + testStrutsLocalizedTextProvider.addDefaultResourceBundle("com/opensymphony/xwork2/util/Bar"); + testStrutsLocalizedTextProvider.addDefaultResourceBundle("com/opensymphony/xwork2/util/FindMe"); + + // Perform some standard checks on message retrieval using null or nonexistent keys and various default message combinations. + ValueStack valueStack = ActionContext.getContext().getValueStack(); + AbstractLocalizedTextProvider.GetDefaultMessageReturnArg getDefaultMessageReturnArg = testStrutsLocalizedTextProvider.getDefaultMessageWithAlternateKey(null, null, Locale.ENGLISH, valueStack, null, null); + assertNull("GetDefaultMessageReturnArg result not null with null keys and null default message ?", getDefaultMessageReturnArg); + getDefaultMessageReturnArg = testStrutsLocalizedTextProvider.getDefaultMessageWithAlternateKey("key_does_not_exist", "alternateKey_does_not_exist", Locale.ENGLISH, valueStack, null, null); + assertNull("GetDefaultMessageReturnArg result not null with nonexistent keys and null default message ?", getDefaultMessageReturnArg); + getDefaultMessageReturnArg = testStrutsLocalizedTextProvider.getDefaultMessageWithAlternateKey("key_does_not_exist", "alternateKey_does_not_exist", Locale.ENGLISH, valueStack, null, DEFAULT_MESSAGE); + assertNotNull("GetDefaultMessageReturnArg result is null with nonexistent keys and non-null default message ?", getDefaultMessageReturnArg); + assertFalse("GetDefaultMessageReturnArg result with nonexistent keys indicates message found in bundle ?", getDefaultMessageReturnArg.foundInBundle); + assertEquals("GetDefaultMessageReturnArg result with nonexistent keys indicates message found in bundle ?", DEFAULT_MESSAGE, getDefaultMessageReturnArg.message); + getDefaultMessageReturnArg = testStrutsLocalizedTextProvider.getDefaultMessageWithAlternateKey("key_does_not_exist", "alternateKey_does_not_exist", Locale.ENGLISH, valueStack, paramArray, DEFAULT_MESSAGE_WITH_PARAMS); + assertNotNull("GetDefaultMessageReturnArg result is null with nonexistent keys and non-null default message ?", getDefaultMessageReturnArg); + assertFalse("GetDefaultMessageReturnArg result with nonexistent keys indicates message found in bundle ?", getDefaultMessageReturnArg.foundInBundle); + assertNotNull("GetDefaultMessageReturnArg result message is null ?", getDefaultMessageReturnArg.message); + assertTrue("GetDefaultMessageReturnArg result message does not contain deafult message ?", getDefaultMessageReturnArg.message.contains(DEFAULT_MESSAGE)); + assertTrue("GetDefaultMessageReturnArg result message does not contain param1 ?", getDefaultMessageReturnArg.message.contains(param1)); + assertTrue("GetDefaultMessageReturnArg result message does not contain param2 ?", getDefaultMessageReturnArg.message.contains(param2)); + + // Perform some checks where the initial key is null or does not exist in the default bundles, but the alternate key does. + getDefaultMessageReturnArg = testStrutsLocalizedTextProvider.getDefaultMessageWithAlternateKey(null, "username", Locale.ENGLISH, valueStack, paramArray, null); + assertNotNull("GetDefaultMessageReturnArg result is null with alternate key that exists ?", getDefaultMessageReturnArg); + assertTrue("GetDefaultMessageReturnArg result with alternate key that exists indicates message not found in bundle ?", getDefaultMessageReturnArg.foundInBundle); + assertTrue("GetDefaultMessageReturnArg result with alternate key that exists indicates message is null or empty ?", (getDefaultMessageReturnArg.message != null && !getDefaultMessageReturnArg.message.isEmpty())); + assertEquals("GetDefaultMessageReturnArg result with alternate key 'username' not as expected ?", "Santa", getDefaultMessageReturnArg.message); + getDefaultMessageReturnArg = testStrutsLocalizedTextProvider.getDefaultMessageWithAlternateKey("key_does_not_exist", "invalid.fieldvalue.title", Locale.ENGLISH, valueStack, paramArray, null); + assertNotNull("GetDefaultMessageReturnArg result is null with alternate key that exists ?", getDefaultMessageReturnArg); + assertTrue("GetDefaultMessageReturnArg result with alternate key that exists indicates message not found in bundle ?", getDefaultMessageReturnArg.foundInBundle); + assertTrue("GetDefaultMessageReturnArg result with alternate key that exists indicates message is null or empty ?", (getDefaultMessageReturnArg.message != null && !getDefaultMessageReturnArg.message.isEmpty())); + assertEquals("GetDefaultMessageReturnArg result with alternate key 'invalid.fieldvalue.title' not as expected ?", "Title is invalid!", getDefaultMessageReturnArg.message); + + // Perform some checks where the initial key exists, but the alternate key is null or nonexistent. + getDefaultMessageReturnArg = testStrutsLocalizedTextProvider.getDefaultMessageWithAlternateKey("username", null, Locale.ENGLISH, valueStack, paramArray, null); + assertNotNull("GetDefaultMessageReturnArg result is null with key that exists ?", getDefaultMessageReturnArg); + assertTrue("GetDefaultMessageReturnArg result with key that exists indicates message not found in bundle ?", getDefaultMessageReturnArg.foundInBundle); + assertTrue("GetDefaultMessageReturnArg result with key that exists indicates message is null or empty ?", (getDefaultMessageReturnArg.message != null && !getDefaultMessageReturnArg.message.isEmpty())); + assertEquals("GetDefaultMessageReturnArg result with key 'username' not as expected ?", "Santa", getDefaultMessageReturnArg.message); + getDefaultMessageReturnArg = testStrutsLocalizedTextProvider.getDefaultMessageWithAlternateKey("invalid.fieldvalue.title", "key_does_not_exist", Locale.ENGLISH, valueStack, paramArray, null); + assertNotNull("GetDefaultMessageReturnArg result is null with key that exists ?", getDefaultMessageReturnArg); + assertTrue("GetDefaultMessageReturnArg result with key that exists indicates message not found in bundle ?", getDefaultMessageReturnArg.foundInBundle); + assertTrue("GetDefaultMessageReturnArg result with key that exists indicates message is null or empty ?", (getDefaultMessageReturnArg.message != null && !getDefaultMessageReturnArg.message.isEmpty())); + assertEquals("GetDefaultMessageReturnArg result with key 'invalid.fieldvalue.title' not as expected ?", "Title is invalid!", getDefaultMessageReturnArg.message); + + // Perform some checks where the initial key exists, and the alternate key exists. The result found for the initial key should be returned (not the alternate). + getDefaultMessageReturnArg = testStrutsLocalizedTextProvider.getDefaultMessageWithAlternateKey("username", "invalid.fieldvalue.title", Locale.ENGLISH, valueStack, paramArray, null); + assertNotNull("GetDefaultMessageReturnArg result is null with key that exists ?", getDefaultMessageReturnArg); + assertTrue("GetDefaultMessageReturnArg result with key that exists indicates message not found in bundle ?", getDefaultMessageReturnArg.foundInBundle); + assertTrue("GetDefaultMessageReturnArg result with key that exists indicates message is null or empty ?", (getDefaultMessageReturnArg.message != null && !getDefaultMessageReturnArg.message.isEmpty())); + assertEquals("GetDefaultMessageReturnArg result with key 'username' not as expected ?", "Santa", getDefaultMessageReturnArg.message); + getDefaultMessageReturnArg = testStrutsLocalizedTextProvider.getDefaultMessageWithAlternateKey("invalid.fieldvalue.title", "username", Locale.ENGLISH, valueStack, paramArray, null); + assertNotNull("GetDefaultMessageReturnArg result is null with key that exists ?", getDefaultMessageReturnArg); + assertTrue("GetDefaultMessageReturnArg result with key that exists indicates message not found in bundle ?", getDefaultMessageReturnArg.foundInBundle); + assertTrue("GetDefaultMessageReturnArg result with key that exists indicates message is null or empty ?", (getDefaultMessageReturnArg.message != null && !getDefaultMessageReturnArg.message.isEmpty())); + assertEquals("GetDefaultMessageReturnArg result with key 'invalid.fieldvalue.title' not as expected ?", "Title is invalid!", getDefaultMessageReturnArg.message); + } + + /** + * Test the {@link StrutsLocalizedTextProvider#findText(java.lang.Class, java.lang.String, java.util.Locale, java.lang.String, java.lang.Object[], com.opensymphony.xwork2.util.ValueStack) } + * method for basic correctness. + * + * It is the version of the method that will search the class hierarchy resource bundles first, unless {@link StrutsLocalizedTextProvider#searchDefaultBundlesFirst} + * is true (in which case it will search the default resource bundles first). No matter the flag setting, it should search until it finds a match, or fails to find + * a match and returns the default message parameter that was passed. + */ + public void testFindText_FullParameterSet_FirstParameterIsClass() { + final String DEFAULT_MESSAGE = "This is the default message."; + final String INDEXED_COLLECTION_ONLYGENERALFORM_EXISTS = "title.indexed[20]"; // Only title.indexed[*] exists. + final String EXISTS_IN_DEFAULT_AND_CLASS_BUNDLES = "compare.sameproperty.differentbundles"; // Exists in LocalizedTextUtilTest properties (default bundles), and Bar properties (class bundles only). + final String DEFAULT_MESSAGE_WITH_PARAMS = DEFAULT_MESSAGE + " We provide a couple of parameter placeholders: -{0}- and -{1}- for fun."; + final String param1 = "param1_String"; + final String param2 = "param2_String"; + final String[] paramArray = { param1, param2 }; + TestStrutsLocalizedTextProvider testStrutsLocalizedTextProvider = new TestStrutsLocalizedTextProvider(); + + // Load some specific default bundles already provided and used by other tests within this module. + // Note: Intentionally not including the Bar properties file as a default bundle so that we can test retrievals of items that are only available via the class + // or the default bundles. + testStrutsLocalizedTextProvider.addDefaultResourceBundle("com/opensymphony/xwork2/util/LocalizedTextUtilTest"); + testStrutsLocalizedTextProvider.addDefaultResourceBundle("com/opensymphony/xwork2/util/FindMe"); + + // Perform some standard checks on message retrieval both for correctness checks and code coverage (such as the NONEXISTENT_INDEXED_COLLECTION, + // which exercises the indexed name logic in findText()) + ValueStack valueStack = ActionContext.getContext().getValueStack(); + Bar bar = new Bar(); + assertFalse("Initial setSearchDefaultBundlesFirst state is not false ?", testStrutsLocalizedTextProvider.searchDefaultBundlesFirst); + String messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "title", Locale.ENGLISH, DEFAULT_MESSAGE, paramArray, valueStack); + assertEquals("Bar class title property lookup result does not match expectations (missing or different) ?", "Title:", messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), INDEXED_COLLECTION_ONLYGENERALFORM_EXISTS, Locale.ENGLISH, DEFAULT_MESSAGE, paramArray, valueStack); + assertEquals("Bar class general indexed collection lookup result does not match expectations (missing or different) ?", "Indexed title text for test!", messageResult); + + // Test lookup with search default bundles first set true. For properties that exist only with the class bundle, there should be no change. + // Repeat the tests with properties only in the class bundle. + testStrutsLocalizedTextProvider.setSearchDefaultBundlesFirst(Boolean.TRUE.toString()); + assertTrue("Updated setSearchDefaultBundlesFirst state is not true ?", testStrutsLocalizedTextProvider.searchDefaultBundlesFirst); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "title", Locale.ENGLISH, DEFAULT_MESSAGE, paramArray, valueStack); + assertEquals("Bar class title property lookup result does not match expectations (missing or different) ?", "Title:", messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), INDEXED_COLLECTION_ONLYGENERALFORM_EXISTS, Locale.ENGLISH, DEFAULT_MESSAGE, paramArray, valueStack); + assertEquals("Bar class general indexed collection lookup result does not match expectations (missing or different) ?", "Indexed title text for test!", messageResult); + + // Test with a property that is in both the class bundle and default bundles, with search default bundles first true. + // The property match from the default bundles should be returned. + testStrutsLocalizedTextProvider.setSearchDefaultBundlesFirst(Boolean.TRUE.toString()); + assertTrue("Updated setSearchDefaultBundlesFirst state is not true ?", testStrutsLocalizedTextProvider.searchDefaultBundlesFirst); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), EXISTS_IN_DEFAULT_AND_CLASS_BUNDLES, Locale.ENGLISH, DEFAULT_MESSAGE, paramArray, valueStack); + assertEquals("Result is not the property from the default bundles ?", "This is the value in the LocalizedTextUtilTest properties!", messageResult); + + // Test with a property that is in both the class bundle and default bundles, with search default bundles first false. + // The property match from the Bar class bundle should be returned. + testStrutsLocalizedTextProvider.setSearchDefaultBundlesFirst(Boolean.FALSE.toString()); + assertFalse("Updated setSearchDefaultBundlesFirst state is not false ?", testStrutsLocalizedTextProvider.searchDefaultBundlesFirst); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), EXISTS_IN_DEFAULT_AND_CLASS_BUNDLES, Locale.ENGLISH, DEFAULT_MESSAGE, paramArray, valueStack); + assertEquals("Result is not the property from the Bar bundle ?", "This is the value in the Bar properties!", messageResult); + + // Test with some different properties (including null and nonexistent ones), with search default bundles first false. + testStrutsLocalizedTextProvider.setSearchDefaultBundlesFirst(Boolean.FALSE.toString()); + assertFalse("Updated setSearchDefaultBundlesFirst state is not false ?", testStrutsLocalizedTextProvider.searchDefaultBundlesFirst); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), null, Locale.ENGLISH, null, paramArray, valueStack); + assertNull("Result with null key and null default message is not null ?", messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "key_does_not_exist", Locale.ENGLISH, null, paramArray, valueStack); + assertNull("Result with nonexistent key and null default message is not null ?", messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), null, Locale.ENGLISH, DEFAULT_MESSAGE, paramArray, valueStack); + assertEquals("Result with null key and non-null default message is not the default message ?", DEFAULT_MESSAGE, messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "key_does_not_exist", Locale.ENGLISH, DEFAULT_MESSAGE, paramArray, valueStack); + assertEquals("Result with nonexistent key and non-null default message is not the default message ?", DEFAULT_MESSAGE, messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "key_does_not_exist", Locale.ENGLISH, DEFAULT_MESSAGE_WITH_PARAMS, paramArray, valueStack); + assertNotNull("Result with nonexistent key and non-null default message is null ?", messageResult); + assertTrue("Result with parameterized default message does not contain deafult message ?", messageResult.contains(DEFAULT_MESSAGE)); + assertTrue("Result with parameterized default message does not contain param1 ?", messageResult.contains(param1)); + assertTrue("Result with parameterized default message does not contain param2 ?", messageResult.contains(param2)); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "username", Locale.ENGLISH, null, paramArray, valueStack); + assertEquals("Result of username lookup not as expected ?", "Santa", messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "bean.name", Locale.ENGLISH, null, paramArray, valueStack); + assertEquals("Result of bean.name lookup not as expected ?", "Haha you cant FindMe!", messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "bean2.name", Locale.ENGLISH, null, paramArray, valueStack); + assertEquals("Result of bean2.name lookup not as expected ?", "Okay! You found Me!", messageResult); + + // Test with some different properties (including null and nonexistent ones), with search default bundles first true. + testStrutsLocalizedTextProvider.setSearchDefaultBundlesFirst(Boolean.TRUE.toString()); + assertTrue("Updated setSearchDefaultBundlesFirst state is not true ?", testStrutsLocalizedTextProvider.searchDefaultBundlesFirst); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), null, Locale.ENGLISH, null, paramArray, valueStack); + assertNull("Result with null key and null default message is not null ?", messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "key_does_not_exist", Locale.ENGLISH, null, paramArray, valueStack); + assertNull("Result with nonexistent key and null default message is not null ?", messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), null, Locale.ENGLISH, DEFAULT_MESSAGE, paramArray, valueStack); + assertEquals("Result with null key and non-null default message is not the default message ?", DEFAULT_MESSAGE, messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "key_does_not_exist", Locale.ENGLISH, DEFAULT_MESSAGE, paramArray, valueStack); + assertEquals("Result with nonexistent key and non-null default message is not the default message ?", DEFAULT_MESSAGE, messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "key_does_not_exist", Locale.ENGLISH, DEFAULT_MESSAGE_WITH_PARAMS, paramArray, valueStack); + assertNotNull("Result with nonexistent key and non-null default message is null ?", messageResult); + assertTrue("Result with parameterized default message does not contain deafult message ?", messageResult.contains(DEFAULT_MESSAGE)); + assertTrue("Result with parameterized default message does not contain param1 ?", messageResult.contains(param1)); + assertTrue("Result with parameterized default message does not contain param2 ?", messageResult.contains(param2)); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "username", Locale.ENGLISH, null, paramArray, valueStack); + assertEquals("Result of username lookup not as expected ?", "Santa", messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "bean.name", Locale.ENGLISH, null, paramArray, valueStack); + assertEquals("Result of bean.name lookup not as expected ?", "Haha you cant FindMe!", messageResult); + messageResult = testStrutsLocalizedTextProvider.findText(bar.getClass(), "bean2.name", Locale.ENGLISH, null, paramArray, valueStack); + assertEquals("Result of bean2.name lookup not as expected ?", "Okay! You found Me!", messageResult); + } + @Override protected void setUp() throws Exception { super.setUp(); diff --git a/core/src/test/resources/com/opensymphony/xwork2/util/Bar.properties b/core/src/test/resources/com/opensymphony/xwork2/util/Bar.properties index 75eeb3b..99a1f1a 100644 --- a/core/src/test/resources/com/opensymphony/xwork2/util/Bar.properties +++ b/core/src/test/resources/com/opensymphony/xwork2/util/Bar.properties @@ -18,3 +18,5 @@ # title=Title: invalid.fieldvalue.title=Title is invalid! +title.indexed[*]=Indexed title text for test! +compare.sameproperty.differentbundles=This is the value in the Bar properties! diff --git a/core/src/test/resources/com/opensymphony/xwork2/util/LocalizedTextUtilTest.properties b/core/src/test/resources/com/opensymphony/xwork2/util/LocalizedTextUtilTest.properties index 1475796..16cb9e6 100644 --- a/core/src/test/resources/com/opensymphony/xwork2/util/LocalizedTextUtilTest.properties +++ b/core/src/test/resources/com/opensymphony/xwork2/util/LocalizedTextUtilTest.properties @@ -19,3 +19,4 @@ test.format.date={0,date,short} xw377=xw377 username=Santa +compare.sameproperty.differentbundles=This is the value in the LocalizedTextUtilTest properties!