org.apache.wicket.Localizer Maven / Gradle / Ivy
Show all versions of wicket-core Show documentation
/*
* 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 Application
object.
*
* 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 = "";
/** Cache properties */
private Map 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 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 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
*
*
* String option = getLocalizer().getStringIgnoreSettings(getId() + ".null", this, "");
* if (Strings.isEmpty(option))
* {
* option = getLocalizer().getString("null", this, CHOOSE_ONE);
* }
*
*
* @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: {} - Resource key: {}", component, key);
}
}
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 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 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 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 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;
}
}
}