/* * ============================================================================= * * Copyright (c) 2011-2016, 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; /** * <p> * Standard implementation of {@link IMessageResolver}. * </p> * <p> * 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). * </p> * <p> * <strong>Step 1: Template-based message resolution</strong> * </p> * <p> * 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. * </p> * <p> * Note the order specified above allows container templates to override default message values specified * at children (inserted fragment) templates. * </p> * <p> * For each of these templates, several <tt>.properties</tt> files will be examined. For example, * a message in template <tt>/WEB-INF/templates/home.html</tt> for locale * <tt>gl_ES-gheada</tt> ("gl" = language, "ES" = country, "gheada" = variant) would be looked for * in <tt>.properties</tt> files in the following sequence: * </p> * <ul> * <li><tt>/WEB-INF/templates/home_gl_ES-gheada.properties</tt></li> * <li><tt>/WEB-INF/templates/home_gl_ES.properties</tt></li> * <li><tt>/WEB-INF/templates/home_gl.properties</tt></li> * <li><tt>/WEB-INF/templates/home.properties</tt></li> * </ul> * <p> * Note the resolution mechanism used for accessing these template-based <tt>.properties</tt> 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. * </p> * <p> * <strong>Step 2: Origin-based message resolution</strong> * </p> * <p> * If no suitable message value is found during template-based resolution, origin-based resolution * is performed. This allows the resolution of messages from <tt>.properties</tt> files living * in the classpath (and only in the classpath) in files corresponding with the names of the * classes being used as origin. * </p> * <p> * For example, a processor <tt>my.company.processor.SomeDataProcessor</tt> using its own class * as <em>origin</em> will be able to resolve messages from a * <tt>my/company/processor/SomeDataProcessor_gl_ES.properties</tt> file in the classpath. * </p> * <p> * Also, if a message is not found there, resolution will be tried for each of the superclasses this * <tt>my.company.processor.SomeDataProcessor</tt> class extends, until a suitable message is found, or * no more superclasses (except <tt>java.lang.Object</tt> exist). * </p> * <p> * <strong>Step 3: Defaults-based message resolution</strong> * </p> * <p> * If both template-based and origin-based message resolution fail, resolution will be tried using * the <em>default messages</em> specified via this class's {@link #setDefaultMessages(Properties)} or * {@link #addDefaultMessage(String, String)} methods. * </p> * <p> * Defaults-based message resolution is not locale-dependent. * </p> * <p> * <strong>Absent message specification</strong> * </p> * <p> * 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 <em>absent message representation</em> or not. * This is precisely what the <tt>useAbsentMessageRepresentation</tt> flag does in * {@link ITemplateContext#getMessage(Class, String, Object[], boolean)}. * </p> * <p> * An absent message representation looks like <tt>??mymessage_gl_ES??</tt> and is useful to quickly determine * when a message is lacking from the application's configuration. Note <tt>#{...}</tt> message expressions will * always ask for an <tt>absent message representation</tt>, whereas methods in the <tt>#messages</tt> * expression object will do it depending on the specific method being called. * </p> * <p> * <strong>Message caching</strong> * </p> * <p> * This implementation will cache template-based messages for those templates that are resolved (by their * corresponding {@link org.thymeleaf.templateresolver.ITemplateResolver}) as <em>cacheable</em>. Non-cacheable * templates will not have their messages cached. * </p> * <p> * Origin-based messages will be always cached. * </p> * <p> * <strong>Extensibility</strong> * </p> * <p> * This implementation is designed for allowing the following extension points: * </p> * <ul> * <li>{@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.</li> * <li>{@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.</li> * <li>{@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).</li> * <li>{@link #createAbsentMessageRepresentation(ITemplateContext, Class, String, Object[])}: the * mechanism for creating <em>absent message</em> representations, which can be customized if needed.</li> * </ul> * * @author Daniel Fernández * * @since 3.0.0 * */ public class StandardMessageResolver extends AbstractMessageResolver { private final ConcurrentHashMap<String,ConcurrentHashMap<Locale,Map<String,String>>> messagesByLocaleByTemplate = new ConcurrentHashMap<String,ConcurrentHashMap<Locale,Map<String,String>>>(20, 0.9f, 2); private final ConcurrentHashMap<Class<?>,ConcurrentHashMap<Locale,Map<String,String>>> messagesByLocaleByOrigin = new ConcurrentHashMap<Class<?>,ConcurrentHashMap<Locale,Map<String,String>>>(20, 0.9f, 2); private final Properties defaultMessages; public StandardMessageResolver() { super(); this.defaultMessages = new Properties(); } /** * <p> * Returns the default messages. These messages will be used * if no other messages can be found. * </p> * * @return the default messages */ public final Properties getDefaultMessages() { return this.defaultMessages; } /** * <p> * Sets the default messages. These messages will be used * if no other messages can be found. * </p> * * @param defaultMessages the new default messages */ public final void setDefaultMessages(final Properties defaultMessages) { if (defaultMessages != null) { this.defaultMessages.putAll(defaultMessages); } } /** * <p> * Adds a new message to the set of default messages. * </p> * * @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); } /** * <p> * Clears the set of default messages. * </p> */ 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<String, String> messagesForLocaleForTemplate; // We will ONLY cache messages for cacheable templates. This should adequately control cache growth if (templateCacheable) { ConcurrentHashMap<Locale, Map<String, String>> messagesByLocaleForTemplate = this.messagesByLocaleByTemplate.get(template); if (messagesByLocaleForTemplate == null) { this.messagesByLocaleByTemplate.putIfAbsent(template, new ConcurrentHashMap<Locale, Map<String, String>>(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<Locale, Map<String, String>> messagesByLocaleForOrigin = this.messagesByLocaleByOrigin.get(origin); if (messagesByLocaleForOrigin == null) { this.messagesByLocaleByOrigin.putIfAbsent(origin, new ConcurrentHashMap<Locale, Map<String, String>>(4)); messagesByLocaleForOrigin = this.messagesByLocaleByOrigin.get(origin); } Map<String, String> 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; } /** * <p> * Resolve messages for a specific template and locale. * </p> * <p> * 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. * </p> * <p> * The standard mechanism will look for <tt>.properties</tt> files at the same location as * the template (using the same resource resolution mechanism), and with the same name base. * </p> * * @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<String,String> resolveMessagesForTemplate( final String template, final ITemplateResource templateResource, final Locale locale) { return StandardMessageResolutionUtils.resolveMessagesForTemplate(templateResource, locale); } /** * <p> * Resolve messages for a specific origin and locale. * </p> * <p> * 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. * </p> * <p> * 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 <tt>.properties</tt> * extension. * </p> * * @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<String,String> resolveMessagesForOrigin(final Class<?> origin, final Locale locale) { return StandardMessageResolutionUtils.resolveMessagesForOrigin(origin, locale); } /** * <p> * Format a message, merging it with its parameters, before returning. * </p> * <p> * This is meant to be overridden by subclasses if necessary. The default mechanism will simply * use a standard {@link java.text.MessageFormat} instance. * </p> * * @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+"_" + "??"; } }