blob: edffc0cd0315648df5655c8feca545e102726c02 [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.request;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.tapestry.ApplicationServlet;
import org.apache.tapestry.IMarkupWriter;
import org.apache.tapestry.IRender;
import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.Tapestry;
import org.apache.tapestry.multipart.DefaultMultipartDecoder;
import org.apache.tapestry.multipart.IMultipartDecoder;
import org.apache.tapestry.spec.IApplicationSpecification;
import org.apache.tapestry.util.IRenderDescription;
/**
* This class encapsulates all the relevant data for one request cycle of an
* {@link ApplicationServlet}. This includes:
* <ul>
* <li>{@link HttpServletRequest}
* <li>{@link HttpServletResponse}
* <li>{@link HttpSession}
* <li>{@link javax.servlet.http.HttpServlet}
* </ul>
* <p>It also provides methods for:
* <ul>
* <li>Retrieving the request parameters (even if a file upload is involved)
* <li>Getting, setting and removing request attributes
* <li>Forwarding requests
* <li>Redirecting requests
* <li>Getting and setting Cookies
* <li>Intepreting the request path info
* <li>Writing an HTML description of the <code>RequestContext</code> (for debugging).
* </ul>
*
*
* <p>
* If some cases, it is necesary to provide an implementation of
* {@link IRequestDecoder} (often, due to a firewall).
* If the application specifification
* provides an extension named
* <code>org.apache.tapestry.request-decoder</code>
* then it will be used, instead of a default decoder.
*
* <p>This class is not a component, but does implement {@link IRender}. When asked to render
* (perhaps as the delegate of a {@link org.apache.tapestry.components.Delegator} component}
* it simply invokes {@link #write(IMarkupWriter)} to display all debugging output.
*
* <p>This class is derived from the original class
* <code>com.primix.servlet.RequestContext</code>,
* part of the <b>ServletUtils</b> framework available from
* <a href="http://www.gjt.org/servlets/JCVSlet/list/gjt/com/primix/servlet">The Giant
* Java Tree</a>.
*
*
* @version $Id$
* @author Howard Lewis Ship
*
**/
public class RequestContext implements IRender
{
/** @since 2.2 **/
private static class DefaultRequestDecoder implements IRequestDecoder
{
public DecodedRequest decodeRequest(HttpServletRequest request)
{
DecodedRequest result = new DecodedRequest();
result.setRequestURI(request.getRequestURI());
result.setScheme(request.getScheme());
result.setServerName(request.getServerName());
result.setServerPort(request.getServerPort());
return result;
}
}
private static final Log LOG = LogFactory.getLog(RequestContext.class);
private HttpSession _session;
private HttpServletRequest _request;
private HttpServletResponse _response;
private ApplicationServlet _servlet;
private DecodedRequest _decodedRequest;
private IMultipartDecoder _decoder;
private boolean _decoded;
/**
* A mapping of the cookies available in the request.
*
**/
private Map _cookieMap;
/**
* Used during {@link #write(IMarkupWriter)}.
*
**/
private boolean _evenRow;
/**
* Creates a <code>RequestContext</code> from its components.
*
**/
public RequestContext(
ApplicationServlet servlet,
HttpServletRequest request,
HttpServletResponse response)
throws IOException
{
_servlet = servlet;
_request = request;
_response = response;
// All three parameters may be null if created from
// AbstractEngine.cleanupEngine().
if (_request != null && DefaultMultipartDecoder.isMultipartRequest(request))
{
IMultipartDecoder decoder = obtainMultipartDecoder(servlet, request);
setDecoder(decoder);
}
}
/**
* Invoked from the constructor to create a {@link DefaultMultipartDecoder} instance.
* Applications with specific upload needs may need to override this to
* provide a subclass instance instead. The caller will invoke
* {@link IMultipartDecoder#decode(HttpServletRequest)} on the
* returned object.
*
* <p>
* This implementation checks for application extension
* {@link Tapestry#MULTIPART_DECODER_EXTENSION_NAME}. If that is not
* defined, a shared instance of {@link DefaultMultipartDecoder}
* is returned.
*
*
* @see ApplicationServlet#createRequestContext(HttpServletRequest, HttpServletResponse)
* @since 3.0
*
**/
protected IMultipartDecoder obtainMultipartDecoder(
ApplicationServlet servlet,
HttpServletRequest request)
throws IOException
{
IApplicationSpecification spec = servlet.getApplicationSpecification();
if (spec.checkExtension(Tapestry.MULTIPART_DECODER_EXTENSION_NAME))
return (IMultipartDecoder) spec.getExtension(
Tapestry.MULTIPART_DECODER_EXTENSION_NAME,
IMultipartDecoder.class);
return DefaultMultipartDecoder.getSharedInstance();
}
/**
* Adds a simple {@link Cookie}. To set a Cookie with attributes,
* use {@link #addCookie(Cookie)}.
*
**/
public void addCookie(String name, String value)
{
addCookie(new Cookie(name, value));
}
/**
* Adds a {@link Cookie} to the response. Once added, the
* Cookie will also be available to {@link #getCookie(String)} method.
*
* <p>Cookies should only be added <em>before</em> invoking
* {@link HttpServletResponse#getWriter()}..
*
**/
public void addCookie(Cookie cookie)
{
if (LOG.isDebugEnabled())
LOG.debug("Adding cookie " + cookie);
_response.addCookie(cookie);
if (_cookieMap == null)
readCookieMap();
_cookieMap.put(cookie.getName(), cookie);
}
private void datePair(IMarkupWriter writer, String name, long value)
{
pair(writer, name, new Date(value));
}
/** @since 2.2 **/
private DecodedRequest getDecodedRequest()
{
if (_decodedRequest != null)
return _decodedRequest;
IApplicationSpecification spec = _servlet.getApplicationSpecification();
IRequestDecoder decoder = null;
if (!spec.checkExtension(Tapestry.REQUEST_DECODER_EXTENSION_NAME))
decoder = new DefaultRequestDecoder();
else
decoder =
(IRequestDecoder) spec.getExtension(
Tapestry.REQUEST_DECODER_EXTENSION_NAME,
IRequestDecoder.class);
_decodedRequest = decoder.decodeRequest(_request);
return _decodedRequest;
}
/**
*
* Returns the actual scheme, possibly decoded from the request.
*
* @see IRequestDecoder
* @see javax.servlet.ServletRequest#getScheme()
* @since 2.2
*
**/
public String getScheme()
{
return getDecodedRequest().getScheme();
}
/**
*
* Returns the actual server name, possibly decoded from the request.
*
* @see IRequestDecoder
* @see javax.servlet.ServletRequest#getServerName()
* @since 2.2
*
**/
public String getServerName()
{
return getDecodedRequest().getServerName();
}
/**
*
* Returns the actual server port, possibly decoded from the request.
*
* @see IRequestDecoder
* @see javax.servlet.ServletRequest#getServerPort()
* @since 2.2
*
**/
public int getServerPort()
{
return getDecodedRequest().getServerPort();
}
/**
*
* Returns the actual request URI, possibly decoded from the request.
*
* @see IRequestDecoder
* @see HttpServletRequest#getRequestURI()
* @since 2.2
*
**/
public String getRequestURI()
{
return getDecodedRequest().getRequestURI();
}
/**
* Builds an absolute URL from the given URI, using the {@link HttpServletRequest}
* as the source for scheme, server name and port.
*
* @see #getAbsoluteURL(String, String, String, int)
*
**/
public String getAbsoluteURL(String URI)
{
String scheme = getScheme();
String server = getServerName();
int port = getServerPort();
// Keep things simple ... port 80 is accepted as the
// standard port for http so it can be ommitted.
// Some of the Tomcat code indicates that port 443 is the default
// for https, and that needs to be researched.
if (scheme.equals("http") && port == 80)
port = 0;
return getAbsoluteURL(URI, scheme, server, port);
}
/**
* Does some easy checks to turn a path (or URI) into an absolute URL. We assume
* <ul>
* <li>The presense of a colon means the path is complete already (any other colons
* in the URI portion should have been converted to %3A).
*
* <li>A leading pair of forward slashes means the path is simply missing
* the scheme.
* <li>Otherwise, we assemble the scheme, server, port (if non-zero) and the URI
* as given.
* </ul>
*
**/
public String getAbsoluteURL(String URI, String scheme, String server, int port)
{
StringBuffer buffer = new StringBuffer();
// Though, really, what does a leading colon with no scheme before it
// mean?
if (URI.indexOf(':') >= 0)
return URI;
// Should check the length here, first.
if (URI.substring(0, 1).equals("//"))
{
buffer.append(scheme);
buffer.append(':');
buffer.append(URI);
return buffer.toString();
}
buffer.append(scheme);
buffer.append("://");
buffer.append(server);
if (port > 0)
{
buffer.append(':');
buffer.append(port);
}
if (URI.charAt(0) != '/')
buffer.append('/');
buffer.append(URI);
return buffer.toString();
}
/**
* Gets a named {@link Cookie}.
*
* @param name The name of the Cookie.
* @return The Cookie, or null if no Cookie with that
* name exists.
*
**/
public Cookie getCookie(String name)
{
if (_cookieMap == null)
readCookieMap();
return (Cookie) _cookieMap.get(name);
}
/**
* Reads the named {@link Cookie} and returns its value (if it exists), or
* null if it does not exist.
**/
public String getCookieValue(String name)
{
Cookie cookie;
cookie = getCookie(name);
if (cookie == null)
return null;
return cookie.getValue();
}
/**
* Returns the named parameter from the {@link HttpServletRequest}.
*
* <p>Use {@link #getParameters(String)} for parameters that may
* include multiple values.
*
* <p>This is the preferred way to obtain parameter values (rather than
* obtaining the {@link HttpServletRequest} itself). For form/multipart-data
* encoded requests, this method will still work.
*
**/
public String getParameter(String name)
{
IMultipartDecoder decoder = getDecoder();
if (decoder != null)
return decoder.getString(_request, name);
return _request.getParameter(name);
}
/**
* Convienience method for getting a {@link HttpServletRequest} attribute.
*
* @since 2.3
*
**/
public Object getAttribute(String name)
{
return _request.getAttribute(name);
}
/**
* For parameters that are, or are possibly, multi-valued, this
* method returns all the values as an array of Strings.
*
* @see #getParameter(String)
*
**/
public String[] getParameters(String name)
{
// Note: this may not be quite how we want it to work; we'll have to see.
IMultipartDecoder decoder = getDecoder();
if (decoder != null)
return decoder.getStrings(_request, name);
return _request.getParameterValues(name);
}
/**
* Returns the named {@link IUploadFile}, if it exists, or null if it doesn't.
* Uploads require an encoding of <code>multipart/form-data</code>
* (this is specified in the
* form's enctype attribute). If the encoding type
* is not so, or if no upload matches the name, then this method returns null.
*
**/
public IUploadFile getUploadFile(String name)
{
IMultipartDecoder decoder = getDecoder();
if (decoder == null)
return null;
return decoder.getUploadFile(_request, name);
}
/**
* Invoked at the end of the request cycle to cleanup and temporary resources.
* This is chained to the {@link DefaultMultipartDecoder}, if there is one.
*
* @since 2.0.1
**/
public void cleanup()
{
if (_decoder != null)
_decoder.cleanup(_request);
}
/**
* Returns the request which initiated the current request cycle. Note that
* the methods {@link #getParameter(String)} and {@link #getParameters(String)}
* should be used, rather than obtaining parameters directly from the request
* (since the RequestContext handles the differences between normal and multipart/form
* requests).
*
**/
public HttpServletRequest getRequest()
{
return _request;
}
public HttpServletResponse getResponse()
{
return _response;
}
private String getRowClass()
{
String result;
result = _evenRow ? "even" : "odd";
_evenRow = !_evenRow;
return result;
}
public ApplicationServlet getServlet()
{
return _servlet;
}
/**
* Returns the {@link HttpSession}, if necessary, invoking
* {@link HttpServletRequest#getSession(boolean)}. However,
* this method will <em>not</em> create a session.
*
**/
public HttpSession getSession()
{
if (_session == null)
_session = _request.getSession(false);
return _session;
}
/**
* Like {@link #getSession()}, but forces the creation of
* the {@link HttpSession}, if necessary.
*
**/
public HttpSession createSession()
{
if (_session == null)
{
if (LOG.isDebugEnabled())
LOG.debug("Creating HttpSession");
_session = _request.getSession(true);
}
return _session;
}
private void header(IMarkupWriter writer, String valueName, String dataName)
{
writer.begin("tr");
writer.attribute("class", "request-context-header");
writer.begin("th");
writer.print(valueName);
writer.end();
writer.begin("th");
writer.print(dataName);
writer.end("tr");
_evenRow = true;
}
private void object(IMarkupWriter writer, String objectName)
{
writer.begin("span");
writer.attribute("class", "request-context-object");
writer.print(objectName);
writer.end();
}
private void pair(IMarkupWriter writer, String name, int value)
{
pair(writer, name, Integer.toString(value));
}
private void pair(IMarkupWriter writer, String name, Object value)
{
if (value == null)
return;
if (value instanceof IRenderDescription)
{
IRenderDescription renderValue = (IRenderDescription) value;
writer.begin("tr");
writer.attribute("class", getRowClass());
writer.begin("th");
writer.print(name);
writer.end();
writer.begin("td");
renderValue.renderDescription(writer);
writer.end("tr");
writer.println();
return;
}
pair(writer, name, value.toString());
}
private void pair(IMarkupWriter writer, String name, String value)
{
if (value == null)
return;
if (value.length() == 0)
return;
writer.begin("tr");
writer.attribute("class", getRowClass());
writer.begin("th");
writer.print(name);
writer.end();
writer.begin("td");
writer.print(value);
writer.end("tr");
writer.println();
}
private void pair(IMarkupWriter writer, String name, boolean value)
{
pair(writer, name, value ? "yes" : "no");
}
private void readCookieMap()
{
_cookieMap = new HashMap();
Cookie[] cookies = _request.getCookies();
if (cookies != null)
for (int i = 0; i < cookies.length; i++)
_cookieMap.put(cookies[i].getName(), cookies[i]);
}
/**
* Invokes {@link HttpServletResponse#sendRedirect(String)}</code>,
* but massages <code>path</code>, supplying missing elements to
* make it an absolute URL (i.e., specifying scheme, server, port, etc.).
*
* <p>The 2.2 Servlet API will do this automatically, and a little more,
* according to the early documentation.
*
**/
public void redirect(String path) throws IOException
{
// Now a little magic to convert path into a complete URL. The Servlet
// 2.2 API does this automatically.
String absolutePath = getAbsoluteURL(path);
String encodedURL = _response.encodeRedirectURL(absolutePath);
_response.sendRedirect(encodedURL);
}
private void section(IMarkupWriter writer, String sectionName)
{
writer.begin("tr");
writer.attribute("class", "request-context-section");
writer.begin("th");
writer.attribute("colspan", 2);
writer.print(sectionName);
writer.end("tr");
}
private List getSorted(Enumeration e)
{
List result = new ArrayList();
// JDK 1.4 includes a helper method in Collections for
// this; but we want 1.2 compatibility for the
// forseable future.
while (e.hasMoreElements())
result.add(e.nextElement());
Collections.sort(result);
return result;
}
/**
* Writes the state of the context to the writer, typically for inclusion
* in a HTML page returned to the user. This is useful
* when debugging. The Inspector uses this as well.
*
**/
public void write(IMarkupWriter writer)
{
// Create a box around all of this stuff ...
writer.begin("table");
writer.attribute("class", "request-context-border");
writer.begin("tr");
writer.begin("td");
// Get the session, if it exists, and display it.
HttpSession session = getSession();
if (session != null)
{
object(writer, "Session");
writer.begin("table");
writer.attribute("class", "request-context-object");
section(writer, "Properties");
header(writer, "Name", "Value");
pair(writer, "id", session.getId());
datePair(writer, "creationTime", session.getCreationTime());
datePair(writer, "lastAccessedTime", session.getLastAccessedTime());
pair(writer, "maxInactiveInterval", session.getMaxInactiveInterval());
pair(writer, "new", session.isNew());
List names = getSorted(session.getAttributeNames());
int count = names.size();
for (int i = 0; i < count; i++)
{
if (i == 0)
{
section(writer, "Attributes");
header(writer, "Name", "Value");
}
String name = (String) names.get(i);
pair(writer, name, session.getAttribute(name));
}
writer.end(); // Session
}
object(writer, "Request");
writer.begin("table");
writer.attribute("class", "request-context-object");
// Parameters ...
List parameters = getSorted(_request.getParameterNames());
int count = parameters.size();
for (int i = 0; i < count; i++)
{
if (i == 0)
{
section(writer, "Parameters");
header(writer, "Name", "Value(s)");
}
String name = (String) parameters.get(i);
String[] values = _request.getParameterValues(name);
writer.begin("tr");
writer.attribute("class", getRowClass());
writer.begin("th");
writer.print(name);
writer.end();
writer.begin("td");
if (values.length > 1)
writer.begin("ul");
for (int j = 0; j < values.length; j++)
{
if (values.length > 1)
writer.beginEmpty("li");
writer.print(values[j]);
}
writer.end("tr");
}
section(writer, "Properties");
header(writer, "Name", "Value");
pair(writer, "authType", _request.getAuthType());
pair(writer, "characterEncoding", _request.getCharacterEncoding());
pair(writer, "contentLength", _request.getContentLength());
pair(writer, "contentType", _request.getContentType());
pair(writer, "method", _request.getMethod());
pair(writer, "pathInfo", _request.getPathInfo());
pair(writer, "pathTranslated", _request.getPathTranslated());
pair(writer, "protocol", _request.getProtocol());
pair(writer, "queryString", _request.getQueryString());
pair(writer, "remoteAddr", _request.getRemoteAddr());
pair(writer, "remoteHost", _request.getRemoteHost());
pair(writer, "remoteUser", _request.getRemoteUser());
pair(writer, "requestedSessionId", _request.getRequestedSessionId());
pair(writer, "requestedSessionIdFromCookie", _request.isRequestedSessionIdFromCookie());
pair(writer, "requestedSessionIdFromURL", _request.isRequestedSessionIdFromURL());
pair(writer, "requestedSessionIdValid", _request.isRequestedSessionIdValid());
pair(writer, "requestURI", _request.getRequestURI());
pair(writer, "scheme", _request.getScheme());
pair(writer, "serverName", _request.getServerName());
pair(writer, "serverPort", _request.getServerPort());
pair(writer, "contextPath", _request.getContextPath());
pair(writer, "servletPath", _request.getServletPath());
// Now deal with any headers
List headers = getSorted(_request.getHeaderNames());
count = headers.size();
for (int i = 0; i < count; i++)
{
if (i == 0)
{
section(writer, "Headers");
header(writer, "Name", "Value");
}
String name = (String) headers.get(i);
String value = _request.getHeader(name);
pair(writer, name, value);
}
// Attributes
List attributes = getSorted(_request.getAttributeNames());
count = attributes.size();
for (int i = 0; i < count; i++)
{
if (i == 0)
{
section(writer, "Attributes");
header(writer, "Name", "Value");
}
String name = (String) attributes.get(i);
pair(writer, name, _request.getAttribute(name));
}
// Cookies ...
Cookie[] cookies = _request.getCookies();
if (cookies != null)
{
for (int i = 0; i < cookies.length; i++)
{
if (i == 0)
{
section(writer, "Cookies");
header(writer, "Name", "Value");
}
Cookie cookie = cookies[i];
pair(writer, cookie.getName(), cookie.getValue());
} // Cookies loop
}
writer.end(); // Request
object(writer, "Servlet");
writer.begin("table");
writer.attribute("class", "request-context-object");
section(writer, "Properties");
header(writer, "Name", "Value");
pair(writer, "servlet", _servlet);
pair(writer, "name", _servlet.getServletName());
pair(writer, "servletInfo", _servlet.getServletInfo());
ServletConfig config = _servlet.getServletConfig();
List names = getSorted(config.getInitParameterNames());
count = names.size();
for (int i = 0; i < count; i++)
{
if (i == 0)
{
section(writer, "Init Parameters");
header(writer, "Name", "Value");
}
String name = (String) names.get(i);
;
pair(writer, name, config.getInitParameter(name));
}
writer.end(); // Servlet
ServletContext context = config.getServletContext();
object(writer, "Servlet Context");
writer.begin("table");
writer.attribute("class", "request-context-object");
section(writer, "Properties");
header(writer, "Name", "Value");
pair(writer, "majorVersion", context.getMajorVersion());
pair(writer, "minorVersion", context.getMinorVersion());
pair(writer, "serverInfo", context.getServerInfo());
names = getSorted(context.getInitParameterNames());
count = names.size();
for (int i = 0; i < count; i++)
{
if (i == 0)
{
section(writer, "Initial Parameters");
header(writer, "Name", "Value");
}
String name = (String) names.get(i);
pair(writer, name, context.getInitParameter(name));
}
names = getSorted(context.getAttributeNames());
count = names.size();
for (int i = 0; i < count; i++)
{
if (i == 0)
{
section(writer, "Attributes");
header(writer, "Name", "Value");
}
String name = (String) names.get(i);
pair(writer, name, context.getAttribute(name));
}
writer.end(); // Servlet Context
writeSystemProperties(writer);
writer.end("table"); // The enclosing border
}
private void writeSystemProperties(IMarkupWriter writer)
{
Properties properties = null;
object(writer, "JVM System Properties");
try
{
properties = System.getProperties();
}
catch (SecurityException se)
{
writer.print("<p>");
writer.print(se.toString());
return;
}
String pathSeparator = System.getProperty("path.separator", ";");
writer.begin("table");
writer.attribute("class", "request-context-object");
List names = new ArrayList(properties.keySet());
Collections.sort(names);
int count = names.size();
for (int i = 0; i < count; i++)
{
if (i == 0)
header(writer, "Name", "Value");
String name = (String) names.get(i);
String property = properties.getProperty(name);
if (property != null && property.indexOf(pathSeparator) > 0 && name.endsWith(".path"))
{
writer.begin("tr");
writer.attribute("class", getRowClass());
writer.begin("th");
writer.print(name);
writer.end();
writer.begin("td");
writer.begin("ul");
StringTokenizer tokenizer = new StringTokenizer(property, pathSeparator);
while (tokenizer.hasMoreTokens())
{
writer.beginEmpty("li");
writer.print(tokenizer.nextToken());
}
writer.end("tr");
}
else
{
pair(writer, name, property);
}
}
writer.end(); // System Properties
}
/**
* Invokes {@link #write(IMarkupWriter)}, which is used for debugging.
* Does nothing if the cycle is rewinding.
*
**/
public void render(IMarkupWriter writer, IRequestCycle cycle)
{
if (!cycle.isRewinding())
write(writer);
}
/**
* Returns the multipart decoder and lazily decodes the request parameters.
* This allows both for this operation to be performed only when really needed
* and for opening the request for reading much later, so that the Engine can
* have a chance to set the encoding that the request needs to use.
*
* @return the multipart decoder or null if not needed for this request
* @since 3.0
**/
private IMultipartDecoder getDecoder()
{
if (_decoder != null && !_decoded) {
_decoder.decode(_request);
_decoded = true;
}
return _decoder;
}
/**
* Sets the multipart decoder to be used for the request.
*
* @param decoder the multipart decoder
* @since 3.0
**/
public void setDecoder(IMultipartDecoder decoder)
{
_decoder = decoder;
_decoded = false;
}
}