com.xmlcalabash.library.HttpRequest.java Source code

Java tutorial

Introduction

Here is the source code for com.xmlcalabash.library.HttpRequest.java

Source

/*
QuiXProc: efficient evaluation of XProc Pipelines.
Copyright (C) 2011-2012 Innovimax
2008-2012 Mark Logic Corporation.
Portions Copyright 2007 Sun Microsystems, Inc.
All rights reserved.
    
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 3
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 General Public License for more details.
    
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
package com.xmlcalabash.library;

/*
 * HttpRequest.java
 *
 * Copyright 2008 Mark Logic Corporation.
 * Portions Copyright 2007 Sun Microsystems, Inc.
 * All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common
 * Development and Distribution License("CDDL") (collectively, the
 * "License"). You may not use this file except in compliance with the
 * License. You can obtain a copy of the License at
 * https://runtime.dev.java.net/public/CDDL+GPL.html or
 * docs/CDDL+GPL.txt in the distribution. See the License for the
 * specific language governing permissions and limitations under the
 * License. When distributing the software, include this License Header
 * Notice in each file and include the License file at docs/CDDL+GPL.txt.
 */

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.URI;
import java.util.List;
import java.util.Vector;

import javax.xml.XMLConstants;

import net.sf.saxon.s9api.Axis;
import net.sf.saxon.s9api.QName;
import net.sf.saxon.s9api.SaxonApiException;
import net.sf.saxon.s9api.Serializer;
import net.sf.saxon.s9api.XdmNode;
import net.sf.saxon.s9api.XdmNodeKind;
import net.sf.saxon.s9api.XdmSequenceIterator;

import org.apache.commons.httpclient.Cookie;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HeaderElement;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.json.JSONTokener;
import org.xml.sax.InputSource;

import com.xmlcalabash.core.XProcConstants;
import com.xmlcalabash.core.XProcException;
import com.xmlcalabash.core.XProcRuntime;
import com.xmlcalabash.io.ReadablePipe;
import com.xmlcalabash.io.WritablePipe;
import com.xmlcalabash.runtime.XAtomicStep;
import com.xmlcalabash.util.Base64;
import com.xmlcalabash.util.HttpUtils;
import com.xmlcalabash.util.JSONtoXML;
import com.xmlcalabash.util.MIMEReader;
import com.xmlcalabash.util.RelevantNodes;
import com.xmlcalabash.util.S9apiUtils;
import com.xmlcalabash.util.TreeWriter;
import com.xmlcalabash.util.XMLtoJSON;

public class HttpRequest extends DefaultStep {
    private static final QName c_request = new QName("c", XProcConstants.NS_XPROC_STEP, "request");
    private static final QName cx_timeout = new QName("cx", XProcConstants.NS_CALABASH_EX, "timeout");
    private static final QName cx_cookies = new QName("cx", XProcConstants.NS_CALABASH_EX, "cookies");
    private static final QName cx_save_cookies = new QName("cx", XProcConstants.NS_CALABASH_EX, "save-cookies");
    private static final QName cx_use_cookies = new QName("cx", XProcConstants.NS_CALABASH_EX, "use-cookies");
    private static final QName cx_send_binary = new QName("cx", XProcConstants.NS_CALABASH_EX, "send-binary");

    public static final QName _href = new QName("", "href");
    public static final QName _detailed = new QName("", "detailed");
    public static final QName _status_only = new QName("", "status-only");
    public static final QName _username = new QName("", "username");
    public static final QName _password = new QName("", "password");
    public static final QName _auth_method = new QName("", "auth-method");
    public static final QName _send_authorization = new QName("", "send-authorization");
    public static final QName _override_content_type = new QName("", "override-content-type");
    public static final QName _content_type = new QName("", "content-type");
    public static final QName _name = new QName("", "name");
    public static final QName _value = new QName("", "value");
    public static final QName _id = new QName("", "id");
    public static final QName _description = new QName("", "description");
    public static final QName _disposition = new QName("", "disposition");
    public static final QName _status = new QName("", "status");
    public static final QName _boundary = new QName("", "boundary");
    public static final QName _charset = new QName("", "charset");

    private static final int bufSize = 912 * 8; // A multiple of 3, 4, and 75 for base64 line breaking

    private HttpClient client = null;
    private boolean statusOnly = false;
    private boolean detailed = false;
    private String method = null;
    private URI requestURI = null;
    private Vector<Header> headers = new Vector<Header>();
    private String contentType = null;
    private String overrideContentType = null;
    private String headerContentType = null;
    private boolean encodeBinary = false;

    private ReadablePipe source = null;
    private WritablePipe result = null;

