1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package de.kaiserpfalzedv.commons.core.i18n;
19
20 import java.io.BufferedReader;
21 import java.io.IOException;
22 import java.io.InputStreamReader;
23 import java.net.URL;
24 import java.nio.charset.StandardCharsets;
25 import java.text.MessageFormat;
26 import java.util.HashMap;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.MissingResourceException;
30 import java.util.PropertyResourceBundle;
31 import java.util.ResourceBundle;
32 import java.util.stream.Collectors;
33
34 import de.kaiserpfalzedv.commons.api.i18n.MessageSource;
35 import de.kaiserpfalzedv.commons.api.i18n.NoSuchMessageException;
36 import de.kaiserpfalzedv.commons.api.i18n.Translator;
37 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
38 import lombok.Getter;
39 import lombok.Setter;
40 import lombok.extern.slf4j.Slf4j;
41
42
43
44
45
46
47
48 @Slf4j
49 public class ResourceBundleTranslator implements Translator, MessageSource {
50 private static final long serialVersionUID = 0L;
51
52
53
54
55
56 @SuppressFBWarnings(value = "EI_EXPOSE_REP2", justification = "lombok generated setter")
57 @Setter
58 @Getter
59 private List<String> configuredLanguages = List.of("de","en","fr","nl","es","it");
60
61
62
63
64 private final String defaultBundle;
65
66
67
68
69
70 @Setter
71 @Getter
72 private String defaultLocale = "de";
73
74 private final transient HashMap<String, HashMap<Locale, ResourceBundle>> bundles = new HashMap<>();
75
76
77 public ResourceBundleTranslator() {
78 this("messages");
79 }
80
81 public ResourceBundleTranslator(final String defaultBundle) {
82 this.defaultBundle = defaultBundle;
83 }
84
85
86 @SuppressWarnings("RedundantThrows")
87 @Override
88 public String getMessage(final String key, final Object[] params, final Locale locale) throws NoSuchMessageException {
89 return this.getTranslation(key, locale, params);
90 }
91
92 @Override
93 public String getTranslation(final String key, final Locale locale, final Object... arguments) {
94 return this.getTranslation(this.defaultBundle, key, locale, arguments);
95 }
96
97 @Override
98 public String getTranslation(final Object bundleObject, final String key, final Locale locale, final Object... arguments) {
99 final String bundleName = bundleObject.getClass().getCanonicalName()
100 .replace(".", "/")
101 .replace("_Subclass", "");
102
103 return this.getTranslation(bundleName, key, locale, arguments);
104 }
105
106 @SuppressFBWarnings(value = "DCN_NULLPOINTER_EXCEPTION", justification = "It's the most elegant way.")
107 @Override
108 public String getTranslation(final String bundleName, final String key, final Locale locale, final Object... arguments) {
109 this.loadBundle(bundleName, locale);
110 try {
111 final String pattern = this.bundles.get(bundleName).get(locale).getString(key);
112 final MessageFormat format = new MessageFormat(pattern, locale);
113 return format.format(arguments);
114 } catch (NullPointerException | MissingResourceException ex) {
115 log.warn(
116 "Translation failed. bundle={}, locale={}, key={}",
117 bundleName, locale, key
118 );
119 return "!" + key;
120 }
121 }
122
123
124
125
126
127
128
129
130
131 @SuppressFBWarnings(value = "DCN_NULLPOINTER_EXCEPTION", justification = "It's the most elegant way.")
132 private void loadBundle(final String bundleName, Locale locale) {
133 if (!this.bundles.containsKey(bundleName)) {
134 log.debug("Adding bundle. baseName='{}'", bundleName);
135
136 this.bundles.put(bundleName, new HashMap<>());
137 }
138
139 if (locale == null) {
140 locale = Locale.forLanguageTag(this.defaultLocale);
141 }
142
143 if (!this.bundles.get(bundleName).containsKey(locale)) {
144 log.info("Loading bundle. baseName='{}', locale='{}'", bundleName, locale.getDisplayName());
145
146 ResourceBundle bundle;
147 try {
148 bundle = ResourceBundle.getBundle(bundleName, locale, new UnicodeResourceBundleControl());
149 } catch (NullPointerException | MissingResourceException e) {
150 final Locale l = Locale.forLanguageTag(locale.getLanguage());
151
152 log.warn("Translator did not find the wanted locale for the bundle. bundle={}, locale={}, orig.locale={}, error='{}'",
153 bundleName, l, locale, e.getMessage());
154 try {
155 bundle = ResourceBundle.getBundle(bundleName, l, new UnicodeResourceBundleControl());
156 } catch (NullPointerException | MissingResourceException e1) {
157 log.warn("Translator did not find the wanted bundle. Using default bundle. bundle={}, error='{}'", bundleName, e1.getMessage());
158
159 try {
160 bundle = ResourceBundle.getBundle(this.defaultBundle, Locale.forLanguageTag(this.defaultLocale),
161 new UnicodeResourceBundleControl());
162 } catch (NullPointerException | MissingResourceException e2) {
163 log.error("Resource bundle can't be read. error='{}'", e2.getMessage());
164
165 return;
166 }
167 }
168 }
169 this.bundles.get(bundleName).put(locale, bundle);
170 }
171 }
172
173 @Override
174 public void close() {
175 log.info("Closing all bundles.");
176 this.bundles.clear();
177 }
178
179 @Override
180 public List<Locale> getProvidedLocales() {
181 return this.configuredLanguages.stream()
182 .map(Locale::forLanguageTag)
183 .filter(d -> {log.trace("Mapped language. locale={}", d); return true;})
184 .collect(Collectors.toList());
185 }
186
187
188
189
190
191 private static class UnicodeResourceBundleControl extends ResourceBundle.Control {
192 @Override
193 public ResourceBundle newBundle(
194 final String baseName,
195 final Locale locale,
196 final String format,
197 final ClassLoader loader,
198 final boolean reload
199 ) throws IOException {
200 final ClassLoader used = Thread.currentThread().getContextClassLoader();
201
202 log.debug("Classloader will be ignored. used={}, ignored={}", used, loader);
203
204 final String bundleName = this.toBundleName(baseName, locale);
205 final String resourceName = this.toResourceName(bundleName, "properties");
206 final URL resourceURL = used.getResource(resourceName);
207 if (resourceURL == null)
208 return null;
209
210 try (BufferedReader in = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
211 return new PropertyResourceBundle(in);
212 }
213 }
214 }
215 }