001: // Copyright (C) 1998-2001 by Jason Hunter <jhunter_AT_acm_DOT_org>.
002: // All rights reserved. Use of this class is limited.
003: // Please see the LICENSE for more information.
004:
005: package com.oreilly.servlet;
006:
007: import java.io.*;
008: import java.util.*;
009:
010: import com.oreilly.servlet.LocaleToCharsetMap;
011:
012: /**
013: * A class to aid in servlet internationalization. It determines, from a
014: * client request, the best charset, locale, and resource bundle to use
015: * with the response.
016: * <p>
017: * LocaleNegotiator works by scanning through the client's language
018: * preferences (sent by browsers in the <tt>Accept-Language</tt> header)
019: * looking for any
020: * language for which there exists is a corresponding resource bundle.
021: * When it finds a correspondence, it uses the LocaleToCharsetMap class
022: * to determine the charset. If there's any problem, it tries to fall
023: * back to US English. The logic currently ignores the client's charset
024: * preferences (sent in the <tt>Accept-Charset</tt> header).
025: * <p>
026: * It can be used like this:
027: * <blockquote><pre>
028: * String bundleName = "BundleName";
029: * String acceptLanguage = req.getHeader("Accept-Language");
030: * String acceptCharset = req.getHeader("Accept-Charset");
031: *
032: * LocaleNegotiator negotiator =
033: * new LocaleNegotiator(bundleName, acceptLanguage, acceptCharset);
034: *
035: * Locale locale = negotiator.getLocale();
036: * String charset = negotiator.getCharset();
037: * ResourceBundle bundle = negotiator.getBundle(); // may be null
038: *
039: * res.setContentType("text/plain; charset=" + charset);
040: * res.setHeader("Content-Language", locale.getLanguage());
041: * res.setHeader("Vary", "Accept-Language");
042: *
043: * PrintWriter out = res.getWriter();
044: *
045: * out.println(bundle.getString("resource"));
046: * </pre></blockquote>
047: *
048: * @see com.oreilly.servlet.LocaleToCharsetMap
049: *
050: * @author <b>Jason Hunter</b>, Copyright © 1998
051: * @version 1.0, 98/09/18
052: */
053: public class LocaleNegotiator {
054:
055: private ResourceBundle chosenBundle;
056: private Locale chosenLocale;
057: private String chosenCharset;
058:
059: /**
060: * Constructs a new LocaleNegotiator for the given bundle name, language
061: * list, and charset list.
062: *
063: * @param bundleName the resource bundle name
064: * @param languages the Accept-Language header
065: * @param charsets the Accept-Charset header
066: */
067: public LocaleNegotiator(String bundleName, String languages,
068: String charsets) {
069:
070: // Specify default values:
071: // English language, ISO-8859-1 (Latin-1) charset, English bundle
072: Locale defaultLocale = new Locale("en", "US");
073: String defaultCharset = "ISO-8859-1";
074: ResourceBundle defaultBundle = null;
075: try {
076: defaultBundle = ResourceBundle.getBundle(bundleName,
077: defaultLocale);
078: } catch (MissingResourceException e) {
079: // No default bundle was found. Flying without a net.
080: }
081:
082: // If the client didn't specify acceptable languages, we can keep
083: // the defaults.
084: if (languages == null) {
085: chosenLocale = defaultLocale;
086: chosenCharset = defaultCharset;
087: chosenBundle = defaultBundle;
088: return; // quick exit
089: }
090:
091: // Use a tokenizer to separate acceptable languages
092: StringTokenizer tokenizer = new StringTokenizer(languages, ",");
093:
094: while (tokenizer.hasMoreTokens()) {
095: // Get the next acceptable language.
096: // (The language can look something like "en; qvalue=0.91")
097: String lang = tokenizer.nextToken();
098:
099: // Get the locale for that language
100: Locale loc = getLocaleForLanguage(lang);
101:
102: // Get the bundle for this locale. Don't let the search fallback
103: // to match other languages!
104: ResourceBundle bundle = getBundleNoFallback(bundleName, loc);
105:
106: // The returned bundle is null if there's no match. In that case
107: // we can't use this language since the servlet can't speak it.
108: if (bundle == null)
109: continue; // on to the next language
110:
111: // Find a charset we can use to display that locale's language.
112: String charset = getCharsetForLocale(loc, charsets);
113:
114: // The returned charset is null if there's no match. In that case
115: // we can't use this language since the servlet can't encode it.
116: if (charset == null)
117: continue; // on to the next language
118:
119: // If we get here, there are no problems with this language.
120: chosenLocale = loc;
121: chosenBundle = bundle;
122: chosenCharset = charset;
123: return; // we're done
124: }
125:
126: // No matches, so we let the defaults stand
127: chosenLocale = defaultLocale;
128: chosenCharset = defaultCharset;
129: chosenBundle = defaultBundle;
130: }
131:
132: /**
133: * Gets the chosen bundle.
134: *
135: * @return the chosen bundle
136: */
137: public ResourceBundle getBundle() {
138: return chosenBundle;
139: }
140:
141: /**
142: * Gets the chosen locale.
143: *
144: * @return the chosen locale
145: */
146: public Locale getLocale() {
147: return chosenLocale;
148: }
149:
150: /**
151: * Gets the chosen charset.
152: *
153: * @return the chosen charset
154: */
155: public String getCharset() {
156: return chosenCharset;
157: }
158:
159: /*
160: * Gets a Locale object for a given language string
161: */
162: private Locale getLocaleForLanguage(String lang) {
163: Locale loc;
164: int semi, dash;
165:
166: // Cut off any qvalue that might come after a semi-colon
167: if ((semi = lang.indexOf(';')) != -1) {
168: lang = lang.substring(0, semi);
169: }
170:
171: // Trim any whitespace
172: lang = lang.trim();
173:
174: // Create a Locale from the language. A dash may separate the
175: // language from the country.
176: if ((dash = lang.indexOf('-')) == -1) {
177: loc = new Locale(lang, ""); // No dash, no country
178: } else {
179: loc = new Locale(lang.substring(0, dash), lang
180: .substring(dash + 1));
181: }
182:
183: return loc;
184: }
185:
186: /*
187: * Gets a ResourceBundle object for the given bundle name and locale,
188: * or null if the bundle can't be found. The resource bundle must match
189: * the locale exactly. Fallback matches are not permitted.
190: */
191: private ResourceBundle getBundleNoFallback(String bundleName,
192: Locale loc) {
193:
194: // First get the fallback bundle -- the bundle that will be selected
195: // if getBundle() can't find a direct match. This bundle can be
196: // compared to the bundles returned by later calls to getBundle() in
197: // order to detect when getBundle() finds a direct match.
198: ResourceBundle fallback = null;
199: try {
200: fallback = ResourceBundle.getBundle(bundleName, new Locale(
201: "bogus", ""));
202: } catch (MissingResourceException e) {
203: // No fallback bundle was found.
204: }
205:
206: try {
207: // Get the bundle for the specified locale
208: ResourceBundle bundle = ResourceBundle.getBundle(
209: bundleName, loc);
210:
211: // Is the bundle different than our fallback bundle?
212: if (bundle != fallback) {
213: // We have a real match!
214: return bundle;
215: }
216: // So the bundle is the same as our fallback bundle.
217: // We can still have a match, but only if our locale's language
218: // matches the default locale's language.
219: else if (bundle == fallback
220: && loc.getLanguage().equals(
221: Locale.getDefault().getLanguage())) {
222: // Another way to match
223: return bundle;
224: } else {
225: // No match, keep looking
226: }
227: } catch (MissingResourceException e) {
228: // No bundle available for this locale
229: }
230:
231: return null; // no match
232: }
233:
234: /**
235: * Gets the best charset for a given locale, selecting from a charset list.
236: * Currently ignores the charset list. Subclasses can override this
237: * method to take the list into account.
238: *
239: * @param loc the locale
240: * @param charsets a comma-separated charset list
241: * @return the best charset for the given locale from the given list
242: */
243: protected String getCharsetForLocale(Locale loc, String charsets) {
244: // Note: This method ignores the client-specified charsets
245: return LocaleToCharsetMap.getCharset(loc);
246: }
247: }
|