org.apache.click.util.MessagesMap.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.click.util.MessagesMap.java

Source

/*
 * 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();
        }
    }
}