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