blob: a86e6f3a1b9d9d92283ad59c46f98888ee3f2519 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache 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.apache.org/licenses/LICENSE-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.apache.click.util;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletContext;
import org.apache.click.Context;
import org.apache.click.service.ConfigService;
import org.apache.commons.lang.Validate;
/**
* Provides a localized read only messages Map for Page and Control classes.
* <p/>
* A MessagesMap instance is available in each Velocity page using the name
* "<span class="blue">messages</span>".
* <p/>
* For example suppose you have a localized page title, which is stored in the
* Page's properties file. You can access page "title" message in your page
* template via:
*
* <pre class="codeHtml">
* <span class="blue">$messages.title</span> </pre>
*
* This is roughly equivalent to making the call:
*
* <pre class="codeJava">
* <span class="kw">public void</span> onInit() {
* ..
* addModel(<span class="st">"title"</span>, getMessage(<span class="st">"title"</span>);
* } </pre>
*
* Please note if the specified message does not exist in your Page's
* properties file, or if the Page does not have a properties file, then
* a <tt>MissingResourceException</tt> will be thrown.
* <p/>
* The ClickServlet adds a MessagesMap instance to the Velocity Context before
* it is merged with the page template.
*/
public class MessagesMap implements Map<String, String> {
/** Cache of resource bundle and locales which were not found, with support for multiple class loaders. */
private static final ClassLoaderCache<Set<String>> NOT_FOUND_CLASSLOADER_CACHE
= new ClassLoaderCache<Set<String>>();
/** Provides a synchronized cache of get value reflection methods, with support for multiple class loaders. */
protected static final ClassLoaderCache<Map<CacheKey, Map<String, String>>> MESSAGES_CLASSLOADER_CACHE
= new ClassLoaderCache<Map<CacheKey, Map<String, String>>>();
/** The cache key set load lock. */
protected static final Object CACHE_LOAD_LOCK = new Object();
// ----------------------------------------------------- Instance Variables
/** The base class. */
protected final Class<?> baseClass;
/** The class global resource bundle base name. */
protected final String globalBaseName;
/** The map of localized messages. */
protected Map<String, String> messages;
/** The resource bundle locale. */
protected final Locale locale;
// ----------------------------------------------------------- Constructors
/**
* Create a resource bundle messages <tt>Map</tt> adaptor for the given
* object's class resource bundle, the global resource bundle and
* <tt>Context</tt>.
* <p/>
* Messages located in the object's resource bundle will override any
* messages defined in the global resource bundle.
*
* @param baseClass the target class
* @param globalResource the global resource bundle name
*/
public MessagesMap(Class<?> baseClass, String globalResource) {
this(baseClass, globalResource, Context.getThreadLocalContext().getLocale());
}
/**
* Create a resource bundle messages <tt>Map</tt> adaptor for the given
* object's class resource bundle, the global resource bundle and
* <tt>Context</tt>.
* <p/>
* Messages located in the object's resource bundle will override any
* messages defined in the global resource bundle.
*
* @param baseClass the target class
* @param globalResource the global resource bundle name
* @param locale the resource bundle locale.
*/
public MessagesMap(Class<?> baseClass, String globalResource, Locale locale) {
Validate.notNull(baseClass, "Null object parameter");
this.baseClass = baseClass;
this.globalBaseName = globalResource;
this.locale = locale;
}
// --------------------------------------------------------- Public Methods
/**
* @see java.util.Map#size()
*/
public int size() {
ensureInitialized();
return messages.size();
}
/**
* @see java.util.Map#isEmpty()
*/
public boolean isEmpty() {
ensureInitialized();
return messages.isEmpty();
}
/**
* @see java.util.Map#containsKey(Object)
*/
public boolean containsKey(Object key) {
if (key != null) {
ensureInitialized();
return messages.containsKey(key.toString());
}
return false;
}
/**
* @see java.util.Map#containsValue(Object)
*/
public boolean containsValue(Object value) {
ensureInitialized();
return messages.containsValue(value);
}
/**
* Return localized resource message for the given key. If the message is
* not found a <tt>MissingResourceException</tt> will be thrown.
*
* @see java.util.Map#get(Object)
* @throws MissingResourceException if the given key was not found
*/
public String get(Object key) {
String value = null;
if (key != null) {
ensureInitialized();
value = messages.get(key.toString());
}
if (value == null) {
String msg = "Message \"{0}\" not found in bundle \"{1}\" for locale \"{2}\"";
String keyStr = (key != null) ? key.toString() : null;
Object[] args = { keyStr, baseClass.getName(), locale };
msg = MessageFormat.format(msg, args);
throw new MissingResourceException(msg, baseClass.getName(), keyStr);
}
return value;
}
/**
* This method is not supported and will throw
* <tt>UnsupportedOperationException</tt> if invoked.
*
* @see java.util.Map#put(Object, Object)
*/
public String put(String key, String value) {
throw new UnsupportedOperationException();
}
/**
* This method is not supported and will throw
* <tt>UnsupportedOperationException</tt> if invoked.
*
* @see java.util.Map#remove(Object)
*/
public String remove(Object key) {
throw new UnsupportedOperationException();
}
/**
* This method is not supported and will throw
* <tt>UnsupportedOperationException</tt> if invoked.
*
* @see java.util.Map#putAll(Map)
*/
public void putAll(Map<? extends String, ? extends String> map) {
throw new UnsupportedOperationException();
}
/**
* This method is not supported and will throw
* <tt>UnsupportedOperationException</tt> if invoked.
*
* @see java.util.Map#clear()
*/
public void clear() {
throw new UnsupportedOperationException();
}
/**
* @see java.util.Map#keySet()
*/
public Set<String> keySet() {
ensureInitialized();
return messages.keySet();
}
/**
* @see java.util.Map#values()
*/
public Collection<String> values() {
ensureInitialized();
return messages.values();
}
/**
* @see java.util.Map#entrySet()
*/
public Set<Map.Entry<String, String>> entrySet() {
ensureInitialized();
return messages.entrySet();
}
/**
* @see #toString()
*/
@Override
public String toString() {
ensureInitialized();
return messages.toString();
}
// ------------------------------------------------------ Protected Methods
/**
* Return the ResourceBundle for the given resource name and locale. By
* default this method will create a ResourceBundle using the standard JDK
* method: {@link java.util.ResourceBundle#getBundle(java.lang.String, java.util.Locale, java.lang.ClassLoader)}.
* <p/>
* You can create your own custom ResourceBundle by overriding this method.
* <p/>
* In order for Click to use your custom MessagesMap implementation, you
* need to provide your own {@link org.apache.click.service.MessagesMapService}
* or extend {@link org.apache.click.service.DefaultMessagesMapService}.
* <p/>
* The method {@link org.apache.click.service.MessagesMapService#createMessagesMap(java.lang.Class, java.lang.String, java.util.Locale) createMessagesMap},
* can be implemented to return your custom MessagesMap instances.
*
* @param resourceName the resource bundle name
* @param locale the resource bundle locale.
*
* @return the ResourceBundle for the given resource name and locale
*/
protected ResourceBundle createResourceBundle(String resourceName, Locale locale) {
return ClickUtils.getBundle(resourceName, locale);
}
/**
* This method initializes and populates the internal{@link #messages} map
* and cache {@link #getMessagesCache()} if it is not already initialized.
* <p/>
* <b>Please Note:</b> populating the cache {@link #getMessagesCache()} is not thread safe
* and access to the cache must be properly synchronized.
*/
protected void ensureInitialized() {
if (messages == null) {
CacheKey resourceKey = new CacheKey(globalBaseName,
baseClass.getName(), locale.toString());
messages = getMessagesCache().get(resourceKey);
if (messages != null) {
return;
}
messages = new HashMap<String, String>();
synchronized (CACHE_LOAD_LOCK) {
loadResourceValuesIntoMap(globalBaseName, messages);
List<String> classnameList = new ArrayList<String>();
// Build class list
Class<?> aClass = baseClass;
while (!aClass.getName().equals("java.lang.Object")) {
classnameList.add(aClass.getName());
aClass = aClass.getSuperclass();
}
// Load messages from parent to child order, so that child
// class messages override parent messages.
for (int i = classnameList.size() - 1; i >= 0; i--) {
String className = classnameList.get(i);
loadResourceValuesIntoMap(className, messages);
}
messages = Collections.unmodifiableMap(messages);
ServletContext servletContext = Context.getThreadLocalContext().getServletContext();
ConfigService configService = ClickUtils.getConfigService(servletContext);
if (configService.isProductionMode() || configService.isProfileMode()) {
getMessagesCache().put(resourceKey, messages);
}
}
}
}
/**
* Load the values of the given resourceBundleName into the map.
*
* @param resourceBundleName the resource bundle name
* @param map the map to load resource values into
*/
protected void loadResourceValuesIntoMap(String resourceBundleName, Map<String, String> map) {
if (resourceBundleName == null) {
return;
}
String resourceKey = resourceBundleName + locale.toString();
if (!getNotFoundCache().contains(resourceKey)) {
try {
ResourceBundle resources = createResourceBundle(resourceBundleName, locale);
Enumeration<String> e = resources.getKeys();
while (e.hasMoreElements()) {
String name = e.nextElement();
String value = resources.getString(name);
map.put(name, value);
}
} catch (MissingResourceException mre) {
getNotFoundCache().add(resourceKey);
}
}
}
// Private Methods --------------------------------------------------------
protected static Set<String> getNotFoundCache() {
Set<String> notFoundCache = NOT_FOUND_CLASSLOADER_CACHE.get();
if (notFoundCache == null) {
notFoundCache = new HashSet<String>();
NOT_FOUND_CLASSLOADER_CACHE.put(notFoundCache);
}
return notFoundCache;
}
protected static Map<CacheKey, Map<String, String>> getMessagesCache() {
Map<CacheKey, Map<String, String>> messagesCache = MESSAGES_CLASSLOADER_CACHE.get();
if (messagesCache == null) {
messagesCache = new ConcurrentHashMap<CacheKey, Map<String, String>>();
MESSAGES_CLASSLOADER_CACHE.put(messagesCache);
}
return messagesCache;
}
/**
* See DRY Performance article by Kirk Pepperdine.
* <p/>
* http://www.javaspecialists.eu/archive/Issue134.html
*/
private static class CacheKey {
/** Global base name to encapsulate in cache key. */
private final String globalBaseName;
/** Base class name to encapsulate in cache key. */
private final String baseClass;
/** Locale to encapsulate in cache key. */
private final String locale;
/**
* Constructs a new CacheKey for the given baseName, baseClass and
* locale.
*
* @param globalBaseName the base name to build the cache key for
* @param baseClass the base class name to build the cache key for
* @param locale the request locale to build the cache key for
*/
public CacheKey(String globalBaseName, String baseClass, String locale) {
if (globalBaseName == null) {
throw new IllegalArgumentException("Null globalBaseName parameter");
}
if (baseClass == null) {
throw new IllegalArgumentException("Null baseClass parameter");
}
if (locale == null) {
throw new IllegalArgumentException("Null locale parameter");
}
this.globalBaseName = globalBaseName;
this.baseClass = baseClass;
this.locale = locale;
}
/**
* @see Object#equals(Object)
*
* @param o the object with which to compare this instance with
* @return true if the specified object is the same as this object
*/
@Override
public final boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof CacheKey)) {
return false;
}
CacheKey that = (CacheKey) o;
if (!globalBaseName.equals(that.globalBaseName)) {
return false;
}
if (!baseClass.equals(that.baseClass)) {
return false;
}
if (!locale.equals(that.locale)) {
return false;
}
return true;
}
/**
* @see Object#hashCode()
*
* @return a hash code value for this object.
*/
@Override
public final int hashCode() {
return globalBaseName.hashCode()
* 31 + baseClass.hashCode()
* 31 + locale.hashCode();
}
}
}