org.sakaiproject.util.ResourceLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.sakaiproject.util.ResourceLoader.java

Source

/**********************************************************************************
 * $URL$
 * $Id$
 ***********************************************************************************
 *
 * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 Sakai Foundation
 *
 * Licensed under the Educational Community License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 **********************************************************************************/

package org.sakaiproject.util;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.component.cover.ServerConfigurationService;
import org.sakaiproject.i18n.InternationalizedMessages;
import org.sakaiproject.messagebundle.api.MessageBundleService;
import org.sakaiproject.thread_local.api.ThreadLocalManager;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.user.api.PreferencesService;

import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * ResourceLoader provides an alternate implementation of org.util.ResourceBundle, dynamically selecting the prefered locale from either the user's session or from the user's sakai preferences
 * 
 * @author Sugiura, Tatsuki (University of Nagoya)
 * @author Aaron Zeckoski (azeckoski @ unicon.net)
 */
@SuppressWarnings("rawtypes")
public class ResourceLoader extends DummyMap implements InternationalizedMessages {
    protected static final Log M_log = LogFactory.getLog(ResourceLoader.class);

    // name of ResourceBundle
    protected String baseName = null;

    public String getBaseName() {
        return baseName;
    }

    // Optional ClassLoader for ResourceBundle
    protected ClassLoader classLoader = null;

    public ClassLoader getClassLoader() {
        return classLoader;
    }

    // cached set of ResourceBundle objects
    protected ConcurrentHashMap<Locale, ResourceBundle> bundles = new ConcurrentHashMap<Locale, ResourceBundle>();

    // current user id
    protected String userId = null;

    public String getUserId() {
        return userId;
    }

    // session key string for determining validity of ResourceBundle cache
    protected final String LOCALE_SESSION_KEY = "sakai.locale.";

    // Debugging variables for displaying ResourceBundle name & property   
    protected final String DEBUG_LOCALE = "en_US_DEBUG";
    private final String DBG_PREFIX = "** ";
    private final String DBG_SUFFIX = " **";

    private final static Object LOCK = new Object();

    private static SessionManager sessionManager;

    protected SessionManager getSessionManager() {
        if (sessionManager == null) {
            synchronized (LOCK) {
                sessionManager = (SessionManager) ComponentManager.get(SessionManager.class);
            }
        }
        return sessionManager;
    }

    private static PreferencesService preferencesService;

    protected PreferencesService getPreferencesService() {
        if (preferencesService == null) {
            synchronized (LOCK) {
                preferencesService = (PreferencesService) ComponentManager.get(PreferencesService.class);
            }
        }
        return preferencesService;
    }

    private static MessageBundleService messageBundleService;

    protected MessageBundleService getMessageBundleService() {
        if (messageBundleService == null) {
            synchronized (LOCK) {
                messageBundleService = (MessageBundleService) ComponentManager
                        .get(MessageBundleService.class.getName());
            }
        }
        return messageBundleService;
    }

    private static ThreadLocalManager threadLocalManager;

    protected static ThreadLocalManager getThreadLocalManager() {
        if (threadLocalManager == null) {
            synchronized (LOCK) {
                threadLocalManager = (ThreadLocalManager) ComponentManager.get(ThreadLocalManager.class);
            }
        }
        return threadLocalManager;
    }

    /**
     * Default constructor (may be used to find user's default locale 
     *                      without specifying a bundle)
     */
    public ResourceLoader() {
    }

    /**
     * Constructor: set baseName
     * 
     * @param name
     *        default ResourceBundle base filename
     */
    public ResourceLoader(String name) {
        this.baseName = name;
    }

    /**
     * Constructor: set baseName
     * 
     * @param name default ResourceBundle base filename
     * @param classLoader ClassLoader for ResourceBundle 
     */
    public ResourceLoader(String name, ClassLoader classLoader) {
        this.baseName = name;
        this.classLoader = classLoader;
    }

    /**
     * Constructor: specified userId, specified baseName 
     *              (either may be null)
     * 
     * @param userId user's internal sakai id (e.g. user.getId())
     * @param name  default ResourceBundle base filename
     */
    public ResourceLoader(String userId, String name) {
        this.userId = userId;
        this.baseName = name;
    }

    /**
     ** Return ResourceBundle properties as if Map.entrySet() 
     **/
    @Override
    public Set entrySet() {
        return getBundleAsMap().entrySet();
    }

    /**
     * * Return (generic object) value for specified property in current locale specific ResourceBundle
     * 
     * @param key
     *        property key to look up in current ResourceBundle * *
     * @return value for specified property key
     */
    public Object get(Object key) {
        return getString(key.toString());
    }

