Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions text-provider/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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 + "_"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
common.greeting=Hello from the common bundle
shared.key=Defined in common
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
common.greeting=Witaj z pakietu common
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
labels.title=Loaded from the labels bundle via a wildcard
shared.key=Defined in labels
5 changes: 4 additions & 1 deletion text-provider/src/main/resources/struts.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions text-provider/src/main/webapp/WEB-INF/content/success.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Loading