This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch WW-5668-serialversionuid-compat
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 85a8c43185b73888ab1bcc736667407fa281955d
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Aug 1 10:18:19 2026 +0200

    WW-5668 Keep the localized-text providers deserializable across a version 
upgrade
    
    Follow-up to #1821. Pins serialVersionUID to the value implicitly computed 
for the
    Struts 7.2.1 class shape instead of 1L, so a session serialized by a 7.2.1 
node still
    loads on a 7.3.0 one during a rolling upgrade rather than failing with
    InvalidClassException.
    
    Such a stream carries no value for the new cache settings, and field 
initialisers do
    not run during deserialization, so readObject restores their defaults 
before rebuilding
    the caches - without that guard it failed with a NullPointerException.
    
    Co-Authored-By: Claude Opus 5 <[email protected]>
---
 .../text/AbstractLocalizedTextProvider.java        | 17 ++++++++++--
 .../struts2/text/GlobalLocalizedTextProvider.java  |  3 +-
 .../struts2/text/StrutsLocalizedTextProvider.java  |  3 +-
 .../text/StrutsLocalizedTextProviderTest.java      | 32 ++++++++++++++++++++++
 4 files changed, 51 insertions(+), 4 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 2aaebaee4..6d4a88ea8 100644
--- 
a/core/src/main/java/org/apache/struts2/text/AbstractLocalizedTextProvider.java
+++ 
b/core/src/main/java/org/apache/struts2/text/AbstractLocalizedTextProvider.java
@@ -50,7 +50,10 @@ import java.util.concurrent.CopyOnWriteArrayList;
 
 abstract class AbstractLocalizedTextProvider implements LocalizedTextProvider {
 
-    private static final long serialVersionUID = 1L;
+    // Pinned to the value implicitly computed for the Struts 7.2.1 class 
shape, so sessions serialized by
+    // an older node still deserialize here during a rolling upgrade. The 
caches that became transient are
+    // simply discarded from such a stream and rebuilt by readObject.
+    private static final long serialVersionUID = -4455624669971032217L;
 
     private static final Logger LOG = 
LogManager.getLogger(AbstractLocalizedTextProvider.class);
 
@@ -77,8 +80,10 @@ abstract class AbstractLocalizedTextProvider implements 
LocalizedTextProvider {
     // transient + reinitialised in readObject: a bare Object is not 
Serializable.
     private transient Object bundlesMapLock = new Object();
 
+    private static final int DEFAULT_I18N_CACHE_MAX_SIZE = 10000;
+
     private volatile CacheType i18nCacheType = CacheType.WTLFU;
-    private volatile int i18nCacheMaxSize = 10000;
+    private volatile int i18nCacheMaxSize = DEFAULT_I18N_CACHE_MAX_SIZE;
 
     private <K, V> OgnlCache<K, V> buildI18nCache() {
         return new DefaultOgnlCacheFactory<K, V>(i18nCacheMaxSize, 
i18nCacheType).buildOgnlCache();
@@ -473,6 +478,14 @@ abstract class AbstractLocalizedTextProvider implements 
LocalizedTextProvider {
     private void readObject(java.io.ObjectInputStream in) throws 
java.io.IOException, ClassNotFoundException {
         in.defaultReadObject();
         bundlesMapLock = new Object();
+        // Field initialisers do not run during deserialization, so a stream 
written before these settings
+        // existed (an older node in a rolling upgrade) leaves them at null/0. 
Restore the defaults.
+        if (i18nCacheType == null) {
+            i18nCacheType = CacheType.WTLFU;
+        }
+        if (i18nCacheMaxSize <= 0) {
+            i18nCacheMaxSize = DEFAULT_I18N_CACHE_MAX_SIZE;
+        }
         rebuildI18nCaches();
     }
 
diff --git 
a/core/src/main/java/org/apache/struts2/text/GlobalLocalizedTextProvider.java 
b/core/src/main/java/org/apache/struts2/text/GlobalLocalizedTextProvider.java
index 72383de5e..ad252f66c 100644
--- 
a/core/src/main/java/org/apache/struts2/text/GlobalLocalizedTextProvider.java
+++ 
b/core/src/main/java/org/apache/struts2/text/GlobalLocalizedTextProvider.java
@@ -34,7 +34,8 @@ import java.util.ResourceBundle;
  */
 public class GlobalLocalizedTextProvider extends AbstractLocalizedTextProvider 
{
 
-    private static final long serialVersionUID = 1L;
+    // Pinned to the value implicitly computed for the Struts 7.2.1 class 
shape, see AbstractLocalizedTextProvider.
+    private static final long serialVersionUID = 3777960740495792359L;
 
     private static final Logger LOG = 
LogManager.getLogger(GlobalLocalizedTextProvider.class);
 
diff --git 
a/core/src/main/java/org/apache/struts2/text/StrutsLocalizedTextProvider.java 
b/core/src/main/java/org/apache/struts2/text/StrutsLocalizedTextProvider.java
index 1a0976889..5ec88bd65 100644
--- 
a/core/src/main/java/org/apache/struts2/text/StrutsLocalizedTextProvider.java
+++ 
b/core/src/main/java/org/apache/struts2/text/StrutsLocalizedTextProvider.java
@@ -38,7 +38,8 @@ import java.util.ResourceBundle;
  */
 public class StrutsLocalizedTextProvider extends AbstractLocalizedTextProvider 
{
 
-    private static final long serialVersionUID = 1L;
+    // Pinned to the value implicitly computed for the Struts 7.2.1 class 
shape, see AbstractLocalizedTextProvider.
+    private static final long serialVersionUID = 1939638936989370989L;
 
     private static final Logger LOG = 
LogManager.getLogger(StrutsLocalizedTextProvider.class);
     private transient ReflectionProvider reflectionProvider;
diff --git 
a/core/src/test/java/org/apache/struts2/text/StrutsLocalizedTextProviderTest.java
 
b/core/src/test/java/org/apache/struts2/text/StrutsLocalizedTextProviderTest.java
index 076403dc2..dd836c115 100644
--- 
a/core/src/test/java/org/apache/struts2/text/StrutsLocalizedTextProviderTest.java
+++ 
b/core/src/test/java/org/apache/struts2/text/StrutsLocalizedTextProviderTest.java
@@ -812,6 +812,38 @@ public class StrutsLocalizedTextProviderTest extends 
XWorkTestCase {
         assertEquals("Static cached value", result);
     }
 
+    /**
+     * A stream written before the i18n cache settings existed carries no 
value for them, and field
+     * initialisers do not run during deserialization, so they arrive as 
null/0. The provider must still
+     * come back usable rather than failing while rebuilding its caches.
+     */
+    public void testProviderIsUsableAfterDeserializingLegacyStream() throws 
Exception {
+        TestStrutsLocalizedTextProvider provider = new 
TestStrutsLocalizedTextProvider();
+        ValueStack valueStack = ActionContext.getContext().getValueStack();
+        provider.findText(CacheFixture.class, "cache.static", Locale.ENGLISH, 
null, null, valueStack);
+
+        // Simulate the absent-field state an older stream produces.
+        java.lang.reflect.Field cacheType = 
AbstractLocalizedTextProvider.class.getDeclaredField("i18nCacheType");
+        cacheType.setAccessible(true);
+        cacheType.set(provider, null);
+        java.lang.reflect.Field maxSize = 
AbstractLocalizedTextProvider.class.getDeclaredField("i18nCacheMaxSize");
+        maxSize.setAccessible(true);
+        maxSize.setInt(provider, 0);
+
+        java.io.ByteArrayOutputStream baos = new 
java.io.ByteArrayOutputStream();
+        try (java.io.ObjectOutputStream oos = new 
java.io.ObjectOutputStream(baos)) {
+            oos.writeObject(provider);
+        }
+        Object restored;
+        try (java.io.ObjectInputStream ois = new java.io.ObjectInputStream(new 
java.io.ByteArrayInputStream(baos.toByteArray()))) {
+            restored = ois.readObject();
+        }
+
+        TestStrutsLocalizedTextProvider deserialized = 
(TestStrutsLocalizedTextProvider) restored;
+        String result = deserialized.findText(CacheFixture.class, 
"cache.static", Locale.ENGLISH, null, null, valueStack);
+        assertEquals("Static cached value", result);
+    }
+
     public void testCacheTypeSelectionKeepsProviderWorking() {
         TestStrutsLocalizedTextProvider provider = new 
TestStrutsLocalizedTextProvider();
         provider.setI18nCacheType("basic");

Reply via email to