//$Header: /Development/AEDevelopment/projects/org.activebpel.rt.axis.bpel/src/org/activebpel/rt/axis/bpel/handlers/AeHTTPSender.java,v 1.8 2007/12/11 22:31:07 jbik Exp $
/////////////////////////////////////////////////////////////////////////////
//PROPRIETARY RIGHTS STATEMENT
//The contents of this file represent confidential information that is the 
//proprietary property of Active Endpoints, Inc.  Viewing or use of 
//this information is prohibited without the express written consent of 
//Active Endpoints, Inc. Removal of this PROPRIETARY RIGHTS STATEMENT 
//is strictly forbidden. Copyright (c) 2002-2004 All rights reserved. 
/////////////////////////////////////////////////////////////////////////////
package org.activebpel.rt.axis.bpel.handlers;

/*
 * 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.
 */

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.StringTokenizer;

import javax.wsdl.OperationType;
import javax.xml.soap.MimeHeader;
import javax.xml.soap.MimeHeaders;

import org.apache.axis.AxisFault;
import org.apache.axis.AxisProperties;
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.DefaultCommonsHTTPClientProperties;
import org.apache.axis.components.net.TransportClientProperties;
import org.apache.axis.components.net.TransportClientPropertiesFactory;
import org.apache.axis.description.OperationDesc;
import org.apache.axis.handlers.BasicHandler;
import org.apache.axis.soap.SOAP12Constants;
import org.apache.axis.soap.SOAPConstants;
import org.apache.axis.transport.http.CommonsHTTPSender;
import org.apache.axis.transport.http.HTTPConstants;
import org.apache.axis.utils.Messages;
import org.apache.commons.httpclient.Cookie;
import org.apache.commons.httpclient.Credentials;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread;
import org.apache.commons.logging.Log;

/**
 * This class uses Jakarta Commons's HttpClient to call a SOAP server.
 *
 * @author Davanum Srinivas (dims@yahoo.com)
 * History: By Chandra Talluri
 * Modifications done for maintaining sessions. Cookies needed to be set on
 * HttpState not on MessageContext, since ttpMethodBase overwrites the cookies
 * from HttpState. Also we need to setCookiePolicy on HttpState to
 * CookiePolicy.COMPATIBILITY else it is defaulting to RFC2109Spec and adding
 * Version information to it and tomcat server not recognizing it
 */
public class AeHTTPSender extends BasicHandler
{
   private static final String KEY_IDLE_TIMEOUT = "org.activebpel.httpsender.idle.timeout"; //$NON-NLS-1$
   private static final String KEY_IDLE_SWEEP_INTERVAL = "org.activebpel.httpsender.idle.sweep.interval"; //$NON-NLS-1$
   private static final int DEFAULT_IDLE_CONNECTION_TIMEOUT = 5000;
   private static final int DEFAULT_IDLE_CONNECTION_SWEEP_INTERVAL = 3000;

   /** Field log */
   protected static Log log = LogFactory.getLog(CommonsHTTPSender.class.getName());

   /** default value for the number of open connections per host */
   protected static final int DEFAULT_MAX_CONNECTIONS_PER_HOST = 100;
   /** default value for the total number of open connections allowed */
   protected static final int DEFAULT_MAX_CONNECTIONS = 500;

   /** cached reference to the connection manager */
   private static MultiThreadedHttpConnectionManager sConnectionManager;
   /** cached reference to the client properties */
   private static CommonsHTTPClientProperties sClientProperties;
   /** timeout thread responsible for closing idle connections */
   private static IdleConnectionTimeoutThread sConnectionTimeoutThread;

   private MultiThreadedHttpConnectionManager connectionManager;
   private CommonsHTTPClientProperties clientProperties;
   
   /** @deprecated */
   public AeHTTPSender()
   {
      initialize();
   }

