001: /**
002: * $RCSfile$
003: * $Revision: 9288 $
004: * $Date: 2007-10-07 18:41:39 -0700 (Sun, 07 Oct 2007) $
005: *
006: * Copyright (C) 2004-2005 Jive Software. All rights reserved.
007: *
008: * This software is published under the terms of the GNU Public License (GPL),
009: * a copy of which is included in this distribution.
010: */package org.jivesoftware.util;
011:
012: import org.jivesoftware.openfire.XMPPServer;
013: import org.jivesoftware.openfire.container.Plugin;
014: import org.jivesoftware.openfire.container.PluginManager;
015:
016: import java.text.*;
017: import java.util.*;
018: import java.util.concurrent.ConcurrentHashMap;
019:
020: /**
021: * A set of methods for retrieving and converting locale specific strings and numbers.
022: *
023: * @author Jive Software
024: */
025: public class LocaleUtils {
026:
027: private static final Map<Locale, String[][]> timeZoneLists = new ConcurrentHashMap<Locale, String[][]>();
028:
029: // The basename to use for looking up the appropriate resource bundles
030: // TODO - extract this out into a test that grabs the resource name from JiveGlobals
031: // TODO and defaults to openfire_i18n if nothing set.
032: private static final String resourceBaseName = "openfire_i18n";
033:
034: private LocaleUtils() {
035: }
036:
037: /**
038: * Converts a locale string like "en", "en_US" or "en_US_win" to a Java
039: * locale object. If the conversion fails, null is returned.
040: *
041: * @param localeCode the locale code for a Java locale. See the {@link java.util.Locale}
042: * class for more details.
043: */
044: public static Locale localeCodeToLocale(String localeCode) {
045: Locale locale = null;
046: if (localeCode != null) {
047: String language = null;
048: String country = null;
049: String variant = null;
050: StringTokenizer tokenizer = new StringTokenizer(localeCode,
051: "_");
052: if (tokenizer.hasMoreTokens()) {
053: language = tokenizer.nextToken();
054: if (tokenizer.hasMoreTokens()) {
055: country = tokenizer.nextToken();
056: if (tokenizer.hasMoreTokens()) {
057: variant = tokenizer.nextToken();
058: }
059: }
060: }
061: locale = new Locale(language, ((country != null) ? country
062: : ""), ((variant != null) ? variant : ""));
063: }
064: return locale;
065: }
066:
067: // The list of supported timezone ids. The list tries to include all of the relevant
068: // time zones for the world without any extraneous zones.
069: private static String[] timeZoneIds = new String[] { "GMT",
070: "Pacific/Apia", "HST", "AST", "America/Los_Angeles",
071: "America/Phoenix", "America/Mazatlan", "America/Denver",
072: "America/Belize", "America/Chicago", "America/Mexico_City",
073: "America/Regina", "America/Bogota", "America/New_York",
074: "America/Indianapolis", "America/Halifax",
075: "America/Caracas", "America/Santiago", "America/St_Johns",
076: "America/Sao_Paulo", "America/Buenos_Aires",
077: "America/Godthab", "Atlantic/South_Georgia",
078: "Atlantic/Azores", "Atlantic/Cape_Verde",
079: "Africa/Casablanca", "Europe/Dublin", "Europe/Berlin",
080: "Europe/Belgrade", "Europe/Paris", "Europe/Warsaw", "ECT",
081: "Europe/Athens", "Europe/Bucharest", "Africa/Cairo",
082: "Africa/Harare", "Europe/Helsinki", "Asia/Jerusalem",
083: "Asia/Baghdad", "Asia/Kuwait", "Europe/Moscow",
084: "Africa/Nairobi", "Asia/Tehran", "Asia/Muscat",
085: "Asia/Baku", "Asia/Kabul", "Asia/Yekaterinburg",
086: "Asia/Karachi", "Asia/Calcutta", "Asia/Katmandu",
087: "Asia/Almaty", "Asia/Dhaka", "Asia/Colombo",
088: "Asia/Rangoon", "Asia/Bangkok", "Asia/Krasnoyarsk",
089: "Asia/Hong_Kong", "Asia/Irkutsk", "Asia/Kuala_Lumpur",
090: "Australia/Perth", "Asia/Taipei", "Asia/Tokyo",
091: "Asia/Seoul", "Asia/Yakutsk", "Australia/Adelaide",
092: "Australia/Darwin", "Australia/Brisbane",
093: "Australia/Sydney", "Pacific/Guam", "Australia/Hobart",
094: "Asia/Vladivostok", "Pacific/Noumea", "Pacific/Auckland",
095: "Pacific/Fiji", "Pacific/Tongatapu" };
096:
097: // A mapping from the supported timezone ids to friendly english names.
098: private static final Map<String, String> nameMap = new HashMap<String, String>();
099:
100: static {
101: nameMap.put(timeZoneIds[0], "International Date Line West");
102: nameMap.put(timeZoneIds[1], "Midway Island, Samoa");
103: nameMap.put(timeZoneIds[2], "Hawaii");
104: nameMap.put(timeZoneIds[3], "Alaska");
105: nameMap.put(timeZoneIds[4],
106: "Pacific Time (US & Canada); Tijuana");
107: nameMap.put(timeZoneIds[5], "Arizona");
108: nameMap.put(timeZoneIds[6], "Chihuahua, La Pax, Mazatlan");
109: nameMap.put(timeZoneIds[7], "Mountain Time (US & Canada)");
110: nameMap.put(timeZoneIds[8], "Central America");
111: nameMap.put(timeZoneIds[9], "Central Time (US & Canada)");
112: nameMap.put(timeZoneIds[10],
113: "Guadalajara, Mexico City, Monterrey");
114: nameMap.put(timeZoneIds[11], "Saskatchewan");
115: nameMap.put(timeZoneIds[12], "Bogota, Lima, Quito");
116: nameMap.put(timeZoneIds[13], "Eastern Time (US & Canada)");
117: nameMap.put(timeZoneIds[14], "Indiana (East)");
118: nameMap.put(timeZoneIds[15], "Atlantic Time (Canada)");
119: nameMap.put(timeZoneIds[16], "Caracas, La Paz");
120: nameMap.put(timeZoneIds[17], "Santiago");
121: nameMap.put(timeZoneIds[18], "Newfoundland");
122: nameMap.put(timeZoneIds[19], "Brasilia");
123: nameMap.put(timeZoneIds[20], "Buenos Aires, Georgetown");
124: nameMap.put(timeZoneIds[21], "Greenland");
125: nameMap.put(timeZoneIds[22], "Mid-Atlantic");
126: nameMap.put(timeZoneIds[23], "Azores");
127: nameMap.put(timeZoneIds[24], "Cape Verde Is.");
128: nameMap.put(timeZoneIds[25], "Casablanca, Monrovia");
129: nameMap
130: .put(timeZoneIds[26],
131: "Greenwich Mean Time : Dublin, Edinburgh, Lisbon, London");
132: nameMap.put(timeZoneIds[27],
133: "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna");
134: nameMap.put(timeZoneIds[28],
135: "Belgrade, Bratislava, Budapest, Ljubljana, Prague");
136: nameMap.put(timeZoneIds[29],
137: "Brussels, Copenhagen, Madrid, Paris");
138: nameMap
139: .put(timeZoneIds[30],
140: "Sarajevo, Skopje, Warsaw, Zagreb");
141: nameMap.put(timeZoneIds[31], "West Central Africa");
142: nameMap.put(timeZoneIds[32], "Athens, Istanbul, Minsk");
143: nameMap.put(timeZoneIds[33], "Bucharest");
144: nameMap.put(timeZoneIds[34], "Cairo");
145: nameMap.put(timeZoneIds[35], "Harare, Pretoria");
146: nameMap.put(timeZoneIds[36],
147: "Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius");
148: nameMap.put(timeZoneIds[37], "Jerusalem");
149: nameMap.put(timeZoneIds[38], "Baghdad");
150: nameMap.put(timeZoneIds[39], "Kuwait, Riyadh");
151: nameMap.put(timeZoneIds[40],
152: "Moscow, St. Petersburg, Volgograd");
153: nameMap.put(timeZoneIds[41], "Nairobi");
154: nameMap.put(timeZoneIds[42], "Tehran");
155: nameMap.put(timeZoneIds[43], "Abu Dhabi, Muscat");
156: nameMap.put(timeZoneIds[44], "Baku, Tbilisi, Muscat");
157: nameMap.put(timeZoneIds[45], "Kabul");
158: nameMap.put(timeZoneIds[46], "Ekaterinburg");
159: nameMap.put(timeZoneIds[47], "Islamabad, Karachi, Tashkent");
160: nameMap.put(timeZoneIds[48],
161: "Chennai, Kolkata, Mumbai, New Dehli");
162: nameMap.put(timeZoneIds[49], "Kathmandu");
163: nameMap.put(timeZoneIds[50], "Almaty, Novosibirsk");
164: nameMap.put(timeZoneIds[51], "Astana, Dhaka");
165: nameMap.put(timeZoneIds[52], "Sri Jayawardenepura");
166: nameMap.put(timeZoneIds[53], "Rangoon");
167: nameMap.put(timeZoneIds[54], "Bangkok, Hanoi, Jakarta");
168: nameMap.put(timeZoneIds[55], "Krasnoyarsk");
169: nameMap.put(timeZoneIds[56],
170: "Beijing, Chongqing, Hong Kong, Urumqi");
171: nameMap.put(timeZoneIds[57], "Irkutsk, Ulaan Bataar");
172: nameMap.put(timeZoneIds[58], "Kuala Lumpur, Singapore");
173: nameMap.put(timeZoneIds[59], "Perth");
174: nameMap.put(timeZoneIds[60], "Taipei");
175: nameMap.put(timeZoneIds[61], "Osaka, Sapporo, Tokyo");
176: nameMap.put(timeZoneIds[62], "Seoul");
177: nameMap.put(timeZoneIds[63], "Yakutsk");
178: nameMap.put(timeZoneIds[64], "Adelaide");
179: nameMap.put(timeZoneIds[65], "Darwin");
180: nameMap.put(timeZoneIds[66], "Brisbane");
181: nameMap.put(timeZoneIds[67], "Canberra, Melbourne, Sydney");
182: nameMap.put(timeZoneIds[68], "Guam, Port Moresby");
183: nameMap.put(timeZoneIds[69], "Hobart");
184: nameMap.put(timeZoneIds[70], "Vladivostok");
185: nameMap.put(timeZoneIds[71],
186: "Magadan, Solomon Is., New Caledonia");
187: nameMap.put(timeZoneIds[72], "Auckland, Wellington");
188: nameMap.put(timeZoneIds[73], "Fiji, Kamchatka, Marshall Is.");
189: nameMap.put(timeZoneIds[74], "Nuku'alofa");
190: }
191:
192: /**
193: * Returns a list of all available time zone's as a String [][]. The first
194: * entry in each list item is the timeZoneID, and the second is the
195: * display name.<p>
196: * <p/>
197: * The list of time zones attempts to be inclusive of all of the worlds
198: * zones while being as concise as possible. For "en" language locales
199: * the name is a friendly english name. For non-"en" language locales
200: * the standard JDK name is used for the given Locale. The GMT+/- time
201: * is also included for readability.
202: *
203: * @return a list of time zones, as a tuple of the zime zone ID, and its
204: * display name.
205: */
206: public static String[][] getTimeZoneList() {
207: Locale jiveLocale = JiveGlobals.getLocale();
208:
209: String[][] timeZoneList = timeZoneLists.get(jiveLocale);
210: if (timeZoneList == null) {
211: String[] timeZoneIDs = timeZoneIds;
212: // Now, create String[][] using the unique zones.
213: timeZoneList = new String[timeZoneIDs.length][2];
214: for (int i = 0; i < timeZoneList.length; i++) {
215: String zoneID = timeZoneIDs[i];
216: timeZoneList[i][0] = zoneID;
217: timeZoneList[i][1] = getTimeZoneName(zoneID, jiveLocale);
218: }
219:
220: // Add the new list to the map of locales to lists
221: timeZoneLists.put(jiveLocale, timeZoneList);
222: }
223:
224: return timeZoneList;
225: }
226:
227: /**
228: * Returns the display name for a time zone. The display name is the name
229: * specified by the Java TimeZone class for non-"en" locales or a friendly english
230: * name for "en", with the addition of the GMT offset
231: * for human readability.
232: *
233: * @param zoneID the time zone to get the name for.
234: * @param locale the locale to use.
235: * @return the display name for the time zone.
236: */
237: public static String getTimeZoneName(String zoneID, Locale locale) {
238: TimeZone zone = TimeZone.getTimeZone(zoneID);
239: StringBuffer buf = new StringBuffer();
240: // Add in the GMT part to the name. First, figure out the offset.
241: int offset = zone.getRawOffset();
242: if (zone.inDaylightTime(new Date()) && zone.useDaylightTime()) {
243: offset += (int) JiveConstants.HOUR;
244: }
245:
246: buf.append("(");
247: if (offset < 0) {
248: buf.append("GMT-");
249: } else {
250: buf.append("GMT+");
251: }
252: offset = Math.abs(offset);
253: int hours = offset / (int) JiveConstants.HOUR;
254: int minutes = (offset % (int) JiveConstants.HOUR)
255: / (int) JiveConstants.MINUTE;
256: buf.append(hours).append(":");
257: if (minutes < 10) {
258: buf.append("0").append(minutes);
259: } else {
260: buf.append(minutes);
261: }
262: buf.append(") ");
263:
264: // Use a friendly english timezone name if the locale is en, otherwise use the timezone id
265: if ("en".equals(locale.getLanguage())) {
266: String name = nameMap.get(zoneID);
267: if (name == null) {
268: name = zoneID;
269: }
270:
271: buf.append(name);
272: } else {
273: buf.append(zone.getDisplayName(true, TimeZone.LONG, locale)
274: .replace('_', ' ').replace('/', ' '));
275: }
276:
277: return buf.toString();
278: }
279:
280: /**
281: * Returns the specified resource bundle, which is a properties file
282: * that aids in localization of skins. This method is handy since it
283: * uses the class loader that other Jive classes are loaded from (hence,
284: * it can load bundles that are stored in jive.jar).
285: *
286: * @param baseName the name of the resource bundle to load.
287: * @param locale the desired Locale.
288: * @return the specified resource bundle, if it exists.
289: */
290: public static ResourceBundle getResourceBundle(String baseName,
291: Locale locale) {
292: return ResourceBundle.getBundle(baseName, locale);
293: }
294:
295: /**
296: * Returns an internationalized string loaded from a resource bundle.
297: * The locale used will be the locale specified by JiveGlobals.getLocale().
298: *
299: * @param key the key to use for retrieving the string from the
300: * appropriate resource bundle.
301: * @return the localized string.
302: */
303: public static String getLocalizedString(String key) {
304: Locale locale = JiveGlobals.getLocale();
305:
306: ResourceBundle bundle = ResourceBundle.getBundle(
307: resourceBaseName, locale);
308:
309: return getLocalizedString(key, locale, null, bundle);
310: }
311:
312: /**
313: * Returns an internationalized string loaded from a resource bundle using
314: * the passed in Locale.
315: *
316: * @param key the key to use for retrieving the string from the
317: * appropriate resource bundle.
318: * @param locale the locale to use for retrieving the appropriate
319: * locale-specific string.
320: * @return the localized string.
321: */
322: public static String getLocalizedString(String key, Locale locale) {
323: ResourceBundle bundle = ResourceBundle.getBundle(
324: resourceBaseName, locale);
325:
326: return getLocalizedString(key, locale, null, bundle);
327: }
328:
329: /**
330: * Returns an internationalized string loaded from a resource bundle using
331: * the locale specified by JiveGlobals.getLocale() substituting the passed
332: * in arguments. Substitution is handled using the
333: * {@link java.text.MessageFormat} class.
334: *
335: * @param key the key to use for retrieving the string from the
336: * appropriate resource bundle.
337: * @param arguments a list of objects to use which are formatted, then
338: * inserted into the pattern at the appropriate places.
339: * @return the localized string.
340: */
341: public static String getLocalizedString(String key, List arguments) {
342: Locale locale = JiveGlobals.getLocale();
343:
344: ResourceBundle bundle = ResourceBundle.getBundle(
345: resourceBaseName, locale);
346: return getLocalizedString(key, locale, arguments, bundle);
347: }
348:
349: /**
350: * Returns an internationalized string loaded from a resource bundle from the passed
351: * in plugin. If the plugin name is <tt>null</tt>, the key will be looked up using
352: * the standard resource bundle.
353: *
354: * @param key the key to use for retrieving the string from the
355: * appropriate resource bundle.
356: * @param pluginName the name of the plugin to load the require resource bundle from.
357: * @return the localized string.
358: */
359: public static String getLocalizedString(String key,
360: String pluginName) {
361: return getLocalizedString(key, pluginName, null);
362: }
363:
364: /**
365: * Returns an internationalized string loaded from a resource bundle from the passed
366: * in plugin. If the plugin name is <tt>null</tt>, the key will be looked up using
367: * the standard resource bundle.
368: *
369: * @param key the key to use for retrieving the string from the
370: * appropriate resource bundle.
371: * @param pluginName the name of the plugin to load the require resource bundle from.
372: * @param arguments a list of objects to use which are formatted, then
373: * inserted into the pattern at the appropriate places.
374: * @return the localized string.
375: */
376: public static String getLocalizedString(String key,
377: String pluginName, List arguments) {
378: if (pluginName == null) {
379: return getLocalizedString(key, arguments);
380: }
381:
382: Locale locale = JiveGlobals.getLocale();
383: String i18nFile = pluginName + "_i18n";
384:
385: // Retrieve classloader from pluginName.
386: final XMPPServer xmppServer = XMPPServer.getInstance();
387: PluginManager pluginManager = xmppServer.getPluginManager();
388: Plugin plugin = pluginManager.getPlugin(pluginName);
389: if (plugin == null) {
390: throw new NullPointerException(
391: "Plugin could not be located: " + pluginName);
392: }
393:
394: ClassLoader pluginClassLoader = pluginManager
395: .getPluginClassloader(plugin);
396: try {
397: ResourceBundle bundle = ResourceBundle.getBundle(i18nFile,
398: locale, pluginClassLoader);
399: return getLocalizedString(key, locale, arguments, bundle);
400: } catch (MissingResourceException mre) {
401: Log.error(mre);
402: return key;
403: }
404: }
405:
406: /**
407: * Retrieve the <code>ResourceBundle</code> that is used with this plugin.
408: *
409: * @param pluginName the name of the plugin.
410: * @return the ResourceBundle used with this plugin.
411: * @throws Exception thrown if an exception occurs.
412: */
413: public static ResourceBundle getPluginResourceBundle(
414: String pluginName) throws Exception {
415: final Locale locale = JiveGlobals.getLocale();
416:
417: String i18nFile = pluginName + "_i18n";
418:
419: // Retrieve classloader from pluginName.
420: final XMPPServer xmppServer = XMPPServer.getInstance();
421: PluginManager pluginManager = xmppServer.getPluginManager();
422: Plugin plugin = pluginManager.getPlugin(pluginName);
423: if (plugin == null) {
424: throw new NullPointerException(
425: "Plugin could not be located.");
426: }
427:
428: ClassLoader pluginClassLoader = pluginManager
429: .getPluginClassloader(plugin);
430: return ResourceBundle.getBundle(i18nFile, locale,
431: pluginClassLoader);
432: }
433:
434: /**
435: * Returns an internationalized string loaded from a resource bundle using
436: * the passed in Locale substituting the passed in arguments. Substitution
437: * is handled using the {@link MessageFormat} class.
438: *
439: * @param key the key to use for retrieving the string from the
440: * appropriate resource bundle.
441: * @param locale the locale to use for retrieving the appropriate
442: * locale-specific string.
443: * @param arguments a list of objects to use which are formatted, then
444: * inserted into the pattern at the appropriate places.
445: * @return the localized string.
446: */
447: public static String getLocalizedString(String key, Locale locale,
448: List arguments, ResourceBundle bundle) {
449: if (key == null) {
450: throw new NullPointerException("Key cannot be null");
451: }
452: if (locale == null) {
453: locale = JiveGlobals.getLocale();
454: }
455:
456: String value;
457:
458: // See if the bundle has a value
459: try {
460: // The jdk caches resource bundles on it's own, so we won't bother.
461: value = bundle.getString(key);
462: // perform argument substitutions
463: if (arguments != null) {
464: MessageFormat messageFormat = new MessageFormat("");
465: messageFormat.setLocale(bundle.getLocale());
466: messageFormat.applyPattern(value);
467: try {
468: // This isn't fool-proof, but it's better than nothing
469: // The idea is to try and convert strings into the
470: // types of objects that the formatters expects
471: // i.e. Numbers and Dates
472: Format[] formats = messageFormat.getFormats();
473: for (int i = 0; i < formats.length; i++) {
474: Format format = formats[i];
475: if (format != null) {
476: if (format instanceof DateFormat) {
477: if (arguments.size() > i) {
478: Object val = arguments.get(i);
479: if (val instanceof String) {
480: DateFormat dateFmt = (DateFormat) format;
481: try {
482: val = dateFmt
483: .parse((String) val);
484: arguments.set(i, val);
485: } catch (ParseException e) {
486: Log.error(e);
487: }
488: }
489: }
490: } else if (format instanceof NumberFormat) {
491: if (arguments.size() > i) {
492: Object val = arguments.get(i);
493: if (val instanceof String) {
494: NumberFormat nbrFmt = (NumberFormat) format;
495: try {
496: val = nbrFmt
497: .parse((String) val);
498: arguments.set(i, val);
499: } catch (ParseException e) {
500: Log.error(e);
501: }
502: }
503: }
504: }
505: }
506: }
507: value = messageFormat.format(arguments.toArray());
508: } catch (IllegalArgumentException e) {
509: Log
510: .error("Unable to format resource string for key: "
511: + key
512: + ", argument type not supported");
513: value = "";
514: }
515: }
516: } catch (java.util.MissingResourceException mre) {
517: Log.warn("Missing resource for key: " + key + " in locale "
518: + locale.toString());
519: value = "";
520: }
521:
522: return value;
523: }
524:
525: /**
526: * Returns an internationalized String representation of the number using
527: * the default locale.
528: *
529: * @param number the number to format.
530: * @return an internationalized String representation of the number.
531: */
532: public static String getLocalizedNumber(long number) {
533: return NumberFormat.getInstance().format(number);
534: }
535:
536: /**
537: * Returns an internationalized String representation of the number using
538: * the specified locale.
539: *
540: * @param number the number to format.
541: * @param locale the locale to use for formatting.
542: * @return an internationalized String representation of the number.
543: */
544: public static String getLocalizedNumber(long number, Locale locale) {
545: return NumberFormat.getInstance(locale).format(number);
546: }
547:
548: /**
549: * Returns an internationalized String representation of the number using
550: * the default locale.
551: *
552: * @param number the number to format.
553: * @return an internationalized String representation of the number.
554: */
555: public static String getLocalizedNumber(double number) {
556: return NumberFormat.getInstance().format(number);
557: }
558:
559: /**
560: * Returns an internationalized String representation of the number using
561: * the specified locale.
562: *
563: * @param number the number to format.
564: * @param locale the locale to use for formatting.
565: * @return an internationalized String representation of the number.
566: */
567: public static String getLocalizedNumber(double number, Locale locale) {
568: return NumberFormat.getInstance(locale).format(number);
569: }
570: }
|