001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2015 ForgeRock AS.
015 */
016
017package org.forgerock.util.i18n;
018
019import java.util.Collections;
020import java.util.List;
021import java.util.Locale;
022import java.util.ResourceBundle;
023
024/**
025 * This class encapsulates an ordered list of preferred locales, and the logic
026 * to use those to retrieve i18n {@code ResourceBundle}s.
027 * <p>
028 * {@code ResourceBundle}s are found by iterating over the preferred locales
029 * and returning the first resource bundle for which a non-ROOT locale is
030 * available, that is not listed later in the list, or the ROOT locale if no
031 * better match is found.
032 * <p>
033 * For example, given available locales of {@code en} and {@code fr}:
034 * <ul>
035 *     <li>
036 *         Preferred locales of {@code fr-FR, en}, resource bundle for locale
037 *         {@code fr} is returned.
038 *     </li>
039 *     <li>
040 *         Preferred locales of {@code fr-FR, en, fr}, resource bundle for locale
041 *         {@code en} is returned ({@code fr} is listed lower than {@code en}).
042 *     </li>
043 *     <li>
044 *         Preferred locales of {@code de}, resource bundle for the ROOT locale
045 *         is returned.
046 *     </li>
047 * </ul>
048 */
049public class PreferredLocales {
050
051    private final List<Locale> locales;
052
053    /**
054     * Create a new preference of locales by copying the provided locales list.
055     * @param locales The list of locales that are preferred, with the first item the most preferred.
056     */
057    public PreferredLocales(List<Locale> locales) {
058        if (locales == null || locales.isEmpty()) {
059            locales = Collections.singletonList(Locale.ROOT);
060        }
061        this.locales = Collections.unmodifiableList(locales);
062    }
063
064    /**
065     * Create a new, empty preference of locales.
066     */
067    public PreferredLocales() {
068        this(null);
069    }
070
071    /**
072     * The preferred locale, i.e. the head of the preferred locales list.
073     * @return The most-preferred locale.
074     */
075    public Locale getPreferredLocale() {
076        return locales.get(0);
077    }
078
079    /**
080     * The ordered list of preferred locales.
081     * @return A mutable copy of the preferred locales list.
082     */
083    public List<Locale> getLocales() {
084        return locales;
085    }
086
087    /**
088     * Get a {@code ResourceBundle} using the preferred locale list and using the provided
089     * {@code ClassLoader}.
090     * @param bundleName The of the bundle to load.
091     * @param classLoader The {@code ClassLoader} to use to load the bundle.
092     * @return The bundle in the best matching locale.
093     */
094    public ResourceBundle getBundleInPreferredLocale(String bundleName, ClassLoader classLoader) {
095        for (Locale locale : locales) {
096            ResourceBundle candidate = ResourceBundle.getBundle(bundleName, locale, classLoader);
097            if (matches(locale, candidate.getLocale())) {
098                return candidate;
099            }
100        }
101        return ResourceBundle.getBundle(bundleName, Locale.ROOT, classLoader);
102    }
103
104    /**
105     * Is the candidate locale the best match for the requested locale? Exclude {@code Locale.ROOT} unless it
106     * is the requested locale, as it should be the fallback only when all locales are tried.
107     */
108    private boolean matches(Locale requested, Locale candidate) {
109        return candidate.equals(requested) || (!Locale.ROOT.equals(candidate) && !locales.contains(candidate));
110    }
111
112
113}