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}