/* * Copyright 2001-2004 The Apache Software Foundation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $Id$ */ package org.apache.manifoldcf.connectorcommon.common; import org.apache.manifoldcf.connectorcommon.common.XThreadInputStream; import org.apache.axis.AxisFault; import org.apache.axis.Constants; import org.apache.axis.Message; import org.apache.axis.MessageContext; import org.apache.axis.components.logger.LogFactory; import org.apache.axis.components.net.CommonsHTTPClientProperties; import org.apache.axis.components.net.CommonsHTTPClientPropertiesFactory; import org.apache.axis.components.net.TransportClientProperties; import org.apache.axis.components.net.TransportClientPropertiesFactory; import org.apache.axis.transport.http.HTTPConstants; import org.apache.axis.handlers.BasicHandler; import org.apache.axis.soap.SOAP12Constants; import org.apache.axis.soap.SOAPConstants; import org.apache.axis.utils.JavaUtils; import org.apache.axis.utils.Messages; import org.apache.axis.utils.NetworkUtils; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.Header; import org.apache.http.ProtocolVersion; import org.apache.http.util.EntityUtils; import org.apache.http.message.BasicHeader; import org.apache.http.entity.ContentType; import org.apache.http.conn.ConnectTimeoutException; import org.apache.http.client.RedirectException; import org.apache.http.client.CircularRedirectException; import org.apache.http.NoHttpResponseException; import org.apache.http.HttpException; import org.apache.http.ParseException; import org.apache.commons.logging.Log; import javax.xml.soap.MimeHeader; import javax.xml.soap.MimeHeaders; import javax.xml.soap.SOAPException; import java.io.ByteArrayOutputStream; import java.io.FilterInputStream; import java.io.InterruptedIOException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.InputStreamReader; import java.io.Writer; import java.io.StringWriter; import java.io.File; import java.io.FileOutputStream; import java.io.FileInputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Hashtable; import java.util.Iterator; import java.util.HashMap; import java.util.Map; import java.util.List; import java.nio.charset.Charset; /* Class to use httpcomponents to communicate with a SOAP server. * I've replaced the original rather complicated class with a much simpler one that * relies on having an HttpClient object passed into the invoke() method. Since * the object is already set up, not much needs to be done in here. */ public class CommonsHTTPSender extends BasicHandler { public static final String HTTPCLIENT_PROPERTY = "ManifoldCF_HttpClient"; /** Field log */ protected static Log log = LogFactory.getLog(CommonsHTTPSender.class.getName()); /** Properties */ protected CommonsHTTPClientProperties clientProperties; public CommonsHTTPSender() { this.clientProperties = CommonsHTTPClientPropertiesFactory.create(); } /** * invoke creates a socket connection, sends the request SOAP message and then * reads the response SOAP message back from the SOAP server * * @param msgContext the messsage context * * @throws AxisFault */ public void invoke(MessageContext msgContext) throws AxisFault { if (log.isDebugEnabled()) { log.debug(Messages.getMessage("enter00", "CommonsHTTPSender::invoke")); } // Catch all exceptions and turn them into AxisFaults try { // Get the URL URL targetURL = new URL(msgContext.getStrProp(MessageContext.TRANS_URL)); // Get the HttpClient HttpClient httpClient = (HttpClient)msgContext.getProperty(HTTPCLIENT_PROPERTY); boolean posting = true; // If we're SOAP 1.2, allow the web method to be set from the // MessageContext. if (msgContext.getSOAPConstants() == SOAPConstants.SOAP12_CONSTANTS) { String webMethod = msgContext.getStrProp(SOAP12Constants.PROP_WEBMETHOD); if (webMethod != null) { posting = webMethod.equals(HTTPConstants.HEADER_POST); } } boolean http10 = false; String httpVersion = msgContext.getStrProp(MessageContext.HTTP_TRANSPORT_VERSION); if (httpVersion != null) { if (httpVersion.equals(HTTPConstants.HEADER_PROTOCOL_V10)) { http10 = true; } // assume 1.1 } HttpRequestBase method; if (posting) { HttpPost postMethod = new HttpPost(targetURL.toString()); // set false as default, addContetInfo can overwrite //HttpProtocolParams.setUseExpectContinue(postMethod.getParams(),false); Message reqMessage = msgContext.getRequestMessage(); boolean httpChunkStream = addContextInfo(postMethod, msgContext); HttpEntity requestEntity = null; requestEntity = new MessageRequestEntity(reqMessage, httpChunkStream, http10 || !httpChunkStream); postMethod.setEntity(requestEntity); method = postMethod; } else { method = new HttpGet(targetURL.toString()); } //if (http10) // HttpProtocolParams.setVersion(method.getParams(),new ProtocolVersion("HTTP",1,0)); BackgroundHTTPThread methodThread = new BackgroundHTTPThread(httpClient,method); methodThread.start(); try { int returnCode = methodThread.getResponseCode(); String contentType = getHeader(methodThread, HTTPConstants.HEADER_CONTENT_TYPE); String contentLocation = getHeader(methodThread, HTTPConstants.HEADER_CONTENT_LOCATION); String contentLength = getHeader(methodThread, HTTPConstants.HEADER_CONTENT_LENGTH); if ((returnCode > 199) && (returnCode < 300)) { // SOAP return is OK - so fall through } else if (msgContext.getSOAPConstants() == SOAPConstants.SOAP12_CONSTANTS) { // For now, if we're SOAP 1.2, fall through, since the range of // valid result codes is much greater } else if ((contentType != null) && !contentType.equals("text/html") && ((returnCode > 499) && (returnCode < 600))) { // SOAP Fault should be in here - so fall through } else { String statusMessage = methodThread.getResponseStatus(); AxisFault fault = new AxisFault("HTTP", "(" + returnCode + ")" + statusMessage, null, null); fault.setFaultDetailString( Messages.getMessage("return01", "" + returnCode, getResponseBodyAsString(methodThread))); fault.addFaultDetail(Constants.QNAME_FAULTDETAIL_HTTPERRORCODE, Integer.toString(returnCode)); throw fault; } String contentEncoding = methodThread.getFirstHeader(HTTPConstants.HEADER_CONTENT_ENCODING); if (contentEncoding != null) { AxisFault fault = new AxisFault("HTTP", "unsupported content-encoding of '" + contentEncoding + "' found", null, null); throw fault; } Map<String,List<String>> responseHeaders = methodThread.getResponseHeaders(); InputStream dataStream = methodThread.getSafeInputStream(); Message outMsg = new Message(new BackgroundInputStream(methodThread,dataStream), false, contentType, contentLocation); // Transfer HTTP headers of HTTP message to MIME headers of SOAP message MimeHeaders responseMimeHeaders = outMsg.getMimeHeaders(); for (String name : responseHeaders.keySet()) { List<String> values = responseHeaders.get(name); for (String value : values) { responseMimeHeaders.addHeader(name,value); } } outMsg.setMessageType(Message.RESPONSE); // Put the message in the message context. msgContext.setResponseMessage(outMsg); // Pass off the method thread to the stream for closure methodThread = null; } finally { if (methodThread != null) { methodThread.abort(); methodThread.finishUp(); } } } catch (AxisFault af) { log.debug(af); throw af; } catch (Exception e) { log.debug(e); throw AxisFault.makeFault(e); } if (log.isDebugEnabled()) { log.debug(Messages.getMessage("exit00", "CommonsHTTPSender::invoke")); } } /** * Extracts info from message context. * * @param method Post or get method * @param msgContext the message context */ private static boolean addContextInfo(HttpPost method, MessageContext msgContext) throws AxisFault { boolean httpChunkStream = false; // Get SOAPAction, default to "" String action = msgContext.useSOAPAction() ? msgContext.getSOAPActionURI() : ""; if (action == null) { action = ""; } Message msg = msgContext.getRequestMessage(); if (msg != null){ // First, transfer MIME headers of SOAPMessage to HTTP headers. // Some of these might be overridden later. MimeHeaders mimeHeaders = msg.getMimeHeaders(); if (mimeHeaders != null) { for (Iterator i = mimeHeaders.getAllHeaders(); i.hasNext(); ) { MimeHeader mimeHeader = (MimeHeader) i.next(); method.addHeader(mimeHeader.getName(), mimeHeader.getValue()); } } method.setHeader(new BasicHeader(HTTPConstants.HEADER_CONTENT_TYPE, msg.getContentType(msgContext.getSOAPConstants()))); } method.setHeader(new BasicHeader("Accept","*/*")); method.setHeader(new BasicHeader(HTTPConstants.HEADER_SOAP_ACTION, "\"" + action + "\"")); method.setHeader(new BasicHeader(HTTPConstants.HEADER_USER_AGENT, Messages.getMessage("axisUserAgent"))); // process user defined headers for information. Hashtable userHeaderTable = (Hashtable) msgContext.getProperty(HTTPConstants.REQUEST_HEADERS); if (userHeaderTable != null) { for (Iterator e = userHeaderTable.entrySet().iterator(); e.hasNext();) { Map.Entry me = (Map.Entry) e.next(); Object keyObj = me.getKey(); if (null == keyObj) { continue; } String key = keyObj.toString().trim(); String value = me.getValue().toString().trim(); //if (key.equalsIgnoreCase(HTTPConstants.HEADER_EXPECT) && // value.equalsIgnoreCase(HTTPConstants.HEADER_EXPECT_100_Continue)) { // HttpProtocolParams.setUseExpectContinue(method.getParams(),true); //} else if (key.equalsIgnoreCase(HTTPConstants.HEADER_TRANSFER_ENCODING_CHUNKED)) { String val = me.getValue().toString(); if (null != val) { httpChunkStream = JavaUtils.isTrue(val); } } else { method.addHeader(key, value); } } } return httpChunkStream; } private static String getHeader(BackgroundHTTPThread methodThread, String headerName) throws IOException, InterruptedException, HttpException { String header = methodThread.getFirstHeader(headerName); return (header == null) ? null : header.trim(); } private static String getResponseBodyAsString(BackgroundHTTPThread methodThread) throws IOException, InterruptedException, HttpException { InputStream is = methodThread.getSafeInputStream(); if (is != null) { try { Charset charSet = methodThread.getCharSet(); if (charSet == null) charSet = StandardCharsets.UTF_8; char[] buffer = new char[65536]; Reader r = new InputStreamReader(is,charSet); Writer w = new StringWriter(); try { while (true) { int amt = r.read(buffer); if (amt == -1) break; w.write(buffer,0,amt); } } finally { w.flush(); } return w.toString(); } finally { is.close(); } } return ""; } private static class MessageRequestEntity implements HttpEntity { private final Message message; private final boolean httpChunkStream; //Use HTTP chunking or not. private final boolean contentLengthNeeded; public MessageRequestEntity(Message message, boolean httpChunkStream, boolean contentLengthNeeded) { this.message = message; this.httpChunkStream = httpChunkStream; this.contentLengthNeeded = contentLengthNeeded; } @Override public boolean isChunked() { return httpChunkStream; } @Override @Deprecated public void consumeContent() throws IOException { EntityUtils.consume(this); } @Override public boolean isRepeatable() { return true; } @Override public boolean isStreaming() { return false; } @Override public InputStream getContent() throws IOException, IllegalStateException { // MHL return null; } @Override public void writeTo(OutputStream out) throws IOException { try { this.message.writeTo(out); } catch (SOAPException e) { throw new IOException(e.getMessage()); } } @Override public long getContentLength() { if (contentLengthNeeded) { try { return message.getContentLength(); } catch (Exception e) { } } // Unknown (chunked) length return -1L; } @Override public Header getContentType() { return null; // a separate header is added } @Override public Header getContentEncoding() { return null; } } /** This input stream wraps a background http transaction thread, so that * the thread is ended when the stream is closed. */ private static class BackgroundInputStream extends InputStream { private BackgroundHTTPThread methodThread = null; private InputStream xThreadInputStream = null; /** Construct an http transaction stream. The stream is driven by a background * thread, whose existence is tied to this class. The sequence of activity that * this class expects is as follows: * (1) Construct the httpclient and request object and initialize them * (2) Construct a background method thread, and start it * (3) If the response calls for it, call this constructor, and put the resulting stream * into the message response * (4) Otherwise, terminate the background method thread in the standard manner, * being sure NOT */ public BackgroundInputStream(BackgroundHTTPThread methodThread, InputStream xThreadInputStream) { this.methodThread = methodThread; this.xThreadInputStream = xThreadInputStream; } @Override public int available() throws IOException { if (xThreadInputStream != null) return xThreadInputStream.available(); return super.available(); } @Override public void close() throws IOException { try { if (xThreadInputStream != null) { xThreadInputStream.close(); xThreadInputStream = null; } } finally { if (methodThread != null) { methodThread.abort(); try { methodThread.finishUp(); } catch (InterruptedException e) { throw new InterruptedIOException(e.getMessage()); } methodThread = null; } } } @Override public void mark(int readlimit) { if (xThreadInputStream != null) xThreadInputStream.mark(readlimit); else super.mark(readlimit); } @Override public void reset() throws IOException { if (xThreadInputStream != null) xThreadInputStream.reset(); else super.reset(); } @Override public boolean markSupported() { if (xThreadInputStream != null) return xThreadInputStream.markSupported(); return super.markSupported(); } @Override public long skip(long n) throws IOException { if (xThreadInputStream != null) return xThreadInputStream.skip(n); return super.skip(n); } @Override public int read(byte[] b, int off, int len) throws IOException { if (xThreadInputStream != null) return xThreadInputStream.read(b,off,len); return super.read(b,off,len); } @Override public int read(byte[] b) throws IOException { if (xThreadInputStream != null) return xThreadInputStream.read(b); return super.read(b); } @Override public int read() throws IOException { if (xThreadInputStream != null) return xThreadInputStream.read(); return -1; } } /** This thread does the actual socket communication with the server. * It's set up so that it can be abandoned at shutdown time. * * The way it works is as follows: * - it starts the transaction * - it receives the response, and saves that for the calling class to inspect * - it transfers the data part to an input stream provided to the calling class * - it shuts the connection down * * If there is an error, the sequence is aborted, and an exception is recorded * for the calling class to examine. * * The calling class basically accepts the sequence above. It starts the * thread, and tries to get a response code. If instead an exception is seen, * the exception is thrown up the stack. */ protected static class BackgroundHTTPThread extends Thread { /** Client and method, all preconfigured */ protected final HttpClient httpClient; protected final HttpRequestBase executeMethod; protected HttpResponse response = null; protected Throwable responseException = null; protected XThreadInputStream threadStream = null; protected InputStream bodyStream = null; protected Charset charSet = null; protected boolean streamCreated = false; protected Throwable streamException = null; protected boolean abortThread = false; protected Throwable shutdownException = null; protected Throwable generalException = null; public BackgroundHTTPThread(HttpClient httpClient, HttpRequestBase executeMethod) { super(); setDaemon(true); this.httpClient = httpClient; this.executeMethod = executeMethod; } public void run() { try { try { // Call the execute method appropriately synchronized (this) { if (!abortThread) { try { response = httpClient.execute(executeMethod); } catch (java.net.SocketTimeoutException e) { responseException = e; } catch (ConnectTimeoutException e) { responseException = e; } catch (InterruptedIOException e) { throw e; } catch (Throwable e) { responseException = e; } this.notifyAll(); } } // Start the transfer of the content if (responseException == null) { synchronized (this) { if (!abortThread) { try { HttpEntity entity = response.getEntity(); bodyStream = entity.getContent(); if (bodyStream != null) { threadStream = new XThreadInputStream(bodyStream); try { ContentType ct = ContentType.get(entity); if (ct == null) charSet = null; else charSet = ct.getCharset(); } catch (ParseException e) { charSet = null; } } streamCreated = true; } catch (java.net.SocketTimeoutException e) { streamException = e; } catch (ConnectTimeoutException e) { streamException = e; } catch (InterruptedIOException e) { throw e; } catch (Throwable e) { streamException = e; } this.notifyAll(); } } } if (responseException == null && streamException == null) { if (threadStream != null) { // Stuff the content until we are done threadStream.stuffQueue(); } } } finally { if (bodyStream != null) { try { bodyStream.close(); } catch (IOException e) { } bodyStream = null; } synchronized (this) { try { executeMethod.abort(); } catch (Throwable e) { shutdownException = e; } this.notifyAll(); } } } catch (Throwable e) { // We catch exceptions here that should ONLY be InterruptedExceptions, as a result of the thread being aborted. this.generalException = e; } } public int getResponseCode() throws InterruptedException, IOException, HttpException { // Must wait until the response object is there while (true) { synchronized (this) { checkException(responseException); if (response != null) return response.getStatusLine().getStatusCode(); wait(); } } } public String getResponseStatus() throws InterruptedException, IOException, HttpException { // Must wait until the response object is there while (true) { synchronized (this) { checkException(responseException); if (response != null) return response.getStatusLine().toString(); wait(); } } } public Map<String,List<String>> getResponseHeaders() throws InterruptedException, IOException, HttpException { // Must wait for the response object to appear while (true) { synchronized (this) { checkException(responseException); if (response != null) { Header[] headers = response.getAllHeaders(); Map<String,List<String>> rval = new HashMap<String,List<String>>(); int i = 0; while (i < headers.length) { Header h = headers[i++]; String name = h.getName(); String value = h.getValue(); List<String> values = rval.get(name); if (values == null) { values = new ArrayList<String>(); rval.put(name,values); } values.add(value); } return rval; } wait(); } } } public String getFirstHeader(String headerName) throws InterruptedException, IOException, HttpException { // Must wait for the response object to appear while (true) { synchronized (this) { checkException(responseException); if (response != null) { Header h = response.getFirstHeader(headerName); if (h == null) return null; return h.getValue(); } wait(); } } } public InputStream getSafeInputStream() throws InterruptedException, IOException, HttpException { // Must wait until stream is created, or until we note an exception was thrown. while (true) { synchronized (this) { if (responseException != null) throw new IllegalStateException("Check for response before getting stream"); checkException(streamException); if (streamCreated) return threadStream; wait(); } } } public Charset getCharSet() throws InterruptedException, IOException, HttpException { while (true) { synchronized (this) { if (responseException != null) throw new IllegalStateException("Check for response before getting charset"); checkException(streamException); if (streamCreated) return charSet; wait(); } } } public void abort() { // This will be called during the finally // block in the case where all is well (and // the stream completed) and in the case where // there were exceptions. synchronized (this) { if (streamCreated) { if (threadStream != null) threadStream.abort(); } abortThread = true; } } public void finishUp() throws InterruptedException { join(); } protected synchronized void checkException(Throwable exception) throws IOException, HttpException { if (exception != null) { Throwable e = exception; if (e instanceof IOException) throw (IOException)e; else if (e instanceof HttpException) throw (HttpException)e; else if (e instanceof RuntimeException) throw (RuntimeException)e; else if (e instanceof Error) throw (Error)e; else throw new RuntimeException("Unhandled exception of type: "+e.getClass().getName(),e); } } } }