View Javadoc

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