org.apache.wicket.Localizer.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.Localizer.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.wicket;

import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.wicket.core.util.string.interpolator.ConvertingPropertyVariableInterpolator;
import org.apache.wicket.markup.repeater.AbstractRepeater;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.resource.loader.IStringResourceLoader;
import org.apache.wicket.settings.ResourceSettings;
import org.apache.wicket.util.lang.Generics;
import org.apache.wicket.util.string.AppendingStringBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A utility class that encapsulates all of the localization related functionality in a way that it
 * can be accessed by all areas of the framework in a consistent way. A singleton instance of this
 * class is available via the <code>Application</code> object.
 * <p>
 * You may register additional IStringResourceLoader to extend or replace Wickets default search
 * strategy for the properties. E.g. string resource loaders which load the properties from a
 * database. There should be hardly any need to extend Localizer.
 * 
 * @see org.apache.wicket.settings.ResourceSettings#getLocalizer()
 * @see org.apache.wicket.resource.loader.IStringResourceLoader
 * @see org.apache.wicket.settings.ResourceSettings#getStringResourceLoaders()
 * 
 * @author Chris Turner
 * @author Juergen Donnerstag
 */
public class Localizer {
    private static final Logger log = LoggerFactory.getLogger(Localizer.class);

    /** ConcurrentHashMap does not allow null values */
    private static final String NULL_VALUE = "<null-value>";

    /** Cache properties */
    private Map<String, String> cache = newCache();

    /** Database that maps class names to an integer id. */
    private final ClassMetaDatabase metaDatabase = new ClassMetaDatabase();

    /**
     * @return Same as Application.get().getResourceSettings().getLocalizer()
     */
    public static Localizer get() {
        return Application.get().getResourceSettings().getLocalizer();
    }

    /**
     * Create the utils instance class backed by the configuration information contained within the
     * supplied application object.
     */
    public Localizer() {
    }

    /**
     * Clear all cache entries by instantiating a new cache object
     * 
     * @see #newCache()
     */
    public final void clearCache() {
        if (cache != null) {
            cache = newCache();
        }
    }

    /**
     * @see #getString(String, Component, IModel, Locale, String, String)
     * 
     * @param key
     *            The key to obtain the resource for
     * @param component
     *            The component to get the resource for
     * @return The string resource
     * @throws MissingResourceException
     *             If resource not found and configuration dictates that exception should be thrown
     */
    public String getString(final String key, final Component component) throws MissingResourceException {
        return getString(key, component, null, null, null, (String) null);
    }

    /**
     * @see #getString(String, Component, IModel, Locale, String, String)
     * 
     * @param key
     *            The key to obtain the resource for
     * @param component
     *            The component to get the resource for
     * @param model
     *            The model to use for property substitutions in the strings (optional)
     * @return The string resource
     * @throws MissingResourceException
     *             If resource not found and configuration dictates that exception should be thrown
     */
    public String getString(final String key, final Component component, final IModel<?> model)
            throws MissingResourceException {
        return getString(key, component, model, null, null, (String) null);
    }

    /**
     * @see #getString(String, Component, IModel, Locale, String, String)
     * 
     * @param key
     *            The key to obtain the resource for
     * @param component
     *            The component to get the resource for
     * @param defaultValue
     *            The default value (optional)
     * @return The string resource
     * @throws MissingResourceException
     *             If resource not found and configuration dictates that exception should be thrown
     */
    public String getString(final String key, final Component component, final String defaultValue)
            throws MissingResourceException {
        return getString(key, component, null, null, null, defaultValue);
    }

    /**
     * @see #getString(String, Component, IModel, Locale, String, String)
     * 
     * @param key
     *            The key to obtain the resource for
     * @param component
     *            The component to get the resource for
     * @param model
     *            The model to use for property substitutions in the strings (optional)
     * @param defaultValue
     *            The default value (optional)
     * @return The string resource
     * @throws MissingResourceException
     *             If resource not found and configuration dictates that exception should be thrown
     */
    public String getString(final String key, final Component component, final IModel<?> model,
            final String defaultValue) throws MissingResourceException {
        return getString(key, component, model, null, null, defaultValue);
    }

