This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-5540-localized-text-provider-caching in repository https://gitbox.apache.org/repos/asf/struts.git
commit abd8a55e8824432804b188bf76958c332bff6d12 Author: Lukasz Lenart <[email protected]> AuthorDate: Thu Jul 23 13:07:59 2026 +0200 WW-5540 refactor(core): split raw message resolution from formatting Add getRawMessage/formatMessage and a raw twin findMessageRaw. Re-express getMessage via formatMessage and make findMessage delegate to findMessageRaw + formatMessage; deprecate both as legacy extension points superseded by the raw-resolution path. The deprecated findMessage triggers the bundle reload on entry, preserving the reload side effect the old getMessage-per-probe walk provided. Groundwork for the traversal caches. Co-Authored-By: Claude Opus 4.8 <[email protected]> --- .../text/AbstractLocalizedTextProvider.java | 98 ++++++++++++++++------ 1 file changed, 71 insertions(+), 27 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/text/AbstractLocalizedTextProvider.java b/core/src/main/java/org/apache/struts2/text/AbstractLocalizedTextProvider.java index 6185552c1..222d234a6 100644 --- a/core/src/main/java/org/apache/struts2/text/AbstractLocalizedTextProvider.java +++ b/core/src/main/java/org/apache/struts2/text/AbstractLocalizedTextProvider.java @@ -507,9 +507,45 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { return result; } + /** + * Resolves the raw (untranslated, unformatted) message pattern for a key within a single bundle. + * Returns {@code null} when the bundle or key is absent. This is the cacheable unit relied upon by + * the hierarchy-resolution caches; translation and formatting are applied separately by + * {@link #formatMessage(String, Locale, ValueStack, Object[])}. + */ + private String getRawMessage(String bundleName, Locale locale, String key) { + ResourceBundle bundle = findResourceBundle(bundleName, locale); + if (bundle == null) { + return null; + } + try { + return bundle.getString(key); + } catch (MissingResourceException e) { + LOG.debug("Missing key [{}] in bundle [{}]!", key, bundleName); + return null; + } + } + + /** + * Applies value stack variable translation (when a stack is available) and {@link MessageFormat} + * argument substitution to a raw message pattern. Mirrors the rendering previously performed inline + * by {@link #getMessage(String, Locale, String, ValueStack, Object[])}. + */ + protected String formatMessage(String rawPattern, Locale locale, ValueStack valueStack, Object[] args) { + String message = (valueStack != null) + ? TextParseUtil.translateVariables(rawPattern, valueStack) + : rawPattern; + MessageFormat mf = buildMessageFormat(message, locale); + return formatWithNullDetection(mf, args); + } + /** * @return the message from the named resource bundle. + * @deprecated since 7.3.0 — superseded by the internal raw-resolution + caching path + * ({@link #formatMessage(String, Locale, ValueStack, Object[])} over a raw lookup). Retained for + * backward compatibility with descendant classes. */ + @Deprecated protected String getMessage(String bundleName, Locale locale, String key, ValueStack valueStack, Object[] args) { ResourceBundle bundle = findResourceBundle(bundleName, locale); if (bundle == null) { @@ -519,12 +555,8 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { 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); + String rawPattern = bundle.getString(key); + return formatMessage(rawPattern, locale, valueStack, args); } catch (MissingResourceException e) { LOG.debug("Missing key [{}] in bundle [{}]!", key, bundleName); return null; @@ -532,13 +564,11 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { } /** - * Traverse up class hierarchy looking for message. Looks at class, then implemented interface, - * before going up hierarchy. - * - * @return the message + * Raw-pattern twin of {@link #findMessage}. Walks class, implemented interfaces, then up the + * hierarchy, returning the first raw message pattern found (via {@link #getRawMessage}) without + * translation or formatting. Used by the cached class-hierarchy resolver. */ - protected String findMessage(Class<?> clazz, String key, String indexedKey, Locale locale, Object[] args, Set<String> checked, - ValueStack valueStack) { + private String findMessageRaw(Class<?> clazz, String key, String indexedKey, Locale locale, Set<String> checked) { if (checked == null) { checked = new TreeSet<>(); } else if (checked.contains(clazz.getName())) { @@ -546,15 +576,12 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { } // look in properties of this class - String msg = getMessage(clazz.getName(), locale, key, valueStack, args); - + String msg = getRawMessage(clazz.getName(), locale, key); if (msg != null) { return msg; } - if (indexedKey != null) { - msg = getMessage(clazz.getName(), locale, indexedKey, valueStack, args); - + msg = getRawMessage(clazz.getName(), locale, indexedKey); if (msg != null) { return msg; } @@ -562,17 +589,13 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { // look in properties of implemented interfaces Class<?>[] interfaces = clazz.getInterfaces(); - for (Class<?> anInterface : interfaces) { - msg = getMessage(anInterface.getName(), locale, key, valueStack, args); - + msg = getRawMessage(anInterface.getName(), locale, key); if (msg != null) { return msg; } - if (indexedKey != null) { - msg = getMessage(anInterface.getName(), locale, indexedKey, valueStack, args); - + msg = getRawMessage(anInterface.getName(), locale, indexedKey); if (msg != null) { return msg; } @@ -582,23 +605,44 @@ abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider { // traverse up hierarchy if (clazz.isInterface()) { interfaces = clazz.getInterfaces(); - for (Class<?> anInterface : interfaces) { - msg = findMessage(anInterface, key, indexedKey, locale, args, checked, valueStack); - + msg = findMessageRaw(anInterface, key, indexedKey, locale, checked); if (msg != null) { return msg; } } } else { if (!clazz.equals(Object.class) && !clazz.isPrimitive()) { - return findMessage(clazz.getSuperclass(), key, indexedKey, locale, args, checked, valueStack); + return findMessageRaw(clazz.getSuperclass(), key, indexedKey, locale, checked); } } return null; } + /** + * Traverse up class hierarchy looking for message. Looks at class, then implemented interface, + * before going up hierarchy. + * + * @return the message + * @deprecated since 7.3.0 — superseded by the internal raw-resolution + caching path + * ({@link #findMessageRaw} + {@link #formatMessage(String, Locale, ValueStack, Object[])}). Retained + * for backward compatibility with descendant classes. Note: unlike the pre-7.3.0 implementation, a + * candidate whose formatted value is the literal {@code "null"} no longer causes the search to + * continue deeper in the same hierarchy; this affects only the pathological case of the same key + * redefined at multiple hierarchy levels with the shallow value formatting to {@code "null"}. + * The bundle-reload check is now triggered once on entry (when reload mode is enabled) rather than + * lazily per bundle probe, preserving the reload side effect that the previous getMessage-per-probe + * walk provided. + */ + @Deprecated + protected String findMessage(Class<?> clazz, String key, String indexedKey, Locale locale, Object[] args, Set<String> checked, + ValueStack valueStack) { + reloadBundles(valueStack != null ? valueStack.getContext() : null); + String rawPattern = findMessageRaw(clazz, key, indexedKey, locale, checked); + return rawPattern != null ? formatMessage(rawPattern, locale, valueStack, args) : null; + } + protected String extractIndexedName(String textKey) { String indexedTextName = null; // calculate indexedTextName (collection[*]) if applicable
