/** * Copyright Microsoft Corporation * <p> * 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 * <p> * 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 com.microsoft.azure.storage.blob; import com.microsoft.azure.storage.Constants; import com.microsoft.azure.storage.core.SR; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; public class SubStream extends InputStream { // A mutal exclusion lock shared between other related substream instances. private final Object lock; // Stream to be wrapped. private InputStream wrappedStream; // The current, relative position in the substream. private long substreamCurrentIndex; // The position in the wrapped stream (relative to the last mark) where the substream should logically begin. private long streamBeginIndex; // The length of the substream. private long streamLength; // Tracks the marked position in the substream. private long markIndex; // Buffer for read requests. private byte[] readBuffer; private ByteArrayInputStream readBufferStream; // Keeps track of the remaining valid bytes available in the read buffer. private int readBufferLength; /** * Creates a new substream instance that partitions the wrapped stream <code>source</code> from * <code>startIndex</code> up to <code>streamLength</code>. Each substream instance that wraps the same * underlying <code>InputStream</code> must share the same mutual exclusion <code>lock</code> to avoid race * conditions from concurrent operations. * * @param source The markable InputStream to be wrapped. * @param startIndex A valid index in the wrapped stream where the substream should logically begin. * @param streamLength The length of the substream. * @param lock An intrinsic lock to ensure thread-safe, concurrent operations * on substream instances wrapping the same InputStream. * @throws Exception */ public SubStream(InputStream source, long startIndex, long streamLength, Object lock) { if (startIndex < 0 || streamLength < 1) { throw new IndexOutOfBoundsException(); } else if (source == null) { throw new NullPointerException("Source stream is null."); } else if (!source.markSupported()) { throw new IllegalArgumentException("The source stream to be wrapped must be markable."); } this.wrappedStream = source; this.streamBeginIndex = startIndex; this.substreamCurrentIndex = 0; this.streamLength = streamLength; this.lock = lock; this.readBuffer = new byte[Constants.SUBSTREAM_BUFFER_SIZE]; this.readBufferStream = new ByteArrayInputStream(this.readBuffer); // Set empty read buffer to force refresh upon first read. this.readBufferLength = 0; // By default, mark the beginning of the stream. this.markIndex = 0; this.readBufferStream.mark(Integer.MAX_VALUE); } public InputStream getInputStream() { return this.wrappedStream; } public long getLength() { return this.streamLength; } /** * Reads the next byte of data from the wrapped stream. The value byte is * returned as an <code>int</code> in the range <code>0</code> to * <code>255</code>. If no byte is available because the end of the substream * has been reached, the value <code>-1</code> is returned. This method * blocks until input data is available, the end of the stream is detected, * or an exception is thrown. * * @return the next byte of data, or <code>-1</code> if the end of the * substream is reached. * @throws IOException if an I/O error occurs. */ @Override public int read() throws IOException { throw new IOException(); } /** * Reads some number of bytes from the wrapped stream and stores them into * the buffer array <code>b</code>. The number of bytes actually read is * returned as an integer. This method blocks until input data is * available, end of file is detected, or an exception is thrown. * <p> * <p> If the length of <code>b</code> is zero, then no bytes are read and * <code>0</code> is returned; otherwise, there is an attempt to read at * least one byte. If no byte is available because the substream is at the * end of the file, the value <code>-1</code> is returned; otherwise, at * least one byte is read and stored into <code>b</code>. * <p> * <p> The first byte read is stored into element <code>b[0]</code>, the * next one into <code>b[1]</code>, and so on. The number of bytes read is, * at most, equal to the length of <code>b</code>. Let <i>k</i> be the * number of bytes actually read; these bytes will be stored in elements * <code>b[0]</code> through <code>b[</code><i>k</i><code>-1]</code>, * leaving elements <code>b[</code><i>k</i><code>]</code> through * <code>b[b.length-1]</code> unaffected. * <p> * <p> The <code>read(b)</code> method for class <code>SubStream</code> * has the same effect as: <pre><code> read(b, 0, b.length) </code></pre> * * @param b the buffer into which the data is read. * @return the total number of bytes read into the buffer, or * <code>-1</code> if there is no more data because the end of * the stream has been reached. * @throws IOException If the first byte cannot be read for any reason * other than the end of the file, if the wrapped stream has been closed, or * if some other I/O error occurs. * @throws NullPointerException if <code>b</code> is <code>null</code>. * @see SubStream#read(byte[], int, int) */ @Override public synchronized int read(byte[] b) throws IOException { return this.read(b, 0, b.length); } /** * Reads up to <code>len</code> bytes of data from the substream. Buffers data from the wrapped stream * in order to minimize skip and read overhead. The wrappedstream will only be invoked if the readBuffer * cannot fulfil the the read request. * In order to ensure valid results, the wrapped stream must be marked prior to reading from the substream. * This allows us to reset to the relative substream position in the wrapped stream. * The number of bytes actually read is returned as an integer. All these operations are done * synchronously within an intrinsic lock to ensure other concurrent requests by substream instances * do not result in race conditions. * <p> * <p> The underlying call to the read of the wrapped stream will blocks until input data * is available, end of file is detected, or an exception is thrown. * <p> * <p> If <code>len</code> is zero, then no bytes are read and * <code>0</code> is returned; otherwise, there is an attempt to read at * least one byte. If no byte is available because the substream is at end of * file, the value <code>-1</code> is returned; otherwise, at least one * byte is read and stored into <code>b</code>. * * @param b the buffer into which the data is read. * @param off the start offset in array <code>b</code> * at which the data is written. * @param len the maximum number of bytes to read. * @return the total number of bytes read into the buffer, or * <code>-1</code> if there is no more data because the end of * the stream has been reached. * @throws IOException If the first byte cannot be read for any reason * other than end of file, or if the wrapped stream has been closed, or if * some other I/O error occurs. * @throws NullPointerException If <code>b</code> is <code>null</code>. * @throws IndexOutOfBoundsException If <code>off</code> is negative, * <code>len</code> is negative, or <code>len</code> is greater than * <code>b.length - off</code> * @see SubStream#read() */ @Override public synchronized int read(byte[] b, int off, int len) throws IOException { if (off < 0 || len < 0 || len > b.length - off) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return 0; } int bytesRead = -1; int readLength = len; // Ensure we read within the substream bounds. if (this.substreamCurrentIndex + len > this.streamLength) { readLength = (int) (this.streamLength - this.substreamCurrentIndex); } // Read from previously buffered data and only up until the valid bytes available in the buffer. int bytesFromBuffer = readBufferStream.read(b, off, Math.min(this.readBufferLength, readLength)); bytesRead = Math.max(0, bytesFromBuffer); this.readBufferLength -= bytesRead; // Read request was fully satisfied. if (bytesFromBuffer == readLength) { this.substreamCurrentIndex += bytesRead; return bytesRead; } else if (bytesFromBuffer < readLength) { // Refresh the buffer to fulfil request. this.readBufferStream.reset(); this.readBufferLength = this.readHelper(this.readBuffer, 0, readBuffer.length); if (this.readBufferLength == -1) { this.readBufferLength = 0; } } // Read the remaining bytes from the read buffer. bytesFromBuffer = readBufferStream.read(b, bytesRead + off, Math.min(this.readBufferLength, readLength - bytesRead)); if (bytesFromBuffer != -1) { bytesRead += bytesFromBuffer; this.readBufferLength -= bytesFromBuffer; } this.substreamCurrentIndex += bytesRead; return bytesRead; } private int readHelper(byte[] b, int off, int len) throws IOException { synchronized (this.lock) { wrappedStream.reset(); long bytesSkipped = 0; byte failSkipCount = 0; long streamCurrentIndex = this.streamBeginIndex + this.substreamCurrentIndex; // Must be done in a loop as skip may return less than the requested number of bytes. do { if (failSkipCount > 7) { throw new IOException(SR.STREAM_SKIP_FAILED); } long skipped = wrappedStream.skip(streamCurrentIndex - bytesSkipped); if (skipped == 0) { failSkipCount++; } else { failSkipCount = 0; bytesSkipped += skipped; } } while (bytesSkipped != streamCurrentIndex); return wrappedStream.read(b, off, len); } } /** * Advances the current position of the substream by <code>n</code>. * The <code>skip</code> method does not invoke the underlying <code>skip</code> method * of the wrapped stream class. The actual skipping of bytes will be accounted for * during subsequent substream read operations. * * @param n the number of bytes to be effectively skipped. * @return the actual number of bytes skipped. */ @Override public long skip(long n) { if (this.substreamCurrentIndex + n > this.streamLength) { n = this.streamLength - this.substreamCurrentIndex; } this.substreamCurrentIndex += n; this.readBufferLength = (int) Math.max(0, this.readBufferLength - n); return n; } /** * Marks the current position in the substream. A subsequent call to * the <code>reset</code> method will reposition the stream to this stored position. * * @param readlimit the maximum limit of bytes that can be read before * the mark position becomes invalid. * @see SubStream#reset() */ @Override public synchronized void mark(int readlimit) { this.markIndex = this.substreamCurrentIndex; } /** * Repositions the substream position to the index where the <code>mark</code> method * was last called. * <p> * The new reset position on substream does not take effect until subsequent reads. * * @see SubStream#mark(int) */ @Override public synchronized void reset() { this.substreamCurrentIndex = this.markIndex; } /** * The substream wrapper class is only compatible with markable input streams and hence * will always return true. This requirement is enforced in the class constructor. * * @return <code>true</code> * @see SubStream#mark(int) * @see SubStream#reset() */ @Override public boolean markSupported() { return true; } /** * Closes the substream. */ @Override public void close() throws IOException { this.wrappedStream = null; this.readBuffer = null; this.readBufferStream.close(); this.readBufferStream = null; } }