001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.lang3;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Locale;
025import java.util.Set;
026import java.util.concurrent.ConcurrentHashMap;
027import java.util.concurrent.ConcurrentMap;
028import java.util.function.Predicate;
029import java.util.stream.Collectors;
030
031/**
032 * Operations to assist when working with a {@link Locale}.
033 *
034 * <p>This class tries to handle {@code null} input gracefully.
035 * An exception will not be thrown for a {@code null} input.
036 * Each method documents its behavior in more detail.</p>
037 *
038 * @since 2.2
039 */
040public class LocaleUtils {
041    private static final char UNDERSCORE = '_';
042    private static final char DASH = '-';
043
044    // class to avoid synchronization (Init on demand)
045    static class SyncAvoid {
046        /** Unmodifiable list of available locales. */
047        private static final List<Locale> AVAILABLE_LOCALE_LIST;
048        /** Unmodifiable set of available locales. */
049        private static final Set<Locale> AVAILABLE_LOCALE_SET;
050
051        static {
052            final List<Locale> list = new ArrayList<>(Arrays.asList(Locale.getAvailableLocales()));  // extra safe
053            AVAILABLE_LOCALE_LIST = Collections.unmodifiableList(list);
054            AVAILABLE_LOCALE_SET = Collections.unmodifiableSet(new HashSet<>(list));
055        }
056    }
057
058    /** Concurrent map of language locales by country. */
059    private static final ConcurrentMap<String, List<Locale>> cLanguagesByCountry =
060        new ConcurrentHashMap<>();
061
062    /** Concurrent map of country locales by language. */
063    private static final ConcurrentMap<String, List<Locale>> cCountriesByLanguage =
064        new ConcurrentHashMap<>();
065
066    /**
067     * Obtains an unmodifiable list of installed locales.
068     *
069     * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
070     * It is more efficient, as the JDK method must create a new array each
071     * time it is called.</p>
072     *
073     * @return the unmodifiable list of available locales
074     */
075    public static List<Locale> availableLocaleList() {
076        return SyncAvoid.AVAILABLE_LOCALE_LIST;
077    }
078
079    private static List<Locale> availableLocaleList(final Predicate<Locale> predicate) {
080        return availableLocaleList().stream().filter(predicate).collect(Collectors.toList());
081    }
082
083    /**
084     * Obtains an unmodifiable set of installed locales.
085     *
086     * <p>This method is a wrapper around {@link Locale#getAvailableLocales()}.
087     * It is more efficient, as the JDK method must create a new array each
088     * time it is called.</p>
089     *
090     * @return the unmodifiable set of available locales
091     */
092    public static Set<Locale> availableLocaleSet() {
093        return SyncAvoid.AVAILABLE_LOCALE_SET;
094    }
095
096    /**
097     * Obtains the list of countries supported for a given language.
098     *
099     * <p>This method takes a language code and searches to find the
100     * countries available for that language. Variant locales are removed.</p>
101     *
102     * @param languageCode  the 2 letter language code, null returns empty
103     * @return an unmodifiable List of Locale objects, not null
104     */
105    public static List<Locale> countriesByLanguage(final String languageCode) {
106        if (languageCode == null) {
107            return Collections.emptyList();
108        }
109        return cCountriesByLanguage.computeIfAbsent(languageCode, lc -> Collections.unmodifiableList(
110            availableLocaleList(locale -> languageCode.equals(locale.getLanguage()) && !locale.getCountry().isEmpty() && locale.getVariant().isEmpty())));
111    }
112
113    /**
114     * Checks if the locale specified is in the set of available locales.
115     *
116     * @param locale the Locale object to check if it is available
117     * @return true if the locale is a known locale
118     */
119    public static boolean isAvailableLocale(final Locale locale) {
120        return availableLocaleSet().contains(locale);
121    }
122
123    /**
124     * Checks whether the given String is a ISO 3166 alpha-2 country code.
125     *
126     * @param str the String to check
127     * @return true, is the given String is a ISO 3166 compliant country code.
128     */
129    private static boolean isISO3166CountryCode(final String str) {
130        return StringUtils.isAllUpperCase(str) && str.length() == 2;
131    }
132
133    /**
134     * Checks whether the given String is a ISO 639 compliant language code.
135     *
136     * @param str the String to check.
137     * @return true, if the given String is a ISO 639 compliant language code.
138     */
139    private static boolean isISO639LanguageCode(final String str) {
140        return StringUtils.isAllLowerCase(str) && (str.length() == 2 || str.length() == 3);
141    }
142
143    /**
144     * Checks whether the given String is a UN M.49 numeric area code.
145     *
146     * @param str the String to check
147     * @return true, is the given String is a UN M.49 numeric area code.
148     */
149    private static boolean isNumericAreaCode(final String str) {
150        return StringUtils.isNumeric(str) && str.length() == 3;
151    }
152
153    /**
154     * Obtains the list of languages supported for a given country.
155     *
156     * <p>This method takes a country code and searches to find the
157     * languages available for that country. Variant locales are removed.</p>
158     *
159     * @param countryCode  the 2-letter country code, null returns empty
160     * @return an unmodifiable List of Locale objects, not null
161     */
162    public static List<Locale> languagesByCountry(final String countryCode) {
163        if (countryCode == null) {
164            return Collections.emptyList();
165        }
166        return cLanguagesByCountry.computeIfAbsent(countryCode,
167            k -> Collections.unmodifiableList(availableLocaleList(locale -> countryCode.equals(locale.getCountry()) && locale.getVariant().isEmpty())));
168    }
169
170    /**
171     * Obtains the list of locales to search through when performing
172     * a locale search.
173     *
174     * <pre>
175     * localeLookupList(Locale("fr", "CA", "xxx"))
176     *   = [Locale("fr", "CA", "xxx"), Locale("fr", "CA"), Locale("fr")]
177     * </pre>
178     *
179     * @param locale  the locale to start from
180     * @return the unmodifiable list of Locale objects, 0 being locale, not null
181     */
182    public static List<Locale> localeLookupList(final Locale locale) {
183        return localeLookupList(locale, locale);
184    }
185
186    /**
187     * Obtains the list of locales to search through when performing
188     * a locale search.
189     *
190     * <pre>
191     * localeLookupList(Locale("fr", "CA", "xxx"), Locale("en"))
192     *   = [Locale("fr", "CA", "xxx"), Locale("fr", "CA"), Locale("fr"), Locale("en"]
193     * </pre>
194     *
195     * <p>The result list begins with the most specific locale, then the
196     * next more general and so on, finishing with the default locale.
197     * The list will never contain the same locale twice.</p>
198     *
199     * @param locale  the locale to start from, null returns empty list
200     * @param defaultLocale  the default locale to use if no other is found
201     * @return the unmodifiable list of Locale objects, 0 being locale, not null
202     */
203    public static List<Locale> localeLookupList(final Locale locale, final Locale defaultLocale) {
204        final List<Locale> list = new ArrayList<>(4);
205        if (locale != null) {
206            list.add(locale);
207            if (!locale.getVariant().isEmpty()) {
208                list.add(new Locale(locale.getLanguage(), locale.getCountry()));
209            }
210            if (!locale.getCountry().isEmpty()) {
211                list.add(new Locale(locale.getLanguage(), StringUtils.EMPTY));
212            }
213            if (!list.contains(defaultLocale)) {
214                list.add(defaultLocale);
215            }
216        }
217        return Collections.unmodifiableList(list);
218    }
219
220    /**
221     * Tries to parse a locale from the given String.
222     *
223     * @param str the String to parse a locale from.
224     * @return a Locale instance parsed from the given String.
225     * @throws IllegalArgumentException if the given String can not be parsed.
226     */
227    private static Locale parseLocale(final String str) {
228        if (isISO639LanguageCode(str)) {
229            return new Locale(str);
230        }
231
232        final String[] segments = str.indexOf(UNDERSCORE) != -1
233            ? str.split(String.valueOf(UNDERSCORE), -1)
234            : str.split(String.valueOf(DASH), -1);
235        final String language = segments[0];
236        if (segments.length == 2) {
237            final String country = segments[1];
238            if (isISO639LanguageCode(language) && isISO3166CountryCode(country) ||
239                    isNumericAreaCode(country)) {
240                return new Locale(language, country);
241            }
242        } else if (segments.length == 3) {
243            final String country = segments[1];
244            final String variant = segments[2];
245            if (isISO639LanguageCode(language) &&
246                    (country.isEmpty() || isISO3166CountryCode(country) || isNumericAreaCode(country)) &&
247                    !variant.isEmpty()) {
248                return new Locale(language, country, variant);
249            }
250        }
251        throw new IllegalArgumentException("Invalid locale format: " + str);
252    }
253
254    /**
255     * Returns the given locale if non-{@code null}, otherwise {@link Locale#getDefault()}.
256     *
257     * @param locale a locale or {@code null}.
258     * @return the given locale if non-{@code null}, otherwise {@link Locale#getDefault()}.
259     * @since 3.12.0
260     */
261    public static Locale toLocale(final Locale locale) {
262        return locale != null ? locale : Locale.getDefault();
263    }
264
265    /**
266     * Converts a String to a Locale.
267     *
268     * <p>This method takes the string format of a locale and creates the
269     * locale object from it.</p>
270     *
271     * <pre>
272     *   LocaleUtils.toLocale("")           = new Locale("", "")
273     *   LocaleUtils.toLocale("en")         = new Locale("en", "")
274     *   LocaleUtils.toLocale("en_GB")      = new Locale("en", "GB")
275     *   LocaleUtils.toLocale("en-GB")      = new Locale("en", "GB")
276     *   LocaleUtils.toLocale("en_001")     = new Locale("en", "001")
277     *   LocaleUtils.toLocale("en_GB_xxx")  = new Locale("en", "GB", "xxx")   (#)
278     * </pre>
279     *
280     * <p>(#) The behavior of the JDK variant constructor changed between JDK1.3 and JDK1.4.
281     * In JDK1.3, the constructor upper cases the variant, in JDK1.4, it doesn't.
282     * Thus, the result from getVariant() may vary depending on your JDK.</p>
283     *
284     * <p>This method validates the input strictly.
285     * The language code must be lowercase.
286     * The country code must be uppercase.
287     * The separator must be an underscore or a dash.
288     * The length must be correct.
289     * </p>
290     *
291     * @param str  the locale String to convert, null returns null
292     * @return a Locale, null if null input
293     * @throws IllegalArgumentException if the string is an invalid format
294     * @see Locale#forLanguageTag(String)
295     */
296    public static Locale toLocale(final String str) {
297        if (str == null) {
298            // TODO Should this return the default locale?
299            return null;
300        }
301        if (str.isEmpty()) { // LANG-941 - JDK 8 introduced an empty locale where all fields are blank
302            return new Locale(StringUtils.EMPTY, StringUtils.EMPTY);
303        }
304        if (str.contains("#")) { // LANG-879 - Cannot handle Java 7 script & extensions
305            throw new IllegalArgumentException("Invalid locale format: " + str);
306        }
307        final int len = str.length();
308        if (len < 2) {
309            throw new IllegalArgumentException("Invalid locale format: " + str);
310        }
311        final char ch0 = str.charAt(0);
312        if (ch0 == UNDERSCORE || ch0 == DASH) {
313            if (len < 3) {
314                throw new IllegalArgumentException("Invalid locale format: " + str);
315            }
316            final char ch1 = str.charAt(1);
317            final char ch2 = str.charAt(2);
318            if (!Character.isUpperCase(ch1) || !Character.isUpperCase(ch2)) {
319                throw new IllegalArgumentException("Invalid locale format: " + str);
320            }
321            if (len == 3) {
322                return new Locale(StringUtils.EMPTY, str.substring(1, 3));
323            }
324            if (len < 5) {
325                throw new IllegalArgumentException("Invalid locale format: " + str);
326            }
327            if (str.charAt(3) != ch0) {
328                throw new IllegalArgumentException("Invalid locale format: " + str);
329            }
330            return new Locale(StringUtils.EMPTY, str.substring(1, 3), str.substring(4));
331        }
332
333        return parseLocale(str);
334    }
335
336    /**
337     * {@link LocaleUtils} instances should NOT be constructed in standard programming.
338     * Instead, the class should be used as {@code LocaleUtils.toLocale("en_GB");}.
339     *
340     * <p>This constructor is public to permit tools that require a JavaBean instance
341     * to operate.</p>
342     */
343    public LocaleUtils() {
344    }
345
346}