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