/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the
 * License at http://www.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
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.developersupport.DeveloperSupport;
import com.arsdigita.kernel.Kernel;
import com.arsdigita.util.Assert;
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.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.log4j.Logger;

/**
 * <P>The top-level container for all Bebop components and
 * containers. </P>
 * <UL>
 * <LI>Holds references to the components of a page.</LI>
 * <LI>Provides methods for servicing requests and for notifying
 * other components that a request for this page has been received through
 * {@link ActionListener ActionListeners}.</LI>
 *
 * <LI>Tracks request parameters for stateful components, such as tabbed
 * panes and sortable tables.</LI>
 * </UL>
 * <p>
 * A typical <code>Page</code> may be created as follows:
 * <blockquote><pre><code>
 * Page p = new Page("Hello World");
 * p.add(new Label("Hello World");
 * p.lock();
 * </code></pre></blockquote>
 * </p>
 *
 * @author David Lutterkort 
 * @author Stanislav Freidin 
 * @author Uday Mathur 
 *
 * @version $Id: //core-platform/dev/src/com/arsdigita/bebop/Page.java#22 $
 */
public class Page extends BlockStylable implements Container {
    public static final String versionId =
        "$Id: //core-platform/dev/src/com/arsdigita/bebop/Page.java#22 $" +
        "by $Author: dennis $, $DateTime: 2004/04/07 16:07:11 $";

    private static final Logger s_log = Logger.getLogger(Page.class);

    /**
     * The delimiter character for components naming
     */
    private static final String DELIMITER = ".";

    /**
     * The prefix that gets prepended to all state variables. Components must
     * not use variables starting with this prefix. This guarantees that the
     * page state and variables individual components wish to pass do not
     * interfere with each other.
     */
    private static final String COMPONENT_PREFIX = "bbp" + DELIMITER;

    private static final String INTERNAL = COMPONENT_PREFIX;

    /**
     * The name of the special parameter that indicates which component has
     * been selected.
     */
    static final String SELECTED = INTERNAL + "s";

    static final String CONTROL_EVENT = INTERNAL + "e";

    static final String CONTROL_VALUE = INTERNAL + "v";

    static final Collection CONTROL_EVENT_KEYS;
    static {
        CONTROL_EVENT_KEYS = new ArrayList(3);
        CONTROL_EVENT_KEYS.add(SELECTED);
        CONTROL_EVENT_KEYS.add(CONTROL_EVENT);
        CONTROL_EVENT_KEYS.add(CONTROL_VALUE);
    }

    /**
     * The name of the request parameter used for the visibility state of
     * components stored in m_invisible.
     */
    static final String INVISIBLE = INTERNAL + "i";

    /**
     * Map of stateful components (id --> Component)
     */
    private Map m_componentMap;
    private List m_components;

    /**
     * Map of component -> owned parameter collection
     */
    private Map m_componentParameterMap = new HashMap();

    private FormModel m_stateModel;

    /**
     * <code>Container</code> that renders this <code>Page</code>.
     */
    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 <code>true</code> if this page should export state through
     * the HttpSession instead of the URL query string.
     * <P>If this returns <code>true</code>, then PageState.stateAsURL()
     * will only export the control event as
     * a URL query string.  If this returns <code>false</code>,
     * then stateAsURL() will export the entire page state.

     *
     * @see PageState#stateAsURL
     *
     * @return <code>true</code> if this page should export state through
     * the HttpSession; <code>false</code> 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 <code>true</code> if PageState.stateAsURL()
     * will export only the control event as a URL query string.
     * <code>false</code> if stateAsURL() will export the entire page state.
     */
    public void setUsingHttpSession(boolean b) {
        m_useHttpSession = b;
    }

    /**
     * Creates an empty page with the specified title and
     * panel.
     *
     * @param title title for this page
     *
     * @param panel container for this page
     */
    public Page(String title, Container panel) {
        this(new Label(title), panel);
    }

