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;
018
019import java.beans.PropertyEditor;
020
021import javax.servlet.jsp.JspTagException;
022import javax.servlet.jsp.PageContext;
023
024import org.springframework.lang.Nullable;
025import org.springframework.util.Assert;
026import org.springframework.validation.Errors;
027import org.springframework.web.servlet.support.BindStatus;
028
029/**
030 * The {@code <bind>} tag supports evaluation of binding errors for a certain
031 * bean or bean property. Exposes a "status" variable of type
032 * {@link org.springframework.web.servlet.support.BindStatus},
033 * to both Java expressions and JSP EL expressions.
034 *
035 * <p>Can be used to bind to any bean or bean property in the model.
036 * The specified path determines whether the tag exposes the status of the
037 * bean itself (showing object-level errors), a specific bean property
038 * (showing field errors), or a matching set of bean properties
039 * (showing all corresponding field errors).
040 *
041 * <p>The {@link org.springframework.validation.Errors} object that has
042 * been bound using this tag is exposed to collaborating tags, as well
043 * as the bean property that this errors object applies to. Nested tags
044 * such as the {@link TransformTag} can access those exposed properties.
045 *
046 * <table>
047 * <caption>Attribute Summary</caption>
048 * <thead>
049 * <tr>
050 * <th class="colFirst">Attribute</th>
051 * <th class="colOne">Required?</th>
052 * <th class="colOne">Runtime Expression?</th>
053 * <th class="colLast">Description</th>
054 * </tr>
055 * </thead>
056 * <tbody>
057 * <tr class="altColor">
058 * <td><p>htmlEscape</p></td>
059 * <td><p>false</p></td>
060 * <td><p>true</p></td>
061 * <td><p>Set HTML escaping for this tag, as boolean value. Overrides the default
062 * HTML escaping setting for the current page.</p></td>
063 * </tr>
064 * <tr class="rowColor">
065 * <td><p>ignoreNestedPath</p></td>
066 * <td><p>false</p></td>
067 * <td><p>true</p></td>
068 * <td><p>Set whether to ignore a nested path, if any.
069 * Default is to not ignore.</p></td>
070 * </tr>
071 * <tr class="altColor">
072 * <td><p>path</p></td>
073 * <td><p>true</p></td>
074 * <td><p>true</p></td>
075 * <td><p>The path to the bean or bean property to bind status information for.
076 * For instance account.name, company.address.zipCode or just employee. The status
077 * object will exported to the page scope, specifically for this bean or bean
078 * property</p></td>
079 * </tr>
080 * </tbody>
081 * </table>
082 *
083 * @author Rod Johnson
084 * @author Juergen Hoeller
085 * @see #setPath
086 */
087@SuppressWarnings("serial")
088public class BindTag extends HtmlEscapingAwareTag implements EditorAwareTag {
089
090        /**
091         * Name of the exposed variable within the scope of this tag: "status".
092         */
093        public static final String STATUS_VARIABLE_NAME = "status";
094
095
096        private String path = "";
097
098        private boolean ignoreNestedPath = false;
099
100        @Nullable
101        private BindStatus status;
102
103        @Nullable
104        private Object previousPageStatus;
105
106        @Nullable
107        private Object previousRequestStatus;
108
109
110        /**
111         * Set the path that this tag should apply. Can be a bean (e.g. "person")
112         * to get global errors, or a bean property (e.g. "person.name") to get
113         * field errors (also supporting nested fields and "person.na*" mappings).
114         * "person.*" will return all errors for the specified bean, both global
115         * and field errors.
116         * @see org.springframework.validation.Errors#getGlobalErrors
117         * @see org.springframework.validation.Errors#getFieldErrors
118         */
119        public void setPath(String path) {
120                this.path = path;
121        }
122
123        /**
124         * Return the path that this tag applies to.
125         */
126        public String getPath() {
127                return this.path;
128        }
129
130        /**
131         * Set whether to ignore a nested path, if any.
132         * Default is to not ignore.
133         */
134        public void setIgnoreNestedPath(boolean ignoreNestedPath) {
135                this.ignoreNestedPath = ignoreNestedPath;
136        }
137
138        /**
139         * Return whether to ignore a nested path, if any.
140         */
141        public boolean isIgnoreNestedPath() {
142                return this.ignoreNestedPath;
143        }
144
145
146        @Override
147        protected final int doStartTagInternal() throws Exception {
148                String resolvedPath = getPath();
149                if (!isIgnoreNestedPath()) {
150                        String nestedPath = (String) this.pageContext.getAttribute(
151                                        NestedPathTag.NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
152                        // only prepend if not already an absolute path
153                        if (nestedPath != null && !resolvedPath.startsWith(nestedPath) &&
154                                        !resolvedPath.equals(nestedPath.substring(0, nestedPath.length() - 1))) {
155                                resolvedPath = nestedPath + resolvedPath;
156                        }
157                }
158
159                try {
160                        this.status = new BindStatus(getRequestContext(), resolvedPath, isHtmlEscape());
161                }
162                catch (IllegalStateException ex) {
163                        throw new JspTagException(ex.getMessage());
164                }
165
166                // Save previous status values, for re-exposure at the end of this tag.
167                this.previousPageStatus = this.pageContext.getAttribute(STATUS_VARIABLE_NAME, PageContext.PAGE_SCOPE);
168                this.previousRequestStatus = this.pageContext.getAttribute(STATUS_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
169
170                // Expose this tag's status object as PageContext attribute,
171                // making it available for JSP EL.
172                this.pageContext.removeAttribute(STATUS_VARIABLE_NAME, PageContext.PAGE_SCOPE);
173                this.pageContext.setAttribute(STATUS_VARIABLE_NAME, this.status, PageContext.REQUEST_SCOPE);
174
175                return EVAL_BODY_INCLUDE;
176        }
177
178        @Override
179        public int doEndTag() {
180                // Reset previous status values.
181                if (this.previousPageStatus != null) {
182                        this.pageContext.setAttribute(STATUS_VARIABLE_NAME, this.previousPageStatus, PageContext.PAGE_SCOPE);
183                }
184                if (this.previousRequestStatus != null) {
185                        this.pageContext.setAttribute(STATUS_VARIABLE_NAME, this.previousRequestStatus, PageContext.REQUEST_SCOPE);
186                }
187                else {
188                        this.pageContext.removeAttribute(STATUS_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
189                }
190                return EVAL_PAGE;
191        }
192
193
194        /**
195         * Return the current BindStatus.
196         */
197        private BindStatus getStatus() {
198                Assert.state(this.status != null, "No current BindStatus");
199                return this.status;
200        }
201
202        /**
203         * Retrieve the property that this tag is currently bound to,
204         * or {@code null} if bound to an object rather than a specific property.
205         * Intended for cooperating nesting tags.
206         * @return the property that this tag is currently bound to,
207         * or {@code null} if none
208         */
209        @Nullable
210        public final String getProperty() {
211                return getStatus().getExpression();
212        }
213
214        /**
215         * Retrieve the Errors instance that this tag is currently bound to.
216         * Intended for cooperating nesting tags.
217         * @return the current Errors instance, or {@code null} if none
218         */
219        @Nullable
220        public final Errors getErrors() {
221                return getStatus().getErrors();
222        }
223
224        @Override
225        @Nullable
226        public final PropertyEditor getEditor() {
227                return getStatus().getEditor();
228        }
229
230
231        @Override
232        public void doFinally() {
233                super.doFinally();
234                this.status = null;
235                this.previousPageStatus = null;
236                this.previousRequestStatus = null;
237        }
238
239}