    /**
     ** Return formatted message based on locale-specific pattern
     **
     ** @param key maps to locale-specific pattern in properties file
     ** @param args parameters to format and insert according to above pattern
     ** @return  formatted message
     ** 
     ** @author Sugiura, Tatsuki (University of Nagoya)
     ** @author Jean-Francois Leveque (Universite Pierre et Marie Curie - Paris 6)
     **
     **/
    public String getFormattedMessage(String key, Object... args) {
        if (getLocale().toString().equals(DEBUG_LOCALE))
            return formatDebugPropertiesString(key);

        String pattern = (String) get(key);
        if (M_log.isDebugEnabled()) {
            M_log.debug("getFormattedMessage(key,args) bundle name=" + this.baseName + ", locale="
                    + getLocale().toString() + ", key=" + key + ", pattern=" + pattern);
        }

        return (new MessageFormat(pattern, getLocale())).format(args, new StringBuffer(), null).toString();
    }

    /**
     * Access some named configuration value as an int.
     * 
     * @param key
     *        property key to look up in current ResourceBundle
     * @param dflt
     *        The value to return if not found.
     * @return The property value with this name, or the default value if not found.
     */
    public int getInt(String key, int dflt) {
        String value = getString(key);

        int originalLength = value.length();
        if (originalLength == 0)
            return dflt;

        try {
            value = value.trim();
            if (originalLength != value.length()) {
                M_log.warn("getInt(key, dflt) bundle name=" + this.baseName + ", locale=" + getLocale() + ", key="
                        + key + ", dflt=" + dflt + ", Trailing whitespace trimmed.");
            }
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            if (M_log.isDebugEnabled()) {
                M_log.debug("getInt(key, dflt) bundle name=" + this.baseName + ", locale=" + getLocale() + ", key="
                        + key + ", dflt=" + dflt + ", NumberFormatException");
            }
        }
        return dflt;
    }

    /**
    ** Return a locale's display Name
    **
    ** @return String used to display Locale
    **
    ** @author Jean-Francois Leveque (Universite Pierre et Marie Curie - Paris 6)
    **/
    public String getLocaleDisplayName(Locale loc) {
        Locale preferedLoc = getLocale();

        StringBuilder displayName = new StringBuilder(loc.getDisplayLanguage(loc));

        if (StringUtils.isNotBlank(loc.getDisplayCountry(loc))) {
            displayName.append(" - ").append(loc.getDisplayCountry(loc));
        }

        if (StringUtils.isNotBlank(loc.getVariant())) {
            displayName.append(" (").append(loc.getDisplayVariant(loc)).append(")");
        }

        displayName.append(" [").append(loc.toString()).append("] ");
        displayName.append(loc.getDisplayLanguage(preferedLoc));

        if (StringUtils.isNotBlank(loc.getDisplayCountry(preferedLoc))) {
            displayName.append(" - ").append(loc.getDisplayCountry(preferedLoc));
        }

        return displayName.toString();
    }

    /**
    ** Return orientation of given locale
    **
    ** @param loc Locale to obtain orientation
    ** @return String with two possible values "rtl" or "ltr"
    ** @author jjmerono
    **/
    public String getOrientation(Locale loc) {
        String[] locales = ServerConfigurationService.getStrings("locales.rtl");
        if (locales == null) {
            locales = new String[] { "ar", "dv", "fa", "ha", "he", "iw", "ji", "ps", "ur", "yi" };
        }
        return ArrayUtil.contains(locales, loc.getLanguage()) ? "rtl" : "ltr";
    }

    /**
     ** Return user's prefered locale
     **    First: return locale from Sakai user preferences, if available
     **    Second: return locale from session, if available
     **    Last: return system default locale
     **
     ** @return user's Locale object
    **
    ** @author Sugiura, Tatsuki (University of Nagoya)
    ** @author Jean-Francois Leveque (Universite Pierre et Marie Curie - Paris 6)
    ** @author Steve Swinsburg (steve.swinsburg@gmail.com)
     **/
    public Locale getLocale() {
        Locale loc;
        // check if locale is requested for specific user
        if (this.userId != null) {
            loc = getLocale(this.userId);
        } else {
            try {

                //get current sessionId to use as the key.
                //this allows the anon user to also have locale settings 
                String sessionId = getSessionManager().getCurrentSession().getId();
                M_log.debug("Retrieving locale for sessionId: " + sessionId);
                loc = (Locale) getSessionManager().getCurrentSession().getAttribute(LOCALE_SESSION_KEY + sessionId);

            } catch (NullPointerException e) {
                loc = null;
                if (M_log.isWarnEnabled()) {
                    M_log.warn(
                            "getLocale() swallowing NPE - caused by a null sessionmanager or null session, OK for tests, problem if production");
                    //e.printStackTrace();
                }
            }

            // The locale is not in the session at all, so set in session
            if (loc == null) {
                loc = setContextLocale(null);
            }
        }

        if (loc == null) {
            M_log.info("getLocale() Locale not found in preferences or session, returning default");
            loc = Locale.getDefault();
        }

        M_log.debug("Locale: " + loc.toString());

        return loc;
    }