    /**
     * Creates an empty page with the specified title and
     * panel.
     *
     * @param title title for this page
     *
     * @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 HashMap();
        setErrorDisplay(new PageErrorDisplay());
        setTitle(title);

        // Initialize the RequestLocal where the title for the current
        // request will be kept
        m_currentTitle = new RequestLocal() {
                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
     */
    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)
     */
    public void add(Component c, int constraints) {
        m_panel.add(c, constraints);
    }

    /**
     * Returns <code>true</code> if this list contains the specified element.
     * More formally, returns <code>true</code> if and only if this list
     * contains at least
     * one element e such that (o==null ? e==null : o.equals(e)).
     * <P>
     * This method returns <code>true</code> only if the component has been
     * directly added to this container. If this container contains another
     * container that contains this component, this method returns
     * <code>false</code>.
     *
     * @param  o element whose presence in this container is to be tested
     *
     * @return <code>true</code> if this Container contains the specified
     * component directly; <code>false</code> otherwise.
     */
    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.
     */
    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())
     * @post !contains(c) implies return == -1 */
    public int indexOf(Component c) {
        return m_panel.indexOf(c);
    }

    /**
     * Returns <code>true</code> if the container contains no components.
     *
     * @return <code>true</code> if this container contains no
     * components; <code>false</code> otherwise.
     */
    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.
     */
    public int size() {
        return m_panel.size();
    }

    public Iterator children() {
        return Collections.singletonList(m_panel).iterator();
    }

    /**
     * Returns the panel that the <code>Page</code> 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.
     *
     * @author Matthew Booth (mbooth@redhat.com)
     */
    public void setPanel( Container c ) {
        m_panel = c;
    }

    /**
     * <p>
     * Retrieves the title of this page.
     * </p>
     *
     * @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.assertNotLocked(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.assertNotLocked(this);
        m_title = title;
    }

    /**
     * Sets the {@link Component} that will display the validation errors
     * in the current {@link PageState}. Any validation error in the
     * <code>PageState</code> will cause the <code>Page</code> to completely
     * ignore all other components and render only the error display component.
     * <p>
     * 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 <code>PageState</code>
     */
    public final void setErrorDisplay(Component c) {
        Assert.assertNotLocked(this);
        m_errorDisplay = c;
    }

    /**
     * Gets the {@link Component} that will display the validation errors
     * in the current {@link PageState}. Any validation error in the
     * <code>PageState</code> will cause the <code>Page</code> to completely
     * ignore all other components and render only the error display component.
     * <p>
     * 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 <code>PageState</code>.
     */
    public final Component getErrorDisplay() {
        return m_errorDisplay;
    }

    /**
     * Sets a stylesheet that should be used in HTML output. To use
     * a CSS stylesheet, call something like
     * <code>setStyleSheet("style.css", "text/css")</code>.
     * <p>
     * These values will ultimately wind up in a <tt>&lt;link&gt;</tt> tag in
     * the head of the HTML page.
     * <p>
     * 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 styleSheetType the MIME type of the stylesheet, usually
     *                       <tt>text/css</tt>
     * @pre ! isLocked()
     * @deprecated Use {@link #addClientStylesheet addClientStylesheet}
     * instead. Will be removed on 2001-05-31.
     */
    public void setStyleSheet(String styleSheetURI, String styleSheetType) {
        Assert.assertNotLocked(this);
        addClientStylesheet(styleSheetURI, styleSheetType);
    }

    /**
     * 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
     * <code>setStyleSheet("style.css", "text/css")</code>.
     *
     * <p> These values will ultimately wind up in a <tt>&lt;link&gt;</tt>
     * tag in the head of the HTML page.
     *
     * <p> 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
     *                       <tt>text/css</tt>
     * @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.
     *
     * <p> If the parameter was previously added as a component state
     * parameter, its name is unmangled and stays unmangled.
     *
     * @param p the global parameter to add
     *
     * @see #addComponentStateParam
     *
     * @pre ! isLocked()
     * @pre parameter != null
     */
    public void addGlobalStateParam(ParameterModel p) {
        Assert.assertNotLocked(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.
     * <p>Generates DOM fragment:
     * <pre>
     * &lt;bebop:page>
     *   &lt;bebop:title> ... value set with <i>setTitle</i> ... &lt;/bebop:title>
     *   &lt;bebop:stylesheet href='styleSheetURI' type='mimeType'>
     *   ... page content gnerated by children ...
     * &lt;/bebop:page></pre>
     * The content of the <tt>&lt;title&gt;</tt> element can be set by
     * calling {@link #setTitle setTitle}. The <tt>&lt;stylesheet&gt;</tt>
     * 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
     *
     * @pre isLocked()
     */
    protected Element generateXMLHelper(PageState ps, Document parent) {
        Assert.assertLocked(this);

        Element page = parent.createRootElement("bebop:page", BEBOP_XML_NS);
        exportAttributes(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 <code>state</code>.
     * @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 (Kernel.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.
     */
    public void generateXML(PageState state, Element elt) {
        return;
    }

    /**
     * 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
     * @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 {
            DeveloperSupport.startStage("Bebop Page Process");
            process(result);
        } finally {
            DeveloperSupport.endStage("Bebop Page Process");
        }
        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.assertLocked(this);
        try {
            DeveloperSupport.startStage("Bebop Request Event");
            fireRequestEvent(state);
        } finally {
            DeveloperSupport.endStage("Bebop Request Event");
        }

        // Validate the state; any errors in the state will be displayed
        // by generateXML
        state.forceValidate();

        if (state.isValid()) {
            try {
                DeveloperSupport.startStage("Bebop Respond");
                state.respond();
            } finally {
                DeveloperSupport.endStage("Bebop Respond");
            }
            try {
                DeveloperSupport.startStage("Bebop Action Event");
                fireActionEvent(state);
            } finally {
                DeveloperSupport.endStage("Bebop Action Event");
            }
        }
    }

    /**
     * Does all the servicing of a request except output generation.
     * This includes most of the common duties of buildDocument and print.
     *
     * @deprecated Use {@link
     * #process(HttpServletRequest,HttpServletResponse)} instead.
     */
    protected PageState prepare(HttpServletRequest req, HttpServletResponse res)
        throws ServletException {

        Assert.assertLocked(this);
        PageState state = process(req, res);
        return state;
    }

    /**
     * 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 {
                    DeveloperSupport.startStage("Bebop XML");
                    generateXML(state, doc);
                } finally {
                    DeveloperSupport.endStage("Bebop XML");
                }
                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.assertNotLocked(this);

            Traversal componentRegistrar = new Traversal() {
                    protected void act(Component c) {
                        addComponent(c);
                        c.register(Page.this);
                    }
                };
            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.
     *
     * <p>Locking a page helps in finding mistakes that result from modifying a
     * page's structure.</P>
     */
    public void lock() {
        if (!m_finished) {
            finish();
        }
        m_stateModel.lock();
        Traversal componentLocker = new Traversal() {
                protected void act(Component c) {
                    c.lock();
                }
            };

        componentLocker.preorder(m_panel);

        super.lock();
    }

    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.assertNotLocked(this);
        m_actionListeners.add(l);
    }

    /**
     * Remove a previously registered action listener.
     * @pre l != null
     * @pre ! isLocked()
     */
    public void removeActionListener(ActionListener l) {
        Assert.assertNotLocked(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.assertNotLocked(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.assertNotLocked(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 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);
        }
    }

    // Client-side stylesheet storage
    private class Stylesheet {
        String m_URI;
        String m_type;

        public Stylesheet(String stylesheetURI, String mimeType) {
            m_URI = stylesheetURI;
            m_type = mimeType;
        }

        public void generateXML(Element parent) {
            Element style = parent.newChildElement("bebop:stylesheet", BEBOP_XML_NS);
            style.addAttribute("href", m_URI);
            if (m_type != null) {
                style.addAttribute("type", m_type);
            }
        }
    }

    /**
     * Adds a component to the page model.
     *
     * @deprecated This method will become private in ACS 5.0.
     */
    public void addComponent(Component c) {
        Assert.assertNotLocked(this);

        if ( ! stateContains(c) ) {
            String key = c.getKey();
            if (key == null) {
                key = Integer.toString(m_components.size());
            }
            if (m_componentMap.get(key) != null) {
                throw new IllegalArgumentException
                    ("Component key must not be duplicated.  The key " +
                     key + " is shared by more than one component.");
            }
            m_componentMap.put(key, c);
            m_components.add(c);
        }
    }

    /**
     * Registers a state parameter for a component. It is permissible to
     * register the same state parameter several times, from the same or
     * different components. The name of the parameter will be changed
     * to ensure that it won't clash with any other component's parameter.
     * If the parameter is added more than once, the name is only changed
     * the first time it is added.
     *
     * @param c the component to register the parameter for
     * @param p the state parameter to register
     *
     * @see #addGlobalStateParam
     *
     * @pre stateContains(c)
     * @pre ! isLocked()
     * @pre p != null
     */
    public void addComponentStateParam(Component c, ParameterModel p) {
        Assert.assertNotLocked(this);

        if ( ! stateContains(c)) {
            throw new IllegalArgumentException
                ("Component must be registered in Page");
        }
        if ( ! m_stateModel.containsFormParam(p) ) {
            p.setName(parameterName(c, p.getName()));
            m_stateModel.addFormParam(p);

            Collection params = (Collection)m_componentParameterMap.get(c);
            if (params == null) {
                params = new ArrayList();
                m_componentParameterMap.put(c, params);
            }
            params.add(p);
        }
    }

    /**
     * <p>Get the parameters registered for a given component.</p>
     */
    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())
     * @post !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())
     * @post 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
     * @return <code>true</code> if the component is visible by default;
     * <code>false</code> otherwise.
     * @see #setVisibleDefault setVisibleDefault
     * @see Component#setVisible  Component.setVisible
     */
    public boolean isVisibleDefault(Component c) {
        Assert.assertTrue(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}.
     *
     * <p> When a component is first added to a page, it is visible.
     *
     * @param c a component whose visibility is to be set
     * @param v <code>true</code> if the component is visible;
     * <code>false</code> otherwise.
     * @see Component#setVisible  Component.setVisible
     * @see Component#register Component.register
     */
    public void setVisibleDefault(Component c, boolean  v) {
        Assert.assertNotLocked(this);

        addComponent(c);
        int i = stateIndex(c);
        if ( v ) {
            m_invisible.clear(i);
        } else {
            m_invisible.set(i);
        }
    }

    /**
     * The global name of the parameter <code>name</code> in the component
     * <code>c</code>.
     */
    public String parameterName(Component c, String name) {
        if ( c == null || ! stateContains(c) ) {
            return name;
        }

        return componentPrefix(c) + name;
    }

    /**
     * The global name of the parameter <code>name</code>.
     */
    public String parameterName(String name) {
        return parameterName(null, name);
    }

    void reset(final PageState ps, Component cmpnt) {
        Traversal resetter = new Traversal() {
                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());
    }
}
