1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.i18nsystem;
35
36 import info.magnolia.cms.i18n.MessagesManager;
37 import info.magnolia.i18nsystem.util.LocaleUtils;
38 import info.magnolia.objectfactory.Components;
39 import info.magnolia.resourceloader.Resource;
40 import info.magnolia.resourceloader.ResourceChangeHandler;
41 import info.magnolia.resourceloader.ResourceOrigin;
42 import info.magnolia.resourceloader.ResourceOriginChange;
43 import info.magnolia.resourceloader.util.FileResourceCollectorVisitor;
44 import info.magnolia.resourceloader.util.Functions;
45
46 import java.io.IOException;
47 import java.io.InputStream;
48 import java.io.InputStreamReader;
49 import java.io.Reader;
50 import java.util.Collection;
51 import java.util.Collections;
52 import java.util.HashMap;
53 import java.util.Locale;
54 import java.util.Map;
55 import java.util.Properties;
56 import java.util.Set;
57 import java.util.TreeMap;
58 import java.util.TreeSet;
59
60 import javax.inject.Inject;
61 import javax.inject.Singleton;
62
63 import org.apache.commons.io.input.BOMInputStream;
64 import org.jsoup.Jsoup;
65 import org.jsoup.nodes.Document;
66 import org.jsoup.safety.Cleaner;
67 import org.jsoup.safety.Whitelist;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
70
71 import com.google.common.base.Predicate;
72 import com.google.common.base.Predicates;
73
74
75
76
77 @Singleton
78 public class DefaultMessageBundlesLoader {
79
80 public static final String MGNL_I18N_PATH = "^/[^/]+/i18n/";
81 public static final String MGNL_I18N_PROPERTIES = MGNL_I18N_PATH + ".*\\.properties$";
82
83 private static final String OLD_I18N_PATH = "^/mgnl-i18n/";
84 private static final String OLD_I18N_PROPERTIES = OLD_I18N_PATH + ".*\\.properties$";
85
86 protected static final Predicate<Resource> DIRECTORY_PREDICATE = Predicates.alwaysTrue();
87 protected static final Predicate<Resource> RESOURCE_PREDICATE = Predicates.or(
88 Functions.pathMatches(OLD_I18N_PROPERTIES),
89 Functions.pathMatches(MGNL_I18N_PROPERTIES)
90 );
91
92 private static final Logger log = LoggerFactory.getLogger(DefaultMessageBundlesLoader.class);
93 private static final Cleaner CLEANER = new Cleaner(Whitelist.basic());
94
95 private Map<Locale, Properties> messages = Collections.unmodifiableMap(new HashMap<>());
96
97
98 private final Map<String, DuplicateEntry> duplicateEntriesMap = new HashMap<>();
99
100 @Inject
101 public DefaultMessageBundlesLoader(final ResourceOrigin resourceOrigin) {
102 loadMessages(resourceOrigin, newVisitor());
103
104
105
106
107
108
109
110 log.info("Starting monitoring of {} to load translation files", resourceOrigin);
111 resourceOrigin.registerResourceChangeHandler(new ResourceChangeHandler() {
112 @Override
113 public void onResourceChanged(ResourceOriginChange change) {
114
115
116
117
118
119
120 if (change.getRelatedResourcePath().matches(MGNL_I18N_PROPERTIES)) {
121 loadMessages(resourceOrigin, newVisitor());
122 }
123 }
124 });
125 }
126
127
128
129
130 @Deprecated
131 public DefaultMessageBundlesLoader() {
132 this(Components.getComponent(ResourceOrigin.class));
133 }
134
135 Map<Locale, Properties> getMessages() {
136 return messages;
137 }
138
139 private FileResourceCollectorVisitor newVisitor() {
140 return FileResourceCollectorVisitor.on(DIRECTORY_PREDICATE, RESOURCE_PREDICATE);
141 }
142
143 private void loadMessages(ResourceOrigin resourceOrigin, FileResourceCollectorVisitor visitor) {
144 final Map<Locale, Properties> newMessages = new HashMap<>();
145
146 resourceOrigin.traverseWith(visitor);
147 final Collection<Resource> collected = visitor.getCollectedResources();
148 for (Resource langFile : collected) {
149 final String fileName = langFile.getName();
150 final Locale locale = resolveLocale(fileName);
151 loadResourcesInPropertiesMap(newMessages, langFile, locale);
152 }
153
154 this.messages = Collections.unmodifiableMap(newMessages);
155
156
157 logDuplicates();
158 }
159
160
161
162
163
164
165
166
167
168
169
170
171
172 private void loadResourcesInPropertiesMap(final Map<Locale, Properties> newMessages, Resource propertiesFile, Locale locale) {
173 try (InputStream in = propertiesFile.openStream()) {
174 log.debug("Loading properties file at [{}] with locale [{}]...", propertiesFile, locale);
175
176
177 final Reader inStream = new InputStreamReader(new BOMInputStream(in), "UTF-8");
178
179 final Properties properties = new Properties();
180 properties.load(inStream);
181 Properties existingProperties = newMessages.get(locale);
182
183 if (existingProperties != null) {
184 checkForDuplicates(existingProperties, properties, locale, propertiesFile);
185
186 log.debug("Adding properties to already existing ones under {} locale", locale);
187 properties.putAll(existingProperties);
188 }
189
190 for (Map.Entry<Object, Object> property : properties.entrySet()) {
191 String propertyValue = property.getValue().toString();
192 Document document = Jsoup.parseBodyFragment(propertyValue, "");
193 if (!CLEANER.isValid(document)) {
194 properties.replace(property.getKey(), CLEANER.clean(document).body().html());
195 }
196 }
197
198 newMessages.put(locale, properties);
199 } catch (IOException e) {
200 log.warn("An IO error occurred while trying to read properties file at [{}]", propertiesFile, e);
201 }
202 }
203
204 private void checkForDuplicates(Properties existingProperties, Properties newProperties, Locale locale, Resource resource) {
205 for (String key : newProperties.stringPropertyNames()) {
206 if (existingProperties.containsKey(key)) {
207 DuplicateEntry duplicateEntry = new DuplicateEntry(resource, locale, key);
208 duplicateEntriesMap.put(duplicateEntry.getKeyWithLocaleAndUrl(), duplicateEntry);
209 }
210 }
211 }
212
213 private void logDuplicates() {
214
215 Map<String, DuplicateEntry> perLocaleDuplicates = new TreeMap<>();
216
217 Map<String, DuplicateEntry> perKeyDuplicates = new TreeMap<>();
218 Set<String> urlsSet = new TreeSet<>();
219
220 for (DuplicateEntry duplicateEntry : duplicateEntriesMap.values()) {
221 perLocaleDuplicates.put(duplicateEntry.getKeyWithLocale(), duplicateEntry);
222 perKeyDuplicates.put(duplicateEntry.getKey(), duplicateEntry);
223 urlsSet.add(duplicateEntry.getResource().toString());
224 }
225
226 if (duplicateEntriesMap.size() > 0) {
227 StringBuilder logMsg = new StringBuilder("\n");
228 logMsg.append("------------------------------------\n");
229 logMsg.append("Duplicated keys found while loading message bundles from ./mgnl-i18n :\n");
230 logMsg.append("------------------------------------\n");
231 logMsg.append("Number of duplicates based on key pattern <key>_<locale>_<bundle-url>: ").append(duplicateEntriesMap.size()).append("\n");
232 logMsg.append("Number of duplicates based on key pattern <key>_<locale>: ").append(perLocaleDuplicates.size()).append("\n");
233 logMsg.append("Number of duplicates based on key pattern <key>: ").append(perKeyDuplicates.size()).append("\n");
234 logMsg.append("To get more details concerning the keys, raise the log level to 'DEBUG' for ").append(this.getClass().getName()).append(".\n");
235 logMsg.append("If you encounter a large number of duplicates, it's possible that you are running in a development environment where you have multiple copies of the web-apps in the overlays folder of your web-app.\n");
236 logMsg.append("URLs of the affected files creating duplicate entries:\n");
237 for (String url : urlsSet) {
238 logMsg.append(url).append("\n");
239 }
240 logMsg.append("------------------------------------");
241 log.info(logMsg.toString());
242
243
244 for (DuplicateEntry duplicateEntry : perLocaleDuplicates.values()) {
245 log.debug("Duplicate key found: [{}]; for locale [{}]; in resource [{}]", duplicateEntry.getKey(), duplicateEntry.getLocale(), duplicateEntry.getResource());
246 }
247 }
248
249 duplicateEntriesMap.clear();
250 }
251
252
253
254
255 protected Locale resolveLocale(final String fileName) {
256 return LocaleUtils.parseFromFilename(fileName, getFallbackLocale());
257 }
258
259
260 private Locale getFallbackLocale() {
261 return MessagesManager.getInstance().getDefaultLocale();
262 }
263
264
265 private class DuplicateEntry {
266 private Resource resource;
267 private Locale locale;
268 private String key;
269 private String keyWithLocale;
270 private String keyWithLocaleAndUrl;
271
272 private DuplicateEntry(Resource resource, Locale locale, String key) {
273 this.resource = resource;
274 this.locale = locale;
275 this.key = key;
276 this.keyWithLocale = this.key + "_" + locale.toString();
277 this.keyWithLocaleAndUrl = this.keyWithLocale + "_" + resource.getPath();
278 }
279
280 public Resource getResource() {
281 return resource;
282 }
283
284 private String getKeyWithLocale() {
285 return keyWithLocale;
286 }
287
288 private String getKeyWithLocaleAndUrl() {
289 return keyWithLocaleAndUrl;
290 }
291
292 private Locale getLocale() {
293 return locale;
294 }
295
296 private String getKey() {
297 return key;
298 }
299
300 }
301
302 }