Package org.apache.ace.agent.impl

Source Code of org.apache.ace.agent.impl.ContentRangeInputStream

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you 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.
*/
package org.apache.ace.agent.impl;

import static org.apache.ace.agent.impl.ConnectionUtil.DEFAULT_RETRY_TIME;
import static org.apache.ace.agent.impl.ConnectionUtil.HTTP_RETRY_AFTER;
import static org.apache.ace.agent.impl.ConnectionUtil.handleIOException;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

import org.apache.ace.agent.ConnectionHandler;
import org.apache.ace.agent.RetryAfterException;

/**
* Abstraction for {@link HttpURLConnection}s that might use content range headers, or partial responses, to return the
* contents of an URL.
* <p>
* This implementation is capable of handling partial content requests, using the HTTP 216 response code, and tries to
* follow the recommendations as specified in RFC 2616 as closely as possible. This implementation deviates from the RFC
* by allowing the Content-Range header to be lacking both the last-byte-pos and a resource-length, as used in
* {@link ContentRangeResponseWrapper}. This is an optimization that is used for ACE as we do not want to, nor can, know
* the exact length of some of the streams we're sending.
* </p>
*
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
*/
class ContentRangeInputStream extends InputStream {
    private static final String HDR_CONTENT_RANGE = "Content-Range";
    private static final String HDR_RANGE = "Range";

    private static final String BYTES = "bytes";
    private static final String BYTES_ = BYTES.concat(" ");

    private static final int SC_OK = 200;
    private static final int SC_PARTIAL_CONTENT = 206;
    private static final int SC_RANGE_NOT_SATISFIABLE = 416;
    private static final int SC_SERVICE_UNAVAILABLE = 503;

    private static final int ST_EOF = -1;
    private static final int ST_INITIAL = 0;
    private static final int ST_OPEN = 1;
    private static final int ST_CLOSED = 2;

    private final ConnectionHandler m_handler;
    private final URL m_url;
    private final int m_chunkSize;

    // see ST_* constants...
    private volatile int m_state;
    private volatile URLConnection m_conn;
    // administration...
    private volatile long m_readTotal;
    private volatile long m_readChunk;
    private volatile long[] m_contentInfo;

    /**
     * Creates a new {@link ContentRangeInputStream} instance.
     *
     * @param handler
     *            the connection handler to use, cannot be <code>null</code>;
     * @param url
     *            the URL to connect to, cannot be <code>null</code>.
     */
    public ContentRangeInputStream(ConnectionHandler handler, URL url) {
        this(handler, url, 0L);
    }

    /**
     * Creates a new {@link ContentRangeInputStream} instance.
     *
     * @param handler
     *            the connection handler to use, cannot be <code>null</code>;
     * @param url
     *            the URL to connect to, cannot be <code>null</code>;
     * @param startOffset
     *            the starting offset to start reading from, >= 0.
     */
    public ContentRangeInputStream(ConnectionHandler handler, URL url, long startOffset) {
        this(handler, url, startOffset, -1);
    }

    /**
     * Creates a new {@link ContentRangeInputStream} instance.
     *
     * @param handler
     *            the connection handler to use, cannot be <code>null</code>;
     * @param url
     *            the URL to connect to, cannot be <code>null</code>;
     * @param startOffset
     *            the starting offset to start reading from, >= 0;
     * @param chunkSize
     *            the number of bytes to request in each chunk, use <tt>-1</tt> to read as many bytes as possible in
     *            each chunk.
     */
    public ContentRangeInputStream(ConnectionHandler handler, URL url, long startOffset, int chunkSize) {
        if (handler == null) {
            throw new IllegalArgumentException("Handler cannot be null!");
        }
        if (url == null) {
            throw new IllegalArgumentException("URL cannot be null!");
        }
        m_handler = handler;
        m_url = url;

        m_state = ST_INITIAL;
        m_readTotal = startOffset;
        m_chunkSize = chunkSize;
    }

    @Override
    public int available() throws IOException {
        assertOpen();

        if (!prepareNextChunk()) {
            return 0;
        }

        long chunkSize = m_contentInfo[0];
        if (chunkSize < 0) {
            // size not available (yet)...
            return 0;
        }
        return (int) ((chunkSize - m_readChunk) & 0xFFFFFFFFL);
    }

