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));
+ }
+}