org.jahia.utils.LanguageCodeConverters.java Source code

Java tutorial

Introduction

Here is the source code for org.jahia.utils.LanguageCodeConverters.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *                                 http://www.jahia.com
 *
 *     Copyright (C) 2002-2017 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ==================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ===================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 */
package org.jahia.utils;

import java.text.Collator;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.WordUtils;
import org.jahia.utils.i18n.ResourceBundles;

import javax.servlet.http.HttpServletRequest;

/**
 * <p>Title: Utility class to convert between the different type of language
 * codes encodings.</p>
 * <p>Description: This class was design to offer conversion tools between
 * various language code encodings into Strings, Locales and other types of
 * objects. </p>
 * <p>Copyright: Copyright (c) 2002</p>
 * <p>Company: </p>
 * @author Serge Huber.
 * @version 1.0
 */

public class LanguageCodeConverters {

    public static final Pattern LANGUAGE_PATTERN = Pattern.compile("[a-z]{2}(_[A-Z]{2})?");
    final static String JAVA7_LOCALE_LANGUAGE = "([a-zA-Z]{2,8})";
    final static String JAVA7_LOCALE_COUNTRY = "([a-zA-Z]{2}|[0-9]{3})";
    final static String JAVA7_LOCALE_VARIANT = "(?:_|-)([0-9a-zA-Z\\_\\-\\#]*)";
    public final static String JAVA7_LOCALE_TOSTRING = JAVA7_LOCALE_LANGUAGE + "?_?" + JAVA7_LOCALE_COUNTRY + "?(?:"
            + JAVA7_LOCALE_VARIANT + ")?";
    final static Pattern JAVA7_LOCALE_TOSTRING_PATTERN = Pattern.compile(JAVA7_LOCALE_TOSTRING);

    private static volatile List<Locale> availableLocales;

    private static volatile List<Locale> availableBundleLocales;

    private static Map<String, Locale> locales = new ConcurrentHashMap<String, Locale>();

    /**
     * Tests if the language code is valid or not, according to the Locale.toString() format.
     * @param languageCode
     * @return
     */
    public static boolean isValidLanguageCode(String languageCode) {
        Matcher java7LocaleToStringMatcher = JAVA7_LOCALE_TOSTRING_PATTERN.matcher(languageCode);
        return java7LocaleToStringMatcher.matches();
    }

    /**
     * Converts string such as
     *   en_US, fr_CH_unix, fr, en, _GB, fr__UNIX
     * into Locale objects. This method is the reverse operation of the
     * Locale.toString() output. So a good test case for this method could
     * be :
     *   languageCodeToLocale(Locale.toString()).equals(Locale)
     * @param languageCode the encoded string
     * @return the resulting Locale object, or the default language code if
     * no language was found.
     */
    public static Locale languageCodeToLocale(String languageCode) {
        if (languageCode == null) {
            return null;
        }
        Locale loc = locales.get(languageCode);
        if (loc != null) {
            return loc;
        }

        String[] codeParts = Patterns.UNDERSCORE.split(languageCode);
        String language = "";
        String country = "";
        StringBuilder variant = new StringBuilder();

        if (codeParts.length > 0 && codeParts[0].length() > 0) {
            language = codeParts[0];
        }

        if (codeParts.length > 1 && codeParts[1].length() > 0) {
            country = codeParts[1];
        }

        if (codeParts.length > 2 && codeParts[2].length() > 0) {
            variant.append(codeParts[2]);
        }

        if (codeParts.length > 3) {
            for (int i = 3; i < codeParts.length; i++) {
                variant.append("_").append(codeParts[i]);
            }
        }

        /*
        if (!languageCode.startsWith("_") && codeTokens.hasMoreTokens()) {
        language = codeTokens.nextToken();
        }
        if (codeTokens.hasMoreTokens()) {
        country = codeTokens.nextToken();
        }
        if (codeTokens.hasMoreTokens()) {
        variant = codeTokens.nextToken();
        }
        */

        loc = newLocale(language, country, variant.toString());
        locales.put(languageCode, loc);

        return loc;
    }

    /**
     * Converts (alt) string such as
     *   en_US, fr_CH_unix, fr, en, _GB, fr__UNIX
     * into Locale objects. This method is the reverse operation of the
     * Locale.toString() output. So a good test case for this method could
     * be :
     *   languageCodeToLocale(Locale.toString()).equals(Locale)
     * @param code the encoded string
     * @return the resulting Locale object, or the default language code if
     * no language was found.
     */
    public static Locale getLocaleFromCode(String code) {
        if (code == null || code.length() == 0) {
            return Locale.ENGLISH;
        }
        Locale loc;
        String[] codes = Patterns.UNDERSCORE.split(code);
        if (codes.length == 0) {
            return Locale.ENGLISH;
        } else if (codes.length == 1) {
            loc = new Locale(codes[0]);
        } else if (codes.length == 2) {
            loc = new Locale(codes[0], codes[1]);
        } else if (codes.length == 3) {
            loc = new Locale(codes[0], codes[1], codes[2]);
        } else {
            return Locale.ENGLISH;
        }
        return loc;
    }

