blob: 900459a3d3f7fce5e2a5e2c9581134f07f86f93b [file] [log] [blame]
// Copyright 2006, 2007, 2008 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry5.corelib.components;
import org.apache.tapestry5.*;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Mixin;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Persist;
import org.apache.tapestry5.corelib.internal.FormSupportImpl;
import org.apache.tapestry5.corelib.mixins.RenderInformals;
import org.apache.tapestry5.dom.Element;
import org.apache.tapestry5.internal.services.ClientBehaviorSupport;
import org.apache.tapestry5.internal.services.ComponentInvocationMap;
import org.apache.tapestry5.internal.services.ComponentResultProcessorWrapper;
import org.apache.tapestry5.internal.services.HeartbeatImpl;
import org.apache.tapestry5.internal.util.Base64ObjectInputStream;
import org.apache.tapestry5.internal.util.Base64ObjectOutputStream;
import org.apache.tapestry5.ioc.Location;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.internal.util.InternalUtils;
import org.apache.tapestry5.ioc.internal.util.TapestryException;
import org.apache.tapestry5.runtime.Component;
import org.apache.tapestry5.services.*;
import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.List;
/**
* An HTML form, which will enclose other components to render out the various types of fields.
* <p/>
* A Form emits many notification events. When it renders, it fires a {@link #PREPARE_FOR_RENDER} notification, followed
* by a {@link #PREPARE} notification.
* <p/>
* When the form is submitted, the component emits several notifications: first a {@link #PREPARE_FOR_SUBMIT}, then a
* {@link #PREPARE}: these allow the page to update its state as necessary to prepare for the form submission, then
* (after components enclosed by the form have operated), a {@link #VALIDATE_FORM}event is emitted, to allow for
* cross-form validation. After that, either a {@link #SUCCESS} OR {@link #FAILURE} event (depending on whether the
* {@link ValidationTracker} has recorded any errors). Lastly, a {@link #SUBMIT} event, for any listeners that care only
* about form submission, regardless of success or failure.
* <p/>
* For all of these notifications, the event context is derived from the <strong>context</strong> parameter. This
* context is encoded into the form's action URI (the parameter is not read when the form is submitted, instead the
* values encoded into the form are used).
*/
public class Form implements ClientElement, FormValidationControl
{
/**
* Invoked before {@link #PREPARE} when rendering out the form.
*/
public static final String PREPARE_FOR_RENDER = "prepareForRender";
/**
* Invoked before {@link #PREPARE} when the form is submitted.
*/
public static final String PREPARE_FOR_SUBMIT = "prepareForSubmit";
/**
* Invoked to let the containing component(s) prepare for the form rendering or the form submission.
*/
public static final String PREPARE = "prepare";
/**
* Event type for a notification after the form has submitted. This event notification occurs on any form submit,
* without respect to "success" or "failure".
*/
public static final String SUBMIT = "submit";
/**
* Event type for a notification to perform validation of submitted data. This allows a listener to perform
* cross-field validation. This occurs before the {@link #SUCCESS} or {@link #FAILURE} notification.
*/
public static final String VALIDATE_FORM = "validateForm";
/**
* Event type for a notification after the form has submitted, when there are no errors in the validation tracker.
* This occurs before the {@link #SUBMIT} event.
*/
public static final String SUCCESS = "success";
/**
* Event type for a notification after the form has been submitted, when there are errors in the validation tracker.
* This occurs before the {@link #SUBMIT} event.
*/
public static final String FAILURE = "failure";
/**
* Query parameter name storing form data (the serialized commands needed to process a form submission).
*/
public static final String FORM_DATA = "t:formdata";
/**
* The context for the link (optional parameter). This list of values will be converted into strings and included in
* the URI. The strings will be coerced back to whatever their values are and made available to event handler
* methods.
*/
@Parameter
private List<?> context;
/**
* The object which will record user input and validation errors. The object must be persistent between requests
* (since the form submission and validation occurs in an component event request and the subsequent render occurs
* in a render request). The default is a persistent property of the Form component and this is sufficient for
* nearly all purposes (except when a Form is rendered inside a loop).
*/
@Parameter("defaultTracker")
private ValidationTracker tracker;
/**
* If true (the default) then client validation is enabled for the form, and the default set of JavaScript libraries
* (Prototype, Scriptaculous and the Tapestry library) will be added to the rendered page, and the form will
* register itself for validation. This may be turned off when client validation is not desired; for example, when
* many validations are used that do not operate on the client side at all.
*/
@Parameter("true")
private boolean clientValidation;
/**
* Binding the zone parameter will cause the form submission to be handled as an Ajax request that updates the
* indicated zone. Often a Form will update the same zone that contains it.
*/
@Parameter(defaultPrefix = BindingConstants.LITERAL)
private String zone;
@Inject
private Environment environment;
@Inject
private ComponentResources resources;
@Environmental
private RenderSupport renderSupport;
@Inject
private Request request;
@Inject
private ComponentSource source;
@Persist(PersistenceConstants.FLASH)
private ValidationTracker defaultTracker;
@Inject
private ComponentInvocationMap componentInvocationMap;
private FormSupportImpl formSupport;
private Element form;
private Element div;
// Collects a stream of component actions. Each action goes in as a UTF string (the component
// component id), followed by a ComponentAction
private Base64ObjectOutputStream actions;
@SuppressWarnings("unused")
@Mixin
private RenderInformals renderInformals;
/**
* Set up via the traditional or Ajax component event request handler
*/
@Environmental
private ComponentEventResultProcessor componentEventResultProcessor;
@Environmental
private ClientBehaviorSupport clientBehaviorSupport;
private String name;
public ValidationTracker getDefaultTracker()
{
if (defaultTracker == null) defaultTracker = new ValidationTrackerImpl();
return defaultTracker;
}
public void setDefaultTracker(ValidationTracker defaultTracker)
{
this.defaultTracker = defaultTracker;
}
void beginRender(MarkupWriter writer)
{
try
{
actions = new Base64ObjectOutputStream();
}
catch (IOException ex)
{
throw new RuntimeException(ex);
}
name = renderSupport.allocateClientId(resources);
formSupport = new FormSupportImpl(name, actions, clientBehaviorSupport, clientValidation);
if (zone != null) clientBehaviorSupport.linkZone(name, zone);
// TODO: Forms should not allow to nest. Perhaps a set() method instead of a push() method
// for this kind of check?
environment.push(FormSupport.class, formSupport);
environment.push(ValidationTracker.class, tracker);
// Now that the environment is setup, inform the component or other listeners that the form
// is about to render.
Object[] contextArray = context == null ? new Object[0] : context.toArray();
resources.triggerEvent(PREPARE_FOR_RENDER, contextArray, null);
resources.triggerEvent(PREPARE, contextArray, null);
Link link = resources.createActionLink(EventConstants.ACTION, true, contextArray);
// Save the form element for later, in case we want to write an encoding type attribute.
form = writer
.element("form", "name", name, "id", name, "method", "post", "action", link);
componentInvocationMap.store(form, link);
resources.renderInformalParameters(writer);
div = writer.element("div", "class", CSSClassConstants.INVISIBLE);
for (String parameterName : link.getParameterNames())
{
String value = link.getParameterValue(parameterName);
writer.element("input", "type", "hidden", "name", parameterName, "value", value);
writer.end();
}
writer.end(); // div
environment.peek(Heartbeat.class).begin();
}
void afterRender(MarkupWriter writer)
{
environment.peek(Heartbeat.class).end();
formSupport.executeDeferred();
String encodingType = formSupport.getEncodingType();
if (encodingType != null) form.forceAttributes("enctype", encodingType);
writer.end(); // form
// Now, inject into the div the remaining hidden field (the list of actions).
try
{
actions.close();
}
catch (IOException ex)
{
throw new RuntimeException(ex);
}
div.element("input",
"type", "hidden",
"name", FORM_DATA,
"value", actions.toBase64());
}
void cleanupRender()
{
environment.pop(FormSupport.class);
formSupport = null;
// This forces a change to the tracker, which is nice because its internal state has
// changed.
tracker = environment.pop(ValidationTracker.class);
}
@SuppressWarnings({ "unchecked", "InfiniteLoopStatement" })
Object onAction(EventContext context) throws IOException
{
tracker.clear();
formSupport = new FormSupportImpl();
environment.push(ValidationTracker.class, tracker);
environment.push(FormSupport.class, formSupport);
Heartbeat heartbeat = new HeartbeatImpl();
environment.push(Heartbeat.class, heartbeat);
heartbeat.begin();
try
{
ComponentResultProcessorWrapper callback = new ComponentResultProcessorWrapper(
componentEventResultProcessor);
resources.triggerContextEvent(PREPARE_FOR_SUBMIT, context, callback);
if (callback.isAborted()) return true;
resources.triggerContextEvent(PREPARE, context, callback);
if (callback.isAborted()) return true;
executeStoredActions();
heartbeat.end();
ValidationTracker tracker = environment.peek(ValidationTracker.class);
// Let the listeners peform any final validations
// Update through the parameter because the tracker has almost certainly changed
// internal state.
this.tracker = tracker;
resources.triggerContextEvent(VALIDATE_FORM, context, callback);
if (callback.isAborted()) return true;
formSupport.executeDeferred();
// Let the listeners know about overall success or failure. Most listeners fall into
// one of those two camps.
// If the tracker has no errors, then clear it of any input values
// as well, so that the next page render will be "clean" and show
// true persistent data, not value from the previous form submission.
if (!this.tracker.getHasErrors()) this.tracker.clear();
resources.triggerContextEvent(tracker.getHasErrors() ? FAILURE : SUCCESS, context, callback);
// Lastly, tell anyone whose interested that the form is completely submitted.
if (callback.isAborted()) return true;
resources.triggerContextEvent(SUBMIT, context, callback);
return callback.isAborted();
}
finally
{
environment.pop(Heartbeat.class);
environment.pop(FormSupport.class);
}
}
/**
* Pulls the stored actions out of the request, converts them from MIME stream back to object stream and then
* objects, and executes them.
*/
private void executeStoredActions()
{
String[] values = request.getParameters(FORM_DATA);
if (values == null) return;
// Due to Ajax (FormInjector) there may be multiple values here, so handle each one individually.
for (String actionsBase64 : values)
{
ObjectInputStream ois = null;
Component component = null;
try
{
ois = new Base64ObjectInputStream(actionsBase64);
while (true)
{
String componentId = ois.readUTF();
ComponentAction action = (ComponentAction) ois.readObject();
component = source.getComponent(componentId);
action.execute(component);
component = null;
}
}
catch (EOFException ex)
{
// Expected
}
catch (Exception ex)
{
Location location = component == null ? null : component.getComponentResources().getLocation();
throw new TapestryException(ex.getMessage(), location, ex);
}
finally
{
InternalUtils.close(ois);
}
}
}
public void recordError(String errorMessage)
{
ValidationTracker tracker = this.tracker;
tracker.recordError(errorMessage);
this.tracker = tracker;
}
public void recordError(Field field, String errorMessage)
{
ValidationTracker tracker = this.tracker;
tracker.recordError(field, errorMessage);
this.tracker = tracker;
}
public boolean getHasErrors()
{
return tracker.getHasErrors();
}
public boolean isValid()
{
return !tracker.getHasErrors();
}
// For testing:
void setTracker(ValidationTracker tracker)
{
this.tracker = tracker;
}
public void clearErrors()
{
tracker.clear();
}
/**
* Forms use the same value for their name and their id attribute.
*/
public String getClientId()
{
return name;
}
}