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