001/* 002 * Copyright 2002-2020 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.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.List; 024 025import javax.servlet.jsp.JspException; 026import javax.servlet.jsp.JspTagException; 027 028import org.springframework.context.MessageSource; 029import org.springframework.context.MessageSourceResolvable; 030import org.springframework.context.NoSuchMessageException; 031import org.springframework.lang.Nullable; 032import org.springframework.util.ObjectUtils; 033import org.springframework.util.StringUtils; 034import org.springframework.web.util.JavaScriptUtils; 035import org.springframework.web.util.TagUtils; 036 037/** 038 * The {@code <message>} tag looks up a message in the scope of this page. 039 * Messages are resolved using the ApplicationContext and thus support 040 * internationalization. 041 * 042 * <p>Detects an HTML escaping setting, either on this tag instance, the page level, 043 * or the {@code web.xml} level. Can also apply JavaScript escaping. 044 * 045 * <p>If "code" isn't set or cannot be resolved, "text" will be used as default 046 * message. Thus, this tag can also be used for HTML escaping of any texts. 047 * 048 * <p>Message arguments can be specified via the {@link #setArguments(Object) arguments} 049 * attribute or by using nested {@code <spring:argument>} tags. 050 * 051 * <table> 052 * <caption>Attribute Summary</caption> 053 * <thead> 054 * <tr> 055 * <th>Attribute</th> 056 * <th>Required?</th> 057 * <th>Runtime Expression?</th> 058 * <th>Description</th> 059 * </tr> 060 * </thead> 061 * <tbody> 062 * <tr> 063 * <td>arguments</td> 064 * <td>false</td> 065 * <td>true</td> 066 * <td>Set optional message arguments for this tag, as a (comma-)delimited 067 * String (each String argument can contain JSP EL), an Object array (used as 068 * argument array), or a single Object (used as single argument).</td> 069 * </tr> 070 * <tr> 071 * <td>argumentSeparator</td> 072 * <td>false</td> 073 * <td>true</td> 074 * <td>The separator character to be used for splitting the arguments string 075 * value; defaults to a 'comma' (',').</td> 076 * </tr> 077 * <tr> 078 * <td>code</td> 079 * <td>false</td> 080 * <td>true</td> 081 * <td>The code (key) to use when looking up the message. 082 * If code is not provided, the text attribute will be used.</td> 083 * </tr> 084 * <tr> 085 * <td>htmlEscape</td> 086 * <td>false</td> 087 * <td>true</td> 088 * <td>Set HTML escaping for this tag, as boolean value. 089 * Overrides the default HTML escaping setting for the current page.</td> 090 * </tr> 091 * <tr> 092 * <td>javaScriptEscape</td> 093 * <td>false</td> 094 * <td>true</td> 095 * <td>Set JavaScript escaping for this tag, as boolean value. 096 * Default is false.</td> 097 * </tr> 098 * <tr> 099 * <td>message</td> 100 * <td>false</td> 101 * <td>true</td> 102 * <td>A MessageSourceResolvable argument (direct or through JSP EL). 103 * Fits nicely when used in conjunction with Spring鈥檚 own validation error 104 * classes which all implement the MessageSourceResolvable interface. 105 * For example, this allows you to iterate over all of the errors in a form, 106 * passing each error (using a runtime expression) as the value of this 107 * 'message' attribute, thus effecting the easy display of such error 108 * messages.</td> 109 * </tr> 110 * <tr> 111 * <td>scope</td> 112 * <td>false</td> 113 * <td>true</td> 114 * <td>The scope to use when exporting the result to a variable. This attribute 115 * is only used when var is also set. Possible values are page, request, session 116 * and application.</td> 117 * </tr> 118 * <tr> 119 * <td>text</td> 120 * <td>false</td> 121 * <td>true</td> 122 * <td>Default text to output when a message for the given code could not be 123 * found. If both text and code are not set, the tag will output null.</td> 124 * </tr> 125 * <tr> 126 * <td>var</td> 127 * <td>false</td> 128 * <td>true</td> 129 * <td>The string to use when binding the result to the page, request, session 130 * or application scope. If not specified, the result gets outputted to the writer 131 * (i.e. typically directly to the JSP).</td> 132 * </tr> 133 * </tbody> 134 * </table> 135 * 136 * @author Rod Johnson 137 * @author Juergen Hoeller 138 * @author Nicholas Williams 139 * @see #setCode 140 * @see #setText 141 * @see #setHtmlEscape 142 * @see #setJavaScriptEscape 143 * @see HtmlEscapeTag#setDefaultHtmlEscape 144 * @see org.springframework.web.util.WebUtils#HTML_ESCAPE_CONTEXT_PARAM 145 * @see ArgumentTag 146 */ 147@SuppressWarnings("serial") 148public class MessageTag extends HtmlEscapingAwareTag implements ArgumentAware { 149 150 /** 151 * Default separator for splitting an arguments String: a comma (","). 152 */ 153 public static final String DEFAULT_ARGUMENT_SEPARATOR = ","; 154 155 156 @Nullable 157 private MessageSourceResolvable message; 158 159 @Nullable 160 private String code; 161 162 @Nullable 163 private Object arguments; 164 165 private String argumentSeparator = DEFAULT_ARGUMENT_SEPARATOR; 166 167 private List<Object> nestedArguments = Collections.emptyList(); 168 169 @Nullable 170 private String text; 171 172 @Nullable 173 private String var; 174 175 private String scope = TagUtils.SCOPE_PAGE; 176 177 private boolean javaScriptEscape = false; 178 179 180 /** 181 * Set the MessageSourceResolvable for this tag. 182 * <p>If a MessageSourceResolvable is specified, it effectively overrides 183 * any code, arguments or text specified on this tag. 184 */ 185 public void setMessage(MessageSourceResolvable message) { 186 this.message = message; 187 } 188 189 /** 190 * Set the message code for this tag. 191 */ 192 public void setCode(String code) { 193 this.code = code; 194 } 195 196 /** 197 * Set optional message arguments for this tag, as a comma-delimited 198 * String (each String argument can contain JSP EL), an Object array 199 * (used as argument array), or a single Object (used as single argument). 200 */ 201 public void setArguments(Object arguments) { 202 this.arguments = arguments; 203 } 204 205 /** 206 * Set the separator to use for splitting an arguments String. 207 * Default is a comma (","). 208 * @see #setArguments 209 */ 210 public void setArgumentSeparator(String argumentSeparator) { 211 this.argumentSeparator = argumentSeparator; 212 } 213 214 @Override 215 public void addArgument(@Nullable Object argument) throws JspTagException { 216 this.nestedArguments.add(argument); 217 } 218 219 /** 220 * Set the message text for this tag. 221 */ 222 public void setText(String text) { 223 this.text = text; 224 } 225 226 /** 227 * Set PageContext attribute name under which to expose 228 * a variable that contains the resolved message. 229 * @see #setScope 230 * @see javax.servlet.jsp.PageContext#setAttribute 231 */ 232 public void setVar(String var) { 233 this.var = var; 234 } 235 236 /** 237 * Set the scope to export the variable to. 238 * Default is SCOPE_PAGE ("page"). 239 * @see #setVar 240 * @see org.springframework.web.util.TagUtils#SCOPE_PAGE 241 * @see javax.servlet.jsp.PageContext#setAttribute 242 */ 243 public void setScope(String scope) { 244 this.scope = scope; 245 } 246 247 /** 248 * Set JavaScript escaping for this tag, as boolean value. 249 * Default is "false". 250 */ 251 public void setJavaScriptEscape(boolean javaScriptEscape) throws JspException { 252 this.javaScriptEscape = javaScriptEscape; 253 } 254 255 256 @Override 257 protected final int doStartTagInternal() throws JspException, IOException { 258 this.nestedArguments = new ArrayList<>(); 259 return EVAL_BODY_INCLUDE; 260 } 261 262 /** 263 * Resolves the message, escapes it if demanded, 264 * and writes it to the page (or exposes it as variable). 265 * @see #resolveMessage() 266 * @see org.springframework.web.util.HtmlUtils#htmlEscape(String) 267 * @see org.springframework.web.util.JavaScriptUtils#javaScriptEscape(String) 268 * @see #writeMessage(String) 269 */ 270 @Override 271 public int doEndTag() throws JspException { 272 try { 273 // Resolve the unescaped message. 274 String msg = resolveMessage(); 275 276 // HTML and/or JavaScript escape, if demanded. 277 msg = htmlEscape(msg); 278 msg = this.javaScriptEscape ? JavaScriptUtils.javaScriptEscape(msg) : msg; 279 280 // Expose as variable, if demanded, else write to the page. 281 if (this.var != null) { 282 this.pageContext.setAttribute(this.var, msg, TagUtils.getScope(this.scope)); 283 } 284 else { 285 writeMessage(msg); 286 } 287 288 return EVAL_PAGE; 289 } 290 catch (IOException ex) { 291 throw new JspTagException(ex.getMessage(), ex); 292 } 293 catch (NoSuchMessageException ex) { 294 throw new JspTagException(getNoSuchMessageExceptionDescription(ex)); 295 } 296 } 297 298 @Override 299 public void release() { 300 super.release(); 301 this.arguments = null; 302 } 303 304 305 /** 306 * Resolve the specified message into a concrete message String. 307 * The returned message String should be unescaped. 308 */ 309 protected String resolveMessage() throws JspException, NoSuchMessageException { 310 MessageSource messageSource = getMessageSource(); 311 312 // Evaluate the specified MessageSourceResolvable, if any. 313 if (this.message != null) { 314 // We have a given MessageSourceResolvable. 315 return messageSource.getMessage(this.message, getRequestContext().getLocale()); 316 } 317 318 if (this.code != null || this.text != null) { 319 // We have a code or default text that we need to resolve. 320 Object[] argumentsArray = resolveArguments(this.arguments); 321 if (!this.nestedArguments.isEmpty()) { 322 argumentsArray = appendArguments(argumentsArray, this.nestedArguments.toArray()); 323 } 324 325 if (this.text != null) { 326 // We have a fallback text to consider. 327 String msg = messageSource.getMessage( 328 this.code, argumentsArray, this.text, getRequestContext().getLocale()); 329 return (msg != null ? msg : ""); 330 } 331 else { 332 // We have no fallback text to consider. 333 return messageSource.getMessage( 334 this.code, argumentsArray, getRequestContext().getLocale()); 335 } 336 } 337 338 throw new JspTagException("No resolvable message"); 339 } 340 341 private Object[] appendArguments(@Nullable Object[] sourceArguments, Object[] additionalArguments) { 342 if (ObjectUtils.isEmpty(sourceArguments)) { 343 return additionalArguments; 344 } 345 Object[] arguments = new Object[sourceArguments.length + additionalArguments.length]; 346 System.arraycopy(sourceArguments, 0, arguments, 0, sourceArguments.length); 347 System.arraycopy(additionalArguments, 0, arguments, sourceArguments.length, additionalArguments.length); 348 return arguments; 349 } 350 351 /** 352 * Resolve the given arguments Object into an arguments array. 353 * @param arguments the specified arguments Object 354 * @return the resolved arguments as array 355 * @throws JspException if argument conversion failed 356 * @see #setArguments 357 */ 358 @Nullable 359 protected Object[] resolveArguments(@Nullable Object arguments) throws JspException { 360 if (arguments instanceof String) { 361 return StringUtils.delimitedListToStringArray((String) arguments, this.argumentSeparator); 362 } 363 else if (arguments instanceof Object[]) { 364 return (Object[]) arguments; 365 } 366 else if (arguments instanceof Collection) { 367 return ((Collection<?>) arguments).toArray(); 368 } 369 else if (arguments != null) { 370 // Assume a single argument object. 371 return new Object[] {arguments}; 372 } 373 else { 374 return null; 375 } 376 } 377 378 /** 379 * Write the message to the page. 380 * <p>Can be overridden in subclasses, e.g. for testing purposes. 381 * @param msg the message to write 382 * @throws IOException if writing failed 383 */ 384 protected void writeMessage(String msg) throws IOException { 385 this.pageContext.getOut().write(msg); 386 } 387 388 /** 389 * Use the current RequestContext's application context as MessageSource. 390 */ 391 protected MessageSource getMessageSource() { 392 return getRequestContext().getMessageSource(); 393 } 394 395 /** 396 * Return default exception message. 397 */ 398 protected String getNoSuchMessageExceptionDescription(NoSuchMessageException ex) { 399 return ex.getMessage(); 400 } 401 402}