    /**
     ** This method formats a debugging string using the properties key.
     ** This allows easy identification of context for properties keys, and
     ** also highlights any hard-coded text, when the debug locale is selected
     **/
    protected String formatDebugPropertiesString(String key) {
        return DBG_PREFIX + this.baseName + " " + key + DBG_SUFFIX;
    }

    /**
     ** Get user's preferred locale (or null if not set)
     ***/
    public Locale getLocale(String userId) {
        return getPreferencesService().getLocale(userId);
    }

    /**
     ** Sets user's prefered locale in context
     **    First: sets  locale from Sakai user preferences, if available
     **    Second: sets locale from user session, if available
     **    Last: sets system default locale
     **
     ** @return user's Locale object
    **
    ** @author Sugiura, Tatsuki (University of Nagoya)
    ** @author Jean-Francois Leveque (Universite Pierre et Marie Curie - Paris 6)
    ** @author Steve Swinsburg (steve.swinsburg@gmail.com)
     **/
    public Locale setContextLocale(Locale loc) {

        //    First : find locale from Sakai user preferences, if available
        if (loc == null) {
            try {
                String userId = getSessionManager().getCurrentSessionUserId();

                if (M_log.isDebugEnabled()) {
                    M_log.debug("setContextLocale(Locale), checking user preferences for userId: " + userId);
                }
                loc = getLocale(userId);
            } catch (Exception e) {
                if (M_log.isWarnEnabled()) {
                    M_log.warn("setContextLocale(Locale) swallowing Exception");
                    e.printStackTrace();
                }
            } // ignore and continue
        }

        // Second: find locale from user browser session, if available
        if (loc == null) {
            try {
                if (M_log.isDebugEnabled()) {
                    M_log.debug("setContextLocale(Locale), checking browser session.");
                }
                loc = (Locale) getSessionManager().getCurrentSession().getAttribute("locale");

            } catch (NullPointerException e) {
                if (M_log.isWarnEnabled()) {
                    M_log.warn("setContextLocale(Locale) swallowing NPE");
                    e.printStackTrace();
                }
            } // ignore and continue
        }

        // Last: find system default locale
        if (loc == null) {
            // fallback to default.
            loc = Locale.getDefault();
            if (M_log.isDebugEnabled()) {
                M_log.debug("setContextLocale(Locale), using default locale");
            }
        } else if (!Locale.getDefault().getLanguage().equals("en") && loc.getLanguage().equals("en")
                && !loc.toString().equals(DEBUG_LOCALE)) {
            // Tweak for English: en is default locale. It has no suffix in filename.
            loc = new Locale("");
            if (M_log.isDebugEnabled()) {
                M_log.debug("setContextLocale(Locale), Tweak for English");
            }
        }

        if (M_log.isDebugEnabled()) {
            M_log.debug("Locale is: " + loc.toString());
        }

        //Write the sakai locale into the session with sessionId as key.
        //We do it this way so that anon users can also leverage the locale settings of sites, since anon users have a session.
        //see KNL-984
        try {
            String sessionId = getSessionManager().getCurrentSession().getId();

            if (M_log.isDebugEnabled()) {
                M_log.debug("Setting locale into session: " + sessionId);
            }

            getSessionManager().getCurrentSession().setAttribute(LOCALE_SESSION_KEY + sessionId, loc);
        } catch (Exception e) {
            if (M_log.isWarnEnabled()) {
                M_log.warn("setContextLocale(Locale) swallowing Exception");
                //e.printStackTrace();
            }
        } //Ignore and continue

        return loc;
    }

    /**
     ** Returns true if the given key is defined, otherwise false
     **/
    public boolean getIsValid(String key) {
        try {
            String value = getBundle().getString(key);
            return value != null;
        } catch (MissingResourceException e) {
            return false;
        }
    }

