Skip to content

Commit bc7095d

Browse files
Support multiple properties files with a certain name in HapiLocalizer (#7141)
* Try multi-file resolution * Make inner class static and spotless. * Messy fix for ResourceBundle.Control: 1) Disregard properties files for non-root locales (by overriding a couple of methods). 2) Fix the I/O to read the Properties objects since the old code with StringReader was truncating the properties. * Add new test. * Refactor ResourceBundle.Control implementation to be more efficient and extract it to its own class, with a backing custom ResourceBundle implementation. --------- Co-authored-by: Michael Buckley <[email protected]>
1 parent bcd585f commit bc7095d

File tree

4 files changed

+126
-2
lines changed

4 files changed

+126
-2
lines changed

hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/HapiLocalizer.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import ca.uhn.fhir.context.ConfigurationException;
2323
import ca.uhn.fhir.util.UrlUtil;
2424
import ca.uhn.fhir.util.VersionUtil;
25+
import com.google.common.annotations.VisibleForTesting;
2526

2627
import java.text.MessageFormat;
2728
import java.util.ArrayList;
@@ -169,7 +170,7 @@ MessageFormat newMessageFormat(String theFormatString) {
169170
protected void init(String[] theBundleNames) {
170171
myBundle = new ArrayList<>();
171172
for (String nextName : theBundleNames) {
172-
myBundle.add(ResourceBundle.getBundle(nextName));
173+
myBundle.add(ResourceBundle.getBundle(nextName, new MultiFileResourceBundleControl()));
173174
}
174175
}
175176

@@ -189,4 +190,9 @@ public static void setOurFailOnMissingMessage(boolean ourFailOnMissingMessage) {
189190
public static String toKey(Class<?> theType, String theKey) {
190191
return theType.getName() + '.' + theKey;
191192
}
193+
194+
@VisibleForTesting
195+
List<ResourceBundle> getBundles() {
196+
return myBundle;
197+
}
192198
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package ca.uhn.fhir.i18n;
2+
3+
import jakarta.annotation.Nonnull;
4+
5+
import java.io.IOException;
6+
import java.io.InputStream;
7+
import java.net.URL;
8+
import java.util.Collections;
9+
import java.util.Enumeration;
10+
import java.util.List;
11+
import java.util.Locale;
12+
import java.util.Properties;
13+
import java.util.ResourceBundle;
14+
15+
/**
16+
* Finds all properties files on the class path that match the given base name and merges them.
17+
* This implementation avoids the unnecessary overhead of writing to and reading from a stream.
18+
*/
19+
public class MultiFileResourceBundleControl extends ResourceBundle.Control {
20+
21+
/**
22+
* Returns a list containing only Locale.ROOT as a candidate.
23+
* This forces the loader to look for the base bundle only (e.g., "bundle.properties").
24+
*
25+
* @param baseName the base name of the resource bundle
26+
* @param locale the locale to load (this parameter is ignored)
27+
* @return a singleton list containing Locale.ROOT
28+
*/
29+
@Override
30+
public List<Locale> getCandidateLocales(String baseName, Locale locale) {
31+
return Collections.singletonList(Locale.ROOT);
32+
}
33+
34+
/**
35+
* Prevents fallback to the default locale if the root bundle isn't found.
36+
*
37+
* @param baseName the base name of the resource bundle
38+
* @param locale the locale where the search is failing
39+
* @return null to indicate no fallback should be attempted
40+
*/
41+
@Override
42+
public Locale getFallbackLocale(String baseName, Locale locale) {
43+
return null;
44+
}
45+
46+
@Override
47+
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload)
48+
throws IOException, IllegalAccessException, InstantiationException {
49+
50+
// We only care about handling the root .properties file
51+
if (!"java.properties".equals(format)
52+
|| (locale != null && !locale.toString().isEmpty())) {
53+
return super.newBundle(baseName, locale, format, loader, reload);
54+
}
55+
56+
final String resourceName = toResourceName(toBundleName(baseName, locale), "properties");
57+
final Properties mergedProperties = new Properties();
58+
59+
// Load from all matching resources
60+
final Enumeration<URL> resources = loader.getResources(resourceName);
61+
while (resources.hasMoreElements()) {
62+
final URL url = resources.nextElement();
63+
64+
try (InputStream is = url.openStream()) {
65+
mergedProperties.load(is);
66+
}
67+
}
68+
69+
// Directly wrap the merged Properties object instead of performing a stream round-trip.
70+
return new PropertiesResourceBundle(mergedProperties);
71+
}
72+
73+
/**
74+
* A lightweight, private ResourceBundle implementation that is backed directly by a Properties object.
75+
*/
76+
private static class PropertiesResourceBundle extends ResourceBundle {
77+
private final Properties properties;
78+
79+
PropertiesResourceBundle(Properties properties) {
80+
this.properties = properties;
81+
}
82+
83+
@Override
84+
protected Object handleGetObject(@Nonnull String key) {
85+
return properties.getProperty(key);
86+
}
87+
88+
@Override
89+
@Nonnull
90+
public Enumeration<String> getKeys() {
91+
return Collections.enumeration(properties.stringPropertyNames());
92+
}
93+
}
94+
}

hapi-fhir-base/src/test/java/ca/uhn/fhir/i18n/HapiLocalizerTest.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
import org.junit.jupiter.api.Test;
44

5+
import java.util.ArrayList;
6+
import java.util.Collections;
7+
import java.util.List;
8+
import java.util.ResourceBundle;
59
import java.util.Set;
610

711
import static org.assertj.core.api.Assertions.assertThat;
@@ -36,5 +40,24 @@ public void testGetVersion() {
3640
String version = svc.getMessage("hapi.version");
3741
assertThat(version).matches("[0-9]+.*");
3842
}
39-
43+
44+
@Test
45+
void multiFileBundles() {
46+
final HapiLocalizer hapiLocalizer = new HapiLocalizer();
47+
final List<ResourceBundle> bundles = hapiLocalizer.getBundles();
48+
assertThat(bundles).isNotNull().hasSize(1);
49+
50+
final ResourceBundle onlyResourceBundle = bundles.get(0);
51+
final List<String> keysAsList = Collections.list(onlyResourceBundle.getKeys());
52+
53+
// From the main hapi-messages file
54+
assertThat(keysAsList)
55+
.contains("ca.uhn.fhir.jpa.dao.index.IdHelperService.nonUniqueForcedId");
56+
// From the hapi-messages file in the test resources
57+
assertThat(keysAsList).contains("foo");
58+
59+
assertThat(onlyResourceBundle.getString("ca.uhn.fhir.jpa.dao.index.IdHelperService.nonUniqueForcedId")).isEqualTo("Non-unique ID specified, can not process request");
60+
// And we get the expected value from the test resource
61+
assertThat(onlyResourceBundle.getString("foo")).isEqualTo("bar");
62+
}
4063
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo=bar

0 commit comments

Comments
 (0)