org.exist.http.urlrewrite.XQueryURLRewrite.java Source code

Java tutorial

Introduction

Here is the source code for org.exist.http.urlrewrite.XQueryURLRewrite.java

Source

/*
 *  eXist Open Source Native XML Database
 *  Copyright (C) 2001-08 Wolfgang M. Meier
 *  wolfgang@exist-db.org
 *  http://exist-db.org
 *
 *  This program 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
 *  of the License, or (at your option) any later version.
 *
 *  This program 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 program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *  $Id$
 */
package org.exist.http.urlrewrite;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;

import org.apache.log4j.Logger;

import org.exist.http.servlets.Authenticator;
import org.exist.http.servlets.BasicAuthenticator;
import org.exist.security.internal.web.HttpAccount;
import org.exist.source.Source;
import org.exist.source.DBSource;
import org.exist.source.SourceFactory;
import org.exist.source.FileSource;
import org.exist.xquery.functions.request.RequestModule;
import org.exist.xquery.functions.response.ResponseModule;
import org.exist.xquery.functions.session.SessionModule;
import org.exist.xquery.*;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.Type;
import org.exist.xquery.value.NodeValue;
import org.exist.Namespaces;
import org.exist.EXistException;
import org.exist.collections.Collection;
import org.exist.dom.DocumentImpl;
import org.exist.dom.BinaryDocument;
import org.exist.xmldb.XmldbURI;
import org.exist.security.*;
import org.exist.security.xacml.AccessContext;
import org.exist.storage.BrokerPool;
import org.exist.storage.DBBroker;
import org.exist.storage.XQueryPool;
import org.exist.storage.lock.Lock;
import org.exist.storage.serializers.Serializer;
import org.exist.util.MimeType;
import org.exist.util.serializer.SAXSerializer;
import org.exist.util.serializer.SerializerPool;
import org.exist.http.servlets.HttpRequestWrapper;
import org.exist.http.servlets.HttpResponseWrapper;
import org.exist.http.Descriptor;

import org.apache.commons.io.output.ByteArrayOutputStream;

import org.w3c.dom.Node;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import org.xml.sax.SAXException;
import org.xmldb.api.base.Database;
import org.xmldb.api.DatabaseManager;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.xml.transform.OutputKeys;

