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: &quot;command&quot; */
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}