001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.context.support;
018
019import java.text.MessageFormat;
020import java.util.HashMap;
021import java.util.Locale;
022import java.util.Map;
023
024import org.apache.commons.logging.Log;
025import org.apache.commons.logging.LogFactory;
026
027import org.springframework.util.ObjectUtils;
028
029/**
030 * Base class for message source implementations, providing support infrastructure
031 * such as {@link java.text.MessageFormat} handling but not implementing concrete
032 * methods defined in the {@link org.springframework.context.MessageSource}.
033 *
034 * <p>{@link AbstractMessageSource} derives from this class, providing concrete
035 * {@code getMessage} implementations that delegate to a central template
036 * method for message code resolution.
037 *
038 * @author Juergen Hoeller
039 * @since 2.5.5
040 */
041public abstract class MessageSourceSupport {
042
043        private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");
044
045        /** Logger available to subclasses */
046        protected final Log logger = LogFactory.getLog(getClass());
047
048        private boolean alwaysUseMessageFormat = false;
049
050        /**
051         * Cache to hold already generated MessageFormats per message.
052         * Used for passed-in default messages. MessageFormats for resolved
053         * codes are cached on a specific basis in subclasses.
054         */
055        private final Map<String, Map<Locale, MessageFormat>> messageFormatsPerMessage =
056                        new HashMap<String, Map<Locale, MessageFormat>>();
057
058
059        /**
060         * Set whether to always apply the {@code MessageFormat} rules,
061         * parsing even messages without arguments.
062         * <p>Default is "false": Messages without arguments are by default
063         * returned as-is, without parsing them through MessageFormat.
064         * Set this to "true" to enforce MessageFormat for all messages,
065         * expecting all message texts to be written with MessageFormat escaping.
066         * <p>For example, MessageFormat expects a single quote to be escaped
067         * as "''". If your message texts are all written with such escaping,
068         * even when not defining argument placeholders, you need to set this
069         * flag to "true". Else, only message texts with actual arguments
070         * are supposed to be written with MessageFormat escaping.
071         * @see java.text.MessageFormat
072         */
073        public void setAlwaysUseMessageFormat(boolean alwaysUseMessageFormat) {
074                this.alwaysUseMessageFormat = alwaysUseMessageFormat;
075        }
076
077        /**
078         * Return whether to always apply the MessageFormat rules, parsing even
079         * messages without arguments.
080         */
081        protected boolean isAlwaysUseMessageFormat() {
082                return this.alwaysUseMessageFormat;
083        }
084
085
086        /**
087         * Render the given default message String. The default message is
088         * passed in as specified by the caller and can be rendered into
089         * a fully formatted default message shown to the user.
090         * <p>The default implementation passes the String to {@code formatMessage},
091         * resolving any argument placeholders found in them. Subclasses may override
092         * this method to plug in custom processing of default messages.
093         * @param defaultMessage the passed-in default message String
094         * @param args array of arguments that will be filled in for params within
095         * the message, or {@code null} if none.
096         * @param locale the Locale used for formatting
097         * @return the rendered default message (with resolved arguments)
098         * @see #formatMessage(String, Object[], java.util.Locale)
099         */
100        protected String renderDefaultMessage(String defaultMessage, Object[] args, Locale locale) {
101                return formatMessage(defaultMessage, args, locale);
102        }
103
104        /**
105         * Format the given message String, using cached MessageFormats.
106         * By default invoked for passed-in default messages, to resolve
107         * any argument placeholders found in them.
108         * @param msg the message to format
109         * @param args array of arguments that will be filled in for params within
110         * the message, or {@code null} if none
111         * @param locale the Locale used for formatting
112         * @return the formatted message (with resolved arguments)
113         */
114        protected String formatMessage(String msg, Object[] args, Locale locale) {
115                if (msg == null || (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args))) {
116                        return msg;
117                }
118                MessageFormat messageFormat = null;
119                synchronized (this.messageFormatsPerMessage) {
120                        Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage.get(msg);
121                        if (messageFormatsPerLocale != null) {
122                                messageFormat = messageFormatsPerLocale.get(locale);
123                        }
124                        else {
125                                messageFormatsPerLocale = new HashMap<Locale, MessageFormat>();
126                                this.messageFormatsPerMessage.put(msg, messageFormatsPerLocale);
127                        }
128                        if (messageFormat == null) {
129                                try {
130                                        messageFormat = createMessageFormat(msg, locale);
131                                }
132                                catch (IllegalArgumentException ex) {
133                                        // Invalid message format - probably not intended for formatting,
134                                        // rather using a message structure with no arguments involved...
135                                        if (isAlwaysUseMessageFormat()) {
136                                                throw ex;
137                                        }
138                                        // Silently proceed with raw message if format not enforced...
139                                        messageFormat = INVALID_MESSAGE_FORMAT;
140                                }
141                                messageFormatsPerLocale.put(locale, messageFormat);
142                        }
143                }
144                if (messageFormat == INVALID_MESSAGE_FORMAT) {
145                        return msg;
146                }
147                synchronized (messageFormat) {
148                        return messageFormat.format(resolveArguments(args, locale));
149                }
150        }
151
152        /**
153         * Create a MessageFormat for the given message and Locale.
154         * @param msg the message to create a MessageFormat for
155         * @param locale the Locale to create a MessageFormat for
156         * @return the MessageFormat instance
157         */
158        protected MessageFormat createMessageFormat(String msg, Locale locale) {
159                return new MessageFormat((msg != null ? msg : ""), locale);
160        }
161
162        /**
163         * Template method for resolving argument objects.
164         * <p>The default implementation simply returns the given argument array as-is.
165         * Can be overridden in subclasses in order to resolve special argument types.
166         * @param args the original argument array
167         * @param locale the Locale to resolve against
168         * @return the resolved argument array
169         */
170        protected Object[] resolveArguments(Object[] args, Locale locale) {
171                return args;
172        }
173
174}