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