ITrackerResources.java

/*
 * This software was designed and created by Jason Carroll.
 * Copyright (c) 2002, 2003, 2004 Jason Carroll.
 * The author can be reached at jcarroll@cowsultants.com
 * ITracker website: http://www.cowsultants.com
 * ITracker forums: http://www.cowsultants.com/phpBB/index.php
 *
 * This program is free software; you can redistribute it and/or modify
 * it only under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

package org.itracker.core.resources;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.itracker.ITrackerDirtyResourceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

/**
 * Please comment this class here. What is it for?
 *
 * @author ready
 */
public class ITrackerResources {

    private static final Logger logger = LoggerFactory
            .getLogger(ITrackerResources.class);

    public static final String RESOURCE_BUNDLE_NAME = "org.itracker.core.resources.ITracker";

    public static final String DEFAULT_LOCALE = "en_US";

    public static final String BASE_LOCALE = "BASE";

    public static final String KEY_BASE_CUSTOMFIELD_TYPE = "itracker.web.generic.";

    public static final String KEY_BASE_WORKFLOW_EVENT = "itracker.workflow.field.event.";

    public static final String KEY_BASE_PROJECT_STATUS = "itracker.project.status.";

    public static final String KEY_BASE_PERMISSION = "itracker.user.permission.";

    public static final String KEY_BASE_PRIORITY = "itracker.script.priority.";

    public static final String KEY_BASE_PRIORITY_LABEL = ".label";

    public static final String KEY_BASE_PRIORITY_SIZE = "size";

    public static final String KEY_BASE_RESOLUTION = "itracker.resolution.";

    public static final String KEY_BASE_ISSUE_RELATION = "itracker.issuerelation.";

    public static final String KEY_BASE_SEVERITY = "itracker.severity.";

    public static final String KEY_BASE_STATUS = "itracker.status.";

    public static final String KEY_BASE_USER_STATUS = "itracker.user.status.";

    public static final String KEY_BASE_CUSTOMFIELD = "itracker.customfield.";

    public static final String KEY_BASE_CUSTOMFIELD_OPTION = ".option.";

    public static final String KEY_BASE_CUSTOMFIELD_LABEL = ".label";

    public static final String KEY_BASE_LOCALE_NAME = "itracker.locale.name";

    private static String defaultLocale = DEFAULT_LOCALE;

    private static HashMap<String, Locale> locales = new HashMap<String, Locale>();

    private static HashMap<Locale, ResourceBundle> languages = new HashMap<Locale, ResourceBundle>();

    private static ITrackerResourcesProvider configurationService;
    private static Collection<String> availableLocales;

    private static boolean initialized = false;

    private static final Object bundleLock = new Object();


    public static Locale getLocale() {
        return getLocale(getDefaultLocale());
    }

    public static Locale getLocale(String localeString) {

        if (localeString == null || localeString.trim().equals("")) {
            return getLocale(getDefaultLocale());
        }

        Locale locale = locales.get(localeString);
        if (locale == null
                && !StringUtils.isEmpty(localeString)) {
            try {
                if (logger.isDebugEnabled()) {
                    logger.debug("Creating new locale for '" + localeString
                            + "'");
                }
                if (localeString.length() == 5) {
                    locale = new Locale(localeString.substring(0, 2),
                            localeString.substring(3));
                } else if (localeString.length() == 2) {
                    locale = new Locale(localeString, "");
                } else if (localeString.equals(BASE_LOCALE)) {
                    locale = new Locale("", "");
                } else {

                    logger
                            .error("Invalid locale '"
                                    + localeString
                                    + "' specified.  It must be either LN or LN_CN.");
                    throw new Exception("Invalid locale string");
                }
            } catch (Exception ex) {
                if (!localeString.equals(getDefaultLocale())) {
                    logger.error("Failed creating new locale for '"
                            + localeString
                            + "' attempting for default locale '"
                            + getDefaultLocale() + "'", ex);
                    return getLocale(getDefaultLocale());
                } else {
                    logger.error("Failed creating new default locale for '"
                            + getDefaultLocale()
                            + "' attempting for DEFAULT_LOCALE '"
                            + DEFAULT_LOCALE + "'", ex);
                    return getLocale(DEFAULT_LOCALE);
                }
            }
            locales.put(localeString, locale);
        }
        return locale;
    }