    /**
     * Get the localized string using all of the supplied parameters. This method is left public to
     * allow developers full control over string resource loading. However, it is recommended that
     * one of the other convenience methods in the class are used as they handle all of the work
     * related to obtaining the current user locale and style information.
     * 
     * @param key
     *            The key to obtain the resource for
     * @param component
     *            The component to get the resource for (optional)
     * @param model
     *            The model to use for substitutions in the strings (optional)
     * @param locale
     *            If != null, it'll supersede the component's locale
     * @param style
     *            If != null, it'll supersede the component's style
     * @param defaultValue
     *            The default value (optional)
     * @return The string resource
     * @throws MissingResourceException
     *             If resource not found and configuration dictates that exception should be thrown
     */
    public String getString(final String key, final Component component, final IModel<?> model, final Locale locale,
            final String style, final String defaultValue) throws MissingResourceException {
        IModel<String> defaultValueModel = defaultValue != null ? Model.of(defaultValue) : null;
        return getString(key, component, model, locale, style, defaultValueModel);
    }

    /**
     * Get the localized string using all of the supplied parameters. This method is left public to
     * allow developers full control over string resource loading. However, it is recommended that
     * one of the other convenience methods in the class are used as they handle all of the work
     * related to obtaining the current user locale and style information.
     *
     * @param key
     *            The key to obtain the resource for
     * @param component
     *            The component to get the resource for (optional)
     * @param model
     *            The model to use for substitutions in the strings (optional)
     * @param locale
     *            If != null, it'll supersede the component's locale
     * @param style
     *            If != null, it'll supersede the component's style
     * @param defaultValue
     *            The default value (optional)
     * @return The string resource
     * @throws MissingResourceException
     *             If resource not found and configuration dictates that exception should be thrown
     */
    public String getString(final String key, final Component component, final IModel<?> model, final Locale locale,
            final String style, final IModel<String> defaultValue) throws MissingResourceException {
        final ResourceSettings resourceSettings = Application.get().getResourceSettings();

        String value = getStringIgnoreSettings(key, component, model, locale, style, null);

        // If a property value has been found, or a default value was given,
        // then replace the placeholder and we are done
        if (value != null) {
            return value;
        } else if (defaultValue != null && resourceSettings.getUseDefaultOnMissingResource()) {
            // Resource not found, so handle missing resources based on
            // application configuration and try the default value
            value = defaultValue.getObject();

            if (value != null) {
                // If a property value has been found, or a default value was given,
                // then replace the placeholder and we are done
                return substitutePropertyExpressions(component, value, model);
            }
        }

        if (resourceSettings.getThrowExceptionOnMissingResource()) {
            AppendingStringBuffer message = new AppendingStringBuffer("Unable to find property: '");
            message.append(key);
            message.append('\'');

            if (component != null) {
                message.append(" for component: ");
                message.append(component.getPageRelativePath());
                message.append(" [class=").append(component.getClass().getName()).append(']');
            }
            message.append(". Locale: ").append(locale).append(", style: ").append(style);

            throw new MissingResourceException(message.toString(),
                    (component != null ? component.getClass().getName() : ""), key);
        }

        return "[Warning: Property for '" + key + "' not found]";
    }

    /**
     * @see #getStringIgnoreSettings(String, Component, IModel, Locale, String, String)
     * 
     * @param key
     *            The key to obtain the resource for
     * @param component
     *            The component to get the resource for (optional)
     * @param model
     *            The model to use for substitutions in the strings (optional)
     * @param defaultValue
     *            The default value (optional)
     * @return The string resource
     */
    public String getStringIgnoreSettings(final String key, final Component component, final IModel<?> model,
            final String defaultValue) {
        return getStringIgnoreSettings(key, component, model, null, null, defaultValue);
    }