    /**
     * Converts language tags encoded (RFC 3066) to Locales. Examples of
     * language tags encoding are :
     *   en-us, en, en-scouse
     * Warning : this is not entirely RFC 3066 because we cannot convert from
     * 3 characters ISO 639 part 2 to ISO 639 part 1 codes. In the case a
     * 3 characters code is found we return null.
     * @param languageCode the encoded string
     * @return a Locale corresponding to the encoding.
     */
    public static Locale languageTagsToLocale(String languageCode) {
        StringTokenizer codeTokens = new StringTokenizer(languageCode, "-");
        String language = "";
        String country = "";
        String variant = "";

        String primaryTag = "";
        String secondSubTag = "";
        String thirdSubTag = "";

        if (codeTokens.hasMoreTokens()) {
            primaryTag = codeTokens.nextToken();
        }
        if (codeTokens.hasMoreTokens()) {
            secondSubTag = codeTokens.nextToken();
        }
        if (codeTokens.hasMoreTokens()) {
            thirdSubTag = codeTokens.nextToken();
        }

        // now we must determine what is a language, what is a dialect or
        // a variant depending on RFC 3066's definitions.

        // 1. let's analyse the primary sub tag to see if we can use it as
        //    a language
        if (primaryTag.length() != 2) {
            // language codes other than 2 characters are not supported for
            // the moment.

            // we could correct or reduce this problem by implementing a
            // conversion table from ISO 639 part 2 (3-char) to ISO 639 part 1
            // (2-char), but for the moment we don't.

            /** @todo implement convertion from 3-char to 2-char for languages */
            return null;
        }
        language = primaryTag;

        // 2. the second tag can be either a country code if it's 2 characters
        //    long, or variant or dialect information.
        if (secondSubTag.length() == 2) {
            country = secondSubTag;
            variant = thirdSubTag;
        } else {
            variant = secondSubTag;
        }

        return newLocale(language, country, variant);
    }

    /**
     * Returns a language tags encoded (RFC 3066) compliant string from
     * a Java Locale object. Note that Locales without any languages will
     * not be accepted by this method and will return a null String since
     * language tags MUST contain a language
     *
     * @param locale the locale we want to be converted.
     *
     * @return a String containing the RFC 3066 encoded language information
     * extracted from the tag. If their is no language found in the locale a
     * NULL string is returned instead !
     */
    public static String localeToLanguageTag(Locale locale) {
        StringBuilder result = new StringBuilder();
        if (!("".equals(locale.getLanguage()))) {
            result.append(locale.getLanguage());
        } else {
            // if there is no language we can't return a valid
            // language tag.
            return null;
        }
        if (!("".equals(locale.getCountry()))) {
            result.append("-");
            result.append(locale.getCountry());
        }
        if (!("".equals(locale.getVariant()))) {
            result.append("-");
            result.append(locale.getVariant());
        }

        return result.toString();
    }

    /**
     * A small helper method that allows the various parameters to be empty
     * strings and always return valid locale objects. It mostly behaves likes
     * the regular Locale constructor, except for the case where the language
     * is empty that makes it return the default system Locale.
     *
     * @param language a 2-character ISO 639 part 1 compliant language code.
     * If the language is empty then the default system locale
     * will be returned.
     * @param country a 2-character ISO 3166 compliant country code
     * @param variant a String indicating the variant of the locale
     * @return a valid locale object using the given parameters, or the default
     * system locale if the language string was empty.
     */
    private static Locale newLocale(String language, String country, String variant) {
        if ("".equals(variant)) {
            if ("".equals(country)) {
                if ("".equals(language)) {
                    return Locale.ENGLISH;
                } else {
                    return new Locale(language, "");
                }
            } else {
                return new Locale(language, country);
            }
        } else {
            return new Locale(language, country, variant);
        }
    }

    public static List<Locale> getSortedLocaleList(Locale currentLocale) {
        List<Locale> sortedLocaleList = new ArrayList<Locale>(getAvailableLocales());
        Collections.sort(sortedLocaleList, LanguageCodeConverters.getLocaleDisplayNameComparator(currentLocale));
        return sortedLocaleList;
    }

    private static List<Locale> getAvailableLocales() {
        if (availableLocales == null) {
            List<Locale> locales = new LinkedList<>();
            for (Locale l : Locale.getAvailableLocales()) {
                if (isSupportedLocale(l)) {
                    locales.add(l);
                }
            }
            availableLocales = Collections.unmodifiableList(locales);
        }
        return availableLocales;
    }

