org.thymeleaf.messageresolver.StandardMessageResolver Maven / Gradle / Ivy
Show all versions of thymeleaf Show documentation
/*
* =============================================================================
*
* Copyright (c) 2011-2018, The THYMELEAF team (http://www.thymeleaf.org)
*
* Licensed 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.thymeleaf.messageresolver;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.engine.TemplateData;
import org.thymeleaf.templateresource.ITemplateResource;
import org.thymeleaf.util.Validate;
/**
*
* Standard implementation of {@link IMessageResolver}.
*
*
* This class will first try to perform message resolution based on the template context, then
* on the origin, and finally on the specified default messages (if any).
*
*
* Step 1: Template-based message resolution
*
*
* For template-based resolution, not only the template being executed will be examined, but also
* templates corresponding to fragments being inserted, so that if template A inserts a fragment from
* B, and that fragment from B inserts a fragment from C, the requested message will be searched
* in this order: A, B, C.
*
*
* Note the order specified above allows container templates to override default message values specified
* at children (inserted fragment) templates.
*
*
* For each of these templates, several {@code .properties} files will be examined. For example,
* a message in template {@code /WEB-INF/templates/home.html} for locale
* {@code gl_ES-gheada} ("gl" = language, "ES" = country, "gheada" = variant) would be looked for
* in {@code .properties} files in the following sequence:
*
*
* - {@code /WEB-INF/templates/home_gl_ES-gheada.properties}
* - {@code /WEB-INF/templates/home_gl_ES.properties}
* - {@code /WEB-INF/templates/home_gl.properties}
* - {@code /WEB-INF/templates/home.properties}
*
*
* Note the resolution mechanism used for accessing these template-based {@code .properties} files will
* be the same used for resolving the templates themselves. So for templates resolved from the ServletContext
* its messages files will be searched at the ServletContext, for templates resolved from URL the corresponding
* derived URLs will be called, etc.
*
*
* Step 2: Origin-based message resolution
*
*
* If no suitable message value is found during template-based resolution, origin-based resolution
* is performed. This allows the resolution of messages from {@code .properties} files living
* in the classpath (and only in the classpath) in files corresponding with the names of the
* classes being used as origin.
*
*
* For example, a processor {@code my.company.processor.SomeDataProcessor} using its own class
* as origin will be able to resolve messages from a
* {@code my/company/processor/SomeDataProcessor_gl_ES.properties} file in the classpath.
*
*
* Also, if a message is not found there, resolution will be tried for each of the superclasses this
* {@code my.company.processor.SomeDataProcessor} class extends, until a suitable message is found, or
* no more superclasses (except {@code java.lang.Object} exist).
*
*
* Step 3: Defaults-based message resolution
*
*
* If both template-based and origin-based message resolution fail, resolution will be tried using
* the default messages specified via this class's {@link #setDefaultMessages(Properties)} or
* {@link #addDefaultMessage(String, String)} methods.
*
*
* Defaults-based message resolution is not locale-dependent.
*
*
* Absent message specification
*
*
* Message resolution will return null if no message is found, in which case callers will have the possibility
* to choose between asking the resolver to create an absent message representation or not.
* This is precisely what the {@code useAbsentMessageRepresentation} flag does in
* {@link ITemplateContext#getMessage(Class, String, Object[], boolean)}.
*
*
* An absent message representation looks like {@code ??mymessage_gl_ES??} and is useful to quickly determine
* when a message is lacking from the application's configuration. Note {@code #{...}} message expressions will
* always ask for an {@code absent message representation}, whereas methods in the {@code #messages}
* expression object will do it depending on the specific method being called.
*
*
* Message caching
*
*
* This implementation will cache template-based messages for those templates that are resolved (by their
* corresponding {@link org.thymeleaf.templateresolver.ITemplateResolver}) as cacheable. Non-cacheable
* templates will not have their messages cached.
*
*
* Origin-based messages will be always cached.
*
*
* Extensibility
*
*
* This implementation is designed for allowing the following extension points:
*
*
* - {@link #resolveMessagesForTemplate(String, ITemplateResource, Locale)}: the mechanism for resolving
* the messages for a specific uncached template. Might be called several times, one per nested template.
* - {@link #resolveMessagesForOrigin(Class, Locale)}: the mechanism for resolving the messages for a specific
* unchecked origin class. Might be called several times, one per class/superclass.
* - {@link #formatMessage(Locale, String, Object[])}: the way resolved messages are actually formated along
* with their parameters (by default a {@link java.text.MessageFormat} is used).
* - {@link #createAbsentMessageRepresentation(ITemplateContext, Class, String, Object[])}: the
* mechanism for creating absent message representations, which can be customized if needed.
*
*
* @author Daniel Fernández
*
* @since 3.0.0
*
*/
public class StandardMessageResolver extends AbstractMessageResolver {
private final ConcurrentHashMap>> messagesByLocaleByTemplate =
new ConcurrentHashMap>>(20, 0.9f, 2);
private final ConcurrentHashMap,ConcurrentHashMap>> messagesByLocaleByOrigin =
new ConcurrentHashMap,ConcurrentHashMap>>(20, 0.9f, 2);
private final Properties defaultMessages;
public StandardMessageResolver() {
super();
this.defaultMessages = new Properties();
}
/**
*
* Returns the default messages. These messages will be used
* if no other messages can be found.
*
*
* @return the default messages
*/
public final Properties getDefaultMessages() {
return this.defaultMessages;
}
/**
*
* Sets the default messages. These messages will be used
* if no other messages can be found.
*
*
* @param defaultMessages the new default messages
*/
public final void setDefaultMessages(final Properties defaultMessages) {
if (defaultMessages != null) {
this.defaultMessages.putAll(defaultMessages);
}
}
/**
*
* Adds a new message to the set of default messages.
*
*
* @param key the message key
* @param value the message value (text)
*/
public final void addDefaultMessage(final String key, final String value) {
Validate.notNull(key, "Key for default message cannot be null");
Validate.notNull(value, "Value for default message cannot be null");
this.defaultMessages.put(key, value);
}
/**
*
* Clears the set of default messages.
*
*/
public final void clearDefaultMessages() {
this.defaultMessages.clear();
}
public final String resolveMessage(
final ITemplateContext context, final Class> origin, final String key, final Object[] messageParameters) {
return resolveMessage(context, origin, key, messageParameters, true, true, true);
}
public final String resolveMessage(
final ITemplateContext context, final Class> origin, final String key, final Object[] messageParameters,
final boolean performTemplateBasedResolution, final boolean performOriginBasedResolution,
final boolean performDefaultBasedResolution) {
Validate.notNull(context, "Context cannot be null");
Validate.notNull(context.getLocale(), "Locale in context cannot be null");
Validate.notNull(key, "Message key cannot be null");
final Locale locale = context.getLocale();
/*
* FIRST STEP: Look for the message using template-based resolution
*
* Note that resolution is top-down, this is, starts at the first-level template (the one being executed)
* and only if a key is not found will try resolving for nested templates in the order they have been nested.
*
* This allows container templates to override the messages defined in fragments, which will act as defaults.
*/
if (performTemplateBasedResolution) {
for (final TemplateData templateData : context.getTemplateStack()) {
final String template = templateData.getTemplate();
final ITemplateResource templateResource = templateData.getTemplateResource();
final boolean templateCacheable = templateData.getValidity().isCacheable();
Map messagesForLocaleForTemplate;
// We will ONLY cache messages for cacheable templates. This should adequately control cache growth
if (templateCacheable) {
ConcurrentHashMap> messagesByLocaleForTemplate = this.messagesByLocaleByTemplate.get(template);
if (messagesByLocaleForTemplate == null) {
this.messagesByLocaleByTemplate.putIfAbsent(template, new ConcurrentHashMap>(4));
messagesByLocaleForTemplate = this.messagesByLocaleByTemplate.get(template);
}
messagesForLocaleForTemplate = messagesByLocaleForTemplate.get(locale);
if (messagesForLocaleForTemplate == null) {
messagesForLocaleForTemplate = resolveMessagesForTemplate(template, templateResource, locale);
if (messagesForLocaleForTemplate == null) {
messagesForLocaleForTemplate = Collections.emptyMap();
}
messagesByLocaleForTemplate.putIfAbsent(locale, messagesForLocaleForTemplate);
// We retrieve it again in order to be sure its the stored map (because of the 'putIfAbsent')
messagesForLocaleForTemplate = messagesByLocaleForTemplate.get(locale);
}
} else {
messagesForLocaleForTemplate = resolveMessagesForTemplate(template, templateResource, locale);
if (messagesForLocaleForTemplate == null) {
messagesForLocaleForTemplate = Collections.emptyMap();
}
}
// Once the messages map has been retrieved, just use it
final String message = messagesForLocaleForTemplate.get(key);
if (message != null) {
return formatMessage(locale, message, messageParameters);
}
// Will try the next resolver (if any)
}
}
/*
* SECOND STEP: Look for the message using origin-based resolution
*/
if (performOriginBasedResolution && origin != null) {
ConcurrentHashMap> messagesByLocaleForOrigin = this.messagesByLocaleByOrigin.get(origin);
if (messagesByLocaleForOrigin == null) {
this.messagesByLocaleByOrigin.putIfAbsent(origin, new ConcurrentHashMap>(4));
messagesByLocaleForOrigin = this.messagesByLocaleByOrigin.get(origin);
}
Map messagesForLocaleForOrigin = messagesByLocaleForOrigin.get(locale);
if (messagesForLocaleForOrigin == null) {
messagesForLocaleForOrigin = resolveMessagesForOrigin(origin, locale);
if (messagesForLocaleForOrigin == null) {
messagesForLocaleForOrigin = Collections.emptyMap();
}
messagesByLocaleForOrigin.putIfAbsent(locale, messagesForLocaleForOrigin);
// We retrieve it again in order to be sure its the stored map (because of the 'putIfAbsent')
messagesForLocaleForOrigin = messagesByLocaleForOrigin.get(locale);
}
// Once the messages map has been retrieved, just use it
final String message = messagesForLocaleForOrigin.get(key);
if (message != null) {
return formatMessage(locale, message, messageParameters);
}
}
/*
* THIRD STEP: Try default messages.
*/
if (performDefaultBasedResolution && this.defaultMessages != null) {
final String message = this.defaultMessages.getProperty(key);
if (message != null) {
return formatMessage(locale, message, messageParameters);
}
}
/*
* NOT FOUND, return null
*/
return null;
}
/**
*
* Resolve messages for a specific template and locale.
*
*
* This is meant to be overridden by subclasses if necessary, so that the way in which messages
* are obtained for a specific template can be modified without changing the rest of the
* message resolution mechanisms.
*
*
* The standard mechanism will look for {@code .properties} files at the same location as
* the template (using the same resource resolution mechanism), and with the same name base.
*
*
* @param template the template
* @param templateResource the template resource
* @param locale the locale
* @return a Map containing all the possible messages for the specified template and locale. Can return null.
*/
protected Map resolveMessagesForTemplate(
final String template, final ITemplateResource templateResource, final Locale locale) {
return StandardMessageResolutionUtils.resolveMessagesForTemplate(templateResource, locale);
}
/**
*
* Resolve messages for a specific origin and locale.
*
*
* This is meant to be overridden by subclasses if necessary, so that the way in which messages
* are obtained for a specific origin can be modified without changing the rest of the
* message resolution mechanisms.
*
*
* The standard mechanism will look for files in the classpath (only classpath),
* at the same package and with the same name as the origin class, with {@code .properties}
* extension.
*
*
* @param origin the origin
* @param locale the locale
* @return a Map containing all the possible messages for the specified origin and locale. Can return null.
*/
protected Map resolveMessagesForOrigin(final Class> origin, final Locale locale) {
return StandardMessageResolutionUtils.resolveMessagesForOrigin(origin, locale);
}
/**
*
* Format a message, merging it with its parameters, before returning.
*
*
* This is meant to be overridden by subclasses if necessary. The default mechanism will simply
* use a standard {@link java.text.MessageFormat} instance.
*
*
* @param locale the locale
* @param message the resolved message
* @param messageParameters the message parameters (might be null)
* @return the formatted message
*/
protected String formatMessage(
final Locale locale, final String message, final Object[] messageParameters) {
return StandardMessageResolutionUtils.formatMessage(locale, message, messageParameters);
}
public String createAbsentMessageRepresentation(
final ITemplateContext context, final Class> origin, final String key, final Object[] messageParameters) {
Validate.notNull(key, "Message key cannot be null");
if (context.getLocale() != null) {
return "??"+key+"_" + context.getLocale().toString() + "??";
}
return "??"+key+"_" + "??";
}
}