WW-4762 Introduces default provider which only uses default bundles
Project: http://git-wip-us.apache.org/repos/asf/struts/repo Commit: http://git-wip-us.apache.org/repos/asf/struts/commit/bb19a620 Tree: http://git-wip-us.apache.org/repos/asf/struts/tree/bb19a620 Diff: http://git-wip-us.apache.org/repos/asf/struts/diff/bb19a620 Branch: refs/heads/default-provider Commit: bb19a6209886795b76385e48fd86424c144ee029 Parents: 2e23d7a Author: Lukasz Lenart <lukaszlen...@apache.org> Authored: Wed Apr 26 20:28:11 2017 +0200 Committer: Lukasz Lenart <lukaszlen...@apache.org> Committed: Wed Apr 26 20:28:11 2017 +0200 ---------------------------------------------------------------------- .../util/AbstractLocalizedTextProvider.java | 145 ++++++++- .../util/DefaultLocalizedTextProvider.java | 310 +++++++++++++++++++ .../util/StrutsLocalizedTextProvider.java | 161 +--------- 3 files changed, 461 insertions(+), 155 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/struts/blob/bb19a620/core/src/main/java/com/opensymphony/xwork2/util/AbstractLocalizedTextProvider.java ---------------------------------------------------------------------- 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 a2578bc..8c377c1 100644 --- a/core/src/main/java/com/opensymphony/xwork2/util/AbstractLocalizedTextProvider.java +++ b/core/src/main/java/com/opensymphony/xwork2/util/AbstractLocalizedTextProvider.java @@ -20,11 +20,12 @@ import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; -public abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { +abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { private static final Logger LOG = LogManager.getLogger(AbstractLocalizedTextProvider.class); @@ -302,6 +303,26 @@ public abstract class AbstractLocalizedTextProvider implements LocalizedTextProv } /** + * Determines if we found the text in the bundles. + * + * @param result the result so far + * @return <tt>true</tt> if we could <b>not</b> find the text, <tt>false</tt> if the text was found (=success). + */ + protected boolean unableToFindTextForKey(GetDefaultMessageReturnArg result) { + if (result == null || result.message == null) { + return true; + } + + // did we find it in the bundle, then no problem? + if (result.foundInBundle) { + return false; + } + + // not found in bundle + return true; + } + + /** * 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. @@ -313,6 +334,128 @@ public abstract class AbstractLocalizedTextProvider implements LocalizedTextProv return prefix + aBundleName + "_" + locale.toString(); } + /** + * @return the default message. + */ + protected GetDefaultMessageReturnArg getDefaultMessage(String key, Locale locale, ValueStack valueStack, Object[] args, + String defaultMessage) { + GetDefaultMessageReturnArg result = null; + boolean found = true; + + if (key != null) { + String message = findDefaultText(key, locale); + + if (message == null) { + message = defaultMessage; + found = false; // not found in bundles + } + + // defaultMessage may be null + if (message != null) { + MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale); + + String msg = formatWithNullDetection(mf, args); + result = new GetDefaultMessageReturnArg(msg, found); + } + } + + return result; + } + + /** + * @return the message from the named resource bundle. + */ + protected String getMessage(String bundleName, Locale locale, String key, ValueStack valueStack, Object[] args) { + ResourceBundle bundle = findResourceBundle(bundleName, locale); + if (bundle == null) { + return null; + } + if (valueStack != null) + reloadBundles(valueStack.getContext()); + try { + 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) { + if (devMode) { + LOG.warn("Missing key [{}] in bundle [{}]!", key, bundleName); + } else { + LOG.debug("Missing key [{}] in bundle [{}]!", key, bundleName); + } + return null; + } + } + + /** + * Traverse up class hierarchy looking for message. Looks at class, then implemented interface, + * before going up hierarchy. + * + * @return the message + */ + protected String findMessage(Class clazz, String key, String indexedKey, Locale locale, Object[] args, Set<String> checked, + ValueStack valueStack) { + if (checked == null) { + checked = new TreeSet<>(); + } else if (checked.contains(clazz.getName())) { + return null; + } + + // look in properties of this class + String msg = getMessage(clazz.getName(), locale, key, valueStack, args); + + if (msg != null) { + return msg; + } + + if (indexedKey != null) { + msg = getMessage(clazz.getName(), locale, indexedKey, valueStack, args); + + if (msg != null) { + return msg; + } + } + + // look in properties of implemented interfaces + Class[] interfaces = clazz.getInterfaces(); + + for (Class anInterface : interfaces) { + msg = getMessage(anInterface.getName(), locale, key, valueStack, args); + + if (msg != null) { + return msg; + } + + if (indexedKey != null) { + msg = getMessage(anInterface.getName(), locale, indexedKey, valueStack, args); + + if (msg != null) { + return msg; + } + } + } + + // traverse up hierarchy + if (clazz.isInterface()) { + interfaces = clazz.getInterfaces(); + + for (Class anInterface : interfaces) { + msg = findMessage(anInterface, key, indexedKey, locale, args, checked, valueStack); + + if (msg != null) { + return msg; + } + } + } else { + if (!clazz.equals(Object.class) && !clazz.isPrimitive()) { + return findMessage(clazz.getSuperclass(), key, indexedKey, locale, args, checked, valueStack); + } + } + + return null; + } + static class MessageFormatKey { String pattern; Locale locale; http://git-wip-us.apache.org/repos/asf/struts/blob/bb19a620/core/src/main/java/com/opensymphony/xwork2/util/DefaultLocalizedTextProvider.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/com/opensymphony/xwork2/util/DefaultLocalizedTextProvider.java b/core/src/main/java/com/opensymphony/xwork2/util/DefaultLocalizedTextProvider.java new file mode 100644 index 0000000..acd3943 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/util/DefaultLocalizedTextProvider.java @@ -0,0 +1,310 @@ +/* + * $Id$ + * + * 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; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.ModelDriven; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.text.MessageFormat; +import java.util.Locale; +import java.util.MissingResourceException; +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. + */ +public class DefaultLocalizedTextProvider extends AbstractLocalizedTextProvider { + + private static final Logger LOG = LogManager.getLogger(DefaultLocalizedTextProvider.class); + + public DefaultLocalizedTextProvider() { + addDefaultResourceBundle(XWORK_MESSAGES_BUNDLE); + addDefaultResourceBundle(STRUTS_MESSAGES_BUNDLE); + } + + /** + * Calls {@link #findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args)} + * with aTextName as the default message. + * + * @param aClass class name + * @param aTextName text name + * @param locale the locale + * @return the localized text, or null if none can be found and no defaultMessage is provided + * @see #findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args) + */ + @Override + public String findText(Class aClass, String aTextName, Locale locale) { + return findText(aClass, aTextName, locale, aTextName, new Object[0]); + } + + /** + * <p> + * Finds a localized text message for the given key, aTextName. Both the key and the message + * itself is evaluated as required. The following algorithm is used to find the requested + * message: + * </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> + * </ol> + * + * <p> + * When looking for the message, if the key indexes a collection (e.g. user.phone[0]) and a + * message for that specific key cannot be found, the general form will also be looked up + * (i.e. user.phone[*]). + * </p> + * + * <p> + * If a message is found, it will also be interpolated. Anything within <code>${...}</code> + * will be treated as an OGNL expression and evaluated as such. + * </p> + * + * @param aClass the class whose name to use as the start point for the search + * @param aTextName the key to find the text message for + * @param locale the locale the message should be for + * @param defaultMessage the message to be returned if no text message can be found in any + * resource bundle + * @param args arguments + * resource bundle + * @return the localized text, or null if none can be found and no defaultMessage is provided + */ + @Override + public String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args) { + ValueStack valueStack = ActionContext.getContext().getValueStack(); + return findText(aClass, aTextName, locale, defaultMessage, args, valueStack); + + } + + /** + * <p> + * Finds a localized text message for the given key, aTextName. Both the key and the message + * itself is evaluated as required. The following algorithm is used to find the requested + * message: + * </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> + * </ol> + * + * <p> + * When looking for the message, if the key indexes a collection (e.g. user.phone[0]) and a + * message for that specific key cannot be found, the general form will also be looked up + * (i.e. user.phone[*]). + * </p> + * + * <p> + * If a message is found, it will also be interpolated. Anything within <code>${...}</code> + * will be treated as an OGNL expression and evaluated as such. + * </p> + * + * <p> + * If a message is <b>not</b> found a WARN log will be logged. + * </p> + * + * @param aClass the class whose name to use as the start point for the search + * @param aTextName the key to find the text message for + * @param locale the locale the message should be for + * @param defaultMessage the message to be returned if no text message can be found in any + * resource bundle + * @param args arguments + * @param valueStack the value stack to use to evaluate expressions instead of the + * one in the ActionContext ThreadLocal + * @return the localized text, or null if none can be found and no defaultMessage is provided + */ + @Override + public String findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args, ValueStack valueStack) { + String indexedTextName = null; + if (aTextName == null) { + LOG.warn("Trying to find text with null key!"); + aTextName = ""; + } + // calculate indexedTextName (collection[*]) if applicable + if (aTextName.contains("[")) { + int i = -1; + + indexedTextName = aTextName; + + while ((i = indexedTextName.indexOf("[", i + 1)) != -1) { + int j = indexedTextName.indexOf("]", i); + String a = indexedTextName.substring(0, i); + String b = indexedTextName.substring(j); + indexedTextName = a + "[*" + b; + } + } + + // 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); + } + + // could we find the text, if not log a warn + if (unableToFindTextForKey(result) && LOG.isDebugEnabled()) { + String warn = "Unable to find text for key '" + aTextName + "' "; + if (indexedTextName != null) { + warn += " or indexed key '" + indexedTextName + "' "; + } + warn += "in class '" + aClass.getName() + "' and locale '" + locale + "'"; + LOG.debug(warn); + } + + return result != null ? result.message : null; + } + + /** + * <p> + * Finds a localized text message for the given key, aTextName, in the specified resource bundle + * with aTextName as the default message. + * </p> + * + * <p> + * If a message is found, it will also be interpolated. Anything within <code>${...}</code> + * will be treated as an OGNL expression and evaluated as such. + * </p> + * + * @param bundle a resource bundle name + * @param aTextName text name + * @param locale the locale + * @return the localized text, or null if none can be found and no defaultMessage is provided + * @see #findText(ResourceBundle, String, Locale, String, Object[]) + */ + @Override + public String findText(ResourceBundle bundle, String aTextName, Locale locale) { + return findText(bundle, aTextName, locale, aTextName, new Object[0]); + } + + /** + * <p> + * Finds a localized text message for the given key, aTextName, in the specified resource + * bundle. + * </p> + * + * <p> + * If a message is found, it will also be interpolated. Anything within <code>${...}</code> + * will be treated as an OGNL expression and evaluated as such. + * </p> + * + * <p> + * If a message is <b>not</b> found a WARN log will be logged. + * </p> + * + * @param bundle the bundle + * @param aTextName the key + * @param locale the locale + * @param defaultMessage the default message to use if no message was found in the bundle + * @param args arguments for the message formatter. + * @return the localized text, or null if none can be found and no defaultMessage is provided + */ + @Override + public String findText(ResourceBundle bundle, String aTextName, Locale locale, String defaultMessage, Object[] args) { + ValueStack valueStack = ActionContext.getContext().getValueStack(); + return findText(bundle, aTextName, locale, defaultMessage, args, valueStack); + } + + /** + * <p> + * Finds a localized text message for the given key, aTextName, in the specified resource + * bundle. + * </p> + * + * <p> + * If a message is found, it will also be interpolated. Anything within <code>${...}</code> + * will be treated as an OGNL expression and evaluated as such. + * </p> + * + * <p> + * If a message is <b>not</b> found a WARN log will be logged. + * </p> + * + * @param bundle the bundle + * @param aTextName the key + * @param locale the locale + * @param defaultMessage the default message to use if no message was found in the bundle + * @param args arguments for the message formatter. + * @param valueStack the OGNL value stack. + * @return the localized text, or null if none can be found and no defaultMessage is provided + */ + @Override + public String findText(ResourceBundle bundle, String aTextName, Locale locale, String defaultMessage, Object[] args, + ValueStack valueStack) { + try { + reloadBundles(valueStack.getContext()); + + String message = TextParseUtil.translateVariables(bundle.getString(aTextName), valueStack); + MessageFormat mf = buildMessageFormat(message, locale); + + return formatWithNullDetection(mf, args); + } catch (MissingResourceException ex) { + if (devMode) { + LOG.warn("Missing key [{}] in bundle [{}]!", aTextName, bundle); + } else { + LOG.debug("Missing key [{}] in bundle [{}]!", aTextName, bundle); + } + } + + GetDefaultMessageReturnArg result = getDefaultMessage(aTextName, locale, valueStack, args, defaultMessage); + if (unableToFindTextForKey(result)) { + LOG.warn("Unable to find text for key '{}' in ResourceBundles for locale '{}'", aTextName, locale); + } + return result != null ? result.message : null; + } + +} http://git-wip-us.apache.org/repos/asf/struts/blob/bb19a620/core/src/main/java/com/opensymphony/xwork2/util/StrutsLocalizedTextProvider.java ---------------------------------------------------------------------- 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 3d03f9a..9552869 100644 --- a/core/src/main/java/com/opensymphony/xwork2/util/StrutsLocalizedTextProvider.java +++ b/core/src/main/java/com/opensymphony/xwork2/util/StrutsLocalizedTextProvider.java @@ -37,9 +37,7 @@ import java.text.MessageFormat; 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, @@ -89,7 +87,6 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { * @param localeStr The locale String to parse. * @param defaultLocale The locale to use if localeStr is <tt>null</tt>. * @return requested Locale - * * @deprecated please use {@link org.apache.commons.lang3.LocaleUtils#toLocale(String)} */ @Deprecated @@ -130,10 +127,9 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { * Calls {@link #findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args)} * with aTextName as the default message. * - * @param aClass class name - * @param aTextName text name - * @param locale the locale - * + * @param aClass class name + * @param aTextName text name + * @param locale the locale * @return the localized text, or null if none can be found and no defaultMessage is provided * @see #findText(Class aClass, String aTextName, Locale locale, String defaultMessage, Object[] args) */ @@ -251,7 +247,7 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { ValueStack valueStack) { String indexedTextName = null; if (aTextName == null) { - LOG.warn("Trying to find text with null key!"); + LOG.warn("Trying to find text with null key!"); aTextName = ""; } // calculate indexedTextName (collection[*]) if applicable @@ -395,26 +391,6 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { } /** - * Determines if we found the text in the bundles. - * - * @param result the result so far - * @return <tt>true</tt> if we could <b>not</b> find the text, <tt>false</tt> if the text was found (=success). - */ - private boolean unableToFindTextForKey(GetDefaultMessageReturnArg result) { - if (result == null || result.message == null) { - return true; - } - - // did we find it in the bundle, then no problem? - if (result.foundInBundle) { - return false; - } - - // not found in bundle - return true; - } - - /** * <p> * Finds a localized text message for the given key, aTextName, in the specified resource bundle * with aTextName as the default message. @@ -425,10 +401,9 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { * will be treated as an OGNL expression and evaluated as such. * </p> * - * @param bundle a resource bundle name - * @param aTextName text name - * @param locale the locale - * + * @param bundle a resource bundle name + * @param aTextName text name + * @param locale the locale * @return the localized text, or null if none can be found and no defaultMessage is provided * @see #findText(java.util.ResourceBundle, String, java.util.Locale, String, Object[]) */ @@ -514,128 +489,6 @@ public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { } /** - * @return the default message. - */ - private GetDefaultMessageReturnArg getDefaultMessage(String key, Locale locale, ValueStack valueStack, Object[] args, - String defaultMessage) { - GetDefaultMessageReturnArg result = null; - boolean found = true; - - if (key != null) { - String message = findDefaultText(key, locale); - - if (message == null) { - message = defaultMessage; - found = false; // not found in bundles - } - - // defaultMessage may be null - if (message != null) { - MessageFormat mf = buildMessageFormat(TextParseUtil.translateVariables(message, valueStack), locale); - - String msg = formatWithNullDetection(mf, args); - result = new GetDefaultMessageReturnArg(msg, found); - } - } - - return result; - } - - /** - * @return the message from the named resource bundle. - */ - private String getMessage(String bundleName, Locale locale, String key, ValueStack valueStack, Object[] args) { - ResourceBundle bundle = findResourceBundle(bundleName, locale); - if (bundle == null) { - return null; - } - if (valueStack != null) - reloadBundles(valueStack.getContext()); - try { - 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) { - if (devMode) { - LOG.warn("Missing key [{}] in bundle [{}]!", key, bundleName); - } else { - LOG.debug("Missing key [{}] in bundle [{}]!", key, bundleName); - } - return null; - } - } - - /** - * Traverse up class hierarchy looking for message. Looks at class, then implemented interface, - * before going up hierarchy. - * - * @return the message - */ - private String findMessage(Class clazz, String key, String indexedKey, Locale locale, Object[] args, Set<String> checked, - ValueStack valueStack) { - if (checked == null) { - checked = new TreeSet<>(); - } else if (checked.contains(clazz.getName())) { - return null; - } - - // look in properties of this class - String msg = getMessage(clazz.getName(), locale, key, valueStack, args); - - if (msg != null) { - return msg; - } - - if (indexedKey != null) { - msg = getMessage(clazz.getName(), locale, indexedKey, valueStack, args); - - if (msg != null) { - return msg; - } - } - - // look in properties of implemented interfaces - Class[] interfaces = clazz.getInterfaces(); - - for (Class anInterface : interfaces) { - msg = getMessage(anInterface.getName(), locale, key, valueStack, args); - - if (msg != null) { - return msg; - } - - if (indexedKey != null) { - msg = getMessage(anInterface.getName(), locale, indexedKey, valueStack, args); - - if (msg != null) { - return msg; - } - } - } - - // traverse up hierarchy - if (clazz.isInterface()) { - interfaces = clazz.getInterfaces(); - - for (Class anInterface : interfaces) { - msg = findMessage(anInterface, key, indexedKey, locale, args, checked, valueStack); - - if (msg != null) { - return msg; - } - } - } else { - if (!clazz.equals(Object.class) && !clazz.isPrimitive()) { - return findMessage(clazz.getSuperclass(), key, indexedKey, locale, args, checked, valueStack); - } - } - - return null; - } - - /** * Clears all the internal lists. * * @deprecated used only in tests