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 005package com.oreilly.servlet; 006 007import java.io.*; 008import java.util.*; 009 010import 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 */ 053public 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, 068 String languages, 069 String charsets) { 070 071 // Specify default values: 072 // English language, ISO-8859-1 (Latin-1) charset, English bundle 073 Locale defaultLocale = new Locale("en", "US"); 074 String defaultCharset = "ISO-8859-1"; 075 ResourceBundle defaultBundle = null; 076 try { 077 defaultBundle = ResourceBundle.getBundle(bundleName, defaultLocale); 078 } 079 catch (MissingResourceException e) { 080 // No default bundle was found. Flying without a net. 081 } 082 083 // If the client didn't specify acceptable languages, we can keep 084 // the defaults. 085 if (languages == null) { 086 chosenLocale = defaultLocale; 087 chosenCharset = defaultCharset; 088 chosenBundle = defaultBundle; 089 return; // quick exit 090 } 091 092 // Use a tokenizer to separate acceptable languages 093 StringTokenizer tokenizer = new StringTokenizer(languages, ","); 094 095 while (tokenizer.hasMoreTokens()) { 096 // Get the next acceptable language. 097 // (The language can look something like "en; qvalue=0.91") 098 String lang = tokenizer.nextToken(); 099 100 // Get the locale for that language 101 Locale loc = getLocaleForLanguage(lang); 102 103 // Get the bundle for this locale. Don't let the search fallback 104 // to match other languages! 105 ResourceBundle bundle = getBundleNoFallback(bundleName, loc); 106 107 // The returned bundle is null if there's no match. In that case 108 // we can't use this language since the servlet can't speak it. 109 if (bundle == null) 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) continue; // on to the next language 117 118 // If we get here, there are no problems with this language. 119 chosenLocale = loc; 120 chosenBundle = bundle; 121 chosenCharset = charset; 122 return; // we're done 123 } 124 125 // No matches, so we let the defaults stand 126 chosenLocale = defaultLocale; 127 chosenCharset = defaultCharset; 128 chosenBundle = defaultBundle; 129 } 130 131 /** 132 * Gets the chosen bundle. 133 * 134 * @return the chosen bundle 135 */ 136 public ResourceBundle getBundle() { 137 return chosenBundle; 138 } 139 140 /** 141 * Gets the chosen locale. 142 * 143 * @return the chosen locale 144 */ 145 public Locale getLocale() { 146 return chosenLocale; 147 } 148 149 /** 150 * Gets the chosen charset. 151 * 152 * @return the chosen charset 153 */ 154 public String getCharset() { 155 return chosenCharset; 156 } 157 158 /* 159 * Gets a Locale object for a given language string 160 */ 161 private Locale getLocaleForLanguage(String lang) { 162 Locale loc; 163 int semi, dash; 164 165 // Cut off any qvalue that might come after a semi-colon 166 if ((semi = lang.indexOf(';')) != -1) { 167 lang = lang.substring(0, semi); 168 } 169 170 // Trim any whitespace 171 lang = lang.trim(); 172 173 // Create a Locale from the language. A dash may separate the 174 // language from the country. 175 if ((dash = lang.indexOf('-')) == -1) { 176 loc = new Locale(lang, ""); // No dash, no country 177 } 178 else { 179 loc = new Locale(lang.substring(0, dash), lang.substring(dash+1)); 180 } 181 182 return loc; 183 } 184 185 /* 186 * Gets a ResourceBundle object for the given bundle name and locale, 187 * or null if the bundle can't be found. The resource bundle must match 188 * the locale exactly. Fallback matches are not permitted. 189 */ 190 private ResourceBundle getBundleNoFallback(String bundleName, Locale loc) { 191 192 // First get the fallback bundle -- the bundle that will be selected 193 // if getBundle() can't find a direct match. This bundle can be 194 // compared to the bundles returned by later calls to getBundle() in 195 // order to detect when getBundle() finds a direct match. 196 ResourceBundle fallback = null; 197 try { 198 fallback = 199 ResourceBundle.getBundle(bundleName, new Locale("bogus", "")); 200 } 201 catch (MissingResourceException e) { 202 // No fallback bundle was found. 203 } 204 205 try { 206 // Get the bundle for the specified locale 207 ResourceBundle bundle = ResourceBundle.getBundle(bundleName, loc); 208 209 // Is the bundle different than our fallback bundle? 210 if (bundle != fallback) { 211 // We have a real match! 212 return bundle; 213 } 214 // So the bundle is the same as our fallback bundle. 215 // We can still have a match, but only if our locale's language 216 // matches the default locale's language. 217 else if (bundle == fallback && 218 loc.getLanguage().equals(Locale.getDefault().getLanguage())) { 219 // Another way to match 220 return bundle; 221 } 222 else { 223 // No match, keep looking 224 } 225 } 226 catch (MissingResourceException e) { 227 // No bundle available for this locale 228 } 229 230 return null; // no match 231 } 232 233 /** 234 * Gets the best charset for a given locale, selecting from a charset list. 235 * Currently ignores the charset list. Subclasses can override this 236 * method to take the list into account. 237 * 238 * @param loc the locale 239 * @param charsets a comma-separated charset list 240 * @return the best charset for the given locale from the given list 241 */ 242 protected String getCharsetForLocale(Locale loc, String charsets) { 243 // Note: This method ignores the client-specified charsets 244 return LocaleToCharsetMap.getCharset(loc); 245 } 246}