blob: 79776bc8df6df7d2d121b48352b7664b811e772e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.cocoon.forms.formmodel;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.apache.cocoon.forms.FormContext;
import org.apache.cocoon.forms.event.FormHandler;
import org.apache.cocoon.forms.event.ProcessingPhase;
import org.apache.cocoon.forms.event.ProcessingPhaseEvent;
import org.apache.cocoon.forms.event.ProcessingPhaseListener;
import org.apache.cocoon.forms.event.WidgetEvent;
import org.apache.cocoon.forms.event.WidgetEventMulticaster;
import org.apache.cocoon.forms.validation.ValidationError;
import org.apache.cocoon.forms.validation.ValidationErrorAware;
import org.apache.commons.collections.list.CursorableLinkedList;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
/**
* A widget that serves as a container for other widgets, the top-level widget in
* a form description file.
*
* @version $Id$
*/
public class Form extends AbstractContainerWidget
implements ValidationErrorAware {
/** Form parameter containing the submit widget's id */
public static final String SUBMIT_ID_PARAMETER = "forms_submit_id";
private static final String FORM_EL = "form";
private final FormDefinition definition;
/**
* If non-null, indicates that form processing should terminate at the end of the current phase.
* If true, interaction with the form is finished. It doesn't imply that the form is valid though.
* If false, interaction isn't finished and the form should be redisplayed (processing was triggered
* by e.g. and action or a field with event listeners).
*/
private Boolean endProcessing;
private Locale locale = Locale.getDefault();
private FormHandler formHandler;
private Widget submitWidget;
private boolean isValid;
private ProcessingPhaseListener listener;
//In the "readFromRequest" phase, events are buffered to ensure that all widgets had the chance
//to read their value before events get fired.
private boolean bufferEvents;
private CursorableLinkedList events;
/** Widgets that need to be updated in the client when in AJAX mode */
private Set updatedWidgets;
/** Widgets that have at least one descendant that has to be updated */
private Set childUpdatedWidgets;
/** Optional id which overrides the value from the form definition */
private String id;
public Form(FormDefinition definition) {
super(definition);
this.definition = definition;
this.listener = definition.getProcessingPhaseListener();
}
/**
* Initialize the form by recursively initializing all its children. Any events occuring within the
* initialization phase are buffered and fired after initialization is complete, so that any action
* from a widget on another one occurs after that other widget has been given the opportunity to
* initialize itself.
*/
public void initialize() {
try {
// Start buffering events
this.bufferEvents = true;
super.initialize();
// Fire events, still buffering them: this ensures they will be handled in the same
// order as they were added.
fireEvents();
} finally {
// Stop buffering events
this.bufferEvents = false;
}
}
public WidgetDefinition getDefinition() {
return this.definition;
}
/**
* Events produced by child widgets should not be fired immediately, but queued in order to ensure
* an overall consistency of the widget tree before being handled.
*
* @param event the event to queue
*/
public void addWidgetEvent(WidgetEvent event) {
if (this.bufferEvents) {
if (this.events == null) {
this.events = new CursorableLinkedList();
}
// FIXME: limit the number of events to detect recursive event loops ?
this.events.add(event);
} else {
// Send it right now
event.getSourceWidget().broadcastEvent(event);
}
}
/**
* Mark a widget as being updated. When it Ajax mode, only updated widgets will be redisplayed
*
* @param widget the updated widget
* @return <code>true</code> if this widget was added to the list (i.e. wasn't alredy marked for update)
*/
public boolean addWidgetUpdate(Widget widget) {
if (this.updatedWidgets != null) {
if (this.updatedWidgets.add(widget.getRequestParameterName())) {
// Wasn't already there: register parents
Widget parent = widget.getParent();
while (parent != this && parent != null) {
if (this.childUpdatedWidgets.add(parent.getRequestParameterName())) {
parent = parent.getParent();
} else {
// Parent already there, and therefore its own parents.
break;
}
}
return true;
}
}
return false;
}
public Set getUpdatedWidgetIds() {
return this.updatedWidgets;
}
public Set getChildUpdatedWidgetIds() {
return this.childUpdatedWidgets;
}
/**
* Fire the events that have been queued.
* Note that event handling can fire new events.
*/
private void fireEvents() {
if (this.events != null) {
try {
CursorableLinkedList.Cursor cursor = this.events.cursor();
while (cursor.hasNext()) {
WidgetEvent event = (WidgetEvent) cursor.next();
event.getSourceWidget().broadcastEvent(event);
if (formHandler != null) {
formHandler.handleEvent(event);
}
}
cursor.close();
} finally {
this.events.clear();
}
}
}
/**
* Inform the form that the values will be loaded.
*/
public void informStartLoadingModel() {
// nothing to do here
// TODO - we could remove this method?
}
/**
* Inform the form that the values are loaded.
*/
public void informEndLoadingModel() {
// Notify the end of the load phase
if (this.listener != null) {
this.listener.phaseEnded(new ProcessingPhaseEvent(this, ProcessingPhase.LOAD_MODEL));
}
}
/**
* Inform the form that the values will be saved.
*/
public void informStartSavingModel() {
// nothing to do here
// TODO - we could remove this method?
}
/**
* Inform the form that the values are saved.
*/
public void informEndSavingModel() {
// Notify the end of the save phase
if (this.listener != null) {
this.listener.phaseEnded(new ProcessingPhaseEvent(this, ProcessingPhase.SAVE_MODEL));
}
}
/**
* Get the locale to be used to process this form.
*
* @return the form's locale.
*/
public Locale getLocale() {
return this.locale;
}
/**
* Get the widget that triggered the current processing. Note that it can be any widget, and
* not necessarily an action or a submit.
*
* @return the widget that submitted this form.
*/
public Widget getSubmitWidget() {
return this.submitWidget;
}
/**
* Set the widget that triggered the current form processing.
*
* @param widget the widget
*/
public void setSubmitWidget(Widget widget) {
if (this.submitWidget == widget) {
return;
}
if (this.submitWidget != null) {
throw new IllegalStateException("Submit widget already set to " + this.submitWidget +
". Cannot set also " + widget);
}
// Check that the submit widget is active
if (widget.getCombinedState() != WidgetState.ACTIVE) {
throw new IllegalStateException("Widget " + widget + " that submitted the form is not active.");
}
// If the submit widget is not an action (e.g. a field with an event listener),
// we end form processing after the current phase and redisplay the form.
// Actions (including submits) will call endProcessing() themselves and it's their
// responsibility to indicate how form processing should continue.
if (!(widget instanceof Action)) {
endProcessing(true);
}
this.submitWidget = widget;
}
public boolean hasFormHandler() {
return (this.formHandler != null);
}
public void setFormHandler(FormHandler formHandler) {
this.formHandler = formHandler;
}
public void addProcessingPhaseListener(ProcessingPhaseListener listener) {
this.listener = WidgetEventMulticaster.add(this.listener, listener);
}
public void removeProcessingPhaseListener(ProcessingPhaseListener listener) {
this.listener = WidgetEventMulticaster.remove(this.listener, listener);
}
/**
* Processes a form submit. If the form is finished, i.e. the form should not be redisplayed to the user,
* then this method returns true, otherwise it returns false. To know if the form was sucessfully
* validated, use the {@link #isValid()} method.
* <p>
* Form processing consists in multiple steps:
* <ul>
* <li>all widgets read their value from the request (i.e.
* {@link #readFromRequest(FormContext)} is called recursively on
* the whole widget tree)
* <li>if there is an action event, call the FormHandler
* <li>perform validation.
* </ul>
* This processing can be interrupted by the widgets (or their event listeners) by calling
* {@link #endProcessing(boolean)}.
* <p>
* Note that this method is synchronized as a Form is not thread-safe. This should not be a
* bottleneck as such concurrent requests can only happen for a single user.
*/
public synchronized boolean process(FormContext formContext) {
// Is this an AJAX request?
if (formContext.getRequest().getParameter("cocoon-ajax") != null) {
this.updatedWidgets = new HashSet();
this.childUpdatedWidgets = new HashSet();
}
// Fire the binding phase events
fireEvents();
// setup processing
this.submitWidget = null;
this.locale = formContext.getLocale();
this.endProcessing = null;
this.isValid = false;
// Notify the end of the current phase
if (this.listener != null) {
this.listener.phaseEnded(new ProcessingPhaseEvent(this, ProcessingPhase.PROCESSING_INITIALIZE));
}
try {
// Start buffering events
this.bufferEvents = true;
this.submitWidget = null;
doReadFromRequest(formContext);
// Find the submit widget, if not an action
// This has to occur after reading from the request, to handle stateless forms
// where the submit widget is recreated when the request is read (e.g. a row-action).
// Note that we don't check this if the submit widget was already set, as it can cause problems
// if the user triggers submit with an input (which sets 'forms_submit_id'), then clicks back
// and submits using a regular submit button.
if (getSubmitWidget() == null) {
String submitId = formContext.getRequest().getParameter(SUBMIT_ID_PARAMETER);
if (!StringUtils.isEmpty(submitId)) {
// if the form has an ID, it is used as part of the submitId too and must be removed
if(!StringUtils.isEmpty(this.getId())) {
submitId = submitId.substring(submitId.indexOf('.')+1);
}
Widget submit = this.lookupWidget(submitId.replace('.', '/'));
if (submit == null) {
throw new IllegalArgumentException("Invalid submit id (no such widget): " + submitId);
}
setSubmitWidget(submit);
}
}
// Fire events, still buffering them: this ensures they will be handled in the same
// order as they were added.
fireEvents();
} finally {
// No need for buffering in the following phases
this.bufferEvents = false;
}
// Notify the end of the current phase
if (this.listener != null) {
this.listener.phaseEnded(new ProcessingPhaseEvent(this, ProcessingPhase.READ_FROM_REQUEST));
}
if (this.endProcessing != null) {
return this.endProcessing.booleanValue();
}
return validate();
}
/**
* End the current form processing after the current phase.
*
* @param redisplayForm indicates if the form should be redisplayed to the user.
*/
public void endProcessing(boolean redisplayForm) {
// Set the indicator that terminates the form processing.
// If redisplayForm is true, interaction is not finished and process() must
// return false, hence the negation below.
this.endProcessing = BooleanUtils.toBooleanObject(!redisplayForm);
}
/**
* Was form validation successful ?
*
* @return <code>true</code> if the form was successfully validated.
*/
public boolean isValid() {
return this.isValid;
}
public void readFromRequest(FormContext formContext) {
throw new UnsupportedOperationException("Please use Form.process()");
}
private void doReadFromRequest(FormContext formContext) {
// let all individual widgets read their value from the request object
super.readFromRequest(formContext);
}
/**
* Set a validation error on this field. This allows the form to be externally marked as invalid by
* application logic.
*
* @return the validation error
*/
public ValidationError getValidationError() {
return this.validationError;
}
/**
* set a validation error
*/
public void setValidationError(ValidationError error) {
this.validationError = error;
}
/**
* Performs validation phase of form processing.
*/
public boolean validate() {
// Validate the form
this.isValid = super.validate();
// FIXME: Is this check needed, before invoking the listener?
if (this.endProcessing != null) {
this.wasValid = this.endProcessing.booleanValue();
return this.wasValid;
}
// Notify the end of the current phase
if (this.listener != null) {
this.listener.phaseEnded(new ProcessingPhaseEvent(this, ProcessingPhase.VALIDATE));
}
if (this.endProcessing != null) {
// De-validate the form if one of the listeners asked to end the processing
// This allows for additional application-level validation.
this.isValid = false;
this.wasValid = this.endProcessing.booleanValue();
return this.wasValid;
}
this.wasValid = this.isValid && this.validationError == null;
return this.wasValid;
}
public String getXMLElementName() {
return FORM_EL;
}
/**
* @see org.apache.cocoon.forms.formmodel.AbstractWidget#getId()
*/
public String getId() {
if (this.id != null) {
return this.id;
}
return super.getId();
}
/**
* Set the optional id.
* @param value A new id.
*/
public void setId(String value) {
this.id = value;
}
}