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.web.servlet.tags;
018
019import java.io.IOException;
020import java.util.Collection;
021import java.util.LinkedList;
022import java.util.List;
023import javax.servlet.jsp.JspException;
024import javax.servlet.jsp.JspTagException;
025
026import org.springframework.context.MessageSource;
027import org.springframework.context.MessageSourceResolvable;
028import org.springframework.context.NoSuchMessageException;
029import org.springframework.util.ObjectUtils;
030import org.springframework.util.StringUtils;
031import org.springframework.web.util.JavaScriptUtils;
032import org.springframework.web.util.TagUtils;
033
034/**
035 * Custom JSP tag to look up a message in the scope of this page. Messages are
036 * resolved using the ApplicationContext and thus support internationalization.
037 *
038 * <p>Detects an HTML escaping setting, either on this tag instance, the page level,
039 * or the {@code web.xml} level. Can also apply JavaScript escaping.
040 *
041 * <p>If "code" isn't set or cannot be resolved, "text" will be used as default
042 * message. Thus, this tag can also be used for HTML escaping of any texts.
043 *
044 * <p>Message arguments can be specified via the {@link #setArguments(Object) arguments}
045 * attribute or by using nested {@code <spring:argument>} tags.
046 *
047 * @author Rod Johnson
048 * @author Juergen Hoeller
049 * @author Nicholas Williams
050 * @see #setCode
051 * @see #setText
052 * @see #setHtmlEscape
053 * @see #setJavaScriptEscape
054 * @see HtmlEscapeTag#setDefaultHtmlEscape
055 * @see org.springframework.web.util.WebUtils#HTML_ESCAPE_CONTEXT_PARAM
056 * @see ArgumentTag
057 */
058@SuppressWarnings("serial")
059public class MessageTag extends HtmlEscapingAwareTag implements ArgumentAware {
060
061        /**
062         * Default separator for splitting an arguments String: a comma (",")
063         */
064        public static final String DEFAULT_ARGUMENT_SEPARATOR = ",";
065
066
067        private MessageSourceResolvable message;
068
069        private String code;
070
071        private Object arguments;
072
073        private String argumentSeparator = DEFAULT_ARGUMENT_SEPARATOR;
074
075        private List<Object> nestedArguments;
076
077        private String text;
078
079        private String var;
080
081        private String scope = TagUtils.SCOPE_PAGE;
082
083        private boolean javaScriptEscape = false;
084
085
086        /**
087         * Set the MessageSourceResolvable for this tag.
088         * <p>If a MessageSourceResolvable is specified, it effectively overrides
089         * any code, arguments or text specified on this tag.
090         */
091        public void setMessage(MessageSourceResolvable message) {
092                this.message = message;
093        }
094
095        /**
096         * Set the message code for this tag.
097         */
098        public void setCode(String code) {
099                this.code = code;
100        }
101
102        /**
103         * Set optional message arguments for this tag, as a comma-delimited
104         * String (each String argument can contain JSP EL), an Object array
105         * (used as argument array), or a single Object (used as single argument).
106         */
107        public void setArguments(Object arguments) {
108                this.arguments = arguments;
109        }
110
111        /**
112         * Set the separator to use for splitting an arguments String.
113         * Default is a comma (",").
114         * @see #setArguments
115         */
116        public void setArgumentSeparator(String argumentSeparator) {
117                this.argumentSeparator = argumentSeparator;
118        }
119
120        @Override
121        public void addArgument(Object argument) throws JspTagException {
122                this.nestedArguments.add(argument);
123        }
124
125        /**
126         * Set the message text for this tag.
127         */
128        public void setText(String text) {
129                this.text = text;
130        }
131
132        /**
133         * Set PageContext attribute name under which to expose
134         * a variable that contains the resolved message.
135         * @see #setScope
136         * @see javax.servlet.jsp.PageContext#setAttribute
137         */
138        public void setVar(String var) {
139                this.var = var;
140        }
141
142        /**
143         * Set the scope to export the variable to.
144         * Default is SCOPE_PAGE ("page").
145         * @see #setVar
146         * @see org.springframework.web.util.TagUtils#SCOPE_PAGE
147         * @see javax.servlet.jsp.PageContext#setAttribute
148         */
149        public void setScope(String scope) {
150                this.scope = scope;
151        }
152
153        /**
154         * Set JavaScript escaping for this tag, as boolean value.
155         * Default is "false".
156         */
157        public void setJavaScriptEscape(boolean javaScriptEscape) throws JspException {
158                this.javaScriptEscape = javaScriptEscape;
159        }
160
161
162        @Override
163        protected final int doStartTagInternal() throws JspException, IOException {
164                this.nestedArguments = new LinkedList<Object>();
165                return EVAL_BODY_INCLUDE;
166        }
167
168        /**
169         * Resolves the message, escapes it if demanded,
170         * and writes it to the page (or exposes it as variable).
171         * @see #resolveMessage()
172         * @see org.springframework.web.util.HtmlUtils#htmlEscape(String)
173         * @see org.springframework.web.util.JavaScriptUtils#javaScriptEscape(String)
174         * @see #writeMessage(String)
175         */
176        @Override
177        public int doEndTag() throws JspException {
178                try {
179                        // Resolve the unescaped message.
180                        String msg = resolveMessage();
181
182                        // HTML and/or JavaScript escape, if demanded.
183                        msg = htmlEscape(msg);
184                        msg = this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(msg) : msg;
185
186                        // Expose as variable, if demanded, else write to the page.
187                        if (this.var != null) {
188                                pageContext.setAttribute(this.var, msg, TagUtils.getScope(this.scope));
189                        }
190                        else {
191                                writeMessage(msg);
192                        }
193
194                        return EVAL_PAGE;
195                }
196                catch (IOException ex) {
197                        throw new JspTagException(ex.getMessage(), ex);
198                }
199                catch (NoSuchMessageException ex) {
200                        throw new JspTagException(getNoSuchMessageExceptionDescription(ex));
201                }
202        }
203
204        @Override
205        public void release() {
206                super.release();
207                this.arguments = null;
208        }
209
210        /**
211         * Resolve the specified message into a concrete message String.
212         * The returned message String should be unescaped.
213         */
214        protected String resolveMessage() throws JspException, NoSuchMessageException {
215                MessageSource messageSource = getMessageSource();
216                if (messageSource == null) {
217                        throw new JspTagException("No corresponding MessageSource found");
218                }
219
220                // Evaluate the specified MessageSourceResolvable, if any.
221                if (this.message != null) {
222                        // We have a given MessageSourceResolvable.
223                        return messageSource.getMessage(this.message, getRequestContext().getLocale());
224                }
225
226                if (this.code != null || this.text != null) {
227                        // We have a code or default text that we need to resolve.
228                        Object[] argumentsArray = resolveArguments(this.arguments);
229                        if (!this.nestedArguments.isEmpty()) {
230                                argumentsArray = appendArguments(argumentsArray, this.nestedArguments.toArray());
231                        }
232
233                        if (this.text != null) {
234                                // We have a fallback text to consider.
235                                return messageSource.getMessage(
236                                                this.code, argumentsArray, this.text, getRequestContext().getLocale());
237                        }
238                        else {
239                                // We have no fallback text to consider.
240                                return messageSource.getMessage(
241                                                this.code, argumentsArray, getRequestContext().getLocale());
242                        }
243                }
244
245                // All we have is a specified literal text.
246                return this.text;
247        }
248
249        private Object[] appendArguments(Object[] sourceArguments, Object[] additionalArguments) {
250                if (ObjectUtils.isEmpty(sourceArguments)) {
251                        return additionalArguments;
252                }
253                Object[] arguments = new Object[sourceArguments.length + additionalArguments.length];
254                System.arraycopy(sourceArguments, 0, arguments, 0, sourceArguments.length);
255                System.arraycopy(additionalArguments, 0, arguments, sourceArguments.length, additionalArguments.length);
256                return arguments;
257        }
258
259        /**
260         * Resolve the given arguments Object into an arguments array.
261         * @param arguments the specified arguments Object
262         * @return the resolved arguments as array
263         * @throws JspException if argument conversion failed
264         * @see #setArguments
265         */
266        protected Object[] resolveArguments(Object arguments) throws JspException {
267                if (arguments instanceof String) {
268                        String[] stringArray =
269                                        StringUtils.delimitedListToStringArray((String) arguments, this.argumentSeparator);
270                        if (stringArray.length == 1) {
271                                Object argument = stringArray[0];
272                                if (argument != null && argument.getClass().isArray()) {
273                                        return ObjectUtils.toObjectArray(argument);
274                                }
275                                else {
276                                        return new Object[] {argument};
277                                }
278                        }
279                        else {
280                                return stringArray;
281                        }
282                }
283                else if (arguments instanceof Object[]) {
284                        return (Object[]) arguments;
285                }
286                else if (arguments instanceof Collection) {
287                        return ((Collection<?>) arguments).toArray();
288                }
289                else if (arguments != null) {
290                        // Assume a single argument object.
291                        return new Object[] {arguments};
292                }
293                else {
294                        return null;
295                }
296        }
297
298        /**
299         * Write the message to the page.
300         * <p>Can be overridden in subclasses, e.g. for testing purposes.
301         * @param msg the message to write
302         * @throws IOException if writing failed
303         */
304        protected void writeMessage(String msg) throws IOException {
305                pageContext.getOut().write(String.valueOf(msg));
306        }
307
308        /**
309         * Use the current RequestContext's application context as MessageSource.
310         */
311        protected MessageSource getMessageSource() {
312                return getRequestContext().getMessageSource();
313        }
314
315        /**
316         * Return default exception message.
317         */
318        protected String getNoSuchMessageExceptionDescription(NoSuchMessageException ex) {
319                return ex.getMessage();
320        }
321
322}