/* Copyright 2005-2006 Tim Fennell * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.sourceforge.stripes.localization; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.util.StringUtil; import javax.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Collections; import java.util.Map; import java.util.HashMap; import java.nio.charset.Charset; /** * <p>Default locale picker that uses a comma separated list of locales in the servlet init * parameters to determine the set of locales that are supported by the application. Then at * request time matches the user's preference order list as specified by the headers included * in the request until it finds one of those locales in the system list. If a match cannot be * found, the first locale in the system list will be picked. If there is no list of configured * locales then the picker will default the list to a one entry list containing the system locale.</p> * * <p>Locales are hierarchical, with up to three levels designating language, country and * variant. Only the first level (language) is required. To provide the best match possible the * DefaultLocalePicker tracks the one-level matches, two-level matches and three-level matches. If * a three level match is found, it will be returned. If not the first two-level match will be * returned if one was found. If not, the first one-level match will be returned. If not even a * one-level match is found, the first locale supported by the system is returned.</p> * * @author Tim Fennell */ public class DefaultLocalePicker implements LocalePicker { /** * The configuration parameter that is used to lookup a comma separated list of locales that * the system supports. */ public static final String LOCALE_LIST = "LocalePicker.Locales"; /** Log instance for use within the class. */ private static final Log log = Log.getInstance(DefaultLocalePicker.class); /** Stores a reference to the configuration passed in at initialization. */ protected Configuration configuration; /** Stores the configured set of Locales that the system supports, looked up at init time. */ protected List<Locale> locales = new ArrayList<Locale>(); /** Contains a map of Locale to preferred character encoding. */ protected Map<Locale,String> encodings = new HashMap<Locale,String>(); /** * Attempts to read the * @param configuration * @throws Exception */ public void init(Configuration configuration) throws Exception { this.configuration = configuration; String configuredLocales = configuration.getBootstrapPropertyResolver().getProperty(LOCALE_LIST); if (configuredLocales == null || configuredLocales.equals("")) { log.info("No locale list specified, defaulting to single locale: ", Locale.getDefault()); this.locales.add(Locale.getDefault()); } else { // Split apart the Locales on commas, and then parse the local strings into their bits String[] localeStrings = StringUtil.standardSplit(configuredLocales); for (String localeString : localeStrings) { // Each locale string can be made up of two parts, locale:encoding // and the locale can be made up of up to three segment, e.g. en_US_PC String[] halves = localeString.split(":"); String[] parts = halves[0].split("[-_]"); Locale locale = null; if (parts.length == 1) { locale = new Locale(parts[0].trim().toLowerCase()); } else if (parts.length == 2) { locale = new Locale(parts[0].trim().toLowerCase(), parts[1].trim().toUpperCase()); } else if (parts.length == 3) { locale = new Locale(parts[0].trim().toLowerCase(), parts[1].trim().toUpperCase(), parts[2].trim()); } else { log.error("Configuration property ", LOCALE_LIST, " contained a locale value ", "that split into more than three parts! The parts were: ", parts); } this.locales.add(locale); // Now check to see if a character encoding was specified, and if so is it valid if (halves.length == 2) { String encoding = halves[1]; if (Charset.isSupported(encoding)) { this.encodings.put(locale, halves[1]); } else { log.error("Configuration property ", LOCALE_LIST, " contained a locale value ", "with an unsupported character encoding. The offending entry is: ", localeString); } } } log.debug("Configured DefaultLocalePicker with locales: ", this.locales); log.debug("Configured DefaultLocalePicker with encodings: ", this.encodings); } } /** * Uses a preference matching algorithm to pick a Locale for the user's request. Iterates * through the user's acceptable list of Locales, matching them against the system list. On the * way through the list records the first Locale to match on Language, and the first locale to * match on both Language and Country. If a match is found for all three, Language, Country * and Variant, it will be returned. If no three-way match is found the first two-way match * found will be returned. If no two-way match way found the first one-way match found will * be returned. If no one way match was found, the default system locale will be returned. * * @param request the request being processed * @return a Locale to use in processing the request */ @SuppressWarnings("unchecked") public Locale pickLocale(HttpServletRequest request) { Locale oneWayMatch = null; Locale twoWayMatch= null; List<Locale> preferredLocales = Collections.list(request.getLocales()); for (Locale preferredLocale : preferredLocales) { for (Locale systemLocale : this.locales) { if ( systemLocale.getLanguage().equals(preferredLocale.getLanguage()) ) { // We have a language match, let's go for two! oneWayMatch = (oneWayMatch == null ? systemLocale : oneWayMatch); String systemCountry = systemLocale.getCountry(); String preferredCountry = preferredLocale.getCountry(); if ( (systemCountry == null && preferredCountry == null) || (systemCountry != null && systemCountry.equals(preferredCountry)) ) { // Ooh, we have a two way match, can we make three? twoWayMatch = (twoWayMatch == null ? systemLocale : twoWayMatch); String systemVariant = systemLocale.getVariant(); String preferredVariant = preferredLocale.getVariant(); if ( (systemVariant == null && preferredVariant == null) || (systemVariant != null && systemVariant.equals(preferredVariant)) ) { // Bingo! You sunk my battleship! return systemLocale; } } } } } // We didn't get a match complete match, maybe partial will do if (twoWayMatch != null) { return twoWayMatch; } else if (oneWayMatch != null) { return oneWayMatch; } else { return this.locales.get(0); } } /** * Returns the character encoding to use for the request and locale if one has been * specified in the configuration. If no value has been specified, returns null. * * @param request the current request * @param locale the locale picked for the request * @return a valid character encoding or null */ public String pickCharacterEncoding(HttpServletRequest request, Locale locale) { return this.encodings.get(locale); } }