    /**
     * This is similar to {@link #getString(String, Component, IModel, String)} except that the
     * resource settings are ignored. This allows to to code something like
     * 
     * <pre>
     * String option = getLocalizer().getStringIgnoreSettings(getId() + &quot;.null&quot;, this, &quot;&quot;);
     * if (Strings.isEmpty(option))
     * {
     *    option = getLocalizer().getString(&quot;null&quot;, this, CHOOSE_ONE);
     * }
     * </pre>
     * 
     * @param key
     *            The key to obtain the resource for
     * @param component
     *            The component to get the resource for (optional)
     * @param model
     *            The model to use for substitutions in the strings (optional)
     * @param locale
     *            If != null, it'll supersede the component's locale
     * @param style
     *            If != null, it'll supersede the component's style
     * @param defaultValue
     *            The default value (optional)
     * @return The string resource
     */
    public String getStringIgnoreSettings(final String key, final Component component, final IModel<?> model,
            Locale locale, String style, final String defaultValue) {
        boolean addedToPage = false;
        if (component != null) {
            if ((component instanceof Page) || (null != component.findParent(Page.class))) {
                addedToPage = true;
            }

            if (!addedToPage && log.isWarnEnabled()) {
                log.warn(
                        "Tried to retrieve a localized string for a component that has not yet been added to the page. "
                                + "This can sometimes lead to an invalid or no localized resource returned. "
                                + "Make sure you are not calling Component#getString() inside your Component's constructor. "
                                + "Offending component: {}",
                        component);
            }
        }

        String cacheKey = null;
        String value;

        // Make sure locale, style and variation have the right values
        String variation = (component != null ? component.getVariation() : null);

        if ((locale == null) && (component != null)) {
            locale = component.getLocale();
        }
        if (locale == null) {
            locale = Session.exists() ? Session.get().getLocale() : Locale.getDefault();
        }

        if ((style == null) && (component != null)) {
            style = component.getStyle();
        }
        if (style == null) {
            style = Session.exists() ? Session.get().getStyle() : null;
        }

        // If this component is not yet added to page we do not want to check
        // cache as we can generate an invalid cache key
        if ((cache != null) && ((component == null) || addedToPage)) {
            cacheKey = getCacheKey(key, component, locale, style, variation);
        }

        // Value not found are cached as well (value = null)
        if ((cacheKey != null) && cache.containsKey(cacheKey)) {
            value = getFromCache(cacheKey);
            if (log.isDebugEnabled()) {
                log.debug("Property found in cache: '" + key + "'; Component: '"
                        + (component != null ? component.toString(false) : null) + "'; value: '" + value + '\'');
            }
        } else {
            if (log.isDebugEnabled()) {
                log.debug("Locate property: key: '" + key + "'; Component: '"
                        + (component != null ? component.toString(false) : null) + '\'');
            }

            // Iterate over all registered string resource loaders until the property has been found
            Iterator<IStringResourceLoader> iter = getStringResourceLoaders().iterator();
            value = null;
            while (iter.hasNext() && (value == null)) {
                IStringResourceLoader loader = iter.next();
                value = loader.loadStringResource(component, key, locale, style, variation);
            }

            // Cache the result incl null if not found
            if (cacheKey != null) {
                putIntoCache(cacheKey, value);
            }

            if ((value == null) && log.isDebugEnabled()) {
                log.debug("Property not found; key: '" + key + "'; Component: '"
                        + (component != null ? component.toString(false) : null) + '\'');
            }
        }

        if (value == null) {
            value = defaultValue;
        }

        // If a property value has been found, or a default value was given,
        // then replace the placeholder and we are done
        if (value != null) {
            return substitutePropertyExpressions(component, value, model);
        }

        return null;
    }

    /**
     * In case you want to provide your own list of string resource loaders
     * 
     * @return List of string resource loaders
     */
    protected List<IStringResourceLoader> getStringResourceLoaders() {
        return Application.get().getResourceSettings().getStringResourceLoaders();
    }

    /**
     * Put the value into the cache and associate it with the cache key
     * 
     * @param cacheKey
     * @param string
     */
    protected void putIntoCache(final String cacheKey, final String string) {
        if (cache == null) {
            return;
        }

        // ConcurrentHashMap does not allow null values
        if (string == null) {
            cache.put(cacheKey, NULL_VALUE);
        } else {
            cache.put(cacheKey, string);
        }
    }

