001 // Copyright 2006, 2007, 2008, 2009 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.components;
016
017 import org.apache.tapestry5.*;
018 import org.apache.tapestry5.annotations.*;
019 import org.apache.tapestry5.corelib.internal.ComponentActionSink;
020 import org.apache.tapestry5.corelib.internal.FormSupportImpl;
021 import org.apache.tapestry5.corelib.internal.InternalFormSupport;
022 import org.apache.tapestry5.corelib.mixins.RenderInformals;
023 import org.apache.tapestry5.dom.Element;
024 import org.apache.tapestry5.internal.services.ComponentResultProcessorWrapper;
025 import org.apache.tapestry5.internal.services.HeartbeatImpl;
026 import org.apache.tapestry5.internal.util.AutofocusValidationDecorator;
027 import org.apache.tapestry5.ioc.Location;
028 import org.apache.tapestry5.ioc.Messages;
029 import org.apache.tapestry5.ioc.annotations.Inject;
030 import org.apache.tapestry5.ioc.annotations.Symbol;
031 import org.apache.tapestry5.ioc.internal.util.IdAllocator;
032 import org.apache.tapestry5.ioc.internal.util.InternalUtils;
033 import org.apache.tapestry5.ioc.internal.util.TapestryException;
034 import org.apache.tapestry5.ioc.util.ExceptionUtils;
035 import org.apache.tapestry5.runtime.Component;
036 import org.apache.tapestry5.services.*;
037 import org.slf4j.Logger;
038
039 import java.io.EOFException;
040 import java.io.IOException;
041 import java.io.ObjectInputStream;
042
043 /**
044 * An HTML form, which will enclose other components to render out the various types of fields.
045 * <p/>
046 * A Form emits many notification events. When it renders, it fires a {@link org.apache.tapestry5.EventConstants#PREPARE_FOR_RENDER}
047 * notification, followed by a {@link org.apache.tapestry5.EventConstants#PREPARE} notification.
048 * <p/>
049 * When the form is submitted, the component emits several notifications: first a {@link
050 * org.apache.tapestry5.EventConstants#PREPARE_FOR_SUBMIT}, then a {@link org.apache.tapestry5.EventConstants#PREPARE}:
051 * these allow the page to update its state as necessary to prepare for the form submission, then (after components
052 * enclosed by the form have operated), a {@link org.apache.tapestry5.EventConstants#VALIDATE_FORM} event is emitted, to
053 * allow for cross-form validation. After that, either a {@link org.apache.tapestry5.EventConstants#SUCCESS} OR {@link
054 * org.apache.tapestry5.EventConstants#FAILURE} event (depending on whether the {@link ValidationTracker} has recorded
055 * any errors). Lastly, a {@link org.apache.tapestry5.EventConstants#SUBMIT} event, for any listeners that care only
056 * about form submission, regardless of success or failure.
057 * <p/>
058 * For all of these notifications, the event context is derived from the <strong>context</strong> parameter. This
059 * context is encoded into the form's action URI (the parameter is not read when the form is submitted, instead the
060 * values encoded into the form are used).
061 */
062 @Events({ EventConstants.PREPARE_FOR_RENDER, EventConstants.PREPARE, EventConstants.PREPARE_FOR_SUBMIT,
063 EventConstants.VALIDATE_FORM,
064 EventConstants.SUBMIT, EventConstants.FAILURE, EventConstants.SUCCESS })
065 public class Form implements ClientElement, FormValidationControl
066 {
067 /**
068 * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
069 */
070 public static final String PREPARE_FOR_RENDER = EventConstants.PREPARE_FOR_RENDER;
071
072 /**
073 * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
074 */
075 public static final String PREPARE_FOR_SUBMIT = EventConstants.PREPARE_FOR_SUBMIT;
076
077 /**
078 * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
079 */
080 public static final String PREPARE = EventConstants.PREPARE;
081
082 /**
083 * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
084 */
085 public static final String SUBMIT = EventConstants.SUBMIT;
086
087 /**
088 * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
089 */
090 public static final String VALIDATE_FORM = EventConstants.VALIDATE_FORM;
091
092 /**
093 * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
094 */
095 public static final String SUCCESS = EventConstants.SUCCESS;
096
097 /**
098 * @deprecated Use constant from {@link org.apache.tapestry5.EventConstants} instead.
099 */
100 public static final String FAILURE = EventConstants.FAILURE;
101
102 /**
103 * Query parameter name storing form data (the serialized commands needed to process a form submission).
104 */
105 public static final String FORM_DATA = "t:formdata";
106
107 /**
108 * The context for the link (optional parameter). This list of values will be converted into strings and included in
109 * the URI. The strings will be coerced back to whatever their values are and made available to event handler
110 * methods.
111 */
112 @Parameter
113 private Object[] context;
114
115 /**
116 * The object which will record user input and validation errors. The object must be persistent between requests
117 * (since the form submission and validation occurs in an component event request and the subsequent render occurs
118 * in a render request). The default is a persistent property of the Form component and this is sufficient for
119 * nearly all purposes (except when a Form is rendered inside a loop).
120 */
121 @Parameter("defaultTracker")
122 private ValidationTracker tracker;
123
124 @Inject
125 @Symbol(SymbolConstants.FORM_CLIENT_LOGIC_ENABLED)
126 private boolean clientLogicDefaultEnabled;
127
128 /**
129 * If true (the default) then client validation is enabled for the form, and the default set of JavaScript libraries
130 * (Prototype, Scriptaculous and the Tapestry library) will be added to the rendered page, and the form will
131 * register itself for validation. This may be turned off when client validation is not desired; for example, when
132 * many validations are used that do not operate on the client side at all.
133 */
134 @Parameter
135 private boolean clientValidation = clientLogicDefaultEnabled;
136
137 /**
138 * If true (the default), then the JavaScript will be added to position the cursor into the form. The field to
139 * receive focus is the first rendered field that is in error, or required, or present (in that order of priority).
140 *
141 * @see SymbolConstants#FORM_CLIENT_LOGIC_ENABLED
142 */
143 @Parameter
144 private boolean autofocus = clientLogicDefaultEnabled;
145
146 /**
147 * Binding the zone parameter will cause the form submission to be handled as an Ajax request that updates the
148 * indicated zone. Often a Form will update the same zone that contains it.
149 */
150 @Parameter(defaultPrefix = BindingConstants.LITERAL)
151 private String zone;
152
153 /**
154 * Prefix value used when searching for validation messages and constraints. The default is the Form component's
155 * id. This is overriden by {@link org.apache.tapestry5.corelib.components.BeanEditForm}.
156 *
157 * @see org.apache.tapestry5.services.FormSupport#getFormValidationId()
158 */
159 @Parameter
160 private String validationId;
161
162 @Inject
163 private Logger logger;
164
165 @Inject
166 private Environment environment;
167
168 @Inject
169 private ComponentResources resources;
170
171 @Inject
172 private Messages messages;
173
174 @Environmental
175 private RenderSupport renderSupport;
176
177 @Inject
178 private Request request;
179
180 @Inject
181 private ComponentSource source;
182
183 @Persist(PersistenceConstants.FLASH)
184 private ValidationTracker defaultTracker;
185
186 private InternalFormSupport formSupport;
187
188 private Element form;
189
190 private Element div;
191
192 // Collects a stream of component actions. Each action goes in as a UTF string (the component
193 // component id), followed by a ComponentAction
194
195 private ComponentActionSink actionSink;
196
197 @Mixin
198 private RenderInformals renderInformals;
199
200 /**
201 * Set up via the traditional or Ajax component event request handler
202 */
203 @Environmental
204 private ComponentEventResultProcessor componentEventResultProcessor;
205
206 @Environmental
207 private ClientBehaviorSupport clientBehaviorSupport;
208
209 @Inject
210 private ClientDataEncoder clientDataEncoder;
211
212 private String name;
213
214 String defaultValidationId()
215 {
216 return resources.getId();
217 }
218
219 public ValidationTracker getDefaultTracker()
220 {
221 if (defaultTracker == null) defaultTracker = new ValidationTrackerImpl();
222
223 return defaultTracker;
224 }
225
226 public void setDefaultTracker(ValidationTracker defaultTracker)
227 {
228 this.defaultTracker = defaultTracker;
229 }
230
231 void setupRender()
232 {
233 FormSupport existing = environment.peek(FormSupport.class);
234
235 if (existing != null)
236 throw new TapestryException(messages.get("nesting-not-allowed"), existing, null);
237 }
238
239 void beginRender(MarkupWriter writer)
240 {
241 Link link = resources.createFormEventLink(EventConstants.ACTION, context);
242
243 actionSink = new ComponentActionSink(logger, clientDataEncoder);
244
245 name = renderSupport.allocateClientId(resources);
246
247 formSupport = createRenderTimeFormSupport(name, actionSink, new IdAllocator());
248
249 if (zone != null) clientBehaviorSupport.linkZone(name, zone, link);
250
251 // TODO: Forms should not allow to nest. Perhaps a set() method instead of a push() method
252 // for this kind of check?
253
254 environment.push(FormSupport.class, formSupport);
255 environment.push(ValidationTracker.class, tracker);
256
257 if (autofocus)
258 {
259 ValidationDecorator autofocusDecorator = new AutofocusValidationDecorator(environment.peek(
260 ValidationDecorator.class), tracker, renderSupport);
261 environment.push(ValidationDecorator.class, autofocusDecorator);
262 }
263
264 // Now that the environment is setup, inform the component or other listeners that the form
265 // is about to render.
266
267 resources.triggerEvent(EventConstants.PREPARE_FOR_RENDER, context, null);
268
269 resources.triggerEvent(EventConstants.PREPARE, context, null);
270
271 // Save the form element for later, in case we want to write an encoding type attribute.
272
273 form = writer.element("form",
274 "name", name,
275 "id", name,
276 "method", "post",
277 "action", link);
278
279 if ((zone != null || clientValidation) && !request.isXHR())
280 writer.attributes("onsubmit", MarkupConstants.WAIT_FOR_PAGE);
281
282 resources.renderInformalParameters(writer);
283
284 div = writer.element("div", "class", CSSClassConstants.INVISIBLE);
285
286 for (String parameterName : link.getParameterNames())
287 {
288 String value = link.getParameterValue(parameterName);
289
290 writer.element("input",
291 "type", "hidden",
292 "name", parameterName,
293 "value", value);
294 writer.end();
295 }
296
297 writer.end(); // div
298
299 environment.peek(Heartbeat.class).begin();
300 }
301
302 /**
303 * Creates an {@link org.apache.tapestry5.corelib.internal.InternalFormSupport} for this Form. This method is used
304 * by {@link org.apache.tapestry5.corelib.components.FormInjector}.
305 *
306 * @param name the client-side name and client id for the rendered form element
307 * @param actionSink used to collect component actions that will, ultimately, be written as the t:formdata hidden
308 * field
309 * @param allocator used to allocate unique ids
310 * @return form support object
311 */
312 InternalFormSupport createRenderTimeFormSupport(String name, ComponentActionSink actionSink, IdAllocator allocator)
313 {
314 return new FormSupportImpl(resources, name, actionSink, clientBehaviorSupport,
315 clientValidation, allocator, validationId);
316 }
317
318 void afterRender(MarkupWriter writer)
319 {
320 environment.peek(Heartbeat.class).end();
321
322 formSupport.executeDeferred();
323
324 String encodingType = formSupport.getEncodingType();
325
326 if (encodingType != null) form.forceAttributes("enctype", encodingType);
327
328 writer.end(); // form
329
330 div.element("input",
331 "type", "hidden",
332 "name", FORM_DATA,
333 "value", actionSink.getClientData());
334
335 if (autofocus)
336 environment.pop(ValidationDecorator.class);
337 }
338
339 void cleanupRender()
340 {
341 environment.pop(FormSupport.class);
342
343 formSupport = null;
344
345 environment.pop(ValidationTracker.class);
346 }
347
348 @SuppressWarnings({ "unchecked", "InfiniteLoopStatement" })
349 @Log
350 Object onAction(EventContext context) throws IOException
351 {
352 tracker.clear();
353
354 formSupport = new FormSupportImpl(resources, validationId);
355
356 environment.push(ValidationTracker.class, tracker);
357 environment.push(FormSupport.class, formSupport);
358
359 Heartbeat heartbeat = new HeartbeatImpl();
360
361 environment.push(Heartbeat.class, heartbeat);
362
363 heartbeat.begin();
364
365 try
366 {
367 ComponentResultProcessorWrapper callback = new ComponentResultProcessorWrapper(
368 componentEventResultProcessor);
369
370 resources.triggerContextEvent(EventConstants.PREPARE_FOR_SUBMIT, context, callback);
371
372 if (callback.isAborted()) return true;
373
374 resources.triggerContextEvent(EventConstants.PREPARE, context, callback);
375
376 if (callback.isAborted()) return true;
377
378 executeStoredActions();
379
380 heartbeat.end();
381
382 formSupport.executeDeferred();
383
384 fireValidateFormEvent(context, callback);
385
386 if (callback.isAborted()) return true;
387
388 // Let the listeners know about overall success or failure. Most listeners fall into
389 // one of those two camps.
390
391 // If the tracker has no errors, then clear it of any input values
392 // as well, so that the next page render will be "clean" and show
393 // true persistent data, not value from the previous form submission.
394
395 if (!tracker.getHasErrors())
396 tracker.clear();
397
398 resources.triggerContextEvent(tracker.getHasErrors() ? EventConstants.FAILURE : EventConstants.SUCCESS,
399 context, callback);
400
401 // Lastly, tell anyone whose interested that the form is completely submitted.
402
403 if (callback.isAborted()) return true;
404
405 resources.triggerContextEvent(EventConstants.SUBMIT, context, callback);
406
407 return callback.isAborted();
408 }
409 finally
410 {
411 environment.pop(Heartbeat.class);
412 environment.pop(FormSupport.class);
413
414 // This forces an update that feeds through the system and gets the updated
415 // state of the tracker (if using the Form's defaultTracker property, which is flash persisted)
416 // stored back into the session.
417
418 tracker = environment.pop(ValidationTracker.class);
419 }
420 }
421
422 private void fireValidateFormEvent(EventContext context, ComponentResultProcessorWrapper callback)
423 {
424 try
425 {
426 resources.triggerContextEvent(EventConstants.VALIDATE_FORM, context, callback);
427 }
428 catch (RuntimeException ex)
429 {
430 ValidationException ve = ExceptionUtils.findCause(ex, ValidationException.class);
431
432 if (ve != null)
433 {
434 recordError(ve.getMessage());
435 return;
436 }
437
438 throw ex;
439 }
440 }
441
442 /**
443 * Pulls the stored actions out of the request, converts them from MIME stream back to object stream and then
444 * objects, and executes them.
445 */
446 private void executeStoredActions()
447 {
448 String[] values = request.getParameters(FORM_DATA);
449
450 if (!request.getMethod().equals("POST") || values == null)
451 throw new RuntimeException(messages.format("invalid-request", FORM_DATA));
452
453 // Due to Ajax (FormInjector) there may be multiple values here, so handle each one individually.
454
455 for (String clientEncodedActions : values)
456 {
457 if (InternalUtils.isBlank(clientEncodedActions)) continue;
458
459 logger.debug("Processing actions: {}", clientEncodedActions);
460
461 ObjectInputStream ois = null;
462
463 Component component = null;
464
465 try
466 {
467 ois = clientDataEncoder.decodeClientData(clientEncodedActions);
468
469 while (true)
470 {
471 String componentId = ois.readUTF();
472 ComponentAction action = (ComponentAction) ois.readObject();
473
474 component = source.getComponent(componentId);
475
476 logger.debug("Processing: {} {}", componentId, action);
477
478 action.execute(component);
479
480 component = null;
481 }
482 }
483 catch (EOFException ex)
484 {
485 // Expected
486 }
487 catch (Exception ex)
488 {
489 Location location = component == null ? null : component.getComponentResources().getLocation();
490
491 throw new TapestryException(ex.getMessage(), location, ex);
492 }
493 finally
494 {
495 InternalUtils.close(ois);
496 }
497 }
498 }
499
500 public void recordError(String errorMessage)
501 {
502 tracker.recordError(errorMessage);
503 }
504
505 public void recordError(Field field, String errorMessage)
506 {
507 tracker.recordError(field, errorMessage);
508 }
509
510 public boolean getHasErrors()
511 {
512 return tracker.getHasErrors();
513 }
514
515 public boolean isValid()
516 {
517 return !tracker.getHasErrors();
518 }
519
520 // For testing:
521
522 void setTracker(ValidationTracker tracker)
523 {
524 this.tracker = tracker;
525 }
526
527 public void clearErrors()
528 {
529 tracker.clear();
530 }
531
532 /**
533 * Forms use the same value for their name and their id attribute.
534 */
535 public String getClientId()
536 {
537 return name;
538 }
539 }