001// Licensed under the Apache License, Version 2.0 (the "License"); 002// you may not use this file except in compliance with the License. 003// You may obtain a copy of the License at 004// 005// http://www.apache.org/licenses/LICENSE-2.0 006// 007// Unless required by applicable law or agreed to in writing, software 008// distributed under the License is distributed on an "AS IS" BASIS, 009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 010// See the License for the specific language governing permissions and 011// limitations under the License. 012 013package org.apache.tapestry5.corelib.base; 014 015import org.apache.commons.lang3.StringEscapeUtils; 016import org.apache.tapestry5.*; 017import org.apache.tapestry5.annotations.*; 018import org.apache.tapestry5.commons.internal.util.TapestryException; 019import org.apache.tapestry5.corelib.mixins.DiscardBody; 020import org.apache.tapestry5.corelib.mixins.RenderInformals; 021import org.apache.tapestry5.http.services.Request; 022import org.apache.tapestry5.internal.BeanValidationContext; 023import org.apache.tapestry5.internal.InternalComponentResources; 024import org.apache.tapestry5.internal.services.FormControlNameManager; 025import org.apache.tapestry5.ioc.annotations.Inject; 026import org.apache.tapestry5.ioc.annotations.Symbol; 027import org.apache.tapestry5.ioc.internal.util.InternalUtils; 028import org.apache.tapestry5.services.ComponentDefaultProvider; 029import org.apache.tapestry5.services.Environment; 030import org.apache.tapestry5.services.FormSupport; 031import org.apache.tapestry5.services.javascript.JavaScriptSupport; 032 033import java.io.Serializable; 034 035/** 036 * Provides initialization of the clientId and elementName properties. In addition, adds the {@link RenderInformals}, 037 * and {@link DiscardBody} mixins. 038 * 039 * @tapestrydoc 040 */ 041@SuppressWarnings("deprecation") 042@SupportsInformalParameters 043public abstract class AbstractField implements Field 044{ 045 /** 046 * The user presentable label for the field. If not provided, a reasonable label is generated from the component's 047 * id, first by looking for a message key named "id-label" (substituting the component's actual id), then by 048 * converting the actual id to a presentable string (for example, "userId" to "User Id"). 049 */ 050 @Parameter(defaultPrefix = BindingConstants.LITERAL) 051 protected String label; 052 053 /** 054 * If true, then the field will render out with a disabled attribute 055 * (to turn off client-side behavior). When the form is submitted, the 056 * bound value is evaluated again and, if true, the field's value is 057 * ignored (not even validated) and the component's events are not fired. 058 */ 059 @Parameter("false") 060 protected boolean disabled; 061 062 @SuppressWarnings("unused") 063 @Mixin 064 private DiscardBody discardBody; 065 066 @Environmental 067 protected ValidationDecorator decorator; 068 069 @Inject 070 protected Environment environment; 071 072 @Inject 073 @Symbol(SymbolConstants.FORM_FIELD_CSS_CLASS) 074 protected String cssClass; 075 076 static class Setup implements ComponentAction<AbstractField>, Serializable 077 { 078 private static final long serialVersionUID = 2690270808212097020L; 079 080 private final String controlName; 081 082 public Setup(String controlName) 083 { 084 this.controlName = controlName; 085 } 086 087 public void execute(AbstractField component) 088 { 089 component.setupControlName(controlName); 090 } 091 092 @Override 093 public String toString() 094 { 095 return String.format("AbstractField.Setup[%s]", controlName); 096 } 097 } 098 099 static class ProcessSubmission implements ComponentAction<AbstractField>, Serializable 100 { 101 private static final long serialVersionUID = -4346426414137434418L; 102 103 public void execute(AbstractField component) 104 { 105 component.processSubmission(); 106 } 107 108 @Override 109 public String toString() 110 { 111 return "AbstractField.ProcessSubmission"; 112 } 113 } 114 115 /** 116 * Used a shared instance for all types of fields, for efficiency. 117 */ 118 private static final ProcessSubmission PROCESS_SUBMISSION_ACTION = new ProcessSubmission(); 119 120 /** 121 * Used to explicitly set the client-side id of the element for this component. Normally this is not 122 * bound (or null) and {@link org.apache.tapestry5.services.javascript.JavaScriptSupport#allocateClientId(org.apache.tapestry5.ComponentResources)} 123 * is used to generate a unique client-id based on the component's id. In some cases, when creating client-side 124 * behaviors, it is useful to explicitly set a unique id for an element using this parameter. 125 * 126 * Certain values, such as "submit", "method", "reset", etc., will cause client-side conflicts and are not allowed; using such will 127 * cause a runtime exception. 128 */ 129 @Parameter(defaultPrefix = BindingConstants.LITERAL) 130 private String clientId; 131 132 /** 133 * A rarely used option that indicates that the actual client id should start with the clientId parameter (if non-null) 134 * but should still pass that Id through {@link org.apache.tapestry5.services.javascript.JavaScriptSupport#allocateClientId(String)} 135 * to generate the final id. 136 * 137 * An example of this are the components used inside a {@link org.apache.tapestry5.corelib.components.BeanEditor} which 138 * will specify a clientId (based on the property name) but still require that it be unique. 139 * 140 * Defaults to false. 141 * 142 * @since 5.4 143 */ 144 @Parameter 145 private boolean ensureClientIdUnique; 146 147 148 private String assignedClientId; 149 150 private String controlName; 151 152 @Environmental(false) 153 protected FormSupport formSupport; 154 155 @Environmental 156 protected JavaScriptSupport javaScriptSupport; 157 158 @Environmental 159 protected ValidationTracker validationTracker; 160 161 @Inject 162 protected ComponentResources resources; 163 164 @Inject 165 protected ComponentDefaultProvider defaultProvider; 166 167 @Inject 168 protected Request request; 169 170 @Inject 171 protected FieldValidationSupport fieldValidationSupport; 172 173 @Inject 174 private FormControlNameManager formControlNameManager; 175 176 final String defaultLabel() 177 { 178 return defaultProvider.defaultLabel(resources); 179 } 180 181 public final String getLabel() 182 { 183 return label; 184 } 185 186 @SetupRender 187 final void setup() 188 { 189 // Often, these controlName and clientId will end up as the same value. There are many 190 // exceptions, including a form that renders inside a loop, or a form inside a component 191 // that is used multiple times. 192 193 if (formSupport == null) 194 throw new RuntimeException(String.format("Component %s must be enclosed by a Form component.", 195 resources.getCompleteId())); 196 197 assignedClientId = allocateClientId(); 198 199 String controlName = formSupport.allocateControlName(assignedClientId); 200 201 formSupport.storeAndExecute(this, new Setup(controlName)); 202 formSupport.store(this, PROCESS_SUBMISSION_ACTION); 203 } 204 205 private String allocateClientId() 206 { 207 if (clientId == null) 208 { 209 return javaScriptSupport.allocateClientId(resources); 210 } 211 212 213 if (ensureClientIdUnique) 214 { 215 return javaScriptSupport.allocateClientId(clientId); 216 } else 217 { 218 // See https://issues.apache.org/jira/browse/TAP5-1632 219 // Basically, on the client, there can be a convenience lookup inside a HTMLFormElement 220 // by id OR name; so an id of "submit" (for example) will mask the HTMLFormElement.submit() 221 // function. 222 223 if (formControlNameManager.isReserved(clientId)) 224 { 225 throw new TapestryException(String.format( 226 "The value '%s' for parameter clientId is not allowed as it causes a naming conflict in the client-side DOM. " + 227 "Select an id not in the list: %s.", 228 clientId, 229 InternalUtils.joinSorted(formControlNameManager.getReservedNames())), this, null); 230 } 231 } 232 233 return clientId; 234 } 235 236 public final String getClientId() 237 { 238 return assignedClientId; 239 } 240 241 public final String getControlName() 242 { 243 return controlName; 244 } 245 246 public final boolean isDisabled() 247 { 248 return disabled; 249 } 250 251 /** 252 * Invoked from within a ComponentCommand callback, to restore the component's elementName. 253 */ 254 private void setupControlName(String controlName) 255 { 256 this.controlName = controlName; 257 } 258 259 private void processSubmission() 260 { 261 if (!disabled) 262 processSubmission(controlName); 263 } 264 265 /** 266 * Method implemented by subclasses to actually do the work of processing the submission of the form. The element's 267 * controlName property will already have been set. This method is only invoked if the field is <strong>not 268 * {@link #isDisabled() disabled}</strong>. 269 * 270 * @param controlName 271 * the control name of the rendered element (used to find the correct parameter in the request) 272 */ 273 protected abstract void processSubmission(String controlName); 274 275 /** 276 * Allows the validation decorator to write markup before the field itself writes markup. 277 */ 278 @BeginRender 279 final void beforeDecorator() 280 { 281 decorator.beforeField(this); 282 } 283 284 /** 285 * Allows the validation decorator to write markup after the field has written all of its markup. 286 * In addition, may invoke the <code>core/fields:showValidationError</code> function to present 287 * the field's error (if it has one) to the user. 288 */ 289 @AfterRender 290 final void afterDecorator() 291 { 292 decorator.afterField(this); 293 294 String error = validationTracker.getError(this); 295 error = StringEscapeUtils.escapeHtml4(error); 296 297 if (error != null) 298 { 299 javaScriptSupport.require("t5/core/fields").invoke("showValidationError").with(assignedClientId, error); 300 } 301 } 302 303 /** 304 * Invoked from subclasses after they have written their tag and (where appropriate) their informal parameters 305 * <em>and</em> have allowed their {@link Validator} to write markup as well. 306 */ 307 protected final void decorateInsideField() 308 { 309 decorator.insideField(this); 310 } 311 312 protected final void setDecorator(ValidationDecorator decorator) 313 { 314 this.decorator = decorator; 315 } 316 317 protected final void setFormSupport(FormSupport formSupport) 318 { 319 this.formSupport = formSupport; 320 } 321 322 /** 323 * Returns false; most components do not support declarative validation. 324 */ 325 public boolean isRequired() 326 { 327 return false; 328 } 329 330 // This is set to true for some unit test. 331 private boolean beanValidationDisabled = false; 332 333 protected void putPropertyNameIntoBeanValidationContext(String parameterName) 334 { 335 if (beanValidationDisabled) 336 { 337 return; 338 } 339 340 String propertyName = ((InternalComponentResources) resources).getPropertyName(parameterName); 341 342 BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class); 343 344 if (beanValidationContext == null) 345 return; 346 347 // If field is inside BeanEditForm, then property is already set 348 if (beanValidationContext.getCurrentProperty() == null) 349 { 350 beanValidationContext.setCurrentProperty(propertyName); 351 } 352 } 353 354 protected void removePropertyNameFromBeanValidationContext() 355 { 356 if (beanValidationDisabled) 357 { 358 return; 359 } 360 361 BeanValidationContext beanValidationContext = environment.peek(BeanValidationContext.class); 362 363 if (beanValidationContext == null) 364 return; 365 366 beanValidationContext.setCurrentProperty(null); 367 } 368}