001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.i18n;
029
030import org.opencms.file.CmsObject;
031import org.opencms.file.CmsProject;
032import org.opencms.file.CmsPropertyDefinition;
033import org.opencms.file.CmsResource;
034import org.opencms.file.CmsUser;
035import org.opencms.main.CmsEvent;
036import org.opencms.main.CmsException;
037import org.opencms.main.CmsLog;
038import org.opencms.main.I_CmsEventListener;
039import org.opencms.main.OpenCms;
040import org.opencms.monitor.CmsMemoryMonitor;
041import org.opencms.util.CmsStringUtil;
042import org.opencms.xml.I_CmsXmlDocument;
043
044import java.io.InputStream;
045import java.util.ArrayList;
046import java.util.Collections;
047import java.util.Iterator;
048import java.util.List;
049import java.util.Locale;
050import java.util.TimeZone;
051
052import javax.servlet.http.HttpServletRequest;
053
054import org.apache.commons.io.IOUtils;
055import org.apache.commons.lang3.LocaleUtils;
056import org.apache.commons.lang3.StringUtils;
057import org.apache.commons.logging.Log;
058
059import com.cybozu.labs.langdetect.DetectorFactory;
060
061/**
062 * Manages the locales configured for this OpenCms installation.<p>
063 *
064 * Locale configuration is done in the configuration file <code>opencms-system.xml</code>
065 * in the <code>opencms/system/internationalization</code> node and it's sub-nodes.<p>
066 *
067 * @since 6.0.0
068 */
069public class CmsLocaleManager implements I_CmsEventListener {
070
071    /** Runtime property name for locale handler. */
072    public static final String LOCALE_HANDLER = "class_locale_handler";
073
074    /** Locale to use for storing locale-independent XML contents. */
075    public static final Locale MASTER_LOCALE = Locale.ENGLISH;
076
077    /** Request parameter to force encoding selection. */
078    public static final String PARAMETER_ENCODING = "__encoding";
079
080    /** Request parameter to force locale selection. */
081    public static final String PARAMETER_LOCALE = "__locale";
082
083    /** The log object for this class. */
084    private static final Log LOG = CmsLog.getLog(CmsLocaleManager.class);
085
086    /** The default locale, this is the first configured locale. */
087    private static Locale m_defaultLocale = Locale.ENGLISH;
088
089    /**
090     * Required for setting the default locale on the first possible time.<p>
091     */
092    static {
093        setDefaultLocale();
094    }
095
096    /** The set of available locale names. */
097    private List<Locale> m_availableLocales;
098
099    /** The default locale names (must be a subset of the available locale names). */
100    private List<Locale> m_defaultLocales;
101
102    /** Indicates if the locale manager is fully initialized. */
103    private boolean m_initialized;
104
105    /** The configured locale handler. */
106    private I_CmsLocaleHandler m_localeHandler;
107
108    /** The string value of the 'reuse-elements' option. */
109    private String m_reuseElementsStr;
110
111    /** The OpenCms default time zone. */
112    private TimeZone m_timeZone;
113
114    /**
115     * Initializes a new CmsLocaleManager, called from the configuration.<p>
116     */
117    public CmsLocaleManager() {
118
119        setDefaultLocale();
120        setTimeZone("GMT");
121        m_availableLocales = new ArrayList<Locale>();
122        m_defaultLocales = new ArrayList<Locale>();
123        m_localeHandler = new CmsDefaultLocaleHandler();
124        if (CmsLog.INIT.isInfoEnabled()) {
125            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_START_0));
126        }
127        // register this object as event listener
128        OpenCms.addCmsEventListener(this, new int[] {I_CmsEventListener.EVENT_CLEAR_CACHES});
129    }
130
131    /**
132     * Initializes a new CmsLocaleManager, used for OpenCms runlevel 1 (unit tests) only.<p>
133     *
134     * @param defaultLocale the default locale to use
135     */
136    public CmsLocaleManager(Locale defaultLocale) {
137
138        setDefaultLocale();
139        setTimeZone("GMT");
140        m_initialized = false;
141
142        m_availableLocales = new ArrayList<Locale>();
143        m_defaultLocales = new ArrayList<Locale>();
144        m_localeHandler = new CmsDefaultLocaleHandler();
145
146        m_defaultLocale = defaultLocale;
147        m_defaultLocales.add(defaultLocale);
148        m_availableLocales.add(defaultLocale);
149    }
150
151    /**
152     * Returns the default locale configured in <code>opencms-system.xml</code>,
153     * that is the first locale from the list provided
154     * in the <code>opencms/system/internationalization/localesdefault</code> node.<p>
155     *
156     * @return the default locale configured in <code>opencms-system.xml</code>
157     */
158    public static Locale getDefaultLocale() {
159
160        return m_defaultLocale;
161    }
162
163    /**
164     * Returns a locale created from the given full name.<p>
165     *
166     * The full name must consist of language code,
167     * country code(optional), variant(optional) separated by "_".<p>
168     *
169     * This method will always return a valid Locale!
170     * If the provided locale name is not valid (i.e. leads to an Exception
171     * when trying to create the Locale, then the configured default Locale is returned.<p>
172     *
173     * @param localeName the full locale name
174     * @return the locale or <code>null</code> if not available
175     */
176    public static Locale getLocale(String localeName) {
177
178        if (CmsStringUtil.isEmpty(localeName)) {
179            return getDefaultLocale();
180        }
181
182        Locale locale = null;
183        if (OpenCms.getMemoryMonitor() != null) {
184            // this may be used AFTER shutdown
185            locale = OpenCms.getMemoryMonitor().getCachedLocale(localeName);
186        }
187        if (locale != null) {
188            return locale;
189        }
190        try {
191            if ("all".equals(localeName)) {
192                locale = new Locale("all");
193            } else {
194                locale = LocaleUtils.toLocale(localeName);
195            }
196        } catch (Throwable t) {
197            LOG.debug(Messages.get().getBundle().key(Messages.LOG_CREATE_LOCALE_FAILED_1, localeName), t);
198            // map this error to the default locale
199            locale = getDefaultLocale();
200        }
201        if (OpenCms.getMemoryMonitor() != null) {
202            // this may be used AFTER shutdown
203            OpenCms.getMemoryMonitor().cacheLocale(localeName, locale);
204        }
205        return locale;
206    }
207
208    /**
209     * Returns the locale names from the given List of locales as a comma separated String.<p>
210     *
211     * For example, if the input List contains <code>{@link Locale#ENGLISH}</code> and
212     * <code>{@link Locale#GERMANY}</code>, the result will be <code>"en, de_DE"</code>.<p>
213     *
214     * An empty String is returned if the input is <code>null</code>, or contains no elements.<p>
215     *
216     * @param locales the locales to generate a String from
217     *
218     * @return the locale names from the given List of locales as a comma separated String
219     */
220    public static String getLocaleNames(List<Locale> locales) {
221
222        StringBuffer result = new StringBuffer();
223        if (locales != null) {
224            Iterator<Locale> i = locales.iterator();
225            while (i.hasNext()) {
226                result.append(i.next().toString());
227                if (i.hasNext()) {
228                    result.append(", ");
229                }
230            }
231        }
232        return result.toString();
233    }
234
235    /**
236     * Returns a List of locales from an array of locale names.<p>
237     *
238     * @param localeNames array of locale names
239     * @return a List of locales derived from the given locale names
240     */
241    public static List<Locale> getLocales(List<String> localeNames) {
242
243        List<Locale> result = new ArrayList<Locale>(localeNames.size());
244        for (int i = 0; i < localeNames.size(); i++) {
245            result.add(getLocale(localeNames.get(i).toString().trim()));
246        }
247        return result;
248    }
249
250    /**
251     * Returns a List of locales from a comma-separated string of locale names.<p>
252     *
253     * @param localeNames a comma-separated string of locale names
254     * @return a List of locales derived from the given locale names
255     */
256    public static List<Locale> getLocales(String localeNames) {
257
258        if (localeNames == null) {
259            return null;
260        }
261        return getLocales(CmsStringUtil.splitAsList(localeNames, ','));
262    }
263
264    /**
265     * <p>
266     * Extends a base name with locale suffixes and yields the list of extended names
267     * in the order they typically should be used according to the given locale.
268     * </p>
269     * <p>
270     * <strong>Example</strong>: If you have base name <code>base</code> and the locale with {@link String} representation <code>de_DE</code>,
271     * the result will be (assuming <code>en</code> is the default locale):
272     * <ul>
273     *  <li> for <code>wantBase == false</code> and <code>defaultAsBase == false</code>: <code> [base_de_DE, base_de]</li>
274     *  <li> for <code>wantBase == true</code> and <code>defaultAsBase == false</code>: <code> [base_de_DE, base_de, base]</li>
275     *  <li> for <code>wantBase == false</code> and <code>defaultAsBase == true</code>: <code> [base_de_DE, base_de, base_en]</li>
276     *  <li> for <code>wantBase == true</code> and <code>defaultAsBase == true</code>: <code> [base_de_DE, base_de, base, base_en]</li>
277     * </ul>
278     * If the requested locale is a variant of the default locale,
279     * the list will never contain the default locale as last element because it appears already earlier.
280     *
281     * @param basename the base name that should be extended by locale post-fixes
282     * @param locale the locale for which the list of extensions should be generated.
283     * @param wantBase flag, indicating if the base name without locale post-fix should be yielded as well.
284     * @param defaultAsBase flag, indicating, if the variant with the default locale should be used as base.
285     * @return the list of locale variants of the base name in the order they should be used.
286     */
287    public static List<String> getLocaleVariants(
288        String basename,
289        Locale locale,
290        boolean wantBase,
291        boolean defaultAsBase) {
292
293        List<String> result = new ArrayList<String>();
294        if (null == basename) {
295            return result;
296        } else {
297            String localeString = null == locale ? "" : "_" + locale.toString();
298            boolean wantDefaultAsBase = defaultAsBase
299                && !(localeString.startsWith("_" + getDefaultLocale().toString()));
300            while (!localeString.isEmpty()) {
301                result.add(basename + localeString);
302                localeString = localeString.substring(0, localeString.lastIndexOf('_'));
303            }
304            if (wantBase) {
305                result.add(basename);
306            }
307            if (wantDefaultAsBase) {
308                result.add(basename + "_" + getDefaultLocale().toString());
309            }
310            return result;
311        }
312    }
313
314    /**
315     * Utility method to get the primary locale for a given resource.<p>
316     *
317     * @param cms the current CMS context
318     * @param res the resource for which the locale should be retrieved
319     *
320     * @return the primary locale
321     */
322    public static Locale getMainLocale(CmsObject cms, CmsResource res) {
323
324        CmsLocaleManager localeManager = OpenCms.getLocaleManager();
325        List<Locale> defaultLocales = null;
326        // must switch project id in stored Admin context to match current project
327        String defaultNames = null;
328        try {
329            defaultNames = cms.readPropertyObject(res, CmsPropertyDefinition.PROPERTY_LOCALE, true).getValue();
330        } catch (CmsException e) {
331            LOG.warn(e.getLocalizedMessage(), e);
332        }
333        if (defaultNames != null) {
334            defaultLocales = localeManager.getAvailableLocales(defaultNames);
335        }
336
337        if ((defaultLocales == null) || (defaultLocales.isEmpty())) {
338            // no default locales could be determined
339            defaultLocales = localeManager.getDefaultLocales();
340        }
341        Locale locale;
342        // return the first default locale name
343        if ((defaultLocales != null) && (defaultLocales.size() > 0)) {
344            locale = defaultLocales.get(0);
345        } else {
346            locale = CmsLocaleManager.getDefaultLocale();
347        }
348        return locale;
349    }
350
351    /**
352     * Returns the content encoding set for the given resource.<p>
353     *
354     * The content encoding is controlled by the property {@link CmsPropertyDefinition#PROPERTY_CONTENT_ENCODING},
355     * which can be set on the resource or on a parent folder for all resources in this folder.<p>
356     *
357     * In case no encoding has been set, the default encoding from
358     * {@link org.opencms.main.CmsSystemInfo#getDefaultEncoding()} is returned.<p>
359     *
360     * @param cms the current OpenCms user context
361     * @param res the resource to read the encoding for
362     *
363     * @return the content encoding set for the given resource
364     */
365    public static final String getResourceEncoding(CmsObject cms, CmsResource res) {
366
367        String encoding = null;
368        // get the encoding
369        try {
370            encoding = cms.readPropertyObject(res, CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING, true).getValue();
371            if (encoding != null) {
372                encoding = CmsEncoder.lookupEncoding(encoding.trim(), encoding);
373            }
374        } catch (CmsException e) {
375            if (LOG.isInfoEnabled()) {
376                LOG.info(Messages.get().getBundle().key(Messages.ERR_READ_ENCODING_PROP_1, res.getRootPath()), e);
377            }
378        }
379        if (encoding == null) {
380            encoding = OpenCms.getSystemInfo().getDefaultEncoding();
381        }
382        return encoding;
383    }
384
385    /**
386     * Sets the default locale of the Java VM to <code>{@link Locale#ENGLISH}</code> if the
387     * current default has any other language then English set.<p>
388     *
389     * This is required because otherwise the default (English) resource bundles
390     * would not be displayed for the English locale if a translated default locale exists.<p>
391     *
392     * Here's an example of how this issues shows up:
393     * On a German server, the default locale usually is <code>{@link Locale#GERMAN}</code>.
394     * All English translations for OpenCms are located in the "default" message files, for example
395     * <code>org.opencms.i18n.message.properties</code>. If the German localization is installed, it will be
396     * located in <code>org.opencms.i18n.message_de.properties</code>. If user has English selected
397     * as his locale, the default Java lookup mechanism first tries to find
398     * <code>org.opencms.i18n.message_en.properties</code>. However, this file does not exist, since the
399     * English localization is kept in the default file. Next, the Java lookup mechanism tries to find the servers
400     * default locale, which in this example is German. Since there is a German message file, the Java lookup mechanism
401     * is finished and uses this German localization, not the default file. Therefore the
402     * user get the German localization, not the English one.
403     * Setting the default locale explicitly to English avoids this issue.<p>
404     */
405    private static void setDefaultLocale() {
406
407        // set the default locale to english
408        // this is required because otherwise the default (english) resource bundles
409        // would not be displayed for the english locale if a translated locale exists
410
411        Locale oldLocale = Locale.getDefault();
412        if (!(Locale.ENGLISH.getLanguage().equals(oldLocale.getLanguage()))) {
413            // default language is not English
414            try {
415                Locale.setDefault(Locale.ENGLISH);
416                if (CmsLog.INIT.isInfoEnabled()) {
417                    CmsLog.INIT.info(
418                        Messages.get().getBundle().key(Messages.INIT_I18N_DEFAULT_LOCALE_2, Locale.ENGLISH, oldLocale));
419                }
420            } catch (Exception e) {
421                // any Exception: the locale has not been changed, so there may be issues with the English
422                // localization but OpenCms will run in general
423                CmsLog.INIT.error(
424                    Messages.get().getBundle().key(
425                        Messages.LOG_UNABLE_TO_SET_DEFAULT_LOCALE_2,
426                        Locale.ENGLISH,
427                        oldLocale),
428                    e);
429            }
430        } else {
431            if (CmsLog.INIT.isInfoEnabled()) {
432                CmsLog.INIT.info(
433                    Messages.get().getBundle().key(Messages.INIT_I18N_KEEPING_DEFAULT_LOCALE_1, oldLocale));
434            }
435        }
436
437        // initialize the static member with the new default
438        m_defaultLocale = Locale.getDefault();
439    }
440
441    /**
442     * Adds a locale to the list of available locales.<p>
443     *
444     * @param localeName the locale to add
445     */
446    public void addAvailableLocale(String localeName) {
447
448        Locale locale = getLocale(localeName);
449        // add full variation (language / country / variant)
450        if (!m_availableLocales.contains(locale)) {
451            m_availableLocales.add(locale);
452            if (CmsLog.INIT.isInfoEnabled()) {
453                CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_ADD_LOCALE_1, locale));
454            }
455        }
456        // add variation with only language and country
457        locale = new Locale(locale.getLanguage(), locale.getCountry());
458        if (!m_availableLocales.contains(locale)) {
459            m_availableLocales.add(locale);
460            if (CmsLog.INIT.isInfoEnabled()) {
461                CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_ADD_LOCALE_1, locale));
462            }
463        }
464        // add variation with language only
465        locale = new Locale(locale.getLanguage());
466        if (!m_availableLocales.contains(locale)) {
467            m_availableLocales.add(locale);
468            if (CmsLog.INIT.isInfoEnabled()) {
469                CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_ADD_LOCALE_1, locale));
470            }
471        }
472    }
473
474    /**
475     * Adds a locale to the list of default locales.<p>
476     *
477     * @param localeName the locale to add
478     */
479    public void addDefaultLocale(String localeName) {
480
481        Locale locale = getLocale(localeName);
482        if (!m_defaultLocales.contains(locale)) {
483            m_defaultLocales.add(locale);
484            if (CmsLog.INIT.isInfoEnabled()) {
485                CmsLog.INIT.info(
486                    Messages.get().getBundle().key(
487                        Messages.INIT_I18N_CONFIG_DEFAULT_LOCALE_2,
488                        new Integer(m_defaultLocales.size()),
489                        locale));
490
491            }
492        }
493    }
494
495    /**
496     * Implements the CmsEvent interface,
497     * the locale manager the events to clear
498     * the list of cached keys .<p>
499     *
500     * @param event CmsEvent that has occurred
501     */
502    public void cmsEvent(CmsEvent event) {
503
504        switch (event.getType()) {
505            case I_CmsEventListener.EVENT_CLEAR_CACHES:
506                clearCaches();
507                break;
508            default: // no operation
509        }
510    }
511
512    /**
513     * Returns the list of available {@link Locale}s configured in <code>opencms-system.xml</code>,
514     * in the <code>opencms/system/internationalization/localesconfigured</code> node.<p>
515     *
516     * The list of configured available locales contains all locales that are allowed to be used in the VFS,
517     * for example as languages in XML content files.<p>
518     *
519     * The available locales are a superset of the default locales, see {@link #getDefaultLocales()}.<p>
520     *
521     * It's possible to reduce the system default by setting the propery
522     * <code>{@link CmsPropertyDefinition#PROPERTY_AVAILABLE_LOCALES}</code>
523     * to a comma separated list of locale names. However, you can not add new available locales,
524     * only remove from the configured list.<p>
525     *
526     * @return the list of available locale names, e.g. <code>en, de</code>
527     *
528     * @see #getDefaultLocales()
529     */
530    public List<Locale> getAvailableLocales() {
531
532        return Collections.unmodifiableList(m_availableLocales);
533    }
534
535    /**
536     * Returns an array of available locale names for the given resource.<p>
537     *
538     * @param cms the current cms permission object
539     * @param resource the resource
540     *
541     * @return an array of available locale names
542     *
543     * @see #getAvailableLocales()
544     */
545    public List<Locale> getAvailableLocales(CmsObject cms, CmsResource resource) {
546
547        String availableNames = null;
548        try {
549            availableNames = cms.readPropertyObject(
550                resource,
551                CmsPropertyDefinition.PROPERTY_AVAILABLE_LOCALES,
552                true).getValue();
553        } catch (CmsException exc) {
554            LOG.debug("Could not read available locales property for resource " + resource.getRootPath(), exc);
555        }
556
557        List<Locale> result = null;
558        if (availableNames != null) {
559            result = getAvailableLocales(availableNames);
560        }
561        if ((result == null) || (result.size() == 0)) {
562            return Collections.unmodifiableList(m_availableLocales);
563        } else {
564            return result;
565        }
566    }
567
568    /**
569     * Returns an array of available locale names for the given resource.<p>
570     *
571     * @param cms the current cms permission object
572     * @param resourceName the name of the resource
573     *
574     * @return an array of available locale names
575     *
576     * @see #getAvailableLocales()
577     */
578    public List<Locale> getAvailableLocales(CmsObject cms, String resourceName) {
579
580        String availableNames = null;
581        try {
582            availableNames = cms.readPropertyObject(
583                resourceName,
584                CmsPropertyDefinition.PROPERTY_AVAILABLE_LOCALES,
585                true).getValue();
586        } catch (CmsException exc) {
587            LOG.debug("Could not read available locales property for resource " + resourceName, exc);
588        }
589
590        List<Locale> result = null;
591        if (availableNames != null) {
592            result = getAvailableLocales(availableNames);
593        }
594        if ((result == null) || (result.size() == 0)) {
595            return Collections.unmodifiableList(m_availableLocales);
596        } else {
597            return result;
598        }
599    }
600
601    /**
602     * Returns a List of available locales from a comma separated string of locale names.<p>
603     *
604     * All names are filtered against the allowed available locales
605     * configured in <code>opencms-system.xml</code>.<P>
606     *
607     * @param names a comma-separated String of locale names
608     * @return List of locales created from the given locale names
609     *
610     * @see #getAvailableLocales()
611     */
612    public List<Locale> getAvailableLocales(String names) {
613
614        return checkLocaleNames(getLocales(names));
615    }
616
617    /**
618     * Returns the best available locale present in the given XML content, or the default locale.<p>
619     *
620     * @param cms the current OpenCms user context
621     * @param resource the resource
622     * @param content the XML content
623     *
624     * @return the locale
625     */
626    public Locale getBestAvailableLocaleForXmlContent(CmsObject cms, CmsResource resource, I_CmsXmlDocument content) {
627
628        Locale locale = getDefaultLocale(cms, resource);
629        if (!content.hasLocale(locale)) {
630            // if the requested locale is not available, get the first matching default locale,
631            // or the first matching available locale
632            boolean foundLocale = false;
633            if (content.getLocales().size() > 0) {
634                List<Locale> locales = getDefaultLocales(cms, resource);
635                for (Locale defaultLocale : locales) {
636                    if (content.hasLocale(defaultLocale)) {
637                        locale = defaultLocale;
638                        foundLocale = true;
639                        break;
640                    }
641                }
642                if (!foundLocale) {
643                    locales = getAvailableLocales(cms, resource);
644                    for (Locale availableLocale : locales) {
645                        if (content.hasLocale(availableLocale)) {
646                            locale = availableLocale;
647                            foundLocale = true;
648                            break;
649                        }
650                    }
651                }
652            }
653        }
654        return locale;
655    }
656
657    /**
658     * Tries to find the given requested locale (eventually simplified) in the collection of available locales,
659     * if the requested locale is not found it will return the first match from the given list of default locales.<p>
660     *
661     * @param requestedLocale the requested locale, if this (or a simplified version of it) is available it will be returned
662     * @param defaults a list of default locales to use in case the requested locale is not available
663     * @param available the available locales to find a match in
664     *
665     * @return the best matching locale name or null if no name matches
666     */
667    public Locale getBestMatchingLocale(Locale requestedLocale, List<Locale> defaults, List<Locale> available) {
668
669        if ((available == null) || available.isEmpty()) {
670            // no locales are available at all
671            return null;
672        }
673
674        // the requested locale is the match we want to find most
675        if (available.contains(requestedLocale)) {
676            // check if the requested locale is directly available
677            return requestedLocale;
678        }
679        if (requestedLocale.getVariant().length() > 0) {
680            // locale has a variant like "en_EN_whatever", try only with language and country
681            Locale check = new Locale(requestedLocale.getLanguage(), requestedLocale.getCountry(), "");
682            if (available.contains(check)) {
683                return check;
684            }
685        }
686        if (requestedLocale.getCountry().length() > 0) {
687            // locale has a country like "en_EN", try only with language
688            Locale check = new Locale(requestedLocale.getLanguage(), "", "");
689            if (available.contains(check)) {
690                return check;
691            }
692        }
693
694        // available locales do not match the requested locale
695        if ((defaults == null) || defaults.isEmpty()) {
696            // if we have no default locales we are out of luck
697            return null;
698        }
699
700        // no match found for the requested locale, return the first match from the default locales
701        return getFirstMatchingLocale(defaults, available);
702    }
703
704    /**
705     * Returns the "the" default locale for the given resource.<p>
706     *
707     * It's possible to override the system default (see {@link #getDefaultLocale()}) by setting the property
708     * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names.
709     * This property is inherited from the parent folders.
710     * This method will return the first locale from that list.<p>
711     *
712     * The default locale must be contained in the set of configured available locales,
713     * see {@link #getAvailableLocales()}.
714     * In case an invalid locale has been set with the property, this locale is ignored and the
715     * same result as {@link #getDefaultLocale()} is returned.<p>
716     *
717     * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set
718     * on the resource or a parent folder,
719     * this method returns the same result as {@link #getDefaultLocale()}.<p>
720     *
721     * @param cms the current cms permission object
722     * @param resource the resource
723     * @return an array of default locale names
724     *
725     * @see #getDefaultLocales()
726     * @see #getDefaultLocales(CmsObject, String)
727     */
728    public Locale getDefaultLocale(CmsObject cms, CmsResource resource) {
729
730        List<Locale> defaultLocales = getDefaultLocales(cms, resource);
731        Locale result;
732        if (defaultLocales.size() > 0) {
733            result = defaultLocales.get(0);
734        } else {
735            result = getDefaultLocale();
736        }
737        return result;
738    }
739
740    /**
741     * Returns the "the" default locale for the given resource.<p>
742     *
743     * It's possible to override the system default (see {@link #getDefaultLocale()}) by setting the property
744     * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names.
745     * This property is inherited from the parent folders.
746     * This method will return the first locale from that list.<p>
747     *
748     * The default locale must be contained in the set of configured available locales,
749     * see {@link #getAvailableLocales()}.
750     * In case an invalid locale has been set with the property, this locale is ignored and the
751     * same result as {@link #getDefaultLocale()} is returned.<p>
752     *
753     * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set
754     * on the resource or a parent folder,
755     * this method returns the same result as {@link #getDefaultLocale()}.<p>
756     *
757     * @param cms the current cms permission object
758     * @param resourceName the name of the resource
759     * @return an array of default locale names
760     *
761     * @see #getDefaultLocales()
762     * @see #getDefaultLocales(CmsObject, String)
763     */
764    public Locale getDefaultLocale(CmsObject cms, String resourceName) {
765
766        List<Locale> defaultLocales = getDefaultLocales(cms, resourceName);
767        Locale result;
768        if (defaultLocales.size() > 0) {
769            result = defaultLocales.get(0);
770        } else {
771            result = getDefaultLocale();
772        }
773        return result;
774    }
775
776    /**
777     * Returns the list of default {@link Locale}s configured in <code>opencms-system.xml</code>,
778     * in the <code>opencms/system/internationalization/localesdefault</code> node.<p>
779     *
780     * Since the default locale is always available, the result list will always contain at least one Locale.<p>
781     *
782     * It's possible to override the system default by setting the property
783     * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names.
784     * This property is inherited from the parent folders.<p>
785     *
786     * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}.
787     * In case an invalid locale has been set with the property, this locale is ignored.<p>
788     *
789     * The default locale names are used as a fallback mechanism in case a locale is requested
790     * that can not be found, for example when delivering content form an XML content.<p>
791     *
792     * There is a list of default locales (instead of just one default locale) since there
793     * are scenarios when one default is not enough. Consider the following example:<i>
794     * The main default locale is set to "en". An example XML content file contains just one language,
795     * in this case "de" and not "en". Now a request is made to the file for the locale "fr". If
796     * there would be only one default locale ("en"), we would have to give up. But since we allow more then
797     * one default, we can deliver the "de" content instead of a blank page.</I><p>
798     *
799     * @return the list of default locale names, e.g. <code>en, de</code>
800     *
801     * @see #getAvailableLocales()
802     */
803    public List<Locale> getDefaultLocales() {
804
805        return m_defaultLocales;
806    }
807
808    /**
809     * Returns an array of default locales for the given resource.<p>
810     *
811     * Since the default locale is always available, the result list will always contain at least one Locale.<p>
812     *
813     * It's possible to override the system default (see {@link #getDefaultLocales()}) by setting the property
814     * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names.
815     * This property is inherited from the parent folders.<p>
816     *
817     * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}.
818     * In case an invalid locale has been set with the property, this locale is ignored.<p>
819     *
820     * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set
821     * on the resource or a parent folder,
822     * this method returns the same result as {@link #getDefaultLocales()}.<p>
823     *
824     * Use this method in case you need to get all configured default options for a resource,
825     * if you just need the "the" default locale for a resource,
826     * use <code>{@link #getDefaultLocale(CmsObject, String)}</code>.<p>
827     *
828     * @param cms the current cms permission object
829     * @param resource the resource to read the default locale properties for
830     * @return an array of default locale names
831     *
832     * @see #getDefaultLocales()
833     * @see #getDefaultLocale(CmsObject, String)
834     * @see #getDefaultLocales(CmsObject, String)
835     *
836     * @since 7.0.2
837     */
838    public List<Locale> getDefaultLocales(CmsObject cms, CmsResource resource) {
839
840        String defaultNames = null;
841        try {
842            defaultNames = cms.readPropertyObject(resource, CmsPropertyDefinition.PROPERTY_LOCALE, true).getValue();
843        } catch (CmsException e) {
844            LOG.warn(Messages.get().getBundle().key(Messages.ERR_READ_ENCODING_PROP_1, cms.getSitePath(resource)), e);
845        }
846        return getDefaultLocales(defaultNames);
847    }
848
849    /**
850     * Returns an array of default locales for the given resource.<p>
851     *
852     * Since the default locale is always available, the result list will always contain at least one Locale.<p>
853     *
854     * It's possible to override the system default (see {@link #getDefaultLocales()}) by setting the property
855     * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names.
856     * This property is inherited from the parent folders.<p>
857     *
858     * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}.
859     * In case an invalid locale has been set with the property, this locale is ignored.<p>
860     *
861     * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set
862     * on the resource or a parent folder,
863     * this method returns the same result as {@link #getDefaultLocales()}.<p>
864     *
865     * Use this method in case you need to get all configured default options for a resource,
866     * if you just need the "the" default locale for a resource,
867     * use <code>{@link #getDefaultLocale(CmsObject, String)}</code>.<p>
868     *
869     * @param cms the current cms permission object
870     * @param resourceName the name of the resource
871     * @return an array of default locale names
872     *
873     * @see #getDefaultLocales()
874     * @see #getDefaultLocale(CmsObject, String)
875     * @see #getDefaultLocales(CmsObject, CmsResource)
876     */
877    public List<Locale> getDefaultLocales(CmsObject cms, String resourceName) {
878
879        String defaultNames = null;
880        try {
881            defaultNames = cms.readPropertyObject(resourceName, CmsPropertyDefinition.PROPERTY_LOCALE, true).getValue();
882        } catch (CmsException e) {
883            LOG.warn(Messages.get().getBundle().key(Messages.ERR_READ_ENCODING_PROP_1, resourceName), e);
884        }
885        return getDefaultLocales(defaultNames);
886    }
887
888    /**
889     * Returns the first matching locale (eventually simplified) from the available locales.<p>
890     *
891     * In case no match is found, code <code>null</code> is returned.<p>
892     *
893     * @param locales must be an ascending sorted list of locales in order of preference
894     * @param available the available locales to find a match in
895     *
896     * @return the first precise or simplified match, or <code>null</code> in case no match is found
897     */
898    public Locale getFirstMatchingLocale(List<Locale> locales, List<Locale> available) {
899
900        Iterator<Locale> i;
901        // first try a precise match
902        i = locales.iterator();
903        while (i.hasNext()) {
904            Locale locale = i.next();
905            if (available.contains(locale)) {
906                // precise match
907                return locale;
908            }
909        }
910
911        // now try a match only with language and country
912        i = locales.iterator();
913        while (i.hasNext()) {
914            Locale locale = i.next();
915            if (locale.getVariant().length() > 0) {
916                // the locale has a variant, try to match without the variant
917                locale = new Locale(locale.getLanguage(), locale.getCountry(), "");
918                if (available.contains(locale)) {
919                    // match
920                    return locale;
921                }
922            }
923        }
924
925        // finally try a match only with language
926        i = locales.iterator();
927        while (i.hasNext()) {
928            Locale locale = i.next();
929            if (locale.getCountry().length() > 0) {
930                // the locale has a country, try to match without the country
931                locale = new Locale(locale.getLanguage(), "", "");
932                if (available.contains(locale)) {
933                    // match
934                    return locale;
935                }
936            }
937        }
938
939        // no match
940        return null;
941    }
942
943    /**
944     * Returns the the appropriate locale/encoding for a request,
945     * using the "right" locale handler for the given resource.<p>
946     *
947     * Certain system folders (like the Workplace) require a special
948     * locale handler different from the configured handler.
949     * Use this method if you want to resolve locales exactly like
950     * the system does for a request.<p>
951     *
952     * @param req the current http request
953     * @param user the current user
954     * @param project the current project
955     * @param resource the URI of the requested resource (with full site root added)
956     *
957     * @return the i18n information to use for the given request context
958     */
959    public CmsI18nInfo getI18nInfo(HttpServletRequest req, CmsUser user, CmsProject project, String resource) {
960
961        CmsI18nInfo i18nInfo = null;
962
963        // check if this is a request against a Workplace folder
964        if (OpenCms.getSiteManager().isWorkplaceRequest(req)) {
965            // The list of configured localized workplace folders
966            List<String> wpLocalizedFolders = OpenCms.getWorkplaceManager().getLocalizedFolders();
967            for (int i = wpLocalizedFolders.size() - 1; i >= 0; i--) {
968                if (resource.startsWith(wpLocalizedFolders.get(i))) {
969                    // use the workplace locale handler for this resource
970                    i18nInfo = OpenCms.getWorkplaceManager().getI18nInfo(req, user, project, resource);
971                    break;
972                }
973            }
974        }
975        if (i18nInfo == null) {
976            // use default locale handler
977            i18nInfo = m_localeHandler.getI18nInfo(req, user, project, resource);
978        }
979
980        // check the request for special parameters overriding the locale handler
981        Locale locale = null;
982        String encoding = null;
983        if (req != null) {
984            String localeParam = req.getParameter(CmsLocaleManager.PARAMETER_LOCALE);
985            // check request for parameters
986            if (localeParam != null) {
987                // "__locale" parameter found in request
988                locale = CmsLocaleManager.getLocale(localeParam);
989            }
990            // check for "__encoding" parameter in request
991            encoding = req.getParameter(CmsLocaleManager.PARAMETER_ENCODING);
992        }
993
994        // merge values from request with values from locale handler
995        if (locale == null) {
996            locale = i18nInfo.getLocale();
997        }
998        if (encoding == null) {
999            encoding = i18nInfo.getEncoding();
1000        }
1001
1002        // still some values might be "null"
1003        if (locale == null) {
1004            locale = getDefaultLocale();
1005            if (LOG.isDebugEnabled()) {
1006                LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCALE_NOT_FOUND_1, locale));
1007            }
1008        }
1009        if (encoding == null) {
1010            encoding = OpenCms.getSystemInfo().getDefaultEncoding();
1011            if (LOG.isDebugEnabled()) {
1012                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ENCODING_NOT_FOUND_1, encoding));
1013            }
1014        }
1015
1016        // return the merged values
1017        return new CmsI18nInfo(locale, encoding);
1018    }
1019
1020    /**
1021     * Returns the configured locale handler.<p>
1022     *
1023     * This handler is used to derive the appropriate locale/encoding for a request.<p>
1024     *
1025     * @return the locale handler
1026     */
1027    public I_CmsLocaleHandler getLocaleHandler() {
1028
1029        return m_localeHandler;
1030    }
1031
1032    /**
1033     * Gets the string value of the 'reuse-elements' option.<p>
1034     *
1035     * @return the string value of the 'reuse-elements' option
1036     */
1037    public String getReuseElementsStr() {
1038
1039        return m_reuseElementsStr;
1040    }
1041
1042    /**
1043     * Returns the OpenCms default the time zone.<p>
1044     *
1045     * @return the OpenCms default the time zone
1046     */
1047    public TimeZone getTimeZone() {
1048
1049        return m_timeZone;
1050    }
1051
1052    /**
1053     * Initializes this locale manager with the OpenCms system configuration.<p>
1054     *
1055     * @param cms an OpenCms context object that must have been initialized with "Admin" permissions
1056     */
1057    public void initialize(CmsObject cms) {
1058
1059        if (!m_availableLocales.contains(Locale.ENGLISH)) {
1060            throw new RuntimeException("The locale 'en' must be configured in opencms-system.xml.");
1061        }
1062        // init the locale handler
1063        m_localeHandler.initHandler(cms);
1064        // set default locale
1065        m_defaultLocale = m_defaultLocales.get(0);
1066        initLanguageDetection();
1067        // set initialized status
1068        m_initialized = true;
1069        if (CmsLog.INIT.isInfoEnabled()) {
1070            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_VFSACCESS_0));
1071        }
1072    }
1073
1074    /**
1075     * Returns <code>true</code> if this locale manager is fully initialized.<p>
1076     *
1077     * This is required to prevent errors during unit tests,
1078     * simple unit tests will usually not have a fully
1079     * initialized locale manager available.<p>
1080     *
1081     * @return true if the locale manager is fully initialized
1082     */
1083    public boolean isInitialized() {
1084
1085        return m_initialized;
1086    }
1087
1088    /**
1089     * Sets the configured locale handler.<p>
1090     *
1091     * @param localeHandler the locale handler to set
1092     */
1093    public void setLocaleHandler(I_CmsLocaleHandler localeHandler) {
1094
1095        if (localeHandler != null) {
1096            m_localeHandler = localeHandler;
1097        }
1098        if (CmsLog.INIT.isInfoEnabled()) {
1099            CmsLog.INIT.info(
1100                Messages.get().getBundle().key(
1101                    Messages.INIT_I18N_CONFIG_LOC_HANDLER_1,
1102                    m_localeHandler.getClass().getName()));
1103        }
1104    }
1105
1106    /**
1107     * Sets the 'reuse-elemnts option value.<p>
1108     *
1109     * @param reuseElements the option value
1110     */
1111    public void setReuseElements(String reuseElements) {
1112
1113        m_reuseElementsStr = reuseElements;
1114    }
1115
1116    /**
1117     * Sets OpenCms default the time zone.<p>
1118     *
1119     * If the name can not be resolved as time zone ID, then "GMT" is used.<p>
1120     *
1121     * @param timeZoneName the name of the time zone to set, for example "GMT"
1122     */
1123    public void setTimeZone(String timeZoneName) {
1124
1125        // according to JavaDoc, "GMT" is the default time zone if the name can not be resolved
1126        m_timeZone = TimeZone.getTimeZone(timeZoneName);
1127    }
1128
1129    /**
1130     * Returns true if the 'copy page' dialog should reuse elements in auto mode when copying to a different locale.<p>
1131     *
1132     * @return true if auto mode of the 'copy page' dialog should reuse elements
1133     */
1134    public boolean shouldReuseElements() {
1135
1136        boolean isFalseInConfig = Boolean.FALSE.toString().equalsIgnoreCase(StringUtils.trim(m_reuseElementsStr));
1137        return !isFalseInConfig;
1138    }
1139
1140    /**
1141     * Returns a list of available locale names derived from the given locale names.<p>
1142     *
1143     * Each name in the given list is checked against the internal hash map of allowed locales,
1144     * and is appended to the resulting list only if the locale exists.<p>
1145     *
1146     * @param locales List of locales to check
1147     * @return list of available locales derived from the given locale names
1148     */
1149    private List<Locale> checkLocaleNames(List<Locale> locales) {
1150
1151        if (locales == null) {
1152            return null;
1153        }
1154        List<Locale> result = new ArrayList<Locale>();
1155        Iterator<Locale> i = locales.iterator();
1156        while (i.hasNext()) {
1157            Locale locale = i.next();
1158            if (m_availableLocales.contains(locale)) {
1159                result.add(locale);
1160            }
1161        }
1162        return result;
1163    }
1164
1165    /**
1166     * Clears the caches in the locale manager.<p>
1167     */
1168    private void clearCaches() {
1169
1170        // flush all caches
1171        OpenCms.getMemoryMonitor().flushCache(CmsMemoryMonitor.CacheType.LOCALE);
1172        CmsResourceBundleLoader.flushBundleCache();
1173
1174        if (LOG.isDebugEnabled()) {
1175            LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCALE_MANAGER_FLUSH_CACHE_1, "EVENT_CLEAR_CACHES"));
1176        }
1177    }
1178
1179    /**
1180     * Internal helper, returns an array of default locales for the given default names.<p>
1181     *
1182     * If required returns the system configured default locales.<p>
1183     *
1184     * @param defaultNames the default locales to use, can be <code>null</code> or a comma separated list
1185     *      of locales, for example <code>"en, de"</code>
1186     *
1187     * @return an array of default locales for the given default names
1188     */
1189    private List<Locale> getDefaultLocales(String defaultNames) {
1190
1191        List<Locale> result = null;
1192        if (defaultNames != null) {
1193            result = getAvailableLocales(defaultNames);
1194        }
1195        if ((result == null) || (result.size() == 0)) {
1196            return getDefaultLocales();
1197        } else {
1198            return result;
1199        }
1200    }
1201
1202    /**
1203     * Initializes the language detection.<p>
1204     */
1205    private void initLanguageDetection() {
1206
1207        try {
1208            // use a seed for initializing the language detection for making sure the
1209            // same probabilities are detected for the same document contents
1210            DetectorFactory.clear();
1211            DetectorFactory.setSeed(42L);
1212            DetectorFactory.loadProfile(loadProfiles(getAvailableLocales()));
1213        } catch (Exception e) {
1214            LOG.error(Messages.get().getBundle().key(Messages.INIT_I18N_LANG_DETECT_FAILED_0), e);
1215        }
1216    }
1217
1218    /**
1219     * Load the profiles from the classpath.<p>
1220     *
1221     * @param locales the locales to initialize.<p>
1222     *
1223     * @return a list of profiles
1224     *
1225     * @throws Exception if something goes wrong
1226     */
1227    private List<String> loadProfiles(List<Locale> locales) throws Exception {
1228
1229        List<String> profiles = new ArrayList<String>();
1230        List<String> languagesAdded = new ArrayList<String>();
1231        for (Locale locale : locales) {
1232            try {
1233                String lang = locale.getLanguage();
1234                // make sure not to add a profile twice
1235                if (!languagesAdded.contains(lang)) {
1236                    languagesAdded.add(lang);
1237                    String profileFile = "profiles" + "/" + lang;
1238                    InputStream is = getClass().getClassLoader().getResourceAsStream(profileFile);
1239                    if (is != null) {
1240                        String profile = IOUtils.toString(is, "UTF-8");
1241                        if ((profile != null) && (profile.length() > 0)) {
1242                            profiles.add(profile);
1243                        }
1244                        is.close();
1245                    } else {
1246                        LOG.warn(
1247                            Messages.get().getBundle().key(
1248                                Messages.INIT_I18N_LAND_DETECT_PROFILE_NOT_AVAILABLE_1,
1249                                locale));
1250                    }
1251                }
1252            } catch (Exception e) {
1253                LOG.error(
1254                    Messages.get().getBundle().key(Messages.INIT_I18N_LAND_DETECT_LOADING_PROFILE_FAILED_1, locale),
1255                    e);
1256            }
1257        }
1258        return profiles;
1259    }
1260}