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  
65  /**
66   * Loads global message bundles from /mgnl-i18n/.
67   */
68  public class DefaultMessageBundlesLoader {
69      protected static final Predicate<Resource> DIRECTORY_PREDICATE = Functions.pathStartsWith("/mgnl-i18n");
70      protected static final Predicate<Resource> RESOURCE_PREDICATE = Functions.pathMatches("/mgnl-i18n/.*\\.properties");
71      private static final Logger log = LoggerFactory.getLogger(DefaultMessageBundlesLoader.class);
72  
73      private final Map<Locale, Properties> messages = new HashMap<>();
74      // this map collects all duplicates  based on key pattern <key>_<locale>_<bundle-url>
75      private Map<String, DuplicateEntry> duplicateEntriesMap = new HashMap<>();
76  
77      @Inject
78      public DefaultMessageBundlesLoader(ResourceOrigin resourceOrigin) {
79          loadMessages(resourceOrigin, newVisitor());
80      }
81  
82      /**
83       * @deprecated since 5.4.5. Use {@link #DefaultMessageBundlesLoader(info.magnolia.resourceloader.ResourceOrigin)} instead.
84       */
85      @Deprecated
86      public DefaultMessageBundlesLoader() {
87          this(Components.getComponent(ResourceOrigin.class));
88      }
89  
90      Map<Locale, Properties> getMessages() {
91          return messages;
92      }
93  
94      private FileResourceCollectorVisitor newVisitor() {
95          return FileResourceCollectorVisitor.on(DIRECTORY_PREDICATE, RESOURCE_PREDICATE);
96      }
97  
98      private void loadMessages(ResourceOrigin resourceOrigin, FileResourceCollectorVisitor visitor) {
99          resourceOrigin.traverseWith(visitor);
100         final Collection<Resource> collected = visitor.getCollectedResources();
101         for (Resource langFile : collected) {
102             final String fileName = langFile.getName();
103             final Locale locale = resolveLocale(fileName);
104             loadResourcesInPropertiesMap(langFile, locale);
105         }
106         // Log the duplicates AFTER all the resource files have been loaded.
107         logDuplicates();
108     }
109 
110     /**
111      * TODO: Since we use ResourceOrigin, we can't detect duplicates in classpath resources anymore. See MAGNOLIA-6376.
112      * 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.
113      * 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
114      * TODO: restore previous doc: currently being loaded, the method will output a WARN message and will overwrite the existing key with the new one.
115      */
116     private void loadResourcesInPropertiesMap(Resource propertiesFile, Locale locale) {
117         try (InputStream in = propertiesFile.openStream()) {
118             log.debug("Loading properties file at [{}] with locale [{}]...", propertiesFile, locale);
119 
120             // Use UnicodeReader when available https://code.google.com/p/guava-libraries/issues/detail?id=345
121             final Reader inStream = new InputStreamReader(new BOMInputStream(in), "UTF-8");
122 
123             final Properties properties = new Properties();
124             properties.load(inStream);
125             Properties existingProperties = messages.get(locale);
126 
127             if (existingProperties != null) {
128                 checkForDuplicates(existingProperties, properties, locale, propertiesFile);
129                 // this locale already exists in our bundle, add the new ones
130                 log.debug("Adding properties to already existing ones under {} locale", locale);
131                 properties.putAll(existingProperties);
132             }
133 
134             messages.put(locale, properties);
135         } catch (IOException e) {
136             log.warn("An IO error occurred while trying to read properties file at [{}]", propertiesFile, e);
137         }
138     }
139 
140     private void checkForDuplicates(Properties existingProperties, Properties newProperties, Locale locale, Resource resource) {
141         for (String key : newProperties.stringPropertyNames()) {
142             if (existingProperties.containsKey(key)) {
143                 DuplicateEntry duplicateEntry = new DuplicateEntry(resource, locale, key);
144                 duplicateEntriesMap.put(duplicateEntry.getKeyWithLocaleAndUrl(), duplicateEntry);
145             }
146         }
147     }
148 
149     private void logDuplicates() {
150         // this map contains all the duplicates based on key pattern <key>_<locale>
151         Map<String, DuplicateEntry> perLocaleDuplicates = new TreeMap<>();
152         // this map contains all the duplicates based on key pattern <key>
153         Map<String, DuplicateEntry> perKeyDuplicates = new TreeMap<>();
154         Set<String> urlsSet = new TreeSet<>();
155 
156         for (DuplicateEntry duplicateEntry : duplicateEntriesMap.values()) {
157             perLocaleDuplicates.put(duplicateEntry.getKeyWithLocale(), duplicateEntry);
158             perKeyDuplicates.put(duplicateEntry.getKey(), duplicateEntry);
159             urlsSet.add(duplicateEntry.getResource().toString());
160         }
161 
162         if (duplicateEntriesMap.size() > 0) {
163             StringBuilder logMsg = new StringBuilder("\n");
164             logMsg.append("------------------------------------\n");
165             logMsg.append("Duplicated keys found while loading message bundles from ./mgnl-i18n :\n");
166             logMsg.append("------------------------------------\n");
167             logMsg.append("Number of duplicates based on key pattern <key>_<locale>_<bundle-url>: ").append(duplicateEntriesMap.size()).append("\n");
168             logMsg.append("Number of duplicates based on key pattern <key>_<locale>: ").append(perLocaleDuplicates.size()).append("\n");
169             logMsg.append("Number of duplicates based on key pattern <key>: ").append(perKeyDuplicates.size()).append("\n");
170             logMsg.append("To get more details concerning the keys, raise the log level to 'DEBUG' for ").append(this.getClass().getName()).append(".\n");
171             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");
172             logMsg.append("URLs of the affected files creating duplicate entries:\n");
173             for (String url : urlsSet) {
174                 logMsg.append(url).append("\n");
175             }
176             logMsg.append("------------------------------------");
177             log.info(logMsg.toString());
178 
179             // logging all the duplicated keys (based on key pattern <key>_<locale>) only in debug level.
180             for (DuplicateEntry duplicateEntry : perLocaleDuplicates.values()) {
181                 log.debug("Duplicate key found: [{}]; for locale [{}]; in resource [{}]", duplicateEntry.getKey(), duplicateEntry.getLocale(), duplicateEntry.getResource());
182             }
183         }
184         duplicateEntriesMap = null;
185     }
186 
187     /**
188      * @see info.magnolia.i18nsystem.util.LocaleUtils#parseFromFilename(String, java.util.Locale)
189      */
190     protected Locale resolveLocale(final String fileName) {
191         return LocaleUtils.parseFromFilename(fileName, getFallbackLocale());
192     }
193 
194     // TODO - move this to a SystemLocalesManager component
195     private Locale getFallbackLocale() {
196         return MessagesManager.getInstance().getDefaultLocale();
197     }
198 
199     // An inner class to monitor the duplicates.
200     private class DuplicateEntry {
201         private Resource resource;
202         private Locale locale;
203         private String key;
204         private String keyWithLocale;
205         private String keyWithLocaleAndUrl;
206 
207         private DuplicateEntry(Resource resource, Locale locale, String key) {
208             this.resource = resource;
209             this.locale = locale;
210             this.key = key;
211             this.keyWithLocale = this.key + "_" + locale.toString();
212             this.keyWithLocaleAndUrl = this.keyWithLocale + "_" + resource.getPath();
213         }
214 
215         public Resource getResource() {
216             return resource;
217         }
218 
219         private String getKeyWithLocale() {
220             return keyWithLocale;
221         }
222 
223         private String getKeyWithLocaleAndUrl() {
224             return keyWithLocaleAndUrl;
225         }
226 
227         private Locale getLocale() {
228             return locale;
229         }
230 
231         private String getKey() {
232             return key;
233         }
234 
235     }
236 
237 }