001: // Copyright 2006, 2007 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.tapestry.corelib.components;
016:
017: import static java.lang.String.format;
018:
019: import java.io.EOFException;
020: import java.io.IOException;
021: import java.io.ObjectInputStream;
022: import java.util.List;
023:
024: import org.apache.tapestry.Asset;
025: import org.apache.tapestry.ClientElement;
026: import org.apache.tapestry.ComponentAction;
027: import org.apache.tapestry.ComponentEventHandler;
028: import org.apache.tapestry.ComponentResources;
029: import org.apache.tapestry.Field;
030: import org.apache.tapestry.FormValidationControl;
031: import org.apache.tapestry.Link;
032: import org.apache.tapestry.MarkupWriter;
033: import org.apache.tapestry.PageRenderSupport;
034: import org.apache.tapestry.TapestryConstants;
035: import org.apache.tapestry.ValidationTracker;
036: import org.apache.tapestry.ValidationTrackerImpl;
037: import org.apache.tapestry.annotations.Environmental;
038: import org.apache.tapestry.annotations.Inject;
039: import org.apache.tapestry.annotations.Mixin;
040: import org.apache.tapestry.annotations.Parameter;
041: import org.apache.tapestry.annotations.Path;
042: import org.apache.tapestry.annotations.Persist;
043: import org.apache.tapestry.corelib.mixins.RenderInformals;
044: import org.apache.tapestry.dom.Element;
045: import org.apache.tapestry.internal.TapestryInternalUtils;
046: import org.apache.tapestry.internal.services.HeartbeatImpl;
047: import org.apache.tapestry.internal.util.Base64ObjectInputStream;
048: import org.apache.tapestry.internal.util.Base64ObjectOutputStream;
049: import org.apache.tapestry.internal.util.Holder;
050: import org.apache.tapestry.ioc.internal.util.TapestryException;
051: import org.apache.tapestry.runtime.Component;
052: import org.apache.tapestry.services.ActionResponseGenerator;
053: import org.apache.tapestry.services.ComponentEventResultProcessor;
054: import org.apache.tapestry.services.ComponentSource;
055: import org.apache.tapestry.services.Environment;
056: import org.apache.tapestry.services.FormSupport;
057: import org.apache.tapestry.services.Heartbeat;
058: import org.apache.tapestry.services.Request;
059:
060: /**
061: * An HTML form, which will enclose other components to render out the various types of fields.
062: * <p>
063: * A Form emits several notification events; when it renders it sends a {@link #PREPARE prepare}
064: * notification event, to allow any listeners to set up the state of the page prior to rendering out
065: * the form's content.
066: * <p>
067: * When the form is submitted, the component emits four notifications: first another prepare event
068: * to allow the page to update its state as necessary to prepare for the form submission, then
069: * (after components enclosed by the form have operated), a "validate" event is emitted, to allow
070: * for cross-form validation. After that, either a "success" or "failure" event (depending on
071: * whether the {@link ValidationTracker} has recorded any errors). Lastly, a "submit" event, for any
072: * listeners that care only about form submission, regardless of success or failure.
073: * <p>
074: * For all of these notifications, the event context is derived from the <strong>context</strong>
075: * parameter. This context is encoded into the form's action URI (the parameter is not read when the
076: * form is submitted, instead the values encoded into the form are used).
077: */
078: public class Form implements ClientElement, FormValidationControl {
079: /**
080: * Invoked to let the containing component(s) prepare for the form rendering or the form
081: * submission.
082: */
083: public static final String PREPARE = "prepare";
084:
085: /**
086: * Event type for a notification after the form has submitted. This event notification occurs on
087: * any form submit, without respect to "success" or "failure".
088: */
089: public static final String SUBMIT = "submit";
090:
091: /**
092: * Event type for a notification to perform validation of submitted data. This allows a listener
093: * to perform cross-field validation. This occurs before the {@link #SUCCESS} or
094: * {@link #FAILURE} notification.
095: */
096: public static final String VALIDATE = "validate";
097:
098: /**
099: * Event type for a notification after the form has submitted, when there are no errors in the
100: * validation tracker. This occurs before the {@link #SUBMIT} event.
101: */
102: public static final String SUCCESS = "success";
103:
104: /**
105: * Event type for a notification after the form has been submitted, when there are errors in the
106: * validation tracker. This occurs before the {@link #SUBMIT} event.
107: */
108: public static final String FAILURE = "failure";
109:
110: /**
111: * The context for the link (optional parameter). This list of values will be converted into
112: * strings and included in the URI. The strings will be coerced back to whatever their values
113: * are and made available to event handler methods.
114: */
115: @Parameter
116: private List<?> _context;
117:
118: /**
119: * The object which will record user input and validation errors. The object must be persistent
120: * between requests (since the form submission and validation occurs in an component event
121: * request and the subsequent render occurs in a render request). The default is a persistent
122: * property of the Form component and this is sufficient for nearly all purposes (except when a
123: * Form is rendered inside a loop).
124: */
125: @Parameter("defaultTracker")
126: private ValidationTracker _tracker;
127:
128: /**
129: * Query parameter name storing form data (the serialized commands needed to process a form
130: * submission).
131: */
132: public static final String FORM_DATA = "t:formdata";
133:
134: /**
135: * If true (the default) then client validation is enabled for the form, and the default set of
136: * JavaScript libraries (Prototype, Scriptaculous and the Tapestry library) will be added to the
137: * rendered page, and the form will register itself for validation. This may be turned off when
138: * client validation is not desired; for example, when many validations are used that do not
139: * operate on the client side at all.
140: */
141: @Parameter("true")
142: private boolean _clientValidation;
143:
144: @Inject
145: private Environment _environment;
146:
147: @Inject
148: private ComponentResources _resources;
149:
150: @Environmental
151: private PageRenderSupport _pageRenderSupport;
152:
153: @Inject
154: private Request _request;
155:
156: @Inject
157: private ComponentSource _source;
158:
159: @Persist
160: private ValidationTracker _defaultTracker;
161:
162: private FormSupportImpl _formSupport;
163:
164: private Element _form;
165:
166: private Element _div;
167:
168: @Inject
169: @Path("${tapestry.scriptaculous}/prototype.js")
170: private Asset _prototype;
171:
172: @Inject
173: @Path("${tapestry.scriptaculous}/scriptaculous.js")
174: private Asset _scriptaculous;
175:
176: @Inject
177: @Path("classpath:/org/apache/tapestry/tapestry.js")
178: private Asset _tapestry;
179:
180: // Collects a stream of component actions. Each action goes in as a UTF string (the component
181: // component id), followed by a ComponentAction
182:
183: private Base64ObjectOutputStream _actions;
184:
185: @SuppressWarnings("unused")
186: @Mixin
187: private RenderInformals _renderInformals;
188:
189: @Inject
190: private ComponentEventResultProcessor _eventResultProcessor;
191:
192: private String _name;
193:
194: public ValidationTracker getDefaultTracker() {
195: if (_defaultTracker == null)
196: _defaultTracker = new ValidationTrackerImpl();
197:
198: return _defaultTracker;
199: }
200:
201: public void setDefaultTracker(ValidationTracker defaultTracker) {
202: _defaultTracker = defaultTracker;
203: }
204:
205: void beginRender(MarkupWriter writer) {
206:
207: try {
208: _actions = new Base64ObjectOutputStream();
209: } catch (IOException ex) {
210: throw new RuntimeException(ex);
211: }
212:
213: _name = _pageRenderSupport.allocateClientId(_resources.getId());
214:
215: _formSupport = new FormSupportImpl(_name, _actions);
216:
217: // TODO: Forms should not allow to nest. Perhaps a set() method instead of a push() method
218: // for this kind of check?
219:
220: _environment.push(FormSupport.class, _formSupport);
221: _environment.push(ValidationTracker.class, _tracker);
222: // Now that the environment is setup, inform the component or other listeners that the form
223: // is about to render.
224:
225: Object[] contextArray = _context == null ? new Object[0]
226: : _context.toArray();
227:
228: _resources.triggerEvent(PREPARE, contextArray, null);
229:
230: Link link = _resources.createActionLink(
231: TapestryConstants.ACTION_EVENT, true, contextArray);
232:
233: // Save the form element for later, in case we want to write an encoding type attribute.
234:
235: _form = writer.element("form", "name", _name, "id", _name,
236: "method", "post", "action", link);
237:
238: _resources.renderInformalParameters(writer);
239:
240: _div = writer.element("div", "class", "t-invisible");
241:
242: for (String parameterName : link.getParameterNames()) {
243: String value = link.getParameterValue(parameterName);
244:
245: writer.element("input", "type", "hidden", "name",
246: parameterName, "value", value);
247: writer.end();
248: }
249:
250: writer.end(); // div
251:
252: if (_clientValidation) {
253: _pageRenderSupport.addScriptLink(_prototype,
254: _scriptaculous, _tapestry);
255:
256: _pageRenderSupport.addScript(format(
257: "Tapestry.registerForm('%s');", _name));
258: }
259:
260: _environment.peek(Heartbeat.class).begin();
261:
262: }
263:
264: void afterRender(MarkupWriter writer) {
265: _environment.peek(Heartbeat.class).end();
266:
267: _formSupport.executeDeferred();
268:
269: String encodingType = _formSupport.getEncodingType();
270:
271: if (encodingType != null)
272: _form.forceAttributes("enctype", encodingType);
273:
274: writer.end(); // form
275:
276: // Now, inject into the div the remaining hidden field (the list of actions).
277:
278: try {
279: _actions.close();
280: } catch (IOException ex) {
281: throw new RuntimeException(ex);
282: }
283:
284: _div.element("input", "type", "hidden", "name", FORM_DATA,
285: "value", _actions.toBase64());
286: }
287:
288: void cleanupRender() {
289: _environment.pop(FormSupport.class);
290:
291: _formSupport = null;
292:
293: // This forces a change to the tracker, which is nice because its internal state has
294: // changed.
295: _tracker = _environment.pop(ValidationTracker.class);
296: }
297:
298: @SuppressWarnings("unchecked")
299: Object onAction(Object[] context) {
300: _tracker.clear();
301:
302: _formSupport = new FormSupportImpl();
303:
304: _environment.push(ValidationTracker.class, _tracker);
305: _environment.push(FormSupport.class, _formSupport);
306:
307: Heartbeat heartbeat = new HeartbeatImpl();
308:
309: _environment.push(Heartbeat.class, heartbeat);
310:
311: heartbeat.begin();
312:
313: try {
314: final Holder<ActionResponseGenerator> holder = Holder
315: .create();
316:
317: ComponentEventHandler handler = new ComponentEventHandler() {
318: public boolean handleResult(Object result,
319: Component component, String methodDescription) {
320: if (result instanceof Boolean)
321: return ((Boolean) result);
322:
323: holder.put(_eventResultProcessor
324: .processComponentEvent(result, component,
325: methodDescription));
326:
327: return true; // Abort other event processing.
328: }
329: };
330:
331: _resources.triggerEvent(PREPARE, context, handler);
332:
333: if (holder.hasValue())
334: return holder.get();
335:
336: // TODO: Ajax stuff will eventually mean there are multiple values for this parameter
337: // name
338:
339: String actionsBase64 = _request.getParameter(FORM_DATA);
340:
341: ObjectInputStream ois = null;
342:
343: Component component = null;
344:
345: try {
346: ois = new Base64ObjectInputStream(actionsBase64);
347:
348: while (true) {
349: String componentId = ois.readUTF();
350: ComponentAction action = (ComponentAction) ois
351: .readObject();
352:
353: component = _source.getComponent(componentId);
354:
355: action.execute(component);
356:
357: component = null;
358: }
359: } catch (EOFException ex) {
360: // Expected
361: } catch (Exception ex) {
362: throw new TapestryException(ex.getMessage(), component,
363: ex);
364: } finally {
365: TapestryInternalUtils.close(ois);
366: }
367:
368: heartbeat.end();
369:
370: ValidationTracker tracker = _environment
371: .peek(ValidationTracker.class);
372:
373: // Let the listeners peform any final validations
374:
375: // Update through the parameter because the tracker has almost certainly changed
376: // internal state.
377:
378: _tracker = tracker;
379:
380: _resources.triggerEvent(VALIDATE, context, handler);
381:
382: if (holder.hasValue())
383: return holder.get();
384:
385: _formSupport.executeDeferred();
386:
387: // Let the listeners know about overall success or failure. Most listeners fall into
388: // one of those two camps.
389:
390: // If the tracker has no errors, then clear it of any input values
391: // as well, so that the next page render will be "clean" and show
392: // true persistent data, not value from the previous form submission.
393:
394: if (!_tracker.getHasErrors())
395: _tracker.clear();
396:
397: _resources.triggerEvent(tracker.getHasErrors() ? FAILURE
398: : SUCCESS, context, handler);
399:
400: // Lastly, tell anyone whose interested that the form is completely submitted.
401:
402: if (holder.hasValue())
403: return holder.get();
404:
405: _resources.triggerEvent(SUBMIT, context, handler);
406:
407: return holder.get();
408: } finally {
409: _environment.pop(Heartbeat.class);
410: _environment.pop(FormSupport.class);
411: }
412: }
413:
414: public void recordError(String errorMessage) {
415: ValidationTracker tracker = _tracker;
416:
417: tracker.recordError(errorMessage);
418:
419: _tracker = tracker;
420: }
421:
422: public void recordError(Field field, String errorMessage) {
423: ValidationTracker tracker = _tracker;
424:
425: tracker.recordError(field, errorMessage);
426:
427: _tracker = tracker;
428: }
429:
430: public boolean getHasErrors() {
431: return _tracker.getHasErrors();
432: }
433:
434: public boolean isValid() {
435: return !_tracker.getHasErrors();
436: }
437:
438: // For testing:
439:
440: void setTracker(ValidationTracker tracker) {
441: _tracker = tracker;
442: }
443:
444: public void clearErrors() {
445: _tracker.clear();
446: }
447:
448: /**
449: * Forms use the same value for their name and their id attribute.
450: */
451: public String getClientId() {
452: return _name;
453: }
454: }
|