package org.cdlib.xtf.saxonExt.pipe; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.SocketTimeoutException; import java.net.URL; import java.util.Map; import javax.servlet.http.HttpServletResponse; import org.cdlib.xtf.saxonExt.ElementWithContent; import org.cdlib.xtf.saxonExt.InstructionWithContent; import org.cdlib.xtf.servletBase.TextServlet; import net.sf.saxon.expr.Expression; import net.sf.saxon.expr.XPathContext; import net.sf.saxon.instruct.Executable; import net.sf.saxon.instruct.TailCall; import net.sf.saxon.trans.XPathException; /* * Copyright (c) 2009, Regents of the University of California * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * - Neither the name of the University of California nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /** * Implements a Saxon extension that goes out and makes an HTTP request * (either GET or POST) and pipes the results directly through to the servlet * response output stream, bypassing any further stylesheet processing. */ public class PipeRequestElement extends ElementWithContent { public void prepareAttributes() throws XPathException { String[] mandatoryAtts = { "url" }; String[] optionalAtts = { "timeout", "method" }; parseAttributes(mandatoryAtts, optionalAtts); } public Expression compile(Executable exec) throws XPathException { return new PipeRequestInstruction(attribs, compileContent(exec)); } /** Worker class for PipeRequestElement */ private static class PipeRequestInstruction extends InstructionWithContent { public PipeRequestInstruction(Map<String, Expression> attribs, Expression content) { super("pipe:pipeRequest", attribs, content); } /** * The real workhorse. */ @Override public TailCall processLeavingTail(XPathContext context) throws XPathException { byte[] buf = null; OutputStream postOut = null; InputStream reqIn = null; OutputStream servOut = null; // Build the full URL URL fullURL = null; try { fullURL = new URL(attribs.get("url").evaluateAsString(context)); } catch (MalformedURLException e) { dynamicError("'url' must be a well-formed URL", "PIPE_REQ_001", context); } // Parse the timeout if specified, and convert from seconds to milliseconds. int timeoutMsec = 0; if (attribs.containsKey("timeout")) { String timeoutStr = attribs.get("timeout").evaluateAsString(context); if (timeoutStr != null) { try { timeoutMsec = (int)(Float.parseFloat(timeoutStr) * 1000); timeoutMsec = Math.max(0, timeoutMsec); } catch (NumberFormatException e) { dynamicError("'timeout' must be a number", "PIPE_REQ_002", context); } } } // Parse the method if specified. String method = "GET"; if (attribs.containsKey("method")) { String methodStr = attribs.get("method").evaluateAsString(context); if (methodStr == null || methodStr.equals("GET")) method = "GET"; else if (methodStr.equals("POST")) method = "POST"; else dynamicError("'method' must be 'GET' or 'POST'", "PIPE_REQ_003", context); } // Is there input to send? If so always use POST instead of GET. // Convert the content to a string (we can't use evaluateAsString() since // it only pays attention to the first item in a sequence.) // byte[] inputBytes = new byte[0]; if (content != null) { String inputStr = sequenceToString(content, context).trim(); if (inputStr.length() > 0) { try { inputBytes = inputStr.getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } method = "POST"; } } // Now the real work begins. try { // Get a connection object and set timeout on it. HttpURLConnection conn = (HttpURLConnection) fullURL.openConnection(); if (timeoutMsec > 0) { conn.setConnectTimeout(timeoutMsec); conn.setReadTimeout(timeoutMsec); } if (method.equals("POST") || inputBytes.length > 0) { // Set properties for a POST. conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "text/plain"); conn.setRequestProperty("Content-Length", Integer.toString(inputBytes.length)); conn.setUseCaches(false); conn.setDoInput(true); conn.setDoOutput(true); // Send the request data. conn.connect(); postOut = conn.getOutputStream(); postOut.write(inputBytes); postOut.flush(); postOut.close(); postOut = null; } else { // Standard GET request... default properties are fine. conn.connect(); } // Copy the HTTP status to our outgoing response. HttpServletResponse servletResponse = TextServlet.getCurResponse(); int resCode = conn.getResponseCode(); servletResponse.setStatus(resCode); // Copy all the applicable HTTP headers to our outgoing response. for (int i=0; true; i++) { String key = conn.getHeaderFieldKey(i); String val = conn.getHeaderField(i); if (val == null) break; if (key != null && !key.equals("Connection")) servletResponse.setHeader(key, val); } // Pump through the rest of the response without any interpretation. reqIn = conn.getInputStream(); servOut = servletResponse.getOutputStream(); buf = PipeBufferPool.allocBuffer(); int got; while ((got = reqIn.read(buf)) >= 0) servOut.write(buf, 0, got); servOut.flush(); } catch (SocketTimeoutException e) { dynamicError("External piped request timed out", "PIPE_REQ_004", context); } catch (IOException e) { dynamicError("IO Error during pipe request: " + e.toString(), "PIPE_REQ_005", context); } finally { if (buf != null) PipeBufferPool.deallocBuffer(buf); if (servOut != null) try { servOut.close(); } catch (IOException e) { /* ignore */ } if (reqIn != null) try { reqIn.close(); } catch (IOException e) { /* ignore */ } if (postOut != null) try { postOut.close(); } catch (IOException e) { /* ignore */ } } // All done. return null; } } }