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 }