   /** @deprecated */
   protected void initialize()
   {
      if (sConnectionManager == null)
      {
         synchronized (AeHTTPSender.class)
         {
            if (sConnectionManager == null)
            {
               // initialize the connection manager w/ its properties
               MultiThreadedHttpConnectionManager cm = new MultiThreadedHttpConnectionManager();
               sClientProperties = CommonsHTTPClientPropertiesFactory.create();
               
               // max connections per host, defaults to constant above
               int maxConnectionsPerHost = getIntegerOption(
                           DefaultCommonsHTTPClientProperties.MAXIMUM_CONNECTIONS_PER_HOST_PROPERTY_KEY,
                           DEFAULT_MAX_CONNECTIONS_PER_HOST);
               
               cm.setMaxConnectionsPerHost(maxConnectionsPerHost);
               
               // max connections, defaults to constant above
               int maxConnections = getIntegerOption(
                           DefaultCommonsHTTPClientProperties.MAXIMUM_TOTAL_CONNECTIONS_PROPERTY_KEY,
                           DEFAULT_MAX_CONNECTIONS);
               // as per apache-commons problem report #36882 (max connections per host setting does not work)
               // cm.setMaxTotalConnections(maxConnections);
               cm.getParams().setMaxTotalConnections(maxConnections);
               
               // save the connection manager for future calls
               sConnectionManager = cm;
               
               sConnectionTimeoutThread = new IdleConnectionTimeoutThread();               
               sConnectionTimeoutThread.addConnectionManager(sConnectionManager);
               
               int sweepInterval = getIntegerOption(KEY_IDLE_SWEEP_INTERVAL, DEFAULT_IDLE_CONNECTION_SWEEP_INTERVAL);
               sConnectionTimeoutThread.setTimeoutInterval(sweepInterval);

               int idleTimeout = getIntegerOption(KEY_IDLE_TIMEOUT, DEFAULT_IDLE_CONNECTION_TIMEOUT);
               sConnectionTimeoutThread.setConnectionTimeout(idleTimeout);
               sConnectionTimeoutThread.start();
            }
         }
      }
      connectionManager = sConnectionManager;
      clientProperties = sClientProperties;
   }

   /**
    * Gets the option from our handler or returns the default value if not specified.
    * @param aKey
    * @param aDefaultValue
    */
   protected int getIntegerOption(String aKey, int aDefaultValue)
   {
      int value = aDefaultValue;
      try
      {
         Object obj = AxisProperties.getProperty(aKey, null);
         if (obj != null)
         {
            value = Integer.parseInt(obj.toString());
         }
      }
      catch (Exception e)
      {
         // eat any exception and reply w/ default value below
      }
      return value;
   }

   /**
    * Shuts down the connection manager and clears the reference. 
    */
   public static synchronized void shutdown()
   {
      if (sConnectionManager != null)
      {
         sConnectionManager.shutdown();
         sConnectionManager = null;
      }
      
      if (sConnectionTimeoutThread != null)
      {
         sConnectionTimeoutThread.shutdown();
         sConnectionTimeoutThread.interrupt();
         sConnectionTimeoutThread = null;
      }
   }

