| /* |
| * 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.ActionInvocation; |
| import com.opensymphony.xwork2.ModelDriven; |
| import com.opensymphony.xwork2.conversion.impl.XWorkConverter; |
| import com.opensymphony.xwork2.util.reflection.ReflectionProviderFactory; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| |
| import java.beans.PropertyDescriptor; |
| 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. |
| */ |
| public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider { |
| |
| private static final Logger LOG = LogManager.getLogger(StrutsLocalizedTextProvider.class); |
| |
| /** |
| * Clears the internal list of resource bundles. |
| * |
| * @deprecated used only in tests |
| */ |
| @Deprecated |
| public static void clearDefaultResourceBundles() { |
| // no-op |
| } |
| |
| public StrutsLocalizedTextProvider() { |
| addDefaultResourceBundle(XWORK_MESSAGES_BUNDLE); |
| addDefaultResourceBundle(STRUTS_MESSAGES_BUNDLE); |
| } |
| |
| /** |
| * Builds a {@link java.util.Locale} from a String of the form en_US_foo into a Locale |
| * with language "en", country "US" and variant "foo". This will parse the output of |
| * {@link java.util.Locale#toString()}. |
| * |
| * @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 |
| public static Locale localeFromString(String localeStr, Locale defaultLocale) { |
| if ((localeStr == null) || (localeStr.trim().length() == 0) || ("_".equals(localeStr))) { |
| if (defaultLocale != null) { |
| return defaultLocale; |
| } |
| return Locale.getDefault(); |
| } |
| |
| int index = localeStr.indexOf('_'); |
| if (index < 0) { |
| return new Locale(localeStr); |
| } |
| |
| String language = localeStr.substring(0, index); |
| if (index == localeStr.length()) { |
| return new Locale(language); |
| } |
| |
| localeStr = localeStr.substring(index + 1); |
| index = localeStr.indexOf('_'); |
| if (index < 0) { |
| return new Locale(language, localeStr); |
| } |
| |
| String country = localeStr.substring(0, index); |
| if (index == localeStr.length()) { |
| return new Locale(language, country); |
| } |
| |
| localeStr = localeStr.substring(index + 1); |
| return new Locale(language, country, localeStr); |
| } |
| |
| /** |
| * 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>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> |
| * <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 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 |
| * (Note: the lookup is not repeated again if {@link #searchDefaultBundlesFirst} was <code>true</code>).</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>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> |
| * <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 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 |
| * (Note: the lookup is not repeated again if {@link #searchDefaultBundlesFirst} was <code>true</code>).</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 DEBUG level log warning 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; |
| } |
| } |
| |
| // 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); |
| |
| if (msg != null) { |
| return msg; |
| } |
| |
| if (ModelDriven.class.isAssignableFrom(aClass)) { |
| ActionContext context = ActionContext.getContext(); |
| // search up model's class hierarchy |
| ActionInvocation actionInvocation = context.getActionInvocation(); |
| |
| // ActionInvocation may be null if we're being run from a Sitemesh filter, so we won't get model texts if this is null |
| if (actionInvocation != null) { |
| Object action = actionInvocation.getAction(); |
| if (action instanceof ModelDriven) { |
| Object model = ((ModelDriven) action).getModel(); |
| if (model != null) { |
| msg = findMessage(model.getClass(), aTextName, indexedTextName, locale, args, null, valueStack); |
| if (msg != null) { |
| return msg; |
| } |
| } |
| } |
| } |
| } |
| |
| // nothing still? alright, search the package hierarchy now |
| for (Class clazz = aClass; |
| (clazz != null) && !clazz.equals(Object.class); |
| clazz = clazz.getSuperclass()) { |
| |
| String basePackageName = clazz.getName(); |
| while (basePackageName.lastIndexOf('.') != -1) { |
| basePackageName = basePackageName.substring(0, basePackageName.lastIndexOf('.')); |
| String packageName = basePackageName + ".package"; |
| msg = getMessage(packageName, locale, aTextName, valueStack, args); |
| |
| if (msg != null) { |
| return msg; |
| } |
| |
| if (indexedTextName != null) { |
| msg = getMessage(packageName, locale, indexedTextName, valueStack, args); |
| |
| if (msg != null) { |
| return msg; |
| } |
| } |
| } |
| } |
| |
| // see if it's a child property |
| int idx = aTextName.indexOf('.'); |
| |
| if (idx != -1) { |
| String newKey = null; |
| String prop = null; |
| |
| if (aTextName.startsWith(XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX)) { |
| idx = aTextName.indexOf('.', XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX.length()); |
| |
| if (idx != -1) { |
| prop = aTextName.substring(XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX.length(), idx); |
| newKey = XWorkConverter.CONVERSION_ERROR_PROPERTY_PREFIX + aTextName.substring(idx + 1); |
| } |
| } else { |
| prop = aTextName.substring(0, idx); |
| newKey = aTextName.substring(idx + 1); |
| } |
| |
| if (prop != null) { |
| Object obj = valueStack.findValue(prop); |
| try { |
| Object actionObj = ReflectionProviderFactory.getInstance().getRealTarget(prop, valueStack.getContext(), valueStack.getRoot()); |
| if (actionObj != null) { |
| PropertyDescriptor propertyDescriptor = ReflectionProviderFactory.getInstance().getPropertyDescriptor(actionObj.getClass(), prop); |
| |
| if (propertyDescriptor != null) { |
| Class clazz = propertyDescriptor.getPropertyType(); |
| |
| if (clazz != null) { |
| if (obj != null) { |
| valueStack.push(obj); |
| } |
| msg = findText(clazz, newKey, locale, null, args); |
| if (obj != null) { |
| valueStack.pop(); |
| } |
| if (msg != null) { |
| return msg; |
| } |
| } |
| } |
| } |
| } catch (Exception e) { |
| LOG.debug("unable to find property {}", prop, e); |
| } |
| } |
| } |
| |
| // get default |
| // 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 |
| 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(java.util.ResourceBundle, String, java.util.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); |
| } |
| |
| } |