    private static boolean isSupportedLocale(Locale l) {
        // we do not support locales with variants
        // we do not support scripts
        return StringUtils.isEmpty(l.getVariant()) && (StringUtils.isEmpty(l.getScript()));
    }

    public static Locale resolveLocaleForGuest(HttpServletRequest request) {
        List<Locale> availableBundleLocales = getAvailableBundleLocales();
        Enumeration<Locale> browserLocales = request.getLocales();
        Locale resolvedLocale = availableBundleLocales != null && !availableBundleLocales.isEmpty()
                ? availableBundleLocales.get(0)
                : Locale.ENGLISH;
        while (browserLocales != null && browserLocales.hasMoreElements()) {
            Locale candidate = browserLocales.nextElement();
            if (candidate != null) {
                if (availableBundleLocales.contains(candidate)) {
                    resolvedLocale = candidate;
                    break;
                } else if (StringUtils.isNotEmpty(candidate.getCountry())
                        && availableBundleLocales.contains(new Locale(candidate.getLanguage()))) {
                    resolvedLocale = new Locale(candidate.getLanguage());
                    break;
                }
            }
        }

        return resolvedLocale;
    }

    /**
     * Comparator implementation that compares locale display names in a certain
     * current locale.
     */
    public static class LocaleDisplayNameComparator implements Comparator<Locale> {

        private Collator collator = Collator.getInstance();
        private Locale currentLocale;

        public LocaleDisplayNameComparator(Locale locale) {
            if (locale != null) {
                this.currentLocale = locale;
                collator = Collator.getInstance(locale);
            }
        }

        public int compare(Locale locale1, Locale locale2) {
            return collator.compare(locale1.getDisplayName(currentLocale), locale2.getDisplayName(currentLocale));
        }

        public boolean equals(Object obj) {
            if (obj != null && this.getClass() == obj.getClass()) {
                return true;
            } else {
                return false;
            }
        }

        @Override
        public int hashCode() {
            return 0;
        }
    }

    public static LocaleDisplayNameComparator getLocaleDisplayNameComparator(Locale locale) {
        return new LocaleDisplayNameComparator(locale);
    }

    public static List<Locale> getAvailableBundleLocales(String resourceBundleName, Locale defaultLocale) {
        final List<Locale> availableBundleLocales = new LinkedList<Locale>();
        // first let's add the default locale if it exists.
        if (defaultLocale != null && ResourceBundle.getBundle(resourceBundleName, defaultLocale) != null) {
            availableBundleLocales.add(defaultLocale);
        }
        for (Locale locale : getAvailableLocales()) {
            if (!StringUtils.isEmpty(locale.getDisplayName())) { // Avoid "default/system" empty locale
                ResourceBundle res = ResourceBundle.getBundle(resourceBundleName, locale);
                if (res != null && res.getLocale().equals(locale)
                        && (defaultLocale == null || !locale.equals(defaultLocale))) {
                    availableBundleLocales.add(locale);
                }
            }
        }

        return availableBundleLocales;
    }

    public static List<String> localesToLanguageCodes(List<Locale> locales) {
        if (locales == null) {
            return null;
        }
        if (locales.isEmpty()) {
            return new ArrayList<String>();
        }
        List<String> languageCodes = new ArrayList<String>(locales.size());
        for (Locale locale : locales) {
            languageCodes.add(locale.toString());
        }
        return languageCodes;
    }

    public static List<Locale> getAvailableBundleLocales() {
        if (availableBundleLocales == null) {
            availableBundleLocales = getAvailableBundleLocales(ResourceBundles.JAHIA_INTERNAL_RESOURCES, null);
        }
        return availableBundleLocales;
    }

    public static List<Locale> getAvailableBundleLocalesSorted(Locale currentLocale) {
        Map<String, Locale> sortedLocales = new TreeMap<String, Locale>();
        for (Locale locale : getAvailableBundleLocales(ResourceBundles.JAHIA_INTERNAL_RESOURCES, null)) {
            sortedLocales.put(locale.getDisplayName(currentLocale), locale);
        }
        return new LinkedList<Locale>(sortedLocales.values());
    }

    public static List<Locale> getAvailableBundleLocalesSorted() {
        Map<String, Locale> sortedLocales = new TreeMap<String, Locale>();
        for (Locale locale : getAvailableBundleLocales(ResourceBundles.JAHIA_INTERNAL_RESOURCES, null)) {
            sortedLocales.put(WordUtils.capitalizeFully(locale.getDisplayName(locale)), locale);
        }
        return new LinkedList<Locale>(sortedLocales.values());
    }
}