Java tutorial
/* * 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 javax.servlet.ServletContext; import org.apache.commons.lang.Validate; import org.apache.click.Context; import org.apache.click.service.ConfigService; /** * 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. */ protected static final Set<String> NOT_FOUND_CACHE = Collections.synchronizedSet(new HashSet<String>()); /** Cache of messages keyed by bundleName + Locale name. */ protected static final Map<Object, Map<String, String>> MESSAGES_CACHE = new HashMap<Object, 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 {@link #MESSAGES_CACHE} if it is not already initialized. * <p/> * <b>Please Note:</b> populating {@link #MESSAGES_CACHE} 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 = MESSAGES_CACHE.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()) { MESSAGES_CACHE.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 (!NOT_FOUND_CACHE.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) { NOT_FOUND_CACHE.add(resourceKey); } } } // Private Methods -------------------------------------------------------- /** * 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(); } } }