Java tutorial
/** * Copyright (c) 2009--2014 Red Hat, Inc. * * This software is licensed to you under the GNU General Public License, * version 2 (GPLv2). There is NO WARRANTY for this software, express or * implied, including the implied warranties of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2 * along with this software; if not, see * http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt. * * Red Hat trademarks are not licensed under GPLv2. No permission is * granted to use or replicate Red Hat trademarks that are incorporated * in this software or its documentation. */ package com.redhat.rhn.common.localization; import com.redhat.rhn.common.conf.Config; import com.redhat.rhn.common.conf.ConfigDefaults; import com.redhat.rhn.common.db.datasource.DataResult; import com.redhat.rhn.common.db.datasource.ModeFactory; import com.redhat.rhn.common.db.datasource.SelectMode; import com.redhat.rhn.common.util.StringUtil; import com.redhat.rhn.frontend.context.Context; import org.apache.commons.lang.StringEscapeUtils; import org.apache.log4j.Logger; import java.text.Collator; import java.text.DateFormat; import java.text.NumberFormat; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.SortedMap; import java.util.SortedSet; import java.util.StringTokenizer; import java.util.TimeZone; import java.util.TreeMap; import java.util.TreeSet; /** * Localization service class to simplify the job for producing localized * (translated) strings within the product. * * @version $Rev$ */ public class LocalizationService { /** * DateFormat used by RHN database queries. Useful for converting RHN dates * into java.util.Dates so they can be formatted based on Locale. */ public static final String RHN_DB_DATEFORMAT = "yyyy-MM-dd HH:mm:ss"; public static final String RHN_CUSTOM_DATEFORMAT = "yyyy-MM-dd HH:mm:ss z"; private static Logger log = Logger.getLogger(LocalizationService.class); private static Logger msgLogger = Logger.getLogger("com.redhat.rhn.common.localization.messages"); public static final Locale DEFAULT_LOCALE = new Locale("EN", "US"); // private instance of the service. private static LocalizationService instance = new LocalizationService(); // This Map stores the association of the java class names // that map to the message keys found in the StringResources.xml // files. This allows us to have sets of XML ResourceBundles that // are specified in the rhn.jconf private Map<String, String> keyToBundleMap; // List of supported locales private final Map<String, LocaleInfo> supportedLocales = new HashMap<String, LocaleInfo>(); /** * hidden constructor */ private LocalizationService() { initService(); } /** * Initialize the set of strings and keys used by the service */ protected void initService() { // If we are reloading, lets log it. if (keyToBundleMap != null) { // We want to note in the log that we are doing this log.warn("Reloading XML StringResource files."); XmlMessages.getInstance().resetBundleCache(); } keyToBundleMap = new HashMap<String, String>(); // Get the list of configured classnames from the config file. String[] packages = Config.get().getStringArray(ConfigDefaults.WEB_L10N_RESOURCEBUNDLES); for (int i = 0; i < packages.length; i++) { addKeysToMap(packages[i]); } if (supportedLocales.size() > 0) { supportedLocales.clear(); } loadSupportedLocales(); } /** Add the keys from the specified class to the Service's Map. */ private void addKeysToMap(String className) { try { Class z = Class.forName(className); // All the keys must exist in the en_US XML files first. The other // languages may have subsets but no unique keys. If this is a // problem // refactoring will need to take place. Enumeration<String> e = XmlMessages.getInstance().getKeys(z, Locale.US); while (e.hasMoreElements()) { String key = e.nextElement(); keyToBundleMap.put(key, className); } } catch (ClassNotFoundException ce) { String message = "Class not found when trying to initalize " + "the LocalizationService: " + ce.toString(); log.error(message, ce); throw new LocalizationException(message, ce); } } private void loadSupportedLocales() { String rawLocales = Config.get().getString("java.supported_locales"); if (rawLocales == null) { return; } List<String> compoundLocales = new LinkedList<String>(); for (Enumeration<Object> locales = new StringTokenizer(rawLocales, ","); locales.hasMoreElements();) { String locale = (String) locales.nextElement(); if (locale.indexOf('_') > -1) { compoundLocales.add(locale); } LocaleInfo li = new LocaleInfo(locale); this.supportedLocales.put(locale, li); } for (Iterator<String> iter = compoundLocales.iterator(); iter.hasNext();) { String cl = iter.next(); String[] parts = cl.split("_"); LocaleInfo li = new LocaleInfo(parts[0], cl); if (this.supportedLocales.get(parts[0]) == null) { this.supportedLocales.put(parts[0], li); } } } /** * Get the running instance of the LocalizationService * * @return The LocalizationService singleton */ public static LocalizationService getInstance() { return instance; } /** * Reload the resource files from the disk. Only works in development mode. * @return boolean if we reloaded the files or not. */ public boolean reloadResourceFiles() { if (Config.get().getBoolean("java.development_environment")) { initService(); return true; } log.error( "Tried to reload XML StringResource files but " + "we aren't in java.development_environment mode"); return false; } /** * Get a localized version of a String and let the service attempt to figure * out the callee's locale. * @param messageId The key of the message we are fetching * @return Translated String */ public String getMessage(String messageId) { Context ctx = Context.getCurrentContext(); return getMessage(messageId, ctx.getLocale(), new Object[0]); } /** * Get a localized version of a string with the specified locale. * @param messageId The key of the message we are fetching * @param locale The locale to use when fetching the string * @return Translated String */ public String getMessage(String messageId, Locale locale) { return getMessage(messageId, locale, new Object[0]); } /** * Get a localized version of a string with the specified locale. * @param messageId The key of the message we are fetching * @param args arguments for message. * @return Translated String */ public String getMessage(String messageId, Object... args) { Context ctx = Context.getCurrentContext(); return getMessage(messageId, ctx.getLocale(), args); } /** * Gets a Plain Text + localized version of a string with the default locale. * @param messageId The key of the message we are fetching * @param args arguments for message. * @return Translated String */ public String getPlainText(String messageId, Object... args) { String msg = getMessage(messageId, args); String unescaped = StringEscapeUtils.unescapeHtml(msg); return StringUtil.toPlainText(unescaped); } /** * Gets a Plain Text + localized version of a string with the default locale. * @param messageId The key of the message we are fetching * @return Translated String */ public String getPlainText(String messageId) { return getPlainText(messageId, (Object[]) null); } /** * Take in a String array of keys and transform it into a String array of * localized Strings. * @param keys String[] array of key values * @return String[] array of localized strings. */ public String[] getMessages(String[] keys) { String[] retval = new String[keys.length]; for (int i = 0; i < keys.length; i++) { retval[i] = getMessage(keys[i]); } return retval; } /** * Get a localized version of a string with the specified locale. * @param messageId The key of the message we are fetching * @param locale The locale to use when fetching the string * @param args arguments for message. * @return Translated String */ public String getMessage(String messageId, Locale locale, Object... args) { log.debug("getMessage() called with messageId: " + messageId + " and locale: " + locale); // Short-circuit the rest of the method if the messageId is null // See bz 199892 if (messageId == null) { return getMissingMessageString(messageId); } String userLocale = locale == null ? "null" : locale.toString(); if (msgLogger.isDebugEnabled()) { msgLogger.debug("Resolving message \"" + messageId + "\" for locale " + userLocale); } String mess = null; Class z = null; try { // If the keyMap doesn't contain the requested key // then there is no hope and we return. if (!keyToBundleMap.containsKey(messageId)) { return getMissingMessageString(messageId); } z = Class.forName(keyToBundleMap.get(messageId)); // If we already determined that there aren't an bundles // for this Locale then we shouldn't repeatedly fail // attempts to parse the bundle. Instead just force a // call to the default Locale. mess = XmlMessages.getInstance().format(z, locale, messageId, args); } catch (MissingResourceException e) { // Try again with DEFAULT_LOCALE if (msgLogger.isDebugEnabled()) { msgLogger.debug("Resolving message \"" + messageId + "\" for locale " + userLocale + " failed - trying again with default " + "locale " + DEFAULT_LOCALE.toString()); } try { mess = XmlMessages.getInstance().format(z, DEFAULT_LOCALE, messageId, args); } catch (MissingResourceException mre) { if (msgLogger.isDebugEnabled()) { msgLogger.debug("Resolving message \"" + messageId + "\" " + "for default locale " + DEFAULT_LOCALE.toString() + " failed"); } return getMissingMessageString(messageId); } } catch (ClassNotFoundException ce) { String message = "Class not found when trying to fetch a message: " + ce.toString(); log.error(message, ce); throw new LocalizationException(message, ce); } return getDebugVersionOfString(mess); } private String getDebugVersionOfString(String mess) { // If we have put the Service into debug mode we // will wrap all the messages in a marker. boolean debugMode = Config.get().getBoolean("java.l10n_debug"); if (debugMode) { StringBuilder debug = new StringBuilder(); String marker = Config.get().getString("java.l10n_debug_marker", "$$$"); debug.append(marker); debug.append(mess); debug.append(marker); mess = debug.toString(); } return mess; } // returns the first class/method that does not belong to this // package (who calls this actually) - for debugging purposes private StackTraceElement getCallingMethod() { try { throw new RuntimeException("Stacktrace Dummy Exception"); } catch (RuntimeException e) { try { final String prefix = this.getClass().getPackage().getName(); for (StackTraceElement element : e.getStackTrace()) { if (!element.getClassName().startsWith(prefix)) { return element; } } } catch (Throwable t) { // dont break - return nothing rather than stop return null; } } return null; } private String getMissingMessageString(String messageId) { String caller = ""; StackTraceElement callerElement = getCallingMethod(); if (callerElement != null) { caller = " called by " + callerElement; } if (messageId == null) { messageId = "null"; } String message = "*** ERROR: Message with id: [" + messageId + "] not found.***" + caller; log.error(message); boolean exceptionMode = Config.get().getBoolean("java.l10n_missingmessage_exceptions"); if (exceptionMode) { throw new IllegalArgumentException(message); } return "**" + messageId + "**"; } /** * Get localized text for log messages as well as error emails. Determines * Locale of running JVM vs using the current Thread or any other User * related Locale information. TODO mmccune Get Locale out of Config or from * the JVM * @param messageId The key of the message we are fetching * @return String debug message. */ public String getDebugMessage(String messageId) { return getMessage(messageId, Locale.US); } /** * Format the date and let the service determine the locale * @param date Date to be formatted. * @return String representation of given date. */ public String formatDate(Date date) { Context ctx = Context.getCurrentContext(); return formatDate(date, ctx.getLocale()); } /** * Format the date as a short date depending on locale (YYYY-MM-DD in the * US) * @param date Date to be formatted * @return String representation of given date. */ public String formatShortDate(Date date) { Context ctx = Context.getCurrentContext(); return formatShortDate(date, ctx.getLocale()); } /** * Format the date as a short date depending on locale (YYYY-MM-DD in the * US) * * @param date Date to be formatted * @param locale Locale to use for formatting * @return String representation of given date. */ public String formatShortDate(Date date, Locale locale) { StringBuilder dbuff = new StringBuilder(); DateFormat dateI = DateFormat.getDateInstance(DateFormat.SHORT, locale); dbuff.append(dateI.format(date)); return getDebugVersionOfString(dbuff.toString()); } /** * Use today's date and get it back localized and as a String * @return String representation of today's date. */ public String getBasicDate() { return formatDate(new Date()); } /** * Format the date based on the locale and convert it to a String to * display. Uses DateFormat.SHORT. Example: 2004-12-10 13:20:00 PST * * Also includes the timezone of the current User if there is one * * @param date Date to format. * @param locale Locale to use for formatting. * @return String representation of given date in given locale. */ public String formatDate(Date date, Locale locale) { // Example: 2004-12-10 13:20:00 PST StringBuilder dbuff = new StringBuilder(); DateFormat dateI = DateFormat.getDateInstance(DateFormat.SHORT, locale); dateI.setTimeZone(determineTimeZone()); DateFormat timeI = DateFormat.getTimeInstance(DateFormat.LONG, locale); timeI.setTimeZone(determineTimeZone()); dbuff.append(dateI.format(date)); dbuff.append(" "); dbuff.append(timeI.format(date)); return getDebugVersionOfString(dbuff.toString()); } /** * Returns fixed custom format string displayed for the determined timezone * Example: 2010-04-01 15:04:24 CEST * * @param date Date to format. * @return String representation of given date for set timezone */ public String formatCustomDate(Date date) { TimeZone tz = determineTimeZone(); SimpleDateFormat sdf = new SimpleDateFormat(RHN_CUSTOM_DATEFORMAT); sdf.setTimeZone(tz); return sdf.format(date); } /** * Format the Number based on the locale and convert it to a String to * display. * @param numberIn Number to format. * @return String representation of given number in given locale. */ public String formatNumber(Number numberIn) { Context ctx = Context.getCurrentContext(); return formatNumber(numberIn, ctx.getLocale()); } /** * Format the Number based on the locale and convert it to a String to * display. Use a specified number of fraction digits. * @param numberIn Number to format. * @param fractionalDigits The number of fractional digits to use. This is * both the minimum and maximum that will be displayed. * @return String representation of given number in given locale. */ public String formatNumber(Number numberIn, int fractionalDigits) { Context ctx = Context.getCurrentContext(); return formatNumber(numberIn, ctx.getLocale(), fractionalDigits); } /** * Format the Number based on the locale and convert it to a String to * display. * @param numberIn Number to format. * @param localeIn Locale to use for formatting. * @return String representation of given number in given locale. */ public String formatNumber(Number numberIn, Locale localeIn) { return getDebugVersionOfString(NumberFormat.getInstance(localeIn).format(numberIn)); } /** * Format the Number based on the locale and convert it to a String to * display. Use a specified number of fractional digits. * @param numberIn Number to format. * @param localeIn Locale to use for formatting. * @param fractionalDigits The maximum number of fractional digits to use. * @return String representation of given number in given locale. */ public String formatNumber(Number numberIn, Locale localeIn, int fractionalDigits) { NumberFormat nf = NumberFormat.getInstance(localeIn); nf.setMaximumFractionDigits(fractionalDigits); return getDebugVersionOfString(nf.format(numberIn)); } /** * Get alphabet list for callee's Thread's Locale * @return the list of alphanumeric characters from the alphabet */ public List<String> getAlphabet() { return StringUtil.stringToList(getMessage("alphabet")); } /** * Get digit list for callee's Thread's Locale * @return the list of digits */ public List<String> getDigits() { return StringUtil.stringToList(getMessage("digits")); } /** * Get a list of available prefixes and ensure that it is sorted by * returning a SortedSet object. * @return SortedSet sorted set of available prefixes. */ public SortedSet<String> availablePrefixes() { SelectMode prefixMode = ModeFactory.getMode("util_queries", "available_prefixes"); // no params for this query DataResult<Map<String, Object>> dr = prefixMode.execute(new HashMap()); SortedSet<String> ret = new TreeSet<String>(); Iterator<Map<String, Object>> i = dr.iterator(); while (i.hasNext()) { Map<String, Object> row = i.next(); ret.add((String) row.get("prefix")); } return ret; } /** * Get a SortedMap containing NAME/CODE value pairs. The reason we key the * Map based on the NAME is that we desire to maintain a localized sort * order based on the display value and not the code. * * <pre> * {name=Spain, code=ES} * {name=Sri Lanka, code=LK} * {name=Sudan, code=SD} * {name=Suriname, code=SR, } * etc ... * </pre> * * @return SortedMap sorted map of available countries. */ public SortedMap<String, String> availableCountries() { List<String> validCountries = new LinkedList<String>(Arrays.asList(Locale.getISOCountries())); String[] excluded = Config.get().getStringArray(ConfigDefaults.WEB_EXCLUDED_COUNTRIES); if (excluded != null) { validCountries.removeAll(new LinkedList<String>(Arrays.asList(excluded))); } SortedMap<String, String> ret = new TreeMap<String, String>(); for (Iterator<String> iter = validCountries.iterator(); iter.hasNext();) { String isoCountry = iter.next(); ret.put(this.getMessage(isoCountry), isoCountry); } return ret; } /** * Simple util method to determine if the * @param messageId we are searching for * @return boolean if we have loaded this message */ public boolean hasMessage(String messageId) { return this.keyToBundleMap.containsKey(messageId); } /** * Get list of supported locales in string form * @return supported locales */ public List<String> getSupportedLocales() { List<String> tmp = new LinkedList<String>(this.supportedLocales.keySet()); Collections.sort(tmp); return Collections.unmodifiableList(tmp); } /** * Returns the list of configured locales which is most likely a subset of * all the supported locales * @return list of configured locales */ public List<String> getConfiguredLocales() { List<String> tmp = new LinkedList<String>(); for (Iterator<String> iter = this.supportedLocales.keySet().iterator(); iter.hasNext();) { String key = iter.next(); LocaleInfo li = this.supportedLocales.get(key); if (!li.isAlias()) { tmp.add(key); } } Collections.sort(tmp); return Collections.unmodifiableList(tmp); } /** * Determines if locale is supported * @param locale user's locale * @return result */ public boolean isLocaleSupported(Locale locale) { return this.supportedLocales.get(locale.toString()) != null; } /** * Determine the Timezone from the Context. Uses TimeZone.getDefault() if * there isn't one. * * @return TimeZone from the Context */ private TimeZone determineTimeZone() { TimeZone retval = null; Context ctx = Context.getCurrentContext(); if (ctx != null) { retval = ctx.getTimezone(); } if (retval == null) { log.debug("Context is null"); // Get the app server's default timezone retval = TimeZone.getDefault(); } if (log.isDebugEnabled()) { log.debug("Determined timeZone to be: " + retval); } return retval; } /** * Returns a NEW instance of the collator/string comparator * based on the current locale.. * Look at the javadoc for COllator to see what it does... * (basically an i18n aware string comparator) * @return neww instance of the collator */ public Collator newCollator() { Context context = Context.getCurrentContext(); if (context != null && context.getLocale() != null) { return Collator.getInstance(context.getLocale()); } return Collator.getInstance(); } }