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