View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  
19  package org.mycore.services.i18n;
20  
21  import java.io.File;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.text.MessageFormat;
27  import java.util.Enumeration;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.LinkedList;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.MissingResourceException;
35  import java.util.Properties;
36  import java.util.ResourceBundle;
37  import java.util.ResourceBundle.Control;
38  import java.util.Set;
39  import java.util.regex.Matcher;
40  import java.util.regex.Pattern;
41  import java.util.stream.Collectors;
42  
43  import javax.xml.parsers.DocumentBuilder;
44  
45  import org.apache.logging.log4j.LogManager;
46  import org.apache.logging.log4j.Logger;
47  import org.mycore.common.MCRSessionMgr;
48  import org.mycore.common.config.MCRConfiguration2;
49  import org.mycore.common.config.MCRConfigurationDir;
50  import org.mycore.common.config.MCRProperties;
51  import org.mycore.common.xml.MCRDOMUtils;
52  import org.w3c.dom.Document;
53  import org.w3c.dom.Element;
54  
55  /**
56   * provides services for internationalization in mycore application. You have to provide a property file named
57   * messages.properties in your classpath for this class to work.
58   * 
59   * @author Radi Radichev
60   * @author Thomas Scheffler (yagee)
61   */
62  public class MCRTranslation {
63  
64      private static final String MESSAGES_BUNDLE = "messages";
65  
66      private static final String DEPRECATED_MESSAGES_PROPERTIES = "/deprecated-messages.properties";
67  
68      private static final Logger LOGGER = LogManager.getLogger(MCRTranslation.class);
69  
70      private static final Pattern ARRAY_DETECTOR = Pattern.compile(";");
71  
72      private static final Control CONTROL = new MCRCombinedResourceBundleControl();
73  
74      private static boolean DEPRECATED_MESSAGES_PRESENT = false;
75  
76      private static Properties DEPRECATED_MAPPING = loadProperties();
77  
78      private static Set<String> AVAILABLE_LANGUAGES = loadAvailableLanguages();
79  
80      static {
81          debug();
82      }
83  
84      /**
85       * provides translation for the given label (property key). The current locale that is needed for translation is
86       * gathered by the language of the current MCRSession.
87       * 
88       * @param label property key
89       * @return translated String
90       */
91      public static String translate(String label) {
92          Locale currentLocale = getCurrentLocale();
93          return translate(label, currentLocale);
94      }
95  
96      /**
97       * Checks whether there is a value for the given label and current locale.
98       * 
99       * @param label property key
100      * @return <code>true</code> if there is a value, <code>false</code> otherwise
101      */
102     public static boolean exists(String label) {
103         try {
104             ResourceBundle message = getResourceBundle(MESSAGES_BUNDLE, getCurrentLocale());
105             message.getString(label);
106         } catch (MissingResourceException mre) {
107             LOGGER.debug(mre);
108             return false;
109         }
110         return true;
111     }
112 
113     /**
114      * provides translation for the given label (property key). The current locale that is needed for translation is
115      * gathered by the language of the current MCRSession.
116      * 
117      * @param label property key
118      * @param baseName
119      *            a fully qualified class name
120      * @return translated String
121      */
122 
123     public static String translateWithBaseName(String label, String baseName) {
124         Locale currentLocale = getCurrentLocale();
125         return translate(label, currentLocale, baseName);
126     }
127 
128     /**
129      * provides translation for the given label (property key).
130      * 
131      * @param label property key
132      * @param locale
133      *            target locale of translation
134      * @return translated String
135      */
136     public static String translate(String label, Locale locale) {
137         return translate(label, locale, MESSAGES_BUNDLE);
138     }
139 
140     /**
141      * provides translation for the given label (property key).
142      * 
143      * @param label property key
144      * @param locale
145      *            target locale of translation
146      * @param baseName
147      *            a fully qualified class name
148      * @return translated String
149      */
150     public static String translate(String label, Locale locale, String baseName) {
151         LOGGER.debug("Translation for current locale: {}", locale.getLanguage());
152         ResourceBundle message;
153         try {
154             message = getResourceBundle(baseName, locale);
155         } catch (MissingResourceException mre) {
156             //no messages.properties at all
157             LOGGER.debug(mre.getMessage());
158             return "???" + label + "???";
159         }
160         String result = null;
161         try {
162             result = message.getString(label);
163             LOGGER.debug("Translation for {}={}", label, result);
164         } catch (MissingResourceException mre) {
165             // try to get new key if 'label' is deprecated
166             if (!DEPRECATED_MESSAGES_PRESENT) {
167                 LOGGER.warn("Could not load resource '" + DEPRECATED_MESSAGES_PROPERTIES
168                     + "' to check for depreacted I18N keys.");
169             } else if (DEPRECATED_MAPPING.containsKey(label)) {
170                 String newLabel = DEPRECATED_MAPPING.getProperty(label);
171                 try {
172                     result = message.getString(newLabel);
173                 } catch (java.util.MissingResourceException e) {
174                 }
175                 if (result != null) {
176                     LOGGER.warn("Usage of deprected I18N key '{}'. Please use '{}' instead.", label, newLabel);
177                     return result;
178                 }
179             }
180             result = "???" + label + "???";
181             LOGGER.debug(mre.getMessage());
182         }
183         return result;
184     }
185 
186     /**
187      * Returns a map of label/value pairs which match with the given prefix. The current locale that is needed for
188      * translation is gathered by the language of the current MCRSession.
189      * 
190      * @param prefix
191      *            label starts with
192      * @return map of labels with translated values
193      */
194     public static Map<String, String> translatePrefix(String prefix) {
195         Locale currentLocale = getCurrentLocale();
196         return translatePrefix(prefix, currentLocale);
197     }
198 
199     /**
200      * Returns a map of label/value pairs which match with the given prefix.
201      * 
202      * @param prefix
203      *            label starts with
204      * @param locale
205      *            target locale of translation
206      * @return map of labels with translated values
207      */
208     public static Map<String, String> translatePrefix(String prefix, Locale locale) {
209         LOGGER.debug("Translation for locale: {}", locale.getLanguage());
210         HashMap<String, String> map = new HashMap<>();
211         ResourceBundle message = getResourceBundle(MESSAGES_BUNDLE, locale);
212         Enumeration<String> keys = message.getKeys();
213         while (keys.hasMoreElements()) {
214             String key = keys.nextElement();
215             if (key.startsWith(prefix)) {
216                 map.put(key, message.getString(key));
217             }
218         }
219         return map;
220     }
221 
222     /**
223      * provides translation for the given label (property key). The current locale that is needed for translation is
224      * gathered by the language of the current MCRSession.
225      * 
226      * @param label property key
227      * @param arguments
228      *            Objects that are inserted instead of placeholders in the property values
229      * @return translated String
230      */
231     public static String translate(String label, Object... arguments) {
232         Locale currentLocale = getCurrentLocale();
233         String msgFormat = translate(label);
234         MessageFormat formatter = new MessageFormat(msgFormat, currentLocale);
235         String result = formatter.format(arguments);
236         LOGGER.debug("Translation for {}={}", label, result);
237         return result;
238     }
239 
240     /**
241      * provides translation for the given label (property key). The current locale that is needed for translation is
242      * gathered by the language of the current MCRSession. Be aware that any occurence of ';' and '\' in
243      * <code>argument</code> has to be masked by '\'. You can use ';' to build an array of arguments: "foo;bar" would
244      * result in {"foo","bar"} (the array)
245      * 
246      * @param label property key
247      * @param argument
248      *            String that is inserted instead of placeholders in the property values
249      * @return translated String
250      * @see #translate(String, Object[])
251      */
252     public static String translate(String label, String argument) {
253         return translate(label, (Object[]) getStringArray(argument));
254     }
255 
256     public static Locale getCurrentLocale() {
257         String currentLanguage = MCRSessionMgr.getCurrentSession().getCurrentLanguage();
258         return getLocale(currentLanguage);
259     }
260 
261     public static Locale getLocale(String language) {
262         if (language.equals("id")) {
263             // workaround for bug with indonesian
264             // INDONESIAN      ID     OCEANIC/INDONESIAN [*Changed 1989 from original ISO 639:1988, IN]
265             // Java doesn't work with id
266             language = "in";
267             LOGGER.debug("Translation for current locale: {}", language);
268         }
269         return new Locale(language);
270     }
271 
272     public static Set<String> getAvailableLanguages() {
273         return AVAILABLE_LANGUAGES;
274     }
275 
276     public static Document getAvailableLanguagesAsXML() {
277         DocumentBuilder documentBuilder = MCRDOMUtils.getDocumentBuilderUnchecked();
278         try {
279             Document document = documentBuilder.newDocument();
280             Element i18nRoot = document.createElement("i18n");
281             document.appendChild(i18nRoot);
282             for (String lang : AVAILABLE_LANGUAGES) {
283                 Element langElement = document.createElement("lang");
284                 langElement.setTextContent(lang);
285                 i18nRoot.appendChild(langElement);
286             }
287             return document;
288         } finally {
289             MCRDOMUtils.releaseDocumentBuilder(documentBuilder);
290         }
291     }
292 
293     static String[] getStringArray(String masked) {
294         List<String> a = new LinkedList<>();
295         boolean mask = false;
296         StringBuilder buf = new StringBuilder();
297         if (masked == null) {
298             return new String[0];
299         }
300         if (!isArray(masked)) {
301             a.add(masked);
302         } else {
303             for (int i = 0; i < masked.length(); i++) {
304                 switch (masked.charAt(i)) {
305                 case ';':
306                     if (mask) {
307                         buf.append(';');
308                         mask = false;
309                     } else {
310                         a.add(buf.toString());
311                         buf.setLength(0);
312                     }
313                     break;
314                 case '\\':
315                     if (mask) {
316                         buf.append('\\');
317                         mask = false;
318                     } else {
319                         mask = true;
320                     }
321                     break;
322                 default:
323                     buf.append(masked.charAt(i));
324                     break;
325                 }
326             }
327             a.add(buf.toString());
328         }
329         return a.toArray(String[]::new);
330     }
331 
332     static boolean isArray(String masked) {
333         Matcher m = ARRAY_DETECTOR.matcher(masked);
334         while (m.find()) {
335             int pos = m.start();
336             int count = 0;
337             for (int i = pos - 1; i > 0; i--) {
338                 if (masked.charAt(i) == '\\') {
339                     count++;
340                 } else {
341                     break;
342                 }
343             }
344             if (count % 2 == 0) {
345                 return true;
346             }
347         }
348         return false;
349     }
350 
351     static Properties loadProperties() {
352         Properties deprecatedMapping = new Properties();
353         try {
354             final InputStream propertiesStream = MCRTranslation.class
355                 .getResourceAsStream(DEPRECATED_MESSAGES_PROPERTIES);
356             if (propertiesStream == null) {
357                 LOGGER.warn("Could not find resource '" + DEPRECATED_MESSAGES_PROPERTIES + "'.");
358                 return deprecatedMapping;
359             }
360             deprecatedMapping.load(propertiesStream);
361             DEPRECATED_MESSAGES_PRESENT = true;
362         } catch (IOException e) {
363             LOGGER.warn("Could not load resource '" + DEPRECATED_MESSAGES_PROPERTIES + "'.", e);
364         }
365         return deprecatedMapping;
366     }
367 
368     static Set<String> loadAvailableLanguages() {
369         // try to load application relevant languages
370         return MCRConfiguration2.getString("MCR.Metadata.Languages")
371             .map(MCRConfiguration2::splitValue)
372             .map(s -> s.collect(Collectors.toSet()))
373             .orElseGet(() -> loadLanguagesByMessagesBundle()); //all languages by available messages_*.properties
374     }
375 
376     static Set<String> loadLanguagesByMessagesBundle() {
377         Set<String> languages = new HashSet<>();
378         for (Locale locale : Locale.getAvailableLocales()) {
379             try {
380                 if (!locale.getLanguage().equals("")) {
381                     ResourceBundle bundle = getResourceBundle(MESSAGES_BUNDLE, locale);
382                     languages.add(bundle.getLocale().toString());
383                 }
384             } catch (MissingResourceException e) {
385                 LOGGER.debug("Could not load " + MESSAGES_BUNDLE + " for locale: {}", locale);
386             }
387         }
388         return languages;
389     }
390 
391     public static ResourceBundle getResourceBundle(String baseName, Locale locale) {
392         return baseName.contains(".") ? ResourceBundle.getBundle(baseName, locale)
393             : ResourceBundle.getBundle("stacked:" + baseName, locale, CONTROL);
394     }
395 
396     /**
397      * output the current message properties to configuration directory
398      */
399     private static void debug() {
400         for (String lang : MCRTranslation.getAvailableLanguages()) {
401             ResourceBundle rb = MCRTranslation.getResourceBundle("messages", MCRTranslation.getLocale(lang));
402             Properties props = new MCRProperties();
403             rb.keySet().forEach(key -> props.put(key, rb.getString(key)));
404             File resolvedMsgFile = MCRConfigurationDir.getConfigFile("messages_" + lang + ".resolved.properties");
405             if (resolvedMsgFile != null) {
406                 try (OutputStream os = new FileOutputStream(resolvedMsgFile)) {
407                     props.store(os, "MyCoRe Messages for Locale " + lang);
408                 } catch (IOException e) {
409                     LOGGER.warn("Could not store resolved properties to {}", resolvedMsgFile.getAbsolutePath(), e);
410                 }
411             }
412         }
413     }
414 }