/*
* JBoss, Home of Professional Open Source
* Copyright 2012, Red Hat Middleware LLC, and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* 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
* 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.jboss.shrinkwrap.api.nio.file;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SeekableByteChannel;
/**
* {@link SeekableByteChannel} implementation backed by an auto-resizing byte array; thread-safe. Can hold a maxiumum of
* {@link Integer#MAX_VALUE} bytes.
*
* @author <a href="mailto:alr@jboss.org">Andrew Lee Rubinger</a>
*/
public class SeekableInMemoryByteChannel implements SeekableByteChannel {
/**
* Current position; guarded by "this"
*/
private int position;
/**
* Whether or not this {@link SeekableByteChannel} is open; volatile instead of sync is acceptable because this
* field participates in no compound computations or invariants with other instance members.
*/
private volatile boolean open;
/**
* Internal buffer for contents; guarded by "this"
*/
private byte[] contents;
/**
* Creates a new instance with 0 size and 0 position, and open.
*/
public SeekableInMemoryByteChannel() {
this.open = true;
// Set fields
synchronized (this) {
this.position = 0;
this.contents = new byte[0];
}
}
/**
* {@inheritDoc}
*
* @see java.nio.channels.Channel#isOpen()
*/
@Override
public boolean isOpen() {
return this.open;
}
/**
* {@inheritDoc}
*
* @see java.nio.channels.Channel#close()
*/
@Override
public void close() throws IOException {
this.open = false;
}
/**
* {@inheritDoc}
*
* @see java.nio.channels.SeekableByteChannel#read(java.nio.ByteBuffer)
*/
@Override
public int read(final ByteBuffer destination) throws IOException {
// Precondition checks
this.checkClosed();
if (destination == null) {
throw new IllegalArgumentException("Destination buffer must be supplied");
}
// Init
final int spaceInBuffer = destination.remaining();
final int numBytesRemainingInContent, numBytesToRead;
// Sync up before getting at shared mutable state
synchronized (this) {
numBytesRemainingInContent = this.contents.length - this.position;
// Set position was greater than the size? Just return.
if (numBytesRemainingInContent <= 0) {
return -1;
}
// We'll read in either the number of bytes remaining in content or the amount of space in the buffer,
// whichever is smaller
numBytesToRead = numBytesRemainingInContent >= spaceInBuffer ? spaceInBuffer : numBytesRemainingInContent;
// Copy a sub-array of the bytes we'll put into the buffer
destination.put(this.contents, this.position, numBytesToRead);
// Set the new position
this.position += numBytesToRead;
}
// Return the number of bytes read
return numBytesToRead;
}
/**
* {@inheritDoc}
*
* @see java.nio.channels.SeekableByteChannel#write(java.nio.ByteBuffer)
*/
@Override
public int write(final ByteBuffer source) throws IOException {
// Precondition checks
this.checkClosed();
if (source == null) {
throw new IllegalArgumentException("Source buffer must be supplied");
}
// Put the bytes to be written into a byte[]
final int totalBytes = source.remaining();
final byte[] readContents = new byte[totalBytes];
source.get(readContents);
// Sync up, we're gonna access shared mutable state
synchronized (this) {
// Append the read contents to our internal contents
this.contents = this.concat(this.contents, readContents, this.position);
// Increment the position of this channel
this.position += totalBytes;
}
// Return the number of bytes read
return totalBytes;
}
/**
* Creates a new array which is the concatenated result of the two inputs, at the designated position (to be filled
* with 0x00) in the case of a gap).
*
* @param input1
* @param input2
* @param position
* @return
*/
private byte[] concat(final byte[] input1, final byte[] input2, final int position) {
// Preconition checks
assert input1 != null : "Input 1 must be specified";
assert input2 != null : "Input 2 must be specified";
assert position >= 0 : "Position must be 0 or higher";
// Allocate a new array of enough space (either current size or position + input2.length, whichever is greater)
final int newSize = position < input1.length ? input1.length + input2.length : position + input2.length;
final byte[] merged = new byte[newSize];
// Copy in the contents of input 1 with 0 offset
System.arraycopy(input1, 0, merged, 0, input1.length);
// Copy in the contents of input2 with offset the length of input 1
System.arraycopy(input2, 0, merged, position, input2.length);
return merged;
}
/**
* {@inheritDoc}
*
* @see java.nio.channels.SeekableByteChannel#position()
*/
@Override
public long position() throws IOException {
synchronized (this) {
return this.position;
}
}
/**
* {@inheritDoc}
*
* @see java.nio.channels.SeekableByteChannel#position(long)
*/
@Override
public SeekableByteChannel position(final long newPosition) throws IOException {
// Precondition checks
if (newPosition > Integer.MAX_VALUE || newPosition < 0) {
throw new IllegalArgumentException("Valid position for this channel is between 0 and " + Integer.MAX_VALUE);
}
synchronized (this) {
this.position = (int) newPosition;
}
return this;
}
/**
* {@inheritDoc}
*
* @see java.nio.channels.SeekableByteChannel#size()
*/
@Override
public long size() throws IOException {
synchronized (this) {
return this.contents.length;
}
}
/**
* {@inheritDoc}
*
* @see java.nio.channels.SeekableByteChannel#truncate(long)
*/
@Override
public SeekableByteChannel truncate(final long size) throws IOException {
// Precondition checks
if (size < 0 || size > Integer.MAX_VALUE) {
throw new IllegalArgumentException("This implementation permits a size of 0 to " + Integer.MAX_VALUE
+ " inclusive");
}
// Sync up for mucking w/ shared mutable state
synchronized (this) {
final int newSize = (int) size;
final int currentSize = (int) this.size();
// If the current position is greater than the given size, set to the given size (by API spec)
if (this.position > newSize) {
this.position = newSize;
}
// If we've been given a size smaller than we currently are
if (currentSize > newSize) {
// Make new array
final byte[] newContents = new byte[newSize];
// Copy in the contents up to the new size
System.arraycopy(this.contents, 0, newContents, 0, newSize);
// Set the new array as our contents
this.contents = newContents;
}
// If we've been given a size greater than or equal to us then do nothing
// if (newSize >= currentSize) {
// // do nothing
// }
}
// Return this reference
return this;
}
/**
* Obtain a copy of the contents of this {@link Channel} as an {@link InputStream}
*
* @return
*/
InputStream getContents() {
final byte[] copy;
synchronized (this) {
final int length = this.contents.length;
copy = new byte[length];
System.arraycopy(this.contents, 0, copy, 0, this.contents.length);
}
return new ByteArrayInputStream(copy);
}
/**
* Throws a {@link ClosedChannelException} if this {@link SeekableByteChannel} is closed.
*
* @throws ClosedChannelException
*/
private void checkClosed() throws ClosedChannelException {
if (!this.isOpen()) {
throw new ClosedChannelException();
}
}
}