    /** Creates a new instance of HttpRequest */
    public HttpRequest(XProcRuntime runtime, XAtomicStep step) {
        super(runtime, step);
    }

    public void setInput(String port, ReadablePipe pipe) {
        source = pipe;
    }

    public void setOutput(String port, WritablePipe pipe) {
        result = pipe;
    }

    public void reset() {
        source.resetReader(stepContext);
        result.resetWriter(stepContext);
    }

    public void gorun() throws SaxonApiException {
        super.gorun();

        XdmNode requestDoc = source.read(stepContext);
        XdmNode start = S9apiUtils.getDocumentElement(requestDoc);

        if (!c_request.equals(start.getNodeName())) {
            throw XProcException.stepError(40);
        }

        // Check for valid attributes
        XdmSequenceIterator iter = start.axisIterator(Axis.ATTRIBUTE);
        boolean ok = true;
        while (iter.hasNext()) {
            XdmNode attr = (XdmNode) iter.next();
            QName name = attr.getNodeName();
            if (_method.equals(name) || _href.equals(name) || _detailed.equals(name) || _status_only.equals(name)
                    || _username.equals(name) || _password.equals(name) || _auth_method.equals(name)
                    || _send_authorization.equals(name) || _override_content_type.equals(name)) {
                // nop
            } else {
                if (XMLConstants.DEFAULT_NS_PREFIX.equals(name.getNamespaceURI())) {
                    throw new XProcException(step.getNode(),
                            "Unsupported attribute on c:request for p:http-request: " + name);
                }
            }
        }

        String send = step.getExtensionAttribute(cx_send_binary);
        encodeBinary = !"true".equals(send);

        method = start.getAttributeValue(_method);
        statusOnly = "true".equals(start.getAttributeValue(_status_only));
        detailed = "true".equals(start.getAttributeValue(_detailed));
        overrideContentType = start.getAttributeValue(_override_content_type);

        if (method == null) {
            throw XProcException.stepError(6);
        }

        if (statusOnly && !detailed) {
            throw XProcException.stepError(4);
        }

        if (start.getAttributeValue(_href) == null) {
            throw new XProcException(step.getNode(),
                    "The 'href' attribute must be specified on c:request for p:http-request");
        }

        requestURI = start.getBaseURI().resolve(start.getAttributeValue(_href));

        if ("file".equals(requestURI.getScheme())) {
            doFile();
            return;
        }

        // What about cookies
        String saveCookieKey = step.getExtensionAttribute(cx_save_cookies);
        String useCookieKeys = step.getExtensionAttribute(cx_use_cookies);
        String cookieKey = step.getExtensionAttribute(cx_cookies);

        if (saveCookieKey == null) {
            saveCookieKey = cookieKey;
        }

        if (useCookieKeys == null) {
            useCookieKeys = cookieKey;
        }

        client = new HttpClient();
        client.getParams().setCookiePolicy(CookiePolicy.RFC_2109);
        client.getParams().setParameter("http.protocol.single-cookie-header", true);

        HttpState state = client.getState();

        if (useCookieKeys != null) {
            for (String key : useCookieKeys.split("\\s+")) {
                for (Cookie cookie : runtime.getCookies(key)) {
                    state.addCookie(cookie);
                }
            }
        }

        String timeOutStr = step.getExtensionAttribute(cx_timeout);
        if (timeOutStr != null) {
            HttpMethodParams params = client.getParams();
            params.setSoTimeout(Integer.parseInt(timeOutStr));
        }

        ProxySelector proxySelector = ProxySelector.getDefault();
        List<Proxy> plist = proxySelector.select(requestURI);
        // I have no idea what I'm expected to do if I get more than one...
        if (plist.size() > 0) {
            Proxy proxy = plist.get(0);
            switch (proxy.type()) {
            case DIRECT:
                // nop;
                break;
            case HTTP:
                // This can't cause a ClassCastException, right?
                InetSocketAddress addr = (InetSocketAddress) proxy.address();
                String host = addr.getHostName();
                int port = addr.getPort();
                client.getHostConfiguration().setProxy(host, port);
                break;
            default:
                // FIXME: send out a log message
                break;
            }
        }

        if (start.getAttributeValue(_username) != null) {
            String user = start.getAttributeValue(_username);
            String pass = start.getAttributeValue(_password);
            String meth = start.getAttributeValue(_auth_method);

            if (meth == null || !("basic".equals(meth.toLowerCase()) || "digest".equals(meth.toLowerCase()))) {
                throw XProcException.stepError(3, "Unsupported auth-method: " + meth);
            }

            String host = requestURI.getHost();
            int port = requestURI.getPort();
            AuthScope scope = new AuthScope(host, port);

            UsernamePasswordCredentials cred = new UsernamePasswordCredentials(user, pass);

            client.getState().setCredentials(scope, cred);

            if ("basic".equals(meth.toLowerCase())) {
                client.getParams().setAuthenticationPreemptive(true);
            }
        }

        iter = start.axisIterator(Axis.CHILD);
        XdmNode body = null;
        while (iter.hasNext()) {
            XdmNode event = (XdmNode) iter.next();
            // FIXME: What about non-whitespace text nodes?
            if (event.getNodeKind() == XdmNodeKind.ELEMENT) {
                if (body != null) {
                    throw new UnsupportedOperationException("Elements follow c:multipart or c:body");
                }

                if (XProcConstants.c_header.equals(event.getNodeName())) {
                    String name = event.getAttributeValue(_name);
                    if (name == null) {
                        continue; // this can't happen, right?
                    }
                    if (name.toLowerCase().equals("content-type")) {
                        // We'll deal with the content-type header later
                        headerContentType = event.getAttributeValue(_value).toLowerCase();
                    } else {
                        headers.add(new Header(event.getAttributeValue(_name), event.getAttributeValue(_value)));
                    }
                } else if (XProcConstants.c_multipart.equals(event.getNodeName())
                        || XProcConstants.c_body.equals(event.getNodeName())) {
                    body = event;
                } else {
                    throw new UnsupportedOperationException("Unexpected request element: " + event.getNodeName());
                }
            }
        }

        String lcMethod = method.toLowerCase();

        // You can only have a body on PUT or POST
        if (body != null && !("put".equals(lcMethod) || "post".equals(lcMethod))) {
            throw XProcException.stepError(5);
        }

        HttpMethodBase httpResult;

        if ("get".equals(lcMethod)) {
            httpResult = doGet();
        } else if ("post".equals(lcMethod)) {
            httpResult = doPost(body);
        } else if ("put".equals(lcMethod)) {
            httpResult = doPut(body);
        } else if ("head".equals(lcMethod)) {
            httpResult = doHead();
        } else if ("delete".equals(lcMethod)) {
            httpResult = doDelete();
        } else {
            throw new UnsupportedOperationException("Unrecognized http method: " + method);
        }

        TreeWriter tree = new TreeWriter(runtime);
        tree.startDocument(requestURI);

        try {
            // Execute the method.
            int statusCode = client.executeMethod(httpResult);

            // Deal with cookies
            if (saveCookieKey != null) {
                runtime.clearCookies(saveCookieKey);

                state = client.getState();
                Cookie[] cookies = state.getCookies();
                for (Cookie cookie : cookies) {
                    runtime.addCookie(saveCookieKey, cookie);
                }
            }

            String contentType = getContentType(httpResult);
            if (overrideContentType != null) {
                if ((xmlContentType(contentType) && overrideContentType.startsWith("image/"))
                        || (contentType.startsWith("text/") && overrideContentType.startsWith("image/"))
                        || (contentType.startsWith("image/") && xmlContentType(overrideContentType))
                        || (contentType.startsWith("image/") && overrideContentType.startsWith("text/"))
                        || (contentType.startsWith("multipart/") && !overrideContentType.startsWith("multipart/"))
                        || (!contentType.startsWith("multipart/")
                                && overrideContentType.startsWith("multipart/"))) {
                    throw XProcException.stepError(30);
                }

                //System.err.println(overrideContentType + " overrides " + contentType);
                contentType = overrideContentType;
            }

            if (detailed) {
                tree.addStartElement(XProcConstants.c_response);
                tree.addAttribute(_status, "" + statusCode);
                tree.startContent();

                for (Header header : httpResult.getResponseHeaders()) {
                    // I don't understand why/how HeaderElement parsing works. I get very weird results.
                    // So I'm just going to go the long way around...
                    String h = header.toString();
                    int cp = h.indexOf(":");
                    String name = header.getName();
                    String value = h.substring(cp + 1).trim();

                    tree.addStartElement(XProcConstants.c_header);
                    tree.addAttribute(_name, name);
                    tree.addAttribute(_value, value);
                    tree.startContent();
                    tree.addEndElement();
                }

                if (statusOnly) {
                    // Skip reading the result
                } else {
                    // Read the response body.
                    InputStream bodyStream = httpResult.getResponseBodyAsStream();
                    if (bodyStream != null) {
                        readBodyContent(tree, bodyStream, httpResult);
                    }
                }

                tree.addEndElement();
            } else {
                if (statusOnly) {
                    // Skip reading the result
                } else {
                    // Read the response body.
                    InputStream bodyStream = httpResult.getResponseBodyAsStream();
                    if (bodyStream != null) {
                        readBodyContent(tree, bodyStream, httpResult);
                    } else {
                        throw XProcException.dynamicError(6);
                    }
                }
            }
        } catch (Exception e) {
            throw new XProcException(e);
        } finally {
            // Release the connection.
            httpResult.releaseConnection();
        }

        tree.endDocument();

        XdmNode resultNode = tree.getResult();

        result.write(stepContext, resultNode);
    }

