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}