    @Override
    public void close() throws IOException {
        if (m_state != ST_CLOSED) {
            m_state = ST_CLOSED;

            closeChunk();
        }
    }

    @Override
    public int read() throws IOException {
        assertOpen();

        if (!prepareNextChunk()) {
            return -1;
        }

        InputStream is = getInputStream();

        int result = is.read();
        if (result > 0) {
            m_readChunk++;
            m_readTotal++;
        }
        // End of chunk?!
        if ((m_contentInfo[0] > 0) && (m_readChunk >= m_contentInfo[0])) {
            closeChunk();
        }

        return result;
    }

    /**
     * {@inheritDoc}
     * <p>
     * Overridden for efficiency reasons.
     * </p>
     */
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        assertOpen();

        if (!prepareNextChunk()) {
            return -1;
        }

        InputStream is = getInputStream();

        int read = is.read(b, off, len);
        if (read >= 0) {
            m_readChunk += read;
            m_readTotal += read;
        }
        // End of chunk?!
        if ((m_contentInfo[0] > 0) && (m_readChunk >= m_contentInfo[0])) {
            closeChunk();
        }
        return read;
    }

    /**
     * Adds the HTTP-Range header to a given URL connection.
     *
     * @param conn
     *            the URL connection to add the HTTP-Range header to, cannot be <code>null</code>.
     */
    private void applyRangeHeader(URLConnection conn) {
        if (m_readTotal > 0L || m_chunkSize > 0) {
            if (conn instanceof HttpURLConnection) {
                String rangeHeader;
                if (m_chunkSize > 0) {
                    rangeHeader = String.format("%s=%d-%d", BYTES, m_readTotal, (m_readTotal + m_chunkSize));
                }
                else {
                    rangeHeader = String.format("%s=%d-", BYTES, m_readTotal);
                }
                conn.setRequestProperty(HDR_RANGE, rangeHeader);
            }
            else {
                // Non-HTTP connection, skip the first few bytes when calling this method for the first time...
                if (m_contentInfo == null) {
                    long skip = m_readTotal;
                    ConnectionUtil.skip(conn, skip);
                }
            }
        }
    }

    /**
     * Verifies that this stream (and the underlying URL connection) is open.
     *
     * @throws IOException
     *             in case this stream is already closed.
     */
    private void assertOpen() throws IOException {
        if (m_state == ST_CLOSED) {
            throw new IOException("Trying to read from closed stream!");
        }
        else if (m_state != ST_EOF) {
            m_state = ST_OPEN;
        }
    }

    /**
     * Closes the current chunk-connection, if necessary.
     */
    private void closeChunk() {
        if (m_conn != null) {
            m_conn = ConnectionUtil.close(m_conn);
            m_readChunk = 0;
        }
    }

    /**
     * @return <code>true</code> if there is content remaining to be read, <code>false</code> otherwise.
     */
    private boolean contentRemaining() {
        int state = m_state;
        if (state == ST_EOF) {
            return false;
        }
        else if (m_contentInfo == null) {
            // no information yet about the content, so we must read it first...
            return true;
        }
        long totalSize = m_contentInfo[1];
        if ((totalSize > 0L) && (m_readTotal >= totalSize)) {
            m_state = ST_EOF;
            return false;
        }
        return true;
    }

    /**
     * @param conn
     *            the URL connection to get the range information from, cannot be <code>null</code>.
     * @return an array of two elements containing: current chunk size & total size (in bytes).
     * @throws IOException
     *             in case of I/O problems or unexpected content.
     */
    private long[] getContentRangeInfo(URLConnection conn) throws IOException {
        if (conn instanceof HttpURLConnection) {
            return getHttpContentRangeInfo((HttpURLConnection) conn);
        }

        long totalBytes = conn.getContentLength();
        return new long[] { totalBytes, totalBytes };
    }

    private long[] getHttpContentRangeInfo(HttpURLConnection conn) throws IOException {
        int rc;
        try {
            rc = conn.getResponseCode();
        }
        catch (IOException exception) {
            rc = handleIOException(conn);
        }

        if (rc == SC_OK) {
            // Non-chunked response...
            if (m_readTotal > 0) {
                // this is "bad", as we've read some parts and we cannot tell the consumer of this stream that this is
                // happening...
                throw new IOException("Server returned complete content instead of (requested) partial.");
            }

            long totalBytes = conn.getContentLength();

            return new long[] { totalBytes, totalBytes };
        }
        else if (rc == SC_PARTIAL_CONTENT) {
            // Chunked response, see how many bytes we've got to read for it...
            String contentRange = conn.getHeaderField(HDR_CONTENT_RANGE);
            if (contentRange == null) {
                throw new IOException("Server returned no Content-Range for partial content");
            }

            return parseContentRangeHeader(contentRange);
        }
        else if (rc == SC_RANGE_NOT_SATISFIABLE) {
            // Range not satisfiable, we might already have completed the download?
            String contentRange = conn.getHeaderField(HDR_CONTENT_RANGE);
            if (contentRange != null) {
                return parseContentRangeHeader(contentRange);
            }

            // fall through, we cannot handle this...
        }
        else if (rc == SC_SERVICE_UNAVAILABLE) {
            // Service is unavailable, throw an exception to try it again later...
            int retry = ((HttpURLConnection) conn).getHeaderFieldInt(HTTP_RETRY_AFTER, DEFAULT_RETRY_TIME);

            throw new RetryAfterException(retry);
        }

        throw new IOException("Unknown/unexpected status code: " + rc);
    }

    /**
     * @return the current input stream, never <code>null</code>.
     * @throws IOException
     *             in case of I/O problems accessing the input stream.
     */
    private InputStream getInputStream() throws IOException {
        try {
            return m_conn.getInputStream();
        }
        catch (IOException exception) {
            handleIOException(m_conn);
            closeChunk();
            throw exception;
        }
    }

    /**
     * Parses a Content-Range header, which should be a byte-range specification of the content to expect.
     *
     * @param value
     *            the Content-Range header value to parse, cannot be <code>null</code>.
     * @return an array with two elements, the first indicating the length of the current chunk, and the second the
     *         total length of the content.
     * @throws IOException
     *             in case a non-byte Content-Range header value was given.
     */
    private long[] parseContentRangeHeader(String value) throws IOException {
        if (!value.startsWith(BYTES_)) {
            throw new IOException("Server returned non-byte Content-Range " + value);
        }

        String[] parts = value.substring(6).split("/");

        long chunkSize = 0L;
        if (!"*".equals(parts[0])) {
            String[] rangeDef = parts[0].split("-");

            try {
                long start = Long.parseLong(rangeDef[0]);
                long end = Long.parseLong(rangeDef[1]);

                chunkSize = end - start;
            }
            catch (Exception exception) {
                // Ack; invalid range specified, cannot determine chunk size!
                chunkSize = -1L;
            }
        }

        long totalBytes;
        if ("*".equals(parts[1])) {
            totalBytes = -1L;
        }
        else {
            totalBytes = Long.parseLong(parts[1]);
        }

        return new long[] { chunkSize, totalBytes };
    }

    /**
     * Prepares the connection for the next chunk, if needed.
     *
     * @return <code>true</code> if the prepare was successful (there was a next chunk to be read), <code>false</code>
     *         otherwise.
     * @throws IOException
     *             in case of I/O exception.
     */
    private boolean prepareNextChunk() throws IOException {
        if ((m_conn == null) && contentRemaining()) {
            m_conn = m_handler.getConnection(m_url);

            applyRangeHeader(m_conn);

            long[] contentInfo = getContentRangeInfo(m_conn);

            // No, not yet, update our local administration...
            if (m_contentInfo != null) {
                // verify the total size (paranoia sanity check)...
                if (m_contentInfo[1] >= 0 && contentInfo[1] >= 0 && m_contentInfo[1] != contentInfo[1]) {
                    throw new IOException("Stream size mismatch between different chunks!");
                }
            }

            m_contentInfo = contentInfo;
        }
        // Make sure there's still content remaining to be read...
        return (m_conn != null) && contentRemaining();
    }
}
TOP

Related Classes of org.apache.ace.agent.impl.ContentRangeInputStream

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.