   /**
    * 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 message context
    *
    * @throws AxisFault
    * @deprecated
    */
   public void invoke(MessageContext msgContext) throws AxisFault
   {
      HttpMethodBase method = null;
      if (log.isDebugEnabled()) {
         log.debug(Messages.getMessage("enter00", //$NON-NLS-1$
               "CommonsHTTPSender::invoke")); //$NON-NLS-1$
      }
      try {
            URL targetURL =
            new URL(msgContext.getStrProp(MessageContext.TRANS_URL));

         // no need to retain these, as the cookies/credentials are
         // stored in the message context across multiple requests.
         // the underlying connection manager, however, is retained
         // so sockets get recycled when possible.
         HttpClient httpClient = new HttpClient(connectionManager);
         // the timeout value for allocation of connections from the pool
            httpClient.setHttpConnectionFactoryTimeout(clientProperties.getConnectionPoolTimeout());

            HostConfiguration hostConfiguration = getHostConfiguration(httpClient, targetURL);
         httpClient.setHostConfiguration(hostConfiguration);

         // look for option to send credentials preemptively (w/out challenge)
         // Control of Preemptive is controlled via policy on a per call basis.
         String preemptive = (String) msgContext.getProperty("HTTPPreemptive"); //$NON-NLS-1$
         if ("true".equals(preemptive)) //$NON-NLS-1$
         {
             httpClient.getParams().setAuthenticationPreemptive(true);
         }

         String webMethod = null;
         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) {
            webMethod = msgContext.getStrProp(SOAP12Constants.PROP_WEBMETHOD);
                if (webMethod != null) {
               posting = webMethod.equals(HTTPConstants.HEADER_POST);
            }
         }

         Message reqMessage = msgContext.getRequestMessage();
         if (posting)
         {
            method = new PostMethod(targetURL.toString());
            addContextInfo(method, httpClient, msgContext, targetURL);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            reqMessage.writeTo(baos);
            ((PostMethod) method).setRequestBody(new ByteArrayInputStream(baos.toByteArray()));
            ((PostMethod) method).setUseExpectHeader(false); // workaround for
         }
         else
         {
            method = new GetMethod(targetURL.toString());
            addContextInfo(method, httpClient, msgContext, targetURL);
         }
         // don't forget the cookies!
         // Cookies need to be set on HttpState, since HttpMethodBase
         // overwrites the cookies from HttpState
         if (msgContext.getMaintainSession())
         {
            HttpState state = httpClient.getState();
            state.setCookiePolicy(CookiePolicy.COMPATIBILITY);
            String host = hostConfiguration.getHost();
            String path = targetURL.getPath();
            boolean secure = hostConfiguration.getProtocol().isSecure();
            String ck1 = (String) msgContext.getProperty(HTTPConstants.HEADER_COOKIE);

            String ck2 = (String) msgContext.getProperty(HTTPConstants.HEADER_COOKIE2);
            if (ck1 != null)
            {
               int index = ck1.indexOf('=');
               state.addCookie(new Cookie(host, ck1.substring(0, index), ck1.substring(index + 1), path, null, secure));
            }
            if (ck2 != null)
            {
               int index = ck2.indexOf('=');
               state.addCookie(new Cookie(host, ck2.substring(0, index), ck2.substring(index + 1), path, null, secure));
            }
            httpClient.setState(state);
         }
         boolean hasSoapFault = false;
         int returnCode = httpClient.executeMethod(method);
         String contentType = null;
         String contentLocation = null;
         String contentLength = null;
         if (method.getResponseHeader(HTTPConstants.HEADER_CONTENT_TYPE) != null)
         {
            contentType = method.getResponseHeader(
                  HTTPConstants.HEADER_CONTENT_TYPE).getValue();
         }
         if (method.getResponseHeader(HTTPConstants.HEADER_CONTENT_LOCATION) != null)
         {
            contentLocation = method.getResponseHeader(
                  HTTPConstants.HEADER_CONTENT_LOCATION).getValue();
         }
         if (method.getResponseHeader(HTTPConstants.HEADER_CONTENT_LENGTH) != null)
         {
            contentLength = method.getResponseHeader(
                  HTTPConstants.HEADER_CONTENT_LENGTH).getValue();
         }
         contentType = (null == contentType) ? null : contentType.trim();
         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") //$NON-NLS-1$
               && ((returnCode > 499) && (returnCode < 600)))
         {

            // SOAP Fault should be in here - so fall through
            hasSoapFault = true;
         }
         else
         {
            String statusMessage = method.getStatusText();
            AxisFault fault = new AxisFault("HTTP", //$NON-NLS-1$
                  "(" + returnCode + ")" //$NON-NLS-1$ //$NON-NLS-2$
                        + statusMessage, null, null);

            try
            {
               fault.setFaultDetailString(Messages.getMessage("return01", //$NON-NLS-1$
                     "" + returnCode, method.getResponseBodyAsString())); //$NON-NLS-1$
               fault.addFaultDetail(Constants.QNAME_FAULTDETAIL_HTTPERRORCODE,
                     Integer.toString(returnCode));
               throw fault;
            }
            finally
            {
               method.releaseConnection(); // release connection back to pool.
            }
         }

