View Javadoc
1   /**
2    * This file Copyright (c) 2008-2018 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.context.MgnlContext;
37  import info.magnolia.jcr.util.PropertyUtil;
38  
39  import java.util.Collection;
40  import java.util.HashSet;
41  import java.util.Iterator;
42  import java.util.LinkedHashMap;
43  import java.util.Locale;
44  import java.util.Map;
45  import java.util.Set;
46  
47  import javax.jcr.Node;
48  import javax.jcr.Property;
49  import javax.jcr.RepositoryException;
50  
51  import org.apache.commons.lang3.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   */
62  public abstract class AbstractI18nContentSupport implements I18nContentSupport {
63  
64      private static final Logger log = LoggerFactory.getLogger(AbstractI18nContentSupport.class);
65  
66      /**
67       * The content is served for this locale if the the content is not available for the current locale.
68       */
69      private Locale fallbackLocale = new Locale("en");
70  
71      /**
72       * If no locale can be determined the default locale will be set. If no default locale is defined the fallback locale is used.
73       */
74      protected Locale defaultLocale;
75  
76      private boolean enabled = false;
77  
78      /**
79       * The active locales.
80       */
81      private final Map<String, Locale> locales = new LinkedHashMap<String, Locale>();
82  
83      @Override
84      public Locale getLocale() {
85          Locale locale = null;
86          if (MgnlContext.getWebContextOrNull() != null) {
87              locale = MgnlContext.getAggregationState().getLocale();
88          }
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         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             } else if (localeArr.length == 2) {
167                 return new Locale(localeArr[0], localeArr[1]);
168             }
169         }
170         return null;
171     }
172 
173     @Override
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     @Override
191     public String toRawURI(String i18nURI) {
192         if (!isEnabled()) {
193             return i18nURI;
194         }
195 
196         Locale locale = getLocale();
197         if (isLocaleSupported(locale)) {
198             return toRawURI(i18nURI, locale);
199         }
200         return i18nURI;
201     }
202 
203     protected abstract String toRawURI(String i18nURI, Locale locale);
204 
205     @Override
206     public Node getNode(Node node, String name) throws RepositoryException {
207         if (isEnabled()) {
208 
209             try {
210                 // test for the current language
211                 Locale locale = getLocale();
212                 Set<Locale> checkedLocales = new HashSet<Locale>();
213 
214                 // getNextContentLocale() returns null once the end of the locale chain is reached
215                 while (locale != null) {
216                     String localeSpecificChildName = name + "_" + locale;
217                     if (node.hasNode(localeSpecificChildName)) {
218                         return node.getNode(localeSpecificChildName);
219                     }
220                     checkedLocales.add(locale);
221                     locale = getNextContentLocale(locale, checkedLocales);
222                 }
223             } catch (RepositoryException e) {
224                 log.error("can't read i18n node {} from node {}", name, node, e);
225             }
226         }
227 
228         return node.getNode(name);
229     }
230 
231     @Override
232     public boolean hasProperty(Node node, String name) throws RepositoryException {
233         if (!isEnabled()) {
234             return node.hasProperty(name);
235         }
236         try {
237             // get property using all the rules in getProperty method. If not found, then it doesn't exist.
238             getProperty(node, name);
239         } catch (RepositoryException e) {
240             return false;
241         }
242         return true;
243     }
244 
245     @Override
246     public Property getProperty(Node node, String name) throws RepositoryException {
247         if (!isEnabled()) {
248             return node.getProperty(name);
249         }
250         try {
251             // test for the current language
252             Locale locale = getLocale();
253             Set<Locale> checkedLocales = new HashSet<Locale>();
254 
255             // getNextContentLocale() returns null once the end of the locale chain is reached
256             while (locale != null) {
257                 Property property = getProperty(node, name, locale);
258                 if (!isEmpty(property)) {
259                     return property;
260                 } else if (locale.equals(getDefaultLocale()) && node.hasProperty(name)) {
261                     return node.getProperty(name);
262                 }
263                 checkedLocales.add(locale);
264                 locale = getNextContentLocale(locale, checkedLocales);
265             }
266         } catch (RepositoryException e) {
267             log.error("can't read i18n property {} from node {}", name, node, e);
268         }
269 
270         // return the node data
271         return node.getProperty(name);
272     }
273 
274     @Override
275     public Property getProperty(Node node, String name, Locale locale) throws RepositoryException {
276         String propName = name + "_" + locale;
277         if (node.hasProperty(propName)) {
278             return node.getProperty(propName);
279         }
280         return null;
281     }
282 
283     /**
284      * Uses {@link #getNextLocale(Locale)} to find the next locale. If the returned locale is in the
285      * checkedLocales set it falls back to the fall-back locale. If the fall-back locale itself is
286      * passed to the method, the method returns null to signal the end of the chain.
287      */
288     protected Locale getNextContentLocale(Locale locale, Set<Locale> checkedLocales) {
289         if (locale.equals(getFallbackLocale())) {
290             return null;
291         }
292         Locale candidate = getNextLocale(locale);
293         if (!checkedLocales.contains(candidate)) {
294             return candidate;
295         }
296         return getFallbackLocale();
297     }
298 
299     @Override
300     public boolean isEnabled() {
301         return this.enabled;
302     }
303 
304     public void setEnabled(boolean enabled) {
305         this.enabled = enabled;
306     }
307 
308     @Override
309     public Collection<Locale> getLocales() {
310         return this.locales.values();
311     }
312 
313     public void setLocales(Collection<LocaleDefinition> locales) { //used by M2B
314         locales.forEach(this::addLocale);
315     }
316 
317     /**
318      * @deprecated since 6.1. Use {@link #setLocales(java.util.Collection)} instead.
319      */
320     @Deprecated
321     public void setLocales(Map<String, Locale> locales) {
322         this.locales.putAll(locales);
323     }
324 
325     public void addLocale(LocaleDefinition ld) { //used by N2B
326         if (ld.isEnabled()) {
327             this.locales.put(ld.getId(), ld.getLocale());
328         }
329     }
330 
331     protected boolean isLocaleSupported(Locale locale) {
332         return locale != null && locales.containsKey(locale.toString());
333     }
334 
335     /**
336      * Checks if the property field is empty.
337      */
338     protected boolean isEmpty(Property property) {
339         try {
340             if (property != null) {
341                 return (property.isMultiple()) ? property.getValues().length == 0 : StringUtils.isEmpty(PropertyUtil.getValueString(property));
342             }
343         } catch (RepositoryException e) {
344             log.warn("Can't read value from property {}", property, e);
345         }
346         return true;
347     }
348 
349     @Override
350     public Locale getDefaultLocale() {
351         if (this.defaultLocale == null) {
352             return getFallbackLocale();
353         }
354         return this.defaultLocale;
355     }
356 
357     public void setDefaultLocale(Locale defaultLocale) {
358         this.defaultLocale = defaultLocale;
359     }
360 }