From da809a9fc52961d968035768b5253cd982dd1cc7 Mon Sep 17 00:00:00 2001
From: jensp The following table lists all Bebop Link classes and suggests
+ * when they might be used.
+ *
+ * Generates a DOM fragment:
+ *
+ * This class is intended to be subclassed to provide
+ * the page infrastructure required by a project, for
+ * example, adding some navigation components.
+ * The SimplePage class provides a easy implementation
+ * whereby the navigation components can be specified
+ * in the enterprise.init file.
+ */
+public class BasePage extends Page {
+
+ public BasePage(String application,
+ Label title,
+ String id) {
+ super(title, new SimpleContainer());
+
+ Assert.exists(application, "application name");
+ setAttribute("application", application);
+
+ if (id != null) {
+ setAttribute("id", id);
+ }
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Bebop.java b/ccm-core/src/main/java/com/arsdigita/bebop/Bebop.java
new file mode 100644
index 000000000..af58f5e2f
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/Bebop.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import org.apache.log4j.Logger;
+
+/**
+ * @author Justin Ross
+ * @see com.arsdigita.bebop.BebopConfig
+ * @version $Id$
+ */
+public final class Bebop {
+
+ private static final Logger s_log = Logger.getLogger(Bebop.class);
+
+ private static BebopConfig s_config = BebopConfig.getInstance();
+
+ /**
+ * Gets the Generates DOM fragment:
+ * The position of the component within the cell can be influenced with the
+ * following constraints.
+ * Constraints can be combined by OR-ing them together. For example, to print
+ * a component in a row of its own, left-aligned, at the bottom of its cell,
+ * use the constraint Using the The following pseudo-code illustrates the example. (It assumes that
+ * Form and FormSection are decorators of the ColumnPanel.)
+ *
+ * Generates a DOM fragment:
+ * A component can be either visible or
+ * invisible. Invisible components do not produce any output and
+ * containers should be careful to completely hide their presence. The
+ * visibility of a component can be influenced in a number of ways:
+ * The {@link Page} makes sure that the visibility of components is
+ * preserved across repeated requests to the same page.
+ *
+ * Each component supports a few standard attributes that are copied
+ * through to the output when producing either HTML or XML
+ * output. These attributes are not used internally in any way, and setting
+ * them is entirely optional.
+ *
+ * The standard attributes appear in the output as attributes in
+ * the element generated from this component. They correspond directly to
+ * properties with setters and getters. The standard attributes are as folows.
+ *
+ * When extending any Adds a DOM subtree representing this component under the given
+ * parent node. Uses the request values stored in Responds to the request. This method is only called if the request
+ * was made from a link or form that the component put on the page in the
+ * {@link PageState#stateAsURL} previous request. No output should be generated on the HTTP response. The component
+ * can store intermediate results in the This method is called before any output is printed to the HTTP
+ * response so that the component can forward to a different page and
+ * thereby commit the response.
+ * This method returns Warning: Even though a control link lets you add action
+ * listeners, they are not run unless you override {@link #setControlEvent
+ * setControlEvent}. If you need this behavior, you should use an {@link
+ * ActionLink}. A control link is hardly ever useful unless it is contained
+ * in an event-generating component like {@link List} or {@link Table}.
+ *
+ * Example: A control link is mainly useful to send events to other
+ * components. For example, the following control link will cause a control
+ * event delete with associated value 42 to be sent to
+ * the component fooComponent when the user clicks on it:
+ *
+ * This requires that fooComponent is part of the page
+ * hierarchy. The control link l does not have to be part of the
+ * page hierarchy, and may be generated on the fly. (See
+ * {@link PageState} for details on control events.)
+ *
+ * See {@link BaseLink} for a description
+ * of all Bebop Link classes.
+ *
+ * @author Stanislav Freidin
+ * @author David Lutterkort
+ * @version $Id: ControlLink.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public class ControlLink extends BaseLink {
+
+ /**
+ * The XML type attribute for a {@link ControlLink}.
+ */
+ protected final String TYPE_CONTROL = "control";
+
+ /**
+ * A list of all action listeners. The list is instantiated lazily, and
+ * will therefore be null in most applications.
+ */
+ private ArrayList m_actionListeners;
+
+ /**
+ * Constructs a new ControlLink. The link will encapsulates
+ * the child component (which should be a label or an image).
+ *
+ * @param child the component that will be turned into a link
+ */
+ public ControlLink(Component child) {
+ super(child, "");
+ setTypeAttr(TYPE_CONTROL);
+ }
+
+ /**
+ * Constructs a new ControlLink with the given string label.
+ *
+ * @param label the string label for the link
+ */
+ public ControlLink(String label) {
+ this(new Label(label));
+ }
+
+ /**
+ * Adds an
+ * As an example, a form that accepts a first and last name may be set up as follows:
+ *
+ *
+ * This form automatically checks that the user supplied a last name. Only then does it call the
+ *
+ * Generates a DOM fragment:
+ *
+ *
+ * Determine whether or not this Form will redirect after its process listeners are fired. The basic task of a To perform the transformation, a separate instance of
+ * See the Forms API Developer Guide for details on using the
+ * Instances of this class provide a specification for transforming a
+ * set of key-value string pairs into a set of validated Java data
+ * objects.
+ * A single instance of this
+ * class can handle all submissions to a particular form.
+ * The most common usage for this class is
+ * is to use a private variable in a servlet to store the
+ * model, and to construct it in the servlet See the
+ * Forms API Developer Guide for details on using the
+ * This method is only package-friendly since it is only useful to
+ * the Page class. Everybody else should be happy with the public
+ * constructor.
+ *
+ * @param name a URL-encoded keyword used to identify this form model
+ *
+ * @param defaultOverridesNull Initialization events occur when a form is initially
+ * requested by the user, but not when the form is subsequently
+ * submitted. They typically
+ * perform actions such as querying the database for existed values
+ * to set up an edit form or obtaining a sequence value to set up a
+ * create form.
+ * @param listener an instance of a class that implements the
+ * Process events
+ * only occur after a form submission has been successfully
+ * validated. They are typically used to perform a database
+ * transaction or other operation based on the submitted data.
+ * Process listeners are executed in the order in which they are
+ * added.
+ *
+ * @param listener an instance of a class that implements the
+ * If this is a submission, validates the data and (if the
+ * data is valid) calls the process listeners. Returns a FormData object.
+ *
+ * @param state the PageState object holding request-specific information
+ * @return a FormData object.
+ * */
+ public FormData process(PageState state) throws FormProcessException {
+ Assert.isLocked(this);
+ boolean isSubmission =
+ state.getRequest().getParameter(getMagicTagName()) != null;
+ return process(state, isSubmission);
+ }
+
+ /**
+ * Creates a new FormData object that is populated with default values
+ * (for an initial request) or values from the request (for a
+ * submission).
+ * If this is a submission, validates the data and (if the
+ * data is valid) calls the process listeners. Returns a FormData object.
+ *
+ * @param state the PageState object holding request specific information
+ * @param isSubmission Form processing is performed
+ * after the form has been validated. Form validation is performed
+ * after the initial transformation of key-value string
+ * pairs into Java data objects is complete.
+ *
+ * @param state the page state for this request
+ *
+ * @param data the FormData object to validate
+ *
+ * @pre data != null
+ * */
+ void validate(PageState state, FormData data) {
+ Assert.exists(data, "FormData");
+ if (!data.isTransformed()) {
+ throw new IllegalStateException("Request data must be transformed " + "prior to running validation filters.");
+ }
+ fireParameterValidation(new FormSectionEvent(this, state, data));
+ fireFormValidation(new FormSectionEvent(this, state, data));
+ }
+
+ /**
+ * Merge the parameterModels and Listeners from the supplied
+ * FormModel into the current FormModel. This method is useful when
+ * registering FormSections in Forms.
+ *
+ * @param m The FormModel to be merged into this FormModel
+ * */
+ void mergeModel(FormModel m) {
+ Assert.isUnlocked(this);
+ Assert.exists(m, "FormSection's FormModel");
+ m_parameterModels.addAll(m.m_parameterModels);
+ m_listenerList.addAll(m.m_listenerList);
+ }
+
+ /**
+ * Locks this FormModel and all of its ParameterModels.
+ * */
+ public void lock() {
+ for (Iterator i = getParameters(); i.hasNext(); ) {
+ ((ParameterModel) i.next()).lock();
+ }
+ m_locked = true;
+ }
+
+ /**
+ * Checks whether this FormModel is locked.
+ *
+ * @return Since this class is a subclass of Returns the concatenation of {@link #getMessage()} and {@link
+ * #getRootCause()}. Since a Process events
+ * only occur after a form submission has been successfully
+ * validated. They are typically used to perform a database
+ * transaction or other operation based on the submitted data.
+ * Process listeners are executed in the order in which
+ * they are added.
+ *
+ * @param listener an instance of a class that implements the
+ * This method generates DOM to be used with the XSLT template
+ * to produce the appropriate output. A container that prints its components in a table. Each child is
+ * printed in its own table cell. The number of columns can be
+ * specified in the constructor. The components are put into the table
+ * in the order in which they were added to the The position of the component within the cell can be influenced
+ * with the following constraints. Constraints can be combined by
+ * ORing them together. For example, to print a component in a row of its
+ * own, left-aligned, at the bottom of its cell, use the constraint
+ * Using the Generates a DOM fragment:
+ * Creates a new label with the specified text. Creates a new label with the specified text as GlobalizedMessage
+ * and output escaping turned on if
+ * If possible, derived classes should override {@link #getLabel()} instead,
+ * which is called from this method. As long as we don't have a static
+ * method to obtain ApplicationContext, this is a way to get the
+ * RequestContext (to determine the locale). When ApplicationContext gets
+ * available, that will become the suggested way for overriding code to get
+ * context.
+ *
+ * @param state the current page state
+ * @return the string produced for this label
+ */
+ public String getLabel(PageState state) {
+ return (String) getGlobalizedMessage(state).localize(state.getRequest());
+ }
+
+ // /**
+ // * .
+ // *
+ // * This method may be overridden to dynamically generate the default text of
+ // * the label.
+ // *
+ // * @return the string produced for this label.
+ // *
+ // * @deprecated Use {@link #getGlobalizedMessage()}
+ // */
+ // Conflicts with Super's getLabel message of type GlobalizedMessage. But isn't
+ // needed anyway. Should deleted as soon as the refactoring of Label is
+ // completed (i.e. any string Label ironed out).
+ // public String getLabel() {
+ // return getGlobalizedMessage().getKey();
+ // }
+
+ /**
+ * This should really be getLabel(), but since it was marked STABLE I
+ * can't change its return type. This should really be getLabel(), but since it was marked STABLE I
+ * can't change its return type. Vertical List layout. Horizontal List layout. Determines the new selected element and fires a {@link
+ * ChangeEvent} if it has changed. After that, fires an {@link
+ * ActionEvent}.
+ *
+ * @param state the state of the current request
+ * @throws ServletException if the control event is unknown.
+ * @pre state != null
+ * @see #fireStateChanged fireStateChanged
+ * @see #fireActionEvent fireActionEvent
+ */
+ public void respond(PageState state) throws ServletException {
+ String event = state.getControlEventName();
+
+ if ( SELECT_EVENT.equals(event) ) {
+ setSelectedKey(state, state.getControlEventValue());
+ } else {
+ throw new ServletException("Unknown event '" + event + "'");
+ }
+ fireActionEvent(state);
+ }
+
+ /**
+ * Generates XML representing the items in the list. The items are
+ * formatted using a {@link ListCellRenderer}. The XML that is generated has the following form:
+ * Retrieve the current List layout. Set the current List layout.SingleSelectionModel and
+ * Lockable. Those wishing to define a SingleSelectionModel
+ * will ordinarily want to extend this class.
+ *
+ * @version $Id: AbstractSingleSelectionModel.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public abstract class AbstractSingleSelectionModel
+ implements SingleSelectionModel, Lockable {
+
+ private EventListenerList m_listeners;
+ private boolean m_locked;
+
+ /** Creates a new AbstractSingleSelectionModel.
+ */
+ public AbstractSingleSelectionModel() {
+ m_listeners = new EventListenerList();
+ }
+
+ /**
+ * Returns true if there is a selected element.
+ *
+ * @param state the state of the current request
+ * @return true if there is a selected component;
+ * false otherwise.
+ */
+ public boolean isSelected(PageState state) {
+ return getSelectedKey(state) != null;
+ }
+
+ public abstract Object getSelectedKey(PageState state);
+
+ public abstract void setSelectedKey(PageState state, Object key);
+
+ public void clearSelection(PageState state) {
+ setSelectedKey(state, null);
+ }
+
+ // Selection change events
+
+ public void addChangeListener(ChangeListener l) {
+ Assert.isUnlocked(this);
+ m_listeners.add(ChangeListener.class, l);
+ }
+
+ public void removeChangeListener(ChangeListener l) {
+ Assert.isUnlocked(this);
+ m_listeners.remove(ChangeListener.class, l);
+ }
+
+ protected void fireStateChanged(PageState state) {
+ Iterator i = m_listeners.getListenerIterator(ChangeListener.class);
+ ChangeEvent e = null;
+
+ while (i.hasNext()) {
+ if ( e == null ) {
+ e = new ChangeEvent(this, state);
+ }
+ ((ChangeListener) i.next()).stateChanged(e);
+ }
+ }
+
+ // implement Lockable
+ public void lock() {
+ m_locked = true;
+ }
+
+ public final boolean isLocked() {
+ return m_locked;
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/BaseLink.java b/ccm-core/src/main/java/com/arsdigita/bebop/BaseLink.java
new file mode 100755
index 000000000..7f0127a9e
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/BaseLink.java
@@ -0,0 +1,545 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.bebop.event.PrintEvent;
+import com.arsdigita.bebop.event.PrintListener;
+import com.arsdigita.globalization.GlobalizedMessage;
+import com.arsdigita.util.Assert;
+import com.arsdigita.util.UncheckedWrapperException;
+import com.arsdigita.xml.Element;
+import java.util.TooManyListenersException;
+
+/**
+ * The parent of all Bebop Link classes, this class represents a URL on a page.
+ * It may contain a label, an image, or any other component.
+ *
+ *
+ *
+ *
+ * @version $Id: BaseLink.java 998 2005-11-15 22:27:13Z sskracic $
+ */
+public abstract class BaseLink extends DescriptiveComponent
+ implements Cloneable {
+
+ /** The name of the attribute used in XML to indicate which type of link
+ * this link represents. */
+ private final static String TYPE_ATTR = "type";
+ private final static String HREF_NO_JAVASCRIPT = "href_no_javascript";
+ private final static String HREF = "href";
+
+ /** Component used to display the link. Typically a Label, may be
+ * e.g. an image as well. */
+ protected Component m_child;
+
+ /** Property to store the url the Link points to. */
+ protected String m_url;
+
+// Use the parent class' property!
+// /** Property to store informational text for the user about the Link, e.g.
+// * how to use it, or when to use it (or not to use it). */
+// private GlobalizedMessage m_hint;
+
+ protected String m_noJavascriptURL = null;
+
+ private PrintListener m_printListener;
+
+ private String m_sConfirmMsg = "";
+ private GlobalizedMessage m_confirmMsg;
+
+ /**
+ * Constructor creates a link taking url as the target and display it to
+ * the user at the same time. It is the only allowed way to present the
+ * user with a not globlized information. The implementation currently
+ * miss-uses the Label component to display just a not globalized String
+ * which is deprecated.
+ *
+ * @param url
+ * @deprecated use BaseLink(Component,url) instead with a Label using a
+ * GlobalizedMessage instead
+ */
+ public BaseLink(final String url) {
+ this(new Label(url), url);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param child display component (Label, Image, etc.)
+ * @param url URL to point at
+ */
+ public BaseLink(final Component child, final String url) {
+ super();
+ m_url = url;
+ m_child = child;
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param child display component (Label, Image, etc.)
+ * @param listener PrintListener, may be used to change either the Display
+ * text or the url within a locked page.
+ */
+ public BaseLink(final Component child, final PrintListener listener) {
+ this(child, "");
+ try {
+ addPrintListener(listener);
+ } catch (TooManyListenersException e) {
+ // Can't happen
+ throw new UncheckedWrapperException("Too many listeners: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param listener
+ */
+ public BaseLink(final PrintListener listener) {
+ this("", listener);
+ }
+
+ // DEPRECATED constructors
+
+ /**
+ * Constructor.
+ *
+ * @param label as text
+ * @param url
+ * @deprecated use BaseLink(Component,url) instead with a Label using a
+ * GlobalizedMessage instead
+ */
+ public BaseLink(final String label, final String url) {
+ this(new Label(label), url);
+ }
+
+ /**
+ * Constructor.
+ *
+ * @param label as text
+ * @param listener PrintListener, may be used to change either the Display
+ * text or the url within a locked page.
+ * @deprecated use BaseLink(Component,listener) instead with a Label using
+ * a GlobalizedMessage instead
+ */
+ public BaseLink(final String label, final PrintListener listener) {
+ this(new Label(label), listener);
+ }
+
+ // Class Methods
+
+ /**
+ * Clone.
+ * @return
+ * @throws CloneNotSupportedException
+ */
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ final BaseLink result = (BaseLink) super.clone();
+ result.m_printListener = null;
+ return result;
+ }
+
+ /**
+ * Adds a print listener.
+ * Since the
+ *
+ * Link Class
+ * Usage
+ *
+ *
+ * {@link BaseLink}
+ * Parent class of Bebop Link classes. Extend this class to
+ * build your own Link class.
+ *
+ *
+ * {@link Link}
+ * Link class that manages its own URL variables. Session information
+ * is added to the target URL for this type.
+ *
+ *
+ * {@link ExternalLink}
+ * Link that does not encode the URL with any session information.
+ * Used for a link to a page outside the site.
+ *
+ *
+ * {@link ControlLink}
+ * Used for references within its own page (often
+ * as fields in a table header for sorting a column).
+ *
+ *
+ * {@link ActionLink}
+ * Sets its own control event and runs its own
+ * {@link com.arsdigita.bebop.event.ActionListener}s. When the link is clicked,
+ * the code in the Listener's actionPerformed method runs.
+ *
+ *
+ * {@link ToggleLink}
+ * A link that turns into label when it is selected and
+ * turns back into a link when it is unselected.
+ * PrintListener is expected to modify the target
+ * of the PrintEvent, only one print listener can be set
+ * for a link.
+ *
+ * @param listener The print listener. Must not null.
+ * @throws IllegalArgumentException if listener is null.
+ * @throws TooManyListenersException if a print listener has previously been
+ * added.
+ */
+ public void addPrintListener(final PrintListener listener)
+ throws IllegalStateException, TooManyListenersException {
+ if (listener == null) {
+ throw new IllegalArgumentException("Argument listener can not be null");
+ }
+ if (m_printListener != null) {
+ throw new TooManyListenersException("Too many listeners. Can only have one");
+ }
+ m_printListener = listener;
+ }
+
+ /**
+ * Removes a previously added print listener. If the passed in
+ * listener is not the listener that was added with
+ * {@link #addPrintListener addPrintListener}, an IllegalArgumentException
+ * will be thrown.
+ *
+ * @param listener The listener that was previously added with
+ * addPrintListener.
+ * Must not be null.
+ * @throws IllegalArgumentException if listener is not the
+ * currently registered print listener or is null.
+ */
+ public void removePrintListener(final PrintListener listener)
+ throws IllegalArgumentException {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener can not be null");
+ }
+ if (listener != m_printListener) {
+ throw new IllegalArgumentException("listener is not registered with this widget");
+ }
+ m_printListener = null;
+ }
+
+ /**
+ *
+ * @param state
+ * @return
+ */
+ protected BaseLink firePrintEvent(final PageState state) {
+ BaseLink l = this;
+ if (m_printListener != null) {
+ try {
+ l = (BaseLink) this.clone();
+ m_printListener.prepare(new PrintEvent(this, state, l));
+ } catch (CloneNotSupportedException e) {
+ l = this;
+ throw new UncheckedWrapperException(e);
+ }
+ }
+ return l;
+ }
+
+ /**
+ * Retrieves the label component used to display the Link. Typically a Label,
+ * but may be an other type, e.g. an Image, as well.
+ *
+ * @return Component used to display the Link.
+ */
+ public final Component getChild() {
+ return m_child;
+ }
+
+ public void setChild(final Component child) {
+ Assert.isUnlocked(this);
+ m_child = child;
+ }
+
+ /**
+ * Use a GlobalizedMessage to be used to display the link. It's primary
+ * purpose is to hide the parent class' method to prevent its usage because
+ * Labels and GlobalizedMessages are used here differently (a
+ * GlobalizedMessage is here not directly used as a Label by specifying it
+ * as an attribugte, inside a Label component).
+ * @param message
+ */
+ @Override
+ public void setLabel(final GlobalizedMessage message) {
+ Assert.isUnlocked(this);
+ Label label = new Label(message);
+ setChild( (Component)label);
+
+ }
+
+ /**
+ *
+ * @return
+ */
+ public final String getTarget() {
+ return m_url;
+ }
+
+ public final void setTarget(final String url) {
+ Assert.isUnlocked(this);
+
+ m_url = url;
+ }
+
+ /**
+ * Sets the type of link this link represents.
+ *
+ * @param type the type of link
+ */
+ protected void setTypeAttr(final String type) {
+ Assert.isUnlocked(this);
+ setAttribute(TYPE_ATTR, type);
+ }
+
+ /**
+ *
+ * @param state
+ * @param parent
+ */
+ protected abstract void generateURL(final PageState state, final Element parent);
+
+ /**
+ *
+ * <bebop:link href="..." type="..." %bebopAttr;/>
+ *
+ * The href attribute contains the target the link should point
+ * to. The type attribute is used to give more fine grained
+ * control over which kind of link this element represents. The types are
+ * link for a Link, control for a
+ * {@link ControlLink}, and toggle for a {@link ToggleLink}.
+ * There may be additional attributes depending on what type of link this
+ * link represents.
+ *
+ * @see ControlLink#generateXML
+ * @see ToggleLink#generateXML
+ *
+ * @param state The current {@link PageState}.
+ * @param parent The XML element to attach the XML to.
+ */
+ @Override
+ public void generateXML(final PageState state, final Element parent) {
+ if (isVisible(state)) {
+ BaseLink target = firePrintEvent(state);
+
+ Element link = parent.newChildElement("bebop:link", BEBOP_XML_NS);
+
+ target.generateURL(state, link);
+ target.exportConfirmAttributes(state, link);
+ //setup the link without javascript
+ target.setupNoJavascriptURL(state, link);
+ target.exportAttributes(link);
+ target.generateExtraXMLAttributes(state, link);
+ target.generateDescriptionXML(state, link);
+ target.getChild().generateXML(state, link);
+ }
+ }
+
+ /**
+ *
+ * @param state
+ * @param sUrl
+ * @return
+ */
+ private String getAbsoluteUrl(final PageState state, final String sUrl) {
+ String sReturn = "";
+
+ if ((sUrl.indexOf(":") != -1) || sUrl.indexOf("/") == 0) {
+ //if sUrl contains a ":" or begins with a "/", then it is an absolute URL
+ sReturn = sUrl;
+ } else {
+ //otherwise, it is a relative URL, so we need to make it an absolute URL
+
+ //get the current URL
+ String sThisURL = "";
+ try {
+ sThisURL = state.stateAsURL();
+ } catch (java.io.IOException ioe) {
+ //ignore
+ }
+ //trim the current URL back to the last "/" character
+ int iIndex = sThisURL.lastIndexOf("/");
+
+ //if there is no "/" character, then assume we are at server root
+ if (iIndex == -1) {
+ sReturn = "/" + sUrl;
+ } else {
+ sReturn = sThisURL.substring(0, iIndex) + "/" + sUrl;
+ }
+ }
+
+ return sReturn;
+ }
+
+ /**
+ * Sets up no-JavaScript fallback HTML
+ *
+ * @param state The current {@link PageState}.
+ * @param link The link element.
+ */
+ protected void setupNoJavascriptURL(final PageState state, final Element link) {
+ String sURL = null;
+
+ if (m_sConfirmMsg.length() > 0
+ || (m_confirmMsg != null && m_confirmMsg.localize().toString().length() > 0)) {
+
+ //if we want the confirm link, create the link
+ String sOkUrl = getAbsoluteUrl(state, link.getAttribute(HREF));
+ String sCancelUrl = null;
+ try {
+ sCancelUrl = state.stateAsURL();
+ } catch (java.io.IOException e) {
+ Assert.fail("Could not get current page state as URL");
+ }
+
+ if (m_sConfirmMsg.length() > 0) {
+ sURL = ConfirmPage.getConfirmUrl(m_sConfirmMsg, sOkUrl, sCancelUrl);
+ } else if (m_confirmMsg != null) {
+ sURL = ConfirmPage.getConfirmUrl(m_confirmMsg.localize().toString(), sOkUrl, sCancelUrl);
+ }
+
+ } else {
+ //don't want confirm link--just no javascript link
+ if (m_noJavascriptURL == null) {
+ //get the generatedURL and make it the default noJavaScript link
+ sURL = link.getAttribute(HREF);
+ } else {
+ sURL = m_noJavascriptURL;
+ }
+ }
+ link.addAttribute(HREF_NO_JAVASCRIPT, sURL);
+ exportAttributes(link);
+ }
+
+ /**
+ * Adds type-specific XML attributes to the XML element representing
+ * this link. Subclasses should override this method if they introduce
+ * more attributes than the ones {@link #generateXML generateXML}
+ * produces by default.
+ *
+ * @param state The current request
+ * @param link The XML element representing this link
+ */
+ protected void generateExtraXMLAttributes(final PageState state, final Element link) {
+ }
+
+ /**
+ * Sets onClick event and disables the javascript-based double-click
+ * protection for this link. Not for confirmation messages; Should call
+ * setConfirmation for that.
+ *
+ * @param value The confirmation link. To not use the value {@code return confirm(} with this
+ * method.
+ *
+ * @see #setConfirmation
+ */
+ public void setOnClick(final String value) {
+ //should not use this method to set confirmation messages--should
+ //use setConfirmation() instead, or else the javascript will break
+ if (value != null) {
+ Assert.isTrue(!value.toLowerCase().startsWith("return confirm("),
+ "Do not use setOnClick() to set confirmation messages. "
+ + "Use setCofirmation() instead.");
+ }
+
+ setAttribute(ON_CLICK, value);
+ }
+
+ /**
+ * Forces the user to click through a confirmation dialog before this link
+ * is followed. The user is prompted with the specified message. If the
+ * user does not does not confirm, the link is not followed. The current
+ * implementation uses the JavaScript confirm function and the onClick
+ * attribute.
+ * If JavaScript is not enabled in the client browser, this method will
+ * redirect the browser to a Bebop confirmation page rather than use
+ * a JavaScript confirmation.
+ * Subsequent calls to setOnClick will undo the effect of this method.
+ *
+ * @param message the confirmation message presented to the user. This
+ * message cannot have an apostrophe or back slash.
+ * @deprecated Use setConfirmation(final GlobalizedMessage msg) instead
+ */
+ public void setConfirmation(final String message) {
+ //make sure that the message doesn't have any apostrophe's
+ //or back slashes
+
+ if (Assert.isEnabled()) {
+ final boolean isGoodMessage = message.indexOf("'") == -1 && message.indexOf("\\") == -1;
+ Assert.isTrue(isGoodMessage,
+ "confirmation message cannot contain apostrophe or back slash");
+ }
+
+ m_sConfirmMsg = message;
+ }
+
+ /**
+ * Set a GlobalizedMessage as confirmation message
+ * @param msg
+ */
+ public void setConfirmation(final GlobalizedMessage msg) {
+ m_confirmMsg = msg;
+ }
+
+ /**
+ * Generate XML output for confirmation links
+ *
+ * @param state PageState
+ * @param link Parent element
+ */
+ private void exportConfirmAttributes(final PageState state, final Element link) {
+
+ // If a confirmation message is set
+ if (m_sConfirmMsg.length() > 0 || m_confirmMsg != null) {
+
+ // then add the needed attributes to link
+ link.addAttribute("confirm", "confirm");
+
+ // If m_sConfirmMsg is not empty
+ if (m_sConfirmMsg.length() > 0) {
+
+ // then set the onclick attribute for the link with the static message
+ link.addAttribute(ON_CLICK, "return confirm(\\'" + m_sConfirmMsg + "\\');");
+
+ // else if m_configMsg is set
+ } else if (m_confirmMsg != null) {
+
+ //then set the onclick attribute for the link with a globalized message
+ link.addAttribute(ON_CLICK, "return confirm(\\'" + m_confirmMsg.localize() + "\\');");
+
+ }
+ }
+ }
+
+ public final void setNoJavascriptTarget(final String sURL) {
+ Assert.isUnlocked(this);
+ m_noJavascriptURL = sURL;
+ }
+
+ public final String getNoJavascriptTarget() {
+ return m_noJavascriptURL;
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/BasePage.java b/ccm-core/src/main/java/com/arsdigita/bebop/BasePage.java
new file mode 100755
index 000000000..152c044bb
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/BasePage.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.util.Assert;
+
+/**
+ * The base page class for use with the PageFactory
+ * class. It sets two attributes on the XML tag for
+ * the bebop:page, namely application
+ * and id. The values for these two
+ * tags correspond to the parameters passed to the
+ * PageFactory.buildPage method.
+ * BebopConfig object.
+ */
+ public static BebopConfig getConfig() {
+ return s_config;
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/BebopConfig.java b/ccm-core/src/main/java/com/arsdigita/bebop/BebopConfig.java
new file mode 100755
index 000000000..f322fa2d4
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/BebopConfig.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.bebop.page.PageTransformer;
+import com.arsdigita.bebop.util.BebopConstants;
+import com.arsdigita.runtime.AbstractConfig;
+import com.arsdigita.templating.PresentationManager;
+import com.arsdigita.ui.SimplePage;
+import com.arsdigita.util.parameter.BooleanParameter;
+import com.arsdigita.util.parameter.ClassParameter;
+import com.arsdigita.util.parameter.EnumerationParameter;
+import com.arsdigita.util.parameter.Parameter;
+import com.arsdigita.util.parameter.SingletonParameter;
+import com.arsdigita.util.parameter.StringParameter;
+import org.apache.log4j.Logger;
+
+/**
+ * @author Justin Ross
+ * @see com.arsdigita.bebop.Bebop
+ * @version $Id: BebopConfig.java 1498 2007-03-19 16:22:15Z apevec $
+ */
+public final class BebopConfig extends AbstractConfig {
+
+ /** A logger instance to assist debugging. */
+ private static final Logger s_log = Logger.getLogger(BebopConfig.class);
+
+ /** Singleton config object. */
+ private static BebopConfig s_config;
+
+ /**
+ * Gain a BebopConfig object.
+ *
+ * Singleton pattern, don't instantiate a config object using the
+ * constructor directly!
+ * @return
+ */
+ public static synchronized BebopConfig getInstance() {
+ if (s_config == null) {
+ s_config = new BebopConfig();
+ s_config.load();
+ }
+
+ return s_config;
+ }
+
+ // set of configuration parameters
+ // /////////////////////////////////////////////////////////////////
+
+ /**
+ * */
+ private final Parameter m_presenter = new SingletonParameter
+ ("waf.bebop.presentation_manager", Parameter.REQUIRED,
+ new PageTransformer());
+ /**
+ *
+ */
+ private final Parameter m_page = new ClassParameter
+ ("waf.bebop.base_page", Parameter.REQUIRED, SimplePage.class);
+ /** Pointer to JTidy validation listener config file */
+ private final Parameter m_tidy = new StringParameter
+ ("waf.bebop.tidy_config_file", Parameter.REQUIRED,
+ "com/arsdigita/bebop/parameters/tidy.properties");
+ private final Parameter m_fancyErrors = new BooleanParameter
+ ("waf.bebop.fancy_xsl_errors",
+ Parameter.REQUIRED,
+ Boolean.FALSE);
+ /** Double Click Protection, enabled by default for all buttons in a form.*/
+ private final Parameter m_dcpOnButtons = new BooleanParameter
+ ("waf.bebop.dcp_on_buttons", Parameter.REQUIRED, Boolean.TRUE);
+ /** Double Click Protection, disabled by default for all links. */
+ private final Parameter m_dcpOnLinks = new BooleanParameter
+ ("waf.bebop.dcp_on_links", Parameter.REQUIRED, Boolean.FALSE);
+ /**
+ *
+ */
+ private final Parameter m_enableTreeSelect = new BooleanParameter
+ ("waf.bebop.enable_tree_select_attribute",
+ Parameter.REQUIRED,
+ Boolean.FALSE);
+ /** List of supported DHTML editors, first one is default (Xinha) */
+ private final EnumerationParameter m_dhtmlEditor;
+ /** Path to DHTML editor source file, relativ to document root */
+ private final Parameter m_dhtmlEditorSrcFile;
+ /** */
+ private final Parameter m_showClassName = new BooleanParameter
+ ("waf.bebop.show_class_name", Parameter.OPTIONAL, Boolean.FALSE);
+
+ /**
+ * Constructor.
+ * Singelton pattern, don't instantiate a config object using the
+ * constructor directly! Use getConfig() instead.
+ *
+ */
+ public BebopConfig() {
+
+ /** List of supported DHTML editors, first one is default (Xinha) */
+ m_dhtmlEditor = new EnumerationParameter("waf.bebop.dhtml_editor",
+ Parameter.REQUIRED,BebopConstants.BEBOP_XINHAEDITOR);
+ m_dhtmlEditor.put("Xinha", BebopConstants.BEBOP_XINHAEDITOR);
+ m_dhtmlEditor.put("FCKeditor", BebopConstants.BEBOP_FCKEDITOR);
+ // HTMLArea for backwards compatibility with old XSL. to be removed soon!
+ m_dhtmlEditor.put("HTMLArea", BebopConstants.BEBOP_DHTMLEDITOR);
+
+ // Xinha is now default!
+ m_dhtmlEditorSrcFile = new StringParameter
+ ("waf.bebop.dhtml_editor_src", Parameter.REQUIRED,
+ "/assets/xinha/XinhaLoader.js");
+
+ register(m_presenter);
+ register(m_page);
+ register(m_tidy);
+ register(m_fancyErrors);
+ register(m_dhtmlEditor);
+ register(m_dhtmlEditorSrcFile);
+ register(m_dcpOnButtons);
+ register(m_dcpOnLinks);
+ register(m_enableTreeSelect);
+ register(m_showClassName);
+
+ loadInfo();
+ }
+
+ /**
+ * Gets the configured PresentationManger.
+ */
+ public final PresentationManager getPresentationManager() {
+ return (PresentationManager) get(m_presenter);
+ }
+
+ final Class getBasePageClass() {
+ return (Class) get(m_page);
+ }
+
+ /**
+ * I don't *want* to make this public. XXX
+ */
+ public final String getTidyConfigFile() {
+ return (String) get(m_tidy);
+ }
+
+ public boolean wantFancyXSLErrors() {
+ return ((Boolean)get(m_fancyErrors)).booleanValue();
+ }
+
+ public final boolean doubleClickProtectionOnButtons() {
+ return ((Boolean) get(m_dcpOnButtons)).booleanValue();
+ }
+
+ public final boolean doubleClickProtectionOnLinks() {
+ return ((Boolean) get(m_dcpOnLinks)).booleanValue();
+ }
+
+ public final boolean treeSelectAttributeEnabled() {
+ return ((Boolean) get(m_enableTreeSelect)).booleanValue();
+ }
+
+ /**
+ * Gets the DHTML editor to use
+ */
+ public final String getDHTMLEditor() {
+ return (String) get(m_dhtmlEditor);
+ }
+
+ /**
+ * Gets the location of DHTML editor's source file
+ */
+ public final String getDHTMLEditorSrcFile() {
+ return (String) get(m_dhtmlEditorSrcFile);
+ }
+
+ public final boolean showClassName() {
+ return ((Boolean) get(m_showClassName)).booleanValue();
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/BoxPanel.java b/ccm-core/src/main/java/com/arsdigita/bebop/BoxPanel.java
new file mode 100755
index 000000000..f58d6046c
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/BoxPanel.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import java.util.Iterator;
+
+
+import com.arsdigita.xml.Element;
+
+import com.arsdigita.bebop.form.Hidden;
+
+// This interface contains the XML element name of this class
+// in a constant which is used when generating XML
+import com.arsdigita.bebop.util.BebopConstants;
+import com.arsdigita.bebop.util.PanelConstraints;
+
+/**
+ * A container that prints its components in one row, either horizontally or
+ * vertically.
+ *
+ * @author David Lutterkort
+ * @version $Id: BoxPanel.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public class BoxPanel extends SimpleContainer
+ implements BebopConstants, PanelConstraints {
+
+ /** Specifies that components should be laid out left to right. */
+ public final static int HORIZONTAL = 1;
+
+ /** Specifies that components should be laid out top to bottom. */
+ public final static int VERTICAL = 2;
+
+ /** XML attribute for width */
+ private static final String WIDTH_ATTR = "width";
+
+ /** XML attribute wether to draw a border. */
+ private static final String BORDER_ATTR = "border";
+
+ /** Property to whether to draw a HORIZONTAL or VERTICAL box panel. */
+ private int m_axis;
+
+ /** Property to store whether to to center alignment. */
+ private boolean m_centering;
+
+
+ /**
+ * Constructor, creates a box panel that lays out its components from
+ * top to bottom. The components are not centered.
+ */
+ public BoxPanel() {
+ this(VERTICAL);
+ }
+
+ /**
+ * Constructor, creates a box panel that lays out its components in the given
+ * direction. The components are not centered.
+ *
+ * @param axis the axis to use to lay out the components
+ */
+ public BoxPanel(int axis) {
+ this(axis, false);
+ }
+
+ /**
+ * Creates a box panel that lays out its components in the given
+ * direction and centers them if that is specified.
+ *
+ * @param axis the axis to use to lay out the components
+ * @param centering true if the layout should be centered
+ */
+ public BoxPanel(int axis, boolean centering) {
+ m_axis = axis;
+ m_centering = centering;
+ }
+
+ // Instance methods
+
+ /**
+ * Sets the width attribute of the box panel. The given width should be in
+ * a form that is legal as the width attribute of an HTML
+ * table element.
+ *
+ * @param w the width of the box panel
+ */
+ public void setWidth(String w) {
+ setAttribute(WIDTH_ATTR, w);
+ }
+
+// /**
+// * Sets whether a border should be drawn.
+// *
+// * @param isBorder true if a border should be drawn
+// * @deprecated Use {@link #setBorder(int border)} instead.
+// */
+// public void setBorder(boolean isBorder) {
+// if (isBorder) {
+// setAttribute(BORDER_ATTR, "1");
+// } else {
+// setAttribute(BORDER_ATTR, "0");
+// }
+// }
+
+ /**
+ *
+ * Sets the width of the border to draw around the components. This value
+ * will be used for the border attribute in an HTML
+ * table element.
+ *
+ * @param border the width of the border
+ */
+ public void setBorder(int border) {
+ setAttribute(BORDER_ATTR, String.valueOf(border));
+ }
+
+ /**
+ * Adds nodes for the panel and its child components to be rendered,
+ * usually in a table. Any hidden widgets directly contained in the box
+ * panel are added directly to parent and are not in any
+ * of the cells that the box panel generates.
+ *
+ * <bebop:boxPanel [width=...] border=... center... axis...>
+ * <bebop:cell> cell contents </bebop:cell>
+ * </bebop:boxPanel>
+ *
+ * @param parent
+ */
+ @Override
+ public void generateXML(PageState state, Element parent) {
+ if (isVisible(state)) {
+ Element panel = parent.newChildElement(BEBOP_BOXPANEL, BEBOP_XML_NS);
+ // or: rowPanel/columPanel?
+ panel.addAttribute("center", String.valueOf(m_centering));
+ panel.addAttribute("axis", String.valueOf(m_axis));
+ exportAttributes(panel);
+
+ for (Iterator i = children(); i.hasNext();) {
+ Component c = (Component) i.next();
+
+ if (c.isVisible(state)) {
+ if (c instanceof Hidden) {
+ c.generateXML(state, parent);
+ } else {
+ Element cell = panel.newChildElement(BEBOP_CELL, BEBOP_XML_NS);
+ c.generateXML(state, cell);
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/ColumnPanel.java b/ccm-core/src/main/java/com/arsdigita/bebop/ColumnPanel.java
new file mode 100755
index 000000000..ee68b7769
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/ColumnPanel.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.bebop.form.Hidden;
+import com.arsdigita.bebop.util.Attributes;
+import com.arsdigita.bebop.util.PanelConstraints;
+import com.arsdigita.util.Assert;
+import com.arsdigita.xml.Element;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * A container that prints its components in a table. Each child is printed
+ * in its own table cell. The number of columns can be specified in the
+ * constructor. The components are put into the table in the order in which
+ * they were added to the ColumnPanel by filling the table one row
+ * at a time (filling each row from left to right), from the top of the table
+ * to the bottom.
+ *
+ *
+ *
+ *
+ *
+ *
+ * Horizontal alignment
+ * Use LEFT, CENTER, or
+ * RIGHT.
+ *
+ * Vertical alignment
+ * Use TOP, MIDDLE, or
+ * BOTTOM.
+ *
+ * Full width
+ * Use FULL_WIDTH to instruct the panel to
+ * put the component in a row by itself, spanning the full width of the
+ * table.
+ *
+ * Inserting children
+ * Use INSERT to instruct the panel to
+ * insert the corresponding component, assuming that it will also be
+ * laid out by a ColumnPanel with the same number of
+ * columns.FULL_WIDTH | LEFT | BOTTOM.
+ *
+ * INSERT constraint fuses the current ColumnPanel
+ * with the panel of the child to which the constraint is applied. For example,
+ * consider a {@link Form} that is to have a 2-column format with labels in the
+ * left column and widgets in the right column. If a {@link FormSection} is
+ * added to the form, it should be included seamlessly into the parent form.
+ * To do this, set the INSERT constraint when the {@link
+ * FormSection} is added to the ColumnPanel of the {@link Form}. At
+ * the same time, tell the ColumnPanel used to lay out the {@link
+ * FormSection} that it is to be inserted into another panel.
+ *
+ *
+ *
+ * Form form = new Form(new ColumnPanel(2));
+ * FormSection sec = new FormSection(new ColumnPanel(2, true));
+ *
+ * sec.add(new Label("Basic Item Metadata"), ColumnPanel.FULL_WIDTH);
+ * sec.add(new Label("Title:"), ColumnPanel.RIGHT);
+ * sec.add(new Text("title"));
+ *
+ * form.add(sec, ColumnPanel.INSERT);
+ *
+ *
+ * @author David Lutterkort
+ * @version $Id: ColumnPanel.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public class ColumnPanel extends SimpleContainer
+ implements PanelConstraints {
+
+ /** An empty constraint corresponding to the default */
+ private static final Constraint DEFAULT_CONSTRAINT = new Constraint();
+
+ /** The number of columns in the panel */
+ private int m_nCols;
+
+ /** Explicitly registered constraints for child components. Maps
+ * Componentss to Constraints */
+ private Map m_constraints;
+
+ /** Is this panel inserted in another one ? If so, do not produce
+ * <table> tags */
+ private boolean m_inserted;
+
+ /** Border attributes */
+ private Attributes m_border;
+ private Attributes m_padFrame;
+ private Attributes m_pad;
+ private String[] m_columnWidth;
+
+
+// Instance methods
+
+ /**
+ * Creates a table panel with the specified number of columns.
+ *
+ * @param nCols number of columns in the panel
+ */
+ public ColumnPanel(int nCols) {
+ this(nCols, false);
+ makeBorder();
+ makePadFrame();
+ makePad();
+ }
+
+ /**
+ * Creates a table panel with the specified number of columns that will
+ * be printed as a direct child of a ColumnPanel
+ * with the same number of columns.
+ * @see #setInserted
+ *
+ * @param nCols number of columns in the panel
+ * @param inserted true if this panel
+ * is to be printed as a direct child of a
+ * ColumnPanel with the same number of
+ * columns
+ */
+ public ColumnPanel(int nCols, boolean inserted) {
+ m_nCols = nCols;
+ setInserted(inserted);
+ m_constraints = new HashMap();
+ m_columnWidth=new String[nCols];
+ }
+
+ /**
+ * Adds a component, specifying its constraints.
+ *
+ * @param c the component to add
+ * @param constraints the constraints for this component
+ */
+ @Override
+ public void add(Component c, int constraints) {
+ super.add(c);
+ setConstraint(c, constraints);
+ }
+
+ /**
+ * Sets whether this panel will be printed inside a
+ * ColumnPanel with the same number of columns. If
+ * inserted is true, no <table> tags will be produced
+ * to enclose the child components.
+ * @param inserted true if this panel is to be printed
+ * inside a ColumnPanel with the same number of columns
+ */
+ public void setInserted(boolean inserted) {
+ Assert.isUnlocked(this);
+ m_inserted = inserted;
+ }
+
+ /**
+ * Determines whether this panel is to be inserted into another panel.
+ * @return true if this panel is to be inserted
+ * into another panel; false otherwise.
+ *
+ * @see #setInserted
+ */
+ public final boolean isInserted() {
+ return m_inserted;
+ }
+
+ /**
+ * Returns the number of columns for this ColumnPanel
+ * @return the number of columns
+ *
+ */
+ public final int getNumCols() {
+ return m_nCols;
+ }
+
+ /**
+ * Adds child components as a subtree under table-style nodes. If any of the
+ * direct children are hidden form widgets, they are added directly to
+ * parent rather than included in any of the
+ * cell elements of the panel.
+ *
+ *
+ * @param state the current page state
+ * @param parent the parent element for these child components
+ */
+ @Override
+ public void generateXML(PageState state, Element parent) {
+ if ( isVisible(state) ) {
+
+ Element panel = parent.newChildElement("bebop:columnPanel", BEBOP_XML_NS);
+ exportAttributes(panel);
+ // parent.addContent(panel);
+
+ generateChildren(state, parent, generateTopNodes(panel));
+ }
+ }
+
+ // Border attributes
+
+ private void makeBorder() {
+ if ( m_border == null ) {
+ m_border = new Attributes();
+ m_border.setAttribute("cellspacing", "0");
+ m_border.setAttribute("cellpadding", "4");
+ m_border.setAttribute("border", "0");
+ m_border.setAttribute("width", "100%");
+ }
+ }
+
+ /**
+ *
+ *
+ * @param c
+ */
+ public void setBorderColor(String c) {
+ makeBorder();
+ m_border.setAttribute("bgcolor", c);
+ }
+
+ /**
+ *
+ *
+ * @param w
+ */
+ public void setBorderWidth(String w) {
+ makeBorder();
+ m_border.setAttribute("cellpadding", w);
+ }
+
+ public void setColumnWidth(int col, String width) {
+ m_columnWidth[col-1]=width;
+ }
+
+ /**
+ *
+ *
+ * @param b
+ */
+ public void setBorder(boolean b) {
+ if (b) {
+ makeBorder();
+ } else {
+ m_border = null;
+ }
+ }
+
+ // Pad and Padframe attributes
+ private void makePadFrame() {
+ if (m_padFrame == null) {
+ m_padFrame = new Attributes();
+ m_padFrame.setAttribute("cellspacing", "0");
+ m_padFrame.setAttribute("cellpadding", "6");
+ m_padFrame.setAttribute("border", "0");
+ m_padFrame.setAttribute("width", "100%");
+ }
+ }
+
+ /**
+ *
+ *
+ */
+ private void makePad() {
+ if ( m_pad == null ) {
+ m_pad = new Attributes();
+ m_pad.setAttribute("cellspacing", "0");
+ m_pad.setAttribute("cellpadding", "2");
+ m_pad.setAttribute("border", "0");
+ m_pad.setAttribute("width", "100%");
+ }
+ }
+
+ /**
+ *
+ *
+ * @param c
+ */
+ public void setPadColor(String c) {
+ makePadFrame();
+ makePad();
+ m_padFrame.setAttribute("bgcolor", c);
+ m_pad.setAttribute("bgcolor", c);
+ }
+
+ /**
+ *
+ *
+ * @param w
+ */
+ public void setWidth(String w) {
+ makePadFrame();
+ m_padFrame.setAttribute("width", w);
+ }
+
+ /**
+ *
+ *
+ * @param w
+ */
+ public void setPadFrameWidth(String w) {
+ makePadFrame();
+ m_padFrame.setAttribute("cellpadding", w);
+ }
+
+ /**
+ *
+ *
+ * @param border
+ */
+ public void setPadBorder(boolean border) {
+ makePad();
+ if(border) {
+ m_pad.setAttribute("border", "1");
+ } else {
+ m_pad.setAttribute("border", "0");
+ }
+ }
+
+ /**
+ *
+ *
+ * @param padding
+ */
+ public void setPadCellPadding(String padding) {
+ makePad();
+ m_pad.setAttribute("cellpadding", padding);
+ }
+
+ /**
+ * add top tags (will translate to opening/closing),
+ * including display styles
+ */
+ private Element generateTopNodes(Element parent) {
+ // FIXME: set background color, border effects, cell spacing etc.
+ if (isInserted()) {
+ return parent;
+ }
+ String l_class = getClassAttr();
+ if (m_border != null) {
+ Element border = parent.newChildElement("bebop:border",BEBOP_XML_NS);
+ if (l_class != null) {
+ m_border.setAttribute("class", l_class);
+ }
+ m_border.exportAttributes(border);
+ // parent.addContent(border);
+ parent=border; // nest the rest inside border
+ }
+ if ( m_padFrame != null ) {
+ Element padFrame = parent.newChildElement("bebop:padFrame", BEBOP_XML_NS);
+ if (l_class != null) {
+ m_padFrame.setAttribute("class", l_class);
+ }
+ m_padFrame.exportAttributes(padFrame);
+ // parent.addContent(padFrame);
+ parent=padFrame; // nest the rest in padFrame
+ }
+ Element pad = parent.newChildElement("bebop:pad", BEBOP_XML_NS);
+ if (l_class != null) {
+ m_pad.setAttribute("class", l_class);
+ }
+ m_pad.exportAttributes(pad);
+ // parent.addContent(pad);
+ return pad;
+ }
+
+ /**
+ * Lay out the child components using constraints registered for them,
+ * generating a DOM tree and extending another.
+ *
+ * @param state represents the state of the current request
+ * @param hiddenParent the element to which hiddens are added
+ * @param parent the element to which ordinary rows and cells are added
+ */
+ private void generateChildren(PageState state, Element hiddenParent,
+ Element parent) {
+ // Count the number of components printed in the current row
+ int rowLen = m_nCols + 1; // Force generation of first row
+ Element row = null;
+ Element cell = null;
+
+ for (Iterator i = children(); i.hasNext(); ) {
+ Component c = (Component) i.next();
+
+ if ( c.isVisible(state) ) {
+
+ if ( c instanceof Hidden ) {
+ c.generateXML(state, hiddenParent);
+ } else {
+ if ( isInsert(c) ) {
+ c.generateXML(state, parent);
+ rowLen = m_nCols + 1; // Force generation of new row
+ } else {
+ if ( rowLen >= m_nCols || isFullWidth(c)) {
+ rowLen = 0;
+ row = parent.newChildElement("bebop:panelRow", BEBOP_XML_NS);
+ // parent.addContent(row);
+ }
+ cell = row.newChildElement("bebop:cell", BEBOP_XML_NS);
+ // row.addContent(cell);
+ if ( m_columnWidth[rowLen] != null ) {
+ cell.addAttribute("width", m_columnWidth[rowLen]);
+ }
+ getConstraint(c).exportAttributes(cell, m_nCols);
+ c.generateXML(state, cell);
+ rowLen++;
+ if ( isFullWidth(c) ) {
+ // Force a new row if c was full width
+ rowLen = m_nCols + 1;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sets the constraint for one child component.
+ * @param c the child component
+ * @param constraints the constraints to add
+ */
+ public void setConstraint(Component c, int constraints) {
+ Assert.isUnlocked(this);
+ m_constraints.put(c, new Constraint(constraints));
+ }
+
+ /**
+ * Get the constraint object for a component. If no constraints have been
+ * set explicitly, return a default constraint object.
+ *
+ * @post return != null
+ */
+ private Constraint getConstraint(Component c) {
+ Constraint result = (Constraint) m_constraints.get(c);
+ if ( result == null ) {
+ return DEFAULT_CONSTRAINT;
+ } else {
+ return result;
+ }
+ }
+
+ private boolean isInsert(Component c) {
+ return getConstraint(c).isInsert();
+ }
+
+ private boolean isFullWidth(Component c) {
+ return getConstraint(c).isFullWidth();
+ }
+
+
+ // Inner class(es)
+
+ /**
+ * Represent the constraints for one child component
+ */
+ private static class Constraint {
+ private boolean m_fullWidth;
+ private boolean m_insert;
+ private String m_alignment; // for print
+ private String m_halign; // for generateXML
+ private String m_valign; // for generateXML
+
+ public Constraint() {
+ this(0);
+ }
+
+ public Constraint(int constraints) {
+ StringBuilder s = new StringBuilder();
+
+ if ( (constraints & (LEFT|CENTER|RIGHT)) != 0 ) {
+ s.append(" align=\"");
+ if ( (constraints & LEFT) != 0) {
+ s.append(m_halign = "left");
+ } else if ( (constraints & CENTER) != 0) {
+ s.append(m_halign = "center");
+ } else if ( (constraints & RIGHT) != 0) {
+ s.append(m_halign = "right");
+ }
+ s.append("\" ");
+ } else {
+ m_halign = null;
+ }
+
+ if ( (constraints & (TOP|MIDDLE|BOTTOM)) != 0 ) {
+ s.append(" valign=\"");
+ if ( (constraints & TOP) != 0) {
+ s.append(m_valign = "top");
+ } else if ( (constraints & MIDDLE) != 0) {
+ s.append(m_valign = "middle");
+ } else if ( (constraints & BOTTOM) != 0) {
+ s.append(m_valign = "bottom");
+ }
+ s.append("\" ");
+ } else {
+ m_valign = null;
+ }
+
+ m_alignment = s.toString();
+
+ m_fullWidth = (constraints & FULL_WIDTH) != 0;
+ m_insert = (constraints & INSERT) != 0;
+ }
+
+ public final boolean isFullWidth() {
+ return m_fullWidth;
+ }
+
+ public final boolean isInsert() {
+ return m_insert;
+ }
+
+ public final String getAlignment() {
+ return m_alignment;
+ }
+
+ public final String getHAlign() {
+ return m_halign;
+ }
+
+ public final String getVAlign() {
+ return m_valign;
+ }
+
+ public void exportAttributes(Element cell, int nCols) {
+ String halign = getHAlign();
+ String valign = getVAlign();
+ if (halign != null) {
+ cell.addAttribute("align" , halign);
+ }
+ if (valign != null) {
+ cell.addAttribute("valign", valign);
+ }
+ if ( isFullWidth() ) {
+ cell.addAttribute("colspan", Integer.toString(nCols));
+ }
+ }
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Completable.java b/ccm-core/src/main/java/com/arsdigita/bebop/Completable.java
new file mode 100755
index 000000000..5afc2dae3
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/Completable.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.util.Assert;
+
+import com.arsdigita.bebop.event.ActionEvent;
+import com.arsdigita.bebop.event.ActionListener;
+// Stacktraces is a support tool to use in a specifically difficult development
+// situation. It is abundant in production and for normal development work and
+// it provved to have funny side effects in a production environment. So it is
+// commented out here but kept for further references.
+// import com.arsdigita.developersupport.StackTraces;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Completable.
+ *
+ * @author rhs@mit.edu
+ * @version $Revision: #10 $ $Date: 2004/08/16 $
+ * @version $Id: Completable.java 287 2005-02-22 00:29:02Z sskracic $
+ **/
+
+public abstract class Completable implements Component {
+
+ private final static Logger s_log = Logger.getLogger(Completable.class);
+
+ private ArrayList m_completionListeners = new ArrayList();
+
+ public Completable() {
+ // See note above!
+ // if ( s_log.isDebugEnabled() ) {
+ // StackTraces.captureStackTrace(this);
+ // }
+ }
+
+ public void addCompletionListener(ActionListener listener) {
+ Assert.isUnlocked(this);
+ m_completionListeners.add(listener);
+ }
+
+ protected void fireCompletionEvent(PageState ps) {
+ ActionEvent evt = new ActionEvent(this, ps);
+ for (Iterator it = m_completionListeners.iterator(); it.hasNext(); ) {
+ ActionListener listener = (ActionListener) it.next();
+ listener.actionPerformed(evt);
+ }
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Component.java b/ccm-core/src/main/java/com/arsdigita/bebop/Component.java
new file mode 100644
index 000000000..b0b83b748
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/Component.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import java.util.Iterator;
+
+import com.arsdigita.util.Lockable;
+import com.arsdigita.xml.Element;
+
+/**
+ * The common interface implemented by all Bebop components.
+ *
+ * During its lifetime, a component receives the following calls
+ * from the containing page.
+ *
+ *
+ * <bebop:pad>
+ * [<bebop:padFrame>]
+ * [<bebop:border>]
+ * <bebop:panelRow>
+ * <bebop:cell> ... cell contents </bebop:cell>
+ * <bebop:cell> ... cell contents </bebop:cell>
+ * ...
+ * </bebop:panelRow>
+ * <bebop:panelRow>
+ * <bebop:cell> ... cell contents </bebop:cell>
+ * <bebop:cell> ... cell contents </bebop:cell>
+ * ...
+ * </bebop:panelRow>
+ * [</bebop:border>]
+ * [</bebop:padFrame>]
+ * </bebop:boxPanel>
+ *
+ *
+ *
+ *
+ *
+ * FormSection.
+ *
+ *
+ * Visibility
+ *
+ *
+ *
+ * Standard Attributes
+ *
+ *
+ *
+ * Attribute Java property Purpose
+ *
+ *
+ * id
+ *
+ * {@link #getIdAttr getIdAttr}/{@link #setIdAttr setIdAttr}
+ *
+ * Use to uniquely identify a component within the page. Uniqueness is
+ * not enforced. The id attribute allows stylesheet designers to
+ * access individual components.
+ *
+ *
+ * class
+ * {@link #getClassAttr()}/{@link
+ * #setClassAttr(String)}
+ *
+ * Use as the generic name for a
+ * component. For example, if you have a UserInfoDisplay
+ * component, the class attribute can be used to name the
+ * component "UserInfoDisplay" so that a generic template rule
+ * can style all UserInfoDisplay components.
+ *
+ *
+ *
+ *
+ * style
+ * {@link #getStyleAttr getStyleAttr}/{@link
+ * #setStyleAttr setStyleAttr}
+ * Use to add CSS style to an individual element.
+ * Caveat: Race Conditions
+ * Component, honor the
+ * Lockable contract indicated by extends {@link
+ * com.arsdigita.util.Lockable}. Beware that member variables
+ * are not inherently threadsafe, because you may be circumventing
+ * the contract. For variables that might be different for each
+ * request, use {@link RequestLocal}. If you must add member
+ * variables in the derived class, as a minimum be sure to safeguard
+ * any write access to instance variables with {@link
+ * com.arsdigita.util.Assert#assertNotLocked}.
+ * state.state by calling
+ * {@link PageState#setAttribute setAttribute}.null) iterator.
+ *
+ * @return an iterator over the children of this component.
+ *
+ * @post return != null
+ */
+ Iterator children();
+
+ /**
+ * Registers state parameters for the page with its model.
+ *
+ * A simple component with a state parameter param would do
+ * the following in the body of this method:
+ *
+ * p.addComponent(this);
+ * p.addComponentStateParam(this, param);
+ *
+ *
+ * You should override this method to set the default visibility
+ * of your component:
+ *
+ *
+ * public void register(Page p) {
+ * super.register(p);
+ * p.setVisibleDefault(childNotInitiallyShown,false);
+ * p.setVisibleDefault(anotherChild, false);
+ * }
+ *
+ *
+ * Always call super.register when you override
+ * register. Otherwise your component may
+ * malfunction and produce errors like "Widget ... isn't
+ * associated with any Form"
+ *
+ * @param p
+ * @pre p != null
+ */
+ void register(Page p);
+
+ /**
+ * Registers form parameters with the form model for this
+ * form. This method is only important for {@link FormSection form
+ * sections} and {@link com.arsdigita.bebop.form.Widget widgets}
+ * (components that have a connection to an HTML form). Other
+ * components can implement it as a no-op.
+ *
+ * @param f
+ * @param m
+ * @pre f != null
+ * @pre m != null
+ */
+ void register(Form f, FormModel m);
+
+ /* Properties that will get copied straight to the output,
+ both in HTML and in XML
+ */
+
+ /**
+ * Gets the class attribute.
+ *
+ * @return the class attribute.
+ *
+ * @see #setClassAttr(String)
+ * @see Standard Attributes
+ */
+ String getClassAttr();
+
+ /**
+ * Sets the class attribute.
+ * @param theClass a valid XML name
+ * @see Standard Attributes
+ * @see #getClassAttr
+ */
+ void setClassAttr(String theClass);
+
+ /**
+ * Gets the style attribute.
+ *
+ * @return the style attribute.
+ *
+ * @see #setStyleAttr
+ * @see Standard Attributes
+ */
+ String getStyleAttr();
+
+ /**
+ * Sets the style attribute. style should be a valid CSS
+ * style, because its value will be copied verbatim to the output and
+ * appear as a style attribute in the top level XML or HTML
+ * output element.
+ *
+ * @param style a valid CSS style description for use in the
+ * style attribute of an HTML tag
+ * @see Standard Attributes
+ */
+ void setStyleAttr(String style);
+
+ /**
+ * Gets the id attribute.
+ *
+ * @return the id attribute.
+ *
+ * @see Standard Attributes
+ * @see #setIdAttr(String id)
+ */
+ String getIdAttr();
+
+ /**
+ * Sets the id attribute. id
+ * should be an XML name
+ * that is unique within the {@link Page Page} in which this component is
+ * contained. The value of id is copied literally to the
+ * output and not used for internal processing.
+ *
+ * @param id a valid XML identifier
+ * @see Standard Attributes
+ */
+ void setIdAttr(String id);
+
+ /**
+ * Supplies a key for making parameter names unique. To be used
+ * instead of the component's index (see Component Prefix).
+ * To avoid collision with indexOf, it
+ * should (1) be a legal fragment of a cgi parameter, (2) differ from "g",
+ * and (3) not start with a digit.
+ *
+ * @param key
+ * @return
+ */
+ Component setKey(String key);
+
+ /**
+ * Retrieves the programmer-supplied key. Normally, there is no
+ * such key and the method returns null.
+ *
+ * @return the programmer-supplied key.
+ */
+ String getKey();
+
+ /**
+ * Determines whether the component is visible in the request
+ * represented by state.
+ * @see #setVisible setVisible
+ * @see Description of Visibility
+ * above
+ *
+ *
+ * @param state represents the current request
+ * @return true if the component is visible; false
+ * otherwise.
+ * @pre state != null
+ */
+ boolean isVisible(PageState state);
+
+ /**
+ * Changes the visibility of the component. The component will keep the
+ * visibility that is set with this method in subsequent requests to this page.
+ *
+ * @param state represents the current request
+ * @param v true if the component should be visible
+ * @pre state != null
+ * @see Description of Visibility
+ * above
+ */
+ void setVisible(PageState state, boolean v);
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/ConfirmPage.java b/ccm-core/src/main/java/com/arsdigita/bebop/ConfirmPage.java
new file mode 100755
index 000000000..628ed4892
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/ConfirmPage.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+
+import com.arsdigita.bebop.event.FormInitListener;
+import com.arsdigita.bebop.event.FormProcessListener;
+import com.arsdigita.bebop.event.FormSectionEvent;
+import com.arsdigita.bebop.event.PrintEvent;
+import com.arsdigita.bebop.event.PrintListener;
+import com.arsdigita.bebop.form.Submit;
+import com.arsdigita.bebop.parameters.StringParameter;
+import com.arsdigita.web.ParameterMap;
+import com.arsdigita.web.RedirectSignal;
+import com.arsdigita.web.URL;
+
+/**
+ * A Bebop Confirmation Page which should be mounted at ConfirmPage.CONFIRM_URL by the BebopMapDispatcher.
+ * This page takes three URL parameters:
+ *
+ *
+ * The page displays a form asking the confirmation message passed in. If the user hits OK,
+ * Then the page redirects to the OK URL. Otherwise, if the user hits Cancel,
+ * The page redirects to the Cancel URL.
+ * @author Bryan Che
+ */
+
+public class ConfirmPage extends Page {
+ private StringParameter m_ConfirmMsgParam;
+ private StringParameter m_OkUrlParam;
+ private StringParameter m_CancelUrlParam;
+
+ private RequestLocal m_ConfirmMsgRL;
+ private RequestLocal m_OkUrlRL;
+ private RequestLocal m_CancelUrlRL;
+
+ //URL at which to mount this page
+ public static final String CONFIRM_URL = "BEBOP-confirmation-page";
+
+ //URL variable names
+ private static final String CONFIRM_MSG_VAR = "confirm-msg";
+ private static final String OK_URL_VAR = "ok-url";
+ private static final String CANCEL_URL_VAR = "cancel-url";
+
+ public ConfirmPage() {
+ super();
+
+ m_ConfirmMsgParam = new StringParameter(CONFIRM_MSG_VAR);
+ m_OkUrlParam = new StringParameter(OK_URL_VAR);
+ m_CancelUrlParam = new StringParameter(CANCEL_URL_VAR);
+
+ //add global state params
+ addGlobalStateParam(m_ConfirmMsgParam);
+ addGlobalStateParam(m_OkUrlParam);
+ addGlobalStateParam(m_CancelUrlParam);
+
+ //initialize RequestLocals for the URL params
+ m_ConfirmMsgRL = new RequestLocal() {
+ protected Object initialValue(PageState ps) {
+ return ps.getValue(m_ConfirmMsgParam);
+ }
+ };
+ m_OkUrlRL = new RequestLocal() {
+ protected Object initialValue(PageState ps) {
+ return ps.getValue(m_OkUrlParam);
+ }
+ };
+ m_CancelUrlRL = new RequestLocal() {
+ protected Object initialValue(PageState ps) {
+ return ps.getValue(m_CancelUrlParam);
+ }
+ };
+
+ //set the title
+ buildTitle();
+
+ //add the form
+ ConfirmForm cf = new ConfirmForm(m_ConfirmMsgRL, m_OkUrlRL, m_CancelUrlRL);
+ add(cf);
+
+ lock();
+ }
+
+ /**
+ * Returns a URL (minus "http://" string and server name) at which to access the ConfirmPage
+ * with the given Confirmation Message, OK URL, and Cancel URL.
+ * @param sConfirmMsg the Confirmation message to display on the page
+ * @param sOkUrl the URL to which to redirect if the user hits OK
+ * @param sCancelUrl the URL to which to redirect if the user hits Cancel
+ * @return URL at which to access the ConfirmPage
+ */
+ public static String getConfirmUrl(String sConfirmMsg, String sOkUrl, String sCancelUrl) {
+ final ParameterMap params = new ParameterMap();
+
+ params.setParameter(CONFIRM_MSG_VAR, sConfirmMsg);
+ params.setParameter(OK_URL_VAR, sOkUrl);
+ params.setParameter(CANCEL_URL_VAR, sCancelUrl);
+
+ return URL.there("/" + CONFIRM_URL, params).toString();
+ }
+
+ protected void buildTitle() {
+ class ConfirmPagePrintListener implements PrintListener {
+ public void prepare(PrintEvent e) {
+ Label label = (Label) e.getTarget();
+ PageState ps = e.getPageState();
+
+ label.setLabel((String)m_ConfirmMsgRL.get(ps));
+ }
+ }
+
+ setTitle(new Label(new ConfirmPagePrintListener()));
+ }
+
+ private class ConfirmFormPrintListener implements PrintListener {
+ private RequestLocal m_RL;
+
+ ConfirmFormPrintListener(RequestLocal ConfirmMsgRL) {
+ m_RL = ConfirmMsgRL;
+ }
+
+ public void prepare(PrintEvent e) {
+ Label label = (Label) e.getTarget();
+ PageState ps = e.getPageState();
+
+ label.setLabel((String)m_RL.get(ps) );
+ }
+ }
+
+ private class ConfirmForm extends Form implements FormInitListener, FormProcessListener {
+ private Label m_ConfirmMsgLabel;
+ private Submit m_OkButton;
+ private Submit m_CancelButton;
+
+ private RequestLocal m_OkRL;
+ private RequestLocal m_CancelRL;
+
+ private String m_sOkUrl = null;
+ private String m_sCancelUrl = null;
+
+ public ConfirmForm(RequestLocal ConfirmMsgRL, RequestLocal OkUrlRL, RequestLocal CancelUrlRL) {
+ super("ConfirmForm");
+ m_ConfirmMsgLabel = new Label(new ConfirmFormPrintListener(ConfirmMsgRL));
+
+ this.add(m_ConfirmMsgLabel);
+
+ m_OkButton = new Submit("OK");
+ m_OkButton.setButtonLabel("OK");
+ this.add(m_OkButton);
+ m_OkRL = OkUrlRL;
+
+ m_CancelButton = new Submit("Cancel");
+ m_CancelButton.setButtonLabel("Cancel");
+ this.add(m_CancelButton);
+ m_CancelRL = CancelUrlRL;
+
+ this.addInitListener(this);
+ this.addProcessListener(this);
+ }
+
+ public void init(FormSectionEvent e) {
+ PageState ps = e.getPageState();
+
+ //initialize the OK and Cancel URL's
+ m_sOkUrl = (String) m_OkRL.get(ps);
+ m_sCancelUrl = (String) m_CancelRL.get(ps);
+ }
+
+ public void process(FormSectionEvent e) {
+ PageState ps = e.getPageState();
+
+ if (m_OkButton.isSelected(ps)) {
+ throw new RedirectSignal(m_sOkUrl, true);
+ } else {
+ throw new RedirectSignal(m_sCancelUrl, false);
+ }
+ }
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Container.java b/ccm-core/src/main/java/com/arsdigita/bebop/Container.java
new file mode 100755
index 000000000..4b81b5cdb
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/Container.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+
+/**
+ * The common interface that is implemented by all Bebop containers.
+ *
+ * The Container interface extends the Component interface. A container is
+ * simply a component that contains other components.
+ *
+ * @author David Lutterkort
+ * @author Uday Mathur
+ * @version $Id: Container.java 287 2005-02-22 00:29:02Z sskracic $
+ * */
+public interface Container extends Component {
+
+ /**
+ * Adds a component to this container.
+ *
+ * @param pc component to add to this container
+ * @pre pc != null
+ */
+ void add(Component pc);
+
+ /**
+ * Adds a component with the specified layout constraints to this
+ * container. Layout constraints are defined in each layout container as
+ * static ints. To specify multiple constraints, uses bitwise OR.
+ *
+ * @param pc component to add to this container
+ *
+ * @param constraints layout constraints (a
+ * bitwise OR of static ints in the particular layout)
+ *
+ * @pre c != null
+ */
+ void add(Component c, int constraints);
+
+ /**
+ * Returns true if this list contains the specified element.
+ * More formally, returns true if and only if this list
+ * contains at least
+ * one element e such that (o==null ? e==null : o.equals(e)).
+ * true only if the object has been
+ * directly added to this container. If this container contains another
+ * container that contains this object, this method returns
+ * false.
+ *
+ * @param o element whose presence in this container is to be tested
+ *
+ * @return true if this container contains the specified
+ * object directly; false otherwise.
+ * @pre o != null
+ */
+ boolean contains(Object o);
+
+ /**
+ * Gets the component
+ * at the specified position. Each call to the add method increments
+ * the index. Since the user has no control over the index of added
+ * components (other than counting each call to the add method),
+ * this method should be used in conjunction with indexOf.
+ *
+ * @param index the index of the item to be retrieved from this
+ * container
+ *
+ * @return the component at the specified position in this container.
+ *
+ * @pre index >= 0 && index < size()
+ * @post return != null */
+ Component get(int index);
+
+ /**
+ *
+ *
+ * @param pc component to search for
+ *
+ * @return the index in this list of the first occurrence of
+ * the specified element, or -1 if this list does not contain this
+ * element.
+ *
+ * @pre pc != null
+ * @post contains(pc) implies (return >= 0 && return < size())
+ * @post ! contains(pc) implies return == -1
+ */
+ int indexOf(Component pc);
+
+ /**
+ * Returns true if the container contains no components.
+ *
+ * @return true if this container contains no components;
+ * false otherwise.
+ * @post return == ( size() == 0 )
+ */
+ boolean isEmpty();
+
+ /**
+ * Returns the number of elements in this container. This does not
+ * recursively count components that are indirectly contained in this container.
+ *
+ * @return the number of components directly in this container.
+ * @post size() >= 0
+ */
+ int size();
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/ControlLink.java b/ccm-core/src/main/java/com/arsdigita/bebop/ControlLink.java
new file mode 100755
index 000000000..0e844ae1e
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/ControlLink.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+import com.arsdigita.bebop.event.ActionEvent;
+import com.arsdigita.bebop.event.ActionListener;
+import com.arsdigita.util.Assert;
+import com.arsdigita.xml.Element;
+
+/**
+ * A link back to the page in which it is contained. The control link captures
+ * and preserves the current state of the page, and possibly any control events
+ * that have been set. It is most useful inside a {@link
+ * com.arsdigita.bebop.list.ListCellRenderer} or a {@link
+ * com.arsdigita.bebop.table.TableCellRenderer}, where the list or table has
+ * already set up the events 'tight' for the control link to do the right thing.
+ *
+ *
+ * ControlLink l = new ControlLink("click here") {
+ * public void setControlEvent(PageState s) {
+ * s.setControlEvent(fooComponent, "delete", 42);
+ * }
+ * };
+ *
+ *
+ * ActionListener, which will be run when {@link
+ * #respond respond} is called.
+ * @param 1 a listener to add
+ *
+ * @pre l != null
+ * @pre ! isLocked()
+ * @see #respond respond
+ */
+ public void addActionListener(ActionListener l) {
+ Assert.isUnlocked(this);
+ if ( m_actionListeners == null ) {
+ m_actionListeners = new ArrayList();
+ }
+ m_actionListeners.add(l);
+ }
+
+ /**
+ * Removes a previously added ActionListener.
+ * @param 1 the listener to remove
+ * @see #addActionListener addActionListener
+ */
+ public void removeActionListener(ActionListener l) {
+ Assert.isUnlocked(this);
+ if ( m_actionListeners == null ) {
+ return;
+ }
+ m_actionListeners.remove(l);
+ }
+
+ /**
+ * Fires an ActionEvent, which causes all registered
+ * ActionListeners to be run. The source of the event
+ * is the TabbedPane.
+ * @param state the current page state
+ * @pre state != null
+ * @see #respond respond
+ */
+ protected void fireActionEvent(PageState state) {
+ ActionEvent e = null;
+ if (m_actionListeners == null) {
+ return;
+ }
+ for (int i=0; i < m_actionListeners.size(); i++ ) {
+ if ( e == null ) {
+ e = new ActionEvent(this, state);
+ }
+ ((ActionListener) m_actionListeners.get(i)).actionPerformed(e);
+ }
+ }
+
+ /**
+ * Responds to the incoming request. Fires the ActionEvent.
+ * @param state the current page state
+ */
+ @Override
+ public void respond(PageState state) {
+ fireActionEvent(state);
+ }
+
+ /**
+ * Generates the URL for a link and sets it as the "href" attribute
+ * of the parent.
+ *
+ * @param state the current page state
+ * @param parent the parent element
+ */
+ @Override
+ protected void generateURL(PageState state, Element parent) {
+ setControlEvent(state);
+ try {
+ parent.addAttribute("href", state.stateAsURL());
+ } catch (IOException e) {
+ parent.addAttribute("href", "");
+ }
+ exportAttributes(parent);
+ state.clearControlEvent();
+ }
+
+ /**
+ * Sets the page state's control event. Should be overridden by child
+ * classes. By default, the link receives no control events whatsoever.
+ *
+ * @param ps the current page state
+ */
+ // FIXME: Why is this not protected ?
+ public void setControlEvent(PageState ps) {
+ return;
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/DescriptiveComponent.java b/ccm-core/src/main/java/com/arsdigita/bebop/DescriptiveComponent.java
new file mode 100755
index 000000000..9efd421bb
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/DescriptiveComponent.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.globalization.GlobalizedMessage;
+import com.arsdigita.xml.Element;
+
+import org.apache.log4j.Logger;
+
+
+/**
+ * A (Simple) Component with various descriptive information, specifically 'hints'
+ * with explanations about it's proper usage. These hints provide a kind of
+ * online manual.
+ *
+ * @author Peter Boy (pb@zes.uni-bremen.de)
+ * @version $Id: TextStylable.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+abstract public class DescriptiveComponent extends SimpleComponent {
+
+ /** Internal logger instance to faciliate debugging. Enable logging output
+ * by editing /WEB-INF/conf/log4j.properties int the runtime environment
+ * and set com.arsdigita.bebop.DescriptiveComponent=DEBUG
+ * by uncommenting or adding the line. */
+ private static final Logger s_log = Logger.getLogger(DescriptiveComponent.class);
+
+ /** Property to store informational text for the user about the Link, e.g.
+ * how to use it, or when to use it (or not to use it). */
+ private GlobalizedMessage m_hint; //= GlobalizationUtil.globalize("bebop.hint.no_entry_yet");
+
+ /** Property to store a (localized) label (or title) of this widget. A
+ * label is the text (name) displayed for the user to identify and
+ * distinguish the various elements on the screem. */
+ private GlobalizedMessage m_label;
+
+ /**
+ * Sets a popup hint for the component. It usually contains some explanation
+ * for the user about the component, how to use, why it is there, etc.
+ *
+ * @param hint GlobalizedMessage object with the information text.
+ */
+ public void setHint(GlobalizedMessage hint) {
+ m_hint = hint;
+ }
+
+ /**
+ * Retrieve the popup hint for the component. It is specifically meant for
+ * client classes which have to generate the xml on their own and can not
+ * use the generateDescriptionXML method provided.
+ *
+ * @return popup hint message for the component
+ */
+ public GlobalizedMessage getHint() {
+ return m_hint;
+ }
+
+ /**
+ * Sets a popup hint for the Link. It usually contains some explanation for
+ * the user about the link, how to use, why it is there, etc.
+ *
+ * @param label GlobalizedMessage object with the text to identify and
+ * distinguish the component.
+ */
+ public void setLabel(GlobalizedMessage label) {
+ m_label = label;
+ }
+
+ /**
+ * Retrieve the label for the component. It is specifically meant for
+ * client classes which have to generate the XML on their own and can not
+ * use the generateDescriptionXML method provided.
+ *
+ * @return popup hint message for the component
+ */
+ public GlobalizedMessage getLabel() {
+ return m_label;
+ }
+
+ /**
+ * Generates a (J)DOM fragment for clients to include into their generated
+ * XML.
+ *
+ * @param state
+ * @param parent the XML Element instance to add the attributes managed by
+ * by this class
+ */
+ protected void generateDescriptionXML(final PageState state,
+ final Element parent) {
+
+ if (m_label != null) {
+ parent.addAttribute("label", (String) m_label.localize());
+ }
+ if (m_hint != null) {
+ parent.addAttribute("hint", (String) m_hint.localize());
+ }
+ // Do we need this?
+ //exportAttributes(parent);
+ }
+
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Form.java b/ccm-core/src/main/java/com/arsdigita/bebop/Form.java
new file mode 100755
index 000000000..392239323
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/Form.java
@@ -0,0 +1,558 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.bebop.form.Hidden;
+import javax.servlet.ServletException;
+import com.arsdigita.bebop.util.Traversal;
+import com.arsdigita.bebop.util.BebopConstants;
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.web.URL;
+import com.arsdigita.web.Web;
+import com.arsdigita.web.RedirectSignal;
+import com.arsdigita.util.Assert;
+import com.arsdigita.util.UncheckedWrapperException;
+import com.arsdigita.xml.Element;
+import com.arsdigita.globalization.GlobalizedMessage;
+
+import java.util.Iterator;
+
+import org.apache.log4j.Logger;
+
+/**
+ * Represents the visual structure of an HTML form. Forms can be constructed with a Container
+ * argument to specify the type of layout this form will adhere to. The default is a column panel.
+ *
+ *
+ * public class MyForm extends Form implements FormProcessListener {
+ *
+ * private Text m_firstName; private Text m_lastName;
+ *
+ * public MyForm() { super("myform"); add(new Label("First Name:")); m_firstName = new
+ * Text("firstName"); m_firstName.setDefaultValue("John"); add(m_firstName);
+ *
+ * add(new Label("Last Name:")); m_lastName = new Text("lastName");
+ * m_lastName.setDefaultValue("Doe"); m_lastName.addValidationListener(new NotNullValidationListener
+ * ("The last name")); add(m_lastName);
+ *
+ * add(new Submit("save", "Save")); addProcessListener(this); }
+ *
+ * public void process(FormSectionEvent e) { PageState s = e.getPageState();
+ *
+ * System.out.println("You are " + m_firstName.getValue(s) + " " + m_lastName.getValue(s)); } }
+ *
+ *
+ * process method, which prints the user-supplied values.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @author Stas Freidin
+ * @author Rory Solomon
+ * @author David Lutterkort
+ *
+ * @version $Id: Form.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public class Form extends FormSection implements BebopConstants {
+
+ /**
+ * Internal logger instance to faciliate debugging. Enable logging output by editing
+ * /WEB-INF/conf/log4j.properties int hte runtime environment and set
+ * com.arsdigita.bebop.Form=DEBUG by uncommenting or adding the line.
+ */
+ private static final Logger s_log = Logger.getLogger(Form.class);
+
+ /**
+ * Constant for specifying a get submission method for this form. See the W3C HTML specification for
+ * a description of what this attribute does.
+ */
+ public final static String GET = "get";
+
+ /**
+ * Constant for specifying a post submission method for this form. See the W3C HTML specification for
+ * a description of what this attribute does.
+ */
+ public final static String POST = "post";
+
+ /**
+ * The name of the name attribute for the form.
+ */
+ private final static String NAME = "name";
+
+ /**
+ * The name of the method attribute for the form.
+ */
+ private final static String METHOD = "method";
+
+ private String m_action;
+ private boolean m_processInvisible;
+
+ /**
+ * Hold the FormData for one request.
+ */
+ private RequestLocal m_formData;
+
+ /**
+ * Determines whether or not a form is 'redirecting', meaning that it will clear the control
+ * event and redirect to the resulting state after form processing, so that a page reload won't
+ * cause the form to be resubmitted.
+ */
+ private boolean m_isRedirecting = false;
+
+ /**
+ * Constructs a new form with the specified name. At the time of creation, instantiates a new
+ * form model for the form and instantiates a default ColumnPanel to contain the components.
+ *
+ * @param name the name of the form
+ */
+ public Form(String name) {
+ this(name, new GridPanel(2));
+ }
+
+ /**
+ * Constructs a new form with the specified name and container. At the time of creation,
+ * instantiates a new form model for the form and replaces the default ColumnPanel with the
+ * specified container as the implicit container of the components.
+ *
+ * @param name the name attribute of the form
+ * @param panel the implicit container that will hold the components
+ */
+ public Form(String name, Container panel) {
+ super(panel, new FormModel(name));
+ initFormData();
+ setName(name);
+ setProcessInvisible(false);
+ addMagicTag();
+ }
+
+ /**
+ * Writes the output to a DOM to be used with the XSLT template to produce the appropriate
+ * output. If the form is not visible, no output is generated.
+ *
+ *
+ *
+ * @param s the page state used to determine the values of form widgets and page state
+ * attributes
+ * @param parent the XML element to which the form adds its XML representation
+ *
+ * @see PageState#generateXML
+ */
+ @Override
+ public void generateXML(PageState s, Element parent) {
+ if (isVisible(s)) {
+ Element form = generateXMLSansState(s, parent);
+
+ s.setControlEvent(this);
+ s.generateXML(form, getModel().getParametersToExclude());
+ s.clearControlEvent();
+ }
+ }
+
+ /**
+ * Generates the XML representing the form and its widgets, but not the state information from
+ *
+ * <bebop:form action=%url; %bebopAttr;>
+ * .. XML for panel ..
+ * .. XML for page state ..
+ * </bebop:form>
+ *
s.
+ *
+ * @param s represents the curent request
+ * @param parent
+ *
+ * @return the top-level element for the form
+ */
+ protected Element generateXMLSansState(PageState s, Element parent) {
+ Element form = parent.newChildElement("bebop:form", BEBOP_XML_NS);
+
+ // Encode the URL with the servlet session information;
+ // do not use DispatcherHelper.encodeURL because the
+ // ACS global parameters are provided via the FormData.
+ String url = null;
+
+ if (m_action == null) {
+ final URL requestURL = Web.getWebContext().getRequestURL();
+
+ if (requestURL == null) {
+ url = s.getRequest().getRequestURI();
+ } else {
+ url = requestURL.getRequestURI();
+ }
+ } else {
+ url = m_action;
+ }
+
+ form.addAttribute("action", s.getResponse().encodeURL(url));
+
+ exportAttributes(form);
+
+ m_panel.generateXML(s, form);
+
+ generateErrors(s, form);
+
+ return form;
+ }
+
+ /**
+ *
+ * @param ps
+ * @param parent
+ */
+ protected void generateErrors(PageState ps, Element parent) {
+
+ for (Iterator it = getFormData(ps).getErrors(); it.hasNext();) {
+ Element errors = parent.newChildElement(BEBOP_FORMERRORS,
+ BEBOP_XML_NS);
+ Object msg = it.next();
+
+ if (msg == null) {
+ errors.addAttribute("message", "Unknown error");
+ } else {
+ errors.addAttribute("message",
+ (String) ((GlobalizedMessage) msg).localize(ps.getRequest()));
+ }
+ errors.addAttribute("id", getName());
+ }
+
+ }
+
+ /**
+ * state.
+ *
+ * @see #process process(...)
+ *
+ * @param state represents the current request
+ *
+ * @throws javax.servlet.ServletException
+ */
+ @Override
+ public void respond(PageState state) throws ServletException {
+ final FormData data = process(state);
+
+ if (m_isRedirecting && data.isValid()) {
+ state.clearControlEvent();
+
+ throw new RedirectSignal(state.toURL(), true);
+ }
+ }
+
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ // Methods to set the HTML attributes of the FORM element
+ // * * * * * * * * * * * * * * * * * * * * * * * * * * * *
+ /**
+ * Sets the name attribute for the form.
+ *
+ * @param name the name for the form
+ *
+ * @pre ! isLocked()
+ */
+ public void setName(String name) {
+ Assert.isUnlocked(this);
+ setAttribute(NAME, name);
+ }
+
+ /**
+ * Gets the name attribute for this form.
+ *
+ * @return the name for this form.
+ */
+ public String getName() {
+ return (String) getAttribute(NAME);
+ }
+
+ /**
+ * Sets the enctype attribute used in the form element. No encoding
+ * type is specified by default.
+ *
+ * @param encType the encoding type
+ *
+ * @pre ! isLocked()
+ */
+ public void setEncType(String encType) {
+ Assert.isUnlocked(this);
+ setAttribute("enctype", encType);
+ }
+
+ /**
+ * Sets the onSubmit attribute used in the form element. No onsubmit
+ * handler is specified by default.
+ *
+ * @param javascriptCode the javascript code associated with this attribute
+ *
+ * @pre ! isLocked()
+ */
+ public void setOnSubmit(String javascriptCode) {
+ Assert.isUnlocked(this);
+ setAttribute("onSubmit", javascriptCode);
+ }
+
+ /**
+ * Sets the ONRESET attribute used in the FORM element. No onreset
+ * handler is specified by default.
+ *
+ * @param javascriptCode the javascript code associated with this attribute
+ *
+ * @pre ! isLocked()
+ */
+ public void setOnReset(String javascriptCode) {
+ Assert.isUnlocked(this);
+ setAttribute("onReset", javascriptCode);
+ }
+
+ /**
+ * Sets the HTTP method used to submit the form.
+ *
+ * @param method either GET or POST
+ *
+ * @pre ! isLocked()
+ */
+ public void setMethod(String method) {
+ Assert.isUnlocked(this);
+ setAttribute(METHOD, method);
+ }
+
+ private String getMethod() {
+ return getAttribute(METHOD);
+ }
+
+ /**
+ * Returns true if form processing is turned on when the form is invisible.
+ *
+ * @return true if the form listeners should be processed even when the form is not visible on
+ * the page, false otherwise
+ */
+ protected boolean getProcessInvisible() {
+ return m_processInvisible;
+ }
+
+ /**
+ * Turns form processing on/off when the form is invisible.
+ *
+ * @param processInvisible true if the form listeners should be processed even when the form is
+ * not visible on the page
+ */
+ protected void setProcessInvisible(boolean processInvisible) {
+ m_processInvisible = processInvisible;
+ }
+
+ /**
+ * Sets the URL for the form's action attribute. This is the URL to which
+ * submissions will be sent when the user clicks a submit button on the form. By default, the
+ * action is null, instructing the form to set the action to the URL of the page in
+ * which it is used. If the action is set to a different URL, none of the listeners registered
+ * with this form will be run.
+ *
+ * @param action the URL to submit this form to
+ *
+ * @pre ! isLocked()
+ */
+ public void setAction(String action) {
+ Assert.isUnlocked(this);
+ m_action = action;
+ }
+
+ /**
+ * Returns the URL for the form's action attribute.
+ *
+ * @return the URL to which to submit this form.
+ *
+ * @see #setAction setAction
+ */
+ public final String getAction() {
+ return m_action;
+ }
+
+ /**
+ * Processes this form, creating a FormData object. Runs the right set of init,
+ * validation, and process listeners, depending on whether this is an initial request to the
+ * form and whether the form submission was valid. Submission listeners are always run.
+ *
+ * @see #getFormData
+ *
+ * @param state represents the current request
+ *
+ * @return the values extracted from the HTTP request contained in state.
+ *
+ * @throws com.arsdigita.bebop.FormProcessException
+ * @pre state != null
+ * @post return != null
+ */
+ @Override
+ public FormData process(PageState state) throws FormProcessException {
+ Assert.exists(state, "PageState");
+ FormData result = new FormData(getModel(), state.getRequest());
+ setFormData(state, result);
+
+ // Unless invisible form processing is turned on, don't run any
+ // listeners if this form is not visible.
+ if (getProcessInvisible() || state.isVisibleOnPage(this)) {
+ getModel().process(state, result);
+ }
+ return result;
+ }
+
+ /**
+ * Returns the form data constructed by the {@link #process
+ * process} method for the request described by state. Processes the form if it has
+ * not already been processed.
+ *
+ * @param state describes the current request
+ *
+ * @return the values extracted from the HTTP request contained in state, or
+ * null if the form has not been processed yet.
+ *
+ * @pre state != null
+ * @post return != null
+ */
+ public FormData getFormData(PageState state) {
+ return (FormData) m_formData.get(state);
+ }
+
+ /**
+ * Adds a Hidden Tag to this form so that our controller can determine if this is an initial
+ * request.
+ */
+ protected void addMagicTag() {
+ Hidden h = new Hidden(getModel().getMagicTagName());
+ h.setDefaultValue("visited");
+ add(h);
+ }
+
+ /**
+ * Traverses the components contained in this form, collecting parameterModels and Listeners
+ * into this form's FormModel.
+ */
+ protected void traverse() {
+ Traversal formRegistrar = new Traversal() {
+
+ @Override
+ protected void act(Component c) {
+ if (c == Form.this) {
+ return;
+ }
+ if (c instanceof Form) {
+ throw new IllegalStateException("Forms cannot contain other Forms");
+ }
+ c.register(Form.this, getModel());
+ }
+
+ };
+ formRegistrar.preorder(this);
+ }
+
+ /**
+ * Adds this form to the page and traverses the components contained in this form, collecting
+ * parameterModels and Listeners into this form's FormModel.
+ *
+ * @param p page in which to register this form
+ */
+ @Override
+ public void register(Page p) {
+ traverse();
+ p.addComponent(this);
+ }
+
+ /**
+ * TODO
+ *
+ * @param model
+ */
+ public void excludeParameterFromExport(ParameterModel model) {
+ getModel().excludeFormParameterFromExport(model);
+ }
+
+ /**
+ * Initialize m_formData so that accessing the per-request form data forces the
+ * form to be processed on the first access and caches the form data for subsequent requests.
+ */
+ private void initFormData() {
+ m_formData = new RequestLocal() {
+
+ @Override
+ protected Object initialValue(PageState s) {
+ // TODO: We need to come up with the right strategy for
+ // how we deal with FormProcessExceptions. Are they fatal
+ // ? Do we just add them to the form validation errors ?
+ try {
+ return process(s);
+ } catch (FormProcessException e) {
+ s_log.error("Form Process exception", e);
+ throw new UncheckedWrapperException("Form Process error: "
+ + e.getMessage(), e);
+ }
+ }
+
+ };
+ }
+
+ /**
+ * Converts to a String.
+ *
+ * @return a human-readable representation of this.
+ */
+ @Override
+ public String toString() {
+ return super.toString() + " " + "[" + getName() + "," + getAction() + "," + getMethod()
+ + "," + isRedirecting() + "]";
+ }
+
+ /**
+ * Protected access to set the formdata request local. This method is required if a subclass
+ * wishes to override the process method.
+ *
+ * @param state
+ * @param data
+ */
+ protected void setFormData(PageState state, FormData data) {
+ m_formData.set(state, data);
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/FormData.java b/ccm-core/src/main/java/com/arsdigita/bebop/FormData.java
new file mode 100755
index 000000000..0a1ccc694
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/FormData.java
@@ -0,0 +1,832 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Set;
+import com.arsdigita.bebop.parameters.ParameterData;
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.util.Assert;
+import com.arsdigita.globalization.GlobalizedMessage;
+import com.arsdigita.util.URLRewriter;
+
+/**
+ * Manages the data associated with forms and other remote sources.
+
+ * FormData object is to transform
+ * a set of key-value string pairs into a validated set of Java data
+ * objects for use in subsequent processing. In most cases the original
+ * data is an HTTP request.
+
+ * FormModel is used to specify the name and basic data
+ * type of each expected parameter in the set, as well as any
+ * additional validation steps required. The FormData
+ * stores both the transformed data objects and any validation
+ * error messages associated with an individual parameter or the
+ * form as a whole. Once the data has been validated, individual data
+ * objects may be queried from a FormData using the
+ * standard get method of the Map interface.
+ *
+ * FormData objects may also be used to control the
+ * entire lifecycle of self-validating forms, which report errors to
+ * the user in the context of the form itself, rather than on a
+ * separate page.
+ *
+ * FormData class.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @author Stas Freidin
+ * @version $Id: FormData.java 287 2005-02-22 00:29:02Z sskracic $ */
+
+public class FormData implements Map, Cloneable {
+
+
+ private HashMap m_parameterDataValues = new HashMap();
+ private LinkedList m_formErrors;
+ private FormModel m_model;
+
+ private Locale m_locale;
+ private boolean m_isTransformed;
+ private boolean m_isValid;
+
+ private boolean m_isSubmission;
+
+ /**
+ * Ensure that no one can create this object from outside the package
+ * without supplying meaningful parameters
+ */
+ private FormData() {}
+
+ /**
+ * Constructs a new FormData object containing the
+ * transformed and validated query parameters from an HTTP request.
+ *
+ * @param model a FormModel describing the parameters
+ * and validation constraints for this request
+ *
+ * @param request an HTTP request object passed from the servlet
+ * container
+ *
+ * @pre model != null
+ * @pre request != null
+ *
+ * @throws FormProcessException if an error occurs.
+ */
+
+ public FormData(FormModel model, HttpServletRequest request)
+ throws FormProcessException {
+
+ this(model, request, Locale.getDefault());
+ }
+
+ /**
+ * Constructs a new FormData object containing the
+ * transformed and validated query parameters from an HTTP request.
+ *
+ * @param model a FormModel describing the parameters
+ * and validation constraints for this request
+ *
+ * @param request an HTTP request object passed from the servlet
+ * container
+ *
+ * @param isSubmission true if the request should be treated
+ * as a form submission by the user
+ *
+ * @pre model != null
+ * @pre request != null
+ *
+ * @throws FormProcessException if an error occurs.
+ */
+ public FormData(FormModel model, HttpServletRequest request,
+ boolean isSubmission)
+ throws FormProcessException {
+
+ this(model, request, Locale.getDefault(), isSubmission);
+ }
+
+ /**
+ * Constructs a new FormData object containing the
+ * transformed and validated query parameters from an HTTP request.
+ *
+ * @param model a FormModel describing the parameters
+ * and validation constraints for this request
+ *
+ * @param request an HTTP request object passed from the servlet
+ * container
+ *
+ * @param isSubmission true if the request should be treated
+ * as a form submission by the user
+ *
+ * @param fallback a fallback FormData object. If a value for
+ * a parameter in the form model model is not in
+ * the current request parameters, the fallback object is
+ * searched.
+ *
+ * @pre model != null
+ * @pre request != null
+ *
+ * @throws FormProcessException if an error occurs
+ */
+ public FormData(FormModel model, HttpServletRequest request,
+ boolean isSubmission, FormData fallback)
+ throws FormProcessException {
+
+ this(model, request, Locale.getDefault(), isSubmission, fallback);
+ }
+
+ /**
+ * Constructs a new FormData object containing the
+ * transformed and validated query parameters from an HTTP request.
+ * Error messages are provided in the specified locale.
+ *
+ * @param model A FormModel describing the parameters
+ * and validation constraints for this request.
+ *
+ * @param request An HTTP request object passed from the servlet
+ * container.
+ *
+ * @param locale The locale for which all error messages will be
+ * prepared. This may be used in a multilingual environment to
+ * tailor the output to the preferences or geographic location of
+ * individual users on a per-request basis.
+ *
+ * @pre model != null
+ * @pre request != null
+ * @pre locale != null
+ *
+ * @throws FormProcessException if an error occurs
+ */
+ public FormData(FormModel model, HttpServletRequest request, Locale locale)
+ throws FormProcessException {
+ this(model, request, locale,
+ request.getParameter(model.getMagicTagName()) != null);
+ }
+
+ /**
+ * Constructs a new FormData object containing the
+ * transformed and validated query parameters from an HTTP request.
+ * Error messages are provided in the specified locale.
+ *
+ * @param model A FormModel describing the parameters
+ * and validation constraints for this request.
+ *
+ * @param request An HTTP request object passed from the servlet
+ * container.
+ *
+ * @param locale The locale for which all error messages will be
+ * prepared. This may be used in a multilingual environment to
+ * tailor the output to the preferences or geographic location of
+ * individual users on a per-request basis.
+ *
+ * @param isSubmission true if the request should be treated
+ * as a form submission by the user.
+ *
+ * @throws FormProcessException if an error occurs
+ * @pre model != null
+ * @pre request != null
+ * @pre locale != null
+ */
+ public FormData(FormModel model, HttpServletRequest request,
+ Locale locale, boolean isSubmission)
+ throws FormProcessException {
+ this(model, request, locale, isSubmission, null);
+ }
+
+
+ /**
+ * Constructs a new FormData object containing the
+ * transformed and validated query parameters from an HTTP request.
+ * Error messages are provided in the specified locale.
+ *
+ * @param model A FormModel describing the parameters
+ * and validation constraints for this request.
+ *
+ * @param request An HTTP request object passed from the servlet
+ * container.
+ *
+ * @param locale The locale for which all error messages will be
+ * prepared. This may be used in a multilingual environment to
+ * tailor the output to the preferences or geographic location of
+ * individual users on a per-request basis.
+ *
+ * @param isSubmission true if the request should be treated
+ * as a form submission by the user.
+ *
+ * @param fallback a fallback FormData object. If a value for
+ * a parameter in the form model model is not in
+ * the current request parameters, the fallback object is
+ * searched.
+ *
+ * @throws FormProcessException if an error occurs
+ * @pre model != null
+ * @pre request != null
+ * @pre locale != null
+ */
+ public FormData(FormModel model, HttpServletRequest request,
+ Locale locale, boolean isSubmission,
+ FormData fallback)
+ throws FormProcessException {
+
+ Assert.exists(model, "FormModel");
+ Assert.exists(request, "HttpServletRequest");
+ Assert.exists(locale, "Locale");
+
+ m_locale = locale;
+ m_model = model;
+ m_isTransformed = false;
+
+ m_isSubmission = isSubmission;
+ m_isValid = m_isSubmission;
+
+ createParameterData(request, fallback);
+
+ Iterator params = URLRewriter.getGlobalParams(request).iterator();
+ while (params.hasNext()) {
+ ParameterData param = (ParameterData)params.next();
+ setParameter(param.getModel().getName(), param);
+ }
+ }
+
+ /**
+ * Validates this FormData object according to its form model.
+ * If the FormData is already valid, does nothing.
+ *
+ * @param state describes the current page state
+ * @pre state != null
+ */
+ public void validate(PageState state) {
+
+ if (isValid()) {
+ return;
+ }
+
+ m_isValid = true;
+
+ if (m_formErrors != null) {
+ m_formErrors.clear();
+ }
+
+ m_model.validate(state, this);
+ }
+
+ /**
+ * Validates this FormData object against its form model,
+ * regardless of whether the object is currently valid.
+ *
+ * @param state describes the current page state
+ * @pre state != null
+ */
+ public void forceValidate(PageState state) {
+ invalidate();
+ validate(state);
+ }
+
+ /**
+ * Reports a validation error on the form as a whole.
+ *
+ * @param message a String of the error message
+ * @pre message != null
+ * @deprecated refactor and use addError(GlobalizedMessage) instead
+ */
+ public void addError(String message) {
+ addError(new GlobalizedMessage(message));
+ }
+
+ /**
+ * Reports a validation error on the form as a whole.
+ * Uses a GlobalizedMessage for inklusion
+ *
+ * @param message the error message
+ * @pre message != null
+ */
+ public void addError(GlobalizedMessage message) {
+
+ if (m_formErrors == null) {
+ m_formErrors = new LinkedList();
+ }
+
+ m_formErrors.add(message);
+ m_isValid = false;
+ }
+
+ /**
+ * Adds an error message to the ParameterData object associated with
+ * the parameter model identified by name.
+ *
+ * @param name the name of the parameter model to whose
+ * ParameterData the error message will be added
+ *
+ * @param message the text of the error message to add
+ *
+ * @pre name != null
+ * @pre message != null
+ * @deprecated use addError(String name, GlobalizedMessage message) instead
+ */
+ public void addError(String name, String message) {
+
+ ParameterData parameter;
+
+ if (!m_parameterDataValues.containsKey(name)) {
+ throw new IllegalArgumentException
+ ("Attempt to set Error in Non-Existant ParameterData");
+ }
+
+ parameter = (ParameterData) m_parameterDataValues.get(name);
+ parameter.addError(message);
+ m_isValid = false;
+ }
+
+
+ /**
+ * Adds an error message to the ParameterData object associated with
+ * the parameter model identified by name.
+ *
+ * @param name the name of the parameter model to whose
+ * ParameterData the error message will be added
+ *
+ * @param message the text of the error message to add
+ *
+ * @pre name != null
+ * @pre message != null
+ */
+ public void addError(String name, GlobalizedMessage message) {
+
+ ParameterData parameter;
+
+ if (!m_parameterDataValues.containsKey(name)) {
+ throw new IllegalArgumentException
+ ("Attempt to set Error in Non-Existant ParameterData");
+ }
+
+ parameter = (ParameterData) m_parameterDataValues.get(name);
+ parameter.addError(message);
+ m_isValid = false;
+ }
+
+ /**
+ * Returns the errors associated with the specified parameter.
+ *
+ * @param name the name of the parameter whose errors we are interested in
+ *
+ * @return an iterator of errors. Each error is just a string for
+ * now.
+ *
+ * @pre name != null
+ * @post return != null
+ */
+ public Iterator getErrors(String name) {
+
+ ParameterData parameter
+ = (ParameterData)m_parameterDataValues.get(name);
+
+ if (parameter == null) {
+ return Collections.EMPTY_LIST.iterator();
+ }
+
+ return parameter.getErrors();
+ }
+
+ /**
+ * Returns an iterator over all the errors on this form that are not
+ * associated with any particular parameter. Such errors may have
+ * been generated by a FormValidationListener.
+ *
+ * @return an iterator over error messages.
+ * @post return != null
+ */
+ public Iterator getErrors() {
+
+ if (m_formErrors == null) {
+ return Collections.EMPTY_LIST.iterator();
+ }
+
+ return m_formErrors.iterator();
+ }
+
+ /**
+ * Returns an iterator over all of the errors on this form.
+ * This includes both errors associated with particular parameters
+ * and errors associated with the form as a whole.
+ *
+ * @return an iterator over all error messages.
+ * @post return != null
+ */
+ public Iterator getAllErrors() {
+
+ return new Iterator() {
+
+ private Iterator params, paramErrors, formErrors;
+
+ {
+ params = m_parameterDataValues.values().iterator();
+ paramErrors = Collections.EMPTY_LIST.iterator();
+ formErrors = getErrors();
+ }
+
+ private void seekToNextError() {
+ while (! paramErrors.hasNext() && params.hasNext()) {
+ paramErrors
+ = ((ParameterData)params.next()).getErrors();
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ seekToNextError();
+ return paramErrors.hasNext() || formErrors.hasNext();
+ }
+
+ @Override
+ public Object next() throws NoSuchElementException {
+
+ seekToNextError();
+ if (paramErrors.hasNext()) {
+ return paramErrors.next();
+ }
+
+ // An error will be thrown if !formErrors.hasNext()
+ return formErrors.next();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ };
+ }
+
+ /**
+ * Gets the specified ParameterData object.
+ *
+ * @param name the name of the parameterModel to retrieve
+ * @return the parameter data object specified.
+ *
+ */
+ public ParameterData getParameter(String name) {
+ return (ParameterData)m_parameterDataValues.get(name);
+ }
+
+ /**
+ * Sets the ParameterData object identified by the name in this FormData
+ * Object.
+ *
+ * @param name the name of the parameterModel
+ * @param value
+ */
+ public void setParameter(String name, ParameterData value) {
+ m_parameterDataValues.put(name,value);
+ }
+
+ /**
+ * Returns a collection of all the ParameterData objects.
+ *
+ * @return a collection of all the ParameterData objects.
+ */
+ public final Collection getParameters() {
+ return m_parameterDataValues.values();
+ }
+
+ /**
+ * Determines whether this request represents a submission event.
+ *
+ * @return true if this request represents a submission event;
+ * false if it represents an initialization event.
+ */
+ public final boolean isSubmission() {
+ return m_isSubmission;
+ }
+
+ /**
+ * Determines whether the key-value string pairs in the
+ * request have been transformed into Java data objects.
+ *
+ * @return true if the key-value string pairs have been
+ * transformed into Java data objects;
+ * false otherwise.
+ *
+ */
+ public final boolean isTransformed() {
+ return m_isTransformed;
+ }
+
+ /**
+ * Determines whether any errors were found during validation of
+ * a form submission.
+ * @return true if no errors were found; false
+ * otherwise.
+ *
+ */
+ public final boolean isValid() {
+ return m_isValid;
+ }
+
+ /**
+ * Sets isValid to false. We do not allow programmers
+ * to manually toggle the isValid value to true.
+ * Hence this method takes no
+ * arguments and only sets isValid flag to false
+ * @deprecated Use invalidate() instead
+ */
+ public void setInvalid() {
+ invalidate();
+ }
+
+ /**
+ * Set isValid to false. We do not allow programmers
+ * to manually toggle the isValid value to true.
+ */
+ public final void invalidate() {
+ m_isValid = false;
+ }
+
+ // --- private helper methods to initialize object ---
+
+ /**
+ * Sets the value of a parameter within the associated ParameterData
+ * object
+ *
+ * @param name Name of the parameterModel whose ParameterData object
+ * we are setting
+ *
+ * @param value Value to assign the ParmeterData object
+ *
+ */
+ private void setParameterValue(String name, Object value) {
+ ParameterData parameter = (ParameterData) m_parameterDataValues.get(name);
+ if (parameter != null) {
+ parameter.setValue(value);
+ } else {
+ throw new IllegalArgumentException("Parameter " + name +
+ " does not exist");
+ }
+ }
+
+ /**
+ * Iterate through parameterModels extracting values from the
+ * request, and transforming the value according to the parameter
+ * model This code incorporates
+ * ParameterModel.createParameterData(request)
+ *
+ * @param request the HttpServletRequest
+ * @param fallback a fallback FormData object. If any parameter
+ * in the form model does not have a value in the request,
+ * try to locate its value in the fallback object.
+ */
+ private void createParameterData(HttpServletRequest request,
+ FormData fallback)
+ throws FormProcessException {
+ ParameterModel parameterModel;
+ ParameterData parameterData;
+ Iterator parameters = m_model.getParameters();
+
+ while (parameters.hasNext()) {
+ parameterModel = (ParameterModel) parameters.next();
+
+ // createParamterData automagically handles default values
+ // and errors in tranformation.
+
+ Object defaultValue = null;
+ if (fallback != null) {
+ parameterData =
+ fallback.getParameter(parameterModel.getName());
+ if (parameterData != null) {
+ defaultValue = parameterData.getValue();
+ }
+ }
+
+ // specify a default from the fallback
+ parameterData =
+ parameterModel.createParameterData(request,
+ defaultValue,
+ isSubmission());
+ Assert.exists(parameterData);
+ setParameter(parameterModel.getName(), parameterData);
+ }
+ m_isTransformed=true;
+ }
+
+ // --- Public methods to satisfy Map interface ---
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return m_parameterDataValues.containsKey(key);
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ // this is very expensive with ParameterData
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * This is just plain wrong. Either you pretend to be a Map of
+ * things, or you are a Map of ParameterData-s.
+ */
+ @Override
+ public Set entrySet() {
+ return m_parameterDataValues.entrySet();
+ }
+
+ /**
+ * Returns the value contained by the ParameterData object named
+ * by key.
+ * If no key is found, throws IllegalArgumentException.
+ * @param key the parameter data object to retrieve
+ * @return the value in the specified parameter data object.
+ * @throws java.lang.IllegalArgumentException thrown when the key
+ * is not a valid parameter.
+ */
+ @Override
+ public Object get(Object key) throws IllegalArgumentException {
+
+ ParameterData p = getParameter((String)key);
+ if (p != null) {
+ return p.getValue();
+ }
+ throw new IllegalArgumentException("parameter " + key +
+ " not part of the form model");
+ }
+
+ /**
+ * @param m
+ * @return
+ * @deprecated Use get(m.getName()) instead, and then manually check
+ * for model identity
+ */
+ public Object get(ParameterModel m) {
+ ParameterData p = getParameter(m.getName());
+
+ return ( p.getModel() == m ) ? p : null;
+ }
+
+
+ /**
+ * Retrieves a date object for the specified parameter name.
+ * @param key the object to retrieve
+ * @return a date object for the specified parameter name.
+ *
+ */
+ public Date getDate(Object key) {
+ return (Date) get(key);
+ }
+
+ /**
+ * Retrieves an integer object for the specified parameter name.
+ * @param key the object to retrieve
+ * @return an integer object for the specified parameter name.
+ **/
+
+ public Integer getInteger(Object key) {
+ return (Integer) get(key);
+ }
+
+ /**
+ * Retrieves a String object for the specified parameter name.
+ * @param key the object to retrieve
+ * @return a string object for the specified parameter name.
+ **/
+
+ public String getString(Object key) {
+ return (String) get(key);
+ }
+
+
+ @Override
+ public boolean isEmpty() {
+ return m_parameterDataValues.isEmpty();
+ }
+
+ @Override
+ public Set keySet() {
+ return m_parameterDataValues.keySet();
+ }
+
+ @Override
+ public Object put(Object key, Object value) {
+ Object previousValue = get(key);
+ setParameterValue((String)key, value);
+ m_isValid = false;
+ return previousValue;
+ }
+
+ @Override
+ public void putAll(Map t) {
+ for (Iterator i = t.keySet().iterator(); i.hasNext(); ) {
+ String key = (String) i.next();
+ setParameterValue(key, t.get(key));
+ }
+ m_isValid = false;
+ }
+
+ /**
+ *
+ * @param key
+ * @return
+ */
+ @Override
+ public Object remove(Object key) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int size() {
+ return m_parameterDataValues.size();
+ }
+
+ @Override
+ public Collection values() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ *
+ * @return
+ * @throws CloneNotSupportedException
+ */
+ @Override
+ public Object clone() throws CloneNotSupportedException {
+ FormData result = (FormData) super.clone();
+ result.m_parameterDataValues = new HashMap();
+ for (Iterator i= m_parameterDataValues.keySet().iterator();
+ i.hasNext(); ) {
+ Object key = i.next();
+ ParameterData val = (ParameterData) m_parameterDataValues.get(key);
+ result.m_parameterDataValues.put(key, val.clone());
+ }
+ if (m_formErrors != null) {
+ result.m_formErrors = (LinkedList) m_formErrors.clone();
+ }
+
+ return result;
+ }
+
+
+ @Override
+ public String toString() {
+ StringBuilder s = new StringBuilder();
+
+ for (Iterator i = getAllErrors(); i.hasNext();) {
+ s.append(i.next()).append(System.getProperty("line.separator"));
+ }
+
+ return s.toString();
+ }
+
+ /**
+ * Converts to a String.
+ * The method {@link #toString()} returns all errors.
+ *
+ * @return a human-readable representation of this.
+ */
+ public String asString() {
+ String newLine = System.getProperty("line.separator");
+ StringBuilder to = new StringBuilder();
+ to.append(super.toString() + " = {" + newLine);
+ //Map
+ to.append("m_parameterDataValues = ")
+ .append(m_parameterDataValues).append(",").append(newLine);
+ //LinkedList
+ to.append("m_formErrors = " + m_formErrors + "," + newLine);
+ //FormModel
+ to.append("m_model = " + m_model + "," + newLine);
+ to.append("m_locale = " + m_locale + "," + newLine);
+ to.append("m_isTransformed = " + m_isTransformed + "," + newLine);
+ to.append("m_isValid = " + m_isValid + "," + newLine);
+ to.append("m_isSubmission = " + m_isSubmission + newLine);
+ to.append("}");
+ return to.toString();
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/FormModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/FormModel.java
new file mode 100755
index 000000000..e5cb94e89
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/FormModel.java
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.bebop.event.EventListenerList;
+import com.arsdigita.bebop.event.FormInitListener;
+import com.arsdigita.bebop.event.FormProcessListener;
+import com.arsdigita.bebop.event.FormSectionEvent;
+import com.arsdigita.bebop.event.FormSubmissionListener;
+import com.arsdigita.bebop.event.FormValidationListener;
+import com.arsdigita.bebop.parameters.ParameterData;
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.util.Assert;
+import com.arsdigita.util.Lockable;
+import com.arsdigita.util.URLRewriter;
+import com.arsdigita.web.RedirectSignal;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import org.apache.log4j.Logger;
+
+/**
+ * A container for two classes of
+ * objects: ParameterModels and ValidationListeners.
+ *
+ * init method.
+ * That way, the model persists for the lifetime of the servlet, reducing
+ * the memory and processing overhead for each request.
+ * FormModel class.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @author Stas Freidin
+ * @author Rory Solomon
+ * @version $Id: FormModel.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public class FormModel implements Lockable {
+
+ private static final Logger s_log = Logger.getLogger(FormModel.class);
+
+ private static final String MAGIC_TAG_PREFIX = "form.";
+
+ private String m_name = null;
+ private List m_parameterModels = null;
+ private List m_parametersToExclude = null;
+ private boolean m_locked = false;
+ private boolean m_defaultOverridesNull;
+
+ protected EventListenerList m_listenerList;
+
+ /**
+ * Constructs a new form model.
+ *
+ * @param name a URL-encoded keyword used to identify this form model
+ * */
+ public FormModel(String name) {
+ this(name, false);
+ }
+
+ /**
+ * Construct a new form model. The defaultOverridesNull
+ * parameter is passed on to all parameter models that are added to the
+ * form model. If it is true, the parameter model will use
+ * the default value whenever it would normally set the parameter's value
+ * to null, for example if the parameter is missing from the request. If
+ * this value is false, the default parameter value will
+ * only be used if the request being processed is not a submission, but
+ * an initial request for the form model.
+ *
+ * true if the default value for
+ * parameters should be used whenever the value would be
+ * null ordinarily.
+ */
+ FormModel(String name, boolean defaultOverridesNull) {
+ Assert.exists(name, "Name");
+ m_parameterModels = new LinkedList();
+ m_parametersToExclude = new LinkedList();
+ m_listenerList = new EventListenerList();
+ m_name = name;
+ m_defaultOverridesNull = defaultOverridesNull;
+ m_parameterModels.addAll(URLRewriter.getGlobalModels());
+ }
+
+ /**
+ * Returns the name of this form model.
+ *
+ * @return a URL-encoded keyword used to identify requests
+ * conforming to this form model.
+ * */
+ public final String getName() {
+ return m_name;
+ }
+
+ public final void setName(String name) {
+ m_name = name;
+ }
+
+ String getMagicTagName() {
+ return MAGIC_TAG_PREFIX + getName();
+ }
+
+ /**
+ * Adds a parameter model to the form model. The parameter model
+ * should be fully configured before adding it to the form model.
+ *
+ * @param parameter a parameter model object
+ * */
+ public final void addFormParam(ParameterModel parameter) {
+ Assert.exists(parameter, "Parameter");
+ Assert.isUnlocked(this);
+ parameter.setDefaultOverridesNull(m_defaultOverridesNull);
+ m_parameterModels.add(parameter);
+
+ if( s_log.isDebugEnabled() ) {
+ s_log.debug( "Added parameter: " + parameter.getName() + "[" +
+ parameter.getClass().getName() + "]" );
+ }
+ }
+
+
+ /**
+ * Adds a parameter model to the list of parameters that should
+ * not be exported when the form is rendered. Useful examples
+ * of this are for forms that loop back on themselves such as
+ * search forms or a control bar. The parameter model
+ * should be fully configured and have been added to the form model
+ * before adding it to the list of items to exclude
+ *
+ * @param parameter a parameter model object
+ * */
+ public final void excludeFormParameterFromExport(ParameterModel parameter) {
+ Assert.exists(parameter, "Parameter");
+ Assert.isUnlocked(this);
+ m_parametersToExclude.add(parameter);
+ }
+
+
+ /**
+ * Determines whether the form model contains the specified parameter
+ * model.
+ * @param p the parameter model to check for
+ * @return true if the form model contains the specified
+ * parameter model; false otherwise.
+ */
+ public final boolean containsFormParam(ParameterModel p) {
+ Assert.exists(p, "Parameter");
+ return m_parameterModels.contains(p);
+ }
+
+ /**
+ * Returns an iterator over the parameter models contained within
+ * the form model.
+ *
+ * @return an iterator over the parameter models contained within
+ * the form model.
+ * */
+ public final Iterator getParameters() {
+ return m_parameterModels.iterator();
+ }
+
+
+ /**
+ * Returns an iterator over the parameter models that are
+ * contained within the form model but should not be exported
+ * as part of the form's state. This is important for situations
+ * where the form loops back on itself (e.g. a ControlBar or
+ * a Search form).
+ */
+ public final Iterator getParametersToExclude() {
+ return m_parametersToExclude.iterator();
+ }
+
+ /**
+ * Adds a listener that is called as soon as the {@link FormData} has been
+ * initialized with the request parameters, but before any of the init,
+ * validation, or process listeners are run. The listener's
+ * submitted method may throw a
+ * FormProcessException to signal that any further
+ * processing of the form should be aborted.
+ *
+ * @param listener a FormSubmissionListener value
+ */
+ public void addSubmissionListener(FormSubmissionListener listener) {
+ Assert.exists(listener, "Submission Listener");
+ m_listenerList.add(FormSubmissionListener.class, listener);
+ }
+
+ /**
+ * Adds a validation listener, implementing a custom validation
+ * check that applies to the form as a whole. Useful for checks
+ * that require examination of the values of more than one parameter.
+ *
+ * @param listener an instance of a class that implements the
+ * FormValidationListener interface
+ * */
+ public void addValidationListener(FormValidationListener listener) {
+ Assert.exists(listener, "FormValidationListener");
+ Assert.isUnlocked(this);
+ m_listenerList.add(FormValidationListener.class, listener);
+ }
+
+ /**
+ * Adds a listener for form initialization events.
+ * FormInitListener interface
+ * */
+ public void addInitListener(FormInitListener listener) {
+ Assert.exists(listener, "FormInitListener");
+ Assert.isUnlocked(this);
+ m_listenerList.add(FormInitListener.class, listener);
+ }
+
+ /**
+ * Adds a listener for form processing events. FormProcessListener interface
+ * */
+ public void addProcessListener(FormProcessListener listener) {
+ Assert.exists(listener, "FormProcessListener");
+ Assert.isUnlocked(this);
+ m_listenerList.add(FormProcessListener.class, listener);
+ }
+
+ /**
+ * Creates a new FormData object that is populated with default values
+ * (for an initial request) or values from the request (for
+ * a submission).
+ * true if the request is a submission;
+ * false if this is the first request to the form data.
+ */
+ public FormData process(PageState state, boolean isSubmission)
+ throws FormProcessException {
+ Assert.isLocked(this);
+ FormData data = new FormData(this, state.getRequest(), isSubmission);
+ try {
+ process(state, data);
+ } finally {
+ }
+ return data;
+ }
+
+
+ /**
+ * Do the work for the public process method. Uses the
+ * prepopulated FormData and runs listeners on it as
+ * needed.
+ *
+ * @throws FormProcessException if an error occurs
+ */
+ void process(final PageState state, final FormData data)
+ throws FormProcessException {
+ s_log.debug("Processing the form model");
+
+ final FormSectionEvent e = new FormSectionEvent(this, state, data);
+
+ if (data.isSubmission()) {
+ s_log.debug("The request is a form submission; running " +
+ "submission listeners");
+
+ try {
+ fireSubmitted(e);
+ } catch (FormProcessException fpe) {
+ s_log.debug("A FormProcessException was thrown while firing " +
+ "submit; aborting further processing");
+ return;
+ } finally {
+ }
+
+
+ try {
+ s_log.debug("Validating parameters");
+ fireParameterValidation(e);
+
+ s_log.debug("Validating form");
+ fireFormValidation(e);
+ } finally {
+ }
+
+ if (data.isValid()) {
+ s_log.debug("The form data is valid; running process " +
+ "listeners");
+
+ try {
+ fireFormProcess(e);
+ } catch (FormProcessException fpe) {
+ s_log.debug("A FormProcessException was thrown while " +
+ "initializing the form; storing the error", fpe);
+
+ data.addError("Initialization Aborted: " + fpe.getMessages());
+ } finally {
+ }
+ } else {
+ s_log.debug("The form data was not valid; this form " +
+ "will not run its process listeners");
+ }
+ } else {
+ s_log.debug("The request is not a form submission; " +
+ "running init listeners");
+
+ try {
+ fireFormInit(e);
+ } catch (FormProcessException fpe) {
+ s_log.debug("A FormProcessException was thrown while " +
+ "initializing the form; storing the error", fpe);
+
+ data.addError("Initialization Aborted: " + fpe.getMessages());
+ } finally {
+ }
+ }
+ }
+
+ protected void fireSubmitted(FormSectionEvent e)
+ throws FormProcessException {
+ Assert.exists(e.getFormData(), "FormData");
+ Assert.isLocked(this);
+ FormProcessException delayedException = null;
+
+ Iterator i = m_listenerList.getListenerIterator(FormSubmissionListener.class);
+ while (i.hasNext()) {
+ try {
+ ((FormSubmissionListener) i.next()).submitted(e);
+ } catch (FormProcessException ex) {
+ delayedException = ex;
+ }
+ }
+ if ( delayedException != null ) {
+ throw delayedException;
+ }
+ }
+
+ /**
+ * Calls a form initialization listener.
+ *
+ * @param e a FormSectionEvent originating from the form
+ */
+ protected void fireFormInit(FormSectionEvent e) throws FormProcessException {
+ Assert.exists(e.getFormData(), "FormData");
+ Assert.isLocked(this);
+ Iterator i = m_listenerList.getListenerIterator(FormInitListener.class);
+ while (i.hasNext()) {
+ ((FormInitListener) i.next()).init(e);
+ }
+ }
+
+ /**
+ * Private helper method that validates the individual parameters by
+ * calling ParameterValidationListeners from the individual
+ * parameterModels.
+ *
+ * @param e a FormSectionEvent originating from the form
+ * */
+ protected void fireParameterValidation(FormSectionEvent e) {
+ FormData data = e.getFormData();
+ Assert.exists(data, "FormData");
+ Iterator parameters = getParameters();
+ ParameterModel parameterModel;
+ ParameterData parameterData;
+ while (parameters.hasNext()) {
+ parameterModel = (ParameterModel) parameters.next();
+ parameterData = (ParameterData) data.getParameter(parameterModel.getName());
+ try {
+ parameterData.validate();
+ if (!parameterData.isValid()) {
+ data.invalidate();
+ }
+ } catch (FormProcessException fpe) {
+ data.addError("Processing Listener Error: " + fpe.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Private helper method. Validates the form by calling
+ * FormValidationListeners
+ *
+ * @param e a FormSectionEvent originating from the Form
+ * */
+ private void fireFormValidation(FormSectionEvent e) {
+ FormData data = e.getFormData();
+ Assert.exists(data, "FormData");
+ Iterator i = m_listenerList.getListenerIterator(FormValidationListener.class);
+ while (i.hasNext()) {
+ try {
+ ((FormValidationListener) i.next()).validate(e);
+ } catch (FormProcessException fpe) {
+ data.addError(fpe.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Call form process listeners. FormData object. true if this FormModel is locked;
+ * false otherwise.
+ * */
+ public final boolean isLocked() {
+ return m_locked;
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/FormProcessException.java b/ccm-core/src/main/java/com/arsdigita/bebop/FormProcessException.java
new file mode 100644
index 000000000..bb73515cf
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/FormProcessException.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.globalization.GlobalizedMessage;
+import javax.servlet.ServletException;
+
+/**
+ * This class represents exceptions that occur within the processing methods
+ * of any of the form event listeners. Typically the code will catch specific
+ * exceptions such as SQLException and rethrow them as instances
+ * of this class to pass the message to the controller in a standard fashion.
+ *
+ * ServletException, servlets
+ * that do form processing within a doPost or doGet
+ * methods do not need to explicitly catch instances of this class. However,
+ * they may wish to do so for special error reporting to the user, or to notify
+ * the webmaster via e-mail of the problem.
+ *
+ * @version $Id$
+ */
+
+public class FormProcessException extends ServletException {
+
+ /** Globalized version of the exception message, intended for output in the UI */
+ private GlobalizedMessage m_globalizedMessage;
+
+ /**
+ * Constructor using a String as message presented to the user.
+ * @param message
+ * @deprecated Use FormProcessException(GlobalizedMessage) instead. The
+ * error message for the user should always be globalized so it
+ * can be transformed to the current users requested language.
+ */
+ public FormProcessException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructor using both types of messages which may be presented to the
+ * user. It's a kind of fallback just in kind we really need a non-
+ * globalized message. Usage is stropngly discouraged.
+ * @param message
+ * @param globalizedMessage
+ */
+ public FormProcessException(String message,
+ GlobalizedMessage globalizedMessage) {
+ super(message);
+ m_globalizedMessage = globalizedMessage;
+ }
+
+ /**
+ * Constructor using a GlobalizedMessage as the error text presented to the
+ * user. Using this constructor is the strongly recommended way!
+ *
+ * @param globalizedMessage
+ */
+ public FormProcessException(GlobalizedMessage globalizedMessage) {
+ super();
+ m_globalizedMessage = globalizedMessage;
+ }
+
+ /**
+ *
+ * @param message
+ * @param rootCause
+ * @deprecated use FormProcessException(String,GlobalizedMessage,Throwable)
+ * instead
+ */
+ public FormProcessException(String message,
+ Throwable rootCause) {
+ super(message, rootCause);
+ }
+
+ public FormProcessException(String message,
+ GlobalizedMessage globalizedMessage,
+ Throwable rootCause) {
+ super(message, rootCause);
+ m_globalizedMessage = globalizedMessage;
+ }
+
+ public FormProcessException(Throwable rootCause) {
+ super(rootCause);
+ }
+
+ /**
+ * Add a globalized version of the exception message just in case a non-
+ * globalized message enabled constructor has been used.
+ *
+ * @param globalizedMessage the globalized message intended for output in UI
+ */
+ public void setGlobalizedMessage(GlobalizedMessage globalizedMessage) {
+ m_globalizedMessage = globalizedMessage;
+ }
+
+ /**
+ * Retrieve the globalized version of the exception message, intended for
+ * use in the UI widgets.
+ * The standard non-globalizatin enabled exception message is for use in
+ * log entries only!
+ *
+ * @return the globalized message intended for output in UI
+ */
+ GlobalizedMessage getGlobalizedMessage() {
+ return m_globalizedMessage;
+ }
+ /**
+ * In addition to printing the stack trace for this exception, also prints
+ * the stack trace for the root cause, if any. This is a workaround for
+ * those implementations of {@link ServletException} that don't implement
+ * printStackTrace correctly. If you happen to use an
+ * implementation that does, the stack trace for the root cause may be
+ * printed twice, which is not that big of a deal in the grand scheme of
+ * things.
+ */
+ @Override
+ public void printStackTrace() {
+ super.printStackTrace();
+ if (getRootCause() != null) {
+ System.err.print("Root cause: ");
+ getRootCause().printStackTrace();
+ }
+ }
+
+ /**
+ * @param s
+ * @see #printStackTrace()
+ */
+ @Override
+ public void printStackTrace(java.io.PrintStream s) {
+ super.printStackTrace(s);
+ if (getRootCause() != null) {
+ s.println("Root cause: ");
+ getRootCause().printStackTrace(s);
+ }
+ }
+
+ /**
+ * @param s
+ * @see #printStackTrace()
+ */
+ @Override
+ public void printStackTrace(java.io.PrintWriter s) {
+ super.printStackTrace(s);
+ if (getRootCause() != null) {
+ s.println("Root cause: ");
+ getRootCause().printStackTrace(s);
+ }
+ }
+
+ /**
+ * getMessage().Form. A FormSection
+ * contains other Bebop components, most importantly
+ * Widgets and associated listeners. It serves two purposes:
+ *
+ *
+ * FormSection has its own init, validation, and
+ * process listeners, it can do all of its processing without any intervention
+ * from the enclosing form.
+ *
+ * Although a FormSection contains all the same pieces
+ * that a Form does, it can only be used if it is added
+ * directly or indirectly to a Form. FormSections
+ * that are not contained in a Form do not exhibit any useful
+ * behavior.
+ *
+ * @see Form
+ * @see FormModel
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @author Stas Freidin
+ * @author Rory Solomon
+ * @author David Lutterkort
+ *
+ * @version $Id: FormSection.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public class FormSection extends DescriptiveComponent implements Container {
+
+ /** Internal logger instance to faciliate debugging. Enable logging output
+ * by editing /WEB-INF/conf/log4j.properties int the runtime environment
+ * and set com.arsdigita.subsite.FormSection=DEBUG
+ * by uncommenting or adding the line. */
+ private static final Logger s_log = Logger.getLogger(FormSection.class);
+
+ /** Underlying FormModel that stores
+ * the parameter models for all the widgets in this form section. */
+ protected FormModel m_formModel;
+
+ /** The container to which all children are added. A
+ * ColumnPanel by default. */
+ protected Container m_panel;
+
+ /** Contains all the listeners that were added with the various
+ * addXXXListener methods.
+ * We maintain our own list of listeners, so that we can re-send the
+ * events the FormModel generates, but with us as the source, not the
+ * FormModel. */
+ private EventListenerList m_listeners;
+
+ /** Listeners we attach to the FormModel to forward
+ * form model events to our listeners with the right source */
+ private FormSubmissionListener m_forwardSubmission;
+
+ private FormInitListener m_forwardInit;
+ private FormValidationListener m_forwardValidation;
+ private FormProcessListener m_forwardProcess;
+
+ /**
+ * Constructs a new form section. Sets the implicit layout Container of
+ * this FormSection to two column ColumnPanel
+ * by calling the 1-argument constructor.
+ **/
+ public FormSection() {
+ this(new ColumnPanel(2, true));
+ }
+
+ /**
+ * Constructs a new form section. Sets the form model of this
+ * FormSection to a new, anonymous FormModel.
+ *
+ * @param panel
+ **/
+ public FormSection(Container panel) {
+ this(panel, new FormModel("anonymous"));
+ }
+
+ /**
+ * Constructs a new form section. Sets the implicit layout Container of
+ * this FormSection to panel. Sets the form
+ * model of this FormSection to model.
+ *
+ * @param panel the container within this form section that holds the
+ * components that are added to the form section with calls to the
+ * add methods
+ *
+ * @param model the form model for this form section
+ **/
+ protected FormSection(Container panel, FormModel model) {
+ super();
+ m_panel = panel;
+ m_formModel = model;
+ m_listeners = new EventListenerList();
+ }
+
+ /**
+ * Adds a listener that is called as soon as the {@link FormData} has been
+ * initialized with the request parameters but before any of the init,
+ * validation, or process listeners are run. The listener's
+ * submitted method may throw a
+ * FormProcessException to signal that any further
+ * processing of the form should be aborted.
+ *
+ * @param listener a submission listener to run every time the form is
+ * submitted
+ * @see FormModel#addSubmissionListener
+ * @pre listener != null
+ */
+ public void addSubmissionListener(FormSubmissionListener listener) {
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Adding submission listener " + listener + " to " + this);
+ }
+
+ Assert.exists(listener, "Submission Listener");
+ Assert.isUnlocked(this);
+ forwardSubmission();
+ m_listeners.add(FormSubmissionListener.class, listener);
+ }
+
+ /**
+ * Removes the specified submission listener from the
+ * list of submission listeners (if it had previously been added).
+ *
+ * @param listener the submission listener to remove
+ */
+ public void removeSubmissionListener(FormSubmissionListener listener) {
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Removing submission listener " + listener + " from "
+ + this);
+ }
+
+ Assert.exists(listener, "Submission Listener");
+ Assert.isUnlocked(this);
+ m_listeners.remove(FormSubmissionListener.class, listener);
+ }
+
+ /**
+ * Calls the submitted method on all registered submission
+ * listeners.
+ *
+ * @param e the event to pass to the listeners
+ * @throws FormProcessException if one of the listeners throws such an
+ * exception.
+ */
+ protected void fireSubmitted(FormSectionEvent e)
+ throws FormProcessException {
+ Assert.exists(e.getFormData(), "FormData");
+ FormProcessException delayedException = null;
+
+ Iterator i = m_listeners.getListenerIterator(
+ FormSubmissionListener.class);
+ while (i.hasNext()) {
+ final FormSubmissionListener listener = (FormSubmissionListener) i.
+ next();
+
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Firing submission listener " + listener);
+ }
+
+ try {
+ listener.submitted(e);
+ } catch (FormProcessException ex) {
+ s_log.debug(ex);
+ delayedException = ex;
+ }
+ }
+ if (delayedException != null) {
+ throw delayedException;
+ }
+ }
+
+ /**
+ *
+ */
+ protected void forwardSubmission() {
+ if (m_forwardSubmission == null) {
+ m_forwardSubmission = createSubmissionListener();
+ getModel().addSubmissionListener(m_forwardSubmission);
+ }
+ }
+
+ /**
+ * Creates the submission listener that forwards submission events to this
+ * form section.
+ *
+ * @return a submission listener that forwards submission events to this
+ * form section.
+ */
+ protected FormSubmissionListener createSubmissionListener() {
+ return new FormSubmissionListener() {
+
+ @Override
+ public void submitted(FormSectionEvent e)
+ throws FormProcessException {
+ fireSubmitted(new FormSectionEvent(FormSection.this,
+ e.getPageState(),
+ e.getFormData()));
+ }
+ };
+ }
+
+ /**
+ * Adds a listener for form initialization events. Initialization
+ * events occur when a form is initially requested by the user, but
+ * not when the form is subsequently submitted. They typically
+ * perform actions such as querying the database for existing values
+ * to set up an edit form, or obtaining a sequence value to set up a
+ * create form.
+ *
+ * @param listener an instance of a class that implements the
+ * FormInitListener interface
+ * */
+ public void addInitListener(FormInitListener listener) {
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Adding init listener " + listener + " to " + this);
+ }
+
+ Assert.exists(listener, "FormInitListener");
+ Assert.isUnlocked(this);
+ forwardInit();
+ m_listeners.add(FormInitListener.class, listener);
+ }
+
+ /**
+ * Removes the specified init listener from the
+ * list of init listeners (if it had previously been added).
+ *
+ * @param listener the init listener to remove
+ */
+ public void removeInitListener(FormInitListener listener) {
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Removing init listener " + listener + " from " + this);
+ }
+
+ Assert.exists(listener, "Init Listener");
+ Assert.isUnlocked(this);
+ m_listeners.remove(FormInitListener.class, listener);
+ }
+
+ /**
+ * Calls the init method on all registered init
+ * listeners.
+ *
+ * @param e the event to pass to the listeners
+ * @throws FormProcessException if one of the listeners throws such an
+ * exception.
+ */
+ protected void fireInit(FormSectionEvent e) throws FormProcessException {
+ Assert.exists(e.getFormData(), "FormData");
+ Assert.isLocked(this);
+ Iterator i = m_listeners.getListenerIterator(FormInitListener.class);
+ while (i.hasNext()) {
+ final FormInitListener listener = (FormInitListener) i.next();
+
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Firing init listener " + listener);
+ }
+
+ listener.init(e);
+ }
+ }
+
+ /**
+ *
+ */
+ protected void forwardInit() {
+ if (m_forwardInit == null) {
+ m_forwardInit = createInitListener();
+ getModel().addInitListener(m_forwardInit);
+ }
+ }
+
+ /**
+ * Creates the init listener that forwards init events to this form
+ * section.
+ *
+ * @return an init listener that forwards init events to this
+ * form section.
+ */
+ protected FormInitListener createInitListener() {
+ return new FormInitListener() {
+
+ @Override
+ public void init(FormSectionEvent e)
+ throws FormProcessException {
+ fireInit(new FormSectionEvent(FormSection.this,
+ e.getPageState(),
+ e.getFormData()));
+ }
+ };
+ }
+
+ /**
+ * Creates the cancel listener that forwards cancel events to this form
+ * section
+ *
+ * @return an cancel listener
+ */
+ protected FormCancelListener createCancelListener() {
+ return new FormCancelListener() {
+
+ @Override
+ public void cancel(FormSectionEvent e) throws FormProcessException {
+ fireCancel(new FormSectionEvent(FormSection.this,
+ e.getPageState(),
+ e.getFormData()));
+ }
+ };
+ }
+
+ /**
+ * Adds a validation listener, implementing a custom validation
+ * check that applies to the form as a whole. Useful for checks
+ * that require examination of the values of more than one parameter.
+ *
+ * @param listener an instance of a class that implements the
+ * FormValidationListener interface
+ * */
+ public void addValidationListener(FormValidationListener listener) {
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Adding validation listener " + listener + " to " + this);
+ }
+
+ Assert.exists(listener, "FormValidationListener");
+ Assert.isUnlocked(this);
+ forwardValidation();
+ m_listeners.add(FormValidationListener.class, listener);
+ }
+
+ /**
+ * Removes the specified validation listener from the
+ * list of validation listeners (if it had previously been added).
+ *
+ * @param listener a validation listener
+ */
+ public void removeValidationListener(FormValidationListener listener) {
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Removing validation listener " + listener + " from "
+ + this);
+ }
+
+ Assert.exists(listener, "Validation Listener");
+ Assert.isUnlocked(this);
+ m_listeners.remove(FormValidationListener.class, listener);
+ }
+
+ /**
+ * Calls the validate method on all registered validation
+ * listeners.
+ *
+ * @param e the event to pass to the listeners
+ */
+ protected void fireValidate(FormSectionEvent e) {
+ FormData data = e.getFormData();
+ Assert.exists(data, "FormData");
+ Iterator i = m_listeners.getListenerIterator(
+ FormValidationListener.class);
+ while (i.hasNext()) {
+ try {
+ final FormValidationListener listener =
+ (FormValidationListener) i.next();
+
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Firing validation listener " + listener);
+ }
+
+ listener.validate(e);
+ } catch (FormProcessException fpe) {
+ s_log.debug(fpe);
+ data.addError(fpe.getGlobalizedMessage());
+ }
+ }
+ }
+
+ protected void forwardValidation() {
+ if (m_forwardValidation == null) {
+ m_forwardValidation = createValidationListener();
+ getModel().addValidationListener(m_forwardValidation);
+ }
+ }
+
+ /**
+ * Create the validation listener that forwards validation events to this
+ * form section.
+ *
+ * @return a validation listener that forwards validation events to this
+ * form section.
+ */
+ protected FormValidationListener createValidationListener() {
+ return new FormValidationListener() {
+
+ @Override
+ public void validate(FormSectionEvent e) {
+ fireValidate(new FormSectionEvent(FormSection.this,
+ e.getPageState(),
+ e.getFormData()));
+ }
+ };
+ }
+
+ /**
+ * Adds a listener for form processing events. FormProcessListener interface
+ * */
+ public void addProcessListener(final FormProcessListener listener) {
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Adding process listener " + listener + " to " + this);
+ }
+
+ Assert.exists(listener, "FormProcessListener");
+ Assert.isUnlocked(this);
+
+ forwardProcess();
+ m_listeners.add(FormProcessListener.class, listener);
+ }
+
+ /**
+ * Removes the specified process listener from the
+ * list of process listeners (if it had previously been added).
+ *
+ * @param listener the process listener to remove
+ */
+ public void removeProcessListener(FormProcessListener listener) {
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Removing process listener " + listener + " from "
+ + this);
+ }
+
+ Assert.exists(listener, "Process Listener");
+ Assert.isUnlocked(this);
+
+ m_listeners.remove(FormProcessListener.class, listener);
+ }
+
+ protected void forwardProcess() {
+ if (m_forwardProcess == null) {
+ m_forwardProcess = createProcessListener();
+ getModel().addProcessListener(m_forwardProcess);
+ }
+ }
+
+ protected FormProcessListener createProcessListener() {
+ return new FormProcessListener() {
+
+ @Override
+ public void process(FormSectionEvent e)
+ throws FormProcessException {
+ fireProcess(new FormSectionEvent(FormSection.this,
+ e.getPageState(),
+ e.getFormData()));
+ }
+ };
+ }
+
+ /**
+ * Calls the process method on all registered process
+ * listeners.
+ *
+ * @param e the event to pass to the listeners
+ * @throws FormProcessException if one of the listeners throws such an
+ * exception.
+ */
+ protected void fireProcess(FormSectionEvent e)
+ throws FormProcessException {
+ Assert.exists(e.getFormData(), "FormData");
+ Iterator i = m_listeners.getListenerIterator(FormProcessListener.class);
+ while (i.hasNext()) {
+ final FormProcessListener listener = (FormProcessListener) i.next();
+
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Firing process listener " + listener);
+ }
+
+ listener.process(e);
+ }
+ }
+
+ /**
+ * Since a form section cannot be processed, always throws an error.
+ * (Processing of form sections is done by the form in which the
+ * section is contained.)
+ *
+ * @param data
+ * @return
+ * @throws javax.servlet.ServletException because processing a form section
+ * is not meaningful.
+ */
+ public FormData process(PageState data)
+ throws javax.servlet.ServletException {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Adds a listener for form cancellation events. Cancellation
+ * listeners are typically used to clean-up page state and
+ * potentially intermediate changes to the database.
+ *
+ * @param listener an instance of a class that implements the
+ * FormCancelListener interface
+ * */
+ public void addCancelListener(FormCancelListener listener) {
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Adding cancel listener " + listener + " to " + this);
+ }
+
+ Assert.exists(listener, "FormCancelListener");
+ Assert.isUnlocked(this);
+ m_listeners.add(FormCancelListener.class, listener);
+ }
+
+ /**
+ * Removes the specified cancellation listener from the
+ * list of cancellation listeners (if it had previously been added).
+ *
+ * @param listener the cancellation listener to remove
+ */
+ public void removeCancelListener(FormCancelListener listener) {
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Removing cancel listener " + listener + " from " + this);
+ }
+
+ Assert.exists(listener, "Cancel Listener");
+ Assert.isUnlocked(this);
+ m_listeners.remove(FormCancelListener.class, listener);
+ }
+
+ /**
+ * Calls the cancel method on all registered cancellation
+ * listeners.
+ *
+ * @param e the event to pass to the listeners
+ * @throws FormProcessException if one of the listeners throws such an
+ * exception.
+ */
+ protected void fireCancel(FormSectionEvent e)
+ throws FormProcessException {
+ Assert.exists(e.getFormData(), "FormData");
+ Iterator i = m_listeners.getListenerIterator(FormCancelListener.class);
+ while (i.hasNext()) {
+ final FormCancelListener listener = (FormCancelListener) i.next();
+
+ if (s_log.isDebugEnabled()) {
+ s_log.debug("Firing cancel listener " + listener);
+ }
+
+ listener.cancel(e);
+ }
+ }
+
+ /**
+ * Traverses the children this FormSection, collecting parameter models
+ * and listeners into the supplied FormModel. Sets implicit pointers
+ * of widgets in this FormSection to the supplied Form.
+ *
+ * @param f pointer to the form that is set inside Widgets within this
+ * FormSection
+ * @param m the FormModel in which to merge ParameterModels and
+ * Listeners
+ * */
+ @Override
+ public void register(Form f, FormModel m) {
+ m.mergeModel(getModel());
+ }
+
+ /**
+ * Accessor method for this form's FormModel.
+ *
+ * @return FormModel The model of this form.
+ * */
+ protected final FormModel getModel() {
+ return m_formModel;
+ }
+
+ /**
+ * Locks this FormSection, its FormModel, and the implicit Container.
+ * */
+ @Override
+ public void lock() {
+ m_formModel.lock();
+ m_panel.lock();
+ super.lock();
+ }
+
+ @Override
+ public void respond(PageState state) throws javax.servlet.ServletException {
+ //call listeners here.
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns the implicit Container of this FormSection.
+ *
+ * This must not be final, because MetaFrom needs to override it.
+ *
+ * @return
+ */
+ public Container getPanel() {
+ return m_panel;
+ }
+
+ /**
+ * Returns an iterator over the children of this component. If the
+ * component has no children, returns an empty iterator (not
+ * null !).
+ *
+ * @post return != null
+ * */
+ @Override
+ public Iterator children() {
+ return m_panel.children();
+ }
+
+ /**
+ * Builds an XML subtree for this component under the specified
+ * parent. Uses the request values stored in
+ * state.true if this list contains the
+ * specified element. More
+ * formally, returns true if and only if this list contains at least
+ * one element e such that (o==null ? e==null : o.equals(e)).
+ *
+ * This method returns true only if the component has
+ * been directly
+ * added to this container. If this container contains another
+ * container that contains this component, this method returns
+ * false.
+ *
+ * @param o element whose presence in this container is to be tested
+ *
+ * @return true if this Container contains the
+ * specified component directly; false otherwise.
+ *
+ * */
+ @Override
+ public boolean contains(Object o) {
+ return m_panel.contains(o);
+ }
+
+ /**
+ * Returns the
+ * Component at the specified position. Each call to add()
+ * increments the index. This method should be used in conjunction
+ * with indexOf
+ *
+ * @param index The index of the item to be retrieved from this
+ * Container. Since the user has no control over the index of added
+ * components (other than counting each call to add), this method
+ * should be used in conjunction with indexOf.
+ *
+ * @return the component at the specified position in this container
+ * */
+ @Override
+ public Component get(int index) {
+ return (Component) m_panel.get(index);
+ }
+
+ /**
+ *
+ *
+ *
+ * @param pc component to search for
+ *
+ * @return the index in this list of the first occurrence of
+ * the specified element, or -1 if this list does not contain this
+ * element.
+ * */
+ @Override
+ public int indexOf(Component pc) {
+ return m_panel.indexOf(pc);
+ }
+
+ /**
+ * Determines whether the container contains any components.
+ *
+ * @return true if this container contains no components
+ * false otherwise.
+ * */
+ @Override
+ public boolean isEmpty() {
+ return m_panel.isEmpty();
+ }
+
+ /**
+ * Returns the number of elements in this container. This does not
+ * recursively count the components indirectly contained in this container.
+ *
+ * @return the number of components directly in this container.
+ * */
+ @Override
+ public int size() {
+ return m_panel.size();
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/FormStep.java b/ccm-core/src/main/java/com/arsdigita/bebop/FormStep.java
new file mode 100755
index 000000000..a7dbcad81
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/FormStep.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import org.apache.log4j.Logger;
+
+import com.arsdigita.bebop.event.FormInitListener;
+import com.arsdigita.bebop.event.FormSectionEvent;
+import com.arsdigita.bebop.form.Widget;
+import com.arsdigita.bebop.parameters.BooleanParameter;
+import com.arsdigita.bebop.util.Traversal;
+import com.arsdigita.xml.Element;
+
+/**
+ * The FormStep class modifies the behavior of FormSection with respect to
+ * listener firing. Instead of firing init listeners the first time the
+ * enclosing form is displayed on the page, the FormStep class fires init
+ * listeners the first time the FormStep itself is displayed on the page. The
+ * process, validate, and submission listeners are then fired on every
+ * submission following the one in which the init listeners were fired. This
+ * behavior is useful when used in conjunction with {@link MultiStepForm} or
+ * its subclasses to provide initialization in later steps of a multi step
+ * form that depends on the values entered in earlier steps.
+ *
+ * updated chris.gilbert@westsussex.gov.uk - support for session based wizards
+ * (which enable use of actionlinks in wizard)
+
+ * @see Wizard
+ * @see MultiStepForm
+ *
+ * @author rhs@mit.edu
+ * @version $Id: FormStep.java 1414 2006-12-07 14:24:10Z chrisgilbert23 $
+ **/
+
+public class FormStep extends FormSection {
+ private final static Logger s_log = Logger.getLogger(FormStep.class);
+
+ private Form m_form = null;
+
+ // cg - changed to using a parameter that is stored in pagestate so that if there are links
+ // within the form then the init status of the steps is not lost
+ // private Hidden m_initialized;
+
+ private BooleanParameter m_initialized;
+
+
+ /**
+ * Constructs a new FormStep with the given name. The name must uniquely
+ * identify this FormStep within it's enclosing Form.
+ *
+ * @param name A name that uniquely identifies this FormStep within it's
+ * enclosing Form.
+ **/
+
+ public FormStep(String name) {
+ addInitialized(name);
+ }
+
+ /**
+ * Constructs a new FormStep with the given name. The name must uniquely
+ * identify this FormStep within it's enclosing Form.
+ *
+ * @param name A name that uniquely identifies this FormStep within it's
+ * enclosing Form.
+ * @param panel The container used to back this FormStep.
+ **/
+
+ public FormStep(String name, Container panel) {
+ super(panel);
+ addInitialized(name);
+ }
+
+ protected FormStep(String name, Container panel, FormModel model) {
+ super(panel, model);
+ addInitialized(name);
+ }
+
+ public void register(Page p) {
+ super.register(p);
+ p.addComponentStateParam(this, m_initialized);
+ Traversal trav = new Traversal () {
+ protected void act(Component c) {
+ if (c instanceof Widget) {
+ ((Widget) c).setValidateInvisible(false);
+ }
+ }
+ };
+
+ trav.preorder(this);
+ }
+
+ public void register(Form form, FormModel model) {
+ super.register(form, model);
+ m_form = form;
+ }
+
+ private void addInitialized(String name) {
+ // m_initialized = new Hidden(new BooleanParameter(name));
+ // add(m_initialized);
+ m_initialized = new BooleanParameter(name);
+ m_initialized.setDefaultValue(Boolean.FALSE);
+ }
+
+ public boolean isInitialized(PageState ps) {
+ // Object init = m_initialized.getValue(ps);
+ Boolean init = (Boolean)ps.getValue(m_initialized);
+ if (init == null) {
+ s_log.debug("init for step " + m_initialized.getName() + " is null. returning true");
+ // happens if step state is stored in session -
+ // form containing this step clears session
+ // info when processed, but fireProcess invoked
+ // on this step AFTER form is processed. At that point,
+ // the step has been initialised because we are on the
+ // final process at the end of the steps
+ //
+ init = Boolean.TRUE;
+ }
+ return init.booleanValue();
+ }
+
+ private void setInitialized(PageState ps) {
+ //m_initialized.setValue(ps, Boolean.TRUE);
+ ps.setValue(m_initialized, Boolean.TRUE);
+ }
+
+ // Turn off forwarding of init events.
+ protected FormInitListener createInitListener() {
+ return new FormInitListener() {
+ public void init(FormSectionEvent evt) { }
+ };
+ }
+
+ protected void fireSubmitted(FormSectionEvent evt)
+ throws FormProcessException {
+ if (isInitialized(evt.getPageState())) {
+ super.fireSubmitted(evt);
+ }
+ }
+
+ protected void fireValidate(FormSectionEvent evt) {
+ if (isInitialized(evt.getPageState())) {
+ super.fireValidate(evt);
+ }
+ }
+
+ protected void fireProcess(FormSectionEvent evt)
+ throws FormProcessException {
+ s_log.debug("fireprocess invoked on Formstep " + m_initialized.getName());
+ if (isInitialized(evt.getPageState())) {
+ super.fireProcess(evt);
+
+
+ }
+ }
+
+ public void generateXML(PageState ps, Element parent) {
+ if (!isInitialized(ps)) {
+ FormData fd = m_form.getFormData(ps);
+ try {
+ fireInit(new FormSectionEvent(this, ps, fd));
+ setInitialized(ps);
+ } catch (FormProcessException ex) {
+ s_log.debug("initialization aborted", ex);
+ fd.addError("Initialization Aborted: " + ex.getMessages());
+ }
+ }
+
+ super.generateXML(ps, parent);
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/GridPanel.java b/ccm-core/src/main/java/com/arsdigita/bebop/GridPanel.java
new file mode 100755
index 000000000..62e91d764
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/GridPanel.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.bebop.util.BebopConstants;
+import com.arsdigita.bebop.util.PanelConstraints;
+import com.arsdigita.bebop.form.Hidden;
+import com.arsdigita.util.Assert;
+import com.arsdigita.xml.Element;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * GridPanel
+ * by filling the table one row
+ * at a time (filling each row from left to right), from the top of the table
+ * to the bottom.
+ *
+ *
+ *
+ *
+ *
+ *
+ * Horizontal alignment
+ * Use LEFT, CENTER, or
+ * RIGHT.
+ *
+ * Vertical alignment
+ * Use TOP, MIDDLE, or
+ * BOTTOM.
+ *
+ * Full width
+ * Use FULL_WIDTH to instruct the panel to put
+ * the component in a row by itself, spanning the full width of the
+ * table.
+ *
+ * Inserting children
+ * Use INSERT to instruct the panel to
+ * insert the corresponding component, assuming that it will also be
+ * laid out by a ColumnPanel with the same number of
+ * columns.FULL_WIDTH | LEFT | BOTTOM.INSERT constraint fuses the current
+ * GridPanel with the panel of the child to which the
+ * constraint is applied. For example, consider a {@link Form}, that
+ * is to have a 2-column format with labels in the left column
+ * and widgets in the right column. If a {@link FormSection} is added to
+ * the form, it should be included seamlessly into the parent
+ * form. To do this, set the INSERT
+ * constraint when the {@link FormSection} is added to the {@link
+ * Form}'s GridPanel. At the same time, tell the
+ * GridPanel used to lay out the {@link FormSection}
+ * that it is is to be inserted into another panel.
+ * The following
+ * pseudo-code illustrates the example. (It assumes that Form and
+ * FormSection are decorators of the GridPanel.)
+ *
+ * @see BoxPanel
+ * @see SplitPanel
+ * @author David Lutterkort
+ * @author Stanislav Freidin
+ * @author Justin Ross
+ * @version $Id: GridPanel.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public class GridPanel extends SimpleContainer
+ implements BebopConstants, PanelConstraints {
+
+ private static final ChildConstraint DEFAULT_CONSTRAINT
+ = new ChildConstraint();
+
+ private int m_numColumns;
+
+ /*
+ * Explicitly registered constraints for child components. Maps
+ *
+ * Form form = new Form(new GridPanel(2));
+ * FormSection sec = new FormSection(new GridPanel(2, true));
+ * // "true" in the above constructor tells the GridPanel it is inserted.
+ *
+ * sec.add(new Label("Basic Item Metadata"), GridPanel.FULL_WIDTH);
+ * sec.add(new Label("Title:"), GridPanel.RIGHT);
+ * sec.add(new Text("title"));
+ *
+ * form.add(sec, GridPanel.INSERT);
+ * Componentss to Constraints
+ */
+ private Map m_childConstraintMap;
+
+ /*
+ * Is this panel inserted in another one? If so, do not produce
+ * <table> tags.
+ */
+ private boolean m_isInserted;
+
+ /**
+ * Creates a table panel with the specified number of columns.
+ *
+ * @param numColumns the number of columns in the panel
+ */
+ public GridPanel(int numColumns) {
+ this(numColumns, false);
+ }
+
+ /**
+ * Creates a table panel with the specified number of columns and
+ * indicates whether the panel is inserted.
+ *
+ * @param numColumns the number of columns in the panel
+ * @param isInserted true if this panel is to be
+ * printed as a direct child of a GridPanel
+ * with the same number of columns
+ * @see #setInserted
+ */
+ public GridPanel(int numColumns, boolean isInserted) {
+ m_numColumns = numColumns;
+ setInserted(isInserted);
+ m_childConstraintMap = new HashMap();
+ }
+
+ /**
+ * Adds a component, specifying constraints.
+ * @param component the component to add
+ * @param constraints the constraints for the component
+ */
+ public void add(Component component, int constraints) {
+ super.add(component);
+
+ m_childConstraintMap.put(component, new ChildConstraint(constraints));
+ }
+
+ /**
+ * Sets whether this panel will be printed inside a
+ * GridPanel with the same number of columns. If
+ * inserted is true, no <table> tags will be
+ * produced to enclose the child components.
+ * @param true if this panel is to be printed
+ * inside a GridPanel with the same number of columns
+ *
+ */
+ public void setInserted(boolean isInserted) {
+ Assert.isUnlocked(this);
+ m_isInserted = isInserted;
+ }
+
+ /**
+ * Determines whether this panel is to be inserted into another panel.
+ * @return true if this panel is to be inserted into another panel;
+ * false otherwise.
+ * @see #setInserted
+ */
+ public final boolean isInserted() {
+ return m_isInserted;
+ }
+
+ /**
+ * Adds child components as a subtree under table-style nodes. If any of the
+ * direct children are hidden form widgets, they are added directly to
+ * parent rather than included in any of the
+ * cell elements of the panel.
+ *
+ *
+ *
+ * @param pageState
+ * @param parent
+ */
+ @Override
+ public void generateXML(PageState pageState, Element parent) {
+ if (isVisible(pageState)) {
+ if (isInserted()) {
+ generateChildren(pageState, parent);
+ } else {
+ Element panel = parent.newChildElement(BEBOP_GRIDPANEL, BEBOP_XML_NS);
+ exportAttributes(panel);
+ generateChildren(pageState, panel);
+ }
+ }
+ }
+
+ /*
+ * Lay out the child components using constraints registered for them,
+ * generating a DOM tree and extending another.
+ */
+ private void generateChildren(PageState pageState, Element parent) {
+ int positionInRow = 0;
+ boolean newRowRequested = true; // First time through we want a new row.
+ Element row = null;
+ Element cell = null;
+ ChildConstraint constraint = null;
+
+ Iterator iter = children();
+ while (iter.hasNext()) {
+ Component child = (Component)iter.next();
+
+ if (child.isVisible(pageState)) {
+ if (child instanceof Hidden) {
+ child.generateXML(pageState, parent);
+ } else {
+ constraint = getChildConstraint(child);
+
+ if (constraint.m_isInsert) {
+ child.generateXML(pageState, parent);
+
+ newRowRequested = true;
+ } else {
+ if (positionInRow >= m_numColumns
+ || constraint.m_isFullWidth
+ || newRowRequested) {
+ positionInRow = 0;
+
+ row = parent.newChildElement(BEBOP_PANELROW, BEBOP_XML_NS);
+
+ if (constraint.m_isFullWidth) {
+ // If the column was full width, we
+ // want a new row in the next iteration.
+ newRowRequested = true;
+ } else if (newRowRequested) {
+ // Reset to off.
+ newRowRequested = false;
+ }
+ }
+
+ cell = row.newChildElement(BEBOP_CELL, BEBOP_XML_NS);
+
+ child.generateXML(pageState, cell);
+
+ constraint.exportCellAttributes(cell, m_numColumns);
+
+ positionInRow++;
+ }
+ }
+ }
+ }
+ }
+
+ /*
+ * Helper stuff
+ */
+
+ private ChildConstraint getChildConstraint(Component component) {
+ ChildConstraint constraint =
+ (ChildConstraint)m_childConstraintMap.get(component);
+
+ if (constraint == null) {
+ constraint = DEFAULT_CONSTRAINT;
+ }
+
+ return constraint;
+ }
+
+ private static class ChildConstraint {
+ public boolean m_isFullWidth;
+ public boolean m_isInsert;
+ public String m_horizontalAlignment;
+ public String m_verticalAlignment;
+
+ public ChildConstraint() {
+ this(0);
+ }
+
+ public ChildConstraint(int constraints) {
+ if ((constraints & LEFT) != 0) {
+ m_horizontalAlignment = "left";
+ } else if ((constraints & CENTER) != 0) {
+ m_horizontalAlignment = "center";
+ } else if ((constraints & RIGHT) != 0) {
+ m_horizontalAlignment = "right";
+ } else {
+ m_horizontalAlignment = null;
+ }
+
+ if ((constraints & TOP) != 0) {
+ m_verticalAlignment = "top";
+ } else if ((constraints & MIDDLE) != 0) {
+ m_verticalAlignment = "middle";
+ } else if ((constraints & BOTTOM) != 0) {
+ m_verticalAlignment = "bottom";
+ } else {
+ m_verticalAlignment = null;
+ }
+
+ m_isFullWidth = (constraints & FULL_WIDTH) != 0;
+
+ m_isInsert = (constraints & INSERT) != 0;
+ }
+
+ public void exportCellAttributes(Element cell, int numColumns) {
+ if (m_horizontalAlignment != null) {
+ cell.addAttribute("align", m_horizontalAlignment);
+ }
+
+ if (m_verticalAlignment != null) {
+ cell.addAttribute("valign", m_verticalAlignment);
+ }
+
+ if (m_isFullWidth) {
+ cell.addAttribute("colspan", Integer.toString(numColumns));
+ }
+ }
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Label.java b/ccm-core/src/main/java/com/arsdigita/bebop/Label.java
new file mode 100755
index 000000000..0137aba6d
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/Label.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.bebop.event.PrintEvent;
+import com.arsdigita.bebop.event.PrintListener;
+import com.arsdigita.util.Assert;
+import com.arsdigita.globalization.GlobalizedMessage;
+import com.arsdigita.xml.Element;
+
+/**
+ * A text label displayed to the user for information about and identification
+ * of certain parts of the screen. Therefore the label has to use a
+ * GlobalizedMessage for the information presented.
+ *
+ * A Label is meant to provide semantically relevant informatin and may not be
+ * used for fixed arbitrary Text. Use Embedded instead.
+ *
+ * (Previous usage: can be used to generate either some static, fixed
+ * text or a new text string for every request.)
+ *
+ * To modify the information with an already locked label use the {@link
+ * #setLabel(String,PageState)} method which can adjust for each request.
+ *
+ * @author David Lutterkort
+ * @version $Id: Label.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public class Label extends DescriptiveComponent implements Cloneable {
+
+ public static final String BOLD = "b";
+ public static final String ITALIC = "i";
+
+ // the default label
+ private GlobalizedMessage m_label;
+ // a requestlocal set of labels (to avoid printlisteners)
+ private final RequestLocal m_requestLabel = new RequestLocal();
+ private String m_fontWeight;
+
+ /** The setting for output escaping affects how markup in the
+ *
+ * <bebop:gridPanel>
+ * <bebop:panelRow>
+ * <bebop:cell> ... cell contents </bebop:cell>
+ * <bebop:cell> ... cell contents </bebop:cell>
+ * ...
+ * </bebop:panelRow>
+ * <bebop:panelRow>
+ * <bebop:cell> ... cell contents </bebop:cell>
+ * <bebop:cell> ... cell contents </bebop:cell>
+ * ...
+ * </bebop:panelRow>
+ * </bebop:gridPanel>
content is handled.
+ *
+ * Default is false. */
+ private boolean m_escaping = false; // default for a primitive anyway
+ private PrintListener m_printListener;
+
+ /**
+ * Constructor creates a new Label with empty text.
+ */
+ public Label() {
+ // A kind of fallback (or a hack) here. Parameter label is taken as
+ // a key for some (unknown) Resource bundle. Because GlobalizedMessage
+ // will not find a corrresponding message it will display the key
+ // itself, 'faking' a globalized message.
+ m_label = new GlobalizedMessage(" ");
+ }
+
+ /**
+ * Creates a new Label with the specified (fixed) text.
+ *
+ * @param label the text to display
+ * @deprecated refactor to use Label(GlobalizedMessage label) instead
+ */
+ public Label(String label) {
+ this(label, true);
+ }
+
+ /**
+ * Creates a new Label with the specified text and
+ * output escaping turned on if escaping is true.
+ *
+ * The setting for output escaping affects how markup in the
+ * label is handled. For example:
+ *
+ *
+ * @param label the text to display
+ * @param escaping true if output escaping will be in effect;
+ * false if output escaping will be disabled
+ *
+ * @deprecated refactor to Label(GlobalizedMessage label, boolean escaping)
+ * instead
+ */
+ public Label(String label, boolean escaping) {
+ setLabel(label);
+ setOutputEscaping(escaping);
+ }
+
+ /**
+ * escaping is
+ * true. Label that uses the print listener to
+ * generate output.
+ *
+ * @param l the print listener used to produce output
+ */
+ public Label(PrintListener l) {
+ this();
+ addPrintListener(l);
+ }
+
+ /**
+ * Creates a new label with the specified text and fontweight.
+ *
+ * @param label The text to display
+ * @param fontWeight The fontWeight e.g., Label.BOLD
+ *
+ * @deprecated without direct replacement. Refactor to use
+ * Label(GlobalizedMEssage) instead and modify the theme to
+ * use proper text marking. (Or use setFontWeight separately.
+ */
+ public Label(String label, String fontWeight) {
+ this(label, true);
+ m_fontWeight = fontWeight;
+ }
+
+ /**
+ * Provides the Label as Text, localized for the current request.
+ *
+ * Although it is not recommended, this method may be overridden to
+ * dynamically generate the text of the label. Overriding code may need
+ * the page state.
+ * PrintListener is expected to modify the target
+ * of the PrintEvent.
+ *
+ * @param listener the print listener
+ * @throws IllegalArgumentException if listener is null.
+ * @throws IllegalStateException if a print listener has previously been
+ * added.
+ * @pre listener != null
+ */
+ public void addPrintListener(PrintListener listener)
+ throws IllegalStateException, IllegalArgumentException {
+ if (listener == null) {
+ throw new IllegalArgumentException("Argument listener can not be null");
+ }
+ if (m_printListener != null) {
+ throw new IllegalStateException("Too many listeners. Can only have one");
+ }
+ m_printListener = listener;
+ }
+
+ /**
+ * Removes a previously added print listener. If listener is
+ * not the listener that was added with {@link #addPrintListener
+ * addPrintListener}, an IllegalArgumentException will be thrown.
+ *
+ * @param listener the listener that was added with
+ * addPrintListener
+ * @throws IllegalArgumentException if listener is not the
+ * currently registered print listener or is null.
+ * @pre listener != null
+ */
+ public void removePrintListener(PrintListener listener)
+ throws IllegalArgumentException {
+ if (listener == null) {
+ throw new IllegalArgumentException("listener can not be null");
+ }
+ if (listener != m_printListener) {
+ throw new IllegalArgumentException("listener is not registered with this widget");
+ }
+ m_printListener = null;
+ }
+
+ /**
+ * Generates the (J)DOM fragment for a label.
+ *
+ * <bebop:link href="..." type="..." %bebopAttr;/>
+ *
+ *
+ * @param state The current {@link PageState}.
+ * @param parent The XML element to attach the XML to.
+ */
+ @Override
+ public void generateXML(PageState state, Element parent) {
+
+ if (!isVisible(state)) {
+ return;
+ }
+
+ Label target = firePrintEvent(state);
+
+ Element label = parent.newChildElement("bebop:label", BEBOP_XML_NS);
+
+ target.exportAttributes(label);
+ target.generateDescriptionXML(state, label);
+
+ String weight = target.getFontWeight();
+ if (weight != null && weight.length() > 0) {
+ label.addAttribute("weight", weight);
+ }
+
+ if (!target.m_escaping) {
+ label.addAttribute("escape", "yes");
+ } else {
+ label.addAttribute("escape", "no");
+ }
+
+ String key = getGlobalizedMessage()
+ .getKey()
+ .substring(getGlobalizedMessage()
+ .getKey().lastIndexOf(".") + 1);
+
+ // This if clause is needed to prevent printing of keys if the
+ // GlobalizedMessage was created from a String by this class
+ if(!key.equals(target.getLabel(state))) {
+ label.addAttribute("key", key);
+ }
+
+ /*
+ * This may break with normal JDOM. We may need to have a node for
+ * the case where there is no weight. The problem comes in that
+ * setText *may* kill the other content in the node. It will kill
+ * the other text, so it may be a good idea anyways.
+ */
+ label.setText(target.getLabel(state));
+ }
+
+ /**
+ *
+ * @param state
+ * @return
+ */
+ protected Label firePrintEvent(PageState state) {
+ Label l = this;
+
+ if (m_printListener != null) {
+ try {
+ l = (Label) this.clone();
+ m_printListener.prepare(new PrintEvent(this, state, l));
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(
+ "Couldn't clone Label for PrintListener. "
+ + "This probably indicates a serious programming error: "
+ + e.getMessage());
+ }
+ }
+
+ return l;
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/List.java b/ccm-core/src/main/java/com/arsdigita/bebop/List.java
new file mode 100755
index 000000000..32e74865a
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/List.java
@@ -0,0 +1,821 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.servlet.ServletException;
+
+import com.arsdigita.bebop.list.ListModel;
+import com.arsdigita.bebop.list.ListModelBuilder;
+import com.arsdigita.bebop.list.ListCellRenderer;
+import com.arsdigita.bebop.list.DefaultListCellRenderer;
+import com.arsdigita.bebop.event.ActionEvent;
+import com.arsdigita.bebop.event.ActionListener;
+import com.arsdigita.bebop.event.ChangeEvent;
+import com.arsdigita.bebop.event.ChangeListener;
+import com.arsdigita.bebop.event.EventListenerList;
+import com.arsdigita.bebop.parameters.StringParameter;
+import com.arsdigita.util.Assert;
+import com.arsdigita.bebop.util.BebopConstants;
+
+import com.arsdigita.xml.Element;
+
+
+/**
+ * A List, similar to a javax.swing.JList, that
+ * keeps track of a sequence of items and selections of one or more of
+ * these items. A separate model, {@link ListModel}, is used to represent
+ * the items in the list.
+ *
+ * @see ListModel
+ * @see ListModelBuilder
+ * @see com.arsdigita.bebop.list.ListCellRenderer
+ * @author David Lutterkort
+ * @version $Id: List.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public class List extends SimpleComponent implements BebopConstants {
+
+ /**
+ * The name of the StringParameter that the list uses to keep track of
+ * which item is selected.
+ */
+ public static final String SELECTED = "sel";
+
+ /**
+ * The name of the event the list sets when producing links that change
+ * which item is selected.
+ */
+ public static final String SELECT_EVENT = "s";
+
+ /**
+ * The model builder for this list. Is used to produce a new model for
+ * each request served by this List.
+ * @see #setListModelBuilder
+ * @see ArrayListModelBuilder
+ * @see MapListModelBuilder
+ */
+ private ListModelBuilder m_modelBuilder;
+
+ private RequestLocal m_model;
+
+ /**
+ * The renderer used to format list items.
+ * @see DefaultListCellRenderer
+ */
+ private ListCellRenderer m_renderer;
+
+ private EventListenerList m_listeners;
+
+ private SingleSelectionModel m_selection;
+
+ private ChangeListener m_changeListener;
+
+ private Component m_emptyView;
+
+ private boolean m_stateParamsAreRegistered;
+
+
+ /**
+ * List that uses the specified
+ * list model builder to generate
+ * per-request {@link ListModel ListModels}.
+ *
+ * @param b the model builder used for this list
+ * @pre b != null
+ */
+ public List(ListModelBuilder b) {
+ this();
+ m_modelBuilder = b;
+ m_emptyView = null;
+ }
+
+ /**
+ * Creates an empty List.
+ */
+ public List() {
+ // Force the use of the 'right' constructor
+ this((SingleSelectionModel) null);
+ m_selection =
+ new ParameterSingleSelectionModel(new StringParameter(SELECTED));
+ }
+
+ /**
+ * Create an empty List.
+ */
+ public List(SingleSelectionModel selection) {
+ // This is the real constructor. All other constructors must call it
+ // directly or indirectly
+ super();
+ m_renderer = new DefaultListCellRenderer();
+ m_listeners = new EventListenerList();
+ m_selection = selection;
+ setListData(new Object[0]);
+ initListModel();
+ m_emptyView = null;
+ m_stateParamsAreRegistered = true;
+ }
+
+ /**
+ * Creates a new List from an array of objects. Uses an
+ * internal {@link ListModelBuilder}. Each {@link ListModel} that is
+ * built will
+ * iterate through the entries of the object, returning the objects from
+ * calls to {@link ListModel#getElement} and the corresponding index,
+ * which is converted to a String from calls to {@link
+ * ListModel#getKey}.
+ *
+ * @param values an array of items
+ * @pre values != null
+ * @see #setListData(Object[] v)
+ */
+ public List(Object[] values) {
+ this();
+ setListData(values);
+ }
+
+ /**
+ * Creates a new List from a map. Uses an internal {@link
+ * ListModelBuilder}. Each {@link ListModel} that is built will iterate
+ * through the entries in map in the order in which they are
+ * returned by map.entrySet().iterator(). Calls to {@link
+ * ListModel#getElement} return one value in map. Calls to
+ * {@link ListModel#getElement} return the corresponding key, which is
+ * converted
+ * to a String by calling toString() on the key.
+ *
+ * @param map a key-value mapping for the list items
+ * @pre map != null
+ */
+ public List(Map map) {
+ this();
+ setListData(map);
+ }
+
+ /**
+ * Registers this List and its state parameter(s) with the
+ * specified page.
+ *
+ * @param p the page this list is contained in
+ * @pre p != null
+ * @pre ! isLocked()
+ */
+ public void register(Page p) {
+ Assert.isUnlocked(this);
+ if ( m_selection.getStateParameter() != null ) {
+ p.addComponentStateParam(this, m_selection.getStateParameter());
+ }
+ }
+
+ /**
+ * Responds to a request in which this List was the targetted
+ * component. Calls to this method should only be made through links
+ * generated by this list.
+ *
+ * generateXML
+ * is called on each component returned by the renderer.
+ *
+ *
+ * <bebop:list mode="single" %bebopAttr;>
+ * <bebop:cell [selected="selected"] key="itemKey">
+ * ... XML generated for component returned by renderer ...
+ * </bebop:cell>
+ * ... more <bebop:cell> elements, one for each list item ...
+ * </bebop:list>
+ *
+ * @param state the state of the current request
+ * @param parent the element into which XML is generated
+ * @pre state != null
+ * @pre parent != null
+ * @see com.arsdigita.bebop.list.ListCellRenderer
+ */
+ public void generateXML(PageState state, Element parent) {
+
+ if ( ! isVisible(state) ) {
+ return;
+ }
+
+ ListModel m = getModel(state);
+
+ // Check if there are items in the list
+ if(m.next()) {
+
+ // The list has items
+
+ Element list = parent.newChildElement(BEBOP_LIST, BEBOP_XML_NS);
+ exportAttributes(list);
+
+ if (m_layout == VERTICAL) {
+ list.addAttribute("layout", "vertical");
+ } else {
+ list.addAttribute("layout", "horizontal");
+ }
+
+ Component c;
+
+ Object selKey;
+ if(getStateParamsAreRegistered())
+ {
+ selKey = getSelectedKey(state);
+ }
+ else
+ selKey = null;
+
+ int i = 0;
+ do {
+ Element item = list.newChildElement(BEBOP_CELL, BEBOP_XML_NS);
+
+ String key = m.getKey();
+ Assert.exists(key);
+
+ // Converting both keys to String for comparison
+ // since ListModel.getKey returns a String
+ boolean selected = (selKey != null) &&
+ key.equals(selKey.toString());
+
+ item.addAttribute("key", key);
+ if ( selected ) {
+ item.addAttribute("selected", "selected");
+ }
+ if(getStateParamsAreRegistered())
+ state.setControlEvent(this, SELECT_EVENT, key);
+ c = getCellRenderer().getComponent(this, state, m.getElement(),
+ key, i, selected);
+ c.generateXML(state, item);
+ i += 1;
+ } while (m.next());
+
+ } else {
+ // The list has no items
+ if(m_emptyView != null) {
+ // Display the empty view
+ m_emptyView.generateXML(state, parent);
+ } else {
+ // For compatibility reasons, generate an empty
+ // list element. In the future, this should go away
+ Element list = parent.newChildElement(BEBOP_LIST, BEBOP_XML_NS);
+ exportAttributes(list);
+ }
+ }
+
+ state.clearControlEvent();
+ }
+
+
+ /**
+ * ListCellRenderer value
+ * @pre r != null
+ * @pre ! isLocked()
+ * @see com.arsdigita.bebop.list.ListCellRenderer
+ */
+ public final void setCellRenderer(ListCellRenderer r) {
+ Assert.isUnlocked(this);
+ m_renderer = r;
+ }
+
+ /**
+ * Returns the model builder currently used to build each request-specific
+ * {@link ListModel}.
+ *
+ * @return a ListModelBuilder value.
+ * @see #setModelBuilder setModelBuilder
+ * @see ListModelBuilder
+ */
+ public final ListModelBuilder getModelBuilder() {
+ return m_modelBuilder;
+ }
+
+ /**
+ * Sets the model builder used to build each request-specific
+ * {@link ListModel}.
+ *
+ * @param b a ListModelBuilder value
+ * @pre ! isLocked()
+ * @see ListModelBuilder
+ */
+ public final void setModelBuilder(ListModelBuilder b) {
+ Assert.isUnlocked(this);
+ m_modelBuilder = b;
+ }
+
+ /**
+ * Sets the empty view component, which is
+ * shown if there are no items in the list. This component must
+ * be stateless. For example, it could be an Image or a Label.
+ *
+ * @param c the new empty view component
+ */
+ public final void setEmptyView(Component c) {
+ Assert.isUnlocked(this);
+ m_emptyView = c;
+ }
+
+ /**
+ * Gets the empty view component. The empty view component is
+ * shown if there are no items in the list.
+ * @return the empty view component.
+ */
+ public final Component getEmptyView() {
+ return m_emptyView;
+ }
+
+
+ /**
+ * Initialize the private m_model variable. The initial
+ * value is what the model builder returns for the state.
+ */
+ private void initListModel() {
+ m_model = new RequestLocal() {
+ protected Object initialValue(PageState s) {
+ return getModelBuilder().makeModel(List.this, s);
+ }
+ };
+ }
+
+ /**
+ * Gets the list model used in processing the request represented by
+ * state.
+ *
+ * @param state the state of the current request
+ * @return the list model used in processing the request represented by
+ * state.
+ */
+ public ListModel getModel(PageState state) {
+ return (ListModel) m_model.get(state);
+ }
+
+ /**
+ * Sets the list to use for the values in values. Each {@link
+ * ListModel} that is built will iterate through the entries of the object,
+ * returning the objects from calls to {@link ListModel#getElement} and
+ * the corresponding index, which is converted to a String
+ * from calls to {@link ListModel#getKey}.
+ *
+ * @param values an array of items
+ * @pre values != null
+ * @pre ! isLocked()
+ */
+ public void setListData(Object[] values) {
+ Assert.isUnlocked(this);
+ m_modelBuilder = new ArrayListModelBuilder(values);
+ }
+
+ /**
+ * Sets the list to use the entries in map. Each {@link
+ * ListModel} that is built will iterate through the entries in
+ * map in the order in which they are returned by
+ * map.entrySet().iterator(). Calls to {@link
+ * ListModel#getElement} return one value in map. Calls to
+ * {@link ListModel#getElement} return the corresponding key, which is
+ * converted to a String by calling toString()
+ * on the key.
+ * @param map a key-value mapping for the list items
+ * @pre map != null
+ * @pre ! isLocked()
+ */
+ public void setListData(Map map) {
+ Assert.isUnlocked(this);
+ m_modelBuilder = new MapListModelBuilder(map);
+ }
+
+ /**
+ * Gets the selection model. The model keeps track of which list item is
+ * currently selected, and can be used to manipulate the selection
+ * programmatically.
+ *
+ * @return the model used by the list to keep track of the selected list
+ * item.
+ */
+ public final SingleSelectionModel getSelectionModel() {
+ return m_selection;
+ }
+
+ /**
+ * Sets the selection model that the list uses to keep track of the
+ * currently selected list item.
+ *
+ * @param m the new selection model
+ * @pre m != null
+ * @pre ! isLocked()
+ */
+ public final void setSelectionModel(SingleSelectionModel m) {
+ Assert.isUnlocked(this);
+ if ( m_changeListener != null ) {
+ // Transfer the change listener
+ m_selection.removeChangeListener(m_changeListener);
+ m.addChangeListener(m_changeListener);
+ }
+ m_selection = m;
+ }
+
+ /**
+ * Gets the key for the selected list item. This will only
+ * be a valid key
+ * if {@link #isSelected isSelected} is true.
+ *
+ * @param state the state of the current request
+ * @return the key for the selected list item.
+ * @pre isSelected(state)
+ */
+ public Object getSelectedKey(PageState state) {
+ return m_selection.getSelectedKey(state);
+ }
+
+ /**
+ * Sets the selection to the one with the specified key. If
+ * key was not already selected, fires the {@link
+ * ChangeEvent}.
+ *
+ * @param state the state of the current request
+ * @param key the key for the selected list item
+ * @see #fireStateChanged fireStateChanged
+ */
+ public void setSelectedKey(PageState state, String key) {
+ m_selection.setSelectedKey(state, key);
+ }
+
+ /**
+ * Returns true if one of the list items is currently selected.
+ *
+ * @param state the state of the current request
+ * @return true if one of the list items is selected
+ * false otherwise.
+ */
+ public boolean isSelected(PageState state) {
+ return m_selection.isSelected(state);
+ }
+
+ /**
+ * Clears the selection in the request represented by state.
+ *
+ * @param state the state of the current request
+ * @post ! isSelected(state)
+ */
+ public void clearSelection(PageState state) {
+ m_selection.clearSelection(state);
+ }
+
+ /**
+ * Creates the change listener that is used for forwarding change events
+ * fired by
+ * the selection model to change listeners registered with the list. The
+ * returned change listener refires the event with the list,
+ * rather than the selection model, as source.
+ *
+ * @return the change listener used internally by the list.
+ */
+ protected ChangeListener createChangeListener() {
+ return new ChangeListener() {
+ public void stateChanged(ChangeEvent e) {
+ fireStateChanged(e.getPageState());
+ }
+ };
+ }
+
+ /**
+ * Adds a change listener. A change event is fired whenever the selected
+ * list item changes during the processing of a request. The change event
+ * that listeners receive names the list as the source.
+ *
+ * @param l the change listener to run when the selected item changes in
+ * a request
+ * @pre ! isLocked()
+ */
+ public void addChangeListener(ChangeListener l) {
+ Assert.isUnlocked(this);
+ if ( m_changeListener == null ) {
+ m_changeListener = createChangeListener();
+ m_selection.addChangeListener(m_changeListener);
+ }
+ m_listeners.add(ChangeListener.class, l);
+ }
+
+ /**
+ * Removes a change listener. The listener should have been previously
+ * added with {@link #addChangeListener addChangeListener}, although no
+ * error is signalled if the change listener is not found among the
+ * list's listeners.
+ *
+ * @param l the change listener to remove from the list
+ */
+ public void removeChangeListener(ChangeListener l) {
+ Assert.isUnlocked(this);
+ m_listeners.remove(ChangeListener.class, l);
+ }
+
+ /**
+ * Fires a change event to signal that the selected list item has changed
+ * in the request represented by state. The source of the
+ * event is the list.
+ *
+ * @param state the state of the current request
+ */
+ protected void fireStateChanged(PageState state) {
+ Iterator
+ i=m_listeners.getListenerIterator(ChangeListener.class);
+ ChangeEvent e = null;
+
+ while (i.hasNext()) {
+ if ( e == null ) {
+ e = new ChangeEvent(this, state);
+ }
+ ((ChangeListener) i.next()).stateChanged(e);
+ }
+ }
+
+ // Action events
+ /**
+ * Adds an action listener. This method is run whenever {@link #respond respond} is
+ * called on the list. This gives clients a way to track mouse clicks
+ * received by the list.
+ * @param 1 the action listener to add
+ *
+ * @pre l != null
+ * @pre ! isLocked()
+ * @see #respond respond
+ */
+ public void addActionListener(ActionListener l) {
+ Assert.isUnlocked(this);
+ m_listeners.add(ActionListener.class, l);
+ }
+
+ /**
+ * Removes a previously added action listener.
+ * @param 1 the action listener to remove
+ *
+ * @see #addActionListener addActionListener
+ */
+ public void removeActionListener(ActionListener l) {
+ Assert.isUnlocked(this);
+ m_listeners.remove(ActionListener.class, l);
+ }
+
+ /**
+ * Fires an action event signalling that the list received the request
+ * submission. All registered action listeners are run. The source of
+ * the event is the list.
+ * @param state the state of the current request
+ *
+ * @pre state != null
+ * @see #respond respond
+ */
+ protected void fireActionEvent(PageState state) {
+ Iterator
+ i=m_listeners.getListenerIterator(ActionListener.class);
+ ActionEvent e = null;
+
+ while (i.hasNext()) {
+ if ( e == null ) {
+ e = new ActionEvent(this, state);
+ }
+ ((ActionListener) i.next()).actionPerformed(e);
+ }
+ }
+
+ // ListModelBuilder for maps
+
+ /**
+ * Build list models from a map. The list models use the result of
+ * toString() called on the key of the map entries as their
+ * keys and return the associated value as the element for the list items
+ * the list model iterates over.
+ */
+ private static class MapListModelBuilder implements ListModelBuilder {
+ private Map m_map;
+ private boolean m_locked;
+
+ public MapListModelBuilder() {
+ this(Collections.EMPTY_MAP);
+ }
+
+ public MapListModelBuilder(Map m) {
+ m_map = m;
+ }
+
+ public ListModel makeModel(List l, PageState state) {
+ return new ListModel() {
+ private Iterator i = m_map.entrySet().iterator();
+ private Map.Entry e = null;
+
+ public boolean next() {
+ if ( ! i.hasNext() ) {
+ e = null;
+ return false;
+ }
+ e = (Map.Entry) i.next();
+ return true;
+ }
+
+ public Object getElement() {
+ checkState();
+ return e.getValue();
+ }
+
+ public String getKey() {
+ checkState();
+ return e.getKey().toString();
+ }
+
+ private void checkState() {
+ if ( e == null ) {
+ throw new IllegalStateException("No valid current item. "
+ + "Model is either before first item or after last item");
+ }
+ }
+
+ };
+ }
+
+ public void lock() {
+ m_locked = true;
+ }
+
+ public final boolean isLocked() {
+ return m_locked;
+ }
+ }
+
+ // ListModelBuilder for arrays
+
+ /**
+ * Build list models from an array of values. The list models use the
+ * index of the array entries, converted to a String, as the
+ * key for the list items and the array values as their elements.
+ */
+ private static class ArrayListModelBuilder implements ListModelBuilder {
+ private Object[] m_values;
+ private boolean m_locked;
+
+ public ArrayListModelBuilder() {
+ this(new Object[0]);
+ }
+
+ public ArrayListModelBuilder(Object[] values) {
+ m_values = values;
+ }
+
+ public ListModel makeModel(List l, PageState state) {
+ return new ListModel() {
+ private int i = -1;
+
+ public boolean next() {
+ i += 1;
+ return ( i < m_values.length );
+ }
+
+ public Object getElement() {
+ checkState();
+ return m_values[i];
+ }
+
+ public String getKey() {
+ checkState();
+ return String.valueOf(i);
+ }
+
+ private void checkState() {
+ if ( i < 0 ) {
+ throw new IllegalStateException
+ ("Before first item. Call next() first.");
+ }
+ if ( i >= m_values.length ) {
+ throw new IllegalStateException("After last item. Model exhausted.");
+ }
+ }
+ };
+ }
+
+ public void lock() {
+ m_locked = true;
+ }
+
+ public final boolean isLocked() {
+ return m_locked;
+ }
+ }
+
+ /**
+ * A {@link ListModel} that has no rows.
+ */
+ public static final ListModel EMPTY_MODEL = new ListModel() {
+ public boolean next() {
+ return false;
+ }
+
+ public String getKey() {
+ throw new IllegalStateException("ListModel is empty");
+ }
+
+ public Object getElement() {
+ throw new IllegalStateException("ListModel is empty");
+ }
+ };
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/Page.java b/ccm-core/src/main/java/com/arsdigita/bebop/Page.java
new file mode 100755
index 000000000..6ec4cc0de
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/Page.java
@@ -0,0 +1,1344 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.bebop.event.ActionEvent;
+import com.arsdigita.bebop.event.ActionListener;
+import com.arsdigita.bebop.event.RequestEvent;
+import com.arsdigita.bebop.event.RequestListener;
+import com.arsdigita.bebop.parameters.BitSetParameter;
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.bebop.parameters.StringParameter;
+import com.arsdigita.bebop.util.Traversal;
+import com.arsdigita.kernel.KernelConfig;
+import com.arsdigita.util.Assert;
+import com.arsdigita.util.SystemInformation;
+import com.arsdigita.xml.Document;
+import com.arsdigita.xml.Element;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.log4j.Logger;
+
+/**
+ * The top-level container for all Bebop components and containers.
+ *
+ *
+ *
+ *
+ * A typical Page may be created as follows: null
+ * Page p = new Page("Hello World");
+ * p.add(new Label("Hello World");
+ * p.lock();
+ *
Container that renders this Page.
+ */
+ protected Container m_panel;
+ private List m_actionListeners;
+ private List m_requestListeners;
+ /**
+ * The title of the page to be added in the head of HTML output. The title
+ * is wrapped in a Label to allow developers to add PrintListeners to
+ * dynamically change the value of the title.
+ */
+ private Label m_title;
+ /**
+ * Stores the actual title for the current request. The title may be
+ * generated with a PrintListener of the m_title Label.
+ */
+ private RequestLocal m_currentTitle;
+ /**
+ * A list of all the client-side stylesheets. The elements of the list are
+ * of type Page.Stylesheet, defined at the end of this file.
+ */
+ private List m_clientStylesheets;
+ private StringParameter m_selected;
+ private StringParameter m_controlEvent;
+ private StringParameter m_controlValue;
+ /**
+ * The default (initial) visibility of components. The encoding is identical
+ * to that for PageState.m_invisible.
+ *
+ * This variable is package-friendly since it needs to be accessed by
+ * PageState.
+ */
+ protected BitSet m_invisible;
+ /**
+ * The PageErrorDisplay component that will display page state validation
+ * errors on this page
+ */
+ private Component m_errorDisplay;
+ /**
+ * Indicates whether finish() has been called on this Page.
+ */
+ private boolean m_finished = false;
+ /**
+ * indicates whether pageState.stateAsURL() should export the entire state
+ * for this page, or whether it should only export the control event as a
+ * URL and use the HttpSession for the rest of the page state.
+ */
+ private boolean m_useHttpSession = false;
+
+ /**
+ * Returns true if this page should export state through the
+ * HttpSession instead of the URL query string.
+ *
+ * If this returns true, then PageState.stateAsURL() will only
+ * export the control event as a URL query string. If this returns
+ * false, then stateAsURL() will export the entire page state.
+ *
+ * @see PageState#stateAsURL
+ *
+ * @return true if this page should export state through the
+ * HttpSession; false if it should export using the URL
+ * query string.
+ */
+ public boolean isUsingHttpSession() {
+ return m_useHttpSession;
+ }
+
+ /**
+ * Indicates to this page whether it should export its entire state to
+ * subsequent requests through the URL query string, or if it should use the
+ * HttpSession instead and only use the URL query string for the control
+ * event.
+ *
+ * @see PageState#stateAsURL
+ *
+ * @param b true if PageState.stateAsURL() will export only the
+ * control event as a URL query string. false if
+ * stateAsURL() will export the entire page state.
+ */
+ public void setUsingHttpSession(boolean b) {
+ m_useHttpSession = b;
+ }
+
+ // ////////////////////////////////////////////////////////////////////////
+ // Constructor Section
+ // ////////////////////////////////////////////////////////////////////////
+ /**
+ * Constructor, creates an empty page with the specified title and panel.
+ *
+ * @param title title for this page
+ * @param panel container for this page
+ *
+ * @deprecated use Page(Lab el, Container) instead.
+ */
+ public Page(String title, Container panel) {
+ this(new Label(title), panel);
+ }
+
+ /**
+ * Constructor, creates an empty page with the specified title and panel.
+ *
+ * @param title title for this page as (globalized) Label
+ * @param panel container for this page
+ */
+ public Page(Label title, Container panel) {
+ super();
+ m_actionListeners = new LinkedList();
+ m_requestListeners = new LinkedList();
+ m_panel = panel;
+ m_clientStylesheets = new ArrayList();
+ m_components = new ArrayList();
+ m_componentMap = new TreeMap();
+ setErrorDisplay(new PageErrorDisplay());
+ m_title = title;
+
+ // Initialize the RequestLocal where the title for the current
+ // request will be kept
+ m_currentTitle = new RequestLocal() {
+
+ @Override
+ protected Object initialValue(PageState state) {
+ return m_title.firePrintEvent(state);
+ }
+
+ };
+
+ // Initialize the set of state parameters to hold
+ // the ones necessary for keeping track of the selected component and
+ // the name and value of a 'control event'
+ m_selected = new StringParameter(SELECTED);
+ m_controlEvent = new StringParameter(CONTROL_EVENT);
+ m_controlValue = new StringParameter(CONTROL_VALUE);
+
+ m_stateModel = new FormModel("stateModel", true);
+ m_stateModel.addFormParam(m_selected);
+ m_stateModel.addFormParam(m_controlEvent);
+ m_stateModel.addFormParam(m_controlValue);
+
+ // Set up the visibility tracking parameters
+ m_invisible = new BitSet(32);
+ BitSetParameter p = new BitSetParameter(INVISIBLE,
+ BitSetParameter.ENCODE_DGAP);
+ m_stateModel.addFormParam(p);
+ }
+
+ /**
+ * Creates an empty page with default title and implicit BoxPanel container.
+ */
+ public Page() {
+ this("");
+ }
+
+ /**
+ * Creates an empty page with the specified title and implicit BoxPanel
+ * container.
+ *
+ * @param title title for this page
+ */
+ public Page(Label title) {
+ this(title, new BoxPanel());
+ BoxPanel bp = (BoxPanel) m_panel;
+ bp.setWidth("100%");
+ }
+
+ /**
+ * Creates an empty page with the specified title and implicit BoxPanel
+ * container.
+ *
+ * @param title title for this page
+ */
+ public Page(String title) {
+ this(new Label(title));
+ }
+
+ /**
+ * Adds a component to this container.
+ *
+ * @param c component to add to this container
+ */
+ @Override
+ public void add(Component c) {
+ m_panel.add(c);
+
+ }
+
+ /**
+ * Adds a component with the specified layout constraints to this container.
+ * Layout constraints are defined in each layout container as static ints.
+ * To specify multiple constraints, use bitwise OR.
+ *
+ * @param c component to add to this container
+ * @param constraints layout constraints (a bitwise OR of static ints in the
+ * particular layout)
+ */
+ @Override
+ public void add(Component c, int constraints) {
+ m_panel.add(c, constraints);
+ }
+
+ /**
+ * Returns true if this list contains the specified element.
+ * More formally, returns true if and only if this list
+ * contains at least one element e such that (o==null ? e==null :
+ * o.equals(e)).
+ *
+ * This method returns true only if the component has been
+ * directly added to this container. If this container contains another
+ * container that contains this component, this method returns
+ * false.
+ *
+ * @param o element whose presence in this container is to be tested
+ *
+ * @return true if this Container contains the specified
+ * component directly; false otherwise.
+ */
+ @Override
+ public boolean contains(Object o) {
+ return m_panel.contains(o);
+ }
+
+ /**
+ * Returns the component at the specified position. Each call to the add
+ * method increments the index. Since the user has no control over the index
+ * of added components (other than counting each call to add), this method
+ * should be used in conjunction with indexOf.
+ *
+ * @param index the index of the item to be retrieved from this Container
+ *
+ * @return the component at the specified position in this container.
+ */
+ @Override
+ public Component get(int index) {
+ return m_panel.get(index);
+ }
+
+ /**
+ * Gets the index of a component.
+ *
+ * @param c component to search for
+ *
+ * @return the index in this list of the first occurrence of the specified
+ * element, or -1 if this list does not contain this element.
+ *
+ * @pre c != null
+ * @post contains(c) implies (return >= 0) && (return < size()) @pos t
+ * !contains(c) implies return == -1
+ */
+ @Override
+ public int indexOf(Component c) {
+ return m_panel.indexOf(c);
+ }
+
+ /**
+ * Returns true if the container contains no components.
+ *
+ * @return true if this container contains no components;
+ * false otherwise.
+ */
+ @Override
+ public boolean isEmpty() {
+ return m_panel.isEmpty();
+ }
+
+ /**
+ * Returns the number of elements in this container. This does not
+ * recursively count the components that are indirectly contained in this
+ * container.
+ *
+ * @return the number of components directly in this container.
+ */
+ @Override
+ public int size() {
+ return m_panel.size();
+ }
+
+ @Override
+ public Iterator children() {
+ return Collections.singletonList(m_panel).iterator();
+ }
+
+ /**
+ * Returns the panel that the Page uses for rendering its
+ * components.
+ *
+ * @return the panel.
+ */
+ public final Container getPanel() {
+ return m_panel;
+ }
+
+ /**
+ * Set the Container used for rendering components on this page. Caution
+ * should be used with this function, as the existing container is simply
+ * overwritten.
+ *
+ * @param c
+ *
+ * @author Matthew Booth (mbooth@redhat.com)
+ */
+ public void setPanel(Container c) {
+ m_panel = c;
+ }
+
+ /**
+ * Retrieves the title of this page.
+ *
+ * @return the static title of this page.
+ */
+ public final Label getTitle() {
+ return m_title;
+ }
+
+ /**
+ * Retrieves the title of this page as a Bebop label component.
+ *
+ * @param state the state of the current request
+ *
+ * @return the title of the page for the current request.
+ */
+ public final Label getTitle(PageState state) {
+ return (Label) m_currentTitle.get(state);
+ }
+
+ /**
+ * Sets the title for this page from the passed in string.
+ *
+ * @param title title for this page
+ */
+ public void setTitle(String title) {
+ Assert.isUnlocked(this);
+ setTitle(new Label(title));
+ }
+
+ /**
+ * Set the title for this page from the passed in label.
+ *
+ * @param title title for this page
+ */
+ public void setTitle(Label title) {
+ Assert.isUnlocked(this);
+ m_title = title;
+ }
+
+ /**
+ * Sets the {@link Component} that will display the validation errors in the
+ * current {@link PageState}. Any validation error in the
+ * PageState will cause the Page to completely
+ * ignore all other components and render only the error display component.
+ *
+ * By default, a {@link PageErrorDisplay} component is used to display the
+ * validation errors.
+ *
+ * @param c the component that will display the validation errors in the
+ * current PageState
+ */
+ public final void setErrorDisplay(Component c) {
+ Assert.isUnlocked(this);
+ m_errorDisplay = c;
+ }
+
+ /**
+ * Gets the {@link Component} that will display the validation errors in the
+ * current {@link PageState}. Any validation error in the
+ * PageState will cause the Page to completely
+ * ignore all other components and render only the error display component.
+ *
+ * By default, a {@link PageErrorDisplay} component is used to display the
+ * validation errors.
+ *
+ * @return the component that will display the validation errors in the
+ * current PageState.
+ */
+ public final Component getErrorDisplay() {
+ return m_errorDisplay;
+ }
+
+ /**
+ * Adds a client-side stylesheet that should be used in HTML output.
+ * Arbitrarily many client-side stylesheets can be added with this method.
+ * To use a CSS stylesheet, call something like
+ * setStyleSheet("style.css", "text/css").
+ *
+ *
+ * These values will ultimately wind up in a <link> + * tag in the head of the HTML page. + * + *
+ * Note that the stylesheet set with this call has nothing to do with the + * XSLT stylesheet (transformer) that is applied to the XML generated from + * this page! + * + * @param styleSheetURI the location of the stylesheet + * @param mimeType the MIME type of the stylesheet, usually + * text/css + * + * @pre ! isLocked() + */ + public void addClientStylesheet(String styleSheetURI, String mimeType) { + m_clientStylesheets.add(new Stylesheet(styleSheetURI, mimeType)); + } + + /** + * Adds a global state parameter to this page. Global parameters are values + * that need to be preserved between requests, but that have no special + * connection to any of the components on the page. For a page that displays + * details about an item, a global parameter would be used to identify the + * item. + * + * If the parameter was previously added as a component state parameter, its + * name is unmangled and stays unmangled. + * + * @see #addComponentStateParam + * + * @param p the global parameter to add + * + * @pre ! isLocked() + * @pre parameter != null + */ + public void addGlobalStateParam(ParameterModel p) { + Assert.isUnlocked(this); + p.setName(unmangle(p.getName())); + m_stateModel.addFormParam(p); + } + + /** + * Constructs the top nodes of the DOM or JDOM tree. Used by + * generateXML(PageState, Document) below. + *
+ * Generates DOM fragment: + *
+ * <bebop:page> + * <bebop:title> ... value set with setTitle ... </bebop:title> + * <bebop:stylesheet href='styleSheetURI' type='mimeType'> + * ... page content gnerated by children ... + * </bebop:page>The content of the <title> + * element can be set by calling {@link #setTitle setTitle}. The + * <stylesheet> element will only be present if a stylesheet + * has been set with {@link + * #setStyleSheet setStyleSheet}. + * + * @param ps the page state for the current page + * @param parent the DOM node for the whole Document + * + * @return + * + * @pre isLocked() + */ + protected Element generateXMLHelper(PageState ps, Document parent) { + Assert.isLocked(this); + + Element page = parent.createRootElement("bebop:page", BEBOP_XML_NS); + exportAttributes(page); + + /* Generator information */ + exportSystemInformation(page); + + Element title = page.newChildElement("bebop:title", BEBOP_XML_NS); + title.setText(getTitle(ps).getLabel(ps)); + + for (Iterator i = m_clientStylesheets.iterator(); i.hasNext();) { + ((Stylesheet) i.next()).generateXML(page); + } + + return page; + } + + /** + * Constructs a DOM or JDOM tree with all components on the page. The tree + * represents the page that results from the + * {@link javax.servlet.http.HttpServletRequest} kept in the + *
state.
+ *
+ * @param state the page state produced by {@link #process}
+ * @param parent the DOM node for the whole Document
+ *
+ * @see #process process
+ * @pre isLocked()
+ * @pre state != null
+ */
+ public void generateXML(PageState state, Document parent) {
+ // always export page state as HTTP session
+ if (m_useHttpSession) {
+ state.stateAsHttpSession();
+ }
+
+ Element page = generateXMLHelper(state, parent);
+
+ // If the page state has errors, ignore all the components and
+ // render only the error display component
+ if (state.getErrors().hasNext()) {
+ m_errorDisplay.generateXML(state, page);
+ } else {
+ m_panel.generateXML(state, page);
+ }
+
+ if (KernelConfig.getConfig().isDebugEnabled()
+ && debugStructure(state.getRequest())) {
+
+ Element structure = page.newChildElement("bebop:structure",
+ BEBOP_XML_NS);
+
+ showStructure(state, structure);
+ }
+ }
+
+ private static boolean debugStructure(HttpServletRequest req) {
+ return "transform".equals(req.getParameter("debug"));
+ }
+
+ /**
+ * Do nothing. Top-level add nodes is meaningless.
+ *
+ * @param elt
+ */
+ @Override
+ public void generateXML(PageState state, Element elt) {
+ }
+
+ /**
+ * Creates a PageState object and processes it by calling the respond method
+ * on the selected component. Processes a request by notifying the component
+ * from which the process originated and {@link #fireActionEvent
+ * broadcasts} an {@link ActionEvent} to all the listeners that registered
+ * with {@link #addActionListener addActionListener}.
+ *
+ * @see #generateXML(PageState,Document) generateXML
+ *
+ * @param request
+ * @param response
+ *
+ * @return
+ *
+ * @throws javax.servlet.ServletException
+ * @pre isLocked()
+ * @pre request != null
+ * @pre response != null
+ */
+ public PageState process(HttpServletRequest request,
+ HttpServletResponse response)
+ throws ServletException {
+
+ PageState result = new PageState(this, request, response);
+ try {
+ process(result);
+ } finally {
+ }
+ return result;
+ }
+
+ /**
+ * Processes the supplied PageState object according to this PageModel.
+ * Calls the respond method on the selected Bebop component.
+ */
+ public void process(PageState state) throws ServletException {
+ Assert.isLocked(this);
+ try {
+ fireRequestEvent(state);
+ } finally {
+ }
+
+ // Validate the state; any errors in the state will be displayed
+ // by generateXML
+ state.forceValidate();
+
+ if (state.isValid()) {
+ try {
+ state.respond();
+ } finally {
+ }
+ try {
+ fireActionEvent(state);
+ } finally {
+
+ }
+ }
+ }
+
+ /**
+ * Builds a DOM Document from the current request state by doing a
+ * depth-first tree walk on the current set of components in this Page,
+ * calling generateXML on each. Does NOT do the rendering. If the HTTP
+ * response has already been committed, does not build the XML document.
+ *
+ * @return a DOM ready for rendering, or null if the response has already
+ * been committed.
+ *
+ * @post res.isCommitted() == (return == null)
+ */
+ public Document buildDocument(HttpServletRequest req,
+ HttpServletResponse res)
+ throws ServletException {
+ try {
+ Document doc = new Document();
+ PageState state = process(req, res);
+
+ // only generate XML document if the response is not already
+ // committed
+ if (!res.isCommitted()) {
+ try {
+ generateXML(state, doc);
+ } finally {
+ }
+ return doc;
+ } else {
+ return null;
+ }
+ } catch (ParserConfigurationException e) {
+ throw new ServletException(e);
+ }
+ }
+
+ /**
+ * Finishes building the page. The tree of components is traversed and each
+ * component is told to add its state parameters to the page's state model.
+ *
+ * @pre ! isLocked()
+ */
+ private void finish() {
+ if (!m_finished) {
+ Assert.isUnlocked(this);
+
+ Traversal componentRegistrar = new Traversal() {
+
+ @Override
+ protected void act(Component c) {
+ addComponent(c);
+ c.register(Page.this);
+ }
+
+ };
+ if (m_panel == null) {
+ s_log.warn("m_panel is null");
+ }
+ componentRegistrar.preorder(m_panel);
+ if (m_errorDisplay != null) {
+ addComponent(m_errorDisplay);
+ m_errorDisplay.register(Page.this);
+ }
+
+ m_finished = true;
+ }
+ }
+
+ /**
+ * Locks the page and all its components against further modifications.
+ *
+ * + * Locking a page helps in finding mistakes that result from modifying a + * page's structure.
+ */ + @Override + public void lock() { + if (!m_finished) { + finish(); + } + m_stateModel.lock(); + Traversal componentLocker = new Traversal() { + + @Override + protected void act(Component c) { + c.lock(); + } + + }; + + componentLocker.preorder(m_panel); + + super.lock(); + } + + @Override + public void respond(PageState state) throws javax.servlet.ServletException { + throw new UnsupportedOperationException(); + } + + /** + * Registers a listener that is notified whenever a request to this page is + * made, after the selected component has had a chance to respond. + * + * @pre l != null + * @pre ! isLocked() + */ + public void addActionListener(ActionListener l) { + Assert.isUnlocked(this); + m_actionListeners.add(l); + } + + /** + * Remove a previously registered action listener. + * + * @pre l != null + * @pre ! isLocked() + */ + public void removeActionListener(ActionListener l) { + Assert.isUnlocked(this); + m_actionListeners.remove(l); + } + + /** + * Registers a listener that is notified whenever a request to this page is + * made, before the selected component has had a chance to respond. + * + * @pre l != null + * @pre ! isLocked() + */ + public void addRequestListener(RequestListener l) { + Assert.isUnlocked(this); + m_requestListeners.add(l); + } + + /** + * Removes a previously registered request listener. + * + * @param 1 the listener to remove + * + * @pre l != null + * @pre ! isLocked() + */ + public void removeRequestListener(RequestListener l) { + Assert.isUnlocked(this); + m_requestListeners.remove(l); + } + + /** + * Broadcasts an {@link ActionEvent} to all registered listeners. The source + * of the event is this page, and the state recorded in the event is the one + * resulting from processing the current request. + * + * @param the state for this event + * + * @pre state != null + */ + protected void fireActionEvent(PageState state) { + ActionEvent e = null; + + for (Iterator i = m_actionListeners.iterator(); i.hasNext();) { + if (e == null) { + e = new ActionEvent(this, state); + } + + final ActionListener listener = (ActionListener) i.next(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Firing action listener " + listener); + } + + listener.actionPerformed(e); + } + } + + /** + * Broadcasts a {@link RequestEvent} to all registered listeners. The source + * of the event is this page, and the state recorded in the event is the one + * resulting from processing the current request. + * + * @param state the state for this event + * + * @pre state != null + */ + protected void fireRequestEvent(PageState state) { + RequestEvent e = null; + + for (Iterator i = m_requestListeners.iterator(); i.hasNext();) { + if (e == null) { + e = new RequestEvent(this, state); + } + + final RequestListener listener = (RequestListener) i.next(); + + if (s_log.isDebugEnabled()) { + s_log.debug("Firing request listener " + listener); + } + + listener.pageRequested(e); + } + } + + /** + * Export page generator information if set. The m_pageGenerator is a + * HashMap containing the information as key value. In general this should + * include generator name and generator version. + * + * @param page parent element - should be bebeop:page + * + * @pre m_pageGenerator != null && !m_pageGenerator.isEmpty() + */ + final protected void exportSystemInformation(Element page) { + SystemInformation sysInfo = SystemInformation.getInstance(); + if (!sysInfo.isEmpty()) { + Element gen = page.newChildElement("bebop:systemInformation", + BEBOP_XML_NS); + + Iterator+ * Get the parameters registered for a given component.
+ */ + public Collection getComponentParameters(Component c) { + return (Collection) m_componentParameterMap.get(c); + } + + /** + * Gets the state index of a component. This is the number assigned to the + * component in the register traveral + * + * @param c the component to search for + * + * @return the index in this list of the first occurrence of the specified + * element, or -1 if this list does not contain this element. + * + * @pre c != null + * @post contains(c) implies (return >= 0) && (return < size()) @pos t + * !contains(c) implies return == -1 + */ + public int stateIndex(Component c) { + return m_components.indexOf(c); + } + + /** + * The number of components in the page model. + * + * @post return >= 0 + */ + public int stateSize() { + return m_components.size(); + } + + /** + * Checks whether this component is already in the page model. + * + * @pre c != null + */ + public boolean stateContains(Component c) { + return m_components.contains(c); + } + + /** + * Gets a page component by index. + * + * @pre (i >= 0) && (i < size()) @pos t return != null + */ + public Component getComponent(int i) { + return (Component) m_components.get(i); + } + + /** + * Gets a page component by key. + * + * @pre s != null + */ + Component getComponent(String s) { + return (Component) m_componentMap.get(s); + } + + /** + * Gets the form model that contains the parameters for the page's state. + */ + public final FormModel getStateModel() { + return m_stateModel; + } + + /** + * Gets the ParameterModels held in this Page. + * + * @return an iterator of ParameterModels. + */ + public Iterator getParameters() { + return m_stateModel.getParameters(); + } + + /** + * Checks whether the specified component is visible by default on the page. + * + * @param c a component contained in the page + * + * @returntrue if the component is visible by default;
+ * false otherwise.
+ *
+ * @see #setVisibleDefault setVisibleDefault
+ * @see Component#setVisible Component.setVisible
+ */
+ public boolean isVisibleDefault(Component c) {
+ Assert.isTrue(stateContains(c));
+
+ return !m_invisible.get(stateIndex(c));
+ }
+
+ /**
+ * Sets whether the specified component is visible by default. The default
+ * visibility is used when a page is displayed for the first time and on
+ * subsequent requests until the visibility of a component is changed
+ * explicitly with {@link Component#setVisible
+ * Component.setVisible}.
+ *
+ *
+ * When a component is first added to a page, it is visible.
+ *
+ * @param c a component whose visibility is to be set
+ * @param v true if the component is visible;
+ * false otherwise.
+ *
+ * @see Component#setVisible Component.setVisible
+ * @see Component#register Component.register
+ */
+ public void setVisibleDefault(Component c, boolean v) {
+ Assert.isUnlocked(this);
+
+ addComponent(c);
+ int i = stateIndex(c);
+ if (v) {
+ m_invisible.clear(i);
+ } else {
+ m_invisible.set(i);
+ }
+ }
+
+ /**
+ * The global name of the parameter name in the component
+ * c.
+ */
+ public String parameterName(Component c, String name) {
+ if (c == null || !stateContains(c)) {
+ return name;
+ }
+
+ return componentPrefix(c) + name;
+ }
+
+ /**
+ * The global name of the parameter name.
+ */
+ public String parameterName(String name) {
+ return parameterName(null, name);
+ }
+
+ void reset(final PageState ps, Component cmpnt) {
+ Traversal resetter = new Traversal() {
+
+ @Override
+ protected void act(Component c) {
+ Collection cp = getComponentParameters(c);
+ if (cp != null) {
+ Iterator iter = cp.iterator();
+ while (iter.hasNext()) {
+ ParameterModel p = (ParameterModel) iter.next();
+ ps.setValue(p, null);
+ }
+ }
+ c.setVisible(ps, isVisibleDefault(c));
+ }
+
+ };
+ resetter.preorder(cmpnt);
+ }
+
+ /**
+ * Return the prefix that is prepended to each component's state parameters
+ * to keep them unique.
+ */
+ private final String componentPrefix(Component c) {
+ if (c == null) {
+ return COMPONENT_PREFIX + "g" + DELIMITER;
+ } else {
+ // WRS: preferentially use key if it exists
+ String key = c.getKey();
+ if (key == null) {
+ if (stateContains(c)) {
+ key = String.valueOf(stateIndex(c));
+ } else {
+ throw new IllegalArgumentException(
+ "Cannot generate prefix for component: key is null "
+ + "and component " + c.toString() + "/" + c.getKey()
+ + " did not register with page.");
+ }
+ }
+ return COMPONENT_PREFIX + key + DELIMITER;
+ }
+ }
+
+ /**
+ * Undo the name change that {@link #parameterName} does.
+ *
+ * @param name a possibly mangled name
+ *
+ * @return the unmangled name.
+ */
+ private static final String unmangle(String name) {
+ if (!name.startsWith(COMPONENT_PREFIX)) {
+ return name;
+ }
+ // Find the second occurence of delimiter
+ int prefix = name.indexOf(DELIMITER, name.indexOf(DELIMITER) + 1);
+ if (prefix >= 0 && prefix < name.length()) {
+ return name.substring(prefix + 1);
+ }
+ return name;
+ }
+
+ // Procs for debugging output
+ private static String NAME = "name";
+
+ /**
+ * Produces an XML fragment that captures the layout of this page.
+ */
+ private void showStructure(PageState s, Element root) {
+ final HttpServletRequest req = s.getRequest();
+ Element state = root.newChildElement("bebop:state", BEBOP_XML_NS);
+ // Selected component
+ String sel = req.getParameter(m_selected.getName());
+ Element selected = state.newChildElement("bebop:selected", BEBOP_XML_NS);
+
+ selected.addAttribute(NAME, m_selected.getName());
+ selected.setText(sel);
+
+ // Control event
+ Element eventName = state.newChildElement("bebop:eventName",
+ BEBOP_XML_NS);
+ eventName.addAttribute(NAME, m_controlEvent.getName());
+ eventName.setText(req.getParameter(m_controlEvent.getName()));
+ Element eventValue = state.newChildElement("bebop:eventValue",
+ BEBOP_XML_NS);
+ eventValue.addAttribute(NAME, m_controlValue.getName());
+ eventValue.setText(req.getParameter(m_controlValue.getName()));
+
+ // Global parameters
+ Element globalState = root.newChildElement("bebop:params", BEBOP_XML_NS);
+ for (Iterator ii = getStateModel().getParameters(); ii.hasNext();) {
+ ParameterModel p = (ParameterModel) ii.next();
+ if (!p.getName().startsWith(COMPONENT_PREFIX)) {
+ Element param = globalState.newChildElement("bebop:param",
+ BEBOP_XML_NS);
+ param.addAttribute(NAME, p.getName());
+ param.setText(String.valueOf(s.getValue(p)));
+ }
+ }
+
+ showVisibility(s, this, root);
+ }
+
+ /**
+ * @see showStructure(PageState, Element)
+ */
+ private void showVisibility(PageState s, Component c, Element parent) {
+ HttpServletRequest req = s.getRequest();
+
+ Element cmp = parent.newChildElement("bebop:component", BEBOP_XML_NS);
+ cmp.addAttribute(NAME, getDebugLabel(c));
+ cmp.addAttribute("idx", String.valueOf(stateIndex(c)));
+ cmp.addAttribute("isVisible", (s.isVisible(c) ? "yes" : "no"));
+ cmp.addAttribute("class", c.getClass().getName());
+
+ if (c.getKey() != null) {
+ String prefix = componentPrefix(c);
+ for (Iterator i = getStateModel().getParameters(); i.hasNext();) {
+ ParameterModel p = (ParameterModel) i.next();
+ if (!p.getName().startsWith(prefix)) {
+ continue;
+ }
+
+ Element param = parent.newChildElement("bebop:param",
+ BEBOP_XML_NS);
+ param.addAttribute(NAME, unmangle(p.getName()));
+ param.addAttribute("defaultValue",
+ String.valueOf(req.getParameter(p.getName())));
+ param
+ .addAttribute("currentValue", String.valueOf(s.getValue(p)));
+ }
+ }
+ for (Iterator i = c.children(); i.hasNext();) {
+ showVisibility(s, ((Component) i.next()), cmp);
+ }
+ }
+
+ private static String getDebugLabel(Component c) {
+ if (c.getKey() != null) {
+ return c.getKey();
+ }
+
+ String klass = c.getClass().getName();
+ return klass.substring(klass.lastIndexOf(".") + 1, klass.length());
+ }
+
+ /**
+ * return a string that represents an ordered list of component ids used on
+ * the page. For situations where only the components present is of
+ * importance, this may be used by implementations of hashCode & equals
+ *
+ * @return
+ */
+ public String getComponentString() {
+ Iterator it = m_componentMap.keySet().iterator();
+ /*int hash = 0;
+ while (it.hasNext()) {
+ String componentId = (String)it.next();
+ s_log.debug("component id = " + componentId);
+ hash = hash | componentId.hashCode();
+ s_log.debug("hash so far = " + hash);
+ }*/
+ Date start = new Date();
+
+ StringBuilder hashString = new StringBuilder();
+ while (it.hasNext()) {
+ String componentId = (String) it.next();
+ hashString.append(componentId);
+ }
+ s_log.debug("Time to create hashCode for page: " + (new Date().getTime()
+ - start.
+ getTime()));
+ return hashString.toString();
+
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/PageErrorDisplay.java b/ccm-core/src/main/java/com/arsdigita/bebop/PageErrorDisplay.java
new file mode 100755
index 000000000..e424804e6
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/PageErrorDisplay.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.bebop.list.ListModel;
+import com.arsdigita.bebop.list.ListModelBuilder;
+import com.arsdigita.bebop.list.ListCellRenderer;
+import com.arsdigita.util.LockableImpl;
+import com.arsdigita.xml.Element;
+import com.arsdigita.globalization.GlobalizedMessage;
+
+import java.util.Iterator;
+
+/**
+ * Displays validation errors for the page. These might have occured due to validation listeners on
+ * some state parameters within the page.
+ *
+ * @author Stanislav Freidin
+ * @version $Id: PageErrorDisplay.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public class PageErrorDisplay extends List {
+
+ private static final String COLOR = "color";
+
+ /**
+ * Constructs a new PageErrorDisplay.
+ */
+ public PageErrorDisplay() {
+ this(new PageErrorModelBuilder());
+ }
+
+ /**
+ * Constructs a new PageErrorDisplay from the errors supplied by a list model
+ * builder.
+ *
+ * @param builder the {@link ListModelBuilder} that will supply the errors
+ *
+ */
+ protected PageErrorDisplay(ListModelBuilder builder) {
+ super(builder);
+ setCellRenderer(new LabelCellRenderer());
+ setTextColor("red");
+ setClassAttr("pageErrorDisplay");
+ }
+
+ /**
+ * Sets the HTML color of the error messages.
+ *
+ * @param c An HTML color, such as "#99CCFF" or "red"
+ */
+ public void setTextColor(String c) {
+ setAttribute(COLOR, c);
+ }
+
+ /**
+ * Gets the HTML color of the error messages.
+ *
+ * @return the HTML color of the error messages.
+ */
+ public String getTextColor() {
+ return getAttribute(COLOR);
+ }
+
+ /**
+ * Determines if there are errors to display.
+ *
+ * @param state the current page state
+ *
+ * @return true if there are any errors to display; false otherwise.
+ */
+ protected boolean hasErrors(PageState state) {
+ return (state.getErrors().hasNext());
+ }
+
+ /**
+ * Generates the XML for this component. If the state has no errors in it, does not generate any
+ * XML.
+ *
+ * @param state the current page state
+ * @param parent the parent XML element
+ */
+ public void generateXML(PageState state, Element parent) {
+ if (hasErrors(state)) {
+ super.generateXML(state, parent);
+ }
+ }
+
+ // A private class which builds a ListModel based on form errors
+ private static class PageErrorModelBuilder extends LockableImpl
+ implements ListModelBuilder {
+
+ public PageErrorModelBuilder() {
+ super();
+ }
+
+ public ListModel makeModel(List l, PageState state) {
+ return new StringIteratorModel(state.getErrors());
+ }
+
+ }
+
+ // A ListModel which generates items based on an Iterator
+ protected static class StringIteratorModel implements ListModel {
+
+ private Iterator m_iter;
+ private GlobalizedMessage m_error;
+ private int m_i;
+
+ public StringIteratorModel(Iterator iter) {
+ m_iter = iter;
+ m_error = null;
+ m_i = 0;
+ }
+
+ public boolean next() {
+ if (!m_iter.hasNext()) {
+ m_i = 0;
+ return false;
+ }
+
+ m_error = (GlobalizedMessage) m_iter.next();
+ ++m_i;
+
+ return true;
+ }
+
+ private void checkState() {
+ if (m_i == 0) {
+ throw new IllegalStateException(
+ "next() has not been called succesfully"
+ );
+ }
+ }
+
+ public Object getElement() {
+ checkState();
+ return m_error;
+ }
+
+ public String getKey() {
+ checkState();
+ return Integer.toString(m_i);
+ }
+
+ }
+
+ // A ListCellRenderer that renders labels
+ private static class LabelCellRenderer implements ListCellRenderer {
+
+ public Component getComponent(List list, PageState state, Object value,
+ String key, int index, boolean isSelected) {
+ return new Label((GlobalizedMessage) value);
+ }
+
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/PageState.java b/ccm-core/src/main/java/com/arsdigita/bebop/PageState.java
new file mode 100644
index 000000000..137c5d5c4
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/PageState.java
@@ -0,0 +1,1075 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.bebop.parameters.BitSetParameter;
+import com.arsdigita.bebop.parameters.ParameterData;
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.bebop.util.Traversal;
+import com.arsdigita.dispatcher.DispatcherHelper;
+import com.arsdigita.util.Assert;
+import com.arsdigita.util.UncheckedWrapperException;
+import com.arsdigita.web.ParameterMap;
+import com.arsdigita.web.RedirectSignal;
+import com.arsdigita.web.URL;
+import com.arsdigita.web.Web;
+import com.arsdigita.xml.Element;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import org.apache.log4j.Logger;
+
+/**
+ *
The request-specific data (state) for a {@link Page}. All
+ * methods that need access to the state of the associated page during
+ * the processing of an HTTP request are passed a
+ * PageState object. Since PageState
+ * contains request-specific data, it should only be live during the
+ * servicing of one HTTP request. This class has several related
+ * responsibilites:
PageState objects store the values for global and
+ * component state parameters and are responsible for retrieving these
+ * values from the HTTP request. Components can access the values of
+ * their state parameters through the {@link #getValue getValue} and
+ * {@link #setValue setValue} methods.
This class is also responsible for serializing the current state of + * the page in a variety of ways for inclusion in the output: {@link + * #generateXML generateXML} adds the state to an XML element and {@link + * #stateAsURL stateAsURL} encodes the page's URL.
+ * + *The serialized form of the current page state can be quite large + * so the page state can also be preserved in the HttpSession, on the + * server. The Page object specifies this by calling {@link + * Page#setUsingHttpSession setUsingHttpSession(true)}. When this + * flag is set, then the page state will be preserved in the + * HttpSession, and the {@link #stateAsURL stateAsURL} method will + * only serialize the current URL and the control event. It will also + * include a URL variable that the subsequent request uses to retrieve + * the correct page state from the HttpSession. If the page state for + * a particular request cannot be found, the constructor will throw a + * {@link SessionExpiredException SessionExpiredException}.
+ * + *Up to {@link #getMaxStatesInSession getMaxStatesInSession()} + * independent copies of the page state may be stored in the + * HttpSession, to preserve the behavior of the browser's "back" + * button.
+ * + *Note: As a convention, only the component to which a
+ * state parameter belongs should modify it by calling
+ * getValue and setValue. All other objects
+ * should manipulate the state of a component only through
+ * well-defined methods on the component.
The control event consists of a pair of strings, the name + * of the event and its associated value. Components use the + * control event to send themselves a "delayed signal", i.e. a signal + * that is triggered when the same page is requested again through a + * link that has been generated with the result of {@link #stateAsURL} + * as the target. The component can then access the name and value of + * the event with calls to {@link #getControlEventName} and {@link + * #getControlEventValue}.
+ * + * Typically, a component will contain code corresponding to the following
+ * in its generateXML method:
+ *
+ * public void generateXML(PageState state, Element parent) {
+ * if ( isVisible(state) ) {
+ * MyComponent target = firePrintEvent(state);
+ * Element link = new Element ("bebop:link", BEBOP_XML_NS);
+ * parent.addContent(link);
+ * target.generateURL(state, link);
+ * target.exportAttributes(link);
+ * target.generateExtraXMLAttributes(state, link);
+ * target.getChild().generateXML(state, link);
+ * }
+ * }
+ *
+ * (In reality, the component would not write a bebop:link element
+ * directly, but use a {@link ControlLink} to do that automatically.)
+ * When the user clicks on the link that the above generateXML
+ * method produces, the component's respond method is called
+ * and can access the name and value of the event with code similar to the
+ * following:
+ *
+ * public void respond(PageState state) {
+ * String name = state.getControlEventName();
+ * String value = state.getControlEventValue();
+ * if ( "name".equals(name) ) {
+ * doSomeStateChange(value);
+ * } else {
+ * throw new IllegalArgumentException("Can't understand event " + name);
+ * }
+ * }
+ *
+ *
+ *
+ * Request local variables make it possible to store arbitrary objects + * in the page state and allow components (and other objects) to cache + * results of previous computations. For example, the {@link Form} + * component uses a request local variable to store the {@link FormData} it + * generates from the request and make it acessible to the widgets it + * contains, and to other components through calling {@link + * Form#getFormData Form.getFormData(state)}. See the documentation for + * {@link RequestLocal} on how to use your own request local variables. + * + *
PageState objects store references to the HTTP request
+ * and response that is currently being served. Components are free to
+ * manipulate these objects as they see fit.
+ *
Initially, this variable refers to Page.m_invisible. Only when a
+ * call to {@link #setVisible} is made, is that value
+ * copied. (Copy-on-write)
+ */
+ private BitSet m_invisible;
+ private boolean m_visibilityDirty = true;
+
+ private int m_nextSession;
+
+ private final static String SESSION_ATTRIBUTE =
+ "com.arsdigita.bebop.FormData";
+ private final static String SESSION_COUNTER_ATTRIBUTE =
+ "com.arsdigita.bebop.FormData.counter";
+ private final static String CURRENT_SESSION_PARAMETER =
+ "bbp.session";
+ private final static String PAGE_STATE_ATTRIBUTE =
+ "com.arsdigita.bebop.PageState";
+
+ private static int s_maxSessions = 10;
+
+ private Traversal m_visibilityTraversal = new VisibilityTraversal();
+ private List m_visibleComponents;
+
+ /**
+ * Returns the maximum number of independent page states that may be stored
+ * in the HttpSession, for preserving the behavior of the "back" button.
+ * @return the maximum number of independent page states that may
+ * be stored in the HttpSession.
+ */
+ public static int getMaxStatesInSession() {
+ return s_maxSessions;
+ }
+
+ /**
+ * Sets the maximum number of independent page states that may be stored in
+ * the HttpSession, for preserving the behavior of the "back" button.
+ * @param x the maximum number of independent page states to store
+ * in the HttpSession.
+ */
+ public static void setMaxStatesInSession(int x) {
+ s_maxSessions = x;
+ }
+
+ /**
+ * Returns the page state object for the given request, or null if none
+ * exists yet.
+ *
+ * @param request The servlet request.
+ *
+ * @return The page state object for the given request, or null if none
+ * exists yet.
+ **/
+ public static PageState getPageState(HttpServletRequest request) {
+ return (PageState) request.getAttribute(PAGE_STATE_ATTRIBUTE);
+ }
+
+ /**
+ * Returns the page state object for the current request, or null if none
+ * exists yet.
+ *
+ * @return The page state object for the current request, or null if none
+ * exists yet.
+ **/
+
+ public static PageState getPageState() {
+ HttpServletRequest request = DispatcherHelper.getRequest();
+
+ if (request == null) {
+ return null;
+ } else {
+ return getPageState(request);
+ }
+ }
+
+ /**
+ * Construct the PageState for an HTTP request.
+ *
+ * Calls {@link FormModel#process process} on the form model underlying
+ * the page model and calls {@link Component#respond respond} on the
+ * component from which the request originated.
+ *
+ * @param page The model of the page
+ * @param request The request being served
+ * @param response Where the response should be sent
+ *
+ * @pre request != null && request.get(page.PAGE_SELECTED) != null
+ * @pre response != null && ! response.isCommitted()
+ * @pre page != null
+ *
+ */
+ public PageState(Page page, HttpServletRequest request,
+ HttpServletResponse response)
+ throws ServletException {
+ m_page = page;
+
+ if ( m_page == null ) {
+ m_page = new Page();
+ m_page.lock();
+ }
+
+ m_request = request;
+ m_response = response;
+
+ FormData pageStateFromSession = null;
+ if (m_page.isUsingHttpSession()) {
+ pageStateFromSession = getStateFromSession();
+ }
+
+ // always treat the request as a submission
+ m_pageState = new FormData(m_page.getStateModel(), request, true,
+ pageStateFromSession);
+ m_page.getStateModel().process(this, m_pageState);
+ m_invisible = decodeVisibility();
+
+ // Add the PageState to the request
+ m_request.setAttribute(PAGE_STATE_ATTRIBUTE, this);
+ }
+
+
+ protected BitSet decodeVisibility() {
+ BitSet difference = (BitSet)m_pageState.get(Page.INVISIBLE);
+ BitSet current = (BitSet)m_page.m_invisible.clone();
+ if (difference != null) {
+ current.xor(difference);
+ }
+ return current;
+ }
+
+ protected BitSet encodeVisibility(BitSet current) {
+ BitSet difference = (BitSet)m_page.m_invisible.clone();
+ difference.xor(current);
+ return difference;
+ }
+
+ /**
+ * Helper function that returns the PageState object from the
+ * HttpSession.
+ */
+ private FormData getStateFromSession() throws SessionExpiredException {
+ HttpSession session = m_request.getSession(true);
+ FormData state = null;
+
+ // have to bootstrap this manually from the request
+ String key =
+ m_request.getParameter(CURRENT_SESSION_PARAMETER);
+ if (key != null) {
+ state = (FormData)session.getAttribute(SESSION_ATTRIBUTE + key);
+ // session is expired if we're looking for a particular
+ // page state and couldn't find it.
+ if (state == null) {
+ throw new SessionExpiredException();
+ }
+ }
+ return state;
+ }
+
+ /**
+ * Process the PageState and fire necessary events. Call respond on the
+ * selected bebop component.
+ */
+ public void respond() throws ServletException {
+ String compKey = (String) m_pageState.get(Page.SELECTED);
+
+ if ( compKey != null ) {
+ Component c = m_page.getComponent(compKey);
+ if ( c == null ) {
+ throw new ServletException("Selected component not on page.");
+ }
+ try {
+ c.respond(this);
+ } finally {
+ }
+ }
+ }
+
+ /**
+ * Return the page for which this object holds the state.
+ *
+ * @return the page for which this object holds state.
+ * @post return != null
+ */
+ public final Page getPage() {
+ return m_page;
+ }
+
+
+ /**
+ * Return the request object for the HTTP request.
+ *
+ * @return The HTTP request being currently served.
+ */
+ public final HttpServletRequest getRequest() {
+ return m_request;
+ }
+
+ /**
+ * Return the response object for the HTTP response.
+ *
+ * @return The response for the HTTP request being served
+ */
+ public final HttpServletResponse getResponse() {
+ return m_response;
+ }
+
+ /**
+ * The index of a component in the page model
+ *
+ * @pre c != null
+ */
+ private int indexOf(Component c) {
+ return m_page.stateIndex(c);
+ }
+
+ /**
+ * Return true is the comonent c is currently
+ * visible. This method should only be used by components
+ * internally. All other objects should call {@link Component#isVisible
+ * Component.isVisible} on the component.
+ *
+ * @param c the components whose visibility should be returned
+ * @return true if the component is visible
+ */
+ public boolean isVisible(Component c) {
+ if ( ! getPage().stateContains(c)) {
+ return true;
+ }
+ if ( m_invisible == null ) {
+ return m_page.isVisibleDefault(c);
+ } else {
+ return ! m_invisible.get(indexOf(c));
+ }
+ }
+
+ /**
+ * Return true is the component c is currently
+ * visible on the page displayed to the end user. This is true
+ * if the component's visibility flag is set, and it is in a
+ * container visible to the end user.
+ *
+ *
This method is different than isVisible which
+ * only returns the visibility flag for the individual component.
+ *
+ * @param c the components whose visibility should be returned
+ * @return true if the component is visible
+ */
+ public boolean isVisibleOnPage(Component c) {
+ if (m_visibleComponents == null) {
+ m_visibleComponents = new ArrayList();
+ m_visibilityTraversal.preorder(getPage().getPanel());
+ }
+
+ return m_visibleComponents.contains(c);
+ }
+
+ private class VisibilityTraversal extends Traversal {
+ protected void act(Component c) {
+ m_visibleComponents.add(c);
+ }
+
+ protected int test(Component c) {
+ if (isVisible(c)) {
+ return PERFORM_ACTION;
+ } else {
+ // not visible, so neither are children
+ return SKIP_SUBTREE;
+ }
+ }
+ }
+
+
+ /**
+ * Set the visibility of a component. Calls to this method change the
+ * default visibility set with {@link Page#setVisibleDefault
+ * Page.setVisibleDefault}. This method should only be used by
+ * components internally. All other objects should call {@link
+ * Component#setVisible Component.setVisible} on the component.
+ *
+ *
Without explicit changes, a component is visible.
+ *
+ * @param c the component whose visibility is to be changed
+ * @param v true if the component should be visible
+ */
+ public void setVisible(final Component c, final boolean v) {
+ if (Assert.isEnabled()) {
+ Assert.isTrue(getPage().stateContains(c),
+ "Component" + c + " is not registered on Page " +
+ getPage());
+ }
+
+ if (m_invisible == null || m_invisible == getPage().m_invisible) {
+ // copy on write
+ m_invisible = (BitSet) getPage().m_invisible.clone();
+ }
+
+ int i = indexOf(c);
+
+ if (v) {
+ if (!m_invisible.get(i))
+ return;
+ m_invisible.clear(i);
+ } else {
+ if (m_invisible.get(i))
+ return;
+ m_invisible.set(i);
+ }
+ if (s_log.isInfoEnabled()) {
+ s_log.info("Marking visibility parameter as dirty " + m_request + " because of component " + c);
+ }
+ // Do this only in toURL since the RLE is expensive
+ //m_pageState.put(Page.INVISIBLE, encodeVisibility(m_invisible));
+ m_visibilityDirty = true;
+ m_visibleComponents = null;
+ }
+
+ /**
+ * Resets the given component and its children to their default
+ * visibility. Also resets the state parameters of the given
+ * component and its children to null. This is not a speedy
+ * method. Do not call gratuitously.
+ *
+ * @param c the parent component whose state parameters and
+ * visibility you wish to reset.
+ * */
+ public void reset(Component c) {
+ getPage().reset(this, c);
+ }
+
+// /**
+// * Store an attribute keyed on the object key. The
+// * PageState puts no restrictions on what can be stored as
+// * an attribute or how they are managed.
+// *
+// * To remove an attribute, call setAttribute(key, null).
+// *
+// * The attributes are only accessible as long as the
+// * PageState is alive, typically only for the duration of
+// * the request.
+// *
+// * @deprecated Use either setAttribute on {@link
+// * HttpServletRequest the HTTP request object}, or, preferrably, use a
+// * {@link RequestLocal request local} variable. Will be removed on
+// * 2001-06-13.
+// *
+// */
+// public void setAttribute(Object key, Object value) {
+// if ( m_attributes == null ) {
+// m_attributes = new HashMap();
+// }
+// m_attributes.put(key, value);
+// }
+
+// /**
+// * Get the value of an attribute stored with the same key with {@link
+// * #setAttribute setAttribute}.
+// *
+// * @deprecated Use either getAttribute on {@link
+// * HttpServletRequest the HTTP request object}, or, preferrably, use a
+// * {@link RequestLocal request local} variable. Will be removed on
+// * 2001-06-13.
+// *
+// */
+// public Object getAttribute(Object key) {
+// if ( m_attributes == null ) {
+// return null;
+// }
+// return m_attributes.get(key);
+// }
+
+ /**
+ * Set the value of the state parameter p. The concrete
+ * type of value must be compatible with the type of the
+ * state parameter.
+ *
+ *
The parameter must have been previously added with a call to
+ * {@link Page#addComponentStateParam
+ * Page.getComponentStateParam}. This method should only be called by
+ * the component that added the state parameter to the page. Users of
+ * the component should manipulate the parameter through methods the
+ * component provides.
+ *
+ * @param p a state parameter
+ * @param value the new value for this state parameter. The concrete
+ * type depends on the type of the parameter being used.
+ */
+ public void setValue(ParameterModel p, Object value) {
+ m_pageState.put(p.getName(), value);
+ }
+
+ /**
+ * Get the value of state parameter p. The concrete type of
+ * the return value depends on the type of the parameter being
+ * used.
+ *
+ *
The parameter must have been previously added with a call to
+ * {@link Page#addComponentStateParam
+ * Page.addComponentStateParam}. This method should only be called by
+ * the component that added the state parameter to the page. Users of
+ * the component should manipulate the parameter through methods the
+ * component provides.
+ *
+ * @param p a state parameter
+ * @return the current value for this state parameter. The concrete
+ * type depends on the type of the parameter being used.
+ */
+ public Object getValue(ParameterModel p) {
+ return m_pageState.get(p.getName());
+ }
+
+ /**
+ * Get the value of a global state parameter.
+ * @deprecated Use {@link #getValue(ParameterModel m)} instead. If you
+ * don't have a reference to the parameter model, you should not be
+ * calling this method. Instead, the component that registered the
+ * parameter should provide methods to manipulate it. Will be removed
+ * 2001-06-20.
+ */
+ public Object getGlobalValue(String name) {
+ // WRS 9/7/01
+ // work-around: m_pageState will throw an exception when we get
+ // a named parameter that's not in the form model. But for
+ // API stability, we need to trap this and return null; since
+ // there's no way to tell what set of globalValues are legal
+ // in any particular page--that's what ParameterModels are for.
+ try {
+ return m_pageState.get(m_page.parameterName(name));
+ } catch (IllegalArgumentException iae) {
+ return null;
+ }
+ }
+
+// /**
+// * Change the value of a global parameter
+// *
+// * @deprecated Use {@link #setValue(ParameterModel m, Object o)}
+// * instead. If you don't have a reference to the parameter model, you
+// * should not be calling this method. Instead, the component that
+// * registered the parameter should provide methods to manipulate
+// * it. Will be removed 2001-06-20.
+// */
+// public void setGlobalValue(String name, Object value) {
+// m_pageState.put(m_page.parameterName(name), value);
+// }
+
+ // Handling the control event
+
+ /**
+ * Grab the control event. Until {@link #releaseControlEvent
+ * releaseControlEvent(c)} is called, only the component c
+ * can be used in calls to {@link #setControlEvent setControlEvent}.
+ * @pre c != null
+ */
+ public void grabControlEvent(Component c) {
+ if ( m_grabbingComponent != null && m_grabbingComponent != c ) {
+ throw new IllegalStateException
+ ("Component " + m_grabbingComponent.toString() +
+ " already holds the control event");
+ }
+ m_grabbingComponent = c;
+ }
+
+ /**
+ * Set the control event. The control event is a delayed event
+ * that only gets acted on when another request to this Page
+ * is made. It is used to set which component should receive the
+ * submission and lets the component set one component-specific name-value
+ * pair to be used in the submission.
+ *
+ * After calling this method links and hidden form controls generated + * with {@link #stateAsURL} have been amended so that if the user clicks such a + * link or submits a form containing those hidden controls, the exact + * same values can be retrieved with {@link #getControlEventName} and + * {@link #getControlEventValue}. + *
+ * Stateful components can use the control event to change their
+ * state. For example, a tabbed pane t might call
+ * setControlEvent(t, "select", "2") just prior to
+ * generating the link for its second tab.
+ *
+ * The values of name and value have no
+ * meaning to the page state, they are simply passed through without
+ * modifications. It is up to specific components what values of
+ * name and value are meaningful for it.
+ *
+ * @param c
+ * @param name The component specific name of the event, may be
+ * null
+ * @param value The component specific value of the event, may be
+ * null
+ * @pre c == null || getPage().stateContains(c)
+ */
+ public void setControlEvent(Component c, String name, String value) {
+ Assert.isTrue(c == null || getPage().stateContains(c),
+ "c == null || getPage().stateContains(c)");
+ if ( m_grabbingComponent != null && m_grabbingComponent != c ) {
+ throw new IllegalStateException
+ ("Component " + m_grabbingComponent.toString() +
+ " holds the control event");
+ }
+ // FIXME: This needs to take named components into account
+ String key = null;
+ if (c != null) {
+ key = c.getKey();
+ if (key == null) {
+ key = Integer.toString(m_page.stateIndex(c));
+ }
+ }
+ m_pageState.put(Page.SELECTED,
+ (c == null) ? null : key);
+ m_pageState.put(Page.CONTROL_EVENT, name);
+ m_pageState.put(Page.CONTROL_VALUE, value);
+ }
+
+ /**
+ * Set the control event. Both the event name and its value will be
+ * null.
+ */
+ public void setControlEvent(Component c) {
+ setControlEvent(c, null, null);
+ }
+
+ /**
+ * Clear the control event. Links and hidden form variables generated
+ * after this call will not cause any component's respond method to be
+ * called.
+ *
+ * @throws IllegalStateException if any component has grabbed the
+ * control event but not released it yet.
+ */
+ public void clearControlEvent() {
+ setControlEvent(null);
+ }
+
+ /**
+ * Get the name of the control event.
+ */
+ public String getControlEventName() {
+ return (String) m_pageState.get(Page.CONTROL_EVENT);
+ }
+
+ /**
+ * Get the value associated with the control event.
+ */
+ public String getControlEventValue() {
+ return (String) m_pageState.get(Page.CONTROL_VALUE);
+ }
+
+ /**
+ * Release the control event.
+ * @param c The component that was passed to the last call to
+ * grabControlEvent
+ * @pre getPage().stateContains(c)
+ */
+ public void releaseControlEvent(Component c) {
+ if ( m_grabbingComponent == null ) {
+ throw new IllegalStateException
+ ("No component holds the control event, but " + c.toString() +
+ " tries to release it.");
+ }
+ if ( c != m_grabbingComponent ) {
+ throw new IllegalStateException
+ ("Component " + m_grabbingComponent.toString() +
+ " holds the control event, but " + c.toString() +
+ " tries to release it.");
+ }
+ m_grabbingComponent = null;
+ }
+
+ /**
+ * Add elements to parent that represent the current page
+ * state. For each component or global state parameter on the page, a
+ * <bebop:pageState> element is added to
+ * parent. The name and value attributes
+ * of the element contain the name and value of the state parameters as
+ * they should appear in an HTTP request made back to this page.
+ *
+ *
Generates DOM fragment: + *
+ *
+ * @param form This is the form in which the hidden variables will exist
+ *
+ * @see #setControlEvent setControlEvent
+ */
+ public void generateXML(Element parent) {
+ synchronizeVisibility();
+
+ for ( Iterator i = m_pageState.getParameters().iterator();
+ i.hasNext(); ) {
+ ParameterData p = (ParameterData) i.next();
+
+ String key = (String) p.getName();
+ String value = p.marshal();
+
+ if ( value != null ) {
+ Element hidden = parent.newChildElement("bebop:pageState",
+ Component.BEBOP_XML_NS);
+ hidden.addAttribute("name", key);
+ hidden.addAttribute("value", value);
+ }
+ }
+ }
+
+ public void generateXML(Element parent, Iterator models) {
+ synchronizeVisibility();
+
+ List excludeParams = new ArrayList();
+ if (models != null) {
+ while (models.hasNext()) {
+ excludeParams.add(((ParameterModel)models.next()).getName());
+ }
+ }
+
+ for ( Iterator i = m_pageState.getParameters().iterator();
+ i.hasNext(); ) {
+ ParameterData p = (ParameterData) i.next();
+
+ String key = (String) p.getName();
+ String value = p.marshal();
+
+ if (value == null || excludeParams.contains(key)) {
+ continue;
+ }
+
+ Element hidden = parent.newChildElement("bebop:pageState",
+ Component.BEBOP_XML_NS);
+ hidden.addAttribute("name", key);
+ hidden.addAttribute("value", value);
+ }
+ }
+
+ /**
+ * Export the current page state into the HttpSession by putting the entire
+ * m_pageState (type FormData) object into the HttpSession.
+ *
+ *
+ * <bebop:pageState name=... value=.../>
+ *
+ * Package visibility is intentional. + * + * @see #setControlEvent setControlEvent */ + void stateAsHttpSession() { + // create session if we need to + HttpSession session = m_request.getSession(true); + // get + increment counter counter + Integer counterObj = + (Integer)session.getAttribute(SESSION_COUNTER_ATTRIBUTE); + if (counterObj == null) { + m_nextSession = 0; + } else { + m_nextSession = counterObj.intValue() + 1; + } + + session.setAttribute(SESSION_ATTRIBUTE + m_nextSession, m_pageState); + session.setAttribute(SESSION_COUNTER_ATTRIBUTE, + new Integer(m_nextSession)); + // remove an old session + int toRemove = m_nextSession - s_maxSessions; + if (toRemove >= 0) { + session.removeAttribute(SESSION_ATTRIBUTE + toRemove); + } + } + + /** + *
Write the current state of the page as a URL.
+ * + *The URL representing the state points to the same URL that + * the current request was made from and contains a query string + * that represents the page state.
+ * + *If the current page has the useHttpSession flag set, then + * the URL query string that we generate will only contain the + * current value of the control event, and the rest of the page + * state is preserved via the HttpSession. Otherwise, the query + * string contains the entire page state.
+ * + * @return a string containing the current state of a page. + * @see #setControlEvent setControlEvent + * @see Page#isUsingHttpSession Page.isUsingHttpSession + * @see Page#setUsingHttpSession Page.setUsingHttpSession + */ + public String stateAsURL() throws IOException { + return m_response.encodeURL(toURL().toString()); + } + + public final URL toURL() { + synchronizeVisibility(); + + final ParameterMap params = new ParameterMap(); + + if (s_log.isDebugEnabled()) { + dumpVisibility(); + } + + final Iterator iter = m_pageState.getParameters().iterator(); + + while (iter.hasNext()) { + final ParameterData data = (ParameterData) iter.next(); + + final String key = (String) data.getName(); + + if (!m_page.isUsingHttpSession() + || Page.CONTROL_EVENT_KEYS.contains(key)) { + final String value = data.marshal(); + + if (value != null) { + params.setParameter(key, value); + } + } + + } + + if (m_page.isUsingHttpSession()) { + params.setParameter(CURRENT_SESSION_PARAMETER, + new Integer(m_nextSession)); + } + + return URL.request(m_request, params); + } + + + private void synchronizeVisibility() { + if (m_visibilityDirty) { + if (s_log.isInfoEnabled()) { + s_log.info("Encoding visibility parameter " + m_request); + } + m_pageState.put(Page.INVISIBLE, encodeVisibility(m_invisible)); + m_visibilityDirty = false; + } + } + + private void dumpVisibility() { + BitSetParameter raw = new BitSetParameter("raw", BitSetParameter.ENCODE_RAW); + BitSetParameter dgap = new BitSetParameter("dgap", BitSetParameter.ENCODE_DGAP); + + BitSet current = (BitSet)m_invisible; + BitSet base = (BitSet)m_page.m_invisible; + + BitSet difference = (BitSet)current.clone(); + difference.xor(base); + + s_log.debug("Current: " + current.toString()); + s_log.debug("Default: " + base.toString()); + s_log.debug("Difference: " + difference.toString()); + + s_log.debug("Current RAW: " + raw.marshal(current)); + s_log.debug("Default RAW: " + raw.marshal(base)); + s_log.debug("Difference RAW: " + raw.marshal(difference)); + + s_log.debug("Current DGAP: " + dgap.marshal(current)); + s_log.debug("Default DGAP: " + dgap.marshal(base)); + s_log.debug("Difference DGAP: " + dgap.marshal(difference)); + + s_log.debug("Current Result: " + dgap.unmarshal(dgap.marshal(current))); + s_log.debug("Default Result: " + dgap.unmarshal(dgap.marshal(base))); + s_log.debug("Difference Result: " + dgap.unmarshal(dgap.marshal(difference))); + + if (!current.equals(dgap.unmarshal(dgap.marshal(current)))) { + s_log.debug("Broken marshal/unmarshal for current"); + } + if (!base.equals(dgap.unmarshal(dgap.marshal(base)))) { + s_log.debug("Broken marshal/unmarshal for default"); + } + if (!difference.equals(dgap.unmarshal(dgap.marshal(difference)))) { + s_log.debug("Broken marshal/unmarshal for difference"); + } + } + + /** + * Get the URI to which the current request was made. Copes with the + * black magic that is needed to get the URI if the request was handled + * through a dispatcher. If no dispatcher was involved in the request, + * returns the request URI from the HTTP request. + * + * @post return != null + * + * @return the URI to which the current request was made + */ + public String getRequestURI() { + final URL url = Web.getWebContext().getRequestURL(); + + if (url == null) { + return m_request.getRequestURI(); + } else { + return url.getRequestURI(); + } + } + + /** + * Return true if all the global and component state parameters + * extracted from the HTTP request were successfully validated against + * their parameter models in the {@link Page}. + * + * @return true if the values of all global and component state + * parameters are valid with respect to their parameter models. + */ + public boolean isValid() { + return m_pageState.isValid(); + } + + /** + * Return an iterator over the errors that occurred in trying to + * validate the state parameters against their parameter models in + * {@link Page}. + * + * @return an iterator over validation errors + * @see FormData#getErrors + */ + public Iterator getErrors() { + return m_pageState.getAllErrors(); + } + + /** + * Return a string with all the errors that occurred in trying to + * validate the state parameters against their parameter models in + * {@link Page}. The string consists simply of the concatenation of all + * error messages that the result of {@link #getErrors} iterates over. + * + * @return all validation errors concatenated into one string + */ + public String getErrorsString() { + StringBuffer s = new StringBuffer(); + for (Iterator i = m_pageState.getAllErrors(); i.hasNext(); ) { + s.append(i.next().toString()); + s.append(System.getProperty("line.separator")); + } + return s.toString(); + } + + /** + * Force the validation of all global and component state parameters + * against their parameter models. This method only needs to be called if + * the values of the parameters have been changed with {@link #setValue + * setValue} or {@link #setGlobalValue setGlobalValue} and may now + * contain invalid values. + */ + public void forceValidate() { + m_pageState.forceValidate(this); + } + + /** Convert to a String. + * @return a human-readable representation ofthis.
+ */
+ public String toString() {
+ String newLine = System.getProperty("line.separator");
+ String result =
+ super.toString() + " = {" + newLine
+ + "m_page = " + m_page + "," + newLine
+ + "m_request = " + m_request + "," + newLine
+ + "m_response = " + m_response + "," + newLine
+ // FormData
+ + "m_pageState = " + m_pageState.asString() + "," + newLine
+ + "m_attributes = " + m_attributes + "," + newLine
+ // Map to FormData
+ + "," + newLine
+ + "m_grabbingComponent = " + m_grabbingComponent + "," + newLine
+ + "m_invisible = " + m_invisible + newLine
+ + "}";
+ return result;
+ }
+
+ /**
+ * Clear the control event then redirect to the new page state.
+ *
+ * @param isCommitRequested indicates if a commit required before the redirect
+ *
+ * @throws RedirectSignal to the new page state
+ *
+ * @see RedirectSignal#RedirectSignal(String, boolean)
+ */
+ public void redirectWithoutControlEvent(boolean isCommitRequested) {
+ clearControlEvent();
+ try {
+ throw new RedirectSignal(stateAsURL(), true);
+ } catch (IOException ioe) {
+ throw new UncheckedWrapperException(ioe);
+ }
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/ParameterSingleSelectionModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/ParameterSingleSelectionModel.java
new file mode 100755
index 000000000..b9a37bc7d
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/ParameterSingleSelectionModel.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.util.Assert;
+
+/**
+ * An implementation of {@link SingleSelectionModel} that uses
+ * a state parameter for managing the currently selected key.
+ * + * + * A typical use case for this class is as follows. + *
+ * + * @author Stanislav Freidin + * @version $Id: ParameterSingleSelectionModel.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class ParameterSingleSelectionModel + extends AbstractSingleSelectionModel { + + + private ParameterModel m_parameter; + + /** + * Constructs a new ParameterSingleSelectionModel. + * + * @param m the parameter model that will be used to + * keep track of the currently selected key + */ + public ParameterSingleSelectionModel(ParameterModel m) { + super(); + + m_parameter = m; + } + + /** + * Returns the key that identifies the selected element. + * + * @param state apublic TheConstructor() { + * m_parameter = new StringParameter("my_key"); + * m_sel = new ParameterSingleSelectionModel(m_parameter); + * } + * + * public void register(Page p) { + * p.addComponent(this); + * p.addComponentStateParam(this, m_param); + * }
PageState value
+ * @return a String value.
+ */
+ public Object getSelectedKey(PageState state) {
+ final FormModel model = state.getPage().getStateModel();
+ if (model.containsFormParam(m_parameter)) {
+ return state.getValue(m_parameter);
+ } else {
+ return null;
+ }
+ }
+
+ public final ParameterModel getStateParameter() {
+ return m_parameter;
+ }
+
+ /**
+ * Set the selected key.
+ *
+ * @param state represents the state of the current request
+ * @param newKey the new selected key
+ */
+ public void setSelectedKey(PageState state, Object newKey) {
+ final Object oldKey = getSelectedKey(state);
+
+ if (Assert.isEnabled()) {
+ final FormModel model = state.getPage().getStateModel();
+ Assert.isTrue(model.containsFormParam(m_parameter));
+ }
+
+ state.setValue(m_parameter, newKey);
+
+ if (newKey == null && oldKey == null) {
+ return;
+ }
+
+ if (newKey != null && newKey.equals(oldKey)) {
+ return;
+ }
+
+ fireStateChanged(state);
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/RequestLocal.java b/ccm-core/src/main/java/com/arsdigita/bebop/RequestLocal.java
new file mode 100644
index 000000000..1bcace347
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/RequestLocal.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * A variable whose value is local to each request. Objects that need to store
+ * values that change in every request should declare them to be
+ * RequestLocal. These variables hold their values only during a
+ * duration of a request. They get reinitialized by a call to {@link
+ * #initialValue(PageState)} for every new HTTP request.
+ *
+ * For example, a class that wants to implement a request local property
+ * foo would do the following:
+ * public class SomeClass {
+ * private RequestLocal m_foo;
+ *
+ * public SomeClass() {
+ * m_foo = new RequestLocal() {
+ * protected Object initialValue(PageState s) {
+ * // Foo could be a much more complicated value
+ * return s.getRequestURI();
+ * }
+ * };
+ * }
+ *
+ * public String getFoo(PageState s) {
+ * return (String) m_foo.get(s);
+ * }
+ *
+ * public void setFoo(PageState s, String v) {
+ * m_foo.set(s, v);
+ * }
+ * }
+ *
+ *
+ * @author David Lutterkort
+ * @version $Id$
+ */
+public class RequestLocal {
+
+ private static final String ATTRIBUTE_KEY =
+ "com.arsdigita.bebop.RequestLocal";
+
+ // Fetch the map used to store RequestLocals, possibly creating it along the
+ // way
+ private Map getMap(HttpServletRequest request) {
+ // This lock is paranoid. We can remove it if we know that only one
+ // thread will be touching a request object at a time. (Seems likely,
+ // but, like I said, I'm paranoid.)
+ synchronized (request) {
+ Map result = (Map)request.getAttribute(ATTRIBUTE_KEY);
+ result = (Map)request.getAttribute(ATTRIBUTE_KEY);
+ if (result == null) {
+ result = new HashMap();
+ request.setAttribute(ATTRIBUTE_KEY, result);
+ }
+ return result;
+ }
+ }
+
+ /**
+ * Returns the value to be used during the request represented by
+ * state. This method is called at most once per request,
+ * the first time the value of this RequestLocal is
+ * requested with {@link #get get}. RequestLocal must be
+ * subclassed, and this method must be overridden. Typically, an
+ * anonymous inner class will be used.
+ *
+ *
+ * @param state represents the current state of the request
+ * @return the initial value for this request local variable.
+ */
+ protected Object initialValue(PageState state) {
+ return null;
+ }
+
+ /**
+ * Returns the request-specific value for this variable for the request
+ * associated with state.
+ *
+ * @param state represents the current state of the request
+ * @return the value for this request local variable.
+ */
+ public Object get(PageState state) {
+ Map map = getMap(state.getRequest());
+ Object result = map.get(this);
+
+ if ( result == null && !map.containsKey(this) ) {
+ result = initialValue(state);
+ set(state, result);
+ }
+ return result;
+ }
+
+ /**
+ * Sets a new value for the request local variable and associates it with
+ * the request represented by state.
+ *
+ * @param state represents the current state of the request
+ * @param value the new value for this request local variable
+ */
+ public void set(PageState state, Object value) {
+ set(state.getRequest(), value);
+ }
+
+ /**
+ * Sets a new value for the request local variable and associates it with
+ * the request represented by request
This method is intended for use when a Dispatcher needs to assign some + * value to a RequestLocal for Bebop Page processing before Page processing + * begins.
+ * + * @param request represents the current request + * @param value the new value for this request local variable + */ + public void set(HttpServletRequest request, Object value) { + getMap(request).put(this, value); + } +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/SessionExpiredException.java b/ccm-core/src/main/java/com/arsdigita/bebop/SessionExpiredException.java new file mode 100755 index 000000000..9e0aad4ec --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/SessionExpiredException.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import javax.servlet.ServletException; + +/** + * + * + * @version $Id: SessionExpiredException.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class SessionExpiredException extends ServletException { + +} diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/SimpleComponent.java b/ccm-core/src/main/java/com/arsdigita/bebop/SimpleComponent.java new file mode 100755 index 000000000..06eef74e3 --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/bebop/SimpleComponent.java @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ +package com.arsdigita.bebop; + +import java.util.Collections; +import java.util.Iterator; + +import com.arsdigita.bebop.util.Attributes; +import com.arsdigita.kernel.KernelConfig; +import com.arsdigita.util.Assert; +import com.arsdigita.xml.Element; + +/** + * A simple implementation of the Component interface. + * + * + * @author David Lutterkort + * @author Stanislav Freidin + * @author Rory Solomon + * @author Uday Mathur + * + * @version $Id: SimpleComponent.java 1498 2007-03-19 16:22:15Z apevec $ + */ +public class SimpleComponent extends Completable + implements Component, Cloneable { + + private boolean m_locked; + + /** + * The Attribute object is protected to make it easier for the Form Builder + * service to persist the SimpleComponent. + * Locking violation is not a problem since if the SimpleComponent is locked + * then the Attribute object will also be locked. + */ + protected Attributes m_attr; + + private String m_key = null; // name mangling key + + /** + * Clones a component. The clone is not locked and has its own set of + * attributes. + * @return the clone of a component. + * @throws java.lang.CloneNotSupportedException + * @post ! ((SimpleComponent) return).isLocked() + */ + @Override + public Object clone() throws CloneNotSupportedException { + SimpleComponent result = (SimpleComponent) super.clone(); + if ( m_attr != null ) { + result.m_attr = (Attributes) m_attr.clone(); + } + result.m_locked = false; + return result; + } + + /** + * Registers state parameters for the page with its model. Documentation + * from Interface Componment: + * + * A simple component with a state parameterparam would do
+ * the following in the body of this method:
+ * + * p.addComponent(this); + * p.addComponentStateParam(this, param); + *+ * + * You should override this method to set the default visibility + * of your component: + * + *
+ * public void register(Page p) {
+ * super.register(p);
+ * p.setVisibleDefault(childNotInitiallyShown,false);
+ * p.setVisibleDefault(anotherChild, false);
+ * }
+ *
+ *
+ * Always call super.register when you override
+ * register. Otherwise your component may
+ * malfunction and produce errors like "Widget ... isn't
+ * associated with any Form"
+ *
+ * @pre p != null
+ * @param p
+ */
+ @Override
+ public void register(Page p) {
+ return;
+ }
+
+ /**
+ * Registers form parameters with the form model for this form.
+ * This method is only important for {@link FormSection form sections}
+ * and {@link com.arsdigita.bebop.form.Widget widgets} (components that
+ * have a connection to an HTML form). Other components can implement it
+ * as a no-op.
+ *
+ * @param f
+ * @param m
+ * @pre f != null
+ * @pre m != null
+ */
+ @Override
+ public void register(Form f, FormModel m) {
+ return;
+ }
+
+ /**
+ * Does processing that is special to the component
+ * receiving the click.
+ *
+ * @param state the current page state
+ * @throws javax.servlet.ServletException
+ */
+ @Override
+ public void respond(PageState state)
+ throws javax.servlet.ServletException { }
+
+ @Override
+ public Iterator children() {
+ return Collections.EMPTY_LIST.iterator();
+ }
+
+ /** Adds [J]DOM nodes for this component. Specifically for
+ * base class SimpleComponent, does nothing.
+ * @param p
+ */
+ @Override
+ public void generateXML(PageState state, Element p) {
+ return;
+ }
+
+ @Override
+ public final boolean isLocked() {
+ return m_locked;
+ }
+
+ @Override
+ public void lock () {
+ if (m_attr != null) {
+ m_attr.lock();
+ }
+ m_locked = true;
+ }
+
+ /**
+ * Unlocks this component. Package visibility is intentional; the
+ * only time a component should be unlocked is when it's pooled and
+ * gets locked because it's put into a page. It needs to be unlocked
+ * when the instance is recycled.
+ */
+ void unlock() {
+ m_locked = false;
+
+ }
+
+ /* Working with standard component attributes */
+
+ /**
+ * Gets the class attribute.
+ * @return the class attribute.
+ */
+ @Override
+ public String getClassAttr() {
+ return getAttribute(CLASS);
+ }
+
+ /**
+ * Sets the class attribute.
+ * @param theClass a valid XML name
+ */
+ @Override
+ public void setClassAttr(String theClass) {
+ Assert.isUnlocked(this);
+ setAttribute(CLASS, theClass);
+ }
+
+ /**
+ * Gets the style attribute.
+ * @return the style attribute.
+ */
+ @Override
+ public String getStyleAttr() {
+ return getAttribute(STYLE);
+ }
+
+ /**
+ * Sets the style attribute. style should be a valid CSS
+ * style, since its value will be copied verbatim to the output and
+ * appear as a style attribute in the top level XML or HTML
+ * output element.
+ *
+ * @param style a valid CSS style description for use in the
+ * style attribute of an HTML tag
+ * @see Standard Attributes
+ */
+ @Override
+ public void setStyleAttr(String style) {
+ Assert.isUnlocked(this);
+ setAttribute(STYLE, style);
+ }
+
+ /**
+ * Gets the id attribute.
+ * @return the id attribute.
+ * @see #setIdAttr(String id)
+ */
+ @Override
+ public String getIdAttr() {
+ return getAttribute(ID);
+ }
+
+ /**
+ * Sets the id attribute. id
+ * should be an XML name
+ * that is unique within the {@link Page Page} in which this component is
+ * contained. The value of id is copied literally to the
+ * output and not used for internal processing.
+ *
+ * @param id a valid XML identifier
+ * @see Standard Attributes
+ */
+ @Override
+ public void setIdAttr(String id) {
+ Assert.isUnlocked(this);
+ setAttribute(ID, id);
+ }
+
+ /* Methods for attribute management */
+
+ /**
+ * Sets an attribute. Overwrites any old values. These values are used to
+ * generate attributes for the top level XML or HTML element that is
+ * output from this component with {@link #generateXML generateXML}.
+ *
+ * @pre name != null
+ * @post getAttribute(name) == value
+ *
+ * @param name attribute name, case insensitive
+ * @param value new attribute value
+ */
+ final protected void setAttribute(String name, String value) {
+ Assert.isUnlocked(this);
+ if (m_attr == null) {
+ m_attr = new Attributes();
+ }
+ m_attr.setAttribute(name, value);
+ }
+
+ /**
+ * Gets the value of an attribute.
+ *
+ * @pre name != null
+ *
+ * @param name attribute name, case insensitive
+ * @return the string value previously set with {@link #setAttribute
+ * setAttribute}, or null if none was set.
+ * @see #setAttribute
+ */
+ final protected String getAttribute(String name) {
+ return (m_attr == null) ? null : m_attr.getAttribute(name);
+ }
+
+ /**
+ * Adds the attributes set with {@link #setAttribute setAttribute} to the
+ * element target. The attributes set with
+ * exportAttributes overwrite attributes with identical names
+ * that target might already have.
+ *
+ * @pre target != null
+ *
+ * @param target element to which attributes are added
+ * @see #setAttribute
+ */
+ final protected void exportAttributes(com.arsdigita.xml.Element target) {
+ if (m_attr != null) {
+ m_attr.exportAttributes(target);
+ }
+ if (KernelConfig.getConfig().isDebugEnabled() ||
+ Bebop.getConfig().showClassName()) {
+ target.addAttribute("bebop:classname", getClass().getName(),
+ BEBOP_XML_NS);
+ }
+ }
+
+ /**
+ * Returns true if any attributes have been set.
+ * @return true if any attributes have been set;
+ * false otherwise.
+ */
+ final protected boolean hasAttributes() {
+ return m_attr != null;
+ }
+
+ /*
+ * Set an arbitrary meta data attribute on the component.
+ * The name of the attribute in the XML will be prefixed
+ * with the string 'metadata.'
+ */
+ final public void setMetaDataAttribute(String name, String value) {
+ setAttribute("metadata." + name, value);
+ }
+
+ final public String getMetaDataAttribute(String name) {
+ return getAttribute("metadata." + name);
+ }
+
+ /**
+ * Supplies a key for parameter name mangling.
+ *
+ * @param key the key to mangle
+ * @return
+ */
+ @Override
+ public Component setKey(String key) {
+ Assert.isUnlocked(this);
+ if (key.charAt(0) >= 0 && key.charAt(0) <= 9) {
+ throw new IllegalArgumentException("key \"" + key + "\" must not start with a digit.");
+ }
+ m_key = key;
+ return this;
+ }
+
+ /**
+ * Retrieves a key for parameter name mangling.
+ * @return a key for parameter name mangling.
+ */
+ @Override
+ public final String getKey() {
+ return m_key;
+ }
+
+ @Override
+ public boolean isVisible(PageState s) {
+ return s.isVisible(this);
+ }
+
+ @Override
+ public void setVisible(PageState s, boolean v) {
+ s.setVisible(this, v);
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/SimpleContainer.java b/ccm-core/src/main/java/com/arsdigita/bebop/SimpleContainer.java
new file mode 100755
index 000000000..aa2278bc3
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/SimpleContainer.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import com.arsdigita.util.Assert;
+import com.arsdigita.xml.Element;
+
+/**
+ * A basic implementation of the {@link Container} interface which, by default,
+ * renders all of its children directly, without wrapping them in any kind of
+ * tag.
+ *
+ * However, the {@link #SimpleContainer(String, String)} constructor and/or the
+ * {@link #setTag(String)} method can be used to cause the container to wrap
+ * the XML for its children in an arbitrary tag. This functionality is useful
+ * for XSL templating.
+ *
+ * For example, a template rule might be written to arrange the children of this
+ * component in paragraphs:
+ *
+ * + * + * @author David Lutterkort + * @author Stanislav Freidin + * @author Rory Solomon + * @author Uday Mathur + * + * @version $Id: SimpleContainer.java 287 2005-02-22 00:29:02Z sskracic $ + */ +public class SimpleContainer extends SimpleComponent implements Container { + + private List m_components; + private String m_tag, m_ns; + + /** + * Constructs a new, empty+ * // Java Code: + * m_container = new SimpleContainer("cms:foo", CMS_XML_NS); + * + * // XSL code: + * <xsl:template match="cms:foo"> + * <xsl:for-each select="*"> + * <p> + * <xsl:apply-templates select="."/> + * </p> + * </xsl:for-each> + * </xsl:template> + *
SimpleContainer.
+ */
+ public SimpleContainer() {
+ this(null, null);
+ }
+
+ /**
+ * Constructs a new, empty SimpleContainer that will
+ * wrap its children in the specified tag.
+ *
+ * @param tag the name of the XML element that will be used to wrap the
+ * children of this container
+ * @param ns the namespace for the tag
+ */
+ public SimpleContainer(String tag, String ns) {
+ super();
+ m_components = new ArrayList();
+ m_tag = tag;
+ m_ns = ns;
+ }
+
+ /**
+ * Adds a component to this container.
+ *
+ * @param pc the component to be added
+ */
+ public void add(Component pc) {
+ Assert.isUnlocked(this);
+ Assert.exists(pc);
+ m_components.add(pc);
+ }
+
+ /**
+ * Adds a component to this container.
+ *
+ * @param pc the component to be added
+ * @param constraints this parameter is ignored. Child classes should
+ * override the add method if they wish to provide special handling
+ * of constraints.
+ */
+ public void add(Component c, int constraints) {
+ add(c);
+ }
+
+ /**
+ * Determines membership.
+ * @return true if the specified object is in this container;
+ * false otherwise.
+ * @param o the object type, typically a component. Type
+ * Object allows slicker code when o comes from any kind of collection.
+ */
+ public boolean contains(Object o) {
+ return m_components.contains(o);
+ }
+
+ /**
+ * Determines whether the container is empty.
+ *
+ * @return false if the container has any children;
+ * true otherwise.
+ */
+ public boolean isEmpty() {
+ return m_components.isEmpty();
+ }
+
+ /**
+ *
+ *
+ *
+ */
+ public int indexOf(Component pc) {
+ return m_components.indexOf(pc);
+ }
+
+ /**
+ * Returns the number of children inside this container.
+ * @return the number of children inside this container.
+ */
+ public int size() {
+ return m_components.size();
+ }
+
+ /**
+ *
+ *
+ *
+ */
+ public Component get(int index) {
+ return (Component) m_components.get(index);
+ }
+
+ /**
+ * Returns all the components of this container.
+ * @return all the components of this container.
+ */
+ @Override
+ public Iterator children() {
+ return m_components.iterator();
+ }
+
+ /**
+ * Sets the XML tag that will be used to wrap the children of
+ * this container.
+ *
+ * @param tag the XML tag, or null if children will not be wrapped
+ * in any manner.
+ */
+ protected final void setTag(String tag) {
+ Assert.isUnlocked(this);
+ m_tag = tag;
+ }
+
+ /**
+ * Sets the XML namespace for the tag that will be used to wrap
+ * the children of this container.
+ *
+ * @param ns the XML namespace
+ */
+ protected final void setNamespace(String ns) {
+ Assert.isUnlocked(this);
+ m_ns = ns;
+ }
+
+ /**
+ * Retrieves the name of the XML tag that will be used to
+ * wrap the child components.
+ *
+ * @return the name of the XML tag that will be used to
+ * wrap the child components, or null if no tag was specified.
+ */
+ public final String getTag() {
+ return m_tag;
+ }
+
+ /**
+ * Retrieves the name of the XML namespace for the tag that will be used to
+ * wrap the child components.
+ *
+ * @return the name of the XML namespace for the tag that will be used to
+ * wrap the child components, or null if no namespace was specified.
+ */
+ public final String getNamespace() {
+ return m_ns;
+ }
+
+ /**
+ * Generates the containing element. It is added with this
+ * component's tag below the specified parent element. If the passed in
+ * element is null, the method
+ * passes through p.
+
+ * @param p the parent XML element
+ * @return the element to which the children will be added.
+ */
+ protected Element generateParent(Element p) {
+ String tag = getTag();
+ if (tag == null) {
+ return p;
+ }
+ Element parent = p.newChildElement(tag, getNamespace());
+ exportAttributes(parent);
+ return parent;
+ }
+
+ /**
+ * Generates the XML for this container. If the tag property
+ * is nonempty, wraps the children in the specified XML tag.
+ *
+ * @param state represents the current request
+ * @param p the parent XML element
+ * @see #setTag(String)
+ * @see #setNamespace(String)
+ */
+ public void generateChildrenXML(PageState state, Element p) {
+ for (Iterator i = children(); i.hasNext(); ) {
+ Component c = (Component) i.next();
+
+ // XXX this seems to be a redundant vis check
+ if ( c.isVisible(state) ) {
+ c.generateXML(state, p);
+ }
+ }
+ }
+
+ /**
+ * Generates the XML for this container. If the tag property
+ * is nonempty, wraps the children in the specified XML tag.
+ *
+ * @param state represents the current request
+ * @param p the parent XML element
+ * @see #setTag(String)
+ * @see #setNamespace(String)
+ */
+ @Override
+ public void generateXML(PageState state, Element p) {
+ if ( isVisible(state) ) {
+ Element parent = generateParent(p);
+ generateChildrenXML(state, parent);
+ }
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/SingleSelectionModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/SingleSelectionModel.java
new file mode 100755
index 000000000..02d1f3daf
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/SingleSelectionModel.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop;
+
+
+import com.arsdigita.bebop.event.ChangeListener;
+import com.arsdigita.bebop.parameters.ParameterModel;
+
+/**
+ * Encapsulates the selection of a single object from many
+ * possibilities. The SingleSelectionModel allows components to
+ * communicate selections without tying the component that manages the
+ * selection in the user interface (for example a {@link List}) to the
+ * components that consume the selection (such as an edit form that needs
+ * to know which object should be edited).
+ *
+ * Selections are identified by a key, which must identify the
+ * underlying object uniquely among all objects that could possibly be
+ * selected. For objects stored in a database, this is usually a suitable
+ * representation of the object's primary key. The model relies on the
+ * key's equals method to compare keys, and requires that the
+ * key's toString method produces a representation of the key
+ * that can be used in URL strings and hidden form controls.
+ *
+ * @author David Lutterkort
+ * @version $Id: SingleSelectionModel.java 287 2005-02-22 00:29:02Z sskracic $
+ */
+public interface SingleSelectionModel {
+
+ /**
+ * Returns true if there is a selected element.
+ *
+ * @param state the state of the current request
+ * @return true if there is a selected component;
+ * false otherwise.
+ */
+ boolean isSelected(PageState state);
+
+ /**
+ * Returns the key that identifies the selected element.
+ *
+ * @param state a PageState value
+ * @return a String value.
+ */
+ Object getSelectedKey(PageState state);
+
+ /**
+ * Sets the selected key. If key is not in the collection of
+ * objects underlying this model, an
+ * IllegalArgumentException is thrown.
+ *
+ * @param state the state of the current request
+ * @param key the selected key
+ * @throws IllegalArgumentException if the supplied key can not
+ * be selected in the context of the current request.
+ */
+ void setSelectedKey(PageState state, Object key);
+
+ /**
+ * Clears the selection.
+ *
+ * @param state the state of the current request
+ * @post ! isSelected(state)
+ */
+ void clearSelection(PageState state);
+
+ /**
+ * Adds a change listener to the model. The listener's
+ * stateChanged method is called whenever the selected key changes.
+ *
+ * @param l a listener to notify when the selected key changes
+ */
+ void addChangeListener(ChangeListener l);
+
+ /**
+ * Removes a change listener from the model.
+ *
+ * @param l the listener to remove
+ */
+ void removeChangeListener(ChangeListener l);
+
+ /**
+ * Returns the state parameter that will be used to keep track
+ * of the currently selected key. Typically, the implementing
+ * class will simply call:
+ * return new StringParameter("foo");
+ * This method may return null if a state parameter is not
+ * appropriate in the context of the implementing class.
+ *
+ * @return the state parameter to use to keep
+ * track of the currently selected component, or
+ * null if a state parameter is not appropriate.
+ */
+ ParameterModel getStateParameter();
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionEvent.java
new file mode 100755
index 000000000..fba6ef94c
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.Component;
+import com.arsdigita.bebop.PageState;
+
+/**
+ * A component-defined event. Components usually fire an
+ * ActionEvent to indicate that they were the ones receiving
+ * the click in a user's submission, for example, a form might use an
+ * ActionEvent to signal that it has been submitted.
+ *
+ * @see ActionListener
+ * @see java.awt.event.ActionEvent
+ *
+ * @author David Lutterkort
+ *
+ * @version $Id$
+ */
+
+public class ActionEvent extends PageEvent {
+
+ /**
+ * Construct an ActionEvent.
+ *
+ * @param source the component that originated the event
+ * @param state the state of the containing page under the current
+ * request
+ */
+ public ActionEvent(Component source, PageState state) {
+ super(source, state);
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionListener.java
new file mode 100755
index 000000000..a1d7674da
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ActionListener.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import java.util.EventListener;
+
+/**
+ * The listener interface for receiving action events. The class that is
+ * interested in processing an action event implements this interface, and
+ * the object created with that class is registered with a component, using
+ * the component's addActionListener method. When the action event occurs,
+ * that object's actionPerformed method is invoked.
+ *
+ * @see ActionEvent
+ * @see java.awt.event.ActionListener
+ *
+ * @author David Lutterkort
+ *
+ * @version $Id$
+ */
+public interface ActionListener extends EventListener {
+
+ /**
+ * Invoked when an action has been performed.
+ *
+ * @pre e != null
+ */
+ void actionPerformed(ActionEvent e);
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeEvent.java
new file mode 100755
index 000000000..8876f1915
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeEvent.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.PageState;
+
+/**
+ * This class will be
+ * renamed to SelectionEvent.
+ */
+public class ChangeEvent extends PageEvent {
+
+ public ChangeEvent(Object source, PageState state) {
+ super(source, state);
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeListener.java
new file mode 100755
index 000000000..f69dfc1dd
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ChangeListener.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import java.util.EventListener;
+
+/**
+ * This class will be
+ * renamed to SelectionListener.
+ */
+public interface ChangeListener extends EventListener {
+
+ void stateChanged(ChangeEvent e);
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/EventListenerList.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/EventListenerList.java
new file mode 100755
index 000000000..8125c5d2f
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/EventListenerList.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Convenience extensions to {@link javax.swing.event.EventListenerList
+ * Swing's EventListenerList}.
+ * @version $Id$
+ */
+public class EventListenerList extends javax.swing.event.EventListenerList {
+
+ /**
+ * Append all the event listeners from l.
+ *
+ * @param l The list of listeners to copy from
+ *
+ * @pre l != null
+ */
+ public void addAll(EventListenerList l) {
+
+ if ( l.listenerList.length == 0 )
+ return;
+
+ Object[] tmp = new Object[listenerList.length + l.listenerList.length];
+ System.arraycopy(listenerList, 0, tmp, 0, listenerList.length);
+ System.arraycopy(l.listenerList, 0,
+ tmp, listenerList.length, l.listenerList.length);
+ listenerList = tmp;
+ }
+
+ /**
+ * Return an iterator over all event listeners of class t.
+ * This iterator replaces the for loop mentioned in the documentation for
+ * {@link javax.swing.event.EventListenerList Swing's
+ * EventListenerList}.
+ *
+ * @param t The class of the event listeners that should be returned
+ *
+ * @pre t != null
+ * */
+ public Iterator getListenerIterator(final Class t) {
+ return new EventListenerIterator(t);
+ }
+
+ private class EventListenerIterator implements Iterator {
+
+ /**
+ * The listener we will return with the next call to next().
+ * listener[_next] is always a class object of type t, unless all
+ * matching listeners have been returned, in which case _next
+ * is -1
+ * */
+ private int _count;
+ private int _next;
+ private Class _t;
+
+ EventListenerIterator(Class t) {
+
+ _count = getListenerList().length;
+ _next = -2;
+ _t = t;
+ findNext();
+ }
+
+ public boolean hasNext() {
+ return (_next < _count);
+ }
+
+ public Object next() throws NoSuchElementException {
+ if ( ! hasNext() ) {
+ throw new NoSuchElementException("Iterator exhausted");
+ }
+ int result = _next;
+ findNext();
+ return getListenerList()[result+1];
+ }
+
+ public void remove() throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("Removal not supported");
+ }
+
+ /**
+ * Advance _next so that either _next == -1
+ * if all listeners of class _t have been returned in the
+ * enclosing EventListenerList, or that
+ * getListenersList()[_next] == _t and
+ * getListenersList()[_next+1] (the corresponding listener
+ * object) has not been returned yet by next().
+ * */
+ private void findNext() {
+
+ for (int i = _next+2; i<_count; i+=2) {
+
+ if (getListenerList()[i] == _t) {
+ _next = i;
+ return;
+ }
+ }
+ _next = _count;
+ }
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormCancelListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormCancelListener.java
new file mode 100755
index 000000000..7b2a14c92
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormCancelListener.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.FormProcessException;
+import java.util.EventListener;
+
+/**
+ * Defines the interface for a class that performs cleanup after
+ * cancelling out of a form
+ *
+ * @author Kevin Scaldeferri
+ * @version $Id$
+ */
+
+public interface FormCancelListener extends EventListener {
+
+ /**
+ * Performs any necessary cleanup after a user cancels out of
+ * a form
+ *
+ *
Implementations of this method are responsible for catching
+ * specific exceptions that may occur during processing, and either
+ * handling them internally or rethrowing them as instances of
+ * FormProcessException to be handled by the calling
+ * procedure.
+ */
+
+ void cancel(FormSectionEvent e) throws FormProcessException;
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormInitListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormInitListener.java
new file mode 100755
index 000000000..08e964c19
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormInitListener.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.FormProcessException;
+import java.util.EventListener;
+
+/**
+ * Defines the interface for initializing a form with default values.
+ * Typical implementations of this interface query the database to
+ * set up an "edit" form, or obtain an id from a sequence to initialize
+ * a "create" form.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @version $Id$
+ */
+public interface FormInitListener extends EventListener {
+
+ /**
+ * Initializes a FormData object already populated with values from
+ * the request.
+ *
+ * @param date The form data containing data included with this
+ * request. The initializer may require knowledge of form or
+ * parameter properties.
+ *
+ * @param request The HTTP request associated with the
+ * initialization event. This supplied so that the initializer may
+ * rely on contextual information, such information extracted from
+ * headers or cookies or an associated HttpSession
+ * object.
+ * */
+ void init(FormSectionEvent e) throws FormProcessException;
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormProcessListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormProcessListener.java
new file mode 100755
index 000000000..a3abaf87a
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormProcessListener.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.FormProcessException;
+import java.util.EventListener;
+
+/**
+ * Defines the interface for a class that performs a processing step
+ * on valid data.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @version $Id$
+ */
+
+public interface FormProcessListener extends EventListener {
+
+ /**
+ * Performs a processing step on the data in the
+ * FormData object.
+ *
+ *
Implementations of this method are responsible for catching
+ * specific exceptions that may occur during processing, and either
+ * handling them internally or rethrowing them as instances of
+ * FormProcessException to be handled by the calling
+ * procedure.
+ *
+ *
Implementations of this method cannot assume success or
+ * failure of other FormProcessListeners associated with a
+ * particular FormModeel. Each implementation must act independently
+ *
+ * @param model The form model describing the structure and properties
+ * of the form data included with this request.
+ *
+ * @param data The container for all data objects associated with
+ * the request. String values for all parameters specified in the
+ * form model are converted to Java data objects and validated
+ * before processing occurs.
+ *
+ * @param request The HTTP request information from which the form
+ * data was extracted. Note that the request object is supplied
+ * only in case the processing step requires contextual information
+ * (information extracted from cookies or the peer address, for
+ * example) or needs to modify session properties.
+ *
+ * @param response The HTTP response that will be returned to the
+ * user. The processing step may require access to this object to
+ * set cookies or handle errors. */
+
+ void process(FormSectionEvent e) throws FormProcessException;
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSectionEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSectionEvent.java
new file mode 100755
index 000000000..70996cc30
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSectionEvent.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.FormData;
+import com.arsdigita.bebop.PageState;
+
+/**
+ * An event originating from a form. FormSectionEvents are
+ * used to notify listeners that values in a form should be initilialized,
+ * validated or processed.
+ *
+ * @author David Lutterkort
+ *
+ * @version $Id$
+ *
+ * @see FormInitListener
+ * @see FormValidationListener
+ * @see FormProcessListener
+ */
+public class FormSectionEvent extends PageEvent {
+
+ private final transient FormData _formData;
+
+ /**
+ * Get the form data for to the form that fired the event in the current
+ * request.
+ *
+ * @return form data
+ */
+ public final FormData getFormData() {
+ return _formData;
+ }
+
+ /**
+ * Construct a FormSectionEvent.
+ *
+ * @param source the form model that fired the event
+ * @param state the state of the enclosing page
+ * @param formData the form data constructed so far
+ */
+ public FormSectionEvent(Object source,
+ PageState state,
+ FormData formData) {
+ super(source, state);
+ _formData = formData;
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSubmissionListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSubmissionListener.java
new file mode 100755
index 000000000..127c25c33
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormSubmissionListener.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import java.util.EventListener;
+
+import com.arsdigita.bebop.FormProcessException;
+
+/**
+ * The listener called just before a form starts examining a
+ * submission. This listener can throw a {@link FormProcessException} to
+ * indicate that any further processing of the submission should be
+ * aborted. This usually leaves the corresponding {@link
+ * com.arsdigita.bebop.FormData FormData} object in an undefined
+ * state.
+ *
+ * @author David Lutterkort
+ * @version $Id$
+ */
+public interface FormSubmissionListener extends EventListener {
+
+ /**
+ * This method gets called as soon as the FormData for a
+ * form has been filled with the request parameters. The values in the
+ * FormData are transformed but not validated.
+ *
+ * @param e the event encapsulating form data, page state and event source
+ * @throws FormProcessException to signal that further processing of the
+ * form should be aborted.
+ */
+ void submitted(FormSectionEvent e) throws FormProcessException;
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/FormValidationListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormValidationListener.java
new file mode 100755
index 000000000..29ff676c4
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/FormValidationListener.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.FormProcessException;
+import java.util.EventListener;
+
+/**
+ * Defines the interface for a class that implements a validation check
+ * on a set of form data.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @version $Id$
+ */
+public interface FormValidationListener extends EventListener {
+
+ /**
+ * Performs a validation check on the specified FormData
+ * object, involving any number of parameters.
+ *
+ *
The check is always performed after all HTTP request + * parameters have been converted to data objects and stored in the + * FormData object. + * + *
If a validation error is encountered, the setError + * method of the FormData object may be used to set an + * error message for reporting back to the user. + * + *
This method is responsible for catching any exceptions that
+ * may occur during the validation. These exceptions may either
+ * be handled internally, or if they are unrecoverable may be
+ * rethrown as instances of FormProcessException.
+ *
+ * @param e FormSectionEvent containing the FormData as well as the
+ * PageState.
+ * Clients may access the PageState by executing something like
+ * PageState state = fse.getPageState();
+ * Method getFormData() allows access to the Form's data.
+ *
+ * @exception FormProcessException ff the data does not pass the check.
+ */
+
+ void validate(FormSectionEvent e) throws FormProcessException;
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/PageEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/PageEvent.java
new file mode 100755
index 000000000..8491849f3
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/PageEvent.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.PageState;
+
+import java.util.EventObject;
+
+/**
+ * The base class for all page related events. All page related events
+ * should be derived from this class, since it defines a standard way to
+ * get at the source of the event and at the state of the page under the
+ * request that is currently being processed.
+ *
+ * @author David Lutterkort
+ *
+ * @version $Id$
+ */
+public class PageEvent extends EventObject {
+
+ private transient PageState _state;
+
+ /**
+ * Construct a new PageEvent.
+ * @param source the object firing the event, usually a {@link
+ * com.arsdigita.bebop.Component Component}.
+ * @param state the state of the page under the current request
+ */
+ public PageEvent(Object source, PageState state) {
+ super(source);
+ _state = state;
+ }
+
+ /**
+ * Get the state of the page under the request in which the event was fired
+ */
+ public final PageState getPageState() {
+ return _state;
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterEvent.java
new file mode 100755
index 000000000..96696577d
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterEvent.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.PageState;
+import com.arsdigita.bebop.parameters.ParameterData;
+
+import java.util.EventObject;
+
+/**
+ * An event connected to a request parameter.
+ *
+ * @author David Lutterkort
+ *
+ * @version $Id$
+ *
+ * @see ParameterListener
+ * @see com.arsdigita.bebop.parameters.ParameterModel
+ * @see com.arsdigita.bebop.parameters.ParameterData
+ */
+
+public class ParameterEvent extends EventObject {
+
+ /* The request specific data about the event */
+ private transient ParameterData m_data;
+ private transient PageState m_state;
+
+ /**
+ * Construct a ParameterEvent
+ *
+ * @param source the object that originated the event
+ * @param data the data for the parameter from the current request
+ **/
+
+ public ParameterEvent(Object source, ParameterData data) {
+ super(source);
+ m_data = data;
+ m_state = PageState.getPageState();
+ }
+
+
+ /**
+ * Get the request specific data about the parameter.
+ **/
+
+ public final ParameterData getParameterData() {
+ return m_data;
+ }
+
+
+ /**
+ *
+ **/
+
+ public PageState getPageState() {
+ return m_state;
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterListener.java
new file mode 100755
index 000000000..dd1a2499e
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/ParameterListener.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.FormProcessException;
+import java.util.EventListener;
+
+/**
+ * Defines the interface for a class that validates the values of a
+ * single parameter.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @version $Id$ */
+
+public interface ParameterListener extends EventListener {
+
+ /**
+ * Performs a validation check on the data objects associated with a
+ * specific parameter. Validate should call
+ * ParameterData.addError() with a message regarding the nature
+ * of the error.
+ * @param e
+ * @throws com.arsdigita.bebop.FormProcessException
+ */
+ void validate(ParameterEvent e) throws FormProcessException;
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintEvent.java
new file mode 100755
index 000000000..7b22af400
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintEvent.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.PageState;
+
+/**
+ * An event originating from a component. PrintEvents are
+ * fired just before the source component is output either as
+ * part of an XML document or as part of an HTML page.
+ *
+ * @see PrintListener
+ *
+ * @author Uday Mathur
+ * @author David Lutterkort
+ *
+ * @version $Id$
+ *
+ */
+public class PrintEvent extends PageEvent {
+
+ private Object m_target;
+
+ /**
+ * Construct a PrintEvent
+ *
+ * @param source the object that originated the event
+ * @param data the data for the parameter from the current request
+ * @pre source != null
+ * @pre target != null
+ */
+ public PrintEvent(Object source, PageState state, Object target) {
+ super(source, state);
+ m_target = target;
+ }
+
+ /**
+ * Get the target object, the one that can be freely modified by print
+ * listeners. Initially, the target is an unlocked clone of the source of
+ * the event.
+ * @post return != null
+ */
+ public final Object getTarget() {
+ return m_target;
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintListener.java
new file mode 100755
index 000000000..0613f31ba
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/PrintListener.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import java.util.EventListener;
+
+/**
+ * Listeners of this class are called just before a {@link com.arsdigita.bebop.Component} is
+ * about to be output, either in the form of an XML element, or by printing
+ * its HTML representation. The {@link #prepare prepare method} of the
+ * listener can make modifications to the {@link PrintEvent#getTarget
+ * target} of the event. The target will then be used to produce output
+ * instead of the source.
+ *
+ * {@link PrintEvent PrintEvents} are unicast events, which means
+ * that components should only permit the registration of one
+ * PrintListener. Since the PrintListener is
+ * expected to modify the target, allowing multiple listeners to modify the
+ * target of one event would make it impossible to predict the resulting
+ * target component, since an individual listener can not know which
+ * listeners have run before it and which ones will run after it.
+ *
+ * As an example consider the following code: + *
+ * Label l = new Label("Default text");
+ * l.addPrintListener( new PrintListener {
+ * private static final BigDecimal ONE = new BigDecimal(1);
+ * private BigDecimal count = new BigDecimal(0);
+ * public void prepare(PrintEvent e) {
+ * Label t = e.getTarget();
+ * synchronized (count) {
+ * count.add(ONE);
+ * }
+ * t.setLabel("Call no." + count + " since last server restart");
+ * }
+ * });
+ * Adding the label l to a page will lead to a label that
+ * changes in every request and print how many times the containing label
+ * has been called.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @author David Lutterkort
+ * @version $Id$
+ */
+
+public interface PrintListener extends EventListener {
+
+ /**
+ * Prepare the target component returned by {@link PrintEvent#getTarget
+ * e.getTarget()} for output. The target component is an unlocked clone
+ * of the source of the event and can be freely modified within this
+ * method.
+ *
+ * @param e Event containing the page state, the source and the target of
+ * the event
+ *
+ * @see PrintEvent
+ */
+
+ void prepare(PrintEvent e);
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestEvent.java
new file mode 100755
index 000000000..f2017c112
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestEvent.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.Component;
+import com.arsdigita.bebop.PageState;
+
+/**
+ * An event indicating that a Bebop page is being loaded, and control
+ * is about to be passed to the currently selected component
+ *
+ * @author David Lutterkort
+ *
+ * @version $Id$
+ *
+ * @see ActionListener
+ * @see java.awt.event.ActionEvent
+ */
+
+public class RequestEvent extends PageEvent {
+
+ /**
+ * Construct an ActionEvent.
+ *
+ * @param source the component that originated the event
+ * @param state the state of the containing page under the current
+ * request
+ */
+ public RequestEvent(Component source, PageState state) {
+ super(source, state);
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestListener.java
new file mode 100755
index 000000000..3219cf2e8
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/RequestListener.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import java.util.EventListener;
+
+/**
+ * The listener interface for receiving request events. The class that is
+ * interested in processing a request event implements this interface, and
+ * the object created with that class is registered with a Bebop page, using
+ * the Page.addRequestListener method. When the page has finished processing
+ * the page state, and is about to pass control to the currently selected
+ * component, the pageRequested method will be called.
+ *
+ * @author David Lutterkort
+ *
+ * @version $Id$
+ *
+ * @see ActionEvent
+ * @see java.awt.event.ActionListener
+ */
+public interface RequestListener extends EventListener {
+
+ /**
+ * Invoked when an action has been performed.
+ *
+ * @pre e != null
+ */
+ void pageRequested(RequestEvent e);
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectListener.java
new file mode 100755
index 000000000..1f626414a
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectListener.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import java.util.EventListener;
+
+/**
+ * Analogous to Widget
+ * PrintListeners, this is called when the widget is displayed (or
+ * validated) to get the dataset. The dataset should be created
+ * dynamically so it can vary according to form variables.
+ * Eventually, this may also support setting the initial value for a
+ * SearchAndSelect widget, so that it may act as an edit widget as
+ * well.
+ *
+ * @author Patrick McNeill
+ * @version $Id$
+ * @since 4.5 */
+public interface SearchAndSelectListener extends EventListener {
+
+ SearchAndSelectModel getModel( PageEvent e );
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectModel.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectModel.java
new file mode 100755
index 000000000..a8585fa10
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/SearchAndSelectModel.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+/**
+ * Listener interface for
+ * the SeachAndSelect Bebop widget. SearchAndSelect requires
+ * knowledge about the data it is searching over (to determine the
+ * display method and to actually execute the query).
+ *
+ * @author Patrick McNeill
+ * @version $Id$
+ * @since 4.5 */
+public interface SearchAndSelectModel {
+
+ /**
+ * Specify the user's search and restrict the result set to those queries
+ * that match. An empty string should return all results.
+ *
+ * @param query the user's search string, space or comma delimited words
+ */
+ void setQuery ( String query );
+
+ /**
+ * Retrieve the query that was last used.
+ *
+ * @return the query string
+ */
+ String getQuery ();
+
+ /**
+ * Return the number of items that are currently selected by the query
+ * string. If the query string is empty, this should return the number
+ * of items in the dataset.
+ *
+ * @return the number of currently selected items
+ */
+ int resultsCount ();
+
+ /**
+ * Get the "i"th label (0 based indexing)
+ *
+ * @param i the label number to retrieve
+ * @return the ith label
+ */
+ String getLabel (int i);
+
+ /**
+ * Get the "i"th ID (0 based indexing)
+ *
+ * @param i the ID number to retrieve
+ * @return the ith ID
+ */
+ String getID (int i);
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionAdapter.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionAdapter.java
new file mode 100755
index 000000000..d9225ffef
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionAdapter.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+/**
+ * An implentation of the TableActionListener interface meant to save the
+ * developer from having to override both the {@link
+ * #cellSelected(TableActionEvent)} and {@link #headSelected(TableActionEvent)}
+ * methods when they only need to change the behavior of one.
+ *
+ * @see TableActionEvent
+ * @author David Lutterkort
+ * @version $Id$
+ */
+public class TableActionAdapter implements TableActionListener {
+
+ /**
+ * A no-op implementation of {@link
+ * TableActionListener#cellSelected(TableActionEvent)}.
+ *
+ * @param e the event fired for the table.
+ */
+ public void cellSelected(TableActionEvent e) {
+ return;
+ }
+
+ /**
+ * A no-op implementation of {@link
+ * TableActionListener#headSelected(TableActionEvent)}.
+ *
+ * @param e the event fired for the table.
+ */
+ public void headSelected(TableActionEvent e) {
+ return;
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionEvent.java
new file mode 100755
index 000000000..f141f31a0
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionEvent.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.Component;
+import com.arsdigita.bebop.PageState;
+
+/**
+ * An event for the {@link com.arsdigita.bebop.Table} component.
+ * Table will fire this event when one of its active cells receives a
+ * click.
+ *
+ * @see TableActionListener
+ * @see TableActionAdapter
+ * @author David Lutterkort
+ * @version $Id$
+ */
+public class TableActionEvent extends ActionEvent {
+
+ private Object m_rowKey;
+ private Integer m_column;
+
+ /**
+ * Construct a TableActionEvent for a click on a particular row
+ * and a particular column.
+ *
+ * @param source the Component generating the event.
+ * @param s the state for the current request.
+ * @param rowKey the key for the row where the click was registered.
+ * @param column the index of the column where the click was registered.
+ */
+ public TableActionEvent(Component source, PageState s,
+ Object rowKey, Integer column) {
+ super(source, s);
+ m_rowKey = rowKey;
+ m_column = column;
+ }
+
+ /**
+ * Construct a TableActionEvent for a click on a particular row.
+ *
+ * @param source the Component generating the event.
+ * @param s the state for the current request.
+ * @param rowKey the key for the row where the click was registered.
+ */
+ public TableActionEvent(Component source, PageState s, Object rowKey) {
+ this(source, s, rowKey, new Integer(-1));
+ }
+
+ /**
+ * Get the key for the row that received the click.
+ *
+ * @return the key for the row that received the click.
+ */
+ public final Object getRowKey() {
+ return m_rowKey;
+ }
+
+ /**
+ * Get the index of the column that received the click.
+ *
+ * @return the index of the column that received the click.
+ */
+ public final Integer getColumn() {
+ return m_column;
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionListener.java
new file mode 100755
index 000000000..2e6cbd4a4
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/TableActionListener.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import java.util.EventListener;
+
+/**
+ * Specifies the interface for handling events on {@link
+ * com.arsdigita.bebop.Table}. Programmers wishing to override just
+ * one of these methods, not both, may prefer to use {@link
+ * TableActionAdapter}.
+ *
+ * @see TableActionEvent
+ * @see TableActionAdapter
+ * @author David Lutterkort
+ * @version $Id$
+ */
+public interface TableActionListener extends EventListener {
+
+ /**
+ * An event handler for actions on a particular cell or a set of
+ * cells.
+ *
+ * @param e the event fired for the table.
+ */
+ void cellSelected(TableActionEvent e);
+
+ /**
+ * An event handler for actions on a particular column heading or
+ * set of column headings.
+ *
+ * @param e the event fired for the table.
+ */
+ void headSelected(TableActionEvent e);
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionEvent.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionEvent.java
new file mode 100755
index 000000000..db4f05586
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import com.arsdigita.bebop.Component;
+import com.arsdigita.bebop.PageState;
+
+/**
+ * An event for the {@link com.arsdigita.bebop.Tree} component.
+ * Tree will fire this event when one of its nodes is expanded or
+ * collapsed.
+ *
+ * @author David Lutterkort
+ * @version $Id$
+ */
+public class TreeExpansionEvent extends ActionEvent {
+
+ private Object m_nodeKey;
+
+ public TreeExpansionEvent(Component source, PageState s, Object nodeKey) {
+ super(source, s);
+ m_nodeKey = nodeKey;
+ }
+
+ /**
+ * Get the key for the node that was expanded or collapsed.
+ *
+ * @return the key for the node that was expanded or collapsed.
+ */
+ public final Object getNodeKey() {
+ return m_nodeKey;
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionListener.java b/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionListener.java
new file mode 100755
index 000000000..a5c8e12cd
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/event/TreeExpansionListener.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.event;
+
+import java.util.EventListener;
+
+/**
+ * The listener that is notified when a tree node is expanded or
+ * collapsed.
+ *
+ * @author David Lutterkort
+ * @version $Id$
+ */
+public interface TreeExpansionListener extends EventListener {
+
+ /**
+ * Called whenever an item in the tree has been collapsed.
+ */
+ void treeCollapsed(TreeExpansionEvent event);
+
+ /**
+ * Called whenever an item in the tree has been expanded.
+ */
+ void treeExpanded(TreeExpansionEvent event);
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/CheckboxGroup.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/CheckboxGroup.java
new file mode 100755
index 000000000..38c3adb69
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/CheckboxGroup.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.form;
+
+
+
+
+import com.arsdigita.bebop.parameters.ArrayParameter;
+// This interface contains the XML element name of this class
+// in a constant which is used when generating XML
+import com.arsdigita.bebop.util.BebopConstants;
+
+/**
+ * A class representing a group of associated checkboxes.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @author Rory Solomon
+ * @author Michael Pih
+ * @version $Id$
+ */
+public class CheckboxGroup extends OptionGroup implements BebopConstants {
+
+ public CheckboxGroup(String name) {
+ this(new ArrayParameter(name));
+ }
+
+ public CheckboxGroup(ArrayParameter param) {
+ super(param);
+ //m_xmlElement = BEBOP_CHECKBOX;
+ }
+
+ /**
+ * Returns a string naming the type of this widget.
+ */
+ public String getType() {
+ return "checkbox";
+ }
+
+ /** The XML tag.
+ * @return The tag to be used for the top level DOM element
+ * generated for this type of Widget. */
+ @Override
+ protected String getElementTag() {
+ return BEBOP_CHECKBOXGROUP;
+ }
+
+ @Override
+ public String getOptionXMLElement() {
+ return BEBOP_CHECKBOX;
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/DHTMLEditor.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/DHTMLEditor.java
new file mode 100755
index 000000000..84bbe2eb5
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/DHTMLEditor.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2001-2006 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.form;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import com.arsdigita.bebop.Bebop;
+import com.arsdigita.bebop.PageState;
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.bebop.parameters.StringParameter;
+import com.arsdigita.web.Web;
+import com.arsdigita.xml.Element;
+
+/**
+ * Displays and manages a WYSIWYG HTML editor that takes advantage of DHTML scripting features. This
+ * class can use: - FCKeditor
+ * - HTMLarea for backwards compatibility, development discontinued Editor is choosen based on the
+ * config parameter waf.bebop.dhtml_editor, default is "Xinha", which is the successor of HTMLarea
+ *
+ * @author Jim Parsons
+ * @author Richard Li
+ * @author Chris Burnett
+ * @author Alan Pevec
+ *
+ * @version $Id$
+ */
+public class DHTMLEditor extends TextArea {
+
+ /**
+ * Constant for specifying OFF value for the WRAP attribute of this image
+ * input.
+ *
+ * See here
+ * for a description of what this attribute does.
+ */
+ public static final int OFF = 0;
+
+ /**
+ * Constant for specifying HARD value for the WRAP attribute of this image
+ * input.
+ *
+ * See here
+ * for a description of what this attribute does.
+ */
+ public static final int HARD = 1;
+
+ /**
+ * Constant for specifying SOFT value for the WRAP attribute of this image
+ * input. See here
+ * for a description of what this attribute does.
+ */
+ public static final int SOFT = 2;
+
+ /**
+ * Config objects for supported DHTMP editors
+ */
+ public static class Config {
+
+ // WARNING: Processing of these default values by CMSConfig does NOT
+ // work correctly because of deviciencies in unmarshal method there.
+ public static final Config STANDARD = new Config("Xinha.Config",
+ "/assets/xinha/CCMcoreXinhaConfig.js");
+
+ /**
+ * Example FCKEditor configuration.
+ */
+ public static final Config FCK_STANDARD = new Config("FCKEditor.Config.StyleDefault",
+ "/assets/fckeditor/config/fckconfigstyledefault.js");
+
+ public static final Config FCK_CMSADMIN = new Config("FCKEditor.Config.StyleCMSAdmin",
+ "/assets/fckeditor/config/fckconfigstylecmsadmin.js");
+
+ /**
+ * Example old HTMLarea configuration.
+ */
+ public static final Config HTMLAREA = new Config("HTMLArea.Config", null);
+
+ private String m_name;
+ private String m_path;
+
+ public Config(String name) {
+ this(name, null);
+ }
+
+ public Config(String name,
+ String path) {
+ m_name = name;
+ m_path = path;
+ }
+
+ public String getName() {
+ return m_name;
+ }
+
+ public String getPath() {
+ return m_path;
+ }
+
+ public static Config valueOf(String cfg) {
+ int offset = cfg.indexOf(",");
+ if (offset != -1) {
+ return new Config(cfg.substring(0, offset),
+ cfg.substring(offset + 1));
+ } else {
+ return new Config(cfg);
+ }
+ }
+
+ public String toString() {
+ if (m_path == null) {
+ return m_name;
+ } else {
+ return m_name + "," + m_path;
+ }
+ }
+
+ } //end config object(s)
+
+ private Config m_config;
+ private Set m_plugins;
+ private Set m_hiddenButtons;
+
+ /**
+ * Constructor
+ *
+ * @param name
+ */
+ public DHTMLEditor(String name) {
+ this(new StringParameter(name));
+ }
+
+ /**
+ * Constructor
+ *
+ * @param model
+ */
+ public DHTMLEditor(ParameterModel model) {
+ this(model, Config.STANDARD);
+ }
+
+ /**
+ * Constructor
+ *
+ * @param model
+ * @param config
+ */
+ public DHTMLEditor(ParameterModel model,
+ Config config) {
+ super(model);
+ m_config = config;
+ m_plugins = new HashSet();
+ m_hiddenButtons = new HashSet();
+ }
+
+ /**
+ * Returns a string naming the type of this widget.
+ */
+ public String getType() {
+ return "DHTMLEditor";
+ }
+
+ public String getEditorURL() {
+ return Bebop.getConfig().getDHTMLEditorSrcFile().substring(
+ 0, Bebop.getConfig().getDHTMLEditorSrcFile().lastIndexOf("/") + 1);
+ }
+
+ public String getEditorSrc() {
+ return Bebop.getConfig().getDHTMLEditorSrcFile();
+ }
+
+ /**
+ * deprecated - use {@link setConfig(Config)}
+ *
+ * @param config
+ */
+ public void setConfig(String config) {
+ setAttribute("config", config);
+ }
+
+ public void setConfig(Config config) {
+ m_config = config;
+ }
+
+ public void addPlugin(String name) {
+ m_plugins.add(name);
+ }
+
+ /**
+ * Prevent the specified button from being displayed in the editor toolbar.
+ *
+ * @param name name of the button, as specified in the btnList of the htmlarea.js file
+ *
+ */
+ public void hideButton(String name) {
+ m_hiddenButtons.add(name);
+ }
+
+ /**
+ * Sets the ROWS attribute for the TEXTAREA tag.
+ */
+ @Override
+ public void setRows(int rows) {
+ setAttribute("rows", String.valueOf(rows));
+ }
+
+ /**
+ * Sets the COLS attribute for the TEXTAREA tag.
+ */
+ @Override
+ public void setCols(int cols) {
+ setAttribute("cols", String.valueOf(cols));
+ }
+
+ /**
+ * Sets the COLS attribute for the TEXTAREA tag.
+ */
+ @Override
+ public void setWrap(int wrap) {
+ String wrapString = null;
+
+ switch (wrap) {
+ case OFF:
+ wrapString = "off";
+ break;
+ case HARD:
+ wrapString = "hard";
+ break;
+ case SOFT:
+ wrapString = "soft";
+ break;
+ }
+
+ if (wrapString != null) {
+ setAttribute("wrap", wrapString);
+ }
+ }
+
+ /**
+ * The XML tag.
+ *
+ * @return The tag to be used for the top level DOM element generated for this type of Widget.
+ */
+ @Override
+ protected String getElementTag() {
+ return Bebop.getConfig().getDHTMLEditor();
+ }
+
+ /**
+ * Generates the DOM for the DHTML editor widget
+ * + * Generates DOM fragment: + *
+ * Generates DOM fragment:
+ * Multiple select widget pair for knowledge types. This FormStep
+ * displays two multiple select widgets, one which contains possible
+ * the user may want to add, and the right displays the options that
+ * are currently applicable. To use the widget, you should call {@link
+ * #setLeftMultipleSelect(RequestLocal)} and {@link
+ * #setRightMultipleSelect(RequestLocal)} and pass in the appropriate
+ * collections to initialize the MutlipleSelect options. Then, in the
+ * process listener of the form in which the MultipleSelectPairWidget
+ * is embedded, call {@link #getSelectedOptions(PageState)} and {@link
+ * #getUnselectedOptions(PageState)} to get the chosen values. The process
+ * listener for the parent form must use the Submit.isSelected(ps) so
+ * that the process listener can distinguish between different types
+ * of form submits. Note that the right multiple select can be empty and does not need
+ * to be set. This class also uses a relatively inefficient
+ * implementation of removeOption in {@link OptionGroup OptionGroup}
+ * so that operations run in O(N^2). This can be reduced to O(N) with
+ * a more optimal implementation of OptionGroup.
+ * Each URL has the following accessors, here set next to an example URL
+ * instance,
+ *
+ * Atomic parts:
+ *
+ * <bebop:dhtmleditor name=... value=... [onXXX=...]/>
+ *
+ */
+ @Override
+ public void generateWidget(PageState state, Element parent) {
+ String value = getParameterData(state).marshal();
+ Element editor = parent.newChildElement(getElementTag(), BEBOP_XML_NS);
+
+ editor.addAttribute("name", getName());
+ generateDescriptionXML(state, editor);
+
+ // Set the needed config params so they don't have to be hardcoded in the theme
+ editor.addAttribute("editor_url", Web.getWebappContextPath().concat(getEditorURL()));
+ editor.addAttribute("editor_src", Web.getWebappContextPath().concat(getEditorSrc()));
+
+ if (value != null) {
+ editor.setText(value);
+ }
+
+ exportAttributes(editor);
+
+ Element config = editor.newChildElement("bebop:config", BEBOP_XML_NS);
+ config.addAttribute("name", m_config.getName());
+ if (m_config.getPath() != null) {
+ config.addAttribute("path", Web.getWebappContextPath().concat(m_config.getPath()));
+ }
+ if (m_hiddenButtons.size() > 0) {
+
+ StringBuffer hiddenButtons = new StringBuffer();
+ // list must start and end with a space
+ hiddenButtons.append(" ");
+ Iterator hidden = m_hiddenButtons.iterator();
+ while (hidden.hasNext()) {
+ hiddenButtons.append(hidden.next());
+ hiddenButtons.append(" ");
+ }
+ config.addAttribute("hidden-buttons", hiddenButtons.toString());
+ }
+ Iterator plugins = m_plugins.iterator();
+ while (plugins.hasNext()) {
+ String name = (String) plugins.next();
+ Element plugin = editor.newChildElement("bebop:plugin", BEBOP_XML_NS);
+ plugin.addAttribute("name", name);
+ }
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/Date.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/Date.java
new file mode 100755
index 000000000..f12f74f75
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/Date.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.form;
+
+import java.text.DateFormatSymbols;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+import com.arsdigita.util.Assert;
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.bebop.parameters.DateParameter;
+import com.arsdigita.bebop.parameters.ParameterData;
+import com.arsdigita.bebop.PageState;
+import com.arsdigita.bebop.Form;
+import com.arsdigita.bebop.FormData;
+import com.arsdigita.bebop.parameters.*;
+// This interface contains the XML element name of this class
+// in a constant which is used when generating XML
+import com.arsdigita.bebop.util.BebopConstants;
+
+import com.arsdigita.globalization.GlobalizationHelper;
+import com.arsdigita.bebop.util.GlobalizationUtil;
+
+import com.arsdigita.xml.Element;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/**
+ * A class representing a date field in an HTML form.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @author Michael Pih
+ * @author Sören Bernstein Form Object for this Widget.
+ *
+ * @exception IllegalStateException if form already set.
+ */
+ @Override
+ public void setForm(Form f) {
+ super.setForm(f);
+ m_year.setForm(f);
+ m_month.setForm(f);
+ m_day.setForm(f);
+ }
+
+ public Object getFragmentValue(PageState ps, int field) {
+ Assert.exists(ps, "PageState");
+ FormData f = getForm().getFormData(ps);
+ if (f != null) {
+ java.util.Date value = (java.util.Date) f.get(getName());
+ if (value != null) {
+ Calendar c = Calendar.getInstance();
+ c.setTime(value);
+ return new Integer(c.get(field));
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void setClassAttr(String at) {
+ m_month.setClassAttr(at);
+ m_year.setClassAttr(at);
+ m_day.setClassAttr(at);
+ super.setClassAttr(at);
+ }
+
+ private void populateMonthOptions() {
+
+ Locale locale = GlobalizationHelper.getNegotiatedLocale();
+
+ if (m_locale == null || (locale != null && !m_locale.equals(locale))) {
+
+ DateFormatSymbols dfs = new DateFormatSymbols(locale);
+ String[] months = dfs.getMonths();
+
+ m_month.clearOptions();
+
+ if (this.getParameterModel() instanceof IncompleteDateParameter) {
+ if (((IncompleteDateParameter) this.getParameterModel()).isSkipMonthAllowed()) {
+ m_month.addOption(new Option("", ""));
+ }
+ }
+ for (int i = 0; i < months.length; i += 1) {
+ // This check is necessary because
+ // java.text.DateFormatSymbols.getMonths() returns an array
+ // of 13 Strings: 12 month names and an empty string.
+ if (months[i].length() > 0) {
+ m_month.addOption(new Option(String.valueOf(i), months[i]));
+ }
+ }
+ m_locale = GlobalizationHelper.getNegotiatedLocale();
+ }
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/DateTime.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/DateTime.java
new file mode 100755
index 000000000..dc780534f
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/DateTime.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.form;
+
+import com.arsdigita.bebop.Form;
+import com.arsdigita.bebop.PageState;
+import com.arsdigita.bebop.parameters.DateTimeParameter;
+import com.arsdigita.bebop.parameters.NotNullValidationListener;
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.bebop.util.BebopConstants;
+import com.arsdigita.xml.Element;
+
+/**
+ * A class representing a date and time field in an HTML form.
+ * (based on the code in Date.java)
+ *
+ * @author Sören Bernstein Form Object for this Widget.
+ * @exception IllegalStateException if form already set.
+ */
+ @Override
+ public void setForm(Form f) {
+ super.setForm(f);
+ m_date.setForm(f);
+ m_time.setForm(f);
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/Deditor.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/Deditor.java
new file mode 100755
index 000000000..23342b5c5
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/Deditor.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.form;
+
+
+import com.arsdigita.xml.Element;
+
+import com.arsdigita.bebop.PageState;
+import com.arsdigita.bebop.form.Widget;
+import com.arsdigita.bebop.parameters.ParameterModel;
+// This interface contains the XML element name of this class
+// in a constant which is used when generating XML
+import com.arsdigita.bebop.util.BebopConstants;
+
+
+
+/**
+ * A class representing a textarea field in an HTML form.
+ *
+ * @deprecated See {@link DHTMLEditor}
+ * @author Jim Parsons
+ */
+public class Deditor extends Widget implements BebopConstants {
+
+
+ /**
+ * Constant for specifying OFF value for the
+ * WRAP attribute of this image input. See here
+ * for a description of what this attribute does. */
+
+ public static final int OFF = 0;
+
+ /**
+ * Constant for specifying HARD value for the
+ * WRAP attribute of this image input. * See here
+ * for a description of what this attribute does.
+ */
+ public static final int HARD = 1;
+
+ /**
+ * Constant for specifying SOFT value for the
+ * WRAP attribute of this image input. See here
+ * for a description of what this attribute does.
+ */
+ public static final int SOFT = 2;
+
+ public Deditor(String name) {
+ super(name);
+ }
+
+ public Deditor(ParameterModel model) {
+ super(model);
+ }
+
+
+ /**
+ * Returns a string naming the type of this widget.
+ */
+ public String getType() {
+ return "deditor";
+ }
+
+
+ /**
+ * Set the default value (text)
+ * @deprecated [since 17Aug2001] use {@link Widget#setDefaultValue(Object)}
+ */
+ public void setValue( String text ) {
+ this.setDefaultValue(text);
+ }
+
+ /**
+ * Sets the ROWS attribute for the TEXTAREA tag.
+ */
+ public void setRows(int rows) {
+ setAttribute("rows", String.valueOf(rows));
+ }
+
+ /**
+ * Sets the COLS attribute for the TEXTAREA tag.
+ */
+ public void setCols(int cols) {
+ setAttribute("cols", String.valueOf(cols));
+ }
+
+ /**
+ * Sets the COLS attribute for the TEXTAREA tag.
+ */
+ public void setWrap(int wrap) {
+ String wrapString = null;
+
+ switch (wrap) {
+ case OFF:
+ wrapString = "off";
+ break;
+ case HARD:
+ wrapString = "hard";
+ break;
+ case SOFT:
+ wrapString = "soft";
+ break;
+ }
+
+ if (wrapString != null) {
+ setAttribute("wrap", wrapString);
+ }
+ }
+
+ /**
+ * Is this a compound widget?
+ * @return false
+ */
+ public boolean isCompound() {
+ return false;
+ }
+
+ /** The XML tag.
+ * @return The tag to be used for the top level DOM element
+ * generated for this type of Widget. */
+ protected String getElementTag() {
+ return "bebop:deditor";
+ }
+
+ /**
+ * Generates the DOM for the textarea widget
+ * <bebop:textarea name=... value=... [onXXX=...]/>
+ *
+ */
+ public void generateWidget( PageState state, Element parent ) {
+
+ Element deditor = parent.newChildElement(getElementTag(), BEBOP_XML_NS);
+
+ deditor.addAttribute("name", getName());
+
+ String userAgent =
+ state.getRequest().getHeader("user-agent").toLowerCase();
+ boolean isIE55 =
+ (userAgent != null &&
+ ((userAgent.indexOf("msie 5.5") != -1) ||
+ (userAgent.indexOf("msie 6") != -1)));
+
+ deditor.addAttribute("isIE55", (new Boolean(isIE55)).toString());
+
+
+ String value = getParameterData(state).marshal();
+ if ( value == null ) {
+ value = "";
+ }
+ Element texter = deditor.newChildElement("bebop:textcontent",BEBOP_XML_NS);
+ texter.setCDATASection(value);
+ exportAttributes(deditor);
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/Fieldset.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/Fieldset.java
new file mode 100644
index 000000000..40922d78a
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/Fieldset.java
@@ -0,0 +1,36 @@
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.arsdigita.bebop.form;
+
+import com.arsdigita.bebop.Label;
+import com.arsdigita.bebop.PageState;
+import com.arsdigita.bebop.SimpleContainer;
+import com.arsdigita.globalization.GlobalizedMessage;
+import com.arsdigita.xml.Element;
+
+/**
+ * A fieldset for form.
+ *
+ * @author Sören Bernstein FormErrorDisplay
+ *
+ * @param form The parent form whose errors will be displayed by
+ * this widget
+ */
+ public FormErrorDisplay(Form form) {
+ super(new FormErrorModelBuilder(form));
+ m_form = form;
+ }
+
+ /**
+ * Return the form whose errors are to be displayed
+ * @return the form whose errors are to be displayed
+ */
+ public final Form getForm() {
+ return m_form;
+ }
+
+ /**
+ * Determine if there are errors to display
+ *
+ * @param state the current page state
+ * @return true if there are any errors to display; false otherwise
+ */
+ protected boolean hasErrors(PageState state) {
+ FormData data = m_form.getFormData(state);
+ return (data != null && data.getErrors().hasNext());
+ }
+
+ // A private class which builds a ListModel based on form errors
+ private static class FormErrorModelBuilder extends LockableImpl
+ implements ListModelBuilder {
+
+ private Form m_form;
+
+ public FormErrorModelBuilder(Form form) {
+ super();
+ m_form = form;
+ }
+
+ public ListModel makeModel(List l, PageState state) {
+ FormData data = m_form.getFormData(state);
+ if(data == null) {
+ return new StringIteratorModel(Collections.EMPTY_LIST.iterator());
+ } else {
+ return new StringIteratorModel(data.getErrors());
+ }
+ }
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/Hidden.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/Hidden.java
new file mode 100755
index 000000000..968df902b
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/Hidden.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.form;
+
+import com.arsdigita.bebop.parameters.ParameterModel;
+
+/**
+ * A class representing a hidden HTML form element.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @author Stas Freidin
+ * @author Rory Solomon
+ * @author Michael Pih
+ * @version $Id$
+ */
+public class Hidden extends Widget {
+
+ public Hidden(String name) {
+ super(name);
+ }
+
+ public Hidden(ParameterModel model) {
+ super(model);
+ }
+
+ /**
+ * Returns a string naming the type of this widget.
+ */
+ public String getType() {
+ return "hidden";
+ }
+
+ public boolean isCompound() {
+ return false;
+ }
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/ImageSubmit.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/ImageSubmit.java
new file mode 100755
index 000000000..94728e23e
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/ImageSubmit.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.form;
+
+import com.arsdigita.bebop.parameters.ParameterModel;
+import com.arsdigita.bebop.util.PanelConstraints;
+
+/**
+ * A class representing an image HTML form element.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @author Rory Solomon
+ * @author Michael Pih
+ * @version $Id$
+ */
+public class ImageSubmit extends Widget implements PanelConstraints {
+
+
+ /**
+ * Constructor.
+ *
+ * @param name
+ */
+ public ImageSubmit(String name) {
+ super(name);
+ }
+
+ public ImageSubmit(ParameterModel model) {
+ super(model);
+ }
+
+ /**
+ * Returns a string naming the type of this widget.
+ *
+ * @return
+ */
+ @Override
+ public String getType() {
+ return "image";
+ }
+
+ /**
+ * Sets the SRC attribute for the INPUT tag
+ * used to render this form element.
+ *
+ * @param location
+ */
+ public void setSrc(String location) {
+ setAttribute("src",location);
+ }
+
+ /*
+ * Sets the ALRT attribute for the INPUT tag
+ * used to render this form element.
+ */
+ public void setAlt(String alt) {
+ setAttribute("alt",alt);
+ }
+
+ /**
+ * Sets the ALIGN attribute for the INPUT tag
+ * used to render this form element. Uses the positional constants defined
+ * in Interface PanelConstraints.
+ * Note: These may be refactored in future versions.
+ *
+ * @param align Symbolic constant denoting the alignment.
+ */
+ public void setAlign(int align) {
+ String alignString = null;
+
+ switch (align) {
+ case LEFT:
+ alignString = "left";
+ break;
+ case RIGHT:
+ alignString = "right";
+ break;
+ case TOP:
+ alignString = "top";
+ break;
+ case ABSMIDDLE:
+ alignString = "absmiddle";
+ break;
+ case ABSBOTTOM:
+ alignString = "absbottom";
+ break;
+ case TEXTTOP:
+ alignString = "texttop";
+ break;
+ case MIDDLE:
+ alignString = "middle";
+ break;
+ case BASELINE:
+ alignString = "baseline";
+ break;
+ case BOTTOM:
+ alignString = "botton";
+ break;
+ }
+
+ if (alignString != null)
+ setAttribute("align",alignString);
+ }
+
+ @Override
+ public boolean isCompound() {
+ return false;
+ }
+
+ /**
+ * Callback method for rendering this Image widget in a visitor.
+ */
+ /* public void accept(FormVisitor visitor) throws IOException {
+ visitor.visitImage(this);
+ }
+ */
+
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelect.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelect.java
new file mode 100755
index 000000000..cbcdcc65c
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelect.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.form;
+
+import com.arsdigita.bebop.parameters.ArrayParameter;
+// This interface contains the XML element name of this class
+// in a constant which is used when generating XML
+import com.arsdigita.bebop.util.BebopConstants;
+
+/**
+ * A class
+ * representing an HTML SELECT element.
+ *
+ * @author Karl Goldstein
+ * @author Uday Mathur
+ * @author Rory Solomon
+ * @author Michael Pih
+ * @version $Id$ */
+public class MultipleSelect extends Select implements BebopConstants {
+
+ public MultipleSelect(String name) {
+ super(new ArrayParameter(name));
+ }
+
+ /** State that this is a multiple select
+ * @return true
+ */
+ public boolean isMultiple()
+ { return true; }
+
+ /** The XML tag for this derived class of Widget. */
+
+ protected String getElementTag() {
+ return BEBOP_MULTISELECT;
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelectPairWidget.java b/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelectPairWidget.java
new file mode 100755
index 000000000..e9d8c14f0
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/bebop/form/MultipleSelectPairWidget.java
@@ -0,0 +1,561 @@
+/*
+ * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.bebop.form;
+
+import com.arsdigita.bebop.FormStep;
+import com.arsdigita.bebop.GridPanel;
+import com.arsdigita.bebop.Label;
+import com.arsdigita.bebop.PageState;
+import com.arsdigita.bebop.RequestLocal;
+import com.arsdigita.bebop.event.FormInitListener;
+import com.arsdigita.bebop.event.FormProcessListener;
+import com.arsdigita.bebop.event.FormSectionEvent;
+import com.arsdigita.bebop.parameters.ArrayParameter;
+import com.arsdigita.util.Assert;
+import com.arsdigita.xml.Element;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Iterator;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.log4j.Logger;
+
+/**
+ * http://example.com:8080/ccmapp/forum/index.jsp?cat=2&cat=5:
+ *
+ *
+ * getScheme() -> "http"
+ * getServerName() -> "example.com"
+ * getServerPort() -> 8080
+ * getWebContextPath() -> "/ccmapp"
+ * getServletPath() -> "/forum"
+ * getPathInfo() -> "/index.jsp"
+ * getParameter("cat") -> "2"
+ * getParameterValues("cat") -> {"2", "5"}
+ *
+ * Composite parts: + * + *
+ * + * + * + *+ * toString() -> "/ccmapp/forum/index.jsp?cat=2&cat=5" + * getURL() -> "http://example.com:8080/ccmapp/forum/index.jsp?cat=2&cat=5 + * getServerURI() -> "http://example.com:8080" // No trailing "/" + * getRequestURI() -> "/ccmapp/forum/index.jsp" + * getQueryString() -> "cat=2&cat=5" // No leading "?" + * getParameterMap() -> {cat={"2", "5"}} + *
+ * The toString() method returns a URL suitable for use in
+ * hyperlinks; since in the common case, the scheme, server name, and port are
+ * best left off, toString() omits them. The getURL()
+ * method returns a String URL which is fully qualified. Both
+ * getURL() and getServerURI() omit the port from
+ * their return values if the server port is the default, port 80.
+ * Creating URLs will usually be done via one of the static create methods:
+ * + *
+ * URL.root() creates a URL pointing at the server's root path,
+ * "/".
+ * URL.request(req, params) creates a URL reflecting the request
+ * the client made but using the passed-in parameters instead.
+ * URL.there(req, path, params) and its variants produce URLs that
+ * go through the CCM main dispatcher. The variant
+ * URL.there(req, app, pathInfo, params) dispatches to
+ * pathInfo under the specified application. The variant
+ * URL.here(req, pathInfo, params) dispatches to
+ * pathInfo under the current application.
+ * URL.excursion(req, path, params) produces URLs that go through
+ * the dispatcher to a destination but also encode and store the origin. This is
+ * used by LoginSignal and ReturnSignal to implement
+ * UI excursions.
+ * All static create methods taking an HttpServletRequest (1)
+ * preserve the request's scheme, server name, and port and (2) run parameter
+ * listeners if the URL's parameter map is not null.
+ *
+ * Those methods not taking an HttpServletRequest use the scheme,
+ * server name, and port defined in WebConfig.
+ * All static create methods taking a ParameterMap take null to
+ * mean no query string at all. URLs defined this way will have no query string
+ * and no "?".
+ * Those methods not taking a ParameterMap argument implicitly
+ * create an empty parameter map. Note that this is different from creating a
+ * URL with a null parameter map, which produces a URL with no query string.
+ * Assembles a fully qualified URL from its fundamental pieces. The contract
+ * of URL dictates that once params is passed in to this
+ * constructor, no parameters should be added or removed. This is to make
+ * URL in practice a read-only object.
"http", for example; see {@link
+ * javax.servlet.ServletRequest#getScheme()}
+ *
+ * @param serverName a valid domain name, for example
+ * "ccm.redhat.com"; see {@link
+ * javax.servlet.ServletRequest#getServerName()}
+ *
+ * @param serverPort 8080, for instance; see {@link
+ * javax.servlet.ServletRequest#getServerPort()}
+ *
+ * @param contextPath the path to your web app; empty string indicates the
+ * default context; any other values for contextPath must
+ * start with "/" but not end in
+ * "/"; contextPath cannot be null; see {@link
+ * javax.servlet.http.HttpServletRequest#getContextPath()}
+ *
+ * @param servletPath the path to your servlet; empty string and values
+ * starting with "/" are valid, but null is
+ * not; see {@link
+ * javax.servlet.http.HttpServletRequest#getServletPath()}
+ *
+ * @param pathInfo the path data remaining after the servlet path but
+ * before the query string; pathInfo may be null; see {@link
+ * javax.servlet.http.HttpServletRequest#getPathInfo()}
+ *
+ * @param params a ParameterMap representing a set of
+ * query parameters
+ *
+ * @return a fully specified URL
+ */
+ public URL(final String scheme,
+ final String serverName,
+ final int serverPort,
+ final String contextPath,
+ final String servletPath,
+ final String pathInfo,
+ final ParameterMap params) {
+ HttpServletRequest req = Web.getRequest();
+ String dispatcherPrefix = req == null ? null : DispatcherHelper.
+ getDispatcherPrefix(req);
+
+ init(scheme,
+ serverName,
+ serverPort,
+ contextPath,
+ servletPath,
+ dispatcherPrefix,
+ pathInfo,
+ params);
+ }
+
+ /**
+ * (private) Constructor.
+ *
+ * @param sreq
+ * @param params
+ */
+ private URL(final HttpServletRequest sreq,
+ final ParameterMap params) {
+ final String dispatcherPrefix = DispatcherHelper.getDispatcherPrefix(
+ sreq);
+ final HttpHost host = new HttpHost(sreq);
+
+ init(sreq.getScheme(),
+ host.getName(),
+ host.getPort(),
+ sreq.getContextPath(),
+ sreq.getServletPath(),
+ dispatcherPrefix,
+ sreq.getPathInfo(),
+ params);
+ }
+
+ /**
+ * + * Constructor, produce a URL representation of the given request.
+ * + * @param sreq anHttpServletRequest from which to copy
+ *
+ * @return a URL whose contents correspond to the request used to create it
+ */
+ public URL(final HttpServletRequest sreq) {
+ this(sreq, new ParameterMap(sreq));
+ }
+
+ /**
+ * + * Produces a short description of a URL suitable for debugging.
+ * + * @return a debugging representation of this URL + */ + public final String toDebugString() { + return super.toString() + " " + "[" + getScheme() + "," + + getServerName() + "," + getServerPort() + "," + + getContextPath() + "," + getServletPath() + "," + + getDispatcherPrefix() + "," + getPathInfo() + "," + + getQueryString() + "]"; + } + + /** + * Returns aString representation of the URL, fully qualified.
+ * The port is omitted if it is the standard HTTP port, 80.
+ *
+ * @return a String URL, with all of its parts
+ */
+ public final String getURL() {
+ if (m_params == null) {
+ return m_url.toString();
+ } else {
+ return m_url.toString() + m_params;
+ }
+ }
+
+ /**
+ *
+ * Returns the scheme (sometimes called the protocol) of the URL. Examples
+ * are "http" and "https".
String representing the URL's scheme
+ */
+ public final String getScheme() {
+ return m_url.substring(0, m_schemeEnd);
+ }
+
+ /**
+ *
+ * Returns the domain name part of the URL. For instance,
+ * "ccm.redhat.com".
String representing the URL's server name
+ */
+ public final String getServerName() {
+ return m_url.substring(m_schemeEnd + 3, m_serverNameEnd);
+ }
+
+ /**
+ *
+ * Returns the port number of the URL. 8080, for example.
int for the URL's port number
+ */
+ public final int getServerPort() {
+ final String port = m_url.substring(m_serverNameEnd, m_serverPortEnd);
+
+ if (port.equals("")) {
+ return 80;
+ } else {
+ return Integer.parseInt(port.substring(1));
+ }
+ }
+
+ /**
+ * + * Returns the server half of the URL, as opposed to the "file" half. For + * example, "http://ccm.redhat.com:8080". Note that there is no trailing + * slash; any characters following the server port are considered part of + * the {@link #getRequestURI() request + * URI}.
+ * + *+ * This method has no equivalent in the Servlet API, but it is similar in + * spirit to {@link + * javax.servlet.http.HttpServletRequest#getRequestURI()}.
+ * + *+ * It is defined to return + * + *
getScheme() + "://" + getServerName() + ":"
+ * + getServerPort()
+ *
+ * or, if the server port is 80,
+ *
+ * getScheme() + "://" +
+ * getServerName()
+ *
+ *
+ *
+ * @see #getRequestURI()
+ * @return a String comprised of the scheme, server name, and
+ * server port plus connecting bits
+ */
+ public final String getServerURI() {
+ return m_url.substring(0, m_serverPortEnd);
+ }
+
+ /**
+ *
+ * Returns the context path of the URL. The value cannot be null, and values
+ * starting with "/" do not end in "/"; empty
+ * string is a valid return value that stands for the default web app.
+ * Example values are "" and "/ccm-app".
String path to a web app context
+ */
+ public final String getContextPath() {
+ return m_url.substring(m_serverPortEnd, m_contextPathEnd);
+ }
+
+ /**
+ * + * Experimental
+ *+ * Returns the dispatcher prefix of this request as set by the + * InternalPrefixerServlet + */ + public final String getDispatcherPrefix() { + if (m_dispatcherPrefixEnd < m_servletPathEnd) { + //there is no dispatcher prefix + return ""; + } else { + return m_url.substring(m_servletPathEnd, m_dispatcherPrefixEnd); + } + } + + /** + *
+ * Returns the servlet path of the URL. The value cannot be null.
+ * + * @see javax.servlet.http.HttpServletRequest#getServletPath() + * @return aString path to a servlet
+ */
+ public final String getServletPath() {
+ return m_url.substring(m_dispatcherPrefixEnd, m_servletPathEnd);
+ }
+
+ /**
+ *
+ * Returns the servlet-local path data of the URL. The value may be null. If
+ * it is not null, the value begins with a "/". Examples are
+ * null, "/", and "/remove.jsp".
String of path data addressed to a servlet
+ */
+ public final String getPathInfo() {
+ final String pathInfo = m_url.substring(m_servletPathEnd);
+
+ if (pathInfo.equals("")) {
+ return null;
+ } else {
+ return pathInfo;
+ }
+ }
+
+ /**
+ *
+ * Returns the "file" part of the URL, in contrast to the
+ * {@link #getServerURI() server part}. The value cannot be null and always
+ * starts with a "/". For example, "/ccm/forum/thread.jsp".
+ * This method is defined to return the equivalent of * getWebContextPath() + getServletPath() +
+ getPathInfo().
String comprised of the context path, servlet
+ * path, and path info
+ */
+ public final String getRequestURI() {
+ return m_url.substring(m_serverPortEnd);
+ }
+
+ /**
+ *
+ * Returns the query string of the URL. If the URL was constructed with a
+ * null ParameterMap, this method returns null. If the URL was
+ * constructed with an empty ParameterMap, this method returns
+ * the empty string. Example values are null, "",
+ * and "ticket-id=56&user-id=24".
String representing the query parameters of the
+ * URL
+ */
+ public final String getQueryString() {
+ if (m_params == null) {
+ return null;
+ } else {
+ return m_params.getQueryString();
+ }
+ }
+
+ /**
+ *
+ * Returns the value of one query parameter. If the URL was constructed with
+ * a null ParameterMap, this method returns null. If the
+ * parameter requested has multiple values, this method will only return the
+ * first; use {@link
+ * #getParameterValues(String)} to get all of the values.
String value of the parameter
+ */
+ public final String getParameter(final String name) {
+ if (m_params == null) {
+ return null;
+ } else {
+ return m_params.getParameter(name);
+ }
+ }
+
+ /**
+ *
+ * Returns the values for a parameter. If the URL was constructed with a
+ * null ParameterMap, this method returns null.
String[] of values for the parameter
+ */
+ public final String[] getParameterValues(final String name) {
+ if (m_params == null) {
+ return null;
+ } else {
+ return m_params.getParameterValues(name);
+ }
+ }
+
+ /**
+ *
+ * Returns an immutable map of the query parameters. The map's keys are
+ * Strings and the map's values are String[]s. If
+ * the URL was constructed with a null ParameterMap, this
+ * method returns null.
Map of the URL's query parameters
+ */
+ public final Map getParameterMap() {
+ if (m_params == null) {
+ return null;
+ } else {
+ return m_params.getParameterMap();
+ }
+ }
+
+ /**
+ *
+ * Creates a URL to the site's root path. For example,
+ * http://somewhere.net/.
URL to your server's root path
+ */
+ public static final URL root() {
+ final WebConfig config = Web.getConfig();
+
+ URL url = new URL(config.getDefaultScheme(),
+ config.getServer().getName(),
+ config.getServer().getPort(),
+ "",
+ "/",
+ null,
+ null);
+
+ return url;
+ }
+
+ /**
+ * + * Creates a URL using the elements of the user's original request but with + * the given set of parameters instead of the original ones.
+ * + * @param sreq the servlet request + * @param params aParameterMap of params to replace those of
+ * the request
+ *
+ * @return a URL representing the original request except for
+ * its parameters
+ */
+ public static final URL request(final HttpServletRequest sreq,
+ final ParameterMap params) {
+ if (params != null) {
+ params.runListeners(sreq);
+ }
+
+ final URL url = Web.getWebContext().getRequestURL();
+
+ if (url == null) {
+ // If the URL is being generated outside of a WebContext,
+ // use the request to fill out the URL.
+
+ return new URL(sreq, params);
+ } else {
+ return new URL(url.getScheme(),
+ url.getServerName(),
+ url.getServerPort(),
+ url.getContextPath(),
+ url.getServletPath(),
+ url.getPathInfo(),
+ params);
+ }
+ }
+
+ /**
+ *
+ * Creates a URL to path under the CCM main dispatcher and with
+ * the given parameters. A null ParameterMap indicates that the
+ * URL has no query string at all. If the parameter map is not null, its
+ * parameter listeners are run and may further edit the parameter map.
String path to which to dispatch
+ * @param params a ParameterMap of parameters to use; this
+ * value may be null
+ *
+ * @return a URL with a path to dispatch to
+ */
+ public static final URL there(final HttpServletRequest sreq,
+ final String path,
+ final ParameterMap params) {
+ final WebConfig config = Web.getConfig();
+
+ Assert.exists(sreq, "HttpServletRequest sreq");
+ Assert.exists(config, "WebConfig config");
+
+ if (params != null) {
+ params.runListeners(sreq);
+ }
+
+ final HttpHost host = new HttpHost(sreq);
+
+ return new URL(sreq.getScheme(),
+ host.getName(),
+ host.getPort(),
+ config.getDispatcherContextPath(),
+ config.getDispatcherServletPath(),
+ path,
+ params);
+ }
+
+ /**
+ * Method similar to there(), but which checks the
+ * waf.web.dynamic_host_provider parameter to generate the site name and
+ * port dynamically.
+ *
+ * @see com.arsdigita.web.DispatcherServlet
+ * @param sreq the servlet request
+ * @param path a String path to which to dispatch
+ * @param params a ParameterMap of parameters to use; this
+ * value may be null
+ *
+ * @return a URL with a path to dispatch to
+ */
+ public static final URL dynamicHostThere(final HttpServletRequest sreq,
+ final String path,
+ final ParameterMap params) {
+ final WebConfig config = Web.getConfig();
+ DynamicHostProvider provider = Web.getConfig().getDynamicHostProvider();
+
+ if (provider == null) {
+ return there(sreq, path, params);
+ }
+
+ Assert.exists(sreq, "HttpServletRequest sreq");
+ Assert.exists(config, "WebConfig config");
+
+ if (params != null) {
+ params.runListeners(sreq);
+ }
+
+ final HttpHost host = new HttpHost(sreq);
+
+ return new URL(sreq.getScheme(),
+ provider.getName(),
+ provider.getPort(),
+ config.getDispatcherContextPath(),
+ config.getDispatcherServletPath(),
+ path,
+ params);
+ }
+
+ /**
+ *
+ * Creates a URL with no local parameters to path under the CCM
+ * main dispatcher. This method implicitly creates an empty parameter map
+ * (not a null one); this empty map may be altered by parameter listeners,
+ * for instance to include global parameters.
String path to dispatch to
+ *
+ * @return a URL to a path under the dispatcher and with an
+ * empty parameter map
+ */
+ public static final URL there(final HttpServletRequest sreq,
+ final String path) {
+ final WebConfig config = Web.getConfig();
+
+ Assert.exists(sreq, "HttpServletRequest sreq");
+ Assert.exists(config, "WebConfig config");
+
+ final HttpHost host = new HttpHost(sreq);
+
+ return new URL(sreq.getScheme(),
+ host.getName(),
+ host.getPort(),
+ config.getDispatcherContextPath(),
+ config.getDispatcherServletPath(),
+ path,
+ (ParameterMap) s_empty.get());
+ }
+
+ /**
+ *
+ * Creates a URL to pathInfo under the specified application
+ * and using the given parameters. The parmeter map argument may be null,
+ * indicating that the URL has no query string.
Application to dispatch to
+ * @param pathInfo a String of extra path info for the
+ * application
+ * @param params a ParameterMap of parameters to use
+ *
+ * @return a URL to an application with a particular
+ * pathInfo
+ */
+ public static final URL there(final HttpServletRequest sreq,
+ final Application app,
+ final String pathInfo,
+ final ParameterMap params) {
+ if (Assert.isEnabled() && pathInfo != null) {
+ Assert.isTrue(pathInfo.startsWith("/"),
+ "pathInfo, if not null, must " + "start with a slash");
+ }
+
+ if (pathInfo == null) {
+ return URL.there(sreq, app.getPrimaryUrl().toString(), params);
+ } else {
+ return URL.there(sreq, app.getPrimaryUrl().toString() + pathInfo,
+ params);
+ }
+ }
+
+ /**
+ *
+ * Creates a URL with no local parameters to pathInfo under the
+ * specified application.
+ *
+ * @param sreq the servlet request
+ * @param app the Application to dispatch to
+ * @param pathInfo a String of extra path info for the
+ * application
+ *
+ * @return a URL to an application with a particular
+ * pathInfo
+ */
+ public static final URL there(final HttpServletRequest sreq,
+ final Application app,
+ final String pathInfo) {
+ if (Assert.isEnabled() && pathInfo != null) {
+ Assert.isTrue(pathInfo.startsWith("/"),
+ "pathInfo, if not null, must " + "start with a slash");
+ }
+
+ if (pathInfo == null) {
+ return URL.there(sreq, app.getPrimaryUrl().toString());
+ } else {
+ return URL.there(sreq, app.getPrimaryUrl().toString() + pathInfo);
+ }
+ }
+
+ /**
+ *
+ * Creates a URL with local parameters.
+ * + *
+ * This function should not be used unless you really don't have an
+ * HttpServletRequest object as it will ignore any Host header
+ * given by the client.
+ * Create a URL with local parameters to pathInfo under the
+ * specified application.
+ * This function should not be used unless you really don't have an
+ * HttpServletRequest object as it will ignore any Host header
+ * given by the client.
String representation of the URL suitable for use
+ * as a hyperlink. The scheme, server name, and port are omitted.
+ *
+ * @return a String URL
+ */
+ @Override
+ public final String toString() {
+ if (m_params == null) {
+ return m_url.substring(m_serverPortEnd);
+ } else {
+ String str = m_url.substring(m_serverPortEnd);
+ if (str.contains("?")) {
+ return String.format("%s%s", m_url.substring(m_serverPortEnd),
+ m_params.toString().replace('?', '&'));
+ } else {
+ return m_url.substring(m_serverPortEnd) + m_params;
+ }
+ }
+ }
+
+ /**
+ *
+ * @return
+ */
+ public static String getDispatcherPath() {
+ final WebConfig config = Web.getConfig();
+ final HttpServletRequest req = Web.getRequest();
+
+ final String context = config.getDispatcherContextPath();
+ final String servlet = config.getDispatcherServletPath();
+
+ if (req == null) {
+ return context + servlet;
+ } else {
+ final String prefix = DispatcherHelper.getDispatcherPrefix(req);
+
+ if (prefix == null) {
+ return context + servlet;
+ } else {
+ return context + prefix + servlet;
+ }
+ }
+ }
+
+ private static class EmptyParameterMap extends InternalRequestLocal {
+
+ @Override
+ protected final Object initialValue() {
+ return new ParameterMap();
+ }
+
+ @Override
+ protected final void prepareValue(final HttpServletRequest sreq) {
+ ((ParameterMap) get()).runListeners(sreq);
+ }
+
+ @Override
+ protected final void clearValue() {
+ ((ParameterMap) get()).clear();
+ }
+
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/web/Web.java b/ccm-core/src/main/java/com/arsdigita/web/Web.java
new file mode 100644
index 000000000..d9b28505e
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/web/Web.java
@@ -0,0 +1,777 @@
+/*
+ * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ */
+package com.arsdigita.web;
+
+import com.arsdigita.util.Assert;
+import com.arsdigita.util.StringUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.log4j.Logger;
+import org.libreccm.core.CcmSessionContext;
+
+/**
+ * An entry point for functions of the web package.
+ *
+ * @author Rafael Schloming <rhs@mit.edu>
+ * @author Justin Ross <jross@redhat.com>
+ * @version $Id$
+ */
+public class Web {
+
+ /**
+ * Internal logger instance to faciliate debugging. Enable logging output by
+ * editing /WEB-INF/conf/log4j.properties int the runtime environment and
+ * set com.arsdigita.web.Web=DEBUG by uncommenting or adding the line.
+ */
+ private static final Logger s_log = Logger.getLogger(Web.class);
+
+ private static final WebConfig s_config = WebConfig.getInstanceOf();
+
+ private static final ThreadLocal s_request = new InternalRequestLocal();
+ private static final ThreadLocal s_servletContext
+ = new InternalRequestLocal();
+ private static final ThreadLocal s_userContext = new InternalRequestLocal();
+ private static ThreadLocal s_context;
+
+ static final WebContext s_initialContext = new WebContext();
+
+ /**
+ * Internal service property to temporarly save the ServletContext as
+ * determined by findResource(resource) method to make it available to those
+ * methods of this class which use findResource to lookup a resource as a
+ * base for determining additional information, e.g. provide a dispatcher
+ * (findResourceDispatcher)
+ */
+ static private ServletContext s_urlContext;
+
+ /**
+ * String containing the webapp context path portion of the WEB application
+ * where this CCM instance is executed. (I.e. where the WEB-INF directory is
+ * located in the servlet container webapps directory).
+ */
+ private static String s_contextPath;
+
+ /**
+ * Static Initializer block.
+ */
+ static void init(final HttpServletRequest sreq,
+ final ServletContext sc,
+ final CcmSessionContext uc) {
+
+ Assert.exists(sreq, HttpServletRequest.class);
+ Assert.exists(sc, ServletContext.class);
+ Assert.exists(uc, CcmSessionContext.class);
+
+ s_request.set(sreq);
+ s_servletContext.set(sc);
+ s_contextPath = CCMDispatcherServlet.getContextPath();
+ s_userContext.set(uc);
+ }
+
+ /**
+ * Provide the configuration record for code in the web package.
+ *
+ * @return A WebConfig configuration record; it cannot be null
+ */
+ public static WebConfig getConfig() {
+ return s_config;
+ }
+
+ /**
+ * Gets the web context object from the current thread.
+ *
+ * @return A WebContext object; it cannot be null Note: Rename
+ * from getContext()
+ */
+ public static WebContext getWebContext() {
+ if (s_context == null) {
+ s_context = new WebContextLocal();
+ }
+ return (WebContext) s_context.get();
+ }
+
+ /**
+ * Gets the servlet request object of the current thread.
+ *
+ * @return The current HttpServletRequest; it can be null
+ */
+ public static HttpServletRequest getRequest() {
+ return (HttpServletRequest) s_request.get();
+ }
+
+ /**
+ * Gets the servlet context of the current thread.
+ *
+ * @return The current ServletContext; it can be null
+ */
+ public static ServletContext getServletContext() {
+ return (ServletContext) s_servletContext.get();
+ }
+
+ /**
+ * Gets the user context object of the current thread.
+ *
+ * @return The current UserContext object; it can be null
+ */
+ public static CcmSessionContext getUserContext() {
+ return (CcmSessionContext) s_userContext.get();
+ }
+
+ /**
+ * Gets the webapp context path portion of the WEB application where this
+ * CCM instance is executed. (I.e. where the WEB-INF directory is located
+ * in the servlet container webapps directory, known as ServletContext in
+ * the Servlet API)
+ *
+ * @return web context path portion as a String, may be used to construct
+ * a URL (NOT the RealPath!). The ROOT context returns an empty
+ * String("").
+ */
+ public static String getWebappContextPath() {
+ return (String) s_contextPath;
+ }
+
+ /**
+ * Sets the webapp context path portion of the WEB application where this
+ * CCM instance is executed. (I.e. where the WEB-INF directory is located
+ * in the servlet container webapps directory, known as ServletContext in
+ * the Servlet API)
+ * Meant to be executed by CCMDispatcherServlet only.
+ *
+ * @param contextPath
+ */
+ protected static void setWebappContextPath(String contextPath) {
+ s_contextPath = contextPath;
+ }
+
+
+ /**
+ * Processes an URL String trying to identify a corresponding recource which
+ * is mapped to the given path String. The method ensures that the resource
+ * definitely exists (using the URL returned) or definitely not (returning
+ * null).
+ *
+ * The resourcePath may be stored at various sources (file system, jar file,
+ * database, etc) depending on the implementation of the URL handlers and
+ * URLConnection objects.
+ *
+ *
+ * @param resourcePath Path to the resource as String. It may include the
+ * web context in its first part or may be relative to
+ * the current webapp document root (i.e. its context).
+ * Additionally, the web application component (if any)
+ * may be a comma separate list of webapps to search for
+ * the rest of the path String. So, if the
+ * 'resourcePath' is: + * /myproj,ccm-cms/themes/heirloom/admin/index.xsl + *then this method will look for resourcePaths at + *
+ * /myproj/themes/heirloom/admin/index.xsl + * /ccm-cms/themes/heirloom/admin/index.xsl + *+ * + * @return the URL for the resourcePath, or null if no resource is mapped to + * the resourcePath String + */ + public static URL findResource(String resourcePath) { + + if (resourcePath == null) { + if (s_log.isDebugEnabled()) { + s_log.debug("Parameter resource is null. Giving up."); + } + return null; + } + // ensure a leading "/" + if (!resourcePath.startsWith("/")) { + resourcePath = "/" + resourcePath; + } + if (resourcePath.length() < 2) { + if (s_log.isDebugEnabled()) { + s_log + .debug("Resource spec is too short: >" + resourcePath + "<"); + } + return null; + } + + // determine my own webapp context + ServletContext myctx = getServletContext(); + + // Check for old style resourcePath format including a comma seoarated list + // of webapps + if (resourcePath.indexOf(",") <= 0) { + // no comma separated list found, process as normal + + // just try to find the resourcePath in my own context + try { + URL url = myctx.getResource(resourcePath); + if (url != null) { + if (s_log.isDebugEnabled()) { + s_log.debug("Got URL " + url + " for " + resourcePath); + } + return url; // Return adjusted resourcePath url + } + } catch (MalformedURLException ex) { + if (s_log.isDebugEnabled()) { + s_log.debug("Cannot get resource for " + resourcePath); + } + // Try the first part of resourcePath as a webapp context path and + // check far a resourcePath there + int offset = resourcePath.indexOf("/", 1); // search for second "/" + String testPath = resourcePath.substring(1, offset); + String path = resourcePath.substring(offset); + + if (s_log.isDebugEnabled()) { + s_log.debug("Try to find a context at " + testPath); + } + // Try to achieve a context + ServletContext ctx = myctx.getContext(testPath); + if (s_log.isDebugEnabled()) { + s_log + .debug("Servlet context for " + testPath + " is " + ctx); + } + if (ctx != null) { + // successs, try to finf a resourcePath for the remaining + // string as path + try { + URL url = ctx.getResource(path); + if (url != null) { + if (s_log.isDebugEnabled()) { + s_log.debug("Got URL " + url + " for " + path); + } + return url; // Return adjusted resourcePath url + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("No URL present for " + path); + } + } + } catch (MalformedURLException exc) { + if (s_log.isDebugEnabled()) { + s_log.debug("cannot get resource for " + path); + } + } + } + } + + return null; // fall through + + } else { + // comma separated list found + // processing old style, comma separated webapp list + int offset = resourcePath.indexOf("/", 1); // search for second "/" + String webappList = resourcePath.substring(1, offset); + String path = resourcePath.substring(offset); + + String[] webapps = StringUtils.split(webappList, ','); + if (s_log.isDebugEnabled()) { + s_log.debug("Web app list " + webappList + " path " + path); + } + + for (int i = (webapps.length - 1); i >= 0; i--) { + + String ctxPath = webapps[i]; + if (!ctxPath.startsWith("/")) { + ctxPath = "/" + ctxPath; + } + // No trailing slash allowed by servlet API! + // if (!ctxPath.endsWith("/")) { + // ctxPath = ctxPath + "/"; + // } + + ServletContext ctx = myctx.getContext(ctxPath); + if (s_log.isDebugEnabled()) { + s_log.debug("Servlet context for " + ctxPath + " is " + ctx); + } + if (ctx != null) { + try { + URL url = ctx.getResource(path); + if (url != null) { + if (s_log.isDebugEnabled()) { + s_log.debug("Got URL " + url + " for " + path); + } + return url; // Return adjusted resourcePath url + } else { + if (s_log.isDebugEnabled()) { + s_log.debug("No URL present for " + path); + } + } + } catch (MalformedURLException ex) { + if (s_log.isDebugEnabled()) { + s_log.debug("cannot get resource for " + path); + } + } + } + + } + + return null; // fall through when nothing found + } // end processing old style comma separated list + } + + /** + * Follows the same rules as findResource(String[], String), but instead + * returns an input stream for reading the resource + * + * @param resource Path to the resource as String. It may include the web + * context in its first part or may be relative to the + * current webapp document root (i.e. its context). + * Additionally, it the web application component (if any) + * may be a comma separate list of webapps to search for the + * rest of the path String. So, if the 'resource' is:
+ * /myproj,ccm-cms/themes/heirloom/admin/index.xsl + *then this method will look for resources at + *
+ * /myproj/themes/heirloom/admin/index.xsl + * /ccm-cms/themes/heirloom/admin/index.xsl + *+ * + * @return the input stream for the resource, or null + * + * @throws java.io.IOException + */ + public static InputStream findResourceAsStream(String resource) + throws IOException { + + URL url = findResource(resource); + return url == null ? null : url.openStream(); + } + + /** + * Follows the same rules as findResource(String), but instead returns a + * request dispatcher for serving the resource. It is mainly used to find an + * application's jsp template(s) stored in the file system (or war file in + * case of unexploded distribution) and provide a handle to execute it. + * These jsp templates used to be stored a directory named "templates" and + * there within a directory carrying the modules name. As example: + * "/templates/ccm-navigation/index.jsp". Inside the modules subdirectory + * there might by a module specific subdirectory structure. It's up to the + * module. + * + * @param resourcePath Path to the resource as String. It may include the + * web context in its first part or may be relative to + * the current webapp document root (i.e. its context). + * LEGACY FORMAT: Additionally, the web application + * component (if any) may be a comma separate list of + * webapps to search for the rest of the path String. + * So, if the 'resource' is:
+ * /myproj,ccm-cms/themes/heirloom/admin/index.xsl + *then this method will look for resources at + *
+ * /myproj/themes/heirloom/admin/index.xsl + * /ccm-cms/themes/heirloom/admin/index.xsl + *LEGACY FORMAT SUPPORT NOT IMPLEMENTED YET! LEGACY FORMAT MAY BE + * COMPLETELY REMOVED IN FUTURE RELEASE + * + * @return the request dispatcher for the resource, or null + */ + public static RequestDispatcher findResourceDispatcher(String resourcePath) { + + if (resourcePath == null) { + return null; + } + ServletContext ctx = getServletContext(); + URL url = null; + + // Check for old style resource format including a comma seoarated list + // of webapps + if (resourcePath.indexOf(",") <= 0) { + // no comma separated list found, process as normal + + try { + url = ctx.getResource(resourcePath); + } catch (MalformedURLException ex) { + if (s_log.isDebugEnabled()) { + s_log.debug("Resource for " + resourcePath + " not found."); + } + // throw new UncheckedWrapperException( + // "No resource at " + resourcePath, ex); + return null; + } + if (url == null) { + return null; + } else { + RequestDispatcher rd = (ctx == null) ? null : ctx + .getRequestDispatcher(resourcePath); + return rd; + } + + } else { + + // old style format not implemented yet here + return null; + + } + + } + + /** + * + */ + private static class WebContextLocal extends InternalRequestLocal { + + @Override + protected Object initialValue() { + return Web.s_initialContext.copy(); + } + + @Override + protected void clearValue() { + ((WebContext) get()).clear(); + } + + } + + // /////////////////////////////////////////////////////////////////////// + // + // DEPRECATED METHODS + // ================== + // This method assume the main ccm application installed in the hosts root + // context and somme other ccm applications installed in its own context. + // It assumes futher that each ccm applications registers itself in a + // home made application directory and it is viable to query this + // directory to find the context for a given ccm application. + // + // /////////////////////////////////////////////////////////////////////// + /** + * Constant to denote the context of the ROOT application (main CCM app). + * Used by some classes to determine the application context (in terms of + * servlet specification, i.e. document root of the web application where + * all the code, specifically WEB-INF, is copied into when unpacking the WAR + * file. + * + * This results in a fixed location (context) for CCM which is no longer + * valid. Replace by invoking method getWebappContextPath + * + * @deprecated without direct replacement. See above + */ +// private static final String ROOT_WEBAPP = "ROOT"; + /** + * Map containing a list of registered ccm webapps and corresponding webapp + * context (ServletContext in JavaEE terms). + * + * @deprecated without direct replacement, see above. + */ +// private static final Map s_contexts = new HashMap(); + /** + * @deprecated renamed to getWebContext + */ +// getContext() + /** + * Gets the servlet context matching the provided URI. The URI is relative + * to the root of the server and must start and end with a '/'. It is + * provided by ContextRegistrationServlet as manually configured in web.xml + * + * This should be used in preference to ServletContext#getWebContext(String) + * since on all versions of Tomcat, this fails if the path of the context + * requested is below the current context. + * + * @param uri the context URI + * + * @return the servlet context matching uri, or null + * + * @deprecated currently without direct replacement The hash map s_contexts + * contains a kind of repository where (i.e. in which web application + * context) a resource may be found. Part of the code access the file system + * directly which is normally forbidden and violates the principle of + * isolation Previously it has been used to allow the installation of some + * modules in its own web application context (e.g. Themedirector) where + * each module used to register here via ContextRegistrationServlet. This + * mechanism has to be replaced by a inter-web-application communication, if + * modules should be enabled to execute in it's web application context. + * + */ +// public static ServletContext getServletContext(String uri) { +// Assert.isTrue(uri.startsWith("/"), "uri must start with /"); +// Assert.isTrue(uri.endsWith("/"), "uri must end with /"); +// return (ServletContext)s_contexts.get(uri); +// } + /** + * Registers a servlet context against a URI. Only intended to be used by + * ContextRegistrationServlet + * + * @deprecated without direct replacement. See getServletContext + */ +// static final void registerServletContext(String uri, +// ServletContext ctx) { +// s_log.debug("Mapping " + ctx + " to " + uri); +// Assert.isTrue(s_contexts.get(uri) == null, +// "a context mapping exists at " + uri); +// // Save the web context as manually configured in web.xml +// // along with the context as provided by ServletContext. +// s_contexts.put(uri, ctx); +// } + /** + * Unregisters the servlet context against a URI. Only intended to be used + * by ContextRegistrationServlet + * + * @deprecated without direct replacement. See getServletContext + */ +// static final void unregisterServletContext(String uri) { +// s_log.debug("Unmapping " + uri); +// s_contexts.remove(uri); +// } + /** + * Finds a concrete URL corresponding to an abstract webapp resource. The + * first argument is a list of webapp paths to search through for the path. + * So if the webapps param is { 'myproj', 'ccm-cms', 'ROOT' } and the path + * parma is '/themes/heirloom/apps/content-section/index.xsl' then the paths + * that are searched are: + *
+ * /myproj/themes/heirloom/apps/content-section/index.sl + * /ccm-cms/themes/heirloom/apps/content-section/index.sl + * /ROOT/themes/heirloom/apps/content-section/index.sl + *+ * + * @param webapps the list of webapps + * @param path the resource path + * + * @return the URL for the resource, or null + * + * @deprecated without direct replacement at the moment. + */ +// public static URL findResource(String[] webapps, +// String path) { +// +// ServletContext ctx = findResourceContext(webapps, +// path); +// +// URL url = null; +// try { +// url = (ctx == null ? null : +// ctx.getResource(path)); +// } catch (IOException ex) { +// throw new UncheckedWrapperException("cannot get URL for " + path, ex); +// } +// if (s_log.isDebugEnabled()) { +// s_log.debug("URL for " + path + " is " + url); +// } +// return url; +// } + /** + * Follows the same rules as findResource(String), but instead returns an + * input stream for reading the resource + * + * @param resource the resource name + * + * @return the input stream for the resource, or null + * + * @deprecated without direct replacement at the moment. + */ +// public static InputStream findResourceAsStream(String resource) +// throws IOException { +// ResourceSpec spec = parseResource(resource); +// +// return findResourceAsStream(spec.getWebapps(), +// spec.getPath()); +// } + /** + * Follows the same rules as findResource(String[], String), but instead + * returns an input stream for reading the resource + * + * @param webapps the list of webapps + * @param path the resource path + * + * @return the input stream for the resource, or null + * + * @deprecated without direct replacement at the moment. + */ +// public static InputStream findResourceAsStream(String[] webapps, +// String path) +// throws IOException { +// +// URL url = findResource(webapps, path); +// +// return url == null ? null : +// url.openStream(); +// } + /** + * Follows the same rules as findResource(String), but instead returns a + * request dispatcher for serving the resource + * + * @param resource the resource name + * + * @return the request dispatcher for the resource, or null + * + * @deprecated without direct replacement at the moment. + */ +// public static RequestDispatcher findResourceDispatcher(String resource) { +// ResourceSpec spec = parseResource(resource); +// +// return findResourceDispatcher(spec.getWebapps(), +// spec.getPath()); +// } +// /** +// * Follows the same rules as findResource(String[], String), but +// * instead returns a request dispatcher for serving +// * the resource +// * +// * @param webapps the list of webapps +// * @param path the resource path +// * @return the request dispatcher for the resource, or null +// * @deprecated without direct replacement at the moment. +// */ +// public static RequestDispatcher findResourceDispatcher(String[] webapps, +// String path) { +// ServletContext ctx = findResourceContext(webapps, +// path); +// +// return ctx == null ? null : ctx.getRequestDispatcher(path); +// } + /** + * + * @param webapps + * @param path path to the resource, starting with "/" and relative to + * the current context root, or relative to the + * /META-INF/resources directory of a JAR file inside the web + * application's /WEB-INF/lib directory + * + * @return + * + * @deprecated without direct replacement at the moment. + */ +// private static ServletContext findResourceContext(String[] webapps, +// String path) { +// for (int i = (webapps.length - 1) ; i >= 0 ; i--) { +// // trash here, depends of a kind of "home made" list of +// // webapps/webcontexts (or ServletContexts) which are part of CCM +// // but installed in its own context (it is the structure of APLAWS +// // until 1.0.4. +// String ctxPath = ROOT_WEBAPP.equals(webapps[i]) ? +// "" : webapps[i]; +// +// if (!ctxPath.startsWith("/")) { +// ctxPath = "/" + ctxPath; +// } +// if (!ctxPath.endsWith("/")) { +// ctxPath = ctxPath + "/"; +// } +// +// ServletContext ctx = getServletContext(ctxPath); +// if (s_log.isDebugEnabled()) { +// s_log.debug("Servlet context for " + ctxPath + " is " + ctx); +// } +// +// if (ctx != null) { +// try { +// URL url = ctx.getResource(path); +// if (url != null) { +// if (s_log.isDebugEnabled()) { +// s_log.debug("Got URL " + url + " for " + path); +// } +// return ctx; +// } else { +// if (s_log.isDebugEnabled()) { +// s_log.debug("No URL present for " + path); +// } +// } +// } catch (IOException ex) { +// throw new UncheckedWrapperException( +// "cannot get resource " + path, ex); +// } +// } +// } +// return null; +// } + // ///////////////////////////////////////////////////////////////////////// + // Private classes and methods + // ///////////////////////////////////////////////////////////////////////// + /** + * Splits the resource string into a StringArray of webapps (ServletContexts + * in terms of JavaEE) and a path to a resource inside a the that webapp. + * The part between the first and the second slash is always treated as + * webapp part! This part may consist of a comma separated list in which + * case the result is an array of webapps > 1. + * + * As of version 6.6x CCM is installed into one webapp context by default + * and the assumption of the first part being a web app is nolonger + * reloiable. Therefore this routine provides invalid results in some + * circumstances! In best cases it provides redundancy specifying just the + * local webapp. + * + * Code may be refactored to ensure the first part is really a webapp by + * inquiring the servlet container using javax.management + * + * @param resource + * + * @return + * + * @deprecated without direct replacement. + */ +// private static ResourceSpec parseResource(String resource) { +// if (resource == null || resource.length() < 2) { +// throw new IllegalArgumentException( +// "Resource spec is too short: " + resource); +// } +// +// int offset = resource.indexOf("/", 1); +// if (offset == -1) { +// throw new IllegalArgumentException( +// "Cannot find second '/' in resource spec : " + resource); +// } +// +// String webappList = resource.substring(1, offset); +// String path = resource.substring(offset); +// +// String[] webapps = StringUtils.split(webappList, ','); +// +// if (s_log.isInfoEnabled()) { +// s_log.info("Web app list " + webappList + " path " + path); +// } +// +// return new ResourceSpec(webapps, path); +// } +// +// +// /** +// * Container to hold a pointer to a resource. The pointer specifically +// * consists of an array of webapps probably containing the requested +// * resource and a path to that resource that has to be equal for each +// * webapp. +// * @deprecated without direct replacement at the moment. +// */ +// private static class ResourceSpec { +// private final String[] m_webapps; +// private final String m_path; +// +// /** +// * Constructor. +// * @param webapps +// * @param path +// */ +// public ResourceSpec(String[] webapps, +// String path) { +// m_webapps = webapps; +// m_path = path; +// } +// +// public String[] getWebapps() { +// return m_webapps; +// } +// +// public String getPath() { +// return m_path; +// } +// } +} diff --git a/ccm-core/src/main/java/com/arsdigita/web/WebConfig.java b/ccm-core/src/main/java/com/arsdigita/web/WebConfig.java new file mode 100644 index 000000000..a845b3a5c --- /dev/null +++ b/ccm-core/src/main/java/com/arsdigita/web/WebConfig.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2015 LibreCCM Foundation. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301 USA + */ +package com.arsdigita.web; + +import com.arsdigita.runtime.AbstractConfig; +import com.arsdigita.util.parameter.BooleanParameter; +import com.arsdigita.util.parameter.EnumerationParameter; +import com.arsdigita.util.parameter.ErrorList; +import com.arsdigita.util.parameter.Parameter; +import com.arsdigita.util.parameter.ParameterError; +import com.arsdigita.util.parameter.SingletonParameter; +import com.arsdigita.util.parameter.StringArrayParameter; +import com.arsdigita.util.parameter.StringParameter; +import com.arsdigita.util.servlet.HttpHost; +import com.arsdigita.util.servlet.HttpHostParameter; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * A record containing server-session scoped configuration properties. + * + * Accessors of this class may return null. Developers should take care to trap + * null return values in their code. + * + * @see com.arsdigita.web.Web + * @author Justin Ross <jross@redhat.com> + * @author Jens Pelzetter + */ +public class WebConfig extends AbstractConfig { + + private static final Logger LOGGER = LogManager.getLogger(WebConfig.class); + + private static WebConfig config; + + /** + * Returns the singleton configuration record for the content section + * environment. + * + * @return The
CMSConfig record; it cannot be null
+ */
+ public static synchronized WebConfig getInstanceOf() {
+ if (config == null) {
+ config = new WebConfig();
+ config.load();
+ }
+ return config;
+ }
+
+ // /////////////////////////////////////////////////////////////////////////
+ // Configuration parameter section
+ // /////////////////////////////////////////////////////////////////////////
+ /**
+ * Determines what HTTP scheme prefix is used by default to generate URLs
+ * (either http od https)
+ */
+ private final Parameter m_scheme = new DefaultSchemeParameter(
+ "waf.web.default_scheme",
+ Parameter.REQUIRED, "http");
+ /**
+ * Sets the name and port that users of a site will see in URLs generated by
+ * CCM for the site. This is a required parameter during installation, e.g.
+ * example.com:80
+ */
+ private final Parameter m_server = new HttpHostParameter("waf.web.server");
+ /**
+ * Name and port that users of a site will see in secure URLs generated by
+ * CCM for the site. As an example: example.com:443
+ */
+ private final Parameter m_secureServer = new HttpHostParameter(
+ "waf.web.secure_server",
+ Parameter.OPTIONAL, null);
+ /**
+ * The name of your website, for use in page footers for example. It's not
+ * necessarily the URL but rather a title, e.g. "House of HTML". If not
+ * specified set to the server's URL.
+ */
+ private final Parameter m_site = new StringParameter("waf.web.site_name",
+ Parameter.OPTIONAL,
+ null) {
+
+ @Override
+ public final Object getDefaultValue() {
+ final HttpHost host = getServer();
+ if (host == null) {
+ return null;
+ } else {
+ return host.toString();
+ }
+ }
+
+ };
+ /**
+ * Sets the name and port of the machine on which the CCM instance is
+ * running. Used to fetch some resources by a local URL avoiding external
+ * internet traffic (and delay). If not specified set to the servers's name
+ * redirecting all traffic to external internet address.
+ */
+ private final Parameter m_host = new HttpHostParameter("waf.web.host",
+ Parameter.OPTIONAL,
+ null) {
+
+ @Override
+ public final Object getDefaultValue() {
+ return getServer();
+ }
+
+ };
+
+ /**
+ * List of URLs which accessed by insecure (normal HTTP) connection produce
+ * a redirect to a HTTPS equivalent. List is comma separated.
+ */
+ private final Parameter m_secureRequired = new StringArrayParameter(
+ "waf.web.secure_required", Parameter.OPTIONAL, null);
+ /**
+ * List of URLs which accessed by secure (HTTPS) connection produce a
+ * redirect to a HTTP equivalent. List is comma separated.
+ */
+ private final Parameter m_secureSwitchBack = new StringArrayParameter(
+ "waf.web.secure_switchback", Parameter.OPTIONAL, null);
+
+ /**
+ * Dispatcher servlet path. It's the prefix to the main entry point for any
+ * application request (CCMDispatcherServlet). By default /ccm
+ */
+ private final Parameter m_servlet = new StringParameter(
+ "waf.web.dispatcher_servlet_path", Parameter.REQUIRED, "/ccm");
+
+ /**
+ * Specifies by name which implementation of ApplicationFileResolver is used
+ * to dynamically resolve static files. By default
+ * DefaultApplicationFileResolver() is used.
+ */
+ private final Parameter m_resolver = new SingletonParameter(
+ "waf.web.application_file_resolver",
+ Parameter.OPTIONAL,
+ new DefaultApplicationFileResolver());
+
+ private final Parameter m_deactivate_cache_host_notifications
+ = new BooleanParameter(
+ "waf.web.deactivate_cache_host_notifications",
+ Parameter.OPTIONAL, Boolean.FALSE);
+
+ private final Parameter m_dynamic_host_provider = new StringParameter(
+ "waf.web.dynamic_host_provider",
+ Parameter.OPTIONAL, "");
+
+ /**
+ * Constructor, but do NOT instantiate this class directly, use
+ * getInstanceOf() instead. (Singleton pattern!)
+ *
+ */
+ public WebConfig() {
+
+ register(m_scheme);
+ register(m_server);
+ register(m_secureServer);
+ register(m_site);
+ register(m_host);
+ register(m_secureRequired);
+ register(m_secureSwitchBack);
+ register(m_servlet);
+ register(m_resolver);
+ register(m_deactivate_cache_host_notifications);
+ register(m_dynamic_host_provider);
+
+ loadInfo();
+ }
+
+ public final String getDefaultScheme() {
+ return (String) get(m_scheme);
+ }
+
+ public final HttpHost getServer() {
+ return (HttpHost) get(m_server);
+ }
+
+ public final HttpHost getSecureServer() {
+ return (HttpHost) get(m_secureServer);
+ }
+
+ public final boolean isSecureRequired(String uri) {
+ String[] secured = (String[]) get(m_secureRequired);
+ if (secured != null) {
+ for (int i = 0, n = secured.length; i < n; i++) {
+ if (uri.startsWith(secured[i])) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public final boolean isNonSecureSwitchRequired(String uri) {
+ String[] switchBack = (String[]) get(m_secureSwitchBack);
+ if (switchBack != null) {
+ for (int i = 0, n = switchBack.length; i < n; i++) {
+ if (uri.startsWith(switchBack[i])) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public final String getDispatcherServletPath() {
+ return (String) get(m_servlet);
+ }
+
+ public final ApplicationFileResolver getApplicationFileResolver() {
+ return (ApplicationFileResolver) get(m_resolver);
+ }
+
+ public final HttpHost getHost() {
+ return (HttpHost) get(m_host);
+ }
+
+ final void setHost(final HttpHost host) {
+ set(m_host, host);
+ }
+
+ public final String getSiteName() {
+ return (String) get(m_site);
+ }
+
+ /**
+ *
+ * @return
+ * @deprecated use Web.getContextPath() instead. The installation context
+ * must no longer manually configured
+ */
+ // NO LONGER configured by configuration option but determined at runtime
+ // by CCMDispatcherServlet itself.
+ // // dispatcherContextPath option in old Initializer, set to ""
+ // m_context = new StringParameter
+ // ("waf.web.dispatcher_context_path", Parameter.REQUIRED, "");
+ public final String getDispatcherContextPath() {
+ // return (String) get(m_context);
+ return CCMDispatcherServlet.getContextPath();
+ }
+
+
+ private static class DispatcherServletPathParameter
+ extends StringParameter {
+
+ DispatcherServletPathParameter(final String name) {
+ super(name);
+ }
+
+ @Override
+ protected void doValidate(final Object value, final ErrorList errors) {
+ final String string = (String) value;
+
+ if (string.endsWith("/")) {
+ final ParameterError error = new ParameterError(this,
+ "The value must not end in a '/'");
+ errors.add(error);
+ }
+ }
+
+ }
+
+ private static class DefaultSchemeParameter extends EnumerationParameter {
+
+ DefaultSchemeParameter(final String name,
+ final int multiplicity,
+ final Object defaalt) {
+ super(name, multiplicity, defaalt);
+
+ put("http", "http");
+ put("https", "https");
+ }
+
+ }
+
+ protected DynamicHostProvider dhProvider = null;
+ protected boolean dhProviderInited = false;
+
+ public final DynamicHostProvider getDynamicHostProvider() {
+ if (dhProviderInited == false) {
+ String classname = (String) get(m_dynamic_host_provider);
+ if (classname != null) {
+ try {
+ Class klass = Class.forName(classname);
+ dhProvider = (DynamicHostProvider) klass.newInstance();
+ } catch (Exception e) {
+ LOGGER.error(
+ "Could not instantiate DynamicHostProvider using classname : "
+ + classname, e);
+ }
+ }
+ dhProviderInited = true;
+ }
+ return dhProvider;
+ }
+
+ public final boolean getDeactivateCacheHostNotifications() {
+ return ((Boolean) get(m_deactivate_cache_host_notifications))
+ .booleanValue();
+ }
+
+}
diff --git a/ccm-core/src/main/java/com/arsdigita/web/WebContext.java b/ccm-core/src/main/java/com/arsdigita/web/WebContext.java
new file mode 100644
index 000000000..c563df3e4
--- /dev/null
+++ b/ccm-core/src/main/java/com/arsdigita/web/WebContext.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2002-2004 Red Hat Inc. All Rights Reserved.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ */
+package com.arsdigita.web;
+
+// import com.arsdigita.web.Application;
+import com.arsdigita.util.Assert;
+import com.arsdigita.util.Record;
+
+import org.apache.log4j.Logger;
+import org.libreccm.core.CcmSessionContext;
+import org.libreccm.core.User;
+import org.libreccm.web.Application;
+
+/**
+ * + * A session object that provides an environment in which code can execute. The + * WebContext contains all session-specific variables. One session object is + * maintained per thread.
+ * + *+ * Accessors of this class may return null. Developers should take care to trap + * null return values in their code.
+ * + * @author Rafael Schloming + * @author Justin Ross + * @version $Id$ + */ +public final class WebContext extends Record { + + /** + * Internal logger instance to faciliate debugging. Enable logging output by + * editing /WEB-INF/conf/log4j.properties int the runtime environment and + * set com.arsdigita.web.WebContext=DEBUG by uncommenting or adding the + * line. + */ + private static final Logger s_log = Logger.getLogger(WebContext.class); + + private Application m_application = null; + private URL m_requestURL = null; + + /** + * List of properties making up a Web Context + */ + private static String[] s_fields = new String[]{ + "User", + "Application", + "RequestURL" + }; + + /** + * Constructor + */ + WebContext() { + super(WebContext.class, s_log, s_fields); + } + + /** + * Creates a copy of this WebContext + * + * @return a new WebContext as a copy of this one + */ + final WebContext copy() { + WebContext result = new WebContext(); + + result.m_application = m_application; + result.m_requestURL = m_requestURL; + + return result; + } + + /** + * Initializes this WebContext object and setting its properties. + * + * @param app + * @param requestURL + */ + final void init(final Application app, final URL requestURL) { + setApplication(app); + setRequestURL(requestURL); + } + + final void clear() { + m_application = null; + m_requestURL = null; + } + + public final User getUser() { + CcmSessionContext context = Web.getUserContext(); + + if (context == null || !context.isLoggedIn()) { + return null; + } else { + return (User) context.getCurrentSubject(); + } + } + + /** + * + * @return + */ + public final Application getApplication() { + return m_application; + } + + /** + * + * @param app + */ + final void setApplication(final Application app) { + m_application = app; + + mutated("Application"); + } + + /** + * + * @return + */ + public final URL getRequestURL() { + return m_requestURL; + } + + /** + * + * @param url + */ + final void setRequestURL(final URL url) { + Assert.exists(url, "URL url"); + + m_requestURL = url; + + mutated("RequestURL"); + } + +} diff --git a/ccm-core/src/main/java/org/libreccm/core/CcmSessionContext.java b/ccm-core/src/main/java/org/libreccm/core/CcmSessionContext.java index d4417bd44..f05d63764 100644 --- a/ccm-core/src/main/java/org/libreccm/core/CcmSessionContext.java +++ b/ccm-core/src/main/java/org/libreccm/core/CcmSessionContext.java @@ -53,6 +53,10 @@ public class CcmSessionContext implements Serializable { this.effectiveSubject = effectiveSubject; } + public boolean isLoggedIn() { + return currentSubject != null; + } + /** * Execute code under different privileges. Useful if no current user is * available, for example in the startup phase. diff --git a/ccm-core/src/main/java/org/libreccm/core/User.java b/ccm-core/src/main/java/org/libreccm/core/User.java index 59a94ff14..4a2a4267b 100644 --- a/ccm-core/src/main/java/org/libreccm/core/User.java +++ b/ccm-core/src/main/java/org/libreccm/core/User.java @@ -183,18 +183,6 @@ public class User extends Subject implements Serializable { @XmlElement(name = "group-membership", namespace = CORE_XML_NS) private ListgetServletPath method of the application class.
+ * Applications which have their own Servlet should be annotated with this
+ * annotation. The name provided here must be mapped to the Servlet by the
+ * {@link WebServlet} annotation or by the web.xml.
+ *
+ * NOTE: According to Servlet API the path always starts with a leading '/' and
+ * includes either the servlet name or a path to the servlet, but does not
+ * include any extra path information or a query string. Returns an empty string
+ * ("") is the servlet used was matched using the "/*" pattern.
+ *
+ *
+ * @author Jens Pelzetter
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE})
+public @interface ServletPath {
+
+ String value() default URL.SERVLET_DIR + "/legacy-adapter";
+
+}
diff --git a/ccm-docrepo/src/main/java/org/libreccm/docrepo/Resource.java b/ccm-docrepo/src/main/java/org/libreccm/docrepo/Resource.java
index 19dd8fd30..a23957f96 100644
--- a/ccm-docrepo/src/main/java/org/libreccm/docrepo/Resource.java
+++ b/ccm-docrepo/src/main/java/org/libreccm/docrepo/Resource.java
@@ -46,13 +46,13 @@ import java.util.List;
*
* @author Tobias Osmers
*/
-@Entity
+@Entity(name = "DocRepoResource")
@Table(schema = "CCM_DOCREPO", name = "RESOURCES")
@NamedQueries({
@NamedQuery(name = "findChildrenByParent",
- query = "SELECT r FROM Resource r WHERE r.parent = :parentID"),
+ query = "SELECT r FROM DocRepoResource r WHERE r.parent = :parentID"),
@NamedQuery(name = "findResourceByPath",
- query = "SELECT r FROM Resource r WHERE r.path = :pathName")})
+ query = "SELECT r FROM DocRepoResource r WHERE r.path = :pathName")})
public abstract class Resource extends CcmObject {
private static final long serialVersionUID = -910317798106611214L;
diff --git a/pom.xml b/pom.xml
index f44bd004f..bb00d40de 100644
--- a/pom.xml
+++ b/pom.xml
@@ -397,7 +397,13 @@