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

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts-examples.git


The following commit(s) were added to refs/heads/main by this push:
     new 3bbb0a9  WW-3452 feat(text-provider): expand wildcard entries in 
struts.custom.i18n.resources (#552)
3bbb0a9 is described below

commit 3bbb0a94a1d6bd921c80236c82962d6a4c74e7b6
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Oct 10 14:36:45 2026 +0200

    WW-3452 feat(text-provider): expand wildcard entries in 
struts.custom.i18n.resources (#552)
    
    Shows how an application can register every message bundle of a package
    with a "com.acme.messages.*" entry by overriding
    setCustomI18NResources in its own LocalizedTextProvider, without any
    framework change.
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 text-provider/pom.xml                              |  6 ++
 .../struts_example/MyLocalizedTextProvider.java    |  4 +-
 .../WildcardLocalizedTextProvider.java             | 94 ++++++++++++++++++++++
 .../struts_example/messages/common.properties      |  2 +
 .../struts_example/messages/common_pl.properties   |  1 +
 .../struts_example/messages/labels.properties      |  2 +
 text-provider/src/main/resources/struts.xml        |  5 +-
 .../src/main/webapp/WEB-INF/content/success.jsp    |  7 ++
 .../WildcardLocalizedTextProviderTest.java         | 71 ++++++++++++++++
 9 files changed, 188 insertions(+), 4 deletions(-)

diff --git a/text-provider/pom.xml b/text-provider/pom.xml
index 0513ea5..521a033 100644
--- a/text-provider/pom.xml
+++ b/text-provider/pom.xml
@@ -23,6 +23,12 @@
             <artifactId>struts2-convention-plugin</artifactId>
             <version>${struts2.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.struts</groupId>
+            <artifactId>struts2-junit-plugin</artifactId>
+            <version>${struts2.version}</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git 
a/text-provider/src/main/java/org/apache/struts_example/MyLocalizedTextProvider.java
 
b/text-provider/src/main/java/org/apache/struts_example/MyLocalizedTextProvider.java
index 05f3ed5..7dae0ba 100644
--- 
a/text-provider/src/main/java/org/apache/struts_example/MyLocalizedTextProvider.java
+++ 
b/text-provider/src/main/java/org/apache/struts_example/MyLocalizedTextProvider.java
@@ -1,13 +1,11 @@
 package org.apache.struts_example;
 
-import org.apache.struts2.text.StrutsLocalizedTextProvider;
-
 import java.io.File;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
 
-public class MyLocalizedTextProvider extends StrutsLocalizedTextProvider {
+public class MyLocalizedTextProvider extends WildcardLocalizedTextProvider {
 
     public MyLocalizedTextProvider() {
         super();
diff --git 
a/text-provider/src/main/java/org/apache/struts_example/WildcardLocalizedTextProvider.java
 
b/text-provider/src/main/java/org/apache/struts_example/WildcardLocalizedTextProvider.java
new file mode 100644
index 0000000..ed0eda8
--- /dev/null
+++ 
b/text-provider/src/main/java/org/apache/struts_example/WildcardLocalizedTextProvider.java
@@ -0,0 +1,94 @@
+/*
+ * 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 org.apache.struts_example;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts2.text.StrutsLocalizedTextProvider;
+import org.apache.struts2.util.finder.ResourceFinder;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Expands entries such as {@code com.acme.messages.*} in {@code 
struts.custom.i18n.resources}
+ * into every bundle placed directly in that package (sub-packages are not 
scanned).
+ */
+public class WildcardLocalizedTextProvider extends StrutsLocalizedTextProvider 
{
+
+    private static final Logger LOG = 
LogManager.getLogger(WildcardLocalizedTextProvider.class);
+
+    private static final String WILDCARD_SUFFIX = ".*";
+    private static final String PROPERTIES_EXTENSION = ".properties";
+
+    // No @Inject here: the container already calls this through the annotated 
parent method,
+    // annotating the override as well would inject it twice
+    @Override
+    public void setCustomI18NResources(String bundles) {
+        if (bundles == null || bundles.isEmpty()) {
+            return;
+        }
+        List<String> expanded = new ArrayList<>();
+        for (String name : bundles.split("[,\\s]+")) {
+            if (name.endsWith(WILDCARD_SUFFIX)) {
+                expanded.addAll(findBundlesInPackage(name.substring(0, 
name.length() - WILDCARD_SUFFIX.length())));
+            } else if (!name.isEmpty()) {
+                expanded.add(name);
+            }
+        }
+        super.setCustomI18NResources(String.join(",", expanded));
+    }
+
+    /**
+     * Bundles are returned sorted, so lookup order does not depend on how the 
container lists the classpath:
+     * a bundle registered later is searched first, so on a duplicated key the 
last name in sorted order wins.
+     */
+    private Set<String> findBundlesInPackage(String packageName) {
+        Set<String> baseNames = new TreeSet<>();
+        try {
+            for (String fileName : new 
ResourceFinder("").getResourcesMap(packageName.replace('.', '/')).keySet()) {
+                if (fileName.endsWith(PROPERTIES_EXTENSION)) {
+                    baseNames.add(fileName.substring(0, fileName.length() - 
PROPERTIES_EXTENSION.length()));
+                }
+            }
+        } catch (IOException e) {
+            LOG.error("Could not scan package [{}] for message bundles. 
Skipping", packageName, e);
+        }
+        Set<String> bundles = new TreeSet<>();
+        for (String baseName : baseNames) {
+            if (!isLocaleVariant(baseName, baseNames)) {
+                bundles.add(packageName + "." + baseName);
+            }
+        }
+        if (bundles.isEmpty()) {
+            LOG.warn("No message bundles found in package [{}], [{}{}] 
registers nothing", packageName, packageName, WILDCARD_SUFFIX);
+        } else {
+            LOG.debug("Expanded [{}{}] to bundles {}", packageName, 
WILDCARD_SUFFIX, bundles);
+        }
+        return bundles;
+    }
+
+    private static boolean isLocaleVariant(String name, Set<String> baseNames) 
{
+        return baseNames.stream().anyMatch(base -> name.startsWith(base + 
"_"));
+    }
+}
diff --git 
a/text-provider/src/main/resources/org/apache/struts_example/messages/common.properties
 
b/text-provider/src/main/resources/org/apache/struts_example/messages/common.properties
new file mode 100644
index 0000000..ac813dd
--- /dev/null
+++ 
b/text-provider/src/main/resources/org/apache/struts_example/messages/common.properties
@@ -0,0 +1,2 @@
+common.greeting=Hello from the common bundle
+shared.key=Defined in common
diff --git 
a/text-provider/src/main/resources/org/apache/struts_example/messages/common_pl.properties
 
b/text-provider/src/main/resources/org/apache/struts_example/messages/common_pl.properties
new file mode 100644
index 0000000..161fc5f
--- /dev/null
+++ 
b/text-provider/src/main/resources/org/apache/struts_example/messages/common_pl.properties
@@ -0,0 +1 @@
+common.greeting=Witaj z pakietu common
diff --git 
a/text-provider/src/main/resources/org/apache/struts_example/messages/labels.properties
 
b/text-provider/src/main/resources/org/apache/struts_example/messages/labels.properties
new file mode 100644
index 0000000..e206fa3
--- /dev/null
+++ 
b/text-provider/src/main/resources/org/apache/struts_example/messages/labels.properties
@@ -0,0 +1,2 @@
+labels.title=Loaded from the labels bundle via a wildcard
+shared.key=Defined in labels
diff --git a/text-provider/src/main/resources/struts.xml 
b/text-provider/src/main/resources/struts.xml
index df7ec47..404f74c 100644
--- a/text-provider/src/main/resources/struts.xml
+++ b/text-provider/src/main/resources/struts.xml
@@ -6,7 +6,10 @@
   <constant name="struts.devMode" value="true"/>
   <constant name="struts.convention.action.packages" 
value="org.apache.struts_example"/>
 
-  <constant name="struts.custom.i18n.resources" value="DefaultMessages"/>
+  <!--
+  The ".*" entry is expanded by WildcardLocalizedTextProvider, plain Struts 
does not support wildcards here
+  -->
+  <constant name="struts.custom.i18n.resources" 
value="DefaultMessages,org.apache.struts_example.messages.*"/>
 
   <!--
   This factory overrides the one defined in Struts and will be used a default 
one across the framework
diff --git a/text-provider/src/main/webapp/WEB-INF/content/success.jsp 
b/text-provider/src/main/webapp/WEB-INF/content/success.jsp
index 5b089a3..ca6be2e 100644
--- a/text-provider/src/main/webapp/WEB-INF/content/success.jsp
+++ b/text-provider/src/main/webapp/WEB-INF/content/success.jsp
@@ -15,6 +15,13 @@
   <s:text name="global.key"/>
 </h2>
 
+<h3>Bundles registered with a wildcard</h3>
+<ul>
+  <li><s:text name="common.greeting"/></li>
+  <li><s:text name="labels.title"/></li>
+  <li><s:text name="shared.key"/></li>
+</ul>
+
 <h3>Select your provider</h3>
 <ul>
   <li>
diff --git 
a/text-provider/src/test/java/org/apache/struts_example/WildcardLocalizedTextProviderTest.java
 
b/text-provider/src/test/java/org/apache/struts_example/WildcardLocalizedTextProviderTest.java
new file mode 100644
index 0000000..458b5dd
--- /dev/null
+++ 
b/text-provider/src/test/java/org/apache/struts_example/WildcardLocalizedTextProviderTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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 org.apache.struts_example;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Locale;
+
+import static org.junit.Assert.assertEquals;
+
+public class WildcardLocalizedTextProviderTest {
+
+    private static final Locale POLISH = Locale.forLanguageTag("pl");
+
+    private Locale defaultLocale;
+    private WildcardLocalizedTextProvider provider;
+
+    @Before
+    public void setUp() {
+        defaultLocale = Locale.getDefault();
+        Locale.setDefault(Locale.ENGLISH);
+        provider = new WildcardLocalizedTextProvider();
+        
provider.setCustomI18NResources("DefaultMessages,org.apache.struts_example.messages.*");
+    }
+
+    @After
+    public void tearDown() {
+        Locale.setDefault(defaultLocale);
+    }
+
+    @Test
+    public void wildcardRegistersEveryBundleInThePackage() {
+        assertEquals("Hello from the common bundle", 
provider.findDefaultText("common.greeting", Locale.ENGLISH));
+        assertEquals("Loaded from the labels bundle via a wildcard", 
provider.findDefaultText("labels.title", Locale.ENGLISH));
+    }
+
+    @Test
+    public void localeVariantsResolveThroughTheirBaseBundle() {
+        assertEquals("Hello from the common bundle", 
provider.findDefaultText("common.greeting", Locale.ENGLISH));
+        assertEquals("Witaj z pakietu common", 
provider.findDefaultText("common.greeting", POLISH));
+    }
+
+    @Test
+    public void laterBundleInSortedOrderWinsOnDuplicateKey() {
+        assertEquals("Defined in labels", 
provider.findDefaultText("shared.key", Locale.ENGLISH));
+    }
+
+    @Test
+    public void namesWithoutWildcardPassThrough() {
+        assertEquals("Default: Custom Text Provider example", 
provider.findDefaultText("default.index.title", Locale.ENGLISH));
+    }
+}

Reply via email to