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.datamodel.language;
20  
21  import java.util.Arrays;
22  import java.util.HashMap;
23  import java.util.Locale;
24  import java.util.Map;
25  
26  import org.apache.logging.log4j.LogManager;
27  import org.apache.logging.log4j.Logger;
28  import org.mycore.backend.jpa.MCREntityManagerProvider;
29  import org.mycore.common.MCRConstants;
30  import org.mycore.common.MCRSession;
31  import org.mycore.common.MCRSessionMgr;
32  import org.mycore.common.MCRTransactionHelper;
33  import org.mycore.common.config.MCRConfiguration2;
34  import org.mycore.datamodel.classifications2.MCRCategory;
35  import org.mycore.datamodel.classifications2.MCRCategoryDAO;
36  import org.mycore.datamodel.classifications2.MCRCategoryDAOFactory;
37  import org.mycore.datamodel.classifications2.MCRCategoryID;
38  import org.mycore.datamodel.classifications2.MCRLabel;
39  
40  /**
41   * Returns MCRLanguage instances. The languages most commonly used, English and German,
42   * are provided as constants. Other languages are read from a classification thats ID can be
43   * configured using the property "MCR.LanguageClassification". That classification should use
44   * ISO 639-1 code as category ID, where ISO 639-2 codes can be added by extra labels x-term and x-bibl
45   * for the category. Unknown languages are created by code as required, but a warning is logged.
46   *
47   * @author Frank L\u00FCtzenkirchen
48   */
49  public class MCRLanguageFactory {
50      private static final Logger LOGGER = LogManager.getLogger();
51  
52      private static final MCRLanguageFactory SINGLETON = new MCRLanguageFactory();
53  
54      public static final MCRLanguage GERMAN = MCRLanguageFactory.instance().getLanguage("de");
55  
56      public static final MCRLanguage ENGLISH = MCRLanguageFactory.instance().getLanguage("en");
57  
58      /**
59       * Map of languages by ISO 639-1 or -2 code
60       */
61      private Map<String, MCRLanguage> languageByCode = new HashMap<>();
62  
63      /**
64       * The ID of the classification containing the language codes and labels
65       */
66      private MCRCategoryID classification;
67  
68      private MCRCategoryDAO categoryDAO = MCRCategoryDAOFactory.getInstance();
69  
70      /**
71       * Language classification may change at runtime, so we remember the time we last read the languages in.
72       */
73      private long classificationLastRead = Long.MIN_VALUE;
74  
75      /**
76       * The default language is configured via "MCR.Metadata.DefaultLang"
77       */
78      private String codeOfDefaultLanguage;
79  
80      private MCRLanguageFactory() {
81          codeOfDefaultLanguage = MCRConfiguration2.getString("MCR.Metadata.DefaultLang")
82              .orElse(MCRConstants.DEFAULT_LANG);
83          initDefaultLanguages();
84  
85          classification = MCRConfiguration2.getString("MCR.LanguageClassification")
86              .map(MCRCategoryID::rootID)
87              .orElse(null);
88      }
89  
90      /**
91       * Returns the MCRLanguageFactory singleton
92       */
93      public static MCRLanguageFactory instance() {
94          return SINGLETON;
95      }
96  
97      /**
98       * Returns the default language, as configured by "MCR.Metadata.DefaultLang"
99       */
100     public MCRLanguage getDefaultLanguage() {
101         return getLanguage(codeOfDefaultLanguage);
102     }
103 
104     /**
105      * Returns the language with the given ISO 639-1 or -2 code. When the given code contains a
106      * subcode like "en-us", and the main language is configured, that main language is returned.
107      * When the given code does not match any language configured, a language is created on-the-fly,
108      * but a warning is logged.
109      */
110     public MCRLanguage getLanguage(String code) {
111         if (classificationHasChanged()) {
112             initLanguages();
113         }
114 
115         return lookupLanguage(code);
116     }
117 
118     private MCRLanguage lookupLanguage(String code) {
119         if ((!languageByCode.containsKey(code)) && code.contains("-") && !code.startsWith("x-")) {
120             code = code.split("-")[0];
121         }
122 
123         if (!languageByCode.containsKey(code)) {
124             LOGGER.warn("Unknown language: {}", code);
125             buildLanguage(code, code.length() > 2 ? code : null, null);
126         }
127 
128         return languageByCode.get(code);
129     }
130 
131     /**
132      * This method check the language string base on RFC 1766 to the supported
133      * languages in MyCoRe in a current application environment. Without appending
134      * this MCRLanguageFactory only ENGLISH and GERMAN are supported.
135      *
136      * @param code
137      *            the language string in RFC 1766 syntax
138      * @return true if the language code is supported. It return true too if the code starts
139      *            with x- or i-, otherwise return false;
140      */
141     public final boolean isSupportedLanguage(String code) {
142         if (code == null) {
143             return false;
144         }
145         code = code.trim();
146         if (code.length() == 0) {
147             return false;
148         }
149         if (code.startsWith("x-") || code.startsWith("i-")) {
150             return true;
151         }
152         return languageByCode.containsKey(code);
153     }
154 
155     /**
156      * Checks if any classification has changed in the persistent store, so that the languages
157      * should be read again.
158      */
159     private boolean classificationHasChanged() {
160         //TODO: remove usage of MCREntityManagerProvider
161         return MCRConfiguration2.getBoolean("MCR.Persistence.Database.Enable").orElse(true)
162             && MCREntityManagerProvider.getEntityManagerFactory() != null && (classification != null)
163             && (categoryDAO.getLastModified() > classificationLastRead);
164     }
165 
166     /**
167      * Builds the default languages and reads in the languages configured by classification
168      */
169     private void initLanguages() {
170         languageByCode.clear();
171         initDefaultLanguages();
172         readLanguageClassification();
173     }
174 
175     /**
176      * Builds the default languages
177      */
178     private void initDefaultLanguages() {
179         MCRLanguage de = buildLanguage("de", "deu", "ger");
180         MCRLanguage en = buildLanguage("en", "eng", null);
181         de.setLabel(de, "Deutsch");
182         de.setLabel(en, "German");
183         en.setLabel(de, "Englisch");
184         en.setLabel(en, "English");
185     }
186 
187     /**
188      * Builds a new language object with the given ISO codes.
189      *
190      * @param xmlCode ISO 639-1 code as used for xml:lang
191      * @param termCode ISO 639-2 terminologic code, may be null
192      * @param biblCode ISO 639-2 bibliographical code, may be null
193      */
194     private MCRLanguage buildLanguage(String xmlCode, String termCode, String biblCode) {
195         MCRLanguage language = new MCRLanguage();
196         addCode(language, MCRLanguageCodeType.xmlCode, xmlCode);
197         if (termCode != null) {
198             addCode(language, MCRLanguageCodeType.termCode, termCode);
199             addCode(language, MCRLanguageCodeType.biblCode, biblCode == null ? termCode : biblCode);
200         }
201         Locale locale = Arrays.stream(Locale.getAvailableLocales())
202             .filter(l -> l.toString().equals(xmlCode))
203             .findFirst()
204             .orElseGet(() -> {
205                 String[] codeParts = xmlCode.split("_");
206                 switch (codeParts.length) {
207                 case 1:
208                     return new Locale(codeParts[0]);
209                 case 2:
210                     return new Locale(codeParts[0], codeParts[1]);
211                 default:
212                     return new Locale(codeParts[0], codeParts[1], codeParts[2]);
213                 }
214 
215             });
216         language.setLocale(locale);
217         return language;
218     }
219 
220     /**
221      * Adds and registers the code for the language
222      */
223     private void addCode(MCRLanguage language, MCRLanguageCodeType type, String code) {
224         language.setCode(type, code);
225         languageByCode.put(code, language);
226     }
227 
228     /**
229      * Reads in the language classification and builds language objects from its categories
230      */
231     private void readLanguageClassification() {
232         MCRSession session = MCRSessionMgr.getCurrentSession();
233         if (!MCRTransactionHelper.isTransactionActive()) {
234             MCRTransactionHelper.beginTransaction();
235             buildLanguagesFromClassification();
236             MCRTransactionHelper.commitTransaction();
237         } else {
238             buildLanguagesFromClassification();
239         }
240     }
241 
242     /**
243      * Builds language objects from classification categories
244      */
245     private void buildLanguagesFromClassification() {
246         this.classificationLastRead = categoryDAO.getLastModified();
247 
248         MCRCategory root = categoryDAO.getCategory(classification, -1);
249         if (root == null) {
250             LOGGER.warn("Language classification {} not found", classification.getRootID());
251             return;
252         }
253 
254         for (MCRCategory category : root.getChildren()) {
255             buildLanguage(category);
256         }
257     }
258 
259     /**
260      * Builds a new language object from the given category
261      */
262     private void buildLanguage(MCRCategory category) {
263         String xmlCode = category.getId().getID();
264         String termCode = category.getLabel("x-term").map(MCRLabel::getText).orElse(null);
265         String biblCode = category.getLabel("x-bibl").map(MCRLabel::getText).orElse(termCode);
266 
267         MCRLanguage language = buildLanguage(xmlCode, termCode, biblCode);
268 
269         category
270             .getLabels()
271             .stream()
272             .filter(l -> !l.getLang().startsWith("x-"))
273             .sequential() //MCRLanguage is not thread safe
274             .forEach(l -> {
275                 MCRLanguage languageOfLabel = lookupLanguage(l.getLang());
276                 language.setLabel(languageOfLabel, l.getText());
277             });
278     }
279 }