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.web.servlet.tags.form; 018 019import java.io.UnsupportedEncodingException; 020import java.util.Map; 021import javax.servlet.ServletRequest; 022import javax.servlet.ServletResponse; 023import javax.servlet.http.HttpServletRequest; 024import javax.servlet.http.HttpServletResponse; 025import javax.servlet.jsp.JspException; 026import javax.servlet.jsp.PageContext; 027 028import org.springframework.beans.PropertyAccessor; 029import org.springframework.core.Conventions; 030import org.springframework.http.HttpMethod; 031import org.springframework.util.CollectionUtils; 032import org.springframework.util.ObjectUtils; 033import org.springframework.util.StringUtils; 034import org.springframework.web.servlet.support.RequestDataValueProcessor; 035import org.springframework.web.util.HtmlUtils; 036import org.springframework.web.util.UriUtils; 037 038/** 039 * Databinding-aware JSP tag for rendering an HTML '{@code form}' whose 040 * inner elements are bound to properties on a <em>form object</em>. 041 * 042 * <p>Users should place the form object into the 043 * {@link org.springframework.web.servlet.ModelAndView ModelAndView} when 044 * populating the data for their view. The name of this form object can be 045 * configured using the {@link #setModelAttribute "modelAttribute"} property. 046 * 047 * @author Rob Harrop 048 * @author Juergen Hoeller 049 * @author Scott Andrews 050 * @author Rossen Stoyanchev 051 * @since 2.0 052 */ 053@SuppressWarnings("serial") 054public class FormTag extends AbstractHtmlElementTag { 055 056 /** The default HTTP method using which form values are sent to the server: "post" */ 057 private static final String DEFAULT_METHOD = "post"; 058 059 /** The default attribute name: "command" */ 060 public static final String DEFAULT_COMMAND_NAME = "command"; 061 062 /** The name of the '{@code modelAttribute}' setting */ 063 private static final String MODEL_ATTRIBUTE = "modelAttribute"; 064 065 /** 066 * The name of the {@link javax.servlet.jsp.PageContext} attribute under which the 067 * form object name is exposed. 068 */ 069 public static final String MODEL_ATTRIBUTE_VARIABLE_NAME = 070 Conventions.getQualifiedAttributeName(AbstractFormTag.class, MODEL_ATTRIBUTE); 071 072 /** Default method parameter, i.e. {@code _method}. */ 073 private static final String DEFAULT_METHOD_PARAM = "_method"; 074 075 private static final String FORM_TAG = "form"; 076 077 private static final String INPUT_TAG = "input"; 078 079 private static final String ACTION_ATTRIBUTE = "action"; 080 081 private static final String METHOD_ATTRIBUTE = "method"; 082 083 private static final String TARGET_ATTRIBUTE = "target"; 084 085 private static final String ENCTYPE_ATTRIBUTE = "enctype"; 086 087 private static final String ACCEPT_CHARSET_ATTRIBUTE = "accept-charset"; 088 089 private static final String ONSUBMIT_ATTRIBUTE = "onsubmit"; 090 091 private static final String ONRESET_ATTRIBUTE = "onreset"; 092 093 private static final String AUTOCOMPLETE_ATTRIBUTE = "autocomplete"; 094 095 private static final String NAME_ATTRIBUTE = "name"; 096 097 private static final String VALUE_ATTRIBUTE = "value"; 098 099 private static final String TYPE_ATTRIBUTE = "type"; 100 101 102 private TagWriter tagWriter; 103 104 private String modelAttribute = DEFAULT_COMMAND_NAME; 105 106 private String name; 107 108 private String action; 109 110 private String servletRelativeAction; 111 112 private String method = DEFAULT_METHOD; 113 114 private String target; 115 116 private String enctype; 117 118 private String acceptCharset; 119 120 private String onsubmit; 121 122 private String onreset; 123 124 private String autocomplete; 125 126 private String methodParam = DEFAULT_METHOD_PARAM; 127 128 /** Caching a previous nested path, so that it may be reset */ 129 private String previousNestedPath; 130 131 132 /** 133 * Set the name of the form attribute in the model. 134 * <p>May be a runtime expression. 135 */ 136 public void setModelAttribute(String modelAttribute) { 137 this.modelAttribute = modelAttribute; 138 } 139 140 /** 141 * Get the name of the form attribute in the model. 142 */ 143 protected String getModelAttribute() { 144 return this.modelAttribute; 145 } 146 147 /** 148 * Set the name of the form attribute in the model. 149 * <p>May be a runtime expression. 150 * @see #setModelAttribute 151 * @deprecated as of Spring 4.3, in favor of {@link #setModelAttribute} 152 */ 153 @Deprecated 154 public void setCommandName(String commandName) { 155 this.modelAttribute = commandName; 156 } 157 158 /** 159 * Get the name of the form attribute in the model. 160 * @see #getModelAttribute 161 * @deprecated as of Spring 4.3, in favor of {@link #getModelAttribute} 162 */ 163 @Deprecated 164 protected String getCommandName() { 165 return this.modelAttribute; 166 } 167 168 /** 169 * Set the value of the '{@code name}' attribute. 170 * <p>May be a runtime expression. 171 * <p>Name is not a valid attribute for form on XHTML 1.0. However, 172 * it is sometimes needed for backward compatibility. 173 */ 174 public void setName(String name) { 175 this.name = name; 176 } 177 178 /** 179 * Get the value of the '{@code name}' attribute. 180 */ 181 @Override 182 protected String getName() throws JspException { 183 return this.name; 184 } 185 186 /** 187 * Set the value of the '{@code action}' attribute. 188 * <p>May be a runtime expression. 189 */ 190 public void setAction(String action) { 191 this.action = (action != null ? action : ""); 192 } 193 194 /** 195 * Get the value of the '{@code action}' attribute. 196 */ 197 protected String getAction() { 198 return this.action; 199 } 200 201 /** 202 * Set the value of the '{@code action}' attribute through a value 203 * that is to be appended to the current servlet path. 204 * <p>May be a runtime expression. 205 * @since 3.2.3 206 */ 207 public void setServletRelativeAction(String servletRelativeAction) { 208 this.servletRelativeAction = (servletRelativeAction != null ? servletRelativeAction : ""); 209 } 210 211 /** 212 * Get the servlet-relative value of the '{@code action}' attribute. 213 * @since 3.2.3 214 */ 215 protected String getServletRelativeAction() { 216 return this.servletRelativeAction; 217 } 218 219 /** 220 * Set the value of the '{@code method}' attribute. 221 * <p>May be a runtime expression. 222 */ 223 public void setMethod(String method) { 224 this.method = method; 225 } 226 227 /** 228 * Get the value of the '{@code method}' attribute. 229 */ 230 protected String getMethod() { 231 return this.method; 232 } 233 234 /** 235 * Set the value of the '{@code target}' attribute. 236 * <p>May be a runtime expression. 237 */ 238 public void setTarget(String target) { 239 this.target = target; 240 } 241 242 /** 243 * Get the value of the '{@code target}' attribute. 244 */ 245 public String getTarget() { 246 return this.target; 247 } 248 249 /** 250 * Set the value of the '{@code enctype}' attribute. 251 * <p>May be a runtime expression. 252 */ 253 public void setEnctype(String enctype) { 254 this.enctype = enctype; 255 } 256 257 /** 258 * Get the value of the '{@code enctype}' attribute. 259 */ 260 protected String getEnctype() { 261 return this.enctype; 262 } 263 264 /** 265 * Set the value of the '{@code acceptCharset}' attribute. 266 * <p>May be a runtime expression. 267 */ 268 public void setAcceptCharset(String acceptCharset) { 269 this.acceptCharset = acceptCharset; 270 } 271 272 /** 273 * Get the value of the '{@code acceptCharset}' attribute. 274 */ 275 protected String getAcceptCharset() { 276 return this.acceptCharset; 277 } 278 279 /** 280 * Set the value of the '{@code onsubmit}' attribute. 281 * <p>May be a runtime expression. 282 */ 283 public void setOnsubmit(String onsubmit) { 284 this.onsubmit = onsubmit; 285 } 286 287 /** 288 * Get the value of the '{@code onsubmit}' attribute. 289 */ 290 protected String getOnsubmit() { 291 return this.onsubmit; 292 } 293 294 /** 295 * Set the value of the '{@code onreset}' attribute. 296 * <p>May be a runtime expression. 297 */ 298 public void setOnreset(String onreset) { 299 this.onreset = onreset; 300 } 301 302 /** 303 * Get the value of the '{@code onreset}' attribute. 304 */ 305 protected String getOnreset() { 306 return this.onreset; 307 } 308 309 /** 310 * Set the value of the '{@code autocomplete}' attribute. 311 * May be a runtime expression. 312 */ 313 public void setAutocomplete(String autocomplete) { 314 this.autocomplete = autocomplete; 315 } 316 317 /** 318 * Get the value of the '{@code autocomplete}' attribute. 319 */ 320 protected String getAutocomplete() { 321 return this.autocomplete; 322 } 323 324 /** 325 * Set the name of the request param for non-browser supported HTTP methods. 326 */ 327 public void setMethodParam(String methodParam) { 328 this.methodParam = methodParam; 329 } 330 331 /** 332 * Get the name of the request param for non-browser supported HTTP methods. 333 * @since 4.2.3 334 */ 335 @SuppressWarnings("deprecation") 336 protected String getMethodParam() { 337 return getMethodParameter(); 338 } 339 340 /** 341 * Get the name of the request param for non-browser supported HTTP methods. 342 * @deprecated as of 4.2.3, in favor of {@link #getMethodParam()} which is 343 * a proper pairing for {@link #setMethodParam(String)} 344 */ 345 @Deprecated 346 protected String getMethodParameter() { 347 return this.methodParam; 348 } 349 350 /** 351 * Determine if the HTTP method is supported by browsers (i.e. GET or POST). 352 */ 353 protected boolean isMethodBrowserSupported(String method) { 354 return ("get".equalsIgnoreCase(method) || "post".equalsIgnoreCase(method)); 355 } 356 357 358 /** 359 * Writes the opening part of the block '{@code form}' tag and exposes 360 * the form object name in the {@link javax.servlet.jsp.PageContext}. 361 * @param tagWriter the {@link TagWriter} to which the form content is to be written 362 * @return {@link javax.servlet.jsp.tagext.Tag#EVAL_BODY_INCLUDE} 363 */ 364 @Override 365 protected int writeTagContent(TagWriter tagWriter) throws JspException { 366 this.tagWriter = tagWriter; 367 368 tagWriter.startTag(FORM_TAG); 369 writeDefaultAttributes(tagWriter); 370 tagWriter.writeAttribute(ACTION_ATTRIBUTE, resolveAction()); 371 writeOptionalAttribute(tagWriter, METHOD_ATTRIBUTE, getHttpMethod()); 372 writeOptionalAttribute(tagWriter, TARGET_ATTRIBUTE, getTarget()); 373 writeOptionalAttribute(tagWriter, ENCTYPE_ATTRIBUTE, getEnctype()); 374 writeOptionalAttribute(tagWriter, ACCEPT_CHARSET_ATTRIBUTE, getAcceptCharset()); 375 writeOptionalAttribute(tagWriter, ONSUBMIT_ATTRIBUTE, getOnsubmit()); 376 writeOptionalAttribute(tagWriter, ONRESET_ATTRIBUTE, getOnreset()); 377 writeOptionalAttribute(tagWriter, AUTOCOMPLETE_ATTRIBUTE, getAutocomplete()); 378 379 tagWriter.forceBlock(); 380 381 if (!isMethodBrowserSupported(getMethod())) { 382 assertHttpMethod(getMethod()); 383 String inputName = getMethodParam(); 384 String inputType = "hidden"; 385 tagWriter.startTag(INPUT_TAG); 386 writeOptionalAttribute(tagWriter, TYPE_ATTRIBUTE, inputType); 387 writeOptionalAttribute(tagWriter, NAME_ATTRIBUTE, inputName); 388 writeOptionalAttribute(tagWriter, VALUE_ATTRIBUTE, processFieldValue(inputName, getMethod(), inputType)); 389 tagWriter.endTag(); 390 } 391 392 // Expose the form object name for nested tags... 393 String modelAttribute = resolveModelAttribute(); 394 this.pageContext.setAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME, modelAttribute, PageContext.REQUEST_SCOPE); 395 396 // Save previous nestedPath value, build and expose current nestedPath value. 397 // Use request scope to expose nestedPath to included pages too. 398 this.previousNestedPath = 399 (String) this.pageContext.getAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE); 400 this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME, 401 modelAttribute + PropertyAccessor.NESTED_PROPERTY_SEPARATOR, PageContext.REQUEST_SCOPE); 402 403 return EVAL_BODY_INCLUDE; 404 } 405 406 private String getHttpMethod() { 407 return (isMethodBrowserSupported(getMethod()) ? getMethod() : DEFAULT_METHOD); 408 } 409 410 private void assertHttpMethod(String method) { 411 for (HttpMethod httpMethod : HttpMethod.values()) { 412 if (httpMethod.name().equalsIgnoreCase(method)) { 413 return; 414 } 415 } 416 throw new IllegalArgumentException("Invalid HTTP method: " + method); 417 } 418 419 /** 420 * Autogenerated IDs correspond to the form object name. 421 */ 422 @Override 423 protected String autogenerateId() throws JspException { 424 return resolveModelAttribute(); 425 } 426 427 /** 428 * {@link #evaluate Resolves} and returns the name of the form object. 429 * @throws IllegalArgumentException if the form object resolves to {@code null} 430 */ 431 protected String resolveModelAttribute() throws JspException { 432 Object resolvedModelAttribute = evaluate(MODEL_ATTRIBUTE, getModelAttribute()); 433 if (resolvedModelAttribute == null) { 434 throw new IllegalArgumentException(MODEL_ATTRIBUTE + " must not be null"); 435 } 436 return (String) resolvedModelAttribute; 437 } 438 439 /** 440 * Resolve the value of the '{@code action}' attribute. 441 * <p>If the user configured an '{@code action}' value then the result of 442 * evaluating this value is used. If the user configured an 443 * '{@code servletRelativeAction}' value then the value is prepended 444 * with the context and servlet paths, and the result is used. Otherwise, the 445 * {@link org.springframework.web.servlet.support.RequestContext#getRequestUri() 446 * originating URI} is used. 447 * @return the value that is to be used for the '{@code action}' attribute 448 */ 449 protected String resolveAction() throws JspException { 450 String action = getAction(); 451 String servletRelativeAction = getServletRelativeAction(); 452 if (StringUtils.hasText(action)) { 453 action = getDisplayString(evaluate(ACTION_ATTRIBUTE, action)); 454 return processAction(action); 455 } 456 else if (StringUtils.hasText(servletRelativeAction)) { 457 String pathToServlet = getRequestContext().getPathToServlet(); 458 if (servletRelativeAction.startsWith("/") && 459 !servletRelativeAction.startsWith(getRequestContext().getContextPath())) { 460 servletRelativeAction = pathToServlet + servletRelativeAction; 461 } 462 servletRelativeAction = getDisplayString(evaluate(ACTION_ATTRIBUTE, servletRelativeAction)); 463 return processAction(servletRelativeAction); 464 } 465 else { 466 String requestUri = getRequestContext().getRequestUri(); 467 String encoding = this.pageContext.getResponse().getCharacterEncoding(); 468 try { 469 requestUri = UriUtils.encodePath(requestUri, encoding); 470 } 471 catch (UnsupportedEncodingException ex) { 472 // shouldn't happen - if it does, proceed with requestUri as-is 473 } 474 ServletResponse response = this.pageContext.getResponse(); 475 if (response instanceof HttpServletResponse) { 476 requestUri = ((HttpServletResponse) response).encodeURL(requestUri); 477 String queryString = getRequestContext().getQueryString(); 478 if (StringUtils.hasText(queryString)) { 479 requestUri += "?" + HtmlUtils.htmlEscape(queryString); 480 } 481 } 482 if (StringUtils.hasText(requestUri)) { 483 return processAction(requestUri); 484 } 485 else { 486 throw new IllegalArgumentException("Attribute 'action' is required. " + 487 "Attempted to resolve against current request URI but request URI was null."); 488 } 489 } 490 } 491 492 /** 493 * Process the action through a {@link RequestDataValueProcessor} instance 494 * if one is configured or otherwise returns the action unmodified. 495 */ 496 private String processAction(String action) { 497 RequestDataValueProcessor processor = getRequestContext().getRequestDataValueProcessor(); 498 ServletRequest request = this.pageContext.getRequest(); 499 if (processor != null && request instanceof HttpServletRequest) { 500 action = processor.processAction((HttpServletRequest) request, action, getHttpMethod()); 501 } 502 return action; 503 } 504 505 /** 506 * Closes the '{@code form}' block tag and removes the form object name 507 * from the {@link javax.servlet.jsp.PageContext}. 508 */ 509 @Override 510 public int doEndTag() throws JspException { 511 RequestDataValueProcessor processor = getRequestContext().getRequestDataValueProcessor(); 512 ServletRequest request = this.pageContext.getRequest(); 513 if (processor != null && request instanceof HttpServletRequest) { 514 writeHiddenFields(processor.getExtraHiddenFields((HttpServletRequest) request)); 515 } 516 this.tagWriter.endTag(); 517 return EVAL_PAGE; 518 } 519 520 /** 521 * Writes the given values as hidden fields. 522 */ 523 private void writeHiddenFields(Map<String, String> hiddenFields) throws JspException { 524 if (!CollectionUtils.isEmpty(hiddenFields)) { 525 this.tagWriter.appendValue("<div>\n"); 526 for (String name : hiddenFields.keySet()) { 527 this.tagWriter.appendValue("<input type=\"hidden\" "); 528 this.tagWriter.appendValue("name=\"" + name + "\" value=\"" + hiddenFields.get(name) + "\" "); 529 this.tagWriter.appendValue("/>\n"); 530 } 531 this.tagWriter.appendValue("</div>"); 532 } 533 } 534 535 /** 536 * Clears the stored {@link TagWriter}. 537 */ 538 @Override 539 public void doFinally() { 540 super.doFinally(); 541 542 this.pageContext.removeAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME, PageContext.REQUEST_SCOPE); 543 if (this.previousNestedPath != null) { 544 // Expose previous nestedPath value. 545 this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME, this.previousNestedPath, PageContext.REQUEST_SCOPE); 546 } 547 else { 548 // Remove exposed nestedPath value. 549 this.pageContext.removeAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE); 550 } 551 this.tagWriter = null; 552 this.previousNestedPath = null; 553 } 554 555 556 /** 557 * Override resolve CSS class since error class is not supported. 558 */ 559 @Override 560 protected String resolveCssClass() throws JspException { 561 return ObjectUtils.getDisplayString(evaluate("cssClass", getCssClass())); 562 } 563 564 /** 565 * Unsupported for forms. 566 * @throws UnsupportedOperationException always 567 */ 568 @Override 569 public void setPath(String path) { 570 throw new UnsupportedOperationException("The 'path' attribute is not supported for forms"); 571 } 572 573 /** 574 * Unsupported for forms. 575 * @throws UnsupportedOperationException always 576 */ 577 @Override 578 public void setCssErrorClass(String cssErrorClass) { 579 throw new UnsupportedOperationException("The 'cssErrorClass' attribute is not supported for forms"); 580 } 581 582}