From 9b0f932f223cd23c3387d52b5d23e5ccef3fe9f2 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 10 Oct 2026 14:33:50 +0200 Subject: [PATCH] WW-3452 feat(text-provider): expand wildcard entries in struts.custom.i18n.resources 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 --- text-provider/pom.xml | 6 ++ .../MyLocalizedTextProvider.java | 4 +- .../WildcardLocalizedTextProvider.java | 94 +++++++++++++++++++ .../struts_example/messages/common.properties | 2 + .../messages/common_pl.properties | 1 + .../struts_example/messages/labels.properties | 2 + text-provider/src/main/resources/struts.xml | 5 +- .../main/webapp/WEB-INF/content/success.jsp | 7 ++ .../WildcardLocalizedTextProviderTest.java | 71 ++++++++++++++ 9 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 text-provider/src/main/java/org/apache/struts_example/WildcardLocalizedTextProvider.java create mode 100644 text-provider/src/main/resources/org/apache/struts_example/messages/common.properties create mode 100644 text-provider/src/main/resources/org/apache/struts_example/messages/common_pl.properties create mode 100644 text-provider/src/main/resources/org/apache/struts_example/messages/labels.properties create mode 100644 text-provider/src/test/java/org/apache/struts_example/WildcardLocalizedTextProviderTest.java diff --git a/text-provider/pom.xml b/text-provider/pom.xml index 0513ea50..521a033f 100644 --- a/text-provider/pom.xml +++ b/text-provider/pom.xml @@ -23,6 +23,12 @@ struts2-convention-plugin ${struts2.version} + + org.apache.struts + struts2-junit-plugin + ${struts2.version} + test + 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 05f3ed5c..7dae0bad 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 00000000..ed0eda8f --- /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 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 findBundlesInPackage(String packageName) { + Set 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 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 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 00000000..ac813dd6 --- /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 00000000..161fc5f8 --- /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 00000000..e206fa30 --- /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 df7ec479..404f74cb 100644 --- a/text-provider/src/main/resources/struts.xml +++ b/text-provider/src/main/resources/struts.xml @@ -6,7 +6,10 @@ - + +