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-2016 ForgeRock AS. 015 */ 016 017package org.forgerock.util.i18n; 018 019import static org.forgerock.util.Utils.isNullOrEmpty; 020 021import java.util.Collections; 022import java.util.List; 023import java.util.Locale; 024import java.util.ResourceBundle; 025 026import org.slf4j.Logger; 027import org.slf4j.LoggerFactory; 028 029/** 030 * This class encapsulates an ordered list of preferred locales, and the logic 031 * to use those to retrieve i18n {@code ResourceBundle}s. 032 * <p> 033 * {@code ResourceBundle}s are found by iterating over the preferred locales 034 * and returning the first resource bundle for which a non-ROOT locale is 035 * available, that is not listed later in the list, or the ROOT locale if no 036 * better match is found. 037 * <p> 038 * For example, given available locales of {@code en} and {@code fr}: 039 * <ul> 040 * <li> 041 * Preferred locales of {@code fr-FR, en}, resource bundle for locale 042 * {@code fr} is returned. 043 * </li> 044 * <li> 045 * Preferred locales of {@code fr-FR, en, fr}, resource bundle for locale 046 * {@code en} is returned ({@code fr} is listed lower than {@code en}). 047 * </li> 048 * <li> 049 * Preferred locales of {@code de}, resource bundle for the ROOT locale 050 * is returned. 051 * </li> 052 * </ul> 053 * It is expected that the default resource bundle locale (i.e. the locale for 054 * the bundle properties file that doesn't have a language tag, e.g. 055 * {@code MyBundle.properties}) is {@code en-US}. If this is not the case for an 056 * application, it can be changed by setting the 057 * {@code org.forgerock.defaultBundleLocale} system property. 058 */ 059public class PreferredLocales { 060 061 private static final Locale DEFAULT_RESOURCE_BUNDLE_LOCALE = 062 Locale.forLanguageTag(System.getProperty("org.forgerock.defaultBundleLocale", "en-US")); 063 private static final Logger logger = LoggerFactory.getLogger(PreferredLocales.class); 064 065 private final List<Locale> locales; 066 private final int numberLocales; 067 068 /** 069 * Create a new preference of locales by copying the provided locales list. 070 * @param locales The list of locales that are preferred, with the first item the most preferred. 071 */ 072 public PreferredLocales(List<Locale> locales) { 073 if (locales == null || locales.isEmpty()) { 074 locales = Collections.singletonList(Locale.ROOT); 075 } 076 this.locales = Collections.unmodifiableList(locales); 077 this.numberLocales = locales.size(); 078 } 079 080 /** 081 * Create a new, empty preference of locales. 082 */ 083 public PreferredLocales() { 084 this(null); 085 } 086 087 /** 088 * The preferred locale, i.e. the head of the preferred locales list. 089 * @return The most-preferred locale. 090 */ 091 public Locale getPreferredLocale() { 092 return locales.get(0); 093 } 094 095 /** 096 * The ordered list of preferred locales. 097 * @return A mutable copy of the preferred locales list. 098 */ 099 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}