         // wrap the response body stream so that close() also releases the connection back to the pool.
         InputStream releaseConnectionOnCloseStream = createConnectionReleasingInputStream(method);

         Message outMsg = new Message(releaseConnectionOnCloseStream, false,
               contentType, contentLocation);
         // Transfer HTTP headers of HTTP message to MIME headers of SOAP message
         Header[] responseHeaders = method.getResponseHeaders();
         MimeHeaders responseMimeHeaders = outMsg.getMimeHeaders();
         for (int i = 0; i < responseHeaders.length; i++)
         {
            Header responseHeader = responseHeaders[i];
            responseMimeHeaders.addHeader(responseHeader.getName(),
                  responseHeader.getValue());
         }

         OperationDesc operation = msgContext.getOperation();
         if (hasSoapFault || operation.getMep().equals(OperationType.REQUEST_RESPONSE))
         {
            msgContext.setResponseMessage(outMsg);
         }
         else
         {
            // Change #1
            //
            // If the operation is a one-way, then don't set the response
            // on the msg context. Doing so will cause Axis to attempt to
            // read from a non-existent SOAP message which causes errors.
            //
            // Note: also checking to see if the return type is our "VOID"
            // QName from the AeInvokeHandler since that's our workaround
            // for avoiding Axis's Thread creation in Call.invokeOneWay()
            //
            // Since the message context won't have a chance to consume the
            // response stream (which closes the connection), close the
            // connection here.
            method.releaseConnection();
         }

         if (log.isDebugEnabled())
         {
            if (null == contentLength)
            {
               log.debug("\n" //$NON-NLS-1$
                     + Messages.getMessage("no00", "Content-Length")); //$NON-NLS-1$ //$NON-NLS-2$
            }
            log.debug("\n" + Messages.getMessage("xmlRecd00")); //$NON-NLS-1$ //$NON-NLS-2$
            log.debug("-----------------------------------------------"); //$NON-NLS-1$
            log.debug(outMsg.getSOAPPartAsString());
         }

