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     * Note that if the <code>localesconfigured<code> node contains  a locale variant for a specific country (e.g. de_DE),
527     * then both that locale and the locale without the country suffix will be in the returned list.
528     *
529     * @return the list of available locale names, e.g. <code>en, de</code>
530     *
531     * @see #getDefaultLocales()
532     */
533    public List<Locale> getAvailableLocales() {
534
535        return Collections.unmodifiableList(m_availableLocales);
536    }
537
538    /**
539     * Returns an array of available locale names for the given resource.<p>
540     *
541     * @param cms the current cms permission object
542     * @param resource the resource
543     *
544     * @return an array of available locale names
545     *
546     * @see #getAvailableLocales()
547     */
548    public List<Locale> getAvailableLocales(CmsObject cms, CmsResource resource) {
549
550        String availableNames = null;
551        try {
552            availableNames = cms.readPropertyObject(
553                resource,
554                CmsPropertyDefinition.PROPERTY_AVAILABLE_LOCALES,
555                true).getValue();
556        } catch (CmsException exc) {
557            LOG.debug("Could not read available locales property for resource " + resource.getRootPath(), exc);
558        }
559
560        List<Locale> result = null;
561        if (availableNames != null) {
562            result = getAvailableLocales(availableNames);
563        }
564        if ((result == null) || (result.size() == 0)) {
565            return Collections.unmodifiableList(m_availableLocales);
566        } else {
567            return result;
568        }
569    }
570
571    /**
572     * Returns an array of available locale names for the given resource.<p>
573     *
574     * @param cms the current cms permission object
575     * @param resourceName the name of the resource
576     *
577     * @return an array of available locale names
578     *
579     * @see #getAvailableLocales()
580     */
581    public List<Locale> getAvailableLocales(CmsObject cms, String resourceName) {
582
583        String availableNames = null;
584        try {
585            availableNames = cms.readPropertyObject(
586                resourceName,
587                CmsPropertyDefinition.PROPERTY_AVAILABLE_LOCALES,
588                true).getValue();
589        } catch (CmsException exc) {
590            LOG.debug("Could not read available locales property for resource " + resourceName, exc);
591        }
592
593        List<Locale> result = null;
594        if (availableNames != null) {
595            result = getAvailableLocales(availableNames);
596        }
597        if ((result == null) || (result.size() == 0)) {
598            return Collections.unmodifiableList(m_availableLocales);
599        } else {
600            return result;
601        }
602    }
603
604    /**
605     * Returns a List of available locales from a comma separated string of locale names.<p>
606     *
607     * All names are filtered against the allowed available locales
608     * configured in <code>opencms-system.xml</code>.<P>
609     *
610     * @param names a comma-separated String of locale names
611     * @return List of locales created from the given locale names
612     *
613     * @see #getAvailableLocales()
614     */
615    public List<Locale> getAvailableLocales(String names) {
616
617        return checkLocaleNames(getLocales(names));
618    }
619
620    /**
621     * Returns the best available locale present in the given XML content, or the default locale.<p>
622     *
623     * @param cms the current OpenCms user context
624     * @param resource the resource
625     * @param content the XML content
626     *
627     * @return the locale
628     */
629    public Locale getBestAvailableLocaleForXmlContent(CmsObject cms, CmsResource resource, I_CmsXmlDocument content) {
630
631        Locale locale = getDefaultLocale(cms, resource);
632        if (!content.hasLocale(locale)) {
633            // if the requested locale is not available, get the first matching default locale,
634            // or the first matching available locale
635            boolean foundLocale = false;
636            if (content.getLocales().size() > 0) {
637                List<Locale> locales = getDefaultLocales(cms, resource);
638                for (Locale defaultLocale : locales) {
639                    if (content.hasLocale(defaultLocale)) {
640                        locale = defaultLocale;
641                        foundLocale = true;
642                        break;
643                    }
644                }
645                if (!foundLocale) {
646                    locales = getAvailableLocales(cms, resource);
647                    for (Locale availableLocale : locales) {
648                        if (content.hasLocale(availableLocale)) {
649                            locale = availableLocale;
650                            foundLocale = true;
651                            break;
652                        }
653                    }
654                }
655            }
656        }
657        return locale;
658    }
659
660    /**
661     * Tries to find the given requested locale (eventually simplified) in the collection of available locales,
662     * if the requested locale is not found it will return the first match from the given list of default locales.<p>
663     *
664     * @param requestedLocale the requested locale, if this (or a simplified version of it) is available it will be returned
665     * @param defaults a list of default locales to use in case the requested locale is not available
666     * @param available the available locales to find a match in
667     *
668     * @return the best matching locale name or null if no name matches
669     */
670    public Locale getBestMatchingLocale(Locale requestedLocale, List<Locale> defaults, List<Locale> available) {
671
672        if ((available == null) || available.isEmpty()) {
673            // no locales are available at all
674            return null;
675        }
676
677        // the requested locale is the match we want to find most
678        if (available.contains(requestedLocale)) {
679            // check if the requested locale is directly available
680            return requestedLocale;
681        }
682        if (requestedLocale.getVariant().length() > 0) {
683            // locale has a variant like "en_EN_whatever", try only with language and country
684            Locale check = new Locale(requestedLocale.getLanguage(), requestedLocale.getCountry(), "");
685            if (available.contains(check)) {
686                return check;
687            }
688        }
689        if (requestedLocale.getCountry().length() > 0) {
690            // locale has a country like "en_EN", try only with language
691            Locale check = new Locale(requestedLocale.getLanguage(), "", "");
692            if (available.contains(check)) {
693                return check;
694            }
695        }
696
697        // available locales do not match the requested locale
698        if ((defaults == null) || defaults.isEmpty()) {
699            // if we have no default locales we are out of luck
700            return null;
701        }
702
703        // no match found for the requested locale, return the first match from the default locales
704        return getFirstMatchingLocale(defaults, available);
705    }
706
707    /**
708     * Returns the "the" default locale for the given resource.<p>
709     *
710     * It's possible to override the system default (see {@link #getDefaultLocale()}) by setting the property
711     * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names.
712     * This property is inherited from the parent folders.
713     * This method will return the first locale from that list.<p>
714     *
715     * The default locale must be contained in the set of configured available locales,
716     * see {@link #getAvailableLocales()}.
717     * In case an invalid locale has been set with the property, this locale is ignored and the
718     * same result as {@link #getDefaultLocale()} is returned.<p>
719     *
720     * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set
721     * on the resource or a parent folder,
722     * this method returns the same result as {@link #getDefaultLocale()}.<p>
723     *
724     * @param cms the current cms permission object
725     * @param resource the resource
726     * @return an array of default locale names
727     *
728     * @see #getDefaultLocales()
729     * @see #getDefaultLocales(CmsObject, String)
730     */
731    public Locale getDefaultLocale(CmsObject cms, CmsResource resource) {
732
733        List<Locale> defaultLocales = getDefaultLocales(cms, resource);
734        Locale result;
735        if (defaultLocales.size() > 0) {
736            result = defaultLocales.get(0);
737        } else {
738            result = getDefaultLocale();
739        }
740        return result;
741    }
742
743    /**
744     * Returns the "the" default locale for the given resource.<p>
745     *
746     * It's possible to override the system default (see {@link #getDefaultLocale()}) by setting the property
747     * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names.
748     * This property is inherited from the parent folders.
749     * This method will return the first locale from that list.<p>
750     *
751     * The default locale must be contained in the set of configured available locales,
752     * see {@link #getAvailableLocales()}.
753     * In case an invalid locale has been set with the property, this locale is ignored and the
754     * same result as {@link #getDefaultLocale()} is returned.<p>
755     *
756     * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set
757     * on the resource or a parent folder,
758     * this method returns the same result as {@link #getDefaultLocale()}.<p>
759     *
760     * @param cms the current cms permission object
761     * @param resourceName the name of the resource
762     * @return an array of default locale names
763     *
764     * @see #getDefaultLocales()
765     * @see #getDefaultLocales(CmsObject, String)
766     */
767    public Locale getDefaultLocale(CmsObject cms, String resourceName) {
768
769        List<Locale> defaultLocales = getDefaultLocales(cms, resourceName);
770        Locale result;
771        if (defaultLocales.size() > 0) {
772            result = defaultLocales.get(0);
773        } else {
774            result = getDefaultLocale();
775        }
776        return result;
777    }
778
779    /**
780     * Returns the list of default {@link Locale}s configured in <code>opencms-system.xml</code>,
781     * in the <code>opencms/system/internationalization/localesdefault</code> node.<p>
782     *
783     * Since the default locale is always available, the result list will always contain at least one Locale.<p>
784     *
785     * It's possible to override the system default by setting the property
786     * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names.
787     * This property is inherited from the parent folders.<p>
788     *
789     * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}.
790     * In case an invalid locale has been set with the property, this locale is ignored.<p>
791     *
792     * The default locale names are used as a fallback mechanism in case a locale is requested
793     * that can not be found, for example when delivering content form an XML content.<p>
794     *
795     * There is a list of default locales (instead of just one default locale) since there
796     * are scenarios when one default is not enough. Consider the following example:<i>
797     * The main default locale is set to "en". An example XML content file contains just one language,
798     * in this case "de" and not "en". Now a request is made to the file for the locale "fr". If
799     * there would be only one default locale ("en"), we would have to give up. But since we allow more then
800     * one default, we can deliver the "de" content instead of a blank page.</I><p>
801     *
802     * @return the list of default locale names, e.g. <code>en, de</code>
803     *
804     * @see #getAvailableLocales()
805     */
806    public List<Locale> getDefaultLocales() {
807
808        return m_defaultLocales;
809    }
810
811    /**
812     * Returns an array of default locales for the given resource.<p>
813     *
814     * Since the default locale is always available, the result list will always contain at least one Locale.<p>
815     *
816     * It's possible to override the system default (see {@link #getDefaultLocales()}) by setting the property
817     * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names.
818     * This property is inherited from the parent folders.<p>
819     *
820     * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}.
821     * In case an invalid locale has been set with the property, this locale is ignored.<p>
822     *
823     * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set
824     * on the resource or a parent folder,
825     * this method returns the same result as {@link #getDefaultLocales()}.<p>
826     *
827     * Use this method in case you need to get all configured default options for a resource,
828     * if you just need the "the" default locale for a resource,
829     * use <code>{@link #getDefaultLocale(CmsObject, String)}</code>.<p>
830     *
831     * @param cms the current cms permission object
832     * @param resource the resource to read the default locale properties for
833     * @return an array of default locale names
834     *
835     * @see #getDefaultLocales()
836     * @see #getDefaultLocale(CmsObject, String)
837     * @see #getDefaultLocales(CmsObject, String)
838     *
839     * @since 7.0.2
840     */
841    public List<Locale> getDefaultLocales(CmsObject cms, CmsResource resource) {
842
843        String defaultNames = null;
844        try {
845            defaultNames = cms.readPropertyObject(resource, CmsPropertyDefinition.PROPERTY_LOCALE, true).getValue();
846        } catch (CmsException e) {
847            LOG.warn(Messages.get().getBundle().key(Messages.ERR_READ_ENCODING_PROP_1, cms.getSitePath(resource)), e);
848        }
849        return getDefaultLocales(defaultNames);
850    }
851
852    /**
853     * Returns an array of default locales for the given resource.<p>
854     *
855     * Since the default locale is always available, the result list will always contain at least one Locale.<p>
856     *
857     * It's possible to override the system default (see {@link #getDefaultLocales()}) by setting the property
858     * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names.
859     * This property is inherited from the parent folders.<p>
860     *
861     * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}.
862     * In case an invalid locale has been set with the property, this locale is ignored.<p>
863     *
864     * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set
865     * on the resource or a parent folder,
866     * this method returns the same result as {@link #getDefaultLocales()}.<p>
867     *
868     * Use this method in case you need to get all configured default options for a resource,
869     * if you just need the "the" default locale for a resource,
870     * use <code>{@link #getDefaultLocale(CmsObject, String)}</code>.<p>
871     *
872     * @param cms the current cms permission object
873     * @param resourceName the name of the resource
874     * @return an array of default locale names
875     *
876     * @see #getDefaultLocales()
877     * @see #getDefaultLocale(CmsObject, String)
878     * @see #getDefaultLocales(CmsObject, CmsResource)
879     */
880    public List<Locale> getDefaultLocales(CmsObject cms, String resourceName) {
881
882        String defaultNames = null;
883        try {
884            defaultNames = cms.readPropertyObject(resourceName, CmsPropertyDefinition.PROPERTY_LOCALE, true).getValue();
885        } catch (CmsException e) {
886            LOG.warn(Messages.get().getBundle().key(Messages.ERR_READ_ENCODING_PROP_1, resourceName), e);
887        }
888        return getDefaultLocales(defaultNames);
889    }
890
891    /**
892     * Returns the first matching locale (eventually simplified) from the available locales.<p>
893     *
894     * In case no match is found, code <code>null</code> is returned.<p>
895     *
896     * @param locales must be an ascending sorted list of locales in order of preference
897     * @param available the available locales to find a match in
898     *
899     * @return the first precise or simplified match, or <code>null</code> in case no match is found
900     */
901    public Locale getFirstMatchingLocale(List<Locale> locales, List<Locale> available) {
902
903        Iterator<Locale> i;
904        // first try a precise match
905        i = locales.iterator();
906        while (i.hasNext()) {
907            Locale locale = i.next();
908            if (available.contains(locale)) {
909                // precise match
910                return locale;
911            }
912        }
913
914        // now try a match only with language and country
915        i = locales.iterator();
916        while (i.hasNext()) {
917            Locale locale = i.next();
918            if (locale.getVariant().length() > 0) {
919                // the locale has a variant, try to match without the variant
920                locale = new Locale(locale.getLanguage(), locale.getCountry(), "");
921                if (available.contains(locale)) {
922                    // match
923                    return locale;
924                }
925            }
926        }
927
928        // finally try a match only with language
929        i = locales.iterator();
930        while (i.hasNext()) {
931            Locale locale = i.next();
932            if (locale.getCountry().length() > 0) {
933                // the locale has a country, try to match without the country
934                locale = new Locale(locale.getLanguage(), "", "");
935                if (available.contains(locale)) {
936                    // match
937                    return locale;
938                }
939            }
940        }
941
942        // no match
943        return null;
944    }
945
946    /**
947     * Returns the the appropriate locale/encoding for a request,
948     * using the "right" locale handler for the given resource.<p>
949     *
950     * Certain system folders (like the Workplace) require a special
951     * locale handler different from the configured handler.
952     * Use this method if you want to resolve locales exactly like
953     * the system does for a request.<p>
954     *
955     * @param req the current http request
956     * @param user the current user
957     * @param project the current project
958     * @param resource the URI of the requested resource (with full site root added)
959     *
960     * @return the i18n information to use for the given request context
961     */
962    public CmsI18nInfo getI18nInfo(HttpServletRequest req, CmsUser user, CmsProject project, String resource) {
963
964        CmsI18nInfo i18nInfo = null;
965
966        // check if this is a request against a Workplace folder
967        if (OpenCms.getSiteManager().isWorkplaceRequest(req)) {
968            // The list of configured localized workplace folders
969            List<String> wpLocalizedFolders = OpenCms.getWorkplaceManager().getLocalizedFolders();
970            for (int i = wpLocalizedFolders.size() - 1; i >= 0; i--) {
971                if (resource.startsWith(wpLocalizedFolders.get(i))) {
972                    // use the workplace locale handler for this resource
973                    i18nInfo = OpenCms.getWorkplaceManager().getI18nInfo(req, user, project, resource);
974                    break;
975                }
976            }
977        }
978        if (i18nInfo == null) {
979            // use default locale handler
980            i18nInfo = m_localeHandler.getI18nInfo(req, user, project, resource);
981        }
982
983        // check the request for special parameters overriding the locale handler
984        Locale locale = null;
985        String encoding = null;
986        if (req != null) {
987            String localeParam = req.getParameter(CmsLocaleManager.PARAMETER_LOCALE);
988            // check request for parameters
989            if (localeParam != null) {
990                // "__locale" parameter found in request
991                locale = CmsLocaleManager.getLocale(localeParam);
992            }
993            // check for "__encoding" parameter in request
994            encoding = req.getParameter(CmsLocaleManager.PARAMETER_ENCODING);
995        }
996
997        // merge values from request with values from locale handler
998        if (locale == null) {
999            locale = i18nInfo.getLocale();
1000        }
1001        if (encoding == null) {
1002            encoding = i18nInfo.getEncoding();
1003        }
1004
1005        // still some values might be "null"
1006        if (locale == null) {
1007            locale = getDefaultLocale();
1008            if (LOG.isDebugEnabled()) {
1009                LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCALE_NOT_FOUND_1, locale));
1010            }
1011        }
1012        if (encoding == null) {
1013            encoding = OpenCms.getSystemInfo().getDefaultEncoding();
1014            if (LOG.isDebugEnabled()) {
1015                LOG.debug(Messages.get().getBundle().key(Messages.LOG_ENCODING_NOT_FOUND_1, encoding));
1016            }
1017        }
1018
1019        // return the merged values
1020        return new CmsI18nInfo(locale, encoding);
1021    }
1022
1023    /**
1024     * Returns the configured locale handler.<p>
1025     *
1026     * This handler is used to derive the appropriate locale/encoding for a request.<p>
1027     *
1028     * @return the locale handler
1029     */
1030    public I_CmsLocaleHandler getLocaleHandler() {
1031
1032        return m_localeHandler;
1033    }
1034
1035    /**
1036     * Gets the string value of the 'reuse-elements' option.<p>
1037     *
1038     * @return the string value of the 'reuse-elements' option
1039     */
1040    public String getReuseElementsStr() {
1041
1042        return m_reuseElementsStr;
1043    }
1044
1045    /**
1046     * Returns the OpenCms default the time zone.<p>
1047     *
1048     * @return the OpenCms default the time zone
1049     */
1050    public TimeZone getTimeZone() {
1051
1052        return m_timeZone;
1053    }
1054
1055    /**
1056     * Initializes this locale manager with the OpenCms system configuration.<p>
1057     *
1058     * @param cms an OpenCms context object that must have been initialized with "Admin" permissions
1059     */
1060    public void initialize(CmsObject cms) {
1061
1062        if (!m_availableLocales.contains(Locale.ENGLISH)) {
1063            throw new RuntimeException("The locale 'en' must be configured in opencms-system.xml.");
1064        }
1065        // init the locale handler
1066        m_localeHandler.initHandler(cms);
1067        // set default locale
1068        m_defaultLocale = m_defaultLocales.get(0);
1069        initLanguageDetection();
1070        // set initialized status
1071        m_initialized = true;
1072        if (CmsLog.INIT.isInfoEnabled()) {
1073            CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_VFSACCESS_0));
1074        }
1075    }
1076
1077    /**
1078     * Returns <code>true</code> if this locale manager is fully initialized.<p>
1079     *
1080     * This is required to prevent errors during unit tests,
1081     * simple unit tests will usually not have a fully
1082     * initialized locale manager available.<p>
1083     *
1084     * @return true if the locale manager is fully initialized
1085     */
1086    public boolean isInitialized() {
1087
1088        return m_initialized;
1089    }
1090
1091    /**
1092     * Sets the configured locale handler.<p>
1093     *
1094     * @param localeHandler the locale handler to set
1095     */
1096    public void setLocaleHandler(I_CmsLocaleHandler localeHandler) {
1097
1098        if (localeHandler != null) {
1099            m_localeHandler = localeHandler;
1100        }
1101        if (CmsLog.INIT.isInfoEnabled()) {
1102            CmsLog.INIT.info(
1103                Messages.get().getBundle().key(
1104                    Messages.INIT_I18N_CONFIG_LOC_HANDLER_1,
1105                    m_localeHandler.getClass().getName()));
1106        }
1107    }
1108
1109    /**
1110     * Sets the 'reuse-elemnts option value.<p>
1111     *
1112     * @param reuseElements the option value
1113     */
1114    public void setReuseElements(String reuseElements) {
1115
1116        m_reuseElementsStr = reuseElements;
1117    }
1118
1119    /**
1120     * Sets OpenCms default the time zone.<p>
1121     *
1122     * If the name can not be resolved as time zone ID, then "GMT" is used.<p>
1123     *
1124     * @param timeZoneName the name of the time zone to set, for example "GMT"
1125     */
1126    public void setTimeZone(String timeZoneName) {
1127
1128        // according to JavaDoc, "GMT" is the default time zone if the name can not be resolved
1129        m_timeZone = TimeZone.getTimeZone(timeZoneName);
1130    }
1131
1132    /**
1133     * Returns true if the 'copy page' dialog should reuse elements in auto mode when copying to a different locale.<p>
1134     *
1135     * @return true if auto mode of the 'copy page' dialog should reuse elements
1136     */
1137    public boolean shouldReuseElements() {
1138
1139        boolean isFalseInConfig = Boolean.FALSE.toString().equalsIgnoreCase(StringUtils.trim(m_reuseElementsStr));
1140        return !isFalseInConfig;
1141    }
1142
1143    /**
1144     * Returns a list of available locale names derived from the given locale names.<p>
1145     *
1146     * Each name in the given list is checked against the internal hash map of allowed locales,
1147     * and is appended to the resulting list only if the locale exists.<p>
1148     *
1149     * @param locales List of locales to check
1150     * @return list of available locales derived from the given locale names
1151     */
1152    private List<Locale> checkLocaleNames(List<Locale> locales) {
1153
1154        if (locales == null) {
1155            return null;
1156        }
1157        List<Locale> result = new ArrayList<Locale>();
1158        Iterator<Locale> i = locales.iterator();
1159        while (i.hasNext()) {
1160            Locale locale = i.next();
1161            if (m_availableLocales.contains(locale)) {
1162                result.add(locale);
1163            }
1164        }
1165        return result;
1166    }
1167
1168    /**
1169     * Clears the caches in the locale manager.<p>
1170     */
1171    private void clearCaches() {
1172
1173        // flush all caches
1174        OpenCms.getMemoryMonitor().flushCache(CmsMemoryMonitor.CacheType.LOCALE);
1175        CmsResourceBundleLoader.flushBundleCache();
1176
1177        if (LOG.isDebugEnabled()) {
1178            LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCALE_MANAGER_FLUSH_CACHE_1, "EVENT_CLEAR_CACHES"));
1179        }
1180    }
1181
1182    /**
1183     * Internal helper, returns an array of default locales for the given default names.<p>
1184     *
1185     * If required returns the system configured default locales.<p>
1186     *
1187     * @param defaultNames the default locales to use, can be <code>null</code> or a comma separated list
1188     *      of locales, for example <code>"en, de"</code>
1189     *
1190     * @return an array of default locales for the given default names
1191     */
1192    private List<Locale> getDefaultLocales(String defaultNames) {
1193
1194        List<Locale> result = null;
1195        if (defaultNames != null) {
1196            result = getAvailableLocales(defaultNames);
1197        }
1198        if ((result == null) || (result.size() == 0)) {
1199            return getDefaultLocales();
1200        } else {
1201            return result;
1202        }
1203    }
1204
1205    /**
1206     * Initializes the language detection.<p>
1207     */
1208    private void initLanguageDetection() {
1209
1210        try {
1211            // use a seed for initializing the language detection for making sure the
1212            // same probabilities are detected for the same document contents
1213            DetectorFactory.clear();
1214            DetectorFactory.setSeed(42L);
1215            DetectorFactory.loadProfile(loadProfiles(getAvailableLocales()));
1216        } catch (Exception e) {
1217            LOG.error(Messages.get().getBundle().key(Messages.INIT_I18N_LANG_DETECT_FAILED_0), e);
1218        }
1219    }
1220
1221    /**
1222     * Load the profiles from the classpath.<p>
1223     *
1224     * @param locales the locales to initialize.<p>
1225     *
1226     * @return a list of profiles
1227     *
1228     * @throws Exception if something goes wrong
1229     */
1230    private List<String> loadProfiles(List<Locale> locales) throws Exception {
1231
1232        List<String> profiles = new ArrayList<String>();
1233        List<String> languagesAdded = new ArrayList<String>();
1234        for (Locale locale : locales) {
1235            try {
1236                String lang = locale.getLanguage();
1237                // make sure not to add a profile twice
1238                if (!languagesAdded.contains(lang)) {
1239                    languagesAdded.add(lang);
1240                    String profileFile = "profiles" + "/" + lang;
1241                    InputStream is = getClass().getClassLoader().getResourceAsStream(profileFile);
1242                    if (is != null) {
1243                        String profile = IOUtils.toString(is, "UTF-8");
1244                        if ((profile != null) && (profile.length() > 0)) {
1245                            profiles.add(profile);
1246                        }
1247                        is.close();
1248                    } else {
1249                        LOG.warn(
1250                            Messages.get().getBundle().key(
1251                                Messages.INIT_I18N_LAND_DETECT_PROFILE_NOT_AVAILABLE_1,
1252                                locale));
1253                    }
1254                }
1255            } catch (Exception e) {
1256                LOG.error(
1257                    Messages.get().getBundle().key(Messages.INIT_I18N_LAND_DETECT_LOADING_PROFILE_FAILED_1, locale),
1258                    e);
1259            }
1260        }
1261        return profiles;
1262    }
1263}