001: package org.apache.turbine.services.localization;
002:
003: /*
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021:
022: import java.text.MessageFormat;
023: import java.util.HashMap;
024: import java.util.Locale;
025: import java.util.Map;
026: import java.util.MissingResourceException;
027: import java.util.ResourceBundle;
028:
029: import javax.servlet.http.HttpServletRequest;
030:
031: import org.apache.commons.configuration.Configuration;
032:
033: import org.apache.commons.lang.StringUtils;
034:
035: import org.apache.commons.logging.Log;
036: import org.apache.commons.logging.LogFactory;
037:
038: import org.apache.turbine.Turbine;
039: import org.apache.turbine.services.InitializationException;
040: import org.apache.turbine.services.TurbineBaseService;
041: import org.apache.turbine.util.RunData;
042:
043: /**
044: * <p>This class is the single point of access to all localization
045: * resources. It caches different ResourceBundles for different
046: * Locales.</p>
047: *
048: * <p>Usage example:</p>
049: *
050: * <blockquote><code><pre>
051: * LocalizationService ls = (LocalizationService) TurbineServices
052: * .getInstance().getService(LocalizationService.SERVICE_NAME);
053: * </pre></code></blockquote>
054: *
055: * <p>Then call one of four methods to retrieve a ResourceBundle:
056: *
057: * <ul>
058: * <li>getBundle("MyBundleName")</li>
059: * <li>getBundle("MyBundleName", httpAcceptLanguageHeader)</li>
060: * <li>etBundle("MyBundleName", HttpServletRequest)</li>
061: * <li>getBundle("MyBundleName", Locale)</li>
062: * <li>etc.</li>
063: * </ul></p>
064: *
065: * @author <a href="mailto:jm@mediaphil.de">Jonas Maurus</a>
066: * @author <a href="mailto:jon@latchkey.com">Jon S. Stevens</a>
067: * @author <a href="mailto:novalidemail@foo.com">Frank Y. Kim</a>
068: * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
069: * @author <a href="mailto:leonardr@collab.net">Leonard Richardson</a>
070: * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
071: * @version $Id: TurbineLocalizationService.java 534527 2007-05-02 16:10:59Z tv $
072: */
073: public class TurbineLocalizationService extends TurbineBaseService
074: implements LocalizationService {
075: /** Logging */
076: private static Log log = LogFactory
077: .getLog(TurbineLocalizationService.class);
078:
079: /**
080: * The value to pass to <code>MessageFormat</code> if a
081: * <code>null</code> reference is passed to <code>format()</code>.
082: */
083: private static final Object[] NO_ARGS = new Object[0];
084:
085: /**
086: * Bundle name keys a Map of the ResourceBundles in this
087: * service (which is in turn keyed by Locale).
088: * Key=bundle name
089: * Value=Hashtable containing ResourceBundles keyed by Locale.
090: */
091: private Map bundles = null;
092:
093: /**
094: * The list of default bundles to search.
095: */
096: private String[] bundleNames = null;
097:
098: /**
099: * The name of the default locale to use (includes language and
100: * country).
101: */
102: private Locale defaultLocale = null;
103:
104: /** The name of the default language to use. */
105: private String defaultLanguage = null;
106:
107: /** The name of the default country to use. */
108: private String defaultCountry = null;
109:
110: /**
111: * Constructor.
112: */
113: public TurbineLocalizationService() {
114: bundles = new HashMap();
115: }
116:
117: /**
118: * Called the first time the Service is used.
119: */
120: public void init() throws InitializationException {
121: Configuration conf = Turbine.getConfiguration();
122:
123: initBundleNames(null);
124:
125: Locale jvmDefault = Locale.getDefault();
126:
127: defaultLanguage = conf.getString("locale.default.language",
128: jvmDefault.getLanguage()).trim();
129: defaultCountry = conf.getString("locale.default.country",
130: jvmDefault.getCountry()).trim();
131:
132: defaultLocale = new Locale(defaultLanguage, defaultCountry);
133: setInit(true);
134: }
135:
136: /**
137: * Initialize list of default bundle names.
138: *
139: * @param ignored Ignored.
140: */
141: protected void initBundleNames(String[] ignored) {
142: Configuration conf = Turbine.getConfiguration();
143: bundleNames = conf.getStringArray("locale.default.bundles");
144: String name = conf.getString("locale.default.bundle");
145:
146: if (name != null && name.length() > 0) {
147: // Using old-style single bundle name property.
148: if (bundleNames == null || bundleNames.length <= 0) {
149: bundleNames = new String[] { name };
150: } else {
151: // Prepend "default" bundle name.
152: String[] array = new String[bundleNames.length + 1];
153: array[0] = name;
154: System.arraycopy(bundleNames, 0, array, 1,
155: bundleNames.length);
156: bundleNames = array;
157: }
158: }
159: if (bundleNames == null) {
160: bundleNames = new String[0];
161: }
162: }
163:
164: /**
165: * Retrieves the default language (specified in the config file).
166: */
167: public String getDefaultLanguage() {
168: return defaultLanguage;
169: }
170:
171: /**
172: * Retrieves the default country (specified in the config file).
173: */
174: public String getDefaultCountry() {
175: return defaultCountry;
176: }
177:
178: /**
179: * Retrieves the name of the default bundle (as specified in the
180: * config file).
181: * @see org.apache.turbine.services.localization.LocalizationService#getDefaultBundleName()
182: */
183: public String getDefaultBundleName() {
184: return (bundleNames.length > 0 ? bundleNames[0] : "");
185: }
186:
187: /**
188: * @see org.apache.turbine.services.localization.LocalizationService#getBundleNames()
189: */
190: public String[] getBundleNames() {
191: return (String[]) bundleNames.clone();
192: }
193:
194: /**
195: * This method returns a ResourceBundle given the bundle name
196: * "DEFAULT" and the default Locale information supplied in
197: * TurbineProperties.
198: *
199: * @return A localized ResourceBundle.
200: */
201: public ResourceBundle getBundle() {
202: return getBundle(getDefaultBundleName(), (Locale) null);
203: }
204:
205: /**
206: * This method returns a ResourceBundle given the bundle name and
207: * the default Locale information supplied in TurbineProperties.
208: *
209: * @param bundleName Name of bundle.
210: * @return A localized ResourceBundle.
211: */
212: public ResourceBundle getBundle(String bundleName) {
213: return getBundle(bundleName, (Locale) null);
214: }
215:
216: /**
217: * This method returns a ResourceBundle given the bundle name and
218: * the Locale information supplied in the HTTP "Accept-Language"
219: * header.
220: *
221: * @param bundleName Name of bundle.
222: * @param languageHeader A String with the language header.
223: * @return A localized ResourceBundle.
224: */
225: public ResourceBundle getBundle(String bundleName,
226: String languageHeader) {
227: return getBundle(bundleName, getLocale(languageHeader));
228: }
229:
230: /**
231: * This method returns a ResourceBundle given the Locale
232: * information supplied in the HTTP "Accept-Language" header which
233: * is stored in HttpServletRequest.
234: *
235: * @param req HttpServletRequest.
236: * @return A localized ResourceBundle.
237: */
238: public ResourceBundle getBundle(HttpServletRequest req) {
239: return getBundle(getDefaultBundleName(), getLocale(req));
240: }
241:
242: /**
243: * This method returns a ResourceBundle given the bundle name and
244: * the Locale information supplied in the HTTP "Accept-Language"
245: * header which is stored in HttpServletRequest.
246: *
247: * @param bundleName Name of the bundle to use if the request's
248: * locale cannot be resolved.
249: * @param req HttpServletRequest.
250: * @return A localized ResourceBundle.
251: */
252: public ResourceBundle getBundle(String bundleName,
253: HttpServletRequest req) {
254: return getBundle(bundleName, getLocale(req));
255: }
256:
257: /**
258: * This method returns a ResourceBundle given the Locale
259: * information supplied in the HTTP "Accept-Language" header which
260: * is stored in RunData.
261: *
262: * @param data Turbine information.
263: * @return A localized ResourceBundle.
264: */
265: public ResourceBundle getBundle(RunData data) {
266: return getBundle(getDefaultBundleName(), getLocale(data
267: .getRequest()));
268: }
269:
270: /**
271: * This method returns a ResourceBundle given the bundle name and
272: * the Locale information supplied in the HTTP "Accept-Language"
273: * header which is stored in RunData.
274: *
275: * @param bundleName Name of bundle.
276: * @param data Turbine information.
277: * @return A localized ResourceBundle.
278: */
279: public ResourceBundle getBundle(String bundleName, RunData data) {
280: return getBundle(bundleName, getLocale(data.getRequest()));
281: }
282:
283: /**
284: * This method returns a ResourceBundle for the given bundle name
285: * and the given Locale.
286: *
287: * @param bundleName Name of bundle (or <code>null</code> for the
288: * default bundle).
289: * @param locale The locale (or <code>null</code> for the locale
290: * indicated by the default language and country).
291: * @return A localized ResourceBundle.
292: */
293: public ResourceBundle getBundle(String bundleName, Locale locale) {
294: // Assure usable inputs.
295: bundleName = (StringUtils.isEmpty(bundleName) ? getDefaultBundleName()
296: : bundleName.trim());
297: if (locale == null) {
298: locale = getLocale((String) null);
299: }
300:
301: // Find/retrieve/cache bundle.
302: ResourceBundle rb = null;
303: Map bundlesByLocale = (Map) bundles.get(bundleName);
304: if (bundlesByLocale != null) {
305: // Cache of bundles by locale for the named bundle exists.
306: // Check the cache for a bundle corresponding to locale.
307: rb = (ResourceBundle) bundlesByLocale.get(locale);
308:
309: if (rb == null) {
310: // Not yet cached.
311: rb = cacheBundle(bundleName, locale);
312: }
313: } else {
314: rb = cacheBundle(bundleName, locale);
315: }
316: return rb;
317: }
318:
319: /**
320: * Caches the named bundle for fast lookups. This operation is
321: * relatively expesive in terms of memory use, but is optimized
322: * for run-time speed in the usual case.
323: *
324: * @exception MissingResourceException Bundle not found.
325: */
326: private synchronized ResourceBundle cacheBundle(String bundleName,
327: Locale locale) throws MissingResourceException {
328: Map bundlesByLocale = (HashMap) bundles.get(bundleName);
329: ResourceBundle rb = (bundlesByLocale == null ? null
330: : (ResourceBundle) bundlesByLocale.get(locale));
331:
332: if (rb == null) {
333: bundlesByLocale = (bundlesByLocale == null ? new HashMap(3)
334: : new HashMap(bundlesByLocale));
335: try {
336: rb = ResourceBundle.getBundle(bundleName, locale);
337: } catch (MissingResourceException e) {
338: rb = findBundleByLocale(bundleName, locale,
339: bundlesByLocale);
340: if (rb == null) {
341: throw (MissingResourceException) e
342: .fillInStackTrace();
343: }
344: }
345:
346: if (rb != null) {
347: // Cache bundle.
348: bundlesByLocale.put(rb.getLocale(), rb);
349:
350: Map bundlesByName = new HashMap(bundles);
351: bundlesByName.put(bundleName, bundlesByLocale);
352: this .bundles = bundlesByName;
353: }
354: }
355: return rb;
356: }
357:
358: /**
359: * <p>Retrieves the bundle most closely matching first against the
360: * supplied inputs, then against the defaults.</p>
361: *
362: * <p>Use case: some clients send a HTTP Accept-Language header
363: * with a value of only the language to use
364: * (i.e. "Accept-Language: en"), and neglect to include a country.
365: * When there is no bundle for the requested language, this method
366: * can be called to try the default country (checking internally
367: * to assure the requested criteria matches the default to avoid
368: * disconnects between language and country).</p>
369: *
370: * <p>Since we're really just guessing at possible bundles to use,
371: * we don't ever throw <code>MissingResourceException</code>.</p>
372: */
373: private ResourceBundle findBundleByLocale(String bundleName,
374: Locale locale, Map bundlesByLocale) {
375: ResourceBundle rb = null;
376: if (!StringUtils.isNotEmpty(locale.getCountry())
377: && defaultLanguage.equals(locale.getLanguage())) {
378: /*
379: * log.debug("Requested language '" + locale.getLanguage() +
380: * "' matches default: Attempting to guess bundle " +
381: * "using default country '" + defaultCountry + '\'');
382: */
383: Locale withDefaultCountry = new Locale(
384: locale.getLanguage(), defaultCountry);
385: rb = (ResourceBundle) bundlesByLocale
386: .get(withDefaultCountry);
387: if (rb == null) {
388: rb = getBundleIgnoreException(bundleName,
389: withDefaultCountry);
390: }
391: } else if (!StringUtils.isNotEmpty(locale.getLanguage())
392: && defaultCountry.equals(locale.getCountry())) {
393: Locale withDefaultLanguage = new Locale(defaultLanguage,
394: locale.getCountry());
395: rb = (ResourceBundle) bundlesByLocale
396: .get(withDefaultLanguage);
397: if (rb == null) {
398: rb = getBundleIgnoreException(bundleName,
399: withDefaultLanguage);
400: }
401: }
402:
403: if (rb == null && !defaultLocale.equals(locale)) {
404: rb = getBundleIgnoreException(bundleName, defaultLocale);
405: }
406:
407: return rb;
408: }
409:
410: /**
411: * Retrieves the bundle using the
412: * <code>ResourceBundle.getBundle(String, Locale)</code> method,
413: * returning <code>null</code> instead of throwing
414: * <code>MissingResourceException</code>.
415: */
416: private ResourceBundle getBundleIgnoreException(String bundleName,
417: Locale locale) {
418: try {
419: return ResourceBundle.getBundle(bundleName, locale);
420: } catch (MissingResourceException ignored) {
421: return null;
422: }
423: }
424:
425: /**
426: * This method sets the name of the first bundle in the search
427: * list (the "default" bundle).
428: *
429: * @param defaultBundle Name of default bundle.
430: */
431: public void setBundle(String defaultBundle) {
432: if (bundleNames.length > 0) {
433: bundleNames[0] = defaultBundle;
434: } else {
435: synchronized (this ) {
436: if (bundleNames.length <= 0) {
437: bundleNames = new String[] { defaultBundle };
438: }
439: }
440: }
441: }
442:
443: /**
444: * @see org.apache.turbine.services.localization.LocalizationService#getLocale(HttpServletRequest)
445: */
446: public final Locale getLocale(HttpServletRequest req) {
447: return getLocale(req.getHeader(ACCEPT_LANGUAGE));
448: }
449:
450: /**
451: * @see org.apache.turbine.services.localization.LocalizationService#getLocale(String)
452: */
453: public Locale getLocale(String header) {
454: if (!StringUtils.isEmpty(header)) {
455: LocaleTokenizer tok = new LocaleTokenizer(header);
456: if (tok.hasNext()) {
457: return (Locale) tok.next();
458: }
459: }
460:
461: // Couldn't parse locale.
462: return defaultLocale;
463: }
464:
465: /**
466: * @exception MissingResourceException Specified key cannot be matched.
467: * @see org.apache.turbine.services.localization.LocalizationService#getString(String, Locale, String)
468: */
469: public String getString(String bundleName, Locale locale, String key) {
470: String value = null;
471:
472: if (locale == null) {
473: locale = getLocale((String) null);
474: }
475:
476: // Look for text in requested bundle.
477: ResourceBundle rb = getBundle(bundleName, locale);
478: value = getStringOrNull(rb, key);
479:
480: // Look for text in list of default bundles.
481: if (value == null && bundleNames.length > 0) {
482: String name;
483: for (int i = 0; i < bundleNames.length; i++) {
484: name = bundleNames[i];
485: //System.out.println("getString(): name=" + name +
486: // ", locale=" + locale + ", i=" + i);
487: if (!name.equals(bundleName)) {
488: rb = getBundle(name, locale);
489: value = getStringOrNull(rb, key);
490: if (value != null) {
491: locale = rb.getLocale();
492: break;
493: }
494: }
495: }
496: }
497:
498: if (value == null) {
499: String loc = locale.toString();
500: String mesg = LocalizationService.SERVICE_NAME
501: + " noticed missing resource: " + "bundleName="
502: + bundleName + ", locale=" + loc + ", key=" + key;
503: log.debug(mesg);
504: // Text not found in requested or default bundles.
505: throw new MissingResourceException(mesg, bundleName, key);
506: }
507:
508: return value;
509: }
510:
511: /**
512: * Gets localized text from a bundle if it's there. Otherwise,
513: * returns <code>null</code> (ignoring a possible
514: * <code>MissingResourceException</code>).
515: */
516: protected final String getStringOrNull(ResourceBundle rb, String key) {
517: if (rb != null) {
518: try {
519: return rb.getString(key);
520: } catch (MissingResourceException ignored) {
521: }
522: }
523: return null;
524: }
525:
526: /**
527: * Formats a localized value using the provided object.
528: *
529: * @param bundleName The bundle in which to look for the localizable text.
530: * @param locale The locale for which to format the text.
531: * @param key The identifier for the localized text to retrieve,
532: * @param arg1 The object to use as {0} when formatting the localized text.
533: * @return Formatted localized text.
534: * @see #format(String, Locale, String, Object[])
535: */
536: public String format(String bundleName, Locale locale, String key,
537: Object arg1) {
538: return format(bundleName, locale, key, new Object[] { arg1 });
539: }
540:
541: /**
542: * Formats a localized value using the provided objects.
543: *
544: * @param bundleName The bundle in which to look for the localizable text.
545: * @param locale The locale for which to format the text.
546: * @param key The identifier for the localized text to retrieve,
547: * @param arg1 The object to use as {0} when formatting the localized text.
548: * @param arg2 The object to use as {1} when formatting the localized text.
549: * @return Formatted localized text.
550: * @see #format(String, Locale, String, Object[])
551: */
552: public String format(String bundleName, Locale locale, String key,
553: Object arg1, Object arg2) {
554: return format(bundleName, locale, key, new Object[] { arg1,
555: arg2 });
556: }
557:
558: /**
559: * Looks up the value for <code>key</code> in the
560: * <code>ResourceBundle</code> referenced by
561: * <code>bundleName</code>, then formats that value for the
562: * specified <code>Locale</code> using <code>args</code>.
563: *
564: * @param bundleName The bundle in which to look for the localizable text.
565: * @param locale The locale for which to format the text.
566: * @param key The identifier for the localized text to retrieve,
567: * @param args The objects to use when formatting the localized text.
568: *
569: * @return Localized, formatted text identified by
570: * <code>key</code>.
571: */
572: public String format(String bundleName, Locale locale, String key,
573: Object[] args) {
574: if (locale == null) {
575: // When formatting Date objects and such, MessageFormat
576: // cannot have a null Locale.
577: locale = getLocale((String) null);
578: }
579: String value = getString(bundleName, locale, key);
580: if (args == null) {
581: args = NO_ARGS;
582: }
583: // FIXME: after switching to JDK 1.4, it will be possible to clean
584: // this up by providing the Locale along with the string in the
585: // constructor to MessageFormat. Until 1.4, the following workaround
586: // is required for constructing the format with the appropriate locale:
587: MessageFormat messageFormat = new MessageFormat("");
588: messageFormat.setLocale(locale);
589: messageFormat.applyPattern(value);
590: return messageFormat.format(args);
591: }
592: }
|