    public static String getDefaultLocale() {
        return (defaultLocale == null ? DEFAULT_LOCALE : defaultLocale);
    }

    private static void setDefaultLocale(String value) {
        defaultLocale = value;
    }

    public static String getLocaleDN(String locale, Locale displayLocale) {
        String name;
        if (null == displayLocale) {
            displayLocale = getLocale();
        }
        try {
            name = getBundle(displayLocale).getString(
                    KEY_BASE_LOCALE_NAME + "." + locale);
        } catch (RuntimeException e) {
            name = getLocaleNativeName(getLocale(locale));
        }

        return name;
    }

    public static String getLocaleDN(Locale locale, Locale displayLocale) {

        if (null == displayLocale) {
            return getLocaleNativeName(displayLocale);
        }
        return getLocaleDN(locale.toString(), displayLocale);

    }

    public static String getLocaleFullDN(Locale locale, Locale displayLocale) {

        if (null == locale) {
            locale = new Locale("");
        }
        String fullName = StringUtils.trimToNull(getLocaleNativeName(locale));
        if (null == displayLocale || locale.getLanguage().equals(displayLocale.getLanguage())) {
            return fullName;
        }
        if (StringUtils.equals(fullName, String.valueOf(locale))) {
            fullName = getLocaleDN(locale, displayLocale);
            return fullName;
        } else {
            String localizedName = StringUtils.trimToNull(getLocaleDN(locale, displayLocale));
            if (null != fullName && !StringUtils.equals(fullName, localizedName)) {
                return fullName.trim() + " (" + localizedName.trim() + ")";
            } else if (null != localizedName) {
                return localizedName.trim();
            } else if (null != fullName) {
                return fullName.trim();
            }
        }


        return locale.getDisplayName()
                + (!locale.equals(displayLocale) ? " (" + locale.getDisplayLanguage(locale) + ")" : "");

    }

    public static String getLocaleNativeName(Locale locale) {
        try {
            return getString(KEY_BASE_LOCALE_NAME, locale);
        } catch (MissingResourceException e) {
            return locale.getDisplayName(locale);
        }
    }

    public static Map<String, String> getLocaleNamesMap(Locale locale, Set<String> languageCodes, Map<String, List<String>> languagesMap) {
        Map<String, String> ret = new LinkedHashMap<String, String>();
        for (String languageCode : languageCodes) {
            List<String> languagelist = languagesMap.get(languageCode);

            String name = getLocaleFullDN(ITrackerResources.getLocale(languageCode), locale);

            ret.put(languageCode, name);
            for (String languageitem : languagelist) {
                name = getLocaleFullDN(ITrackerResources.getLocale(languageitem), locale);
                ret.put(languageitem, name);
            }

        }
        if (ret.size() == 0) {
            ret.put(getDefaultLocale(), getLocaleNativeName(getLocale(getDefaultLocale())));
        }
        return ret;

    }

    public static ResourceBundle getBundle() {
        return getBundle(getDefaultLocale());
    }

    public static ResourceBundle getBundle(String locale) {
        if (locale == null || locale.equals("")) {
            locale = getDefaultLocale();
        }

        return getBundle(getLocale(locale));
    }

    public static ResourceBundle getBundle(Locale locale) {
        if (locale == null) {
            locale = getLocale();
        }

        ResourceBundle bundle = languages.get(locale);
        if (bundle == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("getBundle: Loading new resource bundle for locale " + locale
                        + " from the database.");
            }
            if (!isInitialized()) {
                return ITrackerResourceBundle.loadBundle(locale);
            } else {

                Properties languageItems = configurationService
                        .getLanguageProperties(locale);

                logger.info("lazy loading locale bundle resources: {}", locale);

                bundle = ITrackerResourceBundle.loadBundle(locale, languageItems);
                logger.info("getBundle: got loaded for locale {} with {} items from the database.", locale, null == languageItems ? "no" : CollectionUtils.size(languageItems));
                logger.debug("getBundle: got loaded for locale {} with items {} from the database.", locale, languageItems);

                putBundle(locale, bundle);
            }

        }