import java.net.URISyntaxException;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * A servlet to redirect HTTP requests. Similar to the popular UrlRewriteFilter, but
 * based on XQuery.
 *
 * The request is passed to an XQuery whose return value determines where the request will be
 * redirected to. An empty return value means the request will be passed through the filter
 * untouched. Otherwise, the query should return a single XML element, which will instruct the filter
 * how to further process the request. Details about the format can be found in the main documentation.
 *
 * The request is forwarded via {@link javax.servlet.RequestDispatcher#forward(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}.
 * Contrary to HTTP forwarding, there is no additional roundtrip to the client. It all happens on
 * the server. The client will not notice the redirect.
 *
 * Please read the <a href="http://exist-db.org/urlrewrite.html">documentation</a> for further information. 
 */
public class XQueryURLRewrite extends HttpServlet {

    private static final Logger LOG = Logger.getLogger(XQueryURLRewrite.class);

    public final static String RQ_ATTR = "org.exist.forward";
    public final static String RQ_ATTR_REQUEST_URI = "org.exist.forward.request-uri";
    public final static String RQ_ATTR_SERVLET_PATH = "org.exist.forward.servlet-path";
    public final static String RQ_ATTR_RESULT = "org.exist.forward.result";
    public final static String RQ_ATTR_ERROR = "org.exist.forward.error";

    public final static String DRIVER = "org.exist.xmldb.DatabaseImpl";

    private final static Pattern NAME_REGEX = Pattern.compile("^.*/([^/]+)$", 0);

    private ServletConfig config;

    private final Map<String, ModelAndView> urlCache = Collections
            .synchronizedMap(new TreeMap<String, ModelAndView>());

    protected Subject defaultUser = null;
    protected BrokerPool pool;

    // path to the query
    private String query = null;

    //private boolean checkModified = true;

    private boolean compiledCache = true;

    private RewriteConfig rewriteConfig;

    private Authenticator authenticator;

    @Override
    public void init(ServletConfig filterConfig) throws ServletException {
        // save FilterConfig for later use
        this.config = filterConfig;

        query = filterConfig.getInitParameter("xquery");

        //        String opt = filterConfig.getInitParameter("check-modified");
        //        if (opt != null)
        //            checkModified = opt != null && opt.equalsIgnoreCase("true");

        final String opt = filterConfig.getInitParameter("compiled-cache");
        if (opt != null) {
            compiledCache = opt != null && opt.equalsIgnoreCase("true");
        }

    }

    @Override
    protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
            throws IOException, ServletException {
        if (rewriteConfig == null) {
            configure();
            rewriteConfig = new RewriteConfig(this);
        }

        final long start = System.currentTimeMillis();
        final HttpServletRequest request = servletRequest;
        final HttpServletResponse response = servletResponse;

        if (LOG.isTraceEnabled()) {
            LOG.trace(request.getRequestURI());
        }
        final Descriptor descriptor = Descriptor.getDescriptorSingleton();
        if (descriptor != null && descriptor.requestsFiltered()) {
            final String attr = (String) request.getAttribute("XQueryURLRewrite.forwarded");
            if (attr == null) {
                //                request = new HttpServletRequestWrapper(request, /*formEncoding*/ "utf-8" );
                //logs the request if specified in the descriptor
                descriptor.doLogRequestInReplayLog(request);

                request.setAttribute("XQueryURLRewrite.forwarded", "true");
            }
        }

        Subject user = defaultUser;

        Subject requestUser = HttpAccount.getUserFromServletRequest(request);
        if (requestUser != null) {
            user = requestUser;
        } else {
            // Secondly try basic authentication
            final String auth = request.getHeader("Authorization");
            if (auth != null) {
                requestUser = authenticator.authenticate(request, response);
                if (requestUser != null) {
                    user = requestUser;
                }
            }
        }

        try {
            configure();
            //checkCache(user);

            final RequestWrapper modifiedRequest = new RequestWrapper(request);
            final URLRewrite staticRewrite = rewriteConfig.lookup(modifiedRequest);
            if (staticRewrite != null && !staticRewrite.isControllerForward()) {
                modifiedRequest.setPaths(staticRewrite.resolve(modifiedRequest), staticRewrite.getPrefix());

                if (LOG.isTraceEnabled()) {
                    LOG.trace("Forwarding to target: " + staticRewrite.getTarget());
                }
                staticRewrite.doRewrite(modifiedRequest, response);
            } else {

                if (LOG.isTraceEnabled()) {
                    LOG.trace("Processing request URI: " + request.getRequestURI());
                }
                if (staticRewrite != null) {
                    // fix the request URI
                    staticRewrite.updateRequest(modifiedRequest);
                }

                // check if the request URI is already in the url cache
                ModelAndView modelView = getFromCache(request.getHeader("Host") + request.getRequestURI(), user);

                if (LOG.isDebugEnabled()) {
                    LOG.debug("Checked cache for URI: " + modifiedRequest.getRequestURI() + " original: "
                            + request.getRequestURI());
                }
                // no: create a new model and view configuration
                if (modelView == null) {
                    modelView = new ModelAndView();
                    // Execute the query
                    Sequence result = Sequence.EMPTY_SEQUENCE;
                    DBBroker broker = null;
                    try {
                        broker = pool.get(user);
                        modifiedRequest.setAttribute(RQ_ATTR_REQUEST_URI, request.getRequestURI());

                        final Properties outputProperties = new Properties();

                        outputProperties.setProperty(OutputKeys.INDENT, "yes");
                        outputProperties.setProperty(OutputKeys.ENCODING, "UTF-8");
                        outputProperties.setProperty(OutputKeys.MEDIA_TYPE, MimeType.XML_TYPE.getName());

                        result = runQuery(broker, modifiedRequest, response, modelView, staticRewrite,
                                outputProperties);

                        logResult(broker, result);

                        if (response.isCommitted()) {
                            return;
                        }

                        // process the query result
                        if (result.getItemCount() == 1) {
                            final Item resource = result.itemAt(0);
                            if (!Type.subTypeOf(resource.getType(), Type.NODE)) {
                                throw new ServletException(
                                        "XQueryURLRewrite: urlrewrite query should return an element!");
                            }
                            Node node = ((NodeValue) resource).getNode();
                            if (node.getNodeType() == Node.DOCUMENT_NODE) {
                                node = ((Document) node).getDocumentElement();
                            }
                            if (node.getNodeType() != Node.ELEMENT_NODE) {
                                //throw new ServletException("Redirect XQuery should return an XML element!");
                                response(broker, response, outputProperties, result);
                                return;
                            }
                            Element elem = (Element) node;
                            if (!(Namespaces.EXIST_NS.equals(elem.getNamespaceURI()))) {
                                response(broker, response, outputProperties, result);
                                return;
                                //                            throw new ServletException("Redirect XQuery should return an element in namespace " + Namespaces.EXIST_NS);
                            }

                            if (Namespaces.EXIST_NS.equals(elem.getNamespaceURI())
                                    && "dispatch".equals(elem.getLocalName())) {
                                node = elem.getFirstChild();
                                while (node != null) {
                                    if (node.getNodeType() == Node.ELEMENT_NODE
                                            && Namespaces.EXIST_NS.equals(node.getNamespaceURI())) {
                                        final Element action = (Element) node;
                                        if ("view".equals(action.getLocalName())) {
                                            parseViews(modifiedRequest, action, modelView);
                                        } else if ("error-handler".equals(action.getLocalName())) {
                                            parseErrorHandlers(modifiedRequest, action, modelView);
                                        } else if ("cache-control".equals(action.getLocalName())) {
                                            final String option = action.getAttribute("cache");
                                            modelView.setUseCache("yes".equals(option));
                                        } else {
                                            final URLRewrite urw = parseAction(modifiedRequest, action);
                                            if (urw != null) {
                                                modelView.setModel(urw);
                                            }
                                        }
                                    }
                                    node = node.getNextSibling();
                                }
                                if (modelView.getModel() == null) {
                                    modelView.setModel(new PassThrough(config, elem, modifiedRequest));
                                }
                            } else if (Namespaces.EXIST_NS.equals(elem.getNamespaceURI())
                                    && "ignore".equals(elem.getLocalName())) {
                                modelView.setModel(new PassThrough(config, elem, modifiedRequest));
                                final NodeList nl = elem.getElementsByTagNameNS(Namespaces.EXIST_NS,
                                        "cache-control");
                                if (nl.getLength() > 0) {
                                    elem = (Element) nl.item(0);
                                    final String option = elem.getAttribute("cache");
                                    modelView.setUseCache("yes".equals(option));
                                }
                            } else {
                                response(broker, response, outputProperties, result);
                                return;
                            }
                        } else if (result.getItemCount() > 1) {
                            response(broker, response, outputProperties, result);
                            return;
                        }

                        if (modelView.useCache()) {
                            LOG.debug("Caching request to " + request.getRequestURI());
                            urlCache.put(modifiedRequest.getHeader("Host") + request.getRequestURI(), modelView);
                        }

                    } finally {
                        pool.release(broker);
                    }

                    // store the original request URI to org.exist.forward.request-uri
                    modifiedRequest.setAttribute(RQ_ATTR_REQUEST_URI, request.getRequestURI());
                    modifiedRequest.setAttribute(RQ_ATTR_SERVLET_PATH, request.getServletPath());

                }
                if (LOG.isTraceEnabled()) {
                    LOG.trace("URLRewrite took " + (System.currentTimeMillis() - start) + "ms.");
                }
                final HttpServletResponse wrappedResponse = new CachingResponseWrapper(response,
                        modelView.hasViews() || modelView.hasErrorHandlers());
                if (modelView.getModel() == null) {
                    modelView.setModel(new PassThrough(config, modifiedRequest));
                }

                if (staticRewrite != null) {
                    if (modelView.getModel().doResolve()) {
                        staticRewrite.rewriteRequest(modifiedRequest);
                    } else {
                        modelView.getModel().setAbsolutePath(modifiedRequest);
                    }
                }
                modifiedRequest.allowCaching(!modelView.hasViews());
                doRewrite(modelView.getModel(), modifiedRequest, wrappedResponse);

                int status = ((CachingResponseWrapper) wrappedResponse).getStatus();
                if (status == HttpServletResponse.SC_NOT_MODIFIED) {
                    response.flushBuffer();
                } else if (status < 400) {
                    if (modelView.hasViews()) {
                        applyViews(modelView, modelView.views, response, modifiedRequest, wrappedResponse);
                    } else {
                        ((CachingResponseWrapper) wrappedResponse).flush();
                    }
                } else {
                    // HTTP response code indicates an error
                    if (modelView.hasErrorHandlers()) {
                        final byte[] data = ((CachingResponseWrapper) wrappedResponse).getData();
                        if (data != null) {
                            modifiedRequest.setAttribute(RQ_ATTR_ERROR, new String(data, UTF_8));
                        }
                        applyViews(modelView, modelView.errorHandlers, response, modifiedRequest, wrappedResponse);
                    } else {
                        flushError(response, wrappedResponse);
                    }
                }
            }
            //            Sequence result;
            //            if ((result = (Sequence) request.getAttribute(RQ_ATTR_RESULT)) != null) {
            //                writeResults(response, broker, result);
            //            }
        } catch (final Throwable e) {
            LOG.error("Error while processing " + servletRequest.getRequestURI() + ": " + e.getMessage(), e);
            throw new ServletException("An error occurred while processing request to "
                    + servletRequest.getRequestURI() + ": " + e.getMessage(), e);

        }
    }

    private void applyViews(ModelAndView modelView, List<URLRewrite> views, HttpServletResponse response,
            RequestWrapper modifiedRequest, HttpServletResponse currentResponse)
            throws IOException, ServletException {
        int status;
        HttpServletResponse wrappedResponse = currentResponse;
        for (int i = 0; i < views.size(); i++) {
            final URLRewrite view = (URLRewrite) views.get(i);

            // get data returned from last action
            byte[] data = ((CachingResponseWrapper) wrappedResponse).getData();
            // determine request method to use for calling view
            String method = view.getMethod();
            if (method == null) {
                method = "POST"; // default is POST
            }

            final RequestWrapper wrappedReq = new RequestWrapper(modifiedRequest);
            wrappedReq.allowCaching(false);
            wrappedReq.setMethod(method);
            wrappedReq.setBasePath(modifiedRequest.getBasePath());
            wrappedReq.setCharacterEncoding(wrappedResponse.getCharacterEncoding());
            wrappedReq.setContentType(wrappedResponse.getContentType());

            if (data != null) {
                wrappedReq.setData(data);
            }

            wrappedResponse = new CachingResponseWrapper(response, true);
            doRewrite(view, wrappedReq, wrappedResponse);

            // catch errors in the view
            status = ((CachingResponseWrapper) wrappedResponse).getStatus();
            if (status >= 400) {
                if (modelView != null && modelView.hasErrorHandlers()) {
                    data = ((CachingResponseWrapper) wrappedResponse).getData();
                    final String msg = data == null ? "" : new String(data, UTF_8);
                    modifiedRequest.setAttribute(RQ_ATTR_ERROR, msg);
                    applyViews(null, modelView.errorHandlers, response, modifiedRequest, wrappedResponse);
                    break;
                } else {
                    flushError(response, wrappedResponse);
                }
                break;
            } else if (i == views.size() - 1) {
                ((CachingResponseWrapper) wrappedResponse).flush();
            }
        }
    }

    private void response(DBBroker broker, HttpServletResponse response, Properties outputProperties,
            Sequence resultSequence) throws IOException {

        final String encoding = outputProperties.getProperty(OutputKeys.ENCODING);
        final ServletOutputStream sout = response.getOutputStream();
        final PrintWriter output = new PrintWriter(new OutputStreamWriter(sout, encoding));
        if (!response.containsHeader("Content-Type")) {
            String mimeType = outputProperties.getProperty(OutputKeys.MEDIA_TYPE);
            if (mimeType != null) {
                final int semicolon = mimeType.indexOf(';');
                if (semicolon != Constants.STRING_NOT_FOUND) {
                    mimeType = mimeType.substring(0, semicolon);
                }
                response.setContentType(mimeType + "; charset=" + encoding);
            }
        }

        //        response.addHeader( "pragma", "no-cache" );
        //        response.addHeader( "Cache-Control", "no-cache" );

        final Serializer serializer = broker.getSerializer();
        serializer.reset();

        final SerializerPool serializerPool = SerializerPool.getInstance();

        final SAXSerializer sax = (SAXSerializer) serializerPool.borrowObject(SAXSerializer.class);
        try {
            sax.setOutput(output, outputProperties);

            serializer.setProperties(outputProperties);
            serializer.setSAXHandlers(sax, sax);
            serializer.toSAX(resultSequence, 1, resultSequence.getItemCount(), false, false);

        } catch (final SAXException e) {
            throw new IOException(e);
        } finally {
            serializerPool.returnObject(sax);
        }
        output.flush();
        output.close();
    }

    private void flushError(HttpServletResponse response, HttpServletResponse wrappedResponse) throws IOException {
        if (!response.isCommitted()) {
            final byte[] data = ((CachingResponseWrapper) wrappedResponse).getData();
            if (data != null) {
                response.setContentType(wrappedResponse.getContentType());
                response.setCharacterEncoding(wrappedResponse.getCharacterEncoding());
                response.getOutputStream().write(data);
                response.flushBuffer();
            }
        }
    }

    private ModelAndView getFromCache(String url, Subject user)
            throws EXistException, ServletException, PermissionDeniedException {
        /* Make sure we have a broker *before* we synchronize on urlCache or we may run
         * into a deadlock situation (with method checkCache)
         */
        final ModelAndView model = urlCache.get(url);
        if (model == null) {
            return null;
        }

        DBBroker broker = null;
        try {
            broker = pool.get(user);

            model.getSourceInfo().source.validate(broker.getSubject(), Permission.EXECUTE);

            if (model.getSourceInfo().source.isValid(broker) != Source.VALID) {
                ModelAndView removed = urlCache.remove(url);
                return null;
            }

            if (LOG.isDebugEnabled()) {
                LOG.debug("Using cached entry for " + url);
            }
            return model;
        } finally {
            pool.release(broker);
        }
    }

    //    private void checkCache(Subject user) throws EXistException {
    //        if (checkModified) {
    //            // check if any of the currently used sources has been updated
    //           DBBroker broker = null;
    //            try {
    //                broker = pool.get(user);
    //
    //                for (Entry<ModelAndView, Source> entry : sources.entrySet() )
    //                 if (entry.getValue().isValid(broker) != Source.VALID)
    //                    urlCache.remove(entry.getKey());
    //
    //            } finally {
    //                pool.release(broker);
    //            }
    //        }
    //    }

    protected void clearCaches() throws EXistException {
        urlCache.clear();
    }

    /**
     * Process a rewrite action. Method checks if the target path is mapped
     * to another action in controller-config.xml. If yes, replaces the current action
     * with the new action.
     *
     * @param action
     * @param request
     * @param response
     * @throws IOException
     * @throws ServletException
     */
    protected void doRewrite(URLRewrite action, RequestWrapper request, HttpServletResponse response)
            throws IOException, ServletException {
        if (action.getTarget() != null && !(action instanceof Redirect)) {
            final String uri = action.resolve(request);
            URLRewrite staticRewrite = rewriteConfig.lookup(uri, request.getServerName(), true, action);

            if (staticRewrite != null) {
                staticRewrite.copyFrom(action);
                action = staticRewrite;
                RequestWrapper modifiedRequest = new RequestWrapper(request);
                modifiedRequest.setPaths(uri, action.getPrefix());

                if (LOG.isTraceEnabled()) {
                    LOG.trace("Forwarding to : " + action.toString() + " url: " + action.getURI());
                }
                request = modifiedRequest;
            }
        }
        action.prepareRequest(request);
        action.doRewrite(request, response);
    }

    protected ServletConfig getConfig() {
        return config;
    }

    private URLRewrite parseAction(HttpServletRequest request, Element action) throws ServletException {
        URLRewrite rewrite = null;
        if ("forward".equals(action.getLocalName())) {
            rewrite = new PathForward(config, action, request.getRequestURI());
        } else if ("redirect".equals(action.getLocalName())) {
            rewrite = new Redirect(action, request.getRequestURI());
            //        } else if ("call".equals(action.getLocalName())) {
            //            rewrite = new ModuleCall(action, queryContext, request.getRequestURI());
        }
        return rewrite;
    }

    private void parseViews(HttpServletRequest request, Element view, ModelAndView modelView)
            throws ServletException {
        Node node = view.getFirstChild();
        while (node != null) {
            if (node.getNodeType() == Node.ELEMENT_NODE && Namespaces.EXIST_NS.equals(node.getNamespaceURI())) {
                final URLRewrite urw = parseAction(request, (Element) node);
                if (urw != null) {
                    modelView.addView(urw);
                }
            }
            node = node.getNextSibling();
        }
    }

    private void parseErrorHandlers(HttpServletRequest request, Element view, ModelAndView modelView)
            throws ServletException {
        Node node = view.getFirstChild();
        while (node != null) {
            if (node.getNodeType() == Node.ELEMENT_NODE && Namespaces.EXIST_NS.equals(node.getNamespaceURI())) {
                final URLRewrite urw = parseAction(request, (Element) node);
                if (urw != null) {
                    modelView.addErrorHandler(urw);
                }
            }
            node = node.getNextSibling();
        }
    }

    private void configure() throws ServletException {
        if (pool != null) {
            return;
        }
        try {
            final Class<?> driver = Class.forName(DRIVER);
            final Database database = (Database) driver.newInstance();
            database.setProperty("create-database", "true");
            DatabaseManager.registerDatabase(database);
            LOG.debug("Initialized database");
        } catch (final Exception e) {
            final String errorMessage = "Failed to initialize database driver";
            LOG.error(errorMessage, e);
            throw new ServletException(errorMessage + ": " + e.getMessage(), e);
        }

        try {
            pool = BrokerPool.getInstance();
        } catch (final EXistException e) {
            throw new ServletException("Could not intialize db: " + e.getMessage(), e);
        }

        defaultUser = pool.getSecurityManager().getGuestSubject();

        final String username = config.getInitParameter("user");
        if (username != null) {
            final String password = config.getInitParameter("password");
            try {
                Subject user = pool.getSecurityManager().authenticate(username, password);
                if (user != null && user.isAuthenticated()) {
                    defaultUser = user;
                }
            } catch (final AuthenticationException e) {
                LOG.error("User can not be authenticated (" + username + "), using default user.");
            }
        }
        authenticator = new BasicAuthenticator(pool);
    }

    private void logResult(DBBroker broker, Sequence result) throws IOException, SAXException {
        if (LOG.isTraceEnabled() && result.getItemCount() > 0) {
            final Serializer serializer = broker.getSerializer();
            serializer.reset();

            final Item item = result.itemAt(0);
            if (Type.subTypeOf(item.getType(), Type.NODE)) {
                LOG.trace(serializer.serialize((NodeValue) item));
            }
        }
    }

    @Override
    public void destroy() {
        config = null;
    }

    private SourceInfo getSourceInfo(DBBroker broker, RequestWrapper request, URLRewrite staticRewrite)
            throws ServletException {
        final String moduleLoadPath = config.getServletContext().getRealPath("/");
        final String basePath = staticRewrite == null ? "." : staticRewrite.getTarget();
        if (basePath == null) {
            return getSource(broker, moduleLoadPath);
        } else {
            return findSource(request, broker, basePath);
        }
    }

    private Sequence runQuery(DBBroker broker, RequestWrapper request, HttpServletResponse response,
            ModelAndView model, URLRewrite staticRewrite, Properties outputProperties)
            throws ServletException, XPathException, PermissionDeniedException {
        // Try to find the XQuery
        final SourceInfo sourceInfo = getSourceInfo(broker, request, staticRewrite);

        if (sourceInfo == null) {
            return Sequence.EMPTY_SEQUENCE; // no controller found
        }
        final String basePath = staticRewrite == null ? "." : staticRewrite.getTarget();

        final XQuery xquery = broker.getXQueryService();
        final XQueryPool xqyPool = xquery.getXQueryPool();

        CompiledXQuery compiled = null;
        if (compiledCache) {
            compiled = xqyPool.borrowCompiledXQuery(broker, sourceInfo.source);
        }
        XQueryContext queryContext;
        if (compiled == null) {
            queryContext = xquery.newContext(AccessContext.REST);
        } else {
            queryContext = compiled.getContext();
        }
        // Find correct module load path
        queryContext.setModuleLoadPath(sourceInfo.moduleLoadPath);
        declareVariables(queryContext, sourceInfo, staticRewrite, basePath, request, response);
        if (compiled == null) {
            try {
                compiled = xquery.compile(queryContext, sourceInfo.source);
            } catch (final IOException e) {
                throw new ServletException("Failed to read query from " + query, e);
            }
        }
        model.setSourceInfo(sourceInfo);

        //      This used by controller.xql only ?
        //      String xdebug = request.getParameter("XDEBUG_SESSION_START");
        //      if (xdebug != null)
        //         compiled.getContext().setDebugMode(true);

        //      outputProperties.put("base-uri", collectionURI.toString());

        try {
            return xquery.execute(compiled, null, outputProperties);
        } finally {
            queryContext.runCleanupTasks();
            xqyPool.returnCompiledXQuery(sourceInfo.source, compiled);
        }
    }

    protected String adjustPathForSourceLookup(String basePath, String path) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("request path=" + path);
        }
        if (basePath.startsWith(XmldbURI.EMBEDDED_SERVER_URI_PREFIX)
                && path.startsWith(basePath.replace(XmldbURI.EMBEDDED_SERVER_URI_PREFIX, ""))) {
            path = path.replace(basePath.replace(XmldbURI.EMBEDDED_SERVER_URI_PREFIX, ""), "");

        } else if (path.startsWith("/db/")) {
            path = path.substring(4);
        }

        if (path.startsWith("/")) {
            path = path.substring(1);
        }

        if (LOG.isTraceEnabled()) {
            LOG.trace("adjusted request path=" + path);
        }
        return path;
    }

    private SourceInfo findSource(HttpServletRequest request, DBBroker broker, String basePath)
            throws ServletException {
        final String requestURI = request.getRequestURI();
        String path = requestURI.substring(request.getContextPath().length());

        if (LOG.isTraceEnabled()) {
            LOG.trace("basePath=" + basePath);
        }
        path = adjustPathForSourceLookup(basePath, path);

        final String[] components = path.split("/");
        SourceInfo sourceInfo = null;
        if (basePath.startsWith(XmldbURI.XMLDB_URI_PREFIX)) {
            if (LOG.isTraceEnabled()) {
                LOG.trace("Looking for controller.xql in the database, starting from: " + basePath);
            }
            try {
                final XmldbURI locationUri = XmldbURI.xmldbUriFor(basePath);
                final Collection collection = broker.openCollection(locationUri, Lock.READ_LOCK);
                if (collection == null) {
                    LOG.warn("Controller base collection not found: " + basePath);
                    return null;
                }

                Collection subColl = collection;
                DocumentImpl controllerDoc = null;
                for (int i = 0; i < components.length; i++) {
                    DocumentImpl doc = null;
                    try {
                        if (components[i].length() > 0
                                && subColl.hasChildCollection(broker, XmldbURI.createInternal(components[i]))) {
                            final XmldbURI newSubCollURI = subColl.getURI().append(components[i]);
                            if (LOG.isTraceEnabled()) {
                                LOG.trace("Inspecting sub-collection: " + newSubCollURI);
                            }
                            subColl = broker.openCollection(newSubCollURI, Lock.READ_LOCK);
                            if (subColl != null) {
                                if (LOG.isTraceEnabled()) {
                                    LOG.trace("Looking for controller.xql in " + subColl.getURI());
                                }
                                final XmldbURI docUri = subColl.getURI().append("controller.xql");
                                doc = broker.getXMLResource(docUri, Lock.READ_LOCK);
                                if (doc != null) {
                                    if (controllerDoc != null) {
                                        controllerDoc.getUpdateLock().release(Lock.READ_LOCK);
                                    }
                                    controllerDoc = doc;
                                }
                            } else {
                                break;
                            }
                        } else {
                            break;
                        }
                    } catch (final PermissionDeniedException e) {
                        LOG.debug("Permission denied while scanning for XQueryURLRewrite controllers: "
                                + e.getMessage(), e);
                        break;
                    } catch (final Exception e) {
                        LOG.debug("Bad collection URI: " + path);
                        break;

                    } finally {
                        if (doc != null && controllerDoc == null) {
                            doc.getUpdateLock().release(Lock.READ_LOCK);
                        }

                        if (subColl != null && subColl != collection) {
                            subColl.getLock().release(Lock.READ_LOCK);
                        }
                    }
                }
                collection.getLock().release(Lock.READ_LOCK);
                if (controllerDoc == null) {
                    try {
                        final XmldbURI docUri = collection.getURI().append("controller.xql");
                        controllerDoc = broker.getXMLResource(docUri, Lock.READ_LOCK);
                    } catch (final PermissionDeniedException e) {
                        LOG.debug("Permission denied while scanning for XQueryURLRewrite controllers: "
                                + e.getMessage(), e);
                    }
                }
                if (controllerDoc == null) {
                    LOG.warn("XQueryURLRewrite controller could not be found for path: " + path);
                    return null;
                }

                if (LOG.isTraceEnabled()) {
                    LOG.trace("Found controller file: " + controllerDoc.getURI());
                }
                try {
                    if (controllerDoc.getResourceType() != DocumentImpl.BINARY_FILE
                            || !"application/xquery".equals(controllerDoc.getMetadata().getMimeType())) {
                        LOG.warn("XQuery resource: " + query + " is not an XQuery or "
                                + "declares a wrong mime-type");
                        return null;
                    }
                    final String controllerPath = controllerDoc.getCollection().getURI().getRawCollectionPath();

                    sourceInfo = new SourceInfo(new DBSource(broker, (BinaryDocument) controllerDoc, true),
                            "xmldb:exist://" + controllerPath);
                    sourceInfo.controllerPath = controllerPath.substring(locationUri.getCollectionPath().length());

                    return sourceInfo;
                } finally {
                    if (controllerDoc != null) {
                        controllerDoc.getUpdateLock().release(Lock.READ_LOCK);
                    }
                }
            } catch (final URISyntaxException e) {
                LOG.warn("Bad URI for base path: " + e.getMessage(), e);
                return null;
            } catch (final PermissionDeniedException e) {
                LOG.debug("Permission denied while scanning for XQueryURLRewrite controllers: " + e.getMessage(),
                        e);
                return null;
            }
        } else {
            if (LOG.isTraceEnabled()) {
                LOG.trace("Looking for controller.xql in the filesystem, starting from: " + basePath);
            }
            final String realPath = config.getServletContext().getRealPath(basePath);
            final File baseDir = new File(realPath);
            if (!baseDir.isDirectory()) {
                LOG.warn("Base path for XQueryURLRewrite does not point to a directory");
                return null;
            }

            File controllerFile = null;
            File subDir = baseDir;
            for (int i = 0; i < components.length; i++) {
                if (components[i].length() > 0) {
                    subDir = new File(subDir, components[i]);
                    if (subDir.isDirectory()) {
                        File cf = new File(subDir, "controller.xql");
                        if (cf.canRead()) {
                            controllerFile = cf;
                        }
                    } else {
                        break;
                    }
                }
            }
            if (controllerFile == null) {
                File cf = new File(baseDir, "controller.xql");
                if (cf.canRead()) {
                    controllerFile = cf;
                }
            }
            if (controllerFile == null) {
                LOG.warn("XQueryURLRewrite controller could not be found");
                return null;
            }
            if (LOG.isTraceEnabled()) {
                LOG.trace("Found controller file: " + controllerFile.getAbsolutePath());
            }
            final String parentPath = controllerFile.getParentFile().getAbsolutePath();

            sourceInfo = new SourceInfo(new FileSource(controllerFile, "UTF-8", true), parentPath);
            sourceInfo.controllerPath = parentPath.substring(baseDir.getAbsolutePath().length());
            // replace windows path separators
            sourceInfo.controllerPath = sourceInfo.controllerPath.replace('\\', '/');

            return sourceInfo;
        }
    }

    private SourceInfo getSource(DBBroker broker, String moduleLoadPath) throws ServletException {
        SourceInfo sourceInfo;
        if (query.startsWith(XmldbURI.XMLDB_URI_PREFIX)) {
            // Is the module source stored in the database?
            try {
                final XmldbURI locationUri = XmldbURI.xmldbUriFor(query);
                DocumentImpl sourceDoc = null;
                try {
                    sourceDoc = broker.getXMLResource(locationUri.toCollectionPathURI(), Lock.READ_LOCK);
                    if (sourceDoc == null) {
                        throw new ServletException("XQuery resource: " + query + " not found in database");
                    }
                    if (sourceDoc.getResourceType() != DocumentImpl.BINARY_FILE
                            || !"application/xquery".equals(sourceDoc.getMetadata().getMimeType())) {
                        throw new ServletException("XQuery resource: " + query + " is not an XQuery or "
                                + "declares a wrong mime-type");
                    }
                    sourceInfo = new SourceInfo(new DBSource(broker, (BinaryDocument) sourceDoc, true),
                            locationUri.toString());
                } catch (final PermissionDeniedException e) {
                    throw new ServletException("permission denied to read module source from " + query);
                } finally {
                    if (sourceDoc != null) {
                        sourceDoc.getUpdateLock().release(Lock.READ_LOCK);
                    }
                }
            } catch (final URISyntaxException e) {
                throw new ServletException(e.getMessage(), e);
            }
        } else {
            try {
                sourceInfo = new SourceInfo(SourceFactory.getSource(broker, moduleLoadPath, query, true),
                        moduleLoadPath);
            } catch (final IOException e) {
                throw new ServletException("IO error while reading XQuery source: " + query);
            } catch (final PermissionDeniedException e) {
                throw new ServletException("Permission denied while reading XQuery source: " + query);
            }
        }
        return sourceInfo;
    }

    private void declareVariables(XQueryContext context, SourceInfo sourceInfo, URLRewrite staticRewrite,
            String basePath, RequestWrapper request, HttpServletResponse response) throws XPathException {
        final HttpRequestWrapper reqw = new HttpRequestWrapper(request, "UTF-8", "UTF-8", false);
        final HttpResponseWrapper respw = new HttpResponseWrapper(response);
        // context.declareNamespace(RequestModule.PREFIX,
        // RequestModule.NAMESPACE_URI);
        context.declareVariable(RequestModule.PREFIX + ":request", reqw);
        context.declareVariable(ResponseModule.PREFIX + ":response", respw);
        context.declareVariable(SessionModule.PREFIX + ":session", reqw.getSession(false));

        context.declareVariable("exist:controller", sourceInfo.controllerPath);
        request.setAttribute("$exist:controller", sourceInfo.controllerPath);
        context.declareVariable("exist:root", basePath);
        request.setAttribute("$exist:root", basePath);
        context.declareVariable("exist:context", request.getContextPath());
        request.setAttribute("$exist:context", request.getContextPath());
        final String prefix = staticRewrite == null ? null : staticRewrite.getPrefix();
        context.declareVariable("exist:prefix", prefix == null ? "" : prefix);
        request.setAttribute("$exist:prefix", prefix == null ? "" : prefix);
        String path;
        if (sourceInfo.controllerPath.length() > 0 && !"/".equals(sourceInfo.controllerPath)) {
            path = request.getInContextPath().substring(sourceInfo.controllerPath.length());
        } else {
            path = request.getInContextPath();
        }
        final int p = path.lastIndexOf(';');
        if (p != Constants.STRING_NOT_FOUND) {
            path = path.substring(0, p);
        }
        context.declareVariable("exist:path", path);
        request.setAttribute("$exist:path", path);

        String resource = "";
        final Matcher nameMatcher = NAME_REGEX.matcher(path);
        if (nameMatcher.matches()) {
            resource = nameMatcher.group(1);
        }
        context.declareVariable("exist:resource", resource);
        request.setAttribute("$exist:resource", resource);

        if (LOG.isDebugEnabled()) {
            LOG.debug("\nexist:path = " + path + "\nexist:resource = " + resource + "\nexist:controller = "
                    + sourceInfo.controllerPath);
        }
    }

    private class ModelAndView {

        URLRewrite rewrite = null;
        List<URLRewrite> views = new LinkedList<URLRewrite>();
        List<URLRewrite> errorHandlers = null;
        boolean useCache = false;
        SourceInfo sourceInfo = null;

        private ModelAndView() {
        }

        public void setSourceInfo(SourceInfo sourceInfo) {
            this.sourceInfo = sourceInfo;
        }

        public SourceInfo getSourceInfo() {
            return sourceInfo;
        }

        public void setModel(URLRewrite model) {
            this.rewrite = model;
        }

        public URLRewrite getModel() {
            return rewrite;
        }

        public void addErrorHandler(URLRewrite handler) {
            if (errorHandlers == null) {
                errorHandlers = new LinkedList<URLRewrite>();
            }
            errorHandlers.add(handler);
        }

        public void addView(URLRewrite view) {
            views.add(view);
        }

        public boolean hasViews() {
            return views.size() > 0;
        }

        public boolean hasErrorHandlers() {
            return errorHandlers != null && errorHandlers.size() > 0;
        }

        public boolean useCache() {
            return useCache;
        }

        public void setUseCache(boolean useCache) {
            this.useCache = useCache;
        }
    }

    private static class SourceInfo {

        Source source;
        String controllerPath = "";
        String moduleLoadPath;

        private SourceInfo(Source source, String moduleLoadPath) {
            this.source = source;
            this.moduleLoadPath = moduleLoadPath;
        }
    }

    public static class RequestWrapper extends javax.servlet.http.HttpServletRequestWrapper {

        Map<String, List<String>> addedParams = new HashMap<String, List<String>>();

        Map attributes = new HashMap();

        ServletInputStream sis = null;
        BufferedReader reader = null;

        String contentType = null;
        int contentLength = 0;
        String characterEncoding = null;
        String method = null;
        String inContextPath = null;
        String servletPath;
        String basePath = null;
        boolean allowCaching = true;

        private void addNameValue(String name, String value, Map<String, List<String>> map) {
            List<String> values = map.get(name);
            if (values == null) {
                values = new ArrayList<String>();
            }
            values.add(value);
            map.put(name, values);
        }

        protected RequestWrapper(HttpServletRequest request) {
            super(request);

            // copy parameters
            for (final Map.Entry<String, String[]> param : (Set<Map.Entry<String, String[]>>) request
                    .getParameterMap().entrySet()) {
                for (final String paramValue : param.getValue()) {
                    addNameValue(param.getKey(), paramValue, addedParams);
                }
            }
            /*for(Enumeration<String> e = request.getParameterNames(); e.hasMoreElements(); ) {
                
            String key = e.nextElement();
            String[] value = request.getParameterValues(key);
            addedParams.put(key, value);
            }*/

            contentType = request.getContentType();
        }

        protected void allowCaching(boolean cache) {
            this.allowCaching = cache;
        }

        @Override
        public String getRequestURI() {
            String uri = inContextPath == null ? super.getRequestURI() : getContextPath() + inContextPath;

            // Strip jsessionid from uris. New behavior of jetty
            // see jira.codehaus.org/browse/JETTY-1146
            final int pos = uri.indexOf(";jsessionid=");
            if (pos > 0) {
                uri = uri.substring(0, pos);
            }

            return uri;
        }

        public String getInContextPath() {
            if (inContextPath == null) {
                return getRequestURI().substring(getContextPath().length());
            }
            return inContextPath;
        }

        public void setInContextPath(String path) {
            inContextPath = path;
        }

        @Override
        public String getMethod() {
            if (method == null) {
                return super.getMethod();
            }
            return method;
        }

        public void setMethod(String method) {
            this.method = method;
        }

        /**
         * Change the requestURI and the servletPath
         *
         * @param requestURI the URI of the request without the context path
         * @param servletPath the servlet path
         */
        public void setPaths(String requestURI, String servletPath) {
            this.inContextPath = requestURI;
            if (servletPath == null) {
                this.servletPath = requestURI;
            } else {
                this.servletPath = servletPath;
            }
        }

        public void setBasePath(String base) {
            this.basePath = base;
        }

        public String getBasePath() {
            return basePath;
        }

        /**
         * Change the base path of the request, e.g. if the original request pointed
         * to /fs/foo/baz, but the request should be forwarded to /foo/baz.
         *
         * @param base the base path to remove
         */
        public void removePathPrefix(String base) {
            setPaths(getInContextPath().substring(base.length()),
                    servletPath != null ? servletPath.substring(base.length()) : null);
        }

        @Override
        public String getServletPath() {
            return servletPath == null ? super.getServletPath() : servletPath;
        }

        @Override
        public String getPathInfo() {
            final String path = getInContextPath();
            final String sp = getServletPath();
            if (sp == null) {
                return null;
            }
            if (path.length() < sp.length()) {
                LOG.error("Internal error: servletPath = " + sp + " is longer than path = " + path);
                return null;
            }
            return path.length() == sp.length() ? null : path.substring(sp.length());
        }

        @Override
        public String getPathTranslated() {
            final String pathInfo = getPathInfo();
            if (pathInfo == null) {
                super.getPathTranslated();
            }
            if (pathInfo == null) {
                return (null);
            }
            return super.getSession().getServletContext().getRealPath(pathInfo);
        }

        protected void setData(byte[] data) {
            if (data == null) {
                data = new byte[0];
            }
            contentLength = data.length;
            sis = new CachingServletInputStream(data);
        }

        public void addParameter(String name, String value) {
            addNameValue(name, value, addedParams);
        }

        @Override
        public String getParameter(String name) {
            final List<String> paramValues = addedParams.get(name);
            if (paramValues != null && paramValues.size() > 0) {
                return paramValues.get(0);
            }
            return null;
        }

        @Override
        public Map<String, String[]> getParameterMap() {
            final Map<String, String[]> parameterMap = new HashMap<String, String[]>();
            for (final Entry<String, List<String>> param : addedParams.entrySet()) {
                final List<String> values = param.getValue();
                if (values != null) {
                    parameterMap.put(param.getKey(), values.toArray(new String[values.size()]));
                } else {
                    parameterMap.put(param.getKey(), new String[] {});
                }
            }
            return parameterMap;
        }

        @Override
        public Enumeration<String> getParameterNames() {
            return Collections.enumeration(addedParams.keySet());
        }

        @Override
        public String[] getParameterValues(String name) {
            final List<String> values = addedParams.get(name);

            if (values != null) {
                return values.toArray(new String[values.size()]);
            } else {
                return null;
            }
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            if (sis == null) {
                return super.getInputStream();
            }
            return sis;
        }

        @Override
        public BufferedReader getReader() throws IOException {
            if (sis == null) {
                return super.getReader();
            }
            if (reader == null) {
                reader = new BufferedReader(new InputStreamReader(sis, getCharacterEncoding()));
            }
            return reader;
        }

        @Override
        public String getContentType() {
            if (contentType == null) {
                return super.getContentType();
            }
            return contentType;
        }

        protected void setContentType(String contentType) {
            this.contentType = contentType;
        }

        @Override
        public int getContentLength() {
            if (sis == null) {
                return super.getContentLength();
            }
            return contentLength;
        }

        @Override
        public void setCharacterEncoding(String encoding) throws UnsupportedEncodingException {
            this.characterEncoding = encoding;
        }

        @Override
        public String getCharacterEncoding() {
            if (characterEncoding == null) {
                return super.getCharacterEncoding();
            }
            return characterEncoding;
        }

        @Override
        public String getHeader(String s) {
            if ("If-Modified-Since".equals(s) && !allowCaching) {
                return null;
            }
            return super.getHeader(s);
        }

        @Override
        public long getDateHeader(String s) {
            if ("If-Modified-Since".equals(s) && !allowCaching) {
                return -1;
            }
            return super.getDateHeader(s);
        }

        //        public void setAttribute(String key, Object value) {
        //            attributes.put(key, value);
        //        }
        //
        //        public Object getAttribute(String key) {
        //            Object value = attributes.get(key);
        //            if (value == null)
        //                value = super.getAttribute(key);
        //            return value;
        //        }
        //
        //        public Enumeration getAttributeNames() {
        //            Vector v = new Vector();
        //            for (Enumeration e = super.getAttributeNames(); e.hasMoreElements();) {
        //                v.add(e.nextElement());
        //            }
        //            for (Iterator i = attributes.keySet().iterator(); i.hasNext();) {
        //                v.add(i.next());
        //            }
        //            return v.elements();
        //        }
    }

    private class CachingResponseWrapper extends HttpServletResponseWrapper {

        @SuppressWarnings("unused")
        protected HttpServletResponse origResponse;
        protected CachingServletOutputStream sos = null;
        protected PrintWriter writer = null;
        protected int status = HttpServletResponse.SC_OK;
        protected String contentType = null;
        protected boolean cache;

        public CachingResponseWrapper(HttpServletResponse servletResponse, boolean cache) {
            super(servletResponse);
            this.cache = cache;
            this.origResponse = servletResponse;
        }

        @Override
        public PrintWriter getWriter() throws IOException {
            if (!cache) {
                return super.getWriter();
            }
            if (sos != null) {
                throw new IOException("getWriter cannnot be called after getOutputStream");
            }
            sos = new CachingServletOutputStream();
            if (writer == null) {
                writer = new PrintWriter(new OutputStreamWriter(sos, getCharacterEncoding()));
            }
            return writer;
        }

        @Override
        public ServletOutputStream getOutputStream() throws IOException {
            if (!cache) {
                return super.getOutputStream();
            }
            if (writer != null) {
                throw new IOException("getOutputStream cannnot be called after getWriter");
            }
            if (sos == null) {
                sos = new CachingServletOutputStream();
            }
            return sos;
        }

        public byte[] getData() {
            return sos != null ? sos.getData() : null;
        }

        @Override
        public void setContentType(String type) {
            if (contentType != null) {
                return;
            }
            this.contentType = type;
            if (!cache) {
                super.setContentType(type);
            }
        }

        @Override
        public String getContentType() {
            return contentType != null ? contentType : super.getContentType();
        }

        @Override
        public void setHeader(String name, String value) {
            if ("Content-Type".equals(name)) {
                setContentType(value);
            } else {
                super.setHeader(name, value);
            }
        }

        public int getStatus() {
            return status;
        }

        @Override
        public void setStatus(int i) {
            this.status = i;
            super.setStatus(i);
        }

        @Override
        public void setStatus(int i, String msg) {
            this.status = i;
            super.setStatus(i, msg);
        }

        @Override
        public void sendError(int i, String msg) throws IOException {
            this.status = i;
            super.sendError(i, msg);
        }

        @Override
        public void sendError(int i) throws IOException {
            this.status = i;
            super.sendError(i);
        }

        @Override
        public void setContentLength(int i) {
            if (!cache) {
                super.setContentLength(i);
            }
        }

        @Override
        public void flushBuffer() throws IOException {
            if (!cache) {
                super.flushBuffer();
            }
        }

        public void flush() throws IOException {
            if (cache) {
                if (contentType != null) {
                    super.setContentType(contentType);
                }
            }
            if (sos != null) {
                final ServletOutputStream out = super.getOutputStream();
                out.write(sos.getData());
                out.flush();
            }
        }
    }

    private class CachingServletOutputStream extends ServletOutputStream {

        protected ByteArrayOutputStream ostream = new ByteArrayOutputStream(512);

        protected byte[] getData() {
            return ostream.toByteArray();
        }

        @Override
        public void write(int b) throws IOException {
            ostream.write(b);
        }

        @Override
        public void write(byte b[]) throws IOException {
            ostream.write(b);
        }

        @Override
        public void write(byte b[], int off, int len) throws IOException {
            ostream.write(b, off, len);
        }
    }

    private static class CachingServletInputStream extends ServletInputStream {

        protected ByteArrayInputStream istream;

        public CachingServletInputStream(byte[] data) {
            if (data == null) {
                istream = new ByteArrayInputStream(new byte[0]);
            } else {
                istream = new ByteArrayInputStream(data);
            }
        }

        @Override
        public int read() throws IOException {
            return istream.read();
        }

        @Override
        public int read(byte b[]) throws IOException {
            return istream.read(b);
        }

        @Override
        public int read(byte b[], int off, int len) throws IOException {
            return istream.read(b, off, len);
        }

        @Override
        public int available() throws IOException {
            return istream.available();
        }
    }
}