    @Override
    public boolean containsKey(Object key) {
        if (key == null || !(key instanceof String)) {
            return false;
        }
        return getIsValid((String) key);
    }

    /**
    * Return string value for specified property in current locale specific ResourceBundle
    * 
    * @param key property key to look up in current ResourceBundle
    * @return String value for specified property key
    */
    public String getString(String key) {
        if (getLocale().toString().equals(DEBUG_LOCALE)) {
            return formatDebugPropertiesString(key);
        }

        try {
            String value = getBundle().getString(key);
            if (M_log.isDebugEnabled()) {
                M_log.debug("getString(key) bundle name=" + this.baseName + ", locale=" + getLocale().toString()
                        + ", key=" + key + ", value=" + value);
            }
            return value;

        } catch (MissingResourceException e) {
            if (M_log.isWarnEnabled()) {
                M_log.warn(
                        "bundle \'" + baseName + "\'  missing key: \'" + key + "\'  from: " + e.getStackTrace()[3]); // 3-deep gets us out of ResourceLoader
            }
            return "[missing key (mre): " + baseName + " " + key + "]";
        } catch (NullPointerException e) {
            if (M_log.isWarnEnabled()) {
                M_log.warn("bundle \'" + baseName + "\'  null pointer exception: \'" + key + "\'  from: "
                        + e.getStackTrace()[3]); // 3-deep gets us out of ResourceLoader
            }
            return "[missing key (npe): " + baseName + " " + key + "]";
        } catch (ClassCastException e) {
            if (M_log.isWarnEnabled()) {
                M_log.warn("bundle \'" + baseName + "\'  class cast exception: \'" + key + "\'  from: "
                        + e.getStackTrace()[3]); // 3-deep gets us out of ResourceLoader
            }
            return "[missing key (clc): " + baseName + " " + key + "]";
        }
    }

    /**
     * Return string value for specified property in current locale specific ResourceBundle
     * 
     * @param key
     *        property key to look up in current ResourceBundle
     * @param dflt
     *        the default value to be returned in case the property is missing
     * @return String value for specified property key
     */
    public String getString(String key, String dflt) {
        if (getLocale().toString().equals(DEBUG_LOCALE))
            return formatDebugPropertiesString(key);

        try {
            return getBundle().getString(key);
        } catch (MissingResourceException e) {
            return dflt;
        } catch (NullPointerException e) {
            return dflt;
        } catch (ClassCastException e) {
            return dflt;
        }
    }

    /**
     * Access some named property values as an array of strings. The name is the base name. name + ".count" must be defined to be a positive integer - how many are defined. name + "." + i (1..count) must be defined to be the values.
     * 
     * @param key
     *        property key to look up in current ResourceBundle
     * @return The property value with this name, or the null if not found.
     *
     * @author Sugiura, Tatsuki (University of Nagoya)
     * @author Jean-Francois Leveque (Universite Pierre et Marie Curie - Paris 6)
     *
     */
    public String[] getStrings(String key) {
        if (getLocale().toString().equals(DEBUG_LOCALE))
            return new String[] { formatDebugPropertiesString(key) };

        // get the count
        int count = getInt(key + ".count", 0);
        if (count > 0) {
            String[] rv = new String[count];
            for (int i = 1; i <= count; i++) {
                String value = "";
                try {
                    value = getBundle().getString(key + "." + i);
                } catch (MissingResourceException e) {
                    if (M_log.isWarnEnabled()) {
                        M_log.warn("getStrings(" + key + ") swallowing MissingResourceException for String " + i);
                        e.printStackTrace();
                    }
                    // ignore the exception
                }
                rv[i - 1] = value;
            }
            return rv;
        }

        return null;
    }

    /**
     ** Return ResourceBundle properties as if Map.keySet() 
     **/
    public Set keySet() {
        return getBundle().keySet();
    }

    /**
     * * Clear bundles hashmap
     */
    public void purgeCache() {
        this.bundles = new ConcurrentHashMap<Locale, ResourceBundle>();
        M_log.debug("purge bundle cache");
    }

    /**
     * Set baseName
     * 
     * @param name
     *        default ResourceBundle base filename
     */
    public void setBaseName(String name) {
        if (M_log.isDebugEnabled()) {
            M_log.debug("set baseName=" + name);
        }
        this.baseName = name;
    }

    /**
     ** Return ResourceBundle properties as if Map.values() 
     **/
    public Collection values() {
        return getBundleAsMap().values();
    }

    @Override
    public int size() {
        return getBundle().keySet().size();
    }

