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.nio.charset.UnsupportedCharsetException;
020import java.util.Map;
021
022import javax.servlet.ServletRequest;
023import javax.servlet.ServletResponse;
024import javax.servlet.http.HttpServletRequest;
025import javax.servlet.http.HttpServletResponse;
026import javax.servlet.jsp.JspException;
027import javax.servlet.jsp.PageContext;
028
029import org.springframework.beans.PropertyAccessor;
030import org.springframework.core.Conventions;
031import org.springframework.http.HttpMethod;
032import org.springframework.lang.Nullable;
033import org.springframework.util.Assert;
034import org.springframework.util.CollectionUtils;
035import org.springframework.util.ObjectUtils;
036import org.springframework.util.StringUtils;
037import org.springframework.web.servlet.support.RequestDataValueProcessor;
038import org.springframework.web.util.HtmlUtils;
039import org.springframework.web.util.UriUtils;
040
041/**
042 * The {@code <form>} tag renders an HTML 'form' tag and exposes a binding path to
043 * inner tags for binding.
044 *
045 * <p>Users should place the form object into the
046 * {@link org.springframework.web.servlet.ModelAndView ModelAndView} when
047 * populating the data for their view. The name of this form object can be
048 * configured using the {@link #setModelAttribute "modelAttribute"} property.
049 *
050 * <p>
051 * <table>
052 * <caption>Attribute Summary</caption>
053 * <thead>
054 * <tr>
055 * <th class="colFirst">Attribute</th>
056 * <th class="colOne">Required?</th>
057 * <th class="colOne">Runtime Expression?</th>
058 * <th class="colLast">Description</th>
059 * </tr>
060 * </thead>
061 * <tbody>
062 * <tr class="altColor">
063 * <td><p>acceptCharset</p></td>
064 * <td><p>false</p></td>
065 * <td><p>true</p></td>
066 * <td><p>Specifies the list of character encodings for input data that is accepted
067 * by the server processing this form. The value is a space- and/or comma-delimited
068 * list of charset values. The client must interpret this list as an exclusive-or
069 * list, i.e., the server is able to accept any single character encoding per
070 * entity received.</p></td>
071 * </tr>
072 * <tr class="rowColor">
073 * <td><p>action</p></td>
074 * <td><p>false</p></td>
075 * <td><p>true</p></td>
076 * <td><p>HTML Required Attribute</p></td>
077 * </tr>
078 * <tr class="altColor">
079 * <td><p>cssClass</p></td>
080 * <td><p>false</p></td>
081 * <td><p>true</p></td>
082 * <td><p>HTML Optional Attribute</p></td>
083 * </tr>
084 * <tr class="rowColor">
085 * <td><p>cssStyle</p></td>
086 * <td><p>false</p></td>
087 * <td><p>true</p></td>
088 * <td><p>HTML Optional Attribute</p></td>
089 * </tr>
090 * <tr class="altColor">
091 * <td><p>dir</p></td>
092 * <td><p>false</p></td>
093 * <td><p>true</p></td>
094 * <td><p>HTML Standard Attribute</p></td>
095 * </tr>
096 * <tr class="rowColor">
097 * <td><p>enctype</p></td>
098 * <td><p>false</p></td>
099 * <td><p>true</p></td>
100 * <td><p>HTML Optional Attribute</p></td>
101 * </tr>
102 * <tr class="altColor">
103 * <td><p>htmlEscape</p></td>
104 * <td><p>false</p></td>
105 * <td><p>true</p></td>
106 * <td><p>Enable/disable HTML escaping of rendered values.</p></td>
107 * </tr>
108 * <tr class="rowColor">
109 * <td><p>id</p></td>
110 * <td><p>false</p></td>
111 * <td><p>true</p></td>
112 * <td><p>HTML Standard Attribute</p></td>
113 * </tr>
114 * <tr class="altColor">
115 * <td><p>lang</p></td>
116 * <td><p>false</p></td>
117 * <td><p>true</p></td>
118 * <td><p>HTML Standard Attribute</p></td>
119 * </tr>
120 * <tr class="rowColor">
121 * <td><p>method</p></td>
122 * <td><p>false</p></td>
123 * <td><p>true</p></td>
124 * <td><p>HTML Optional Attribute</p></td>
125 * </tr>
126 * <tr class="altColor">
127 * <td><p>methodParam</p></td>
128 * <td><p>false</p></td>
129 * <td><p>true</p></td>
130 * <td><p>The parameter name used for HTTP methods other then GET and POST.
131 * Default is '_method'.</p></td>
132 * </tr>
133 * <tr class="rowColor">
134 * <td><p>modelAttribute</p></td>
135 * <td><p>false</p></td>
136 * <td><p>true</p></td>
137 * <td><p>Name of the model attribute under which the form object is exposed.
138 * Defaults to 'command'.</p></td>
139 * </tr>
140 * <tr class="altColor">
141 * <td><p>name</p></td>
142 * <td><p>false</p></td>
143 * <td><p>true</p></td>
144 * <td><p>HTML Standard Attribute - added for backwards compatibility cases</p></td>
145 * </tr>
146 * <tr class="rowColor">
147 * <td><p>onclick</p></td>
148 * <td><p>false</p></td>
149 * <td><p>true</p></td>
150 * <td><p>HTML Event Attribute</p></td>
151 * </tr>
152 * <tr class="altColor">
153 * <td><p>ondblclick</p></td>
154 * <td><p>false</p></td>
155 * <td><p>true</p></td>
156 * <td><p>HTML Event Attribute</p></td>
157 * </tr>
158 * <tr class="rowColor">
159 * <td><p>onkeydown</p></td>
160 * <td><p>false</p></td>
161 * <td><p>true</p></td>
162 * <td><p>HTML Event Attribute</p></td>
163 * </tr>
164 * <tr class="altColor">
165 * <td><p>onkeypress</p></td>
166 * <td><p>false</p></td>
167 * <td><p>true</p></td>
168 * <td><p>HTML Event Attribute</p></td>
169 * </tr>
170 * <tr class="rowColor">
171 * <td><p>onkeyup</p></td>
172 * <td><p>false</p></td>
173 * <td><p>true</p></td>
174 * <td><p>HTML Event Attribute</p></td>
175 * </tr>
176 * <tr class="altColor">
177 * <td><p>onmousedown</p></td>
178 * <td><p>false</p></td>
179 * <td><p>true</p></td>
180 * <td><p>HTML Event Attribute</p></td>
181 * </tr>
182 * <tr class="rowColor">
183 * <td><p>onmousemove</p></td>
184 * <td><p>false</p></td>
185 * <td><p>true</p></td>
186 * <td><p>HTML Event Attribute</p></td>
187 * </tr>
188 * <tr class="altColor">
189 * <td><p>onmouseout</p></td>
190 * <td><p>false</p></td>
191 * <td><p>true</p></td>
192 * <td><p>HTML Event Attribute</p></td>
193 * </tr>
194 * <tr class="rowColor">
195 * <td><p>onmouseover</p></td>
196 * <td><p>false</p></td>
197 * <td><p>true</p></td>
198 * <td><p>HTML Event Attribute</p></td>
199 * </tr>
200 * <tr class="altColor">
201 * <td><p>onmouseup</p></td>
202 * <td><p>false</p></td>
203 * <td><p>true</p></td>
204 * <td><p>HTML Event Attribute</p></td>
205 * </tr>
206 * <tr class="rowColor">
207 * <td><p>onreset</p></td>
208 * <td><p>false</p></td>
209 * <td><p>true</p></td>
210 * <td><p>HTML Event Attribute</p></td>
211 * </tr>
212 * <tr class="altColor">
213 * <td><p>onsubmit</p></td>
214 * <td><p>false</p></td>
215 * <td><p>true</p></td>
216 * <td><p>HTML Event Attribute</p></td>
217 * </tr>
218 * <tr class="rowColor">
219 * <td><p>servletRelativeAction</p></td>
220 * <td><p>false</p></td>
221 * <td><p>true</p></td>
222 * <td><p>Action reference to be appended to the current servlet path</p></td>
223 * </tr>
224 * <tr class="altColor">
225 * <td><p>target</p></td>
226 * <td><p>false</p></td>
227 * <td><p>true</p></td>
228 * <td><p>HTML Optional Attribute</p></td>
229 * </tr>
230 * <tr class="rowColor">
231 * <td><p>title</p></td>
232 * <td><p>false</p></td>
233 * <td><p>true</p></td>
234 * <td><p>HTML Standard Attribute</p></td>
235 * </tr>
236 * </tbody>
237 * </table>
238 *
239 * @author Rob Harrop
240 * @author Juergen Hoeller
241 * @author Scott Andrews
242 * @author Rossen Stoyanchev
243 * @since 2.0
244 */
245@SuppressWarnings("serial")
246public class FormTag extends AbstractHtmlElementTag {
247
248        /** The default HTTP method using which form values are sent to the server: "post". */
249        private static final String DEFAULT_METHOD = "post";
250
251        /** The default attribute name: &quot;command&quot;. */
252        public static final String DEFAULT_COMMAND_NAME = "command";
253
254        /** The name of the '{@code modelAttribute}' setting. */
255        private static final String MODEL_ATTRIBUTE = "modelAttribute";
256
257        /**
258         * The name of the {@link javax.servlet.jsp.PageContext} attribute under which the
259         * form object name is exposed.
260         */
261        public static final String MODEL_ATTRIBUTE_VARIABLE_NAME =
262                        Conventions.getQualifiedAttributeName(AbstractFormTag.class, MODEL_ATTRIBUTE);
263
264        /** Default method parameter, i.e. {@code _method}. */
265        private static final String DEFAULT_METHOD_PARAM = "_method";
266
267        private static final String FORM_TAG = "form";
268
269        private static final String INPUT_TAG = "input";
270
271        private static final String ACTION_ATTRIBUTE = "action";
272
273        private static final String METHOD_ATTRIBUTE = "method";
274
275        private static final String TARGET_ATTRIBUTE = "target";
276
277        private static final String ENCTYPE_ATTRIBUTE = "enctype";
278
279        private static final String ACCEPT_CHARSET_ATTRIBUTE = "accept-charset";
280
281        private static final String ONSUBMIT_ATTRIBUTE = "onsubmit";
282
283        private static final String ONRESET_ATTRIBUTE = "onreset";
284
285        private static final String AUTOCOMPLETE_ATTRIBUTE = "autocomplete";
286
287        private static final String NAME_ATTRIBUTE = "name";
288
289        private static final String VALUE_ATTRIBUTE = "value";
290
291        private static final String TYPE_ATTRIBUTE = "type";
292
293
294        @Nullable
295        private TagWriter tagWriter;
296
297        private String modelAttribute = DEFAULT_COMMAND_NAME;
298
299        @Nullable
300        private String name;
301
302        @Nullable
303        private String action;
304
305        @Nullable
306        private String servletRelativeAction;
307
308        private String method = DEFAULT_METHOD;
309
310        @Nullable
311        private String target;
312
313        @Nullable
314        private String enctype;
315
316        @Nullable
317        private String acceptCharset;
318
319        @Nullable
320        private String onsubmit;
321
322        @Nullable
323        private String onreset;
324
325        @Nullable
326        private String autocomplete;
327
328        private String methodParam = DEFAULT_METHOD_PARAM;
329
330        /** Caching a previous nested path, so that it may be reset. */
331        @Nullable
332        private String previousNestedPath;
333
334
335        /**
336         * Set the name of the form attribute in the model.
337         * <p>May be a runtime expression.
338         */
339        public void setModelAttribute(String modelAttribute) {
340                this.modelAttribute = modelAttribute;
341        }
342
343        /**
344         * Get the name of the form attribute in the model.
345         */
346        protected String getModelAttribute() {
347                return this.modelAttribute;
348        }
349
350        /**
351         * Set the value of the '{@code name}' attribute.
352         * <p>May be a runtime expression.
353         * <p>Name is not a valid attribute for form on XHTML 1.0. However,
354         * it is sometimes needed for backward compatibility.
355         */
356        public void setName(String name) {
357                this.name = name;
358        }
359
360        /**
361         * Get the value of the '{@code name}' attribute.
362         */
363        @Override
364        @Nullable
365        protected String getName() throws JspException {
366                return this.name;
367        }
368
369        /**
370         * Set the value of the '{@code action}' attribute.
371         * <p>May be a runtime expression.
372         */
373        public void setAction(@Nullable String action) {
374                this.action = (action != null ? action : "");
375        }
376
377        /**
378         * Get the value of the '{@code action}' attribute.
379         */
380        @Nullable
381        protected String getAction() {
382                return this.action;
383        }
384
385        /**
386         * Set the value of the '{@code action}' attribute through a value
387         * that is to be appended to the current servlet path.
388         * <p>May be a runtime expression.
389         * @since 3.2.3
390         */
391        public void setServletRelativeAction(@Nullable String servletRelativeAction) {
392                this.servletRelativeAction = servletRelativeAction;
393        }
394
395        /**
396         * Get the servlet-relative value of the '{@code action}' attribute.
397         * @since 3.2.3
398         */
399        @Nullable
400        protected String getServletRelativeAction() {
401                return this.servletRelativeAction;
402        }
403
404        /**
405         * Set the value of the '{@code method}' attribute.
406         * <p>May be a runtime expression.
407         */
408        public void setMethod(String method) {
409                this.method = method;
410        }
411
412        /**
413         * Get the value of the '{@code method}' attribute.
414         */
415        protected String getMethod() {
416                return this.method;
417        }
418
419        /**
420         * Set the value of the '{@code target}' attribute.
421         * <p>May be a runtime expression.
422         */
423        public void setTarget(String target) {
424                this.target = target;
425        }
426
427        /**
428         * Get the value of the '{@code target}' attribute.
429         */
430        @Nullable
431        public String getTarget() {
432                return this.target;
433        }
434
435        /**
436         * Set the value of the '{@code enctype}' attribute.
437         * <p>May be a runtime expression.
438         */
439        public void setEnctype(String enctype) {
440                this.enctype = enctype;
441        }
442
443        /**
444         * Get the value of the '{@code enctype}' attribute.
445         */
446        @Nullable
447        protected String getEnctype() {
448                return this.enctype;
449        }
450
451        /**
452         * Set the value of the '{@code acceptCharset}' attribute.
453         * <p>May be a runtime expression.
454         */
455        public void setAcceptCharset(String acceptCharset) {
456                this.acceptCharset = acceptCharset;
457        }
458
459        /**
460         * Get the value of the '{@code acceptCharset}' attribute.
461         */
462        @Nullable
463        protected String getAcceptCharset() {
464                return this.acceptCharset;
465        }
466
467        /**
468         * Set the value of the '{@code onsubmit}' attribute.
469         * <p>May be a runtime expression.
470         */
471        public void setOnsubmit(String onsubmit) {
472                this.onsubmit = onsubmit;
473        }
474
475        /**
476         * Get the value of the '{@code onsubmit}' attribute.
477         */
478        @Nullable
479        protected String getOnsubmit() {
480                return this.onsubmit;
481        }
482
483        /**
484         * Set the value of the '{@code onreset}' attribute.
485         * <p>May be a runtime expression.
486         */
487        public void setOnreset(String onreset) {
488                this.onreset = onreset;
489        }
490
491        /**
492         * Get the value of the '{@code onreset}' attribute.
493         */
494        @Nullable
495        protected String getOnreset() {
496                return this.onreset;
497        }
498
499        /**
500         * Set the value of the '{@code autocomplete}' attribute.
501         * May be a runtime expression.
502         */
503        public void setAutocomplete(String autocomplete) {
504                this.autocomplete = autocomplete;
505        }
506
507        /**
508         * Get the value of the '{@code autocomplete}' attribute.
509         */
510        @Nullable
511        protected String getAutocomplete() {
512                return this.autocomplete;
513        }
514
515        /**
516         * Set the name of the request param for non-browser supported HTTP methods.
517         */
518        public void setMethodParam(String methodParam) {
519                this.methodParam = methodParam;
520        }
521
522        /**
523         * Get the name of the request param for non-browser supported HTTP methods.
524         * @since 4.2.3
525         */
526        protected String getMethodParam() {
527                return this.methodParam;
528        }
529
530        /**
531         * Determine if the HTTP method is supported by browsers (i.e. GET or POST).
532         */
533        protected boolean isMethodBrowserSupported(String method) {
534                return ("get".equalsIgnoreCase(method) || "post".equalsIgnoreCase(method));
535        }
536
537
538        /**
539         * Writes the opening part of the block '{@code form}' tag and exposes
540         * the form object name in the {@link javax.servlet.jsp.PageContext}.
541         * @param tagWriter the {@link TagWriter} to which the form content is to be written
542         * @return {@link javax.servlet.jsp.tagext.Tag#EVAL_BODY_INCLUDE}
543         */
544        @Override
545        protected int writeTagContent(TagWriter tagWriter) throws JspException {
546                this.tagWriter = tagWriter;
547
548                tagWriter.startTag(FORM_TAG);
549                writeDefaultAttributes(tagWriter);
550                tagWriter.writeAttribute(ACTION_ATTRIBUTE, resolveAction());
551                writeOptionalAttribute(tagWriter, METHOD_ATTRIBUTE, getHttpMethod());
552                writeOptionalAttribute(tagWriter, TARGET_ATTRIBUTE, getTarget());
553                writeOptionalAttribute(tagWriter, ENCTYPE_ATTRIBUTE, getEnctype());
554                writeOptionalAttribute(tagWriter, ACCEPT_CHARSET_ATTRIBUTE, getAcceptCharset());
555                writeOptionalAttribute(tagWriter, ONSUBMIT_ATTRIBUTE, getOnsubmit());
556                writeOptionalAttribute(tagWriter, ONRESET_ATTRIBUTE, getOnreset());
557                writeOptionalAttribute(tagWriter, AUTOCOMPLETE_ATTRIBUTE, getAutocomplete());
558
559                tagWriter.forceBlock();
560
561                if (!isMethodBrowserSupported(getMethod())) {
562                        assertHttpMethod(getMethod());
563                        String inputName = getMethodParam();
564                        String inputType = "hidden";
565                        tagWriter.startTag(INPUT_TAG);
566                        writeOptionalAttribute(tagWriter, TYPE_ATTRIBUTE, inputType);
567                        writeOptionalAttribute(tagWriter, NAME_ATTRIBUTE, inputName);
568                        writeOptionalAttribute(tagWriter, VALUE_ATTRIBUTE, processFieldValue(inputName, getMethod(), inputType));
569                        tagWriter.endTag();
570                }
571
572                // Expose the form object name for nested tags...
573                String modelAttribute = resolveModelAttribute();
574                this.pageContext.setAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME, modelAttribute, PageContext.REQUEST_SCOPE);
575
576                // Save previous nestedPath value, build and expose current nestedPath value.
577                // Use request scope to expose nestedPath to included pages too.
578                this.previousNestedPath =
579                                (String) this.pageContext.getAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
580                this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME,
581                                modelAttribute + PropertyAccessor.NESTED_PROPERTY_SEPARATOR, PageContext.REQUEST_SCOPE);
582
583                return EVAL_BODY_INCLUDE;
584        }
585
586        private String getHttpMethod() {
587                return (isMethodBrowserSupported(getMethod()) ? getMethod() : DEFAULT_METHOD);
588        }
589
590        private void assertHttpMethod(String method) {
591                for (HttpMethod httpMethod : HttpMethod.values()) {
592                        if (httpMethod.name().equalsIgnoreCase(method)) {
593                                return;
594                        }
595                }
596                throw new IllegalArgumentException("Invalid HTTP method: " + method);
597        }
598
599        /**
600         * Autogenerated IDs correspond to the form object name.
601         */
602        @Override
603        protected String autogenerateId() throws JspException {
604                return resolveModelAttribute();
605        }
606
607        /**
608         * {@link #evaluate Resolves} and returns the name of the form object.
609         * @throws IllegalArgumentException if the form object resolves to {@code null}
610         */
611        protected String resolveModelAttribute() throws JspException {
612                Object resolvedModelAttribute = evaluate(MODEL_ATTRIBUTE, getModelAttribute());
613                if (resolvedModelAttribute == null) {
614                        throw new IllegalArgumentException(MODEL_ATTRIBUTE + " must not be null");
615                }
616                return (String) resolvedModelAttribute;
617        }
618
619        /**
620         * Resolve the value of the '{@code action}' attribute.
621         * <p>If the user configured an '{@code action}' value then the result of
622         * evaluating this value is used. If the user configured an
623         * '{@code servletRelativeAction}' value then the value is prepended
624         * with the context and servlet paths, and the result is used. Otherwise, the
625         * {@link org.springframework.web.servlet.support.RequestContext#getRequestUri()
626         * originating URI} is used.
627         * @return the value that is to be used for the '{@code action}' attribute
628         */
629        protected String resolveAction() throws JspException {
630                String action = getAction();
631                String servletRelativeAction = getServletRelativeAction();
632                if (StringUtils.hasText(action)) {
633                        action = getDisplayString(evaluate(ACTION_ATTRIBUTE, action));
634                        return processAction(action);
635                }
636                else if (StringUtils.hasText(servletRelativeAction)) {
637                        String pathToServlet = getRequestContext().getPathToServlet();
638                        if (servletRelativeAction.startsWith("/") &&
639                                        !servletRelativeAction.startsWith(getRequestContext().getContextPath())) {
640                                servletRelativeAction = pathToServlet + servletRelativeAction;
641                        }
642                        servletRelativeAction = getDisplayString(evaluate(ACTION_ATTRIBUTE, servletRelativeAction));
643                        return processAction(servletRelativeAction);
644                }
645                else {
646                        String requestUri = getRequestContext().getRequestUri();
647                        String encoding = this.pageContext.getResponse().getCharacterEncoding();
648                        try {
649                                requestUri = UriUtils.encodePath(requestUri, encoding);
650                        }
651                        catch (UnsupportedCharsetException ex) {
652                                // shouldn't happen - if it does, proceed with requestUri as-is
653                        }
654                        ServletResponse response = this.pageContext.getResponse();
655                        if (response instanceof HttpServletResponse) {
656                                requestUri = ((HttpServletResponse) response).encodeURL(requestUri);
657                                String queryString = getRequestContext().getQueryString();
658                                if (StringUtils.hasText(queryString)) {
659                                        requestUri += "?" + HtmlUtils.htmlEscape(queryString);
660                                }
661                        }
662                        if (StringUtils.hasText(requestUri)) {
663                                return processAction(requestUri);
664                        }
665                        else {
666                                throw new IllegalArgumentException("Attribute 'action' is required. " +
667                                                "Attempted to resolve against current request URI but request URI was null.");
668                        }
669                }
670        }
671
672        /**
673         * Process the action through a {@link RequestDataValueProcessor} instance
674         * if one is configured or otherwise returns the action unmodified.
675         */
676        private String processAction(String action) {
677                RequestDataValueProcessor processor = getRequestContext().getRequestDataValueProcessor();
678                ServletRequest request = this.pageContext.getRequest();
679                if (processor != null && request instanceof HttpServletRequest) {
680                        action = processor.processAction((HttpServletRequest) request, action, getHttpMethod());
681                }
682                return action;
683        }
684
685        /**
686         * Closes the '{@code form}' block tag and removes the form object name
687         * from the {@link javax.servlet.jsp.PageContext}.
688         */
689        @Override
690        public int doEndTag() throws JspException {
691                RequestDataValueProcessor processor = getRequestContext().getRequestDataValueProcessor();
692                ServletRequest request = this.pageContext.getRequest();
693                if (processor != null && request instanceof HttpServletRequest) {
694                        writeHiddenFields(processor.getExtraHiddenFields((HttpServletRequest) request));
695                }
696                Assert.state(this.tagWriter != null, "No TagWriter set");
697                this.tagWriter.endTag();
698                return EVAL_PAGE;
699        }
700
701        /**
702         * Writes the given values as hidden fields.
703         */
704        private void writeHiddenFields(@Nullable Map<String, String> hiddenFields) throws JspException {
705                if (!CollectionUtils.isEmpty(hiddenFields)) {
706                        Assert.state(this.tagWriter != null, "No TagWriter set");
707                        this.tagWriter.appendValue("<div>\n");
708                        for (Map.Entry<String, String> entry : hiddenFields.entrySet()) {
709                                this.tagWriter.appendValue("<input type=\"hidden\" ");
710                                this.tagWriter.appendValue("name=\"" + entry.getKey() + "\" value=\"" + entry.getValue() + "\" ");
711                                this.tagWriter.appendValue("/>\n");
712                        }
713                        this.tagWriter.appendValue("</div>");
714                }
715        }
716
717        /**
718         * Clears the stored {@link TagWriter}.
719         */
720        @Override
721        public void doFinally() {
722                super.doFinally();
723
724                this.pageContext.removeAttribute(MODEL_ATTRIBUTE_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
725                if (this.previousNestedPath != null) {
726                        // Expose previous nestedPath value.
727                        this.pageContext.setAttribute(NESTED_PATH_VARIABLE_NAME, this.previousNestedPath, PageContext.REQUEST_SCOPE);
728                }
729                else {
730                        // Remove exposed nestedPath value.
731                        this.pageContext.removeAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
732                }
733                this.tagWriter = null;
734                this.previousNestedPath = null;
735        }
736
737
738        /**
739         * Override resolve CSS class since error class is not supported.
740         */
741        @Override
742        protected String resolveCssClass() throws JspException {
743                return ObjectUtils.getDisplayString(evaluate("cssClass", getCssClass()));
744        }
745
746        /**
747         * Unsupported for forms.
748         * @throws UnsupportedOperationException always
749         */
750        @Override
751        public void setPath(String path) {
752                throw new UnsupportedOperationException("The 'path' attribute is not supported for forms");
753        }
754
755        /**
756         * Unsupported for forms.
757         * @throws UnsupportedOperationException always
758         */
759        @Override
760        public void setCssErrorClass(String cssErrorClass) {
761                throw new UnsupportedOperationException("The 'cssErrorClass' attribute is not supported for forms");
762        }
763
764}