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.beans.PropertyEditor;
020import javax.servlet.ServletRequest;
021import javax.servlet.http.HttpServletRequest;
022import javax.servlet.jsp.JspException;
023import javax.servlet.jsp.PageContext;
024
025import org.springframework.beans.PropertyAccessor;
026import org.springframework.util.StringUtils;
027import org.springframework.web.servlet.support.BindStatus;
028import org.springframework.web.servlet.support.RequestDataValueProcessor;
029import org.springframework.web.servlet.tags.EditorAwareTag;
030import org.springframework.web.servlet.tags.NestedPathTag;
031
032/**
033 * Base tag for all data-binding aware JSP form tags.
034 *
035 * <p>Provides the common {@link #setPath path} and {@link #setId id} properties.
036 * Provides sub-classes with utility methods for accessing the {@link BindStatus}
037 * of their bound value and also for {@link #writeOptionalAttribute interacting}
038 * with the {@link TagWriter}.
039 *
040 * @author Rob Harrop
041 * @author Juergen Hoeller
042 * @since 2.0
043 */
044@SuppressWarnings("serial")
045public abstract class AbstractDataBoundFormElementTag extends AbstractFormTag implements EditorAwareTag {
046
047        /**
048         * Name of the exposed path variable within the scope of this tag: "nestedPath".
049         * Same value as {@link org.springframework.web.servlet.tags.NestedPathTag#NESTED_PATH_VARIABLE_NAME}.
050         */
051        protected static final String NESTED_PATH_VARIABLE_NAME = NestedPathTag.NESTED_PATH_VARIABLE_NAME;
052
053
054        /**
055         * The property path from the {@link FormTag#setModelAttribute form object}.
056         */
057        private String path;
058
059        /**
060         * The value of the '{@code id}' attribute.
061         */
062        private String id;
063
064        /**
065         * The {@link BindStatus} of this tag.
066         */
067        private BindStatus bindStatus;
068
069
070        /**
071         * Set the property path from the {@link FormTag#setModelAttribute form object}.
072         * May be a runtime expression.
073         */
074        public void setPath(String path) {
075                this.path = path;
076        }
077
078        /**
079         * Get the {@link #evaluate resolved} property path for the
080         * {@link FormTag#setModelAttribute form object}.
081         */
082        protected final String getPath() throws JspException {
083                String resolvedPath = (String) evaluate("path", this.path);
084                return (resolvedPath != null ? resolvedPath : "");
085        }
086
087        /**
088         * Set the value of the '{@code id}' attribute.
089         * <p>May be a runtime expression; defaults to the value of {@link #getName()}.
090         * Note that the default value may not be valid for certain tags.
091         */
092        @Override
093        public void setId(String id) {
094                this.id = id;
095        }
096
097        /**
098         * Get the value of the '{@code id}' attribute.
099         */
100        @Override
101        public String getId() {
102                return this.id;
103        }
104
105
106        /**
107         * Writes the default set of attributes to the supplied {@link TagWriter}.
108         * Further abstract sub-classes should override this method to add in
109         * any additional default attributes but <strong>must</strong> remember
110         * to call the {@code super} method.
111         * <p>Concrete sub-classes should call this method when/if they want
112         * to render default attributes.
113         * @param tagWriter the {@link TagWriter} to which any attributes are to be written
114         */
115        protected void writeDefaultAttributes(TagWriter tagWriter) throws JspException {
116                writeOptionalAttribute(tagWriter, "id", resolveId());
117                writeOptionalAttribute(tagWriter, "name", getName());
118        }
119
120        /**
121         * Determine the '{@code id}' attribute value for this tag,
122         * autogenerating one if none specified.
123         * @see #getId()
124         * @see #autogenerateId()
125         */
126        protected String resolveId() throws JspException {
127                Object id = evaluate("id", getId());
128                if (id != null) {
129                        String idString = id.toString();
130                        return (StringUtils.hasText(idString) ? idString : null);
131                }
132                return autogenerateId();
133        }
134
135        /**
136         * Autogenerate the '{@code id}' attribute value for this tag.
137         * <p>The default implementation simply delegates to {@link #getName()},
138         * deleting invalid characters (such as "[" or "]").
139         */
140        protected String autogenerateId() throws JspException {
141                return StringUtils.deleteAny(getName(), "[]");
142        }
143
144        /**
145         * Get the value for the HTML '{@code name}' attribute.
146         * <p>The default implementation simply delegates to
147         * {@link #getPropertyPath()} to use the property path as the name.
148         * For the most part this is desirable as it links with the server-side
149         * expectation for data binding. However, some subclasses may wish to change
150         * the value of the '{@code name}' attribute without changing the bind path.
151         * @return the value for the HTML '{@code name}' attribute
152         */
153        protected String getName() throws JspException {
154                return getPropertyPath();
155        }
156
157        /**
158         * Get the {@link BindStatus} for this tag.
159         */
160        protected BindStatus getBindStatus() throws JspException {
161                if (this.bindStatus == null) {
162                        // HTML escaping in tags is performed by the ValueFormatter class.
163                        String nestedPath = getNestedPath();
164                        String pathToUse = (nestedPath != null ? nestedPath + getPath() : getPath());
165                        if (pathToUse.endsWith(PropertyAccessor.NESTED_PROPERTY_SEPARATOR)) {
166                                pathToUse = pathToUse.substring(0, pathToUse.length() - 1);
167                        }
168                        this.bindStatus = new BindStatus(getRequestContext(), pathToUse, false);
169                }
170                return this.bindStatus;
171        }
172
173        /**
174         * Get the value of the nested path that may have been exposed by the
175         * {@link NestedPathTag}.
176         */
177        protected String getNestedPath() {
178                return (String) this.pageContext.getAttribute(NESTED_PATH_VARIABLE_NAME, PageContext.REQUEST_SCOPE);
179        }
180
181        /**
182         * Build the property path for this tag, including the nested path
183         * but <i>not</i> prefixed with the name of the form attribute.
184         * @see #getNestedPath()
185         * @see #getPath()
186         */
187        protected String getPropertyPath() throws JspException {
188                String expression = getBindStatus().getExpression();
189                return (expression != null ? expression : "");
190        }
191
192        /**
193         * Get the bound value.
194         * @see #getBindStatus()
195         */
196        protected final Object getBoundValue() throws JspException {
197                return getBindStatus().getValue();
198        }
199
200        /**
201         * Get the {@link PropertyEditor}, if any, in use for value bound to this tag.
202         */
203        protected PropertyEditor getPropertyEditor() throws JspException {
204                return getBindStatus().getEditor();
205        }
206
207        /**
208         * Exposes the {@link PropertyEditor} for {@link EditorAwareTag}.
209         * <p>Use {@link #getPropertyEditor()} for internal rendering purposes.
210         */
211        @Override
212        public final PropertyEditor getEditor() throws JspException {
213                return getPropertyEditor();
214        }
215
216        /**
217         * Get a display String for the given value, converted by a PropertyEditor
218         * that the BindStatus may have registered for the value's Class.
219         */
220        protected String convertToDisplayString(Object value) throws JspException {
221                PropertyEditor editor = (value != null ? getBindStatus().findEditor(value.getClass()) : null);
222                return getDisplayString(value, editor);
223        }
224
225        /**
226         * Process the given form field through a {@link RequestDataValueProcessor}
227         * instance if one is configured or otherwise returns the same value.
228         */
229        protected final String processFieldValue(String name, String value, String type) {
230                RequestDataValueProcessor processor = getRequestContext().getRequestDataValueProcessor();
231                ServletRequest request = this.pageContext.getRequest();
232                if (processor != null && request instanceof HttpServletRequest) {
233                        value = processor.processFormFieldValue((HttpServletRequest) request, name, value, type);
234                }
235                return value;
236        }
237
238        /**
239         * Disposes of the {@link BindStatus} instance.
240         */
241        @Override
242        public void doFinally() {
243                super.doFinally();
244                this.bindStatus = null;
245        }
246
247}