001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package org.apache.cocoon.forms.formmodel;
018:
019: import java.util.HashSet;
020: import java.util.Locale;
021: import java.util.Set;
022:
023: import org.apache.cocoon.forms.FormContext;
024: import org.apache.cocoon.forms.event.FormHandler;
025: import org.apache.cocoon.forms.event.ProcessingPhase;
026: import org.apache.cocoon.forms.event.ProcessingPhaseEvent;
027: import org.apache.cocoon.forms.event.ProcessingPhaseListener;
028: import org.apache.cocoon.forms.event.WidgetEvent;
029: import org.apache.cocoon.forms.event.WidgetEventMulticaster;
030: import org.apache.cocoon.forms.validation.ValidationError;
031: import org.apache.cocoon.forms.validation.ValidationErrorAware;
032: import org.apache.commons.collections.list.CursorableLinkedList;
033: import org.apache.commons.lang.BooleanUtils;
034: import org.apache.commons.lang.StringUtils;
035:
036: /**
037: * A widget that serves as a container for other widgets, the top-level widget in
038: * a form description file.
039: *
040: * @version $Id: Form.java 475908 2006-11-16 20:15:45Z vgritsenko $
041: */
042: public class Form extends AbstractContainerWidget implements
043: ValidationErrorAware {
044:
045: /** Form parameter containing the submit widget's id */
046: public static final String SUBMIT_ID_PARAMETER = "forms_submit_id";
047:
048: private static final String FORM_EL = "form";
049:
050: private final FormDefinition definition;
051:
052: /**
053: * If non-null, indicates that form processing should terminate at the end of the current phase.
054: * If true, interaction with the form is finished. It doesn't imply that the form is valid though.
055: * If false, interaction isn't finished and the form should be redisplayed (processing was triggered
056: * by e.g. and action or a field with event listeners).
057: */
058: private Boolean endProcessing;
059: private Locale locale = Locale.getDefault();
060: private FormHandler formHandler;
061: private Widget submitWidget;
062: private boolean isValid;
063: private ProcessingPhaseListener listener;
064:
065: //In the "readFromRequest" phase, events are buffered to ensure that all widgets had the chance
066: //to read their value before events get fired.
067: private boolean bufferEvents;
068: private CursorableLinkedList events;
069:
070: /** Widgets that need to be updated in the client when in AJAX mode */
071: private Set updatedWidgets;
072:
073: /** Widgets that have at least one descendant that has to be updated */
074: private Set childUpdatedWidgets;
075:
076: /** Optional id which overrides the value from the form definition */
077: private String id;
078:
079: public Form(FormDefinition definition) {
080: super (definition);
081: this .definition = definition;
082: this .listener = definition.getProcessingPhaseListener();
083: }
084:
085: /**
086: * Initialize the form by recursively initializing all its children. Any events occuring within the
087: * initialization phase are buffered and fired after initialization is complete, so that any action
088: * from a widget on another one occurs after that other widget has been given the opportunity to
089: * initialize itself.
090: */
091: public void initialize() {
092: try {
093: // Start buffering events
094: this .bufferEvents = true;
095: super .initialize();
096: // Fire events, still buffering them: this ensures they will be handled in the same
097: // order as they were added.
098: fireEvents();
099: } finally {
100: // Stop buffering events
101: this .bufferEvents = false;
102: }
103: }
104:
105: public WidgetDefinition getDefinition() {
106: return this .definition;
107: }
108:
109: /**
110: * Events produced by child widgets should not be fired immediately, but queued in order to ensure
111: * an overall consistency of the widget tree before being handled.
112: *
113: * @param event the event to queue
114: */
115: public void addWidgetEvent(WidgetEvent event) {
116:
117: if (this .bufferEvents) {
118: if (this .events == null) {
119: this .events = new CursorableLinkedList();
120: }
121:
122: // FIXME: limit the number of events to detect recursive event loops ?
123: this .events.add(event);
124: } else {
125: // Send it right now
126: event.getSourceWidget().broadcastEvent(event);
127: }
128: }
129:
130: /**
131: * Mark a widget as being updated. When it Ajax mode, only updated widgets will be redisplayed
132: *
133: * @param widget the updated widget
134: * @return <code>true</code> if this widget was added to the list (i.e. wasn't alredy marked for update)
135: */
136: public boolean addWidgetUpdate(Widget widget) {
137: if (this .updatedWidgets != null) {
138: if (this .updatedWidgets.add(widget
139: .getRequestParameterName())) {
140: // Wasn't already there: register parents
141: Widget parent = widget.getParent();
142: while (parent != this && parent != null) {
143: if (this .childUpdatedWidgets.add(parent
144: .getRequestParameterName())) {
145: parent = parent.getParent();
146: } else {
147: // Parent already there, and therefore its own parents.
148: break;
149: }
150: }
151: return true;
152: }
153: }
154: return false;
155: }
156:
157: public Set getUpdatedWidgetIds() {
158: return this .updatedWidgets;
159: }
160:
161: public Set getChildUpdatedWidgetIds() {
162: return this .childUpdatedWidgets;
163: }
164:
165: /**
166: * Fire the events that have been queued.
167: * Note that event handling can fire new events.
168: */
169: private void fireEvents() {
170: if (this .events != null) {
171: try {
172: CursorableLinkedList.Cursor cursor = this .events
173: .cursor();
174: while (cursor.hasNext()) {
175: WidgetEvent event = (WidgetEvent) cursor.next();
176: event.getSourceWidget().broadcastEvent(event);
177: if (formHandler != null) {
178: formHandler.handleEvent(event);
179: }
180: }
181: cursor.close();
182: } finally {
183: this .events.clear();
184: }
185: }
186: }
187:
188: /**
189: * Inform the form that the values will be loaded.
190: */
191: public void informStartLoadingModel() {
192: // nothing to do here
193: // TODO - we could remove this method?
194: }
195:
196: /**
197: * Inform the form that the values are loaded.
198: */
199: public void informEndLoadingModel() {
200: // Notify the end of the load phase
201: if (this .listener != null) {
202: this .listener.phaseEnded(new ProcessingPhaseEvent(this ,
203: ProcessingPhase.LOAD_MODEL));
204: }
205: }
206:
207: /**
208: * Inform the form that the values will be saved.
209: */
210: public void informStartSavingModel() {
211: // nothing to do here
212: // TODO - we could remove this method?
213: }
214:
215: /**
216: * Inform the form that the values are saved.
217: */
218: public void informEndSavingModel() {
219: // Notify the end of the save phase
220: if (this .listener != null) {
221: this .listener.phaseEnded(new ProcessingPhaseEvent(this ,
222: ProcessingPhase.SAVE_MODEL));
223: }
224: }
225:
226: /**
227: * Get the locale to be used to process this form.
228: *
229: * @return the form's locale.
230: */
231: public Locale getLocale() {
232: return this .locale;
233: }
234:
235: /**
236: * Get the widget that triggered the current processing. Note that it can be any widget, and
237: * not necessarily an action or a submit.
238: *
239: * @return the widget that submitted this form.
240: */
241: public Widget getSubmitWidget() {
242: return this .submitWidget;
243: }
244:
245: /**
246: * Set the widget that triggered the current form processing.
247: *
248: * @param widget the widget
249: */
250: public void setSubmitWidget(Widget widget) {
251: if (this .submitWidget == widget) {
252: return;
253: }
254:
255: if (this .submitWidget != null) {
256: throw new IllegalStateException(
257: "Submit widget already set to " + this .submitWidget
258: + ". Cannot set also " + widget);
259: }
260:
261: // Check that the submit widget is active
262: if (widget.getCombinedState() != WidgetState.ACTIVE) {
263: throw new IllegalStateException("Widget " + widget
264: + " that submitted the form is not active.");
265: }
266:
267: // If the submit widget is not an action (e.g. a field with an event listener),
268: // we end form processing after the current phase and redisplay the form.
269: // Actions (including submits) will call endProcessing() themselves and it's their
270: // responsibility to indicate how form processing should continue.
271: if (!(widget instanceof Action)) {
272: endProcessing(true);
273: }
274: this .submitWidget = widget;
275: }
276:
277: public boolean hasFormHandler() {
278: return (this .formHandler != null);
279: }
280:
281: public void setFormHandler(FormHandler formHandler) {
282: this .formHandler = formHandler;
283: }
284:
285: public void addProcessingPhaseListener(
286: ProcessingPhaseListener listener) {
287: this .listener = WidgetEventMulticaster.add(this .listener,
288: listener);
289: }
290:
291: public void removeProcessingPhaseListener(
292: ProcessingPhaseListener listener) {
293: this .listener = WidgetEventMulticaster.remove(this .listener,
294: listener);
295: }
296:
297: /**
298: * Processes a form submit. If the form is finished, i.e. the form should not be redisplayed to the user,
299: * then this method returns true, otherwise it returns false. To know if the form was sucessfully
300: * validated, use the {@link #isValid()} method.
301: * <p>
302: * Form processing consists in multiple steps:
303: * <ul>
304: * <li>all widgets read their value from the request (i.e.
305: * {@link #readFromRequest(FormContext)} is called recursively on
306: * the whole widget tree)
307: * <li>if there is an action event, call the FormHandler
308: * <li>perform validation.
309: * </ul>
310: * This processing can be interrupted by the widgets (or their event listeners) by calling
311: * {@link #endProcessing(boolean)}.
312: * <p>
313: * Note that this method is synchronized as a Form is not thread-safe. This should not be a
314: * bottleneck as such concurrent requests can only happen for a single user.
315: */
316: public synchronized boolean process(FormContext formContext) {
317: // Is this an AJAX request?
318: if (formContext.getRequest().getParameter("cocoon-ajax") != null) {
319: this .updatedWidgets = new HashSet();
320: this .childUpdatedWidgets = new HashSet();
321: }
322:
323: // Fire the binding phase events
324: fireEvents();
325:
326: // setup processing
327: this .submitWidget = null;
328: this .locale = formContext.getLocale();
329: this .endProcessing = null;
330: this .isValid = false;
331:
332: // Notify the end of the current phase
333: if (this .listener != null) {
334: this .listener.phaseEnded(new ProcessingPhaseEvent(this ,
335: ProcessingPhase.PROCESSING_INITIALIZE));
336: }
337:
338: try {
339: // Start buffering events
340: this .bufferEvents = true;
341: this .submitWidget = null;
342:
343: doReadFromRequest(formContext);
344:
345: // Find the submit widget, if not an action
346: // This has to occur after reading from the request, to handle stateless forms
347: // where the submit widget is recreated when the request is read (e.g. a row-action).
348:
349: // Note that we don't check this if the submit widget was already set, as it can cause problems
350: // if the user triggers submit with an input (which sets 'forms_submit_id'), then clicks back
351: // and submits using a regular submit button.
352: if (getSubmitWidget() == null) {
353: String submitId = formContext.getRequest()
354: .getParameter(SUBMIT_ID_PARAMETER);
355: if (!StringUtils.isEmpty(submitId)) {
356: // if the form has an ID, it is used as part of the submitId too and must be removed
357: if (!StringUtils.isEmpty(this .getId())) {
358: submitId = submitId.substring(submitId
359: .indexOf('.') + 1);
360: }
361: Widget submit = this .lookupWidget(submitId.replace(
362: '.', '/'));
363: if (submit == null) {
364: throw new IllegalArgumentException(
365: "Invalid submit id (no such widget): "
366: + submitId);
367: }
368: setSubmitWidget(submit);
369: }
370: }
371:
372: // Fire events, still buffering them: this ensures they will be handled in the same
373: // order as they were added.
374: fireEvents();
375:
376: } finally {
377: // No need for buffering in the following phases
378: this .bufferEvents = false;
379: }
380:
381: // Notify the end of the current phase
382: if (this .listener != null) {
383: this .listener.phaseEnded(new ProcessingPhaseEvent(this ,
384: ProcessingPhase.READ_FROM_REQUEST));
385: }
386:
387: if (this .endProcessing != null) {
388: return this .endProcessing.booleanValue();
389: }
390:
391: return validate();
392: }
393:
394: /**
395: * End the current form processing after the current phase.
396: *
397: * @param redisplayForm indicates if the form should be redisplayed to the user.
398: */
399: public void endProcessing(boolean redisplayForm) {
400: // Set the indicator that terminates the form processing.
401: // If redisplayForm is true, interaction is not finished and process() must
402: // return false, hence the negation below.
403: this .endProcessing = BooleanUtils
404: .toBooleanObject(!redisplayForm);
405: }
406:
407: /**
408: * Was form validation successful ?
409: *
410: * @return <code>true</code> if the form was successfully validated.
411: */
412: public boolean isValid() {
413: return this .isValid;
414: }
415:
416: public void readFromRequest(FormContext formContext) {
417: throw new UnsupportedOperationException(
418: "Please use Form.process()");
419: }
420:
421: private void doReadFromRequest(FormContext formContext) {
422: // let all individual widgets read their value from the request object
423: super .readFromRequest(formContext);
424: }
425:
426: /**
427: * Set a validation error on this field. This allows the form to be externally marked as invalid by
428: * application logic.
429: *
430: * @return the validation error
431: */
432: public ValidationError getValidationError() {
433: return this .validationError;
434: }
435:
436: /**
437: * set a validation error
438: */
439: public void setValidationError(ValidationError error) {
440: this .validationError = error;
441: }
442:
443: /**
444: * Performs validation phase of form processing.
445: */
446: public boolean validate() {
447: // Validate the form
448: this .isValid = super .validate();
449:
450: // FIXME: Is this check needed, before invoking the listener?
451: if (this .endProcessing != null) {
452: this .wasValid = this .endProcessing.booleanValue();
453: return this .wasValid;
454: }
455:
456: // Notify the end of the current phase
457: if (this .listener != null) {
458: this .listener.phaseEnded(new ProcessingPhaseEvent(this ,
459: ProcessingPhase.VALIDATE));
460: }
461:
462: if (this .endProcessing != null) {
463: // De-validate the form if one of the listeners asked to end the processing
464: // This allows for additional application-level validation.
465: this .isValid = false;
466: this .wasValid = this .endProcessing.booleanValue();
467: return this .wasValid;
468: }
469:
470: this .wasValid = this .isValid && this .validationError == null;
471: return this .wasValid;
472: }
473:
474: public String getXMLElementName() {
475: return FORM_EL;
476: }
477:
478: /**
479: * @see org.apache.cocoon.forms.formmodel.AbstractWidget#getId()
480: */
481: public String getId() {
482: if (this .id != null) {
483: return this .id;
484: }
485: return super .getId();
486: }
487:
488: /**
489: * Set the optional id.
490: * @param value A new id.
491: */
492: public void setId(String value) {
493: this.id = value;
494: }
495: }
|