View Javadoc
1   /*
2    * The contents of this file are subject to the terms of the Common Development and
3    * Distribution License (the License). You may not use this file except in compliance with the
4    * License.
5    *
6    * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7    * specific language governing permission and limitations under the License.
8    *
9    * When distributing Covered Software, include this CDDL Header Notice in each file and include
10   * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11   * Header, with the fields enclosed by brackets [] replaced by your own identifying
12   * information: "Portions copyright [year] [name of copyright owner]".
13   *
14   * Copyright 2015-2016 ForgeRock AS.
15   */
16  
17  package org.forgerock.util.i18n;
18  
19  import static org.forgerock.util.Utils.isNullOrEmpty;
20  
21  import java.util.Collections;
22  import java.util.List;
23  import java.util.Locale;
24  import java.util.ResourceBundle;
25  
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  /**
30   * This class encapsulates an ordered list of preferred locales, and the logic
31   * to use those to retrieve i18n {@code ResourceBundle}s.
32   * <p>
33   * {@code ResourceBundle}s are found by iterating over the preferred locales
34   * and returning the first resource bundle for which a non-ROOT locale is
35   * available, that is not listed later in the list, or the ROOT locale if no
36   * better match is found.
37   * <p>
38   * For example, given available locales of {@code en} and {@code fr}:
39   * <ul>
40   *     <li>
41   *         Preferred locales of {@code fr-FR, en}, resource bundle for locale
42   *         {@code fr} is returned.
43   *     </li>
44   *     <li>
45   *         Preferred locales of {@code fr-FR, en, fr}, resource bundle for locale
46   *         {@code en} is returned ({@code fr} is listed lower than {@code en}).
47   *     </li>
48   *     <li>
49   *         Preferred locales of {@code de}, resource bundle for the ROOT locale
50   *         is returned.
51   *     </li>
52   * </ul>
53   * It is expected that the default resource bundle locale (i.e. the locale for
54   * the bundle properties file that doesn't have a language tag, e.g.
55   * {@code MyBundle.properties}) is {@code en-US}. If this is not the case for an
56   * application, it can be changed by setting the
57   * {@code org.forgerock.defaultBundleLocale} system property.
58   */
59  public class PreferredLocales {
60  
61      private static final Locale DEFAULT_RESOURCE_BUNDLE_LOCALE =
62              Locale.forLanguageTag(System.getProperty("org.forgerock.defaultBundleLocale", "en-US"));
63      private static final Logger logger = LoggerFactory.getLogger(PreferredLocales.class);
64  
65      private final List<Locale> locales;
66      private final int numberLocales;
67  
68      /**
69       * Create a new preference of locales by copying the provided locales list.
70       * @param locales The list of locales that are preferred, with the first item the most preferred.
71       */
72      public PreferredLocales(List<Locale> locales) {
73          if (locales == null || locales.isEmpty()) {
74              locales = Collections.singletonList(Locale.ROOT);
75          }
76          this.locales = Collections.unmodifiableList(locales);
77          this.numberLocales = locales.size();
78      }
79  
80      /**
81       * Create a new, empty preference of locales.
82       */
83      public PreferredLocales() {
84          this(null);
85      }
86  
87      /**
88       * The preferred locale, i.e. the head of the preferred locales list.
89       * @return The most-preferred locale.
90       */
91      public Locale getPreferredLocale() {
92          return locales.get(0);
93      }
94  
95      /**
96       * The ordered list of preferred locales.
97       * @return A mutable copy of the preferred locales list.
98       */
99      public List<Locale> getLocales() {
100         return locales;
101     }
102 
103     /**
104      * Get a {@code ResourceBundle} using the preferred locale list and using the provided
105      * {@code ClassLoader}.
106      * @param bundleName The of the bundle to load.
107      * @param classLoader The {@code ClassLoader} to use to load the bundle.
108      * @return The bundle in the best matching locale.
109      */
110     public ResourceBundle getBundleInPreferredLocale(String bundleName, ClassLoader classLoader) {
111         logger.debug("Finding best {} bundle for locales {}", bundleName, locales);
112         for (int i = 0; i < numberLocales; i++) {
113             Locale locale = locales.get(i);
114             ResourceBundle candidate = ResourceBundle.getBundle(bundleName, locale, classLoader);
115             Locale candidateLocale = candidate.getLocale();
116             List<Locale> remainingLocales = locales.subList(i + 1, numberLocales);
117             if (matches(locale, candidateLocale, remainingLocales)) {
118                 logger.debug("Returning {} bundle in {} locale", bundleName, candidateLocale);
119                 return candidate;
120             }
121             if (candidateLocale.equals(Locale.ROOT)
122                     && matches(locale, DEFAULT_RESOURCE_BUNDLE_LOCALE, remainingLocales)) {
123                 return ResourceBundle.getBundle(bundleName, Locale.ROOT, classLoader);
124             }
125         }
126         logger.debug("Returning {} bundle in root locale", bundleName);
127         return ResourceBundle.getBundle(bundleName, Locale.ROOT, classLoader);
128     }
129 
130     /**
131      * Checks if the candidate locale the best match for the requested locale?
132      * Exclude {@code Locale.ROOT}, as it should be the fallback only when all locales are tried.
133      *
134      * @param requested The requested Locale
135      * @param candidate The candidate Locale
136      * @param remainingLocales The remaining locales
137      * @return The candidate is beast match for requested locale
138      */
139     public static boolean matches(Locale requested, Locale candidate, List<Locale> remainingLocales) {
140         logger.trace("Checking candidate locale {} for match with requested {}", candidate, requested);
141         if (requested.equals(candidate)) {
142             return true;
143         }
144         if (candidate.equals(Locale.ROOT)) {
145             logger.trace("Rejecting root locale as it is the default. Requested {}", requested);
146             return false;
147         }
148         String language = candidate.getLanguage();
149         if (!requested.getLanguage().equals(language)) {
150             return false;
151         }
152         String country = candidate.getCountry();
153         String variant = candidate.getVariant();
154         if (!isNullOrEmpty(variant)
155                 && remainingLocales.contains(new Locale(language, country, variant))) {
156             return false;
157         }
158         if ((!isNullOrEmpty(country) || !isNullOrEmpty(variant))
159                 && remainingLocales.contains(new Locale(language, country))) {
160             return false;
161         }
162         if (remainingLocales.contains(new Locale(language))) {
163             return false;
164         }
165         return true;
166     }
167 
168 }