SquirrelJME/SquirrelJME

View on GitHub
modules/io/src/main/java/net/multiphasicapps/io/SizeLimitedInputStream.java

Summary

Maintainability
A
0 mins
Test Coverage
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// SquirrelJME
//     Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the Mozilla Public License Version 2.0.
// For more information see license.mkd.
// ---------------------------------------------------------------------------

package net.multiphasicapps.io;

import java.io.IOException;
import java.io.InputStream;

/**
 * This is an input stream in which the size of the input stream is maximally
 * bound to a given size or fixed to a specific size.
 *
 * This class is not thread safe.
 *
 * @since 2016/03/09
 */
public class SizeLimitedInputStream
    extends InputStream
{
    /** The wrapped stream. */
    protected final InputStream wrapped;
    
    /** Exact size? */
    protected final boolean exact;
    
    /** The read limit. */
    protected final long limit;
    
    /** If {@code true} then close propogates to the wrapped stream. */
    protected final boolean propogate;
    
    /** The current read size. */
    private volatile long _current;
    
    /** Was this closed? */    
    private volatile boolean _closed;
    
    /**
     * Initializes the size limited input stream. The close operation in this
     * stream propogates to the wrapped stream.
     *
     * @param __is Input stream to wrap.
     * @param __li The length of data to limit to.
     * @param __ex If {@code true} then the stream must end at least at the
     * limit and not before it, otherwise if {@code false} then it is only
     * limited to either the limit or the number of bytes in the stream. If
     * {@code true} then closing the stream will read the remaining number of
     * bytes.
     * @throws IllegalArgumentException If the length is negative.
     * @throws NullPointerException On null arguments.
     * @since 2016/03/09
     */
    public SizeLimitedInputStream(InputStream __is, long __li, boolean __ex)
        throws IllegalArgumentException, NullPointerException
    {
        this(__is, __li, __ex, true);
    }
    
    /**
     * Initializes the size limited input stream.
     *
     * @param __is Input stream to wrap.
     * @param __li The length of data to limit to.
     * @param __ex If {@code true} then the stream must end at least at the
     * limit and not before it, otherwise if {@code false} then it is only
     * limited to either the limit or the number of bytes in the stream. If
     * {@code true} then closing the stream will read the remaining number of
     * bytes.
     * @param __prop If {@code true} then when this stream is closed, it will
     * forward the close to the wrapped stream. Otherwise if {@code false}.
     * @throws IllegalArgumentException If the length is negative.
     * @throws NullPointerException On null arguments.
     * @since 2016/08/28
     */
    public SizeLimitedInputStream(InputStream __is, long __li, boolean __ex,
        boolean __prop)
        throws IllegalArgumentException, NullPointerException
    {
        // Check
        if (__is == null)
            throw new NullPointerException("NARG");
        
        /* {@squirreljme.error BD1o The limit is negative. (The negative
        limit)} */
        if (__li < 0)
            throw new IllegalArgumentException(String.format("BD1o %d", __li));
        
        // Set
        this.wrapped = __is;
        this.limit = __li;
        this.exact = __ex;
        this.propogate = __prop;
    }
    
    /**
     * {@inheritDoc}
     * @since 2016/03/09
     */
    @Override
    public int available()
        throws IOException
    {
        // Get the count for the wrapped stream
        long wav = this.wrapped.available();
    
        // Either limited to the max integer size, the number of available
        // bytes, or the remaining stream count.
        return (int)Math.min(Integer.MAX_VALUE,
            Math.min(wav, (this.limit - this._current)));
    }
    
    /**
     * {@inheritDoc}
     * @since 2016/03/09
     */
    @Override
    public void close()
        throws IOException
    {
        // Read in?
        if (!this._closed)
        {
            // Set
            this._closed = true;
            
            // Read in remaining bytes if doing so
            if (this.exact)
            {
                /* {@squirreljme.error BD1p Reached EOF on wrapped stream when
                requesting an exact number of bytes. (The current read
                count; The read limit)} */
                long limit = this.limit;
                long at;
                while ((at = this._current) < limit)
                    if (this.read() < 0)
                        throw new IOException(String.format("BD1p %d %d",
                            at, limit));
            }
        }
        
        // Close the underlying stream, but only if propogating
        if (this.propogate)
            this.wrapped.close();
    }
    
    /**
     * {@inheritDoc}
     * @since 2016/03/09
     */
    @Override
    public int read()
        throws IOException
    {
        // Current position
        long limit = this.limit;
        long cur = this._current;
        
        // Reached the limit?
        if (cur >= limit)
            return -1;
        
        // Read next byte
        int next = this.wrapped.read();
        
        // EOF?
        if (next < 0)
        {
            /* {@squirreljme.error BD1q Required an exact number of bytes
            however the limit was not yet reached, the input stream is too
            short. (The limit; The current position)} */
            if (this.exact && cur != limit)
                throw new IOException(String.format("BD1q %d %d",
                    limit, cur));
            
            // Return original negative
            return next;
        }
        
        // Increase current location
        this._current = cur + 1L;
        
        // Return it
        return next;
    }
    
    /**
     * {@inheritDoc}
     * @since 2016/08/05
     */
    @Override
    public int read(byte[] __b, int __o, int __l)
        throws IndexOutOfBoundsException, IOException, NullPointerException
    {
        // Check
        if (__b == null)
            throw new NullPointerException("NARG");
        if (__o < 0 || __l < 0 || (__o + __l) < 0 || (__o + __l) > __b.length)
            throw new IndexOutOfBoundsException("BAOB");
        
        // If the limit was reached, stop
        long current = this._current;
        long limit = this.limit;
        if (current >= limit)
        {
            /* {@squirreljme.error BD1r Required an exact number of bytes
            however the limit was not yet reached, the file is too short.
            (The limit; The current position)} */
            if (this.exact && current != limit)
                throw new IOException(String.format("BD1r %d %d",
                    limit, current));
            
            return -1;
        }
        
        // Do not read more bytes after the limit
        int cc = (int)Math.min(limit - current, __l);
        
        // Read the next few bytes
        InputStream wrapped = this.wrapped;
        int rc = wrapped.read(__b, __o, cc);
        
        // EOF?
        if (rc < 0)
        {
            /* {@squirreljme.error BD1s Required an exact number of bytes
            however the limit was not yet reached. (The limit; The
            current position)} */
            if (this.exact && current != limit)
                throw new IOException(String.format("BD1s %d %d",
                    limit, current));
            
            // Just EOF
            return -1;
        }
        
        // Set the new current
        this._current = current + rc;
        
        // Return the read count
        return rc;
    }
}