/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.imagepipeline.memory;
import javax.annotation.concurrent.NotThreadSafe;
import java.io.IOException;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.references.CloseableReference;
/**
* An implementation of {@link PooledByteBufferOutputStream} that produces a
* {@link NativePooledByteBuffer}
*/
@NotThreadSafe
public class NativePooledByteBufferOutputStream extends PooledByteBufferOutputStream {
private final NativeMemoryChunkPool mPool; // the pool to allocate memory chunks from
private CloseableReference<NativeMemoryChunk> mBufRef; // the current chunk that we're writing to
private int mCount; // number of bytes 'used' in the current chunk
/**
* Construct a new instance of this outputstream
* @param pool the pool to use
*/
public NativePooledByteBufferOutputStream(NativeMemoryChunkPool pool) {
this(pool, pool.getMinBufferSize());
}
/**
* Construct a new instance of this output stream with this initial capacity
* It is not an error to have this initial capacity be inaccurate. If the actual contents
* end up being larger than the initialCapacity, then we will reallocate memory
* if needed. If the actual contents are smaller, then we'll end up wasting some memory
* @param pool the pool to use
* @param initialCapacity initial capacity to allocate for this stream
*/
public NativePooledByteBufferOutputStream(NativeMemoryChunkPool pool, int initialCapacity) {
super();
Preconditions.checkArgument(initialCapacity > 0);
mPool = Preconditions.checkNotNull(pool);
mCount = 0;
mBufRef = CloseableReference.of(mPool.get(initialCapacity), mPool);
}
/**
* Gets a PooledByteBuffer from the current contents. If the stream has already been closed, then
* an InvalidStreamException is thrown.
* @return a PooledByteBuffer instance for the contents of the stream
* @throws InvalidStreamException if the stream is invalid
*/
@Override
public NativePooledByteBuffer toByteBuffer() {
ensureValid();
return new NativePooledByteBuffer(mBufRef, mCount);
}
/**
* Returns the total number of bytes written to this stream so far.
* @return the number of bytes written to this stream.
*/
@Override
public int size() {
return mCount;
}
/**
* Write one byte to the underlying stream. The underlying stream MUST be valid
* @param oneByte the one byte to write
* @throws InvalidStreamException if the stream is invalid
* @throws IOException in case of an I/O error during the write
*/
@Override
public void write(int oneByte) throws IOException {
byte[] buf = new byte[1];
buf[0] = (byte)oneByte;
this.write(buf);
}
/**
* Writes {@code count} bytes from the byte array {@code buffer} starting at
* position {@code offset} to this stream.
* The underlying stream MUST be valid
*
* @param buffer the source buffer to read from
* @param offset the start position in {@code buffer} from where to get bytes.
* @param count the number of bytes from {@code buffer} to write to this stream.
* @throws IOException if an error occurs while writing to this stream.
* @throws IndexOutOfBoundsException
* if {@code offset < 0} or {@code count < 0}, or if
* {@code offset + count} is bigger than the length of
* {@code buffer}.
* @throws InvalidStreamException if the stream is invalid
*/
public void write(byte[] buffer, int offset, int count) throws IOException {
if (offset < 0 || count < 0 || offset + count > buffer.length) {
throw new ArrayIndexOutOfBoundsException("length=" + buffer.length + "; regionStart=" + offset
+ "; regionLength=" + count);
}
ensureValid();
realloc(mCount + count);
mBufRef.get().write(mCount, buffer, offset, count);
mCount += count;
}
/**
* Closes the stream. Owned resources are released back to the pool. It is not allowed to call
* toByteBuffer after call to this method.
* @throws IOException
*/
@Override
public void close() {
CloseableReference.closeSafely(mBufRef);
mBufRef = null;
mCount = -1;
super.close();
}
/**
* Reallocate the local buffer to hold the new length specified.
* Also copy over existing data to this new buffer
* @param newLength new length of buffer
* @throws InvalidStreamException if the stream is invalid
* @throws BasePool.SizeTooLargeException if the allocation from the pool fails
*/
@VisibleForTesting
void realloc(int newLength) {
ensureValid();
/* Can the buffer handle @i more bytes, if not expand it */
if (newLength <= mBufRef.get().getSize()) {
return;
}
NativeMemoryChunk newbuf = mPool.get(newLength);
mBufRef.get().copy(0, newbuf, 0, mCount);
mBufRef.close();
mBufRef = CloseableReference.of(newbuf, mPool);
}
/**
* Ensure that the current stream is valid, that is underlying closeable reference is not null
* and is valid
* @throws InvalidStreamException if the stream is invalid
*/
private void ensureValid() {
if (!CloseableReference.isValid(mBufRef)) {
throw new InvalidStreamException();
}
}
/**
* An exception indicating that this stream is no longer valid
*/
public static class InvalidStreamException extends RuntimeException {
public InvalidStreamException() {
super("OutputStream no longer valid");
}
}
}