/* * Copyright 2008 Google Inc. * * 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 com.google.gwt.libideas.server; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * A class to choose the best locale given a list of supported locales and * an HTTP Accept-Language header. */ public class LocaleMatcher { /** * Class representing entries in an Accept-Language header. */ private static class AcceptLanguageEntry implements Comparable<AcceptLanguageEntry> { public String language; public double priority; public AcceptLanguageEntry(String language, double priority) { this.language = language; this.priority = priority; } /** * Compare in descending order, since a priority of 1.0 is higher than a 0.8 */ public int compareTo(AcceptLanguageEntry other) { if (priority < other.priority) { return 1; } else if (priority > other.priority) { return -1; } else { return 0; } } @Override public String toString() { return language + " (" + priority + ")"; } } /** * Set of supported locales. The value is the unmangled locale code, but it * is stored in the map smashed to lowercase for ease of matching. */ private Map<String, String> supportedLocales = new HashMap<String, String>(); /** * Construct a locale matcher given a collection of locales. * * The collection of supported locales would commonly be generated by * {@link com.google.gwt.libideas.linker.LocaleListLinker} or some * other custom linker. * * @param locales a collection of supported locales */ public LocaleMatcher(Iterable<String> locales) { for (String locale : locales) { if (!"default".equals(locale)) { supportedLocales.put(locale.toLowerCase(), locale); } } } /** * Construct a locale matcher given a stream with a list of * locales, one per line. The stream should be UTF8 (typically just ASCII). * * The file containing the list of supported locales would commonly be * generated by {@link com.google.gwt.libideas.linker.LocaleListLinker}. * * @param localeListStream input stream to read locale list from -- if null, * no locales are read. * @throws IOException if an error occurs reading the stream */ public LocaleMatcher(InputStream localeListStream) throws IOException { if (localeListStream == null) { return; } BufferedReader reader = new BufferedReader(new InputStreamReader(localeListStream, "UTF-8")); String line; while ((line = reader.readLine()) != null) { if (!"default".equals(line)) { supportedLocales.put(line.toLowerCase(), line); } } } /** * Select the best match from the list of available locales. If no match * is found, "default" will be returned. * * The Accept-Languages header is of the format: * locale[;q=number][,locale[;q=number]]* * * The numbers are priorities between 0 and 1, with a higher value indicating * higher preference. Note that Accept-Language locale identifiers only include * a language and a country code, and they are separated by dashes not underscores. * * @param acceptLang the Accept-Languages header * @return the selected locale name */ public String findBestMatch(String acceptLang) { if (acceptLang != null) { // Break into individual language entries. ArrayList<AcceptLanguageEntry> languages = new ArrayList<AcceptLanguageEntry>(); int pos = 0; while (pos < acceptLang.length()) { int comma = acceptLang.indexOf(',', pos); String oneLang; if (comma >= 0) { oneLang = acceptLang.substring(pos, comma); pos = comma + 1; } else { oneLang = acceptLang.substring(pos); pos = acceptLang.length(); } double priority = 1.0; int semi = oneLang.indexOf(";q="); if (semi >= 0) { priority = Double.valueOf(oneLang.substring(semi + 3)); oneLang = oneLang.substring(0, semi); } if (priority > 0) { languages.add(new AcceptLanguageEntry(oneLang, priority)); } } // Look for exact matches first. Collections.sort(languages); for (AcceptLanguageEntry language : languages) { String languageName = language.language; languageName = languageName.replace('-', '_').toLowerCase(); String match = supportedLocales.get(languageName); if (match != null) { return match; } } // No exact match, try stripping any country tags from each entry. for (AcceptLanguageEntry language : languages) { String languageName = language.language; pos = languageName.indexOf('-'); if (pos >= 0) { languageName = languageName.substring(0, pos).toLowerCase(); String match = supportedLocales.get(languageName); if (match != null) { return match; } } } } return "default"; } }