View Javadoc

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