/*
* 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";
}
}