    @Override
    public boolean isEmpty() {
        return getBundle().keySet().isEmpty();
    }

    /**
    * Return ResourceBundle for user's preferred locale
    * 
    * @return user's ResourceBundle object
    */
    protected ResourceBundle getBundle() {
        Locale loc = getLocale();
        String context = (String) getThreadLocalManager().get(org.sakaiproject.util.RequestFilter.CURRENT_CONTEXT);

        if (M_log.isDebugEnabled())
            M_log.debug("Request for bundle " + baseName + "/" + context + "/" + loc.toString());

        ResourceBundle bundle = this.bundles.get(loc);
        if (bundle == null) {
            bundle = loadBundle(context, loc);
        }

        if (ServerConfigurationService.getBoolean("load.bundles.from.db", false)) {

            Map<String, String> bundleFromDbMap = getMessageBundleService().getBundle(baseName, context, loc);
            if (!bundleFromDbMap.isEmpty()) {
                // skip if there are no modified bundle data
                Map<String, Object> bundleMap = getBundleAsMap(bundle);
                bundleMap.putAll(bundleFromDbMap);
                bundle = new MapResourceBundle(bundleMap, baseName, loc);
            }
            if (M_log.isDebugEnabled()) {
                M_log.debug("Bundle from db added " + bundleFromDbMap.size() + " properties to " + baseName + "/"
                        + context + "/" + loc.toString());
            }
        }
        return bundle;
    }

    /**
     ** Return the ResourceBundle properties as a Map object
     **/
    protected Map<String, Object> getBundleAsMap() {
        return getBundleAsMap(getBundle());
    }

    protected Map<String, Object> getBundleAsMap(ResourceBundle bundle) {
        Map<String, Object> bundleMap = new HashMap<String, Object>();

        for (Enumeration e = bundle.getKeys(); e.hasMoreElements();) {
            Object key = e.nextElement();
            bundleMap.put((String) key, bundle.getObject((String) key));
        }

        return bundleMap;
    }

    /**
     * Return ResourceBundle for specified locale
     * 
     * @param loc
     *
     * @return locale specific ResourceBundle
     *         (or empty ListResourceBundle in case of error)
     */
    protected ResourceBundle loadBundle(String context, Locale loc) {
        ResourceBundle newBundle = null;
        try {
            if (classLoader == null) {
                newBundle = ResourceBundle.getBundle(baseName, loc);
            } else {
                newBundle = ResourceBundle.getBundle(baseName, loc, classLoader);
            }
        } catch (NullPointerException e) {
            // IGNORE FAILURE
        }

        if (ServerConfigurationService.getBoolean("load.bundles.from.db", false)) {
            // typically bundles with an empty context are from shared
            // and we are not adding bundles with an empty context to MessageBundleService
            if (StringUtils.isNotBlank(context)) {
                getMessageBundleService().saveOrUpdate(baseName, context, newBundle, loc);
                setBundle(loc, newBundle);
            }
        } else {
            setBundle(loc, newBundle);
        }
        return newBundle;
    }

    /**
     * Add loc (key) and bundle (value) to this.bundles hash
     * 
     * @param loc
     *        Language/Region Locale *
     * @param bundle
     *        properties bundle
     */
    protected void setBundle(Locale loc, ResourceBundle bundle) {
        if (loc == null || bundle == null)
            return;
        this.bundles.put(loc, bundle);
    }

    @Override
    public String toString() {
        return "ResourceLoader{" + "base='" + baseName + '\'' + ", user='" + userId + '\'' + '}';
    }

}

@SuppressWarnings({ "rawtypes" })
abstract class DummyMap implements Map {
    public void clear() {
        throw new UnsupportedOperationException();
    }

    public boolean containsKey(Object key) {
        return true;
    }

    public boolean containsValue(Object value) {
        throw new UnsupportedOperationException();
    }

    public Set entrySet() {
        throw new UnsupportedOperationException();
    }

    public abstract Object get(Object key);

    public boolean isEmpty() {
        throw new UnsupportedOperationException();
    }

    public Set keySet() {
        throw new UnsupportedOperationException();
    }

    public Object put(Object arg0, Object arg1) {
        throw new UnsupportedOperationException();
    }

    public void putAll(Map arg0) {
        throw new UnsupportedOperationException();
    }

    public Object remove(Object key) {
        throw new UnsupportedOperationException();
    }

    public int size() {
        throw new UnsupportedOperationException();
    }

    public Collection values() {
        throw new UnsupportedOperationException();
    }
}