        return bundle;
    }

    public static ResourceBundle getEditBundle(Locale locale) {
        if (locale == null) {
            locale = getLocale(getDefaultLocale());
        }
        ResourceBundle bundle;
        logger.debug("Loading new resource bundle for locale " + locale
                + " from the database.");
        Properties languageItems = configurationService.getLanguageProperties(
                locale);
        bundle = ITrackerResourceBundle.loadBundle(locale, languageItems);
        putBundle(locale, bundle);
        return bundle;
    }

    public static void putBundle(Locale locale, ResourceBundle bundle) {
        if (locale != null && bundle != null) {
            synchronized (bundleLock) {
                languages.put(locale, bundle);
                String localeString = locale.toString();
                if (localeString.length() == 5) {
                    localeString = localeString.substring(0, 2) + "_"
                            + localeString.substring(3).toUpperCase();
                }
                locales.put(localeString, locale);
            }
        }
    }


    /**
     * Clears a single cached resource bundle. The next time the bundle is
     * accessed, it will be reloaded and placed into the cache.
     */
    public static void clearBundle(Locale locale) {
        if (locale != null) {
            synchronized (bundleLock) {
                languages.remove(locale);
            }
        }
    }

    /**
     * Clears all cached resource bundles. The next time a bundle is accessed,
     * it will be reloaded and placed into the cache.
     */
    public static void clearBundles() {
        synchronized (bundleLock) {
            languages.clear();
        }
    }

    /**
     * Clears a single key from all cached resource bundles. The key is then
     * marked that it is dirty and should be reloaded on hte next access.
     */
    public static void clearKeyFromBundles(String key, boolean markDirty) {
        if (key != null) {
            synchronized (bundleLock) {
                for (ResourceBundle resourceBundle : languages.values()) {
                    ((ITrackerResourceBundle) resourceBundle).removeValue(key,
                            markDirty);
                }
            }
        }
    }

    public static String getString(String key) {
        return getString(key, getLocale(defaultLocale));
    }

    public static String getString(String key, String locale) {
        if (key == null) {
            return "";
        }

        if (locale == null || locale.equals("")) {
            locale = getDefaultLocale();
        }

        return getString(key, getLocale(locale));
    }

    private static String handleMissingResourceException(final MissingResourceException ex, final String key, final Locale locale) {

        logger.warn(
                "no value while retrieving translation key '{}' for locale {}", key, locale);
        Locale l = locale;
        if (null == l) {
            l = getLocale(getDefaultLocale());
        }
        if (StringUtils.isNotEmpty(l.getCountry())) {
            l = new Locale(l.getLanguage());
        } else if (StringUtils.isNotEmpty(l.getLanguage())) {
            l = new Locale("");
        }
        if (l != locale) {
            logger.debug("resolving {} from parent bundle ()", key, l);
            return getString(key, l);
        }
        throw ex;

    }
    private static String handleDirtyResourceException(final ITrackerDirtyResourceException e, final String key, final Locale locale) {

        logger.debug(
                "handleDirtyResourceException: key '{}' for locale {}", new Object[]{key, locale, e});
        ITrackerResourceBundle bundle = (ITrackerResourceBundle)getBundle(locale);
        try {
            final String languageItem = configurationService
                    .getLanguageEntry(key, locale);
            bundle.updateValue(key, languageItem);
            return languageItem;
        } catch (MissingResourceException e2) {
            bundle.removeValue(key, false);
            try {
                return bundle.getString(key);
            } catch (MissingResourceException e3) {
                return handleMissingResourceException(e2, key, locale);
            }
        }
    }

    public static String getString(final String key, final Locale locale) {
        if (key == null) {
            return "";
        }

        String val;
        try {
            final ResourceBundle bundle = getBundle(locale);
            try {
                val = bundle.getString(key);
                return val;

            } catch (ITrackerDirtyResourceException e) {
                val = handleDirtyResourceException(e, key, locale);
            } catch (MissingResourceException e) {
                val = handleMissingResourceException(e, key, locale);
            }
            return val;
        } catch (NullPointerException ex) {
            logger.error(
                    "Unable to get any resources.  The requested locale was "
                            + locale, ex);
            return "MISSING BUNDLE: " + locale;

        } catch (MissingResourceException ex) {
            logger.warn(
                    "MissingResourceException caught while retrieving translation key '{}' for locale {}", key, locale);
            logger.debug(
                    "MissingResourceException was", ex);
            return "MISSING KEY: " + key;
        } catch (RuntimeException ex) {
            logger.info("getString: not found " + key + " locale: " + locale,
                    ex);
            try {
                return getEditBundle(locale).getString(key);
            } catch (Exception ex2) {
                logger.warn(
                        "Exception caught while retrieving translation key '{}' for locale {}: {}", new Object[]{key, locale, ex2.getMessage()});
                logger.debug("Exception was", ex2);
                return "MISSING KEY: " + key;
            }
        }
    }

    public static String getString(String key, Object[] options) {
        return getString(key, getLocale(getDefaultLocale()), options);
    }

    public static String getString(String key, String locale, Object[] options) {
        return getString(key, getLocale(locale), options);
    }

    public static String getString(String key, Locale locale, Object[] options) {
        String message = getString(key, locale);
        return MessageFormat.format(message, options, locale);
    }

    public static String getString(String key, String locale, String option) {
        String message = getString(key, locale);
        return MessageFormat.format(message, new Object[]{option},
                getLocale(locale));
    }

    public static String getString(String key, Locale locale, String option) {
        String message = getString(key, locale);
        return MessageFormat.format(message, new Object[]{option}, locale);
    }

    public static String getCheckForKey(String key)
            throws MissingResourceException {
        return getCheckForKey(key, getLocale());
    }

    public static String getCheckForKey(String key, Locale locale)
            throws MissingResourceException {
        try {
            return getBundle(locale).getString(key);
        } catch (ITrackerDirtyResourceException idre) {
            return getString(key, locale);
        } catch (NullPointerException ex) {
            logger.debug("Unable to get ResourceBundle for locale " + locale,
                    ex);
            throw new MissingResourceException("MISSING LOCALE: " + locale,
                    "ITrackerResources", key);
        }
    }

    public static boolean isLongString(String key) {
        String value = getString(key);
        return value.length() > 80 || value.indexOf('\n') > 0;
    }

    public static String escapeUnicodeString(String str, boolean escapeAll) {
        if (str == null) {
            return "";
        }

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < str.length(); i++) {
            char ch = str.charAt(i);
            if (!escapeAll && ((ch >= 0x0020) && (ch <= 0x007e))) {
                sb.append(ch);
            } else {
                sb.append('\\').append('u');
                sb.append(encodeHex((ch >> 12) & 0xF));
                sb.append(encodeHex((ch >> 8) & 0xF));
                sb.append(encodeHex((ch >> 4) & 0xF));
                sb.append(encodeHex(ch & 0xF));
            }
        }
        return sb.toString();
    }

    public static String unescapeUnicodeString(String str) {
        final StringBuilder sb = new StringBuilder();

        for (int i = 0; i < str.length(); ) {
            char ch = str.charAt(i++);
            if (ch == '\\') {
                if (str.charAt(i++) == 'u') {
                    int value = 0;
                    for (int j = 0; j < 4; j++) {
                        value = (value << 4) + decodeHex(str.charAt(i++));
                    }
                    sb.append((char) value);
                } else {
                    sb.append("\\").append(str.charAt(i));
                }
            } else {
                sb.append(ch);
            }
        }
        return sb.toString();
    }

    public static final String HEXCHARS = "0123456789ABCDEF";

    public static char encodeHex(int value) {
        return HEXCHARS.charAt(value & 0xf);
    }

    public static int decodeHex(char ch) {
        int value = -1;

        if (ch >= '0' && ch <= '9') {
            value = ch - '0';
        } else if (ch >= 'a' && ch <= 'f') {
            value = ch - 'a' + 10;
        } else if (ch >= 'A' && ch <= 'F') {
            value = ch - 'A' + 10;
        }

        return value;
    }

    public static boolean isInitialized() {
        return initialized;
    }

    private static void setInitialized(boolean initialized) {
        ITrackerResources.initialized = initialized;
    }

    public static void setConfigurationService(ITrackerResourcesProvider service) {
        if (isInitialized()) {
            throw new IllegalStateException("Service is already set up.");
        }
        configurationService = service;
        String[] availableLocales = StringUtils.split(configurationService.getProperty("available_locales", getDefaultLocale()), ',');
        ArrayList<String> locales = new ArrayList<>(availableLocales.length);
        for (String l: availableLocales) {
            locales.add(StringUtils.trim(l));
        }
        ITrackerResources.availableLocales = locales;
        ITrackerResources.setDefaultLocale(configurationService.getProperty("default_locale", ITrackerResources.DEFAULT_LOCALE));

        logger.info("Set system default locale to '" + ITrackerResources.getDefaultLocale() + "'");
        setInitialized(true);

    }

}