View Javadoc
1   /**
2    * This file Copyright (c) 2013-2018 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.event.EventBus;
38  import info.magnolia.event.SystemEventBus;
39  import info.magnolia.i18nsystem.module.I18nModule;
40  import info.magnolia.objectfactory.ComponentProvider;
41  import info.magnolia.objectfactory.Components;
42  import info.magnolia.resourceloader.ResourceOrigin;
43  
44  import java.util.Arrays;
45  import java.util.Locale;
46  import java.util.Properties;
47  
48  import javax.inject.Inject;
49  import javax.inject.Named;
50  import javax.inject.Provider;
51  import javax.inject.Singleton;
52  
53  import org.apache.commons.lang3.StringUtils;
54  import org.jsoup.Jsoup;
55  import org.jsoup.nodes.Document;
56  import org.jsoup.safety.Cleaner;
57  import org.jsoup.safety.Whitelist;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  
61  /**
62   * A TranslationService which relies on a "global" message bundle, as loaded per {@link DefaultMessageBundlesLoader}.
63   * Each given key is looked up in the given locale, then in less specific locales, then in the fallback locale.
64   * That is, if [a, b, c] are passed as keys, with country-specific Locale such as de_CH, and "b" is defined in the de_CH
65   * message bundle, it will be returned, even if "a" is defined in the less specific "de" Locale.
66   *
67   * When a basename is explicitly specified, it however uses {@link MessagesManager} to look the keys up, falls back
68   * to the default bundle of MessagesManager, then finally falls back on its own global message bundle.
69   */
70  @Singleton
71  public class TranslationServiceImpl implements TranslationService {
72      private static final Logger log = LoggerFactory.getLogger(TranslationServiceImpl.class);
73      private static final Cleaner CLEANER = new Cleaner(Whitelist.basic());
74  
75      private final Provider<I18nModule> i18nModuleProvider;
76      private final Provider<DefaultMessageBundlesLoader> defaultMessageBundlesLoaderProvider;
77  
78      @Inject
79      public TranslationServiceImpl(final Provider<I18nModule> i18nModuleProvider, final Provider<DefaultMessageBundlesLoader> defaultMessageBundlesLoaderProvider) {
80          this.i18nModuleProvider = i18nModuleProvider;
81          this.defaultMessageBundlesLoaderProvider = defaultMessageBundlesLoaderProvider;
82      }
83  
84      /**
85       * @deprecated since 5.5.2. Use {@link #TranslationServiceImpl(javax.inject.Provider, javax.inject.Provider<DefaultMessageBundlesLoader>)} instead.
86       */
87      @Deprecated
88      public TranslationServiceImpl(final Provider<I18nModule> i18nModuleProvider, final ComponentProvider componentProvider, final ResourceOrigin resourceOrigin, @Named(SystemEventBus.NAME) EventBus systemEventBus) {
89          this(i18nModuleProvider, new Provider<DefaultMessageBundlesLoader>() {
90              @Override
91              public DefaultMessageBundlesLoader get() {
92                  return Components.getComponent(DefaultMessageBundlesLoader.class);
93              }
94          });
95      }
96  
97      /**
98       * @deprecated since 5.4.5. Use {@link #TranslationServiceImpl(javax.inject.Provider, javax.inject.Provider<DefaultMessageBundlesLoader>)} instead.
99       */
100     @Deprecated
101     public TranslationServiceImpl(Provider<I18nModule> i18nModuleProvider) {
102         this(i18nModuleProvider, new Provider<DefaultMessageBundlesLoader>() {
103             @Override
104             public DefaultMessageBundlesLoader get() {
105                 return Components.getComponent(DefaultMessageBundlesLoader.class);
106             }
107         });
108     }
109 
110     /**
111      * @deprecated since 5.4.4. Use {@link #TranslationServiceImpl(javax.inject.Provider, javax.inject.Provider<DefaultMessageBundlesLoader>)} instead.
112      */
113     @Deprecated
114     public TranslationServiceImpl() {
115         this(new Provider<I18nModule>() {
116             @Override
117             public I18nModule get() {
118                 return Components.getComponent(I18nModule.class);
119             }
120         }, new Provider<DefaultMessageBundlesLoader>() {
121             @Override
122             public DefaultMessageBundlesLoader get() {
123                 return Components.getComponent(DefaultMessageBundlesLoader.class);
124             }
125         });
126     }
127 
128     /**
129      * @deprecated since 5.5.2. Use {@link #defaultMessageBundlesLoaderProvider} directly or setup message bundles in {@link DefaultMessageBundlesLoader}.
130      */
131     @Deprecated
132     protected DefaultMessageBundlesLoader setupMessageBundles() {
133         return defaultMessageBundlesLoaderProvider.get();
134     }
135 
136     @Override
137     public String translate(LocaleProvider localeProvider, String[] keys) {
138         return translate(localeProvider, null, keys);
139     }
140 
141     @Override
142     public String translate(LocaleProvider localeProvider, String[] keys, String fallback) {
143         return translate(localeProvider.get(), null, keys, fallback);
144     }
145 
146     @Override
147     public String translate(LocaleProvider localeProvider, String basename, String[] keys) {
148         final Locale locale = localeProvider.getLocale();
149         return translate(locale, basename, keys, I18nText.NO_FALLBACK);
150     }
151 
152     private String translate(Locale locale, String basename, String[] keys, String fallback) {
153         if (locale == null) {
154             throw new IllegalArgumentException("Locale can't be null");
155         }
156         if (keys == null || keys.length < 1) {
157             throw new IllegalArgumentException("Keys can't be null or empty");
158         }
159 
160         if (basename != null) {
161             log.debug("Got an explicit basename ({}) for keys {}", basename, Arrays.asList(keys));
162         }
163 
164         final String message = lookUpKeyUntilFound(keys, locale, basename);
165         if (message != null) {
166             return message;
167         } else {
168             return I18nText.NO_FALLBACK.equals(fallback) ? handleUnknownKey(locale, basename, keys) : fallback;
169         }
170     }
171 
172     private String lookUpKeyUntilFound(final String[] keys, final Locale locale, final String basename) {
173         String message = null;
174 
175         if (StringUtils.isNotBlank(basename)) {
176             log.debug("Looking up key [{}] with basename [{}] and Locale [{}] - legacy method", keys[0], basename, locale);
177             message = legacyLookup(locale, basename, keys[0]);
178             if (message != null) {
179                 return message + (isDebug() ? this.addDebugInfo(keys, keys[0], locale, basename) : StringUtils.EMPTY);
180             }
181         }
182 
183         if (message == null) {
184             log.debug("Looking up in global i18n message bundle with key [{}] and Locale [{}]", Arrays.asList(keys), locale);
185             message = doGetMessage(keys, locale);
186         }
187 
188         if (message == null) {
189             final String country = locale.getCountry();
190             if (country != null) {
191                 final Locale newLocale = new Locale(locale.getLanguage(), country);
192                 message = doGetMessage(keys, newLocale);
193             }
194         }
195 
196         if (message == null) {
197             final Locale newLocale = new Locale(locale.getLanguage());
198             message = doGetMessage(keys, newLocale);
199         }
200 
201         if (message == null) {
202             message = doGetMessage(keys, getFallbackLocale());
203         }
204 
205         return message;
206     }
207 
208     private String addDebugInfo(final String[] keys, String currentlyUsedKey, final Locale locale, final String basename) {
209         return "\n" + Arrays.asList(keys).toString()
210                 .replaceFirst("(?s)" + currentlyUsedKey + "(?!.*?" + currentlyUsedKey + ")", ">" + currentlyUsedKey + "<") //replace last occurrence
211                 + locale.getClass().getSimpleName() + ":" + locale + (basename == null ? StringUtils.EMPTY : ",Using legacy i18n basename:" + basename); //: is used because = is stripped in area/components bars in page editor
212     }
213 
214     protected String handleUnknownKey(Locale locale, String basename, String[] keys) {
215         // TODO - this method could be context dependent, or delegate to a configured component. In dev mode, for instance, we could at least print this out, or return the key, while in production this is neither useful nor needed.
216         log.debug("No translation found for any of {} with locale {} and basename {}", keys, locale, basename != null ? basename : "<unspecified>");
217         return keys[0] + (isDebug() ? this.addDebugInfo(keys, null, locale, basename) : StringUtils.EMPTY);
218     }
219 
220     /**
221      * Looks up a particular key using the given basename and Locale, using the legacy MessagesManager.
222      */
223     private String legacyLookup(Locale locale, String basename, String key) {
224         // Note that this internally chains the given locale with the fallback locale (as known by the MessagesManager), so if a key is known in english, it will be returned in english before we lookup in the default bundles
225         String message = MessagesManager.getMessages(basename, locale).get(key);
226         if (legacyMessageNotFound(message)) {
227             message = MessagesManager.getMessages(MessagesManager.DEFAULT_BASENAME, locale).get(key);
228         }
229         if (legacyMessageNotFound(message)) {
230             // Let's not get any of the legacy "???" markers out of here
231             return null;
232         } else {
233             Document document = Jsoup.parseBodyFragment(message, "");
234             if (!CLEANER.isValid(document)) {
235                 return CLEANER.clean(document).body().html();
236             } else {
237                 return message;
238             }
239         }
240     }
241 
242     private boolean legacyMessageNotFound(final String message) {
243         return message == null || message.startsWith("???");
244     }
245 
246     private String doGetMessage(String[] keys, Locale locale) {
247         final Properties properties = defaultMessageBundlesLoaderProvider.get().getMessages().get(locale);
248         if (properties != null) {
249             for (String key : keys) {
250                 if (key == null) {
251                     // Keys can sometimes be null (e.g when using NullKeyGenerator and the undecorated or configured text is null)
252                     continue;
253                 }
254                 final String message = properties.getProperty(key);
255                 if (message != null) {
256                     return message + (isDebug() ? this.addDebugInfo(keys, key, locale, null) : StringUtils.EMPTY);
257                 }
258             }
259         }
260         return null;
261     }
262 
263     private boolean isDebug() {
264         try {
265             return i18nModuleProvider.get() != null && i18nModuleProvider.get().isDebug();
266         } catch (RuntimeException exception) {
267             // Under certain circumstances the provider will throw this exception (e.g. when module is not loaded yet).
268             // We are catching it as we only need to know whether we want debugging on or not even though the javadoc
269             // of javax.inject.Provider.get() suggests to not handle these.
270             return false;
271         }
272     }
273 
274     // TODO - move this to a SystemLocalesManager component
275     private Locale getFallbackLocale() {
276         return MessagesManager.getInstance().getDefaultLocale();
277     }
278 
279     /**
280      * @deprecated since 5.5.2. Reload message bundles in {@link DefaultMessageBundlesLoader} directly.
281      */
282     @Deprecated
283     @Override
284     public void reloadMessageBundles() {
285         log.warn("Not reloading message bundles. Please make sure to update [{}] instead.", DefaultMessageBundlesLoader.class.getName());
286     }
287 
288 }