001/*
002 * Copyright 2002-2018 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.lang.Nullable;
028import org.springframework.util.ObjectUtils;
029
030/**
031 * Base class for message source implementations, providing support infrastructure
032 * such as {@link java.text.MessageFormat} handling but not implementing concrete
033 * methods defined in the {@link org.springframework.context.MessageSource}.
034 *
035 * <p>{@link AbstractMessageSource} derives from this class, providing concrete
036 * {@code getMessage} implementations that delegate to a central template
037 * method for message code resolution.
038 *
039 * @author Juergen Hoeller
040 * @since 2.5.5
041 */
042public abstract class MessageSourceSupport {
043
044        private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");
045
046        /** Logger available to subclasses. */
047        protected final Log logger = LogFactory.getLog(getClass());
048
049        private boolean alwaysUseMessageFormat = false;
050
051        /**
052         * Cache to hold already generated MessageFormats per message.
053         * Used for passed-in default messages. MessageFormats for resolved
054         * codes are cached on a specific basis in subclasses.
055         */
056        private final Map<String, Map<Locale, MessageFormat>> messageFormatsPerMessage = new HashMap<>();
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, @Nullable 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, @Nullable Object[] args, Locale locale) {
115                if (!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<>();
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, 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(@Nullable Object[] args, Locale locale) {
171                return (args != null ? args : new Object[0]);
172        }
173
174}