| /* |
| * 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; |
| } |
| } |