/********************************************************************************** * $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 java.text.MessageFormat; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.Hashtable; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.Set; 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.tool.api.SessionManager; import org.sakaiproject.user.api.PreferencesService; /** * 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) */ @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; // Optional ClassLoader for ResourceBundle protected ClassLoader classLoader = null; // cached set of ResourceBundle objects protected Hashtable<Locale, ResourceBundle> bundles = new Hashtable<Locale, ResourceBundle>(); // cached set of last time bundle was loaded protected Hashtable<Locale, Date> bundlesTimestamp = new Hashtable<Locale, Date>(); // current user id protected String userId = null; // session key string for determining validity of ResourceBundle cache protected String LOCALE_SESSION_KEY = "sakai.locale."; // Debugging variables for displaying ResourceBundle name & property protected String DEBUG_LOCALE = "en_US_DEBUG"; private String DBG_PREFIX = "** "; private String DBG_SUFFIX = " **"; private static Object LOCK = new Object(); private static SessionManager sessionManager; protected static SessionManager getSessionManager() { if (sessionManager == null) { synchronized (LOCK) { sessionManager = (SessionManager) ComponentManager.get(SessionManager.class); } } return sessionManager; } private static PreferencesService preferencesService; protected static PreferencesService getPreferencesService() { if (preferencesService == null) { synchronized (LOCK) { preferencesService = (PreferencesService) ComponentManager.get(PreferencesService.class); } } return preferencesService; } /** * 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() **/ 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); if (value.length() == 0) return dflt; try { return Integer.parseInt(value); } catch (NumberFormatException e) { // ignore 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 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 = null; // 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 ) { StringBuilder dbgPropertiesString = new StringBuilder(DBG_PREFIX); dbgPropertiesString.append( this.baseName ); dbgPropertiesString.append( " " ); dbgPropertiesString.append( key ); dbgPropertiesString.append( DBG_SUFFIX ); return dbgPropertiesString.toString(); } /** ** 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; } } /** * 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 getBundleAsMap().keySet(); } /** * * Clear bundles hashmap */ public void purgeCache() { this.bundles = new Hashtable<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(); } /** * Return ResourceBundle for user's preferred locale * * @return user's ResourceBundle object */ protected ResourceBundle getBundle() { if (ServerConfigurationService.getBoolean("load.bundles.from.db", false)) { return getBundleFromDb(); } Locale loc = getLocale(); ResourceBundle bundle = (ResourceBundle) this.bundles.get(loc); if (bundle == null) { if (M_log.isDebugEnabled()) { M_log.debug("Load bundle name=" + this.baseName + ", locale=" + getLocale().toString()); } bundle = loadBundle(loc); } return bundle; } /** * * @return ResourceBundle from the cache or retrieved from the MessageService data store */ protected ResourceBundle getBundleFromDb() { Locale loc = getLocale(); //TODO consider using a better caching method here ResourceBundle bundle = (ResourceBundle) this.bundles.get(loc); Date timeStamp = (Date) this.bundlesTimestamp.get(loc); if ((timeStamp == null || timeStamp.getTime() + ServerConfigurationService.getInt("load.bundles.from.db.timeout", 30000) < new Date().getTime() ) ) { M_log.debug("Load bundle name=" + this.baseName + ", locale=" + getLocale().toString()); bundle = loadBundle(loc); } return bundle; } /** ** Return the ResourceBundle properties as a Map object **/ protected Map<Object, Object> getBundleAsMap() { Map<Object, Object> bundle = new Hashtable<Object, Object>(); for (Enumeration e = getBundle().getKeys(); e.hasMoreElements();) { Object key = e.nextElement(); bundle.put(key, getBundle().getObject((String) key)); } return bundle; } /** * Return ResourceBundle for specified locale * * @param loc * * @return locale specific ResourceBundle * (or empty ListResourceBundle in case of error) */ protected ResourceBundle loadBundle(Locale loc) { if (ServerConfigurationService.getBoolean("load.bundles.from.db", false)) { return loadBundleFromDb(loc); } ResourceBundle newBundle = null; try { if ( this.classLoader == null ) newBundle = ResourceBundle.getBundle(this.baseName, loc); else newBundle = ResourceBundle.getBundle(this.baseName, loc, this.classLoader); } catch (NullPointerException e) { } // ignore setBundle(loc, newBundle); return newBundle; } /** * Return ResourceBundle for specified locale. Returns values from the classpath * overridden with any values found in the database via the MessageService. Also, * responsible for indexing any new or changes values. * * @param loc * properties bundle * * * @return locale specific ResourceBundle * (or empty ListResourceBundle in case of error) */ protected ResourceBundle loadBundleFromDb(Locale loc) { ResourceBundle newBundle = null; try { newBundle = DbResourceBundle.addResourceBundle(this.baseName, loc, this.classLoader); } catch (Exception e) { if (M_log.isWarnEnabled()) { M_log.warn("loadBundle "+baseName+" "+loc.toString(), e ); } throw new MissingResourceException("ResourceLoader.loadBundle failed", "", "" ); } DbResourceBundle.indexResourceBundle(this.baseName, newBundle, loc, this.classLoader); 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); this.bundlesTimestamp.put(loc, new Date()); } } @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(); } }