//  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: RequestContext.java 243791 2004-02-19 17:38:13Z hlship $
 *  @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;
    }

}