blob: 5b57110d36f6da9af9298c17951cd8db6cd579d2 [file] [log] [blame]
// Copyright 2004 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry.engine;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tapestry.ApplicationRuntimeException;
import org.apache.tapestry.IComponent;
import org.apache.tapestry.IEngine;
import org.apache.tapestry.IForm;
import org.apache.tapestry.IMarkupWriter;
import org.apache.tapestry.IPage;
import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.RenderRewoundException;
import org.apache.tapestry.StaleLinkException;
import org.apache.tapestry.Tapestry;
import org.apache.tapestry.event.ChangeObserver;
import org.apache.tapestry.event.ObservedChangeEvent;
import org.apache.tapestry.request.RequestContext;
/**
* Provides the logic for processing a single request cycle. Provides access to
* the {@link IEngine engine} and the {@link RequestContext}.
*
* @author Howard Lewis Ship
*
**/
public class RequestCycle implements IRequestCycle, ChangeObserver
{
private static final Log LOG = LogFactory.getLog(RequestCycle.class);
private IPage _page;
private IEngine _engine;
private IEngineService _service;
private RequestContext _requestContext;
private IMonitor _monitor;
private HttpServletResponse _response;
/**
* A mapping of pages loaded during the current request cycle.
* Key is the page name, value is the {@link IPage} instance.
*
**/
private Map _loadedPages;
/**
* A mapping of page recorders for the current request cycle.
* Key is the page name, value is the {@link IPageRecorder} instance.
*
**/
private Map _loadedRecorders;
private boolean _rewinding = false;
private Map _attributes;
private int _actionId;
private int _targetActionId;
private IComponent _targetComponent;
/** @since 2.0.3 **/
private Object[] _serviceParameters;
/**
* Standard constructor used to render a response page.
*
**/
public RequestCycle(
IEngine engine,
RequestContext requestContext,
IEngineService service,
IMonitor monitor)
{
_engine = engine;
_requestContext = requestContext;
_service = service;
_monitor = monitor;
}
/**
* Called at the end of the request cycle (i.e., after all responses have been
* sent back to the client), to release all pages loaded during the request cycle.
*
**/
public void cleanup()
{
if (_loadedPages == null)
return;
IPageSource source = _engine.getPageSource();
Iterator i = _loadedPages.values().iterator();
while (i.hasNext())
{
IPage page = (IPage) i.next();
source.releasePage(page);
}
_loadedPages = null;
_loadedRecorders = null;
}
public IEngineService getService()
{
return _service;
}
public String encodeURL(String URL)
{
if (_response == null)
_response = _requestContext.getResponse();
return _response.encodeURL(URL);
}
public IEngine getEngine()
{
return _engine;
}
public Object getAttribute(String name)
{
if (_attributes == null)
return null;
return _attributes.get(name);
}
public IMonitor getMonitor()
{
return _monitor;
}
public String getNextActionId()
{
return Integer.toHexString(++_actionId);
}
public IPage getPage()
{
return _page;
}
/**
* Gets the page from the engines's {@link IPageSource}.
*
**/
public IPage getPage(String name)
{
IPage result = null;
if (name == null)
throw new NullPointerException(Tapestry.getMessage("RequestCycle.invalid-null-name"));
if (_loadedPages != null)
result = (IPage) _loadedPages.get(name);
if (result == null)
{
_monitor.pageLoadBegin(name);
IPageSource pageSource = _engine.getPageSource();
result = pageSource.getPage(this, name, _monitor);
// Get the recorder that will eventually observe and record
// changes to persistent properties of the page. If the page
// has never emitted any page changes, then it will
// not have a recorder.
IPageRecorder recorder = getPageRecorder(name);
if (recorder != null)
{
// Have it rollback the page to the prior state. Note that
// the page has a null observer at this time.
recorder.rollback(result);
// Now, have the page use the recorder for any future
// property changes.
result.setChangeObserver(recorder);
// And, if this recorder observed changes in a prior request cycle
// (and was locked after committing in that cycle), it's time
// to unlock.
recorder.setLocked(false);
}
else
{
// No page recorder for the page. We'll observe its
// changes and create the page recorder dynamically
// if it emits any.
result.setChangeObserver(this);
}
_monitor.pageLoadEnd(name);
if (_loadedPages == null)
_loadedPages = new HashMap();
_loadedPages.put(name, result);
}
return result;
}
/**
* Returns the page recorder for the named page. This may come
* from the cycle's cache of page recorders or, if not yet encountered
* in this request cycle, the {@link IEngine#getPageRecorder(String, IRequestCycle)} is
* invoked to get the recorder, if it exists.
*
**/
protected IPageRecorder getPageRecorder(String name)
{
IPageRecorder result = null;
if (_loadedRecorders != null)
result = (IPageRecorder) _loadedRecorders.get(name);
if (result != null)
return result;
result = _engine.getPageRecorder(name, this);
if (result == null)
return null;
if (_loadedRecorders == null)
_loadedRecorders = new HashMap();
_loadedRecorders.put(name, result);
return result;
}
/**
*
* Gets the page recorder from the loadedRecorders cache, or from the engine
* (putting it into loadedRecorders). If the recorder does not yet exist,
* it is created.
*
* @see IEngine#createPageRecorder(String, IRequestCycle)
* @since 2.0.3
*
**/
private IPageRecorder createPageRecorder(String name)
{
IPageRecorder result = getPageRecorder(name);
if (result == null)
{
result = _engine.createPageRecorder(name, this);
if (_loadedRecorders == null)
_loadedRecorders = new HashMap();
_loadedRecorders.put(name, result);
}
return result;
}
public RequestContext getRequestContext()
{
return _requestContext;
}
public boolean isRewinding()
{
return _rewinding;
}
public boolean isRewound(IComponent component) throws StaleLinkException
{
// If not rewinding ...
if (!_rewinding)
return false;
if (_actionId != _targetActionId)
return false;
// OK, we're there, is the page is good order?
if (component == _targetComponent)
return true;
// Woops. Mismatch.
throw new StaleLinkException(
component,
Integer.toHexString(_targetActionId),
_targetComponent.getExtendedId());
}
public void removeAttribute(String name)
{
if (LOG.isDebugEnabled())
LOG.debug("Removing attribute " + name);
if (_attributes == null)
return;
_attributes.remove(name);
}
/**
* Renders the page by invoking
* {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}.
* This clears all attributes.
*
**/
public void renderPage(IMarkupWriter writer)
{
String pageName = _page.getPageName();
_monitor.pageRenderBegin(pageName);
_rewinding = false;
_actionId = -1;
_targetActionId = 0;
// Forget any attributes from a previous render cycle.
if (_attributes != null)
_attributes.clear();
try
{
_page.renderPage(writer, this);
}
catch (ApplicationRuntimeException ex)
{
// Nothing much to add here.
throw ex;
}
catch (Throwable ex)
{
// But wrap other exceptions in a RequestCycleException ... this
// will ensure that some of the context is available.
throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex);
}
finally
{
_actionId = 0;
_targetActionId = 0;
}
_monitor.pageRenderEnd(pageName);
}
/**
* Rewinds an individual form by invoking
* {@link IForm#rewind(IMarkupWriter, IRequestCycle)}.
*
* <p>The process is expected to end with a {@link RenderRewoundException}.
* If the entire page is renderred without this exception being thrown, it means
* that the target action id was not valid, and a
* {@link ApplicationRuntimeException}
* is thrown.
*
* <p>This clears all attributes.
*
* @since 1.0.2
**/
public void rewindForm(IForm form, String targetActionId)
{
IPage page = form.getPage();
String pageName = page.getPageName();
_rewinding = true;
_monitor.pageRewindBegin(pageName);
if (_attributes != null)
_attributes.clear();
// Fake things a little for getNextActionId() / isRewound()
_targetActionId = Integer.parseInt(targetActionId, 16);
_actionId = _targetActionId - 1;
_targetComponent = form;
try
{
page.beginPageRender();
form.rewind(NullWriter.getSharedInstance(), this);
// Shouldn't get this far, because the form should
// throw the RenderRewoundException.
throw new StaleLinkException(
Tapestry.format("RequestCycle.form-rewind-failure", form.getExtendedId()),
form);
}
catch (RenderRewoundException ex)
{
// This is acceptible and expected.
}
catch (ApplicationRuntimeException ex)
{
// RequestCycleExceptions don't need to be wrapped.
throw ex;
}
catch (Throwable ex)
{
// But wrap other exceptions in a ApplicationRuntimeException ... this
// will ensure that some of the context is available.
throw new ApplicationRuntimeException(ex.getMessage(), page, null, ex);
}
finally
{
_actionId = 0;
_targetActionId = 0;
_targetComponent = null;
page.endPageRender();
_monitor.pageRewindEnd(pageName);
_rewinding = false;
}
}
/**
* Rewinds the page by invoking
* {@link IPage#renderPage(IMarkupWriter, IRequestCycle)}.
*
* <p>The process is expected to end with a {@link RenderRewoundException}.
* If the entire page is renderred without this exception being thrown, it means
* that the target action id was not valid, and a
* {@link ApplicationRuntimeException}
* is thrown.
*
* <p>This clears all attributes.
*
**/
public void rewindPage(String targetActionId, IComponent targetComponent)
{
String pageName = _page.getPageName();
_rewinding = true;
_monitor.pageRewindBegin(pageName);
if (_attributes != null)
_attributes.clear();
_actionId = -1;
// Parse the action Id as hex since that's whats generated
// by getNextActionId()
_targetActionId = Integer.parseInt(targetActionId, 16);
_targetComponent = targetComponent;
try
{
_page.renderPage(NullWriter.getSharedInstance(), this);
// Shouldn't get this far, because the target component should
// throw the RenderRewoundException.
throw new StaleLinkException(_page, targetActionId, targetComponent.getExtendedId());
}
catch (RenderRewoundException ex)
{
// This is acceptible and expected.
}
catch (ApplicationRuntimeException ex)
{
// ApplicationRuntimeExceptions don't need to be wrapped.
throw ex;
}
catch (Throwable ex)
{
// But wrap other exceptions in a RequestCycleException ... this
// will ensure that some of the context is available.
throw new ApplicationRuntimeException(ex.getMessage(), _page, null, ex);
}
finally
{
_actionId = 0;
_targetActionId = 0;
_targetComponent = null;
_monitor.pageRewindEnd(pageName);
_rewinding = false;
}
}
public void setAttribute(String name, Object value)
{
if (LOG.isDebugEnabled())
LOG.debug("Set attribute " + name + " to " + value);
if (_attributes == null)
_attributes = new HashMap();
_attributes.put(name, value);
}
public void setPage(IPage value)
{
if (LOG.isDebugEnabled())
LOG.debug("Set page to " + value);
_page = value;
}
public void setPage(String name)
{
if (LOG.isDebugEnabled())
LOG.debug("Set page to " + name);
_page = getPage(name);
}
/**
* Invokes {@link IPageRecorder#commit()} on each page recorder loaded
* during the request cycle (even recorders marked for discard).
*
**/
public void commitPageChanges()
{
if (LOG.isDebugEnabled())
LOG.debug("Committing page changes");
if (_loadedRecorders == null || _loadedRecorders.isEmpty())
return;
Iterator i = _loadedRecorders.values().iterator();
while (i.hasNext())
{
IPageRecorder recorder = (IPageRecorder) i.next();
recorder.commit();
}
}
/**
* For pages without a {@link IPageRecorder page recorder},
* we're the {@link ChangeObserver change observer}.
* If such a page actually emits a change, then
* we'll obtain a new page recorder from the
* {@link IEngine engine}, set the recorder
* as the page's change observer, and forward the event
* to the newly created recorder. In addition, the
* new page recorder is remembered so that it will
* be committed by {@link #commitPageChanges()}.
*
**/
public void observeChange(ObservedChangeEvent event)
{
IPage page = event.getComponent().getPage();
String pageName = page.getPageName();
if (LOG.isDebugEnabled())
LOG.debug("Observed change in page " + pageName + "; creating page recorder.");
IPageRecorder recorder = createPageRecorder(pageName);
page.setChangeObserver(recorder);
recorder.observeChange(event);
}
/**
* Finds the page and its page recorder, creating the page recorder if necessary.
* The page recorder is marked for discard regardless of its current state.
*
* <p>This may make the application stateful even if the page recorder does
* not yet exist.
*
* <p>The page recorder will be discarded at the end of the current request cycle.
*
* @since 2.0.2
*
**/
public void discardPage(String name)
{
if (LOG.isDebugEnabled())
LOG.debug("Discarding page " + name);
IPageRecorder recorder = _engine.getPageRecorder(name, this);
if (recorder == null)
{
_page = getPage(name);
recorder = createPageRecorder(name);
_page.setChangeObserver(recorder);
}
recorder.markForDiscard();
}
/** @since 2.0.3 **/
public Object[] getServiceParameters()
{
return _serviceParameters;
}
/** @since 2.0.3 **/
public void setServiceParameters(Object[] serviceParameters)
{
_serviceParameters = serviceParameters;
}
/** @since 3.0 **/
public void activate(String name)
{
IPage page = getPage(name);
activate(page);
}
/** @since 3.0 */
public void activate(IPage page)
{
if (LOG.isDebugEnabled())
LOG.debug("Activating page " + page);
Tapestry.clearMethodInvocations();
page.validate(this);
Tapestry.checkMethodInvocation(
Tapestry.ABSTRACTPAGE_VALIDATE_METHOD_ID,
"validate()",
page);
setPage(page);
}
/**
* @since 3.0
*/
public String toString()
{
ToStringBuilder b = new ToStringBuilder(this);
b.append("rewinding", _rewinding);
if (_service != null)
b.append("service", _service.getName());
b.append("serviceParameters", _serviceParameters);
if (_loadedPages != null)
b.append("loadedPages", _loadedPages.keySet());
b.append("attributes", _attributes);
b.append("targetActionId", _targetActionId);
b.append("targetComponent", _targetComponent);
return b.toString();
}
}