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