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