View Javadoc
1   /**
2    * This file Copyright (c) 2013-2016 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
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.ResourceOrigin;
41  import info.magnolia.resourceloader.util.FileResourceCollectorVisitor;
42  import info.magnolia.resourceloader.util.Functions;
43  
44  import java.io.IOException;
45  import java.io.InputStream;
46  import java.io.InputStreamReader;
47  import java.io.Reader;
48  import java.util.Collection;
49  import java.util.HashMap;
50  import java.util.Locale;
51  import java.util.Map;
52  import java.util.Properties;
53  import java.util.Set;
54  import java.util.TreeMap;
55  import java.util.TreeSet;
56  
57  import javax.inject.Inject;
58  
59  import org.apache.commons.io.input.BOMInputStream;
60  import org.slf4j.Logger;
61  import org.slf4j.LoggerFactory;
62  
63  import com.google.common.base.Predicate;
64  import com.google.common.base.Predicates;
65  
66  /**
67   * Loads global message bundles from /mgnl-i18n/.
68   */
69  public class DefaultMessageBundlesLoader {
70      public static final String MGNL_I18N_PATH = "^/([^/]+/|mgnl-)i18n/";
71      public static final String MGNL_I18N_PROPERTIES = MGNL_I18N_PATH + ".*\\.properties$";
72  
73  
74      protected static final Predicate<Resource> DIRECTORY_PREDICATE = Predicates.alwaysTrue();
75      protected static final Predicate<Resource> RESOURCE_PREDICATE = Functions.pathMatches(MGNL_I18N_PROPERTIES);
76  
77      private static final Logger log = LoggerFactory.getLogger(DefaultMessageBundlesLoader.class);
78  
79      private final Map<Locale, Properties> messages = new HashMap<>();
80      // this map collects all duplicates  based on key pattern <key>_<locale>_<bundle-url>
81      private Map<String, DuplicateEntry> duplicateEntriesMap = new HashMap<>();
82  
83      @Inject
84      public DefaultMessageBundlesLoader(ResourceOrigin resourceOrigin) {
85          loadMessages(resourceOrigin, newVisitor());
86      }
87  
88      /**
89       * @deprecated since 5.4.5. Use {@link #DefaultMessageBundlesLoader(info.magnolia.resourceloader.ResourceOrigin)} instead.
90       */
91      @Deprecated
92      public DefaultMessageBundlesLoader() {
93          this(Components.getComponent(ResourceOrigin.class));
94      }
95  
96      Map<Locale, Properties> getMessages() {
97          return messages;
98      }
99  
100     private FileResourceCollectorVisitor newVisitor() {
101         return FileResourceCollectorVisitor.on(DIRECTORY_PREDICATE, RESOURCE_PREDICATE);
102     }
103 
104     private void loadMessages(ResourceOrigin resourceOrigin, FileResourceCollectorVisitor visitor) {
105         resourceOrigin.traverseWith(visitor);
106         final Collection<Resource> collected = visitor.getCollectedResources();
107         for (Resource langFile : collected) {
108             final String fileName = langFile.getName();
109             final Locale locale = resolveLocale(fileName);
110             loadResourcesInPropertiesMap(langFile, locale);
111         }
112         // Log the duplicates AFTER all the resource files have been loaded.
113         logDuplicates();
114     }
115 
116     /**
117      * TODO: Since we use ResourceOrigin, we can't detect duplicates in classpath resources anymore. See MAGNOLIA-6376.
118      * TODO: restore previous doc: A properties file might be associated with multiple actual resources in the classpath, e.g. when different modules declare a same name file under /mgnl-i18n.
119      * TODO: restore previous doc: This method ensures that in such a case all resources are loaded. Moreover, should an existing properties file already contains keys found in the one
120      * TODO: restore previous doc: currently being loaded, the method will output a WARN message and will overwrite the existing key with the new one.
121      */
122     private void loadResourcesInPropertiesMap(Resource propertiesFile, Locale locale) {
123         try (InputStream in = propertiesFile.openStream()) {
124             log.debug("Loading properties file at [{}] with locale [{}]...", propertiesFile, locale);
125 
126             // Use UnicodeReader when available https://code.google.com/p/guava-libraries/issues/detail?id=345
127             final Reader inStream = new InputStreamReader(new BOMInputStream(in), "UTF-8");
128 
129             final Properties properties = new Properties();
130             properties.load(inStream);
131             Properties existingProperties = messages.get(locale);
132 
133             if (existingProperties != null) {
134                 checkForDuplicates(existingProperties, properties, locale, propertiesFile);
135                 // this locale already exists in our bundle, add the new ones
136                 log.debug("Adding properties to already existing ones under {} locale", locale);
137                 properties.putAll(existingProperties);
138             }
139 
140             messages.put(locale, properties);
141         } catch (IOException e) {
142             log.warn("An IO error occurred while trying to read properties file at [{}]", propertiesFile, e);
143         }
144     }
145 
146     private void checkForDuplicates(Properties existingProperties, Properties newProperties, Locale locale, Resource resource) {
147         for (String key : newProperties.stringPropertyNames()) {
148             if (existingProperties.containsKey(key)) {
149                 DuplicateEntry duplicateEntry = new DuplicateEntry(resource, locale, key);
150                 duplicateEntriesMap.put(duplicateEntry.getKeyWithLocaleAndUrl(), duplicateEntry);
151             }
152         }
153     }
154 
155     private void logDuplicates() {
156         // this map contains all the duplicates based on key pattern <key>_<locale>
157         Map<String, DuplicateEntry> perLocaleDuplicates = new TreeMap<>();
158         // this map contains all the duplicates based on key pattern <key>
159         Map<String, DuplicateEntry> perKeyDuplicates = new TreeMap<>();
160         Set<String> urlsSet = new TreeSet<>();
161 
162         for (DuplicateEntry duplicateEntry : duplicateEntriesMap.values()) {
163             perLocaleDuplicates.put(duplicateEntry.getKeyWithLocale(), duplicateEntry);
164             perKeyDuplicates.put(duplicateEntry.getKey(), duplicateEntry);
165             urlsSet.add(duplicateEntry.getResource().toString());
166         }
167 
168         if (duplicateEntriesMap.size() > 0) {
169             StringBuilder logMsg = new StringBuilder("\n");
170             logMsg.append("------------------------------------\n");
171             logMsg.append("Duplicated keys found while loading message bundles from ./mgnl-i18n :\n");
172             logMsg.append("------------------------------------\n");
173             logMsg.append("Number of duplicates based on key pattern <key>_<locale>_<bundle-url>: ").append(duplicateEntriesMap.size()).append("\n");
174             logMsg.append("Number of duplicates based on key pattern <key>_<locale>: ").append(perLocaleDuplicates.size()).append("\n");
175             logMsg.append("Number of duplicates based on key pattern <key>: ").append(perKeyDuplicates.size()).append("\n");
176             logMsg.append("To get more details concerning the keys, raise the log level to 'DEBUG' for ").append(this.getClass().getName()).append(".\n");
177             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");
178             logMsg.append("URLs of the affected files creating duplicate entries:\n");
179             for (String url : urlsSet) {
180                 logMsg.append(url).append("\n");
181             }
182             logMsg.append("------------------------------------");
183             log.info(logMsg.toString());
184 
185             // logging all the duplicated keys (based on key pattern <key>_<locale>) only in debug level.
186             for (DuplicateEntry duplicateEntry : perLocaleDuplicates.values()) {
187                 log.debug("Duplicate key found: [{}]; for locale [{}]; in resource [{}]", duplicateEntry.getKey(), duplicateEntry.getLocale(), duplicateEntry.getResource());
188             }
189         }
190         duplicateEntriesMap = null;
191     }
192 
193     /**
194      * @see info.magnolia.i18nsystem.util.LocaleUtils#parseFromFilename(String, java.util.Locale)
195      */
196     protected Locale resolveLocale(final String fileName) {
197         return LocaleUtils.parseFromFilename(fileName, getFallbackLocale());
198     }
199 
200     // TODO - move this to a SystemLocalesManager component
201     private Locale getFallbackLocale() {
202         return MessagesManager.getInstance().getDefaultLocale();
203     }
204 
205     // An inner class to monitor the duplicates.
206     private class DuplicateEntry {
207         private Resource resource;
208         private Locale locale;
209         private String key;
210         private String keyWithLocale;
211         private String keyWithLocaleAndUrl;
212 
213         private DuplicateEntry(Resource resource, Locale locale, String key) {
214             this.resource = resource;
215             this.locale = locale;
216             this.key = key;
217             this.keyWithLocale = this.key + "_" + locale.toString();
218             this.keyWithLocaleAndUrl = this.keyWithLocale + "_" + resource.getPath();
219         }
220 
221         public Resource getResource() {
222             return resource;
223         }
224 
225         private String getKeyWithLocale() {
226             return keyWithLocale;
227         }
228 
229         private String getKeyWithLocaleAndUrl() {
230             return keyWithLocaleAndUrl;
231         }
232 
233         private Locale getLocale() {
234             return locale;
235         }
236 
237         private String getKey() {
238             return key;
239         }
240 
241     }
242 
243 }