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 }