         // if we are maintaining session state,
         // handle cookies (if any)
         if (msgContext.getMaintainSession())
         {
            Header[] headers = method.getResponseHeaders();
            for (int i = 0; i < headers.length; i++)
            {
               if (headers[i].getName().equalsIgnoreCase(
                     HTTPConstants.HEADER_SET_COOKIE))
                  msgContext.setProperty(HTTPConstants.HEADER_COOKIE,
                        cleanupCookie(headers[i].getValue()));
               else if (headers[i].getName().equalsIgnoreCase(
                     HTTPConstants.HEADER_SET_COOKIE2))
                  msgContext.setProperty(HTTPConstants.HEADER_COOKIE2,
                        cleanupCookie(headers[i].getValue()));
            }

         }

      }
      catch (Throwable t)
      {
         log.debug(t);

         if (method != null)
         {
            method.releaseConnection();
         }

         // We can call Axis.makeFault() if it's an exception; otherwise
         // construct the AxisFault directly.
         throw (t instanceof Exception) ? AxisFault.makeFault((Exception) t) : new AxisFault(t.getLocalizedMessage(), t);
      }

      if (log.isDebugEnabled())
      {
         log.debug(Messages.getMessage("exit00", //$NON-NLS-1$
               "CommonsHTTPSender::invoke")); //$NON-NLS-1$
      }
   }

   /**
    * cleanup the cookie value.
    *
    * @param cookie initial cookie value
    *
    * @return a cleaned up cookie value.
    */
   private String cleanupCookie(String cookie)
   {
      cookie = cookie.trim();
      // chop after first ; a la Apache SOAP (see HTTPUtils.java there)
      int index = cookie.indexOf(';');

      if (index != -1)
      {
         cookie = cookie.substring(0, index);
      }
      return cookie;
   }

   /**
    * @deprecated
    */
   private HostConfiguration getHostConfiguration(HttpClient client,
         URL targetURL)
   {
      TransportClientProperties tcp = TransportClientPropertiesFactory
            .create(targetURL.getProtocol()); // http or https
      int port = targetURL.getPort();
      boolean hostInNonProxyList = isHostInNonProxyList(targetURL.getHost(),
            tcp.getNonProxyHosts());

      HostConfiguration config = new HostConfiguration();

      if (port == -1)
      {
         port = 80; // even for https
      }

      if (hostInNonProxyList)
      {
         config.setHost(targetURL.getHost(), port, targetURL.getProtocol());
      }
      else
      {
         if (tcp.getProxyHost().length() == 0
               || tcp.getProxyPort().length() == 0)
         {
            config.setHost(targetURL.getHost(), port, targetURL.getProtocol());
         }
         else
         {
            if (tcp.getProxyUser().length() != 0)
            {
               Credentials proxyCred = new UsernamePasswordCredentials(tcp
                     .getProxyUser(), tcp.getProxyPassword());
               client.getState().setProxyCredentials(null, null, proxyCred);
            }
            int proxyPort = new Integer(tcp.getProxyPort()).intValue();
            config.setProxy(tcp.getProxyHost(), proxyPort);
         }
      }
      return config;
   }

   /**
    * Extracts info from message context.
    *
    * @param method Post method
    * @param httpClient The client used for posting
    * @param msgContext the message context
    * @param tmpURL the url to post to.
    *
    * @throws Exception
    * @deprecated
    */
   private void addContextInfo(HttpMethodBase method, HttpClient httpClient,
         MessageContext msgContext, URL tmpURL) throws Exception
   {

      // optionally set a timeout for the request
      if (msgContext.getTimeout() != 0)
      {
         /* ISSUE: these are not the same, but MessageContext has only one
          definition of timeout */
         // SO_TIMEOUT -- timeout for blocking reads
         httpClient.setTimeout(msgContext.getTimeout());
         // timeout for initial connection
         httpClient.setConnectionTimeout(msgContext.getTimeout());
      }

      // Get SOAPAction, default to ""
      String action = msgContext.useSOAPAction() ? msgContext
            .getSOAPActionURI() : ""; //$NON-NLS-1$

      if (action == null)
      {
         action = ""; //$NON-NLS-1$
      }
      Message msg = msgContext.getRequestMessage();
      if (msg != null)
      {
         method.setRequestHeader(new Header(HTTPConstants.HEADER_CONTENT_TYPE,
               msg.getContentType(msgContext.getSOAPConstants())));
      }
      method.setRequestHeader(new Header(HTTPConstants.HEADER_SOAP_ACTION,
            "\"" + action + "\"")); //$NON-NLS-1$ //$NON-NLS-2$
      String userID = msgContext.getUsername();
      String passwd = msgContext.getPassword();

      // if UserID is not part of the context, but is in the URL, use
      // the one in the URL.
      if ((userID == null) && (tmpURL.getUserInfo() != null))
      {
         String info = tmpURL.getUserInfo();
         int sep = info.indexOf(':');

         if ((sep >= 0) && (sep + 1 < info.length()))
         {
            userID = info.substring(0, sep);
            passwd = info.substring(sep + 1);
         }
         else
         {
            userID = info;
         }
      }
      if (userID != null)
      {
         Credentials cred = new UsernamePasswordCredentials(userID, passwd);
         httpClient.getState().setCredentials(null, null, cred);

         // Change #2
         //
         // Comment out the lines below since they force all authentication
         // to be Basic. This is a problem if the web service you're invoking 
         // is expecting Digest.

         // The following 3 lines should NOT be required. But Our SimpleAxisServer fails
         // during all-tests if this is missing.
         //            StringBuffer tmpBuf = new StringBuffer();
         //            tmpBuf.append(userID).append(":").append((passwd == null) ? "" : passwd);
         //            method.addRequestHeader(HTTPConstants.HEADER_AUTHORIZATION, "Basic " + Base64.encode(tmpBuf.toString().getBytes()));
      }

      // Transfer MIME headers of SOAPMessage to HTTP headers.
      MimeHeaders mimeHeaders = msg.getMimeHeaders();
      if (mimeHeaders != null) 
      {
      	for (Iterator i = mimeHeaders.getAllHeaders(); i.hasNext(); ) {
            MimeHeader mimeHeader = (MimeHeader) i.next();
            method.addRequestHeader(mimeHeader.getName(), mimeHeader.getValue());
         }
      }

      // process user defined headers for information.
      Hashtable userHeaderTable = (Hashtable) msgContext.getProperty(HTTPConstants.REQUEST_HEADERS);

      if (userHeaderTable != null)
      {
         for (java.util.Iterator e = userHeaderTable.entrySet().iterator(); e.hasNext();)
         {
            java.util.Map.Entry me = (java.util.Map.Entry) e.next();
            Object keyObj = me.getKey();

            if (null == keyObj)
            {
               continue;
            }
            String key = keyObj.toString().trim();
            String value = me.getValue().toString().trim();

            method.addRequestHeader(key, value);
         }
      }
   }

   /**
    * Check if the specified host is in the list of non proxy hosts.
    *
    * @param host host name
    * @param nonProxyHosts string containing the list of non proxy hosts
    *
    * @return true/false
    */
   protected boolean isHostInNonProxyList(String host, String nonProxyHosts)
   {

      if ((nonProxyHosts == null) || (host == null))
      {
         return false;
      }

      /*
       * The http.nonProxyHosts system property is a list enclosed in
       * double quotes with items separated by a vertical bar.
       */
      StringTokenizer tokenizer = new StringTokenizer(nonProxyHosts, "|\""); //$NON-NLS-1$

      while (tokenizer.hasMoreTokens())
      {
         String pattern = tokenizer.nextToken();

         if (log.isDebugEnabled())
         {
            log.debug(Messages.getMessage("match00", //$NON-NLS-1$
                  new String[] { "HTTPSender", //$NON-NLS-1$
                        host, pattern }));
         }
         if (match(pattern, host, false))
         {
            return true;
         }
      }
      return false;
   }

   /**
    * Matches a string against a pattern. The pattern contains two special
    * characters:
    * '*' which means zero or more characters,
    *
    * @param pattern the (non-null) pattern to match against
    * @param str     the (non-null) string that must be matched against the
    *                pattern
    * @param isCaseSensitive
    *
    * @return <code>true</code> when the string matches against the pattern,
    *         <code>false</code> otherwise.
    */
   protected static boolean match(String pattern, String str,
         boolean isCaseSensitive)
   {

      char[] patArr = pattern.toCharArray();
      char[] strArr = str.toCharArray();
      int patIdxStart = 0;
      int patIdxEnd = patArr.length - 1;
      int strIdxStart = 0;
      int strIdxEnd = strArr.length - 1;
      char ch;
      boolean containsStar = false;

      for (int i = 0; i < patArr.length; i++)
      {
         if (patArr[i] == '*')
         {
            containsStar = true;
            break;
         }
      }
      if (!containsStar)
      {

         // No '*'s, so we make a shortcut
         if (patIdxEnd != strIdxEnd)
         {
            return false; // Pattern and string do not have the same size
         }
         for (int i = 0; i <= patIdxEnd; i++)
         {
            ch = patArr[i];
            if (isCaseSensitive && (ch != strArr[i]))
            {
               return false; // Character mismatch
            }
            if (!isCaseSensitive
                  && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[i])))
            {
               return false; // Character mismatch
            }
         }
         return true; // String matches against pattern
      }
      if (patIdxEnd == 0)
      {
         return true; // Pattern contains only '*', which matches anything
      }

      // Process characters before first star
      while ((ch = patArr[patIdxStart]) != '*' && (strIdxStart <= strIdxEnd))
      {
         if (isCaseSensitive && (ch != strArr[strIdxStart]))
         {
            return false; // Character mismatch
         }
         if (!isCaseSensitive
               && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[strIdxStart])))
         {
            return false; // Character mismatch
         }
         patIdxStart++;
         strIdxStart++;
      }
      if (strIdxStart > strIdxEnd)
      {

         // All characters in the string are used. Check if only '*'s are
         // left in the pattern. If so, we succeeded. Otherwise failure.
         for (int i = patIdxStart; i <= patIdxEnd; i++)
         {
            if (patArr[i] != '*')
            {
               return false;
            }
         }
         return true;
      }

      // Process characters after last star
      while ((ch = patArr[patIdxEnd]) != '*' && (strIdxStart <= strIdxEnd))
      {
         if (isCaseSensitive && (ch != strArr[strIdxEnd]))
         {
            return false; // Character mismatch
         }
         if (!isCaseSensitive
               && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[strIdxEnd])))
         {
            return false; // Character mismatch
         }
         patIdxEnd--;
         strIdxEnd--;
      }
      if (strIdxStart > strIdxEnd)
      {

         // All characters in the string are used. Check if only '*'s are
         // left in the pattern. If so, we succeeded. Otherwise failure.
         for (int i = patIdxStart; i <= patIdxEnd; i++)
         {
            if (patArr[i] != '*')
            {
               return false;
            }
         }
         return true;
      }

      // process pattern between stars. padIdxStart and patIdxEnd point
      // always to a '*'.
      while ((patIdxStart != patIdxEnd) && (strIdxStart <= strIdxEnd))
      {
         int patIdxTmp = -1;

         for (int i = patIdxStart + 1; i <= patIdxEnd; i++)
         {
            if (patArr[i] == '*')
            {
               patIdxTmp = i;
               break;
            }
         }
         if (patIdxTmp == patIdxStart + 1)
         {

            // Two stars next to each other, skip the first one.
            patIdxStart++;
            continue;
         }

         // Find the pattern between padIdxStart & padIdxTmp in str between
         // strIdxStart & strIdxEnd
         int patLength = (patIdxTmp - patIdxStart - 1);
         int strLength = (strIdxEnd - strIdxStart + 1);
         int foundIdx = -1;

         strLoop: for (int i = 0; i <= strLength - patLength; i++)
         {
            for (int j = 0; j < patLength; j++)
            {
               ch = patArr[patIdxStart + j + 1];
               if (isCaseSensitive && (ch != strArr[strIdxStart + i + j]))
               {
                  continue strLoop;
               }
               if (!isCaseSensitive
                     && (Character.toUpperCase(ch) != Character.toUpperCase(strArr[strIdxStart + i + j])))
               {
                  continue strLoop;
               }
            }
            foundIdx = strIdxStart + i;
            break;
         }
         if (foundIdx == -1)
         {
            return false;
         }
         patIdxStart = patIdxTmp;
         strIdxStart = foundIdx + patLength;
      }

      // All characters in the string are used. Check if only '*'s are left
      // in the pattern. If so, we succeeded. Otherwise failure.
      for (int i = patIdxStart; i <= patIdxEnd; i++)
      {
         if (patArr[i] != '*')
         {
            return false;
         }
      }
      return true;
   }

   private InputStream createConnectionReleasingInputStream(
         final HttpMethodBase method) throws IOException
   {
      return new FilterInputStream(method.getResponseBodyAsStream())
      {
         public void close() throws IOException
         {
            try
            {
               super.close();
            }
            finally
            {
               method.releaseConnection();
            }
         }
      };
   }
}

