Skip to content

Commit e68cc61

Browse files
committed
refactor: Revise the i18n error template to be thread-safe
1 parent cecb721 commit e68cc61

File tree

4 files changed

+31
-43
lines changed

4 files changed

+31
-43
lines changed

hivemq-edge/src/main/java/com/hivemq/common/i18n/I18nError.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,4 @@ public interface I18nError {
2222
@NotNull String getKey();
2323

2424
@NotNull String getName();
25-
26-
@NotNull String getResourceName();
2725
}

hivemq-edge/src/main/java/com/hivemq/common/i18n/I18nErrorTemplate.java

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Map;
3434
import java.util.Properties;
3535
import java.util.concurrent.ConcurrentHashMap;
36+
import java.util.function.Function;
3637

3738
/**
3839
* Singleton class to manage OpenAPI i18n error templates using FreeMarker.
@@ -41,27 +42,30 @@
4142
* This class is thread-safe and uses a cache to store configurations for different locales.
4243
*/
4344
public final class I18nErrorTemplate {
44-
45-
private static final I18nErrorTemplate INSTANCE = new I18nErrorTemplate();
46-
45+
private static final @NotNull Logger LOGGER = LoggerFactory.getLogger(I18nErrorTemplate.class);
4746
private final @NotNull Map<String, Configuration> configurationMap;
48-
private final @NotNull Logger logger;
47+
private final @NotNull Function<Locale, String> resourceNameFunction;
4948

50-
private I18nErrorTemplate() {
49+
public I18nErrorTemplate(final @NotNull Function<Locale, String> resourceNameFunction) {
5150
configurationMap = new ConcurrentHashMap<>();
52-
logger = LoggerFactory.getLogger(getClass());
51+
this.resourceNameFunction = resourceNameFunction;
5352
}
5453

55-
public static @NotNull I18nErrorTemplate getInstance() {
56-
return INSTANCE;
57-
}
58-
59-
private Configuration createConfiguration(final @NotNull Locale locale) {
54+
private Configuration createConfiguration(final @NotNull Locale locale) throws IOException {
6055
final Configuration configuration = new Configuration(Configuration.VERSION_2_3_34);
61-
configuration.setDefaultEncoding(StandardCharsets.UTF_8.name());
6256
final StringTemplateLoader stringTemplateLoader = new StringTemplateLoader();
57+
configuration.setDefaultEncoding(StandardCharsets.UTF_8.name());
6358
configuration.setTemplateLoader(stringTemplateLoader);
6459
configuration.setLocale(locale);
60+
final Properties properties = new Properties();
61+
final String resourceName = resourceNameFunction.apply(locale);
62+
try (final StringReader stringReader = new StringReader(IOUtils.resourceToString(resourceName,
63+
StandardCharsets.UTF_8))) {
64+
properties.load(stringReader);
65+
}
66+
for (final Map.Entry<Object, Object> entry : properties.entrySet()) {
67+
stringTemplateLoader.putTemplate(String.valueOf(entry.getKey()), String.valueOf(entry.getValue()));
68+
}
6569
return configuration;
6670
}
6771

@@ -71,25 +75,14 @@ private Configuration createConfiguration(final @NotNull Locale locale) {
7175

7276
public @NotNull String get(final @NotNull I18nError i18NError, final @NotNull Map<String, Object> map) {
7377
final Locale locale = I18nLocaleContext.getLocale();
74-
Configuration configuration = configurationMap.get(locale.toString());
75-
if (configuration == null) {
76-
configuration = createConfiguration(locale);
77-
configurationMap.put(locale.toString(), configuration);
78-
}
7978
try {
80-
final StringTemplateLoader stringTemplateLoader = (StringTemplateLoader) configuration.getTemplateLoader();
81-
if (stringTemplateLoader.findTemplateSource(i18NError.getKey()) == null) {
82-
final Properties properties = new Properties();
83-
try (final StringReader stringReader = new StringReader(IOUtils.resourceToString(i18NError.getResourceName(),
84-
StandardCharsets.UTF_8))) {
85-
properties.load(stringReader);
86-
}
87-
synchronized (configuration) {
88-
for (final Map.Entry<Object, Object> entry : properties.entrySet()) {
89-
stringTemplateLoader.putTemplate(String.valueOf(entry.getKey()),
90-
String.valueOf(entry.getValue()));
91-
}
92-
}
79+
Configuration configuration = configurationMap.get(locale.toString());
80+
if (configuration == null) {
81+
// The whole configuration creation process is not thread-safe, but creating a new configuration
82+
// multiple times among concurrent threads brings no harm.
83+
// So, we don't use a lock here to improve the overall performance.
84+
configuration = createConfiguration(locale);
85+
configurationMap.put(locale.toString(), configuration);
9386
}
9487
final Template template = configuration.getTemplate(i18NError.getKey());
9588
try (final StringWriter stringWriter = new StringWriter()) {
@@ -99,12 +92,12 @@ private Configuration createConfiguration(final @NotNull Locale locale) {
9992
} catch (final TemplateException e) {
10093
final String errorMessage =
10194
"Error: Template " + i18NError.getKey() + " for " + locale + " could not be processed.";
102-
logger.error(errorMessage, e);
95+
LOGGER.error(errorMessage, e);
10396
return errorMessage;
10497
} catch (final IOException e) {
10598
final String errorMessage =
10699
"Error: Template " + i18NError.getKey() + " for " + locale + " could not be loaded.";
107-
logger.error(errorMessage, e);
100+
LOGGER.error(errorMessage, e);
108101
return errorMessage;
109102
}
110103
}

hivemq-edge/src/main/java/com/hivemq/common/i18n/I18nHttpError.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ public enum I18nHttpError implements I18nError {
4343

4444
private static final @NotNull String RESOURCE_NAME_PREFIX = "/templates/http-errors-";
4545
private static final @NotNull String RESOURCE_NAME_SUFFIX = ".properties";
46+
private static final I18nErrorTemplate TEMPLATE =
47+
new I18nErrorTemplate(locale -> RESOURCE_NAME_PREFIX + locale + RESOURCE_NAME_SUFFIX);
4648

4749
I18nHttpError() {
4850
}
@@ -52,7 +54,7 @@ public enum I18nHttpError implements I18nError {
5254
}
5355

5456
public @NotNull String get(final @NotNull Map<String, Object> map) {
55-
return I18nErrorTemplate.getInstance().get(this, map);
57+
return TEMPLATE.get(this, map);
5658
}
5759

5860
@Override
@@ -64,9 +66,4 @@ public enum I18nHttpError implements I18nError {
6466
public @NotNull String getName() {
6567
return name();
6668
}
67-
68-
@Override
69-
public @NotNull String getResourceName() {
70-
return RESOURCE_NAME_PREFIX + I18nLocaleContext.getLocale() + RESOURCE_NAME_SUFFIX;
71-
}
7269
}

hivemq-edge/src/test/java/com/hivemq/common/i18n/I18nHttpErrorTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ public void tearDown() {
4949
public void whenLocaleIsEnUS_thenErrorCountShouldMatch() throws IOException {
5050
final List<I18nHttpError> errors = Arrays.asList(I18nHttpError.values());
5151
assertThat(errors.size()).isGreaterThan(0);
52-
assertThat(errors.stream().map(I18nHttpError::getResourceName).collect(Collectors.toSet())).hasSize(1);
5352
final Properties properties = new Properties();
54-
try (final StringReader stringReader = new StringReader(IOUtils.resourceToString(errors.get(0)
55-
.getResourceName(), StandardCharsets.UTF_8))) {
53+
try (final StringReader stringReader = new StringReader(IOUtils.resourceToString(
54+
"/templates/http-errors-en_US.properties",
55+
StandardCharsets.UTF_8))) {
5656
properties.load(stringReader);
5757
}
5858
final Set<Object> propertyKeySet = properties.keySet();

0 commit comments

Comments
 (0)