View Javadoc
1   /**
2    * This file Copyright (c) 2003-2017 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.cms.i18n;
35  
36  import info.magnolia.cms.util.ObservationUtil;
37  import info.magnolia.context.SystemContext;
38  import info.magnolia.jcr.node2bean.Node2BeanProcessor;
39  import info.magnolia.jcr.node2bean.TransformationState;
40  import info.magnolia.jcr.node2bean.TypeDescriptor;
41  import info.magnolia.jcr.node2bean.TypeMapping;
42  import info.magnolia.jcr.node2bean.impl.Node2BeanTransformerImpl;
43  import info.magnolia.jcr.util.NodeTypes;
44  import info.magnolia.jcr.util.PropertyUtil;
45  import info.magnolia.objectfactory.ComponentProvider;
46  import info.magnolia.objectfactory.Components;
47  import info.magnolia.repository.RepositoryConstants;
48  
49  import java.util.ArrayList;
50  import java.util.Collection;
51  import java.util.Collections;
52  import java.util.HashMap;
53  import java.util.Iterator;
54  import java.util.LinkedHashMap;
55  import java.util.Locale;
56  import java.util.Map;
57  
58  import javax.inject.Inject;
59  import javax.inject.Singleton;
60  import javax.jcr.Node;
61  import javax.jcr.Session;
62  import javax.jcr.observation.EventIterator;
63  import javax.jcr.observation.EventListener;
64  
65  import org.apache.commons.collections4.Transformer;
66  import org.apache.commons.collections4.map.LazyMap;
67  import org.apache.commons.lang3.StringUtils;
68  import org.slf4j.Logger;
69  import org.slf4j.LoggerFactory;
70  
71  
72  /**
73   * Default MessagesManager implementation.
74   */
75  @Singleton
76  public class DefaultMessagesManager extends MessagesManager {
77      private final static Logger log = LoggerFactory.getLogger(DefaultMessagesManager.class);
78  
79      /**
80       * The current locale of the application.
81       */
82      private Locale applicationLocale;
83  
84      /**
85       * List of the available locales.
86       */
87      private final Collection<Locale> availableLocales = new ArrayList<>();
88  
89      /**
90       * Map for the messages.
91       */
92      private Map messages;
93  
94      private String defaultBasename = DEFAULT_BASENAME;
95  
96      private final Node2BeanProcessor nodeToBean;
97      private final SystemContext systemContext;
98  
99      @Inject
100     public DefaultMessagesManager(Node2BeanProcessor nodeToBean, SystemContext systemContext) {
101         this.nodeToBean = nodeToBean;
102         this.systemContext = systemContext;
103 
104         // setting default language (en)
105         setDefaultLocale(FALLBACK_LOCALE);
106 
107         initMap();
108     }
109 
110     /**
111      * @deprecated since 5.5.1 - use {@link #DefaultMessagesManager(info.magnolia.jcr.node2bean.Node2BeanProcessor, info.magnolia.context.SystemContext)} instead.
112      */
113     @Deprecated
114     public DefaultMessagesManager() {
115         this(Components.getComponent(Node2BeanProcessor.class), Components.getComponent(SystemContext.class));
116     }
117 
118     /**
119      * @deprecated since 5.5.1 - use {@link #DefaultMessagesManager(info.magnolia.jcr.node2bean.Node2BeanProcessor, info.magnolia.context.SystemContext)} instead.
120      */
121     @Deprecated
122     public DefaultMessagesManager(Node2BeanProcessor nodeToBean) {
123         this(nodeToBean, Components.getComponent(SystemContext.class));
124     }
125 
126     // for tests
127     void setDefaultBasename(String defaultBasename) {
128         this.defaultBasename = defaultBasename;
129     }
130 
131     /**
132      * Called through the initialization process. (startup of the container)
133      */
134 
135     @Override
136     public void init() {
137         load();
138         registerEventListener();
139     }
140 
141     /**
142      * The lazy Map creates messages objects with a fall back to the default locale.
143      */
144     protected void initMap() {
145         // LazyMap will instantiate bundles on demand.
146         final Map map = LazyMap.lazyMap(new HashMap(), new Transformer() {
147 
148             // this transformer will wrap the Messages in a MessagesChain which
149             // will fall back to a Messages instance for the same bundle with
150             // default locale.
151             @Override
152             public Object transform(Object input) {
153                 final MessagesID id = (MessagesID) input;
154                 return newMessages(id);
155             }
156         });
157         messages = Collections.synchronizedMap(map);
158     }
159 
160     /**
161      * Initializes a new Messages instances for the given MessagesID. By default, we chain to the same bundle with the
162      * default Locale. (so untranslated messages show up in the default language)
163      */
164     protected Messages newMessages(MessagesID messagesID) {
165         Messages messages = new DefaultMessagesImpl(messagesID.basename, messagesID.locale);
166         if (!getDefaultLocale().equals(messagesID.locale)) {
167             messages = new MessagesChain(messages).chain(getMessages(messagesID.basename, getDefaultLocale()));
168         }
169         return messages;
170     }
171 
172     /**
173      * Load i18n configuration.
174      */
175     protected void load() {
176         try {
177             // reading the configuration from the repository, no need for context
178             final Session session = systemContext.getJCRSession(RepositoryConstants.CONFIG);
179 
180             log.info("Loading i18n configuration - {}", I18N_CONFIG_PATH);
181 
182             // checks if node exists
183             if (!session.nodeExists(I18N_CONFIG_PATH)) {
184                 log.warn("{} does not exist yet; skipping.", I18N_CONFIG_PATH);
185                 return;
186             }
187 
188             final Node configNode = session.getNode(I18N_CONFIG_PATH);
189 
190             setDefaultLocale(PropertyUtil.getString(configNode, FALLBACK_NODEDATA, FALLBACK_LOCALE));
191 
192             // get the available languages - creates it if it does not exist - necessary to update to 3.5
193             final Node languagesNode;
194             if (configNode.hasNode(LANGUAGES_NODE_NAME)) {
195                 languagesNode = configNode.getNode(LANGUAGES_NODE_NAME);
196             } else {
197                 languagesNode = configNode.addNode(LANGUAGES_NODE_NAME, NodeTypes.Content.NAME);
198             }
199 
200             final Map<String, LocaleDefinition> localeDefinitions = (Map<String, LocaleDefinition>) nodeToBean.setProperties(new LinkedHashMap<String, LocaleDefinition>(), languagesNode, true, new Node2BeanTransformerImpl() {
201                 @Override
202                 protected TypeDescriptor onResolveType(TypeMapping typeMapping, TransformationState state, TypeDescriptor resolvedType, ComponentProvider componentProvider) {
203                     if (resolvedType == null && state.getLevel() == 2) {
204                         return typeMapping.getTypeDescriptor(LocaleDefinition.class);
205                     }
206                     return resolvedType;
207                 }
208             }, Components.getComponentProvider());
209 
210             // clear collection for reload
211             availableLocales.clear();
212 
213             for (LocaleDefinition localeDefinition : localeDefinitions.values()) {
214                 if (localeDefinition.isEnabled()) {
215                     availableLocales.add(localeDefinition.getLocale());
216                 }
217             }
218         } catch (Exception e) {
219             log.error("Failed to load i18n configuration - {}", I18N_CONFIG_PATH, e);
220         }
221     }
222 
223     /**
224      * Register an event listener: reload configuration when something changes.
225      */
226     private void registerEventListener() {
227         log.info("Registering event listener for i18n");
228         ObservationUtil.registerChangeListener(RepositoryConstants.CONFIG, I18N_CONFIG_PATH, new EventListener() {
229 
230             @Override
231             public void onEvent(EventIterator iterator) {
232                 // reload everything
233                 reload();
234             }
235         });
236     }
237 
238     /**
239      * Reload i18n configuration.
240      */
241     @Override
242     public void reload() {
243         try {
244             // reload all present
245             for (Iterator messagesIterator = messages.values().iterator(); messagesIterator.hasNext(); ) {
246                 Messages messages = (Messages) messagesIterator.next();
247                 messages.reload();
248             }
249         } catch (Exception e) {
250             log.error("Can't reload i18n messages", e);
251         }
252         initMap();
253         load();
254     }
255 
256     @Override
257     public Messages getMessagesInternal(String basename, Locale locale) {
258         if (StringUtils.isEmpty(basename)) {
259             basename = defaultBasename;
260         }
261         return (Messages) messages.get(new MessagesID(basename, locale));
262     }
263 
264     @Override
265     public Locale getDefaultLocale() {
266         return applicationLocale;
267     }
268 
269     /**
270      * @param defaultLocale The defaultLocale to set.
271      * @deprecated since 4.0 - not used and should not be. Use setLocale() on the SystemContext instead. --note: do not
272      *             remove the method, make it private. applicationLocale field is still needed. --and/or remove duplication with
273      *             SystemContext.locale
274      */
275     @Deprecated
276     public void setDefaultLocale(String defaultLocale) {
277         this.applicationLocale = new Locale(defaultLocale);
278     }
279 
280     @Override
281     public Collection getAvailableLocales() {
282         return availableLocales;
283     }
284 
285     public void setMessages(Map messages) {
286         this.messages = messages;
287     }
288 
289     /**
290      * Used as the key in the Map.
291      */
292     public static class MessagesID {
293 
294         private final String basename;
295 
296         private final Locale locale;
297 
298         public MessagesID(String basename, Locale locale) {
299             this.basename = basename;
300             this.locale = locale;
301         }
302 
303         // generated equals and hashcode methods
304 
305         @Override
306         public boolean equals(Object o) {
307             if (this == o) {
308                 return true;
309             }
310             if (o == null || getClass() != o.getClass()) {
311                 return false;
312             }
313 
314             MessagesID that = (MessagesID) o;
315 
316             if (basename != null ? !basename.equals(that.basename) : that.basename != null) {
317                 return false;
318             }
319             if (locale != null ? !locale.equals(that.locale) : that.locale != null) {
320                 return false;
321             }
322 
323             return true;
324         }
325 
326         @Override
327         public int hashCode() {
328             int result = basename != null ? basename.hashCode() : 0;
329             result = 31 * result + (locale != null ? locale.hashCode() : 0);
330             return result;
331         }
332 
333         /**
334          * Returns the basename.
335          *
336          * @return the basename
337          */
338         public String getBasename() {
339             return basename;
340         }
341 
342         /**
343          * Returns the locale.
344          *
345          * @return the locale
346          */
347         public Locale getLocale() {
348             return locale;
349         }
350 
351     }
352 }