    private GetMethod doGet() {
        GetMethod method = new GetMethod(requestURI.toASCIIString());

        // Provide custom retry handler is necessary
        method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler(3, false));

        for (Header header : headers) {
            method.addRequestHeader(header);
        }

        return method;
    }

    private HeadMethod doHead() {
        HeadMethod method = new HeadMethod(requestURI.toASCIIString());

        // Provide custom retry handler is necessary
        method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler(3, false));

        for (Header header : headers) {
            method.addRequestHeader(header);
        }

        return method;
    }

    private DeleteMethod doDelete() {
        DeleteMethod method = new DeleteMethod(requestURI.toASCIIString());

        // Provide custom retry handler is necessary
        method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler(3, false));

        for (Header header : headers) {
            method.addRequestHeader(header);
        }

        return method;
    }

    private PutMethod doPut(XdmNode body) {
        PutMethod method = new PutMethod(requestURI.toASCIIString());
        doPutOrPost(method, body);
        return method;
    }

    private PostMethod doPost(XdmNode body) {
        PostMethod method = new PostMethod(requestURI.toASCIIString());
        doPutOrPost(method, body);
        return method;
    }

    private void doPutOrPost(EntityEnclosingMethod method, XdmNode body) {
        if (XProcConstants.c_multipart.equals(body.getNodeName())) {
            doPutOrPostMultipart(method, body);
        } else {
            doPutOrPostSinglepart(method, body);
        }
    }

    private void doPutOrPostSinglepart(EntityEnclosingMethod method, XdmNode body) {
        // ATTENTION: This doesn't handle multipart, that's done entirely separately

        // Provide custom retry handler is necessary
        method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler(3, false));

        // Check for consistency of content-type
        contentType = body.getAttributeValue(_content_type);
        if (contentType == null) {
            throw new XProcException(step.getNode(), "Content-type on c:body is required.");
        }

        if (headerContentType != null && !headerContentType.equals(contentType.toLowerCase())) {
            throw XProcException.stepError(20);
        }

        for (Header header : headers) {
            method.addRequestHeader(header);
        }

        // FIXME: This sucks rocks. I want to write the data to be posted, not provide some way to read it
        String postContent = null;
        String encoding = body.getAttributeValue(_encoding);
        try {
            if ("base64".equals(encoding)) {
                String charset = body.getAttributeValue(_charset);
                // FIXME: is utf-8 the right default?
                if (charset == null) {
                    charset = "utf-8";
                }
                String escapedContent = decodeBase64(body, charset);
                StringWriter writer = new StringWriter();
                writer.write(escapedContent);
                writer.close();
                postContent = writer.toString();
            } else {
                if (jsonContentType(contentType)) {
                    postContent = XMLtoJSON.convert(body);
                } else if (xmlContentType(contentType)) {
                    Serializer serializer = makeSerializer();

                    if (!S9apiUtils.isDocumentContent(body.axisIterator(Axis.CHILD))) {
                        throw XProcException.stepError(22);
                    }

                    Vector<XdmNode> content = new Vector<XdmNode>();
                    XdmSequenceIterator iter = body.axisIterator(Axis.CHILD);
                    while (iter.hasNext()) {
                        XdmNode node = (XdmNode) iter.next();
                        content.add(node);
                    }

                    // FIXME: set serializer properties appropriately!
                    StringWriter writer = new StringWriter();
                    serializer.setOutputWriter(writer);
                    S9apiUtils.serialize(runtime, content, serializer);
                    writer.close();
                    postContent = writer.toString();
                } else {
                    StringWriter writer = new StringWriter();
                    XdmSequenceIterator iter = body.axisIterator(Axis.CHILD);
                    while (iter.hasNext()) {
                        XdmNode node = (XdmNode) iter.next();
                        if (node.getNodeKind() != XdmNodeKind.TEXT) {
                            throw XProcException.stepError(28);
                        }
                        writer.write(node.getStringValue());
                    }
                    writer.close();
                    postContent = writer.toString();
                }
            }

            StringRequestEntity requestEntity = new StringRequestEntity(postContent, contentType, "UTF-8");
            method.setRequestEntity(requestEntity);

        } catch (IOException ioe) {
            throw new XProcException(ioe);
        } catch (SaxonApiException sae) {
            throw new XProcException(sae);
        }
    }

    private void doPutOrPostMultipart(EntityEnclosingMethod method, XdmNode multipart) {
        // The Apache HTTP libraries just don't handle this case...we treat it as a "single part"
        // and build the body ourselves, using the boundaries etc.

        // Provide custom retry handler is necessary
        method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
                new DefaultHttpMethodRetryHandler(3, false));

        // Check for consistency of content-type
        contentType = multipart.getAttributeValue(_content_type);
        if (contentType == null) {
            contentType = "multipart/mixed";
        }

        if (headerContentType != null && !headerContentType.equals(contentType.toLowerCase())) {
            throw XProcException.stepError(20);
        }

        if (!contentType.startsWith("multipart/")) {
            throw new UnsupportedOperationException("Multipart content-type must be multipart/...");
        }

        for (Header header : headers) {
            method.addRequestHeader(header);
        }

        String boundary = multipart.getAttributeValue(_boundary);

        if (boundary == null) {
            throw new XProcException(step.getNode(), "A boundary value must be specified on c:multipart");
        }

        if (boundary.startsWith("--")) {
            throw XProcException.stepError(2);
        }

        String q = "\"";
        if (boundary.contains(q)) {
            q = "'";
        }
        if (boundary.contains(q)) {
            q = "";
        }

        String multipartContentType = contentType + "; boundary=" + q + boundary + q;

        // FIXME: This sucks rocks. I want to write the data to be posted, not provide some way to read it
        MessageBytes byteContent = new MessageBytes();
        byteContent.append("This is a multipart message.\r\n");
        //String postContent = "This is a multipart message.\r\n";
        for (XdmNode body : new RelevantNodes(runtime, multipart, Axis.CHILD)) {
            if (!XProcConstants.c_body.equals(body.getNodeName())) {
                throw new XProcException(step.getNode(), "A c:multipart may only contain c:body elements.");
            }

            String bodyContentType = body.getAttributeValue(_content_type);
            if (bodyContentType == null) {
                throw new XProcException(step.getNode(), "Content-type on c:body is required.");
            }

            String bodyId = body.getAttributeValue(_id);
            String bodyDescription = body.getAttributeValue(_description);
            String bodyDisposition = body.getAttributeValue(_disposition);

            String bodyCharset = HttpUtils.getCharset(bodyContentType);

            if (bodyContentType.contains(";")) {
                int pos = bodyContentType.indexOf(";");
                bodyContentType = bodyContentType.substring(0, pos);
            }

            String bodyEncoding = body.getAttributeValue(_encoding);
            if (bodyEncoding != null && !"base64".equals(bodyEncoding)) {
                throw new UnsupportedOperationException("The '" + bodyEncoding + "' encoding is not supported");
            }

            if (bodyCharset != null) {
                bodyContentType += "; charset=" + bodyCharset;
            } else {
                // Is utf-8 the right default? What about the image/ case? 
                bodyContentType += "; charset=utf-8";
            }

            //postContent += "--" + boundary + "\r\n";
            //postContent += "Content-Type: " + bodyContentType + "\r\n";
            byteContent.append("--" + boundary + "\r\n");
            byteContent.append("Content-Type: " + bodyContentType + "\r\n");

            if (bodyDescription != null) {
                //postContent += "Content-Description: " + bodyDescription + "\r\n";
                byteContent.append("Content-Description: " + bodyDescription + "\r\n");
            }
            if (bodyId != null) {
                //postContent += "Content-ID: " + bodyId + "\r\n";
                byteContent.append("Content-ID: " + bodyId + "\r\n");
            }
            if (bodyDisposition != null) {
                //postContent += "Content-Disposition: " + bodyDisposition + "\r\n";
                byteContent.append("Content-Disposition: " + bodyDisposition + "\r\n");
            }
            if (bodyEncoding != null) {
                //postContent += "Content-Transfer-Encoding: " + bodyEncoding + "\r\n";
                if (encodeBinary) {
                    byteContent.append("Content-Transfer-Encoding: " + bodyEncoding + "\r\n");
                }
            }
            //postContent += "\r\n";
            byteContent.append("\r\n");

            try {
                if (xmlContentType(bodyContentType)) {
                    Serializer serializer = makeSerializer();

                    Vector<XdmNode> content = new Vector<XdmNode>();
                    XdmSequenceIterator iter = body.axisIterator(Axis.CHILD);
                    while (iter.hasNext()) {
                        XdmNode node = (XdmNode) iter.next();
                        content.add(node);
                    }

                    // FIXME: set serializer properties appropriately!
                    StringWriter writer = new StringWriter();
                    serializer.setOutputWriter(writer);
                    S9apiUtils.serialize(runtime, content, serializer);
                    writer.close();
                    //postContent += writer.toString();
                    byteContent.append(writer.toString());
                } else if (jsonContentType(contentType)) {
                    byteContent.append(XMLtoJSON.convert(body));
                } else if (!encodeBinary && "base64".equals(bodyEncoding)) {
                    byte[] decoded = Base64.decode(body.getStringValue());
                    byteContent.append(decoded, decoded.length);
                } else {
                    StringWriter writer = new StringWriter();
                    XdmSequenceIterator iter = body.axisIterator(Axis.CHILD);
                    while (iter.hasNext()) {
                        XdmNode node = (XdmNode) iter.next();
                        if (node.getNodeKind() != XdmNodeKind.TEXT) {
                            throw XProcException.stepError(28);
                        }
                        writer.write(node.getStringValue());
                    }
                    writer.close();
                    //postContent += writer.toString();
                    byteContent.append(writer.toString());
                }

                //postContent += "\r\n";
                byteContent.append("\r\n");
            } catch (IOException ioe) {
                throw new XProcException(ioe);
            } catch (SaxonApiException sae) {
                throw new XProcException(sae);
            }
        }

        //postContent += "--" + boundary + "--\r\n";
        byteContent.append("--" + boundary + "--\r\n");

        ByteArrayRequestEntity requestEntity = new ByteArrayRequestEntity(byteContent.content(),
                multipartContentType);
        //StringRequestEntity requestEntity = new StringRequestEntity(postContent, multipartContentType, null);
        method.setRequestEntity(requestEntity);
    }

    private String getFullContentType(HttpMethodBase method) {
        Header contentTypeHeader = method.getResponseHeader("Content-Type");
        return getFullContentType(contentTypeHeader);
    }

    private String getFullContentType(Header contentTypeHeader) {
        if (contentTypeHeader == null) {
            // This should never happen, but if it does...
            return "application/octet-stream";
        }

        HeaderElement[] contentTypes = contentTypeHeader.getElements();
        if (contentTypes == null || contentTypes.length == 0) {
            // This should never happen
            return null;
        }

        String ctype = contentTypes[0].getName();
        NameValuePair[] params = contentTypes[0].getParameters();
        if (params != null) {
            for (NameValuePair pair : params) {
                ctype = ctype + "; " + pair.getName() + "=\"" + pair.getValue() + "\"";
            }
        }

        return ctype;
    }

    private String getHeaderValue(Header header) {
        if (header == null) {
            // This should never happen
            return null;
        }

        HeaderElement[] elems = header.getElements();
        if (elems == null || elems.length == 0) {
            // This should never happen
            return null;
        }

        return elems[0].getName();
    }

    private String getContentType(HttpMethodBase method) {
        Header contentTypeHeader = method.getResponseHeader("Content-Type");
        String contentType = getContentType(contentTypeHeader);
        if (contentType == null) {
            // This should never happen either...
            return "application/octet-stream";
        } else {
            return contentType;
        }
    }

    private String getContentType(Header contentTypeHeader) {
        return getHeaderValue(contentTypeHeader);
    }

    private String getContentBoundary(HttpMethodBase method) {
        Header contentTypeHeader = method.getResponseHeader("Content-Type");
        return getContentBoundary(contentTypeHeader);
    }

    private String getContentBoundary(Header contentTypeHeader) {
        if (contentTypeHeader == null) {
            // This should never happen
            return null;
        }

        HeaderElement[] contentTypes = contentTypeHeader.getElements();
        if (contentTypes == null || contentTypes.length == 0) {
            // This should never happen
            return null;
        }

        NameValuePair boundary = contentTypes[0].getParameterByName("boundary");
        return boundary == null ? null : boundary.getValue();
    }

    private String getContentCharset(Header contentTypeHeader) {
        if (contentTypeHeader == null) {
            // This should never happen
            return null;
        }

        HeaderElement[] contentTypes = contentTypeHeader.getElements();
        if (contentTypes == null || contentTypes.length == 0) {
            // This should never happen
            return null;
        }

        NameValuePair cpair = contentTypes[0].getParameterByName("charset");
        if (cpair == null) {
            return "US-ASCII";
        } else {
            return cpair.getValue();
        }
    }

    private boolean xmlContentType(String contentType) {
        return HttpUtils.xmlContentType(contentType);
    }

    private boolean jsonContentType(String contentType) {
        return runtime.transparentJSON() && HttpUtils.jsonContentType(contentType);
    }

    private boolean textContentType(String contentType) {
        return HttpUtils.textContentType(contentType);
    }

    private void readBodyContent(TreeWriter tree, InputStream bodyStream, HttpMethodBase method)
            throws SaxonApiException, IOException {
        String contentType = getFullContentType(method);
        String charset = method.getResponseCharSet();
        String boundary = getContentBoundary(method);

        if (overrideContentType != null) {
            contentType = overrideContentType;
        }

        if (contentType.startsWith("multipart/")) {
            tree.addStartElement(XProcConstants.c_multipart);
            tree.addAttribute(_content_type, contentType);
            tree.addAttribute(_boundary, boundary);
            tree.startContent();

            readMultipartContent(tree, bodyStream, boundary);

            tree.addEndElement();
        } else {
            if (!detailed && (xmlContentType(contentType) || jsonContentType(contentType))) {
                readBodyContentPart(tree, bodyStream, contentType, charset);
            } else {
                tree.addStartElement(XProcConstants.c_body);
                tree.addAttribute(_content_type, contentType);
                if (!xmlContentType(contentType) && !textContentType(contentType)
                        && !jsonContentType(contentType)) {
                    tree.addAttribute(_encoding, "base64");
                }
                tree.startContent();
                readBodyContentPart(tree, bodyStream, contentType, charset);
                tree.addEndElement();
            }
        }
    }

    private void readMultipartContent(TreeWriter tree, InputStream bodyStream, String boundary)
            throws IOException, SaxonApiException {
        MIMEReader reader = new MIMEReader(bodyStream, boundary);
        boolean done = false;
        while (reader.readHeaders()) {
            Header pctype = reader.getHeader("Content-Type");
            Header pclen = reader.getHeader("Content-Length");

            contentType = getHeaderValue(pctype);

            String charset = getContentCharset(pctype);
            String partType = getHeaderValue(pctype);
            InputStream partStream = null;

            if (pclen != null) {
                int len = Integer.parseInt(getHeaderValue(pclen));
                partStream = reader.readBodyPart(len);
            } else {
                partStream = reader.readBodyPart();
            }

            tree.addStartElement(XProcConstants.c_body);
            tree.addAttribute(_content_type, contentType);
            if (!xmlContentType(contentType) && !textContentType(contentType)) {
                tree.addAttribute(_encoding, "base64");
            }
            tree.startContent();

            if (xmlContentType(partType)) {
                BufferedReader preader = new BufferedReader(new InputStreamReader(partStream, charset));
                // Read it as XML
                tree.addSubtree(runtime.parse(new InputSource(preader)));
            } else if (textContentType(partType)) {
                BufferedReader preader = new BufferedReader(new InputStreamReader(partStream, charset));
                // Read it as text
                char buf[] = new char[bufSize];
                int len = preader.read(buf, 0, bufSize);
                while (len >= 0) {
                    // I'm unsure about this. If I'm reading text and injecting it into XML,
                    // I think I need to change CR/LF pairs (and CR not followed by LF) into
                    // plain LFs.

                    char fbuf[] = new char[bufSize];
                    char flen = 0;
                    for (int pos = 0; pos < len; pos++) {
                        if (buf[pos] == '\r') {
                            if (pos + 1 == len) {
                                // FIXME: Check for CR/LF pairs that cross a buffer boundary!
                                // Assume it's part of a CR/LF pair...
                            } else {
                                if (buf[pos + 1] == '\n') {
                                    // nop
                                } else {
                                    fbuf[flen++] = '\n';
                                }
                            }
                        } else {
                            fbuf[flen++] = buf[pos];
                        }
                    }

                    tree.addText(new String(fbuf, 0, flen));
                    len = preader.read(buf, 0, bufSize);
                }
            } else {
                // Read it as binary
                byte bytes[] = new byte[bufSize];
                int pos = 0;
                int readLen = bufSize;
                int len = partStream.read(bytes, 0, bufSize);
                while (len >= 0) {
                    pos += len;
                    readLen -= len;
                    if (readLen == 0) {
                        tree.addText(Base64.encodeBytes(bytes));
                        pos = 0;
                        readLen = bufSize;
                    }

                    len = partStream.read(bytes, pos, readLen);
                }

                if (pos > 0) {
                    byte lastBytes[] = new byte[pos];
                    System.arraycopy(bytes, 0, lastBytes, 0, pos);
                    tree.addText(Base64.encodeBytes(lastBytes));
                }

                tree.addText("\n"); // FIXME: should we be doing this?
            }

            tree.addEndElement();
        }
    }

    public void readBodyContentPart(TreeWriter tree, InputStream bodyStream, String contentType, String charset)
            throws SaxonApiException, IOException {
        if (xmlContentType(contentType)) {
            // Read it as XML
            tree.addSubtree(runtime.parse(new InputSource(bodyStream)));
        } else if (textContentType(contentType)) {
            // Read it as text

            InputStreamReader reader = new InputStreamReader(bodyStream, charset);

            char buf[] = new char[bufSize];
            int len = reader.read(buf, 0, bufSize);
            while (len >= 0) {
                String s = new String(buf, 0, len);
                tree.addText(s);
                len = reader.read(buf, 0, bufSize);
            }
        } else if (jsonContentType(contentType)) {
            InputStreamReader reader = new InputStreamReader(bodyStream);
            JSONTokener jt = new JSONTokener(reader);
            XdmNode jsonDoc = JSONtoXML.convert(runtime.getProcessor(), jt, runtime.jsonFlavor());
            tree.addSubtree(jsonDoc);
        } else {
            // Read it as binary
            byte bytes[] = new byte[bufSize];
            int pos = 0;
            int readLen = bufSize;
            int len = bodyStream.read(bytes, 0, bufSize);
            while (len >= 0) {
                pos += len;
                readLen -= len;
                if (readLen == 0) {
                    String encoded = Base64.encodeBytes(bytes);
                    tree.addText(encoded);
                    pos = 0;
                    readLen = bufSize;
                }

                len = bodyStream.read(bytes, pos, readLen);
            }

            if (pos > 0) {
                byte lastBytes[] = new byte[pos];
                System.arraycopy(bytes, 0, lastBytes, 0, pos);
                tree.addText(Base64.encodeBytes(lastBytes));
            }

            tree.addText("\n"); // FIXME: should we be doing this?
        }
    }

    private String extractText(XdmNode doc) {
        String content = "";
        XdmSequenceIterator iter = doc.axisIterator(Axis.CHILD);
        while (iter.hasNext()) {
            XdmNode child = (XdmNode) iter.next();
            if (child.getNodeKind() == XdmNodeKind.ELEMENT || child.getNodeKind() == XdmNodeKind.TEXT) {
                content += child.getStringValue();
            }
        }
        return content;
    }

    private String decodeBase64(XdmNode doc, String charset) {
        String content = extractText(doc);
        byte[] decoded = Base64.decode(content);
        try {
            return new String(decoded, charset);
        } catch (UnsupportedEncodingException uee) {
            throw XProcException.stepError(10, uee);
        }
    }

    private void doFile() {
        // Find the content type
        String contentType = overrideContentType;

        if (contentType == null) {
            contentType = "application/octet-stream";
        }

        // FIXME: Is ISO-8859-1 the right default?
        String charset = HttpUtils.getCharset(contentType, "ISO-8859-1");

        TreeWriter tree = new TreeWriter(runtime);
        tree.startDocument(requestURI);

        try {
            File file = new File(requestURI.getPath());
            FileInputStream bodyStream = null;
            bodyStream = new FileInputStream(file);

            if (xmlContentType(contentType)) {
                readBodyContentPart(tree, bodyStream, contentType, charset);
            } else {
                tree.addStartElement(XProcConstants.c_body);
                tree.addAttribute(_content_type, contentType);
                if (!xmlContentType(contentType) && !textContentType(contentType)) {
                    tree.addAttribute(_encoding, "base64");
                }
                tree.startContent();
                readBodyContentPart(tree, bodyStream, contentType, charset);
                tree.addEndElement();
            }

            tree.endDocument();

            XdmNode doc = tree.getResult();
            result.write(stepContext, doc);
        } catch (FileNotFoundException fnfe) {
            throw new XProcException(fnfe);
        } catch (SaxonApiException sae) {
            throw new XProcException(sae);
        } catch (IOException ioe) {
            throw new XProcException(ioe);
        }
    }

    private class MessageBytes {
        int chunkSize = 8192;
        byte[] byteContent = new byte[chunkSize];
        int pos = 0;

        public MessageBytes() {
        }

        public void append(String string) {
            try {
                byte[] bytes = string.getBytes("US-ASCII");
                append(bytes, bytes.length);
            } catch (UnsupportedEncodingException uee) {
                // This never happens!
                throw new XProcException(uee);
            }
        }

        public void append(byte[] bytes, int size) {
            if (pos + bytes.length > byteContent.length) {
                byte[] newBytes = new byte[byteContent.length + bytes.length + chunkSize];
                System.arraycopy(byteContent, 0, newBytes, 0, byteContent.length);
                byteContent = newBytes;
            }
            System.arraycopy(bytes, 0, byteContent, pos, bytes.length);
            pos += bytes.length;
        }

        public byte[] content() {
            byte[] bytes = new byte[pos];
            System.arraycopy(byteContent, 0, bytes, 0, pos);
            return bytes;
        }
    }
}