1079 lines
39 KiB
Java
Executable File
1079 lines
39 KiB
Java
Executable File
/*
|
|
* 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.developersupport.DeveloperSupport;
|
|
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;
|
|
|
|
/**
|
|
* <p>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
|
|
* <code>PageState</code> object. Since <code>PageState</code>
|
|
* contains request-specific data, it should only be live during the
|
|
* servicing of one HTTP request. This class has several related
|
|
* responsibilites:</p>
|
|
*
|
|
* <h3>State Management</h3>
|
|
*
|
|
* <p><code>PageState</code> 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.</p>
|
|
*
|
|
* <p>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. </p>
|
|
*
|
|
* <p>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}.</p>
|
|
*
|
|
* <p>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.</p>
|
|
*
|
|
* <p><strong>Note:</strong> As a convention, only the component to which a
|
|
* state parameter belongs should modify it by calling
|
|
* <code>getValue</code> and <code>setValue</code>. All other objects
|
|
* should manipulate the state of a component <i>only</i> through
|
|
* well-defined methods on the component.</p>
|
|
*
|
|
* <h3>Control Events</h3>
|
|
*
|
|
* <p>The control event consists of a pair of strings, the <i>name</i>
|
|
* of the event and its associated <i>value</i>. 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}. </p>
|
|
*
|
|
* <p> Typically, a component will contain code corresponding to the following
|
|
* in its <code>generateXML</code> method:
|
|
* <pre>
|
|
* public void generateXML(PageState state, Element parent) {
|
|
* if ( isVisible(state) ) {
|
|
* <em>MyComponent</em> 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);
|
|
* }
|
|
* }
|
|
* </pre>
|
|
* (In reality, the component would not write a <code>bebop:link</code> element
|
|
* directly, but use a {@link ControlLink} to do that automatically.)
|
|
* <p> When the user clicks on the link that the above <code>generateXML</code>
|
|
* method produces, the component's <code>respond</code> method is called
|
|
* and can access the name and value of the event with code similar to the
|
|
* following:
|
|
* <pre>
|
|
* 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);
|
|
* }
|
|
* }
|
|
* </pre>
|
|
*
|
|
*
|
|
* <h3>Temporary Storage</h3>
|
|
*
|
|
* <p> 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.
|
|
*
|
|
* <h3>Convenience Access to Related Objects</h3>
|
|
*
|
|
* <p> <code>PageState</code> 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.
|
|
* </p>
|
|
*
|
|
* @author David Lutterkort
|
|
* @author Uday Mathur
|
|
* @version $Id: PageState.java 975 2005-11-07 09:27:08Z clasohm $
|
|
*/
|
|
public class PageState {
|
|
|
|
/** Class specific logger instance. */
|
|
private static final Logger s_log = Logger.getLogger(PageState.class);
|
|
|
|
/** The underlying Page object. */
|
|
private Page m_page;
|
|
|
|
/**
|
|
* The request to which this object corresponds
|
|
*/
|
|
private HttpServletRequest m_request;
|
|
|
|
/**
|
|
* The response to which results will be sent.
|
|
*/
|
|
private HttpServletResponse m_response;
|
|
|
|
/**
|
|
* The values of global and component specific state parameters extracted
|
|
* from the request.
|
|
*/
|
|
private FormData m_pageState;
|
|
|
|
/**
|
|
* Temporary storage of arbitrary objects.
|
|
*/
|
|
private Map m_attributes;
|
|
|
|
/**
|
|
* The component that currently holds exclusive access to the control
|
|
* event. Usually null, unlesss a component calls {@link
|
|
* #grabControlEvent}.
|
|
*/
|
|
private Component m_grabbingComponent;
|
|
|
|
/**
|
|
* The visibility state of components. For a component with n =
|
|
* Page.stateIndex, the n-th bit is set if the component is <i>not</i>
|
|
* visible.
|
|
*
|
|
* <p> 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 {
|
|
DeveloperSupport.startStage("Bebop Respond " + c.getClass().getName());
|
|
c.respond(this);
|
|
} finally {
|
|
DeveloperSupport.endStage("Bebop Respond " + c.getClass().getName());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 <code>true</code> is the comonent <code>c</code> 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 <code>true</code> 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 <code>true</code> is the component <code>c</code> 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.
|
|
*
|
|
* <p>This method is different than <code>isVisible</code> which
|
|
* only returns the visibility flag for the individual component.
|
|
*
|
|
* @param c the components whose visibility should be returned
|
|
* @return <code>true</code> 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.
|
|
*
|
|
* <p> Without explicit changes, a component is visible.
|
|
*
|
|
* @param c the component whose visibility is to be changed
|
|
* @param v <code>true</code> 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 <code>key</code>. The
|
|
// * <code>PageState</code> puts no restrictions on what can be stored as
|
|
// * an attribute or how they are managed.
|
|
// *
|
|
// * To remove an attribute, call <code>setAttribute(key, null)</code>.
|
|
// *
|
|
// * The attributes are only accessible as long as the
|
|
// * <code>PageState</code> is alive, typically only for the duration of
|
|
// * the request.
|
|
// *
|
|
// * @deprecated Use either <code>setAttribute</code> 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 <code>getAttribute</code> 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 <code>p</code>. The concrete
|
|
* type of <code>value</code> must be compatible with the type of the
|
|
* state parameter.
|
|
*
|
|
* <p> 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 <code>p</code>. The concrete type of
|
|
* the return value depends on the type of the parameter being
|
|
* used.
|
|
*
|
|
* <p> 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 <code>c</code>
|
|
* 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 <i>delayed</i> event
|
|
* that only gets acted on when another request to this <code>Page</code>
|
|
* 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.
|
|
* <p>
|
|
* 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}.
|
|
* <p>
|
|
* Stateful components can use the control event to change their
|
|
* state. For example, a tabbed pane <code>t</code> might call
|
|
* <code>setControlEvent(t, "select", "2")</code> just prior to
|
|
* generating the link for its second tab.
|
|
* <p>
|
|
* The values of <code>name</code> and <code>value</code> have no
|
|
* meaning to the page state, they are simply passed through without
|
|
* modifications. It is up to specific components what values of
|
|
* <code>name</code> and <code>value</code> are meaningful for it.
|
|
*
|
|
* @param c
|
|
* @param name The component specific name of the event, may be
|
|
* <code>null</code>
|
|
* @param value The component specific value of the event, may be
|
|
* <code>null</code>
|
|
* @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
|
|
* <code>null</code>.
|
|
*/
|
|
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
|
|
* <code>grabControlEvent</code>
|
|
* @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 <code>parent</code> that represent the current page
|
|
* state. For each component or global state parameter on the page, a
|
|
* <tt><bebop:pageState></tt> element is added to
|
|
* <code>parent</code>. The <tt>name</tt> and <tt>value</tt> 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.
|
|
*
|
|
* <p>Generates DOM fragment:
|
|
* <p><code><pre>
|
|
* <bebop:pageState name=... value=.../>
|
|
* </code></pre>
|
|
*
|
|
* @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.
|
|
*
|
|
* <p>
|
|
* 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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <p>Write the current state of the page as a URL.</p>
|
|
*
|
|
* <p>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.</p>
|
|
*
|
|
* <p>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.</p>
|
|
*
|
|
* @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.getContext().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 of <code>this</code>.
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
}
|