    /**
     * Get the value associated with the key from the cache.
     * 
     * @param cacheKey
     * @return The value of the key
     */
    protected String getFromCache(final String cacheKey) {
        if (cache == null) {
            return null;
        }

        final String value = cache.get(cacheKey);

        // ConcurrentHashMap does not allow null values
        if (NULL_VALUE == value) {
            return null;
        }
        return value;
    }

    /**
     * Gets the cache key
     * 
     * @param key
     * @param component
     * @param locale
     *            Guaranteed to be != null
     * @param style
     * @param variation
     * @return The value of the key
     */
    protected String getCacheKey(final String key, final Component component, final Locale locale,
            final String style, final String variation) {
        if (component != null) {
            StringBuilder buffer = new StringBuilder(200);
            buffer.append(key);

            Component cursor = component;

            while (cursor != null) {
                buffer.append('-').append(metaDatabase.id(cursor.getClass()));

                if (cursor instanceof Page) {
                    break;
                }

                /*
                 * only append component id if component is not a loop item because (a) these ids
                 * are irrelevant when generating resource cache keys (b) they cause a lot of
                 * redundant keys to be generated
                 * 
                 * also if the cursor component is an auto component we append a constant string
                 * instead of component's id because auto components have a newly generated id on
                 * every render.
                 */
                final Component parent = cursor.getParent();
                final boolean skip = parent instanceof AbstractRepeater;

                if (skip == false) {
                    String cursorKey = cursor.isAuto() ? "wicket-auto" : cursor.getId();
                    buffer.append(':').append(cursorKey);
                }

                cursor = parent;
            }

            buffer.append('-').append(locale);
            buffer.append('-').append(style);
            buffer.append('-').append(variation);

            return buffer.toString();
        } else {
            // locale is guaranteed to be != null
            return key + '-' + locale.toString() + '-' + style;
        }
    }

    /**
        * Helper method to handle property variable substitution in strings.
        * 
        * @param component
        *            The component requesting a model value or {@code null]
        * @param string
        *            The string to substitute into
        * @param model
        *            The model
        * @return The resulting string
        */
    public String substitutePropertyExpressions(final Component component, final String string,
            final IModel<?> model) {
        if ((string != null) && (model != null)) {
            final IConverterLocator locator;
            final Locale locale;
            if (component == null) {
                locator = Application.get().getConverterLocator();

                if (Session.exists()) {
                    locale = Session.get().getLocale();
                } else {
                    locale = Locale.getDefault();
                }
            } else {
                locator = component;
                locale = component.getLocale();
            }

            return new ConvertingPropertyVariableInterpolator(string, model.getObject(), locator, locale)
                    .toString();
        }
        return string;
    }

    /**
     * By default the cache is enabled. Disabling the cache will disable it and clear the cache.
     * This can be handy for example in development mode.
     * 
     * @param value
     */
    public final void setEnableCache(boolean value) {
        if (value == false) {
            cache = null;
        } else if (cache == null) {
            cache = newCache();
        }
    }

    /**
     * Create a new cache, override this method if you want a different map to store the cache keys,
     * for example a map that hold only the last X number of elements..
     * 
     * By default it uses the {@link ConcurrentHashMap}
     * 
     * @return cache
     */
    protected Map<String, String> newCache() {
        return new ConcurrentHashMap<>();
    }

    /**
     * Database that maps class names to an integer id. This is used to make localizer keys shorter
     * because sometimes they can contain a large number of class names.
     * 
     * @author igor.vaynberg
     */
    private static class ClassMetaDatabase {
        private final ConcurrentMap<String, Long> nameToId = Generics.newConcurrentHashMap();
        private final AtomicLong nameCounter = new AtomicLong();

        /**
         * Returns a unique id that represents this class' name. This can be used for compressing
         * class names. Notice this id should not be used across cluster nodes.
         * 
         * @param clazz
         * @return long id of class name
         */
        public long id(Class<?> clazz) {
            final String name = clazz.getName();
            Long id = nameToId.get(name);
            if (id == null) {
                id = nameCounter.incrementAndGet();
                Long previousId = nameToId.putIfAbsent(name, id);
                if (previousId != null) {
                    id = previousId;
                }
            }
            return id;
        }
    }
}