/***********************************************************************************************************************
* Copyright (C) 2010-2013 by the Stratosphere project (http://stratosphere.eu)
*
* 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 eu.stratosphere.nephele.util;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.ArrayDeque;
import java.util.Queue;
/**
* This class is a special test implementation of a {@link ReadableByteChannel} and {@link WritableByteChannel}. Data is
* first written into main memory through the {@link WritableByteChannel} interface. Afterwards, the data can be read
* again through the {@link ReadableByteChannel} abstraction. The implementation is capable of simulating interruptions
* in the byte stream.
* <p>
* This class is not thread-safe.
*
*/
public class InterruptibleByteChannel implements ReadableByteChannel, WritableByteChannel {
/**
* The initial size of the internal memory buffer in bytes.
*/
private static final int INITIAL_BUFFER_SIZE = 8192;
/**
* Stores the requested interruptions of the byte stream during write operations that still have to be processed.
*/
private final Queue<Integer> writeInterruptPositions = new ArrayDeque<Integer>();
/**
* Stores the requested interruptions of the byte stream during read operations that still have to be processed.
*/
private final Queue<Integer> readInterruptPositions = new ArrayDeque<Integer>();
/**
* The internal memory buffer used to hold the written data.
*/
private ByteBuffer buffer = ByteBuffer.allocate(INITIAL_BUFFER_SIZE);
/**
* Stores if the channel is still open.
*/
private boolean isOpen = true;
/**
* Stores if the channel is still in write phase.
*/
private boolean isInWritePhase = true;
/**
* Constructs a new interruptible byte channel.
*
* @param writeInterruptPositions
* the positions to interrupt the byte stream during write operations or <code>null</code> if no
* interruptions are requested
* @param readInterruptPositions
* the positions to interrupt the byte stream during read operations or <code>null</code> if no interruptions
* are requested
*/
public InterruptibleByteChannel(final int[] writeInterruptPositions, final int[] readInterruptPositions) {
if (writeInterruptPositions != null) {
for (int i = 0; i < writeInterruptPositions.length - 1; ++i) {
if (writeInterruptPositions[i] > writeInterruptPositions[i + 1]) {
throw new IllegalArgumentException("Write interrupt positions must be ordered ascendingly");
}
this.writeInterruptPositions.add(Integer.valueOf(writeInterruptPositions[i]));
}
this.writeInterruptPositions.add(Integer
.valueOf(writeInterruptPositions[writeInterruptPositions.length - 1]));
}
if (readInterruptPositions != null) {
for (int i = 0; i < readInterruptPositions.length - 1; ++i) {
if (readInterruptPositions[i] > readInterruptPositions[i + 1]) {
throw new IllegalArgumentException("Read interrupt positions must be ordered ascendingly");
}
this.readInterruptPositions.add(Integer.valueOf(readInterruptPositions[i]));
}
this.readInterruptPositions.add(Integer
.valueOf(readInterruptPositions[readInterruptPositions.length - 1]));
}
}
@Override
public boolean isOpen() {
return this.isOpen;
}
@Override
public void close() throws IOException {
this.isOpen = false;
}
@Override
public int write(final ByteBuffer src) throws IOException {
if (!this.isOpen) {
throw new ClosedChannelException();
}
if (!this.isInWritePhase) {
throw new IllegalStateException("Channel is not in write phase anymore");
}
if (src.remaining() > this.buffer.remaining()) {
increaseBufferSize();
}
int numberOfBytesToAccept = src.remaining();
if (!this.writeInterruptPositions.isEmpty()
&& (this.buffer.position() + numberOfBytesToAccept < this.writeInterruptPositions.peek().intValue())) {
numberOfBytesToAccept = this.writeInterruptPositions.poll().intValue() - this.buffer.position();
this.buffer.limit(this.buffer.position() + numberOfBytesToAccept);
this.buffer.put(src);
this.buffer.limit(this.buffer.capacity());
return numberOfBytesToAccept;
}
this.buffer.put(src);
return numberOfBytesToAccept;
}
@Override
public int read(final ByteBuffer dst) throws IOException {
if (!this.isOpen) {
throw new ClosedChannelException();
}
if (this.isInWritePhase) {
throw new IllegalStateException("Channel is still in write phase");
}
if (!this.buffer.hasRemaining()) {
return -1;
}
int numberOfBytesToRetrieve = Math.min(this.buffer.remaining(), dst.remaining());
if (!this.readInterruptPositions.isEmpty()
&& (this.buffer.position() + numberOfBytesToRetrieve > this.readInterruptPositions.peek().intValue())) {
numberOfBytesToRetrieve = this.readInterruptPositions.poll().intValue() - this.buffer.position();
}
final int oldLimit = this.buffer.limit();
this.buffer.limit(this.buffer.position() + numberOfBytesToRetrieve);
dst.put(this.buffer);
this.buffer.limit(oldLimit);
return numberOfBytesToRetrieve;
}
/**
* Switches the channel to read phase.
*/
public void switchToReadPhase() {
if (!this.isInWritePhase) {
throw new IllegalStateException("Channel is already in read phase");
}
this.isInWritePhase = false;
this.buffer.flip();
}
/**
* Doubles the capacity of the internal byte buffer while preserving its content.
*/
private void increaseBufferSize() {
final ByteBuffer newBuf = ByteBuffer.allocate(this.buffer.capacity() * 2);
this.buffer.flip();
newBuf.put(this.buffer);
this.buffer = newBuf;
}
}