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}