View Javadoc

1   /**
2    * This file Copyright (c) 2008-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.NodeData;
38  import info.magnolia.cms.util.NodeDataUtil;
39  import info.magnolia.context.MgnlContext;
40  import info.magnolia.jcr.util.PropertyUtil;
41  
42  import java.util.Collection;
43  import java.util.HashSet;
44  import java.util.Iterator;
45  import java.util.LinkedHashMap;
46  import java.util.Locale;
47  import java.util.Map;
48  import java.util.Set;
49  
50  import javax.jcr.Node;
51  import javax.jcr.Property;
52  import javax.jcr.RepositoryException;
53  
54  import org.apache.commons.lang.StringUtils;
55  import org.slf4j.Logger;
56  import org.slf4j.LoggerFactory;
57  
58  /**
59   * An abstract implementation of {@link I18nContentSupport} which stores the
60   * locale specific content in node data having a local suffix:
61   * <name>_<locale>.
62   *
63   * The detection of the current locale, based on the URI for instance, is left to the concrete implementation.
64   * @author philipp
65   *
66   */
67  public abstract class AbstractI18nContentSupport implements I18nContentSupport {
68  
69      private static final Logger log = LoggerFactory.getLogger(AbstractI18nContentSupport.class);
70  
71      /**
72       * The content is served for this locale if the the content is not available for the current locale.
73       */
74      private Locale fallbackLocale = new Locale("en");
75  
76      /**
77       * If no locale can be determined the default locale will be set. If no default locale is defined the fallback locale is used.
78       */
79      protected Locale defaultLocale;
80  
81      private boolean enabled = false;
82  
83      /**
84       * The active locales.
85       */
86      private final Map<String, Locale> locales = new LinkedHashMap<String, Locale>();
87  
88      @Override
89      public Locale getLocale() {
90          Locale locale = null;
91          if(MgnlContext.getWebContextOrNull() != null){
92              locale = MgnlContext.getAggregationState().getLocale();
93          }
94          if (locale == null) {
95              return fallbackLocale;
96          }
97          return locale;
98      }
99  
100     @Override
101     public void setLocale(Locale locale) {
102         MgnlContext.getAggregationState().setLocale(locale);
103     }
104 
105     @Override
106     public Locale getFallbackLocale() {
107         return this.fallbackLocale;
108     }
109 
110     @Override
111     public void setFallbackLocale(Locale fallbackLocale) {
112         this.fallbackLocale = fallbackLocale;
113     }
114 
115     /**
116      * Returns the closest locale for which {@link #isLocaleSupported(Locale)} is true.
117      * <ul>
118      * <li>If the locale has a country specified (fr_CH) the locale without country (fr) will be returned.</li>
119      * <li>If the locale has no country specified (fr) the first locale with the same language but specific country (fr_CH) will be returned.</li>
120      * <li>If this fails the fall-back locale is returned</li>
121      * </ul>
122      * Warning: if you have configured both (fr and fr_CH) this method will jiter between this two values.
123      */
124     protected Locale getNextLocale(Locale locale) {
125         // if this locale defines a country
126         if(StringUtils.isNotEmpty(locale.getCountry())){
127             // try to use the language only
128             Locale langOnlyLocale = new Locale(locale.getLanguage());
129             if(isLocaleSupported(langOnlyLocale)){
130                 return langOnlyLocale;
131             }
132         }
133         // try to find a locale with the same language (ignore the country)
134         for (Iterator<Locale> iter = getLocales().iterator(); iter.hasNext();) {
135             Locale otherCountryLocale = iter.next();
136             // same lang, but not the same country as well or we end up in the loop
137             if(locale.getLanguage().equals(otherCountryLocale.getLanguage()) && !locale.equals(otherCountryLocale)){
138                 return otherCountryLocale;
139             }
140         }
141         return getFallbackLocale();
142     }
143 
144     /**
145      * Extracts the language from the uri.
146      */
147     @Override
148     public Locale determineLocale() {
149         Locale locale;
150 
151         locale = onDetermineLocale();
152 
153         // depending on the implementation the returned local can be null (not defined)
154         if(locale == null){
155             locale = getDefaultLocale();
156         }
157         // if we have a locale but it is not supported we try to get the closest locale
158         if(!isLocaleSupported(locale)){
159             locale = getNextLocale(locale);
160         }
161         // instead of returning the content fallback language
162         // we are going to return the default locale which might differ
163         if(locale.equals(getFallbackLocale())){
164             locale = getDefaultLocale();
165         }
166         return locale;
167     }
168 
169     protected abstract Locale onDetermineLocale();
170 
171     protected static Locale determineLocalFromString(String localeStr) {
172         if(StringUtils.isNotEmpty(localeStr)){
173             String[] localeArr = StringUtils.split(localeStr, "_");
174             if(localeArr.length ==1){
175                 return new Locale(localeArr[0]);
176             }
177             else if(localeArr.length == 2){
178                 return new Locale(localeArr[0],localeArr[1]);
179             }
180         }
181         return null;
182     }
183 
184     @Override
185     public String toI18NURI(String uri) {
186         if (!isEnabled()) {
187             return uri;
188         }
189         Locale locale = getLocale();
190         if (isLocaleSupported(locale)) {
191             return toI18NURI(uri, locale);
192         }
193         return uri;
194     }
195 
196     protected abstract String toI18NURI(String uri, Locale locale);
197 
198     /**
199      * Removes the prefix.
200      */
201     @Override
202     public String toRawURI(String i18nURI) {
203         if (!isEnabled()) {
204             return i18nURI;
205         }
206 
207         Locale locale = getLocale();
208         if (isLocaleSupported(locale)) {
209             return toRawURI(i18nURI, locale);
210         }
211         return i18nURI;
212     }
213 
214     protected abstract String toRawURI(String i18nURI, Locale locale);
215 
216     @Override
217     public NodeData getNodeData(Content node, String name, Locale locale) throws RepositoryException {
218         String nodeDataName = name + "_" + locale;
219         if (node.hasNodeData(nodeDataName)) {
220             return node.getNodeData(nodeDataName);
221         }
222         return null;
223     }
224 
225     /**
226      * Returns the nodedata with the name &lt;name&gt;_&lt;current language&gt; or &lt;name&gt;_&lt;fallback language&gt
227      * otherwise returns &lt;name&gt;.
228      */
229     @Override
230     public NodeData getNodeData(Content node, String name) {
231         NodeData nd = null;
232 
233         if (isEnabled()) {
234             try {
235                 // test for the current language
236                 Locale locale = getLocale();
237                 Set<Locale> checkedLocales = new HashSet<Locale>();
238 
239                 // getNextContentLocale() returns null once the end of the locale chain is reached
240                 while(locale != null){
241                     nd = getNodeData(node, name, locale);
242                     if (!isEmpty(nd)) {
243                         return nd;
244                     }
245                     checkedLocales.add(locale);
246                     locale = getNextContentLocale(locale, checkedLocales);
247                 }
248             }
249             catch (RepositoryException e) {
250                 log.error("can't read i18n nodeData " + name + " from node " + node, e);
251             }
252         }
253 
254         // return the node data
255         return node.getNodeData(name);
256     }
257 
258     @Override
259     public Node getNode(Node node, String name) throws RepositoryException {
260         if (isEnabled()) {
261 
262             try {
263                 // test for the current language
264                 Locale locale = getLocale();
265                 Set<Locale> checkedLocales = new HashSet<Locale>();
266 
267                 // getNextContentLocale() returns null once the end of the locale chain is reached
268                 while(locale != null){
269                     String localeSpecificChildName = name + "_" + locale;
270                     if (node.hasNode(localeSpecificChildName)) {
271                         return node.getNode(localeSpecificChildName);
272                     }
273                     checkedLocales.add(locale);
274                     locale = getNextContentLocale(locale, checkedLocales);
275                 }
276             }
277             catch (RepositoryException e) {
278                 log.error("can't read i18n node " + name + " from node " + node, e);
279             }
280         }
281 
282         return node.getNode(name);
283     }
284 
285     @Override
286     public boolean hasProperty(Node node, String name) throws RepositoryException {
287         if (!isEnabled()) {
288             return node.hasProperty(name);
289         }
290         try {
291             // get property using all the rules in getProperty method. If not found, then it doesn't exist.
292             getProperty(node, name);
293         } catch (RepositoryException e) {
294             return false;
295         }
296         return true;
297     }
298 
299     @Override
300     public Property getProperty(Node node, String name) throws RepositoryException {
301         if (!isEnabled()) {
302             return node.getProperty(name);
303         }
304         try {
305             // test for the current language
306             Locale locale = getLocale();
307             Set<Locale> checkedLocales = new HashSet<Locale>();
308 
309             // getNextContentLocale() returns null once the end of the locale chain is reached
310             while (locale != null) {
311                 Property property = getProperty(node, name, locale);
312                 if (!isEmpty(property)) {
313                     return property;
314                 }
315                 checkedLocales.add(locale);
316                 locale = getNextContentLocale(locale, checkedLocales);
317             }
318         } catch (RepositoryException e) {
319             log.error("can't read i18n nodeData " + name + " from node " + node, e);
320         }
321 
322         // return the node data
323         return node.getProperty(name);
324     }
325 
326     @Override
327     public Property getProperty(Node node, String name, Locale locale) throws RepositoryException {
328         String propName = name + "_" + locale;
329         if (node.hasProperty(propName)) {
330             return node.getProperty(propName);
331         }
332         return null;
333     }
334 
335     /**
336      * Uses {@link #getNextLocale(Locale)} to find the next locale. If the returned locale is in the
337      * checkedLocales set it falls back to the fall-back locale. If the fall-back locale itself is
338      * passed to the method, the method returns null to signal the end of the chain.
339      */
340     protected Locale getNextContentLocale(Locale locale, Set<Locale> checkedLocales) {
341         if(locale.equals(getFallbackLocale())){
342             return null;
343         }
344         Locale candidate = getNextLocale(locale);
345         if(!checkedLocales.contains(candidate)){
346             return candidate;
347         }
348         return getFallbackLocale();
349     }
350 
351     @Override
352     public boolean isEnabled() {
353         return this.enabled;
354     }
355 
356     public void setEnabled(boolean enabled) {
357         this.enabled = enabled;
358     }
359 
360     @Override
361     public Collection<Locale> getLocales() {
362         return this.locales.values();
363     }
364 
365     public void addLocale(LocaleDefinition ld) {
366         if (ld.isEnabled()) {
367             this.locales.put(ld.getId(), ld.getLocale());
368         }
369     }
370 
371     protected boolean isLocaleSupported(Locale locale) {
372         return locale != null && locales.containsKey(locale.toString());
373     }
374 
375     /**
376      * Checks if the nodedata field is empty.
377      * 
378      * @deprecated since 4.5.4. Use {@link #isEmpty(Property)} instead.
379      */
380     @Deprecated
381     protected boolean isEmpty(NodeData nd) {
382         if (nd != null && nd.isExist()) {
383             // TODO use a better way to find out if it is empty
384             return StringUtils.isEmpty(NodeDataUtil.getValueString(nd));
385         }
386         return true;
387     }
388 
389     /**
390      * Checks if the property field is empty.
391      */
392     protected boolean isEmpty(Property nd) {
393         if (nd != null) {
394             return StringUtils.isEmpty(PropertyUtil.getValueString(nd));
395         }
396         return true;
397     }
398 
399     public Locale getDefaultLocale() {
400         if(this.defaultLocale == null){
401             return getFallbackLocale();
402         }
403         return this.defaultLocale;
404     }
405 
406     public void setDefaultLocale(Locale defaultLocale) {
407         this.defaultLocale = defaultLocale;
408     }
409 }