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

Reply via email to