All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.lastbamboo.common.http.client.HttpClientRunner Maven / Gradle / Ivy

package org.lastbamboo.common.http.client;

import java.io.IOException;
import java.io.InputStream;
import java.net.SocketTimeoutException;
import java.util.zip.GZIPInputStream;

import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.LongRange;
import org.littleshoot.util.InputStreamHandler;
import org.littleshoot.util.RuntimeIoException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Performs an HTTP download using HttpClient.
 */
public final class HttpClientRunner implements Runnable
    {

    /**
     * Logger for this class.
     */
    private final Logger m_log = 
        LoggerFactory.getLogger(HttpClientRunner.class);

    private final InputStreamHandler m_inputStreamHandler;

    /**
     * Listener for download events.
     */
    private final HttpListener m_listener;

    private final CommonsHttpClient m_httpClient;

    private final HttpMethod m_httpMethod;

    /**
     * Creates a new HTTP client Runnable with the specified
     * collaborating classes.
     *
     * @param handler The class that should receive the InputStream
     * for the HTTP message body.
     * @param client The HttpClient instance that will send the
     * HTTP request.
     * @param method The HTTP method handler for the request.  This could be,
     * "GET" or "HEAD", for example.
     * @param listener The listener for HTTP events during the download.
     */
    public HttpClientRunner(final InputStreamHandler handler,
        final CommonsHttpClient client, final HttpMethod method,
        final HttpListener listener)
        {
        this.m_inputStreamHandler = handler;
        this.m_httpClient = client;
        this.m_httpMethod = method;
        this.m_listener = listener;
        }

    /**
     * Performs the download on a separate thread.
     */
    public void run()
        {
        try
            {
            handleDownload();
            }
        catch (final RuntimeIoException e)
            {
            m_log.debug("Could not connect to host or connection lost?", e);
            this.m_listener.onFailure();
            }
        catch (final Throwable t)
            {
            m_log.error("Unexpected exception", t);
            this.m_listener.onFailure();
            }
        finally
            {
            // Release the connection back to the pool.
            try
                {
                m_log.debug("Releasing HTTP connection...");
                m_httpMethod.releaseConnection();
                }
            catch (final RuntimeException e)
                {
                m_log.warn("Exception releasing connection", e);
                }
            m_log.trace("Released connection...");
            }
        }

    /**
     * Handles the download request catching any expected exceptions.
     */
    private void handleDownload()
        {
        this.m_listener.onStatusEvent("Connecting...");
        this.m_listener.onDownloadStarted ();
        try
            {
            executeHttpRequest ();
            }
        catch (final HttpException e)
            {
            m_log.warn("HTTP exception downloading, method: " + 
                this.m_httpMethod.getPath() + " " + e.getReason(), e);
            final int reasonCode = e.getReasonCode();
            final String status;
            if (reasonCode >= 100 && reasonCode < 600)
                {
                status = "Could Not Access User";
                }
            else
                {
                status = "Unknown response";
                }

            this.m_listener.onStatusEvent(status);
            this.m_listener.onHttpException(e);
            }
        catch (final SocketTimeoutException e)
            {
            warn("Socket timeout: ", e);
            this.m_listener.onFailure();
            }
        catch (final IOException e)
            {
            m_log.debug("Connection or copying error", e);
            this.m_listener.onFailure();
            }
        }

    private void warn(final String msg, final Throwable t)
        {
        try
            {
            m_log.warn(msg + this.m_httpMethod.getURI(), t);
            }
        catch (final URIException e)
            {
            }
        }

    /**
     * Executes the HTTP request for the download.
     *
     * @throws IOException If an I/O error occurs.
     */
    private void executeHttpRequest () throws IOException
        {
        m_log.trace ("Sending HTTP GET request for: {}", 
            this.m_httpMethod.getQueryString());
        
        final long start = System.currentTimeMillis();
        m_httpClient.executeMethod (m_httpMethod);
        final long connected = System.currentTimeMillis();

        m_log.trace ("Received status code: " + m_httpMethod.getStatusCode ());

        // Notify the listener that we've connected to the user.
        this.m_listener.onConnect(connected-start);
        
        final int statusCode = m_httpMethod.getStatusCode ();
        if (statusCode == HttpStatus.SC_OK)
            {
            onTwoHundredResponse();
            }
        else if (statusCode == HttpStatus.SC_PARTIAL_CONTENT)
            {
            onPartialContent();
            }
        else
            {
            onNonTwoHundredLevelResponse();
            }
        }

    /**
     * Handles the case where we receive a non-200 level response.
     * 
     * @throws IOException If we can't read the response stream.
     */
    private void onNonTwoHundredLevelResponse () throws IOException
        {
        final InputStream inputStream = m_httpMethod.getResponseBodyAsStream ();
        try
            {
            // Just send it into the ether.  We just need to make sure we still 
            // read the body here.
            IOUtils.copy(inputStream, new NullOutputStream());
            m_log.warn("Did not receive 200 OK response for request to URI: " +
                this.m_httpMethod.getURI() + "\n"+
                "\nRequest headers:\n"+
                headerString(this.m_httpMethod.getRequestHeaders())+
                "\nResponse:\n" +
                this.m_httpMethod.getStatusLine() + "\n" +
                headerString(this.m_httpMethod.getResponseHeaders()));
            }
        catch (final URIException e)
            {
            m_log.error("Could not resolve URI", e);
            }
        finally
            {
            IOUtils.closeQuietly(inputStream);
            m_listener.onNoTwoHundredOk (m_httpMethod.getStatusCode ());
            }

        }

    private static String headerString(final Header[] headers)
        {
        final StringBuilder sb = new StringBuilder();
        for (int i = 0; i < headers.length; i++)
            {
            sb.append(headers[i].getName());
            sb.append(": ");
            sb.append(headers[i].getValue());
            sb.append("\n");
            }
        return sb.toString();
        }

    private void onPartialContent() throws IOException
        {
        final Header rangeHeader = 
            this.m_httpMethod.getResponseHeader ("Content-Range");

        m_log.debug("Received range header: {}", rangeHeader);
        if (rangeHeader == null)
            {
            m_log.warn("No range header!!");
            throw new IOException("Received Partial Content response with " +
                "no Content-Range header");
            }
        
        notifyRange(rangeHeader);
        onTwoHundredResponse();
        }
    
    /**
     * The method called when a 200-level response is received when making 
     * the HTTP request.
     *
     * @throws IOException If an I/O error occurs.
     */
    private void onTwoHundredResponse () throws IOException
        {
        //m_log.trace("Received "+this.m_httpMethod.getStatusCode() + 
        //    " for request: " +  this.m_httpMethod.getPath());
        
        final Header length = m_httpMethod.getResponseHeader ("Content-Length");
        if (length != null)
            {
            m_listener.onContentLength(
                Long.parseLong (length.getValue ()));
            }
        
        final Header rangeHeader = 
            this.m_httpMethod.getResponseHeader ("Content-Range");
        if (rangeHeader != null)
            {
            notifyRange(rangeHeader);
            }
        
        // Uncompress the data if it's gzipped, otherwise just process it.
        final Header contentEncoding =
            m_httpMethod.getResponseHeader("Content-Encoding");

        // Note:  We don't close this input stream in a finally block because 
        // it's ultimately the input stream of the socket, and we likely want
        // to re-use it.  We just let HttpClient take care of connection
        // management.
        final InputStream inputStream = m_httpMethod.getResponseBodyAsStream ();

        m_log.debug("Got input stream...sending to: {}", m_inputStreamHandler);
        if (contentEncoding == null)
            {
            handleInputStream(inputStream);
            }
        else if (StringUtils.contains (contentEncoding.getValue (), "gzip"))
            {
            m_log.trace("Handling gzipped message body...");
            handleInputStream(new GZIPInputStream (inputStream));
            }
        else
            {
            m_log.warn("Unrecognized content encoding: {}", contentEncoding);
            }
        }

    private void handleInputStream(final InputStream inputStream) 
        throws IOException
        {
        try
            {
            m_inputStreamHandler.handleInputStream (inputStream);
            m_listener.onMessageBodyRead ();
            }
        catch (final NoContentRangeException e)
            {
            // This is not recoverable -- it's unlikely the server will
            // provide a content range the next time around.
            this.m_listener.onPermanentFailure();
            }
        }

    private void notifyRange(final Header rangeHeader) throws IOException
        {
        String rangeString = rangeHeader.getValue().trim();
        if (!rangeString.startsWith("bytes"))
            {
            m_log.warn("Bad header!!  {}", rangeHeader);
            this.m_listener.onBadHeader(rangeHeader.toString());
            throw new IOException("Could not read header: "+rangeHeader);
            }
        
        rangeString = 
            StringUtils.substringBetween(rangeString, "bytes", "/").trim();
        
        final String minString = 
            StringUtils.substringBefore(rangeString, "-").trim();
        final String maxString = 
            StringUtils.substringAfter(rangeString, "-").trim();
        final long min = Long.parseLong(minString);
        final long max = Long.parseLong(maxString);
        final LongRange range = new LongRange(min, max);
        m_listener.onContentRange(range);
        }
    }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy