package org.limewire.nio.statemachine;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.nio.NIODispatcher;
import org.limewire.nio.channel.ChannelReadObserver;
import org.limewire.nio.channel.ChannelWriter;
import org.limewire.nio.channel.InterestReadableByteChannel;
import org.limewire.nio.channel.InterestScatteringByteChannel;
import org.limewire.nio.channel.InterestWritableByteChannel;
import org.limewire.util.BufferUtils;
/**
* State machine for reading and writing.
*/
public class IOStateMachine implements ChannelReadObserver, ChannelWriter, InterestScatteringByteChannel {
private static final Log LOG = LogFactory.getLog(IOStateMachine.class);
/** Observer to notify when this finishes or fails. */
private IOStateObserver observer;
/** The states this will use while handshaking.*/
private List<IOState> states;
/** The current state. */
private IOState currentState;
/** The sink we write to. */
private volatile InterestWritableByteChannel writeSink;
/** The sink we read from. */
private volatile InterestReadableByteChannel readSink;
/** The ByteBuffer to use for reading. */
private ByteBuffer readBuffer;
/** Whether or not we've shutdown this handshaker. */
private volatile boolean shutdown;
public IOStateMachine(IOStateObserver observer, List<IOState> states) {
this(observer, states, 2048);
}
public IOStateMachine(IOStateObserver observer, List<IOState> states, int bufferSize) {
this.observer = observer;
this.states = states;
this.readBuffer = NIODispatcher.instance().getBufferCache().getHeap(bufferSize);
if(!states.isEmpty())
this.currentState = states.remove(0);
}
/**
* Adds a new state to process.
*/
public void addState(final IOState newState) {
NIODispatcher.instance().getScheduledExecutorService().execute(new Runnable() {
public void run() {
if(LOG.isDebugEnabled())
LOG.debug("Adding single state: " + newState);
states.add(newState);
if(states.size() == 1) {
nextState(false, false);
}
}
});
}
/**
* Adds a collection of new states to process.
*/
public void addStates(final List<? extends IOState> newStates) {
NIODispatcher.instance().getScheduledExecutorService().execute(new Runnable() {
public void run() {
if(LOG.isDebugEnabled())
LOG.debug("Adding multiple states: " + newStates);
states.addAll(newStates);
if(states.size() == newStates.size())
nextState(false, false);
}
});
}
/**
* Adds an array of new states to process.
*/
public void addStates(final IOState... newStates) {
NIODispatcher.instance().getScheduledExecutorService().execute(new Runnable() {
public void run() {
if(LOG.isDebugEnabled())
LOG.debug("Adding multiple states...");
for(int i = 0; i < newStates.length; i++) {
if(LOG.isDebugEnabled())
LOG.debug(" state[" + i + "]: " + newStates[i]);
states.add(newStates[i]);
}
if(states.size() == newStates.length)
nextState(false, false);
}
});
}
/**
* Notification that a read can be performed. If the current state is for writing,
* <code>handleRead</code> turns off future interest events. Otherwise
* the current state is told to process.
*/
public void handleRead() {
if(currentState != null) {
if(currentState.isWriting()) {
LOG.warn("Got a read notification while writing.");
processCurrentState(null, true); // read up the data into the buffer
readSink.interestRead(false);
} else {
processCurrentState(currentState, true);
}
} else {
LOG.warn("Got a read notification with no current state");
processCurrentState(null, true);
readSink.interestRead(false);
}
}
/**
* Notification that a write can be performed. If the current state is for reading,
* <code>handleWrite</code> turns off future interest events. Otherwise
* the current state is told to process.
*/
public boolean handleWrite() {
if(currentState != null) {
if(currentState.isReading()) {
LOG.warn("Got a write notification while reading");
writeSink.interestWrite(this, false);
return false;
} else {
return processCurrentState(currentState, false);
}
} else {
LOG.warn("Got a write notification with no current state");
writeSink.interestWrite(this, false);
return false;
}
}
/**
* Process the current state. If any exceptions occur while processing,
* we'll notify the observer of them. If the state indicated it needs to be
* processed again, we do not move to the next state. Otherwise, if the state
* indicated that it's done, we move to the next state.
* <p>
* This will return true if we're writing and we have more to write.
*/
private boolean processCurrentState(IOState state, boolean reading) {
if(!shutdown) {
try {
// if(LOG.isDebugEnabled())
// LOG.debug("Processing (" + (reading ? "R" : "W") + ") state: " + currentState);
if (reading) {
if(state == null) {
if(LOG.isDebugEnabled())
LOG.debug("Processing a read with no state");
// We must read up data otherwise it could be lost.
// (it would be lost if we were transfering observers
// and the prior observer had already read data)
int read = 0;
while(readBuffer.hasRemaining() && (read = readSink.read(readBuffer)) > 0);
if(readBuffer.position() == 0 && read == -1)
throw new ClosedChannelException();
} else if (!state.process(readSink, readBuffer))
nextState(true, false);
} else {
if (!state.process(writeSink, null))
nextState(false, true);
else
return true;
}
} catch (IOException iox) {
if(LOG.isWarnEnabled())
LOG.warn("IOX while processing state: " + state, iox);
synchronized(this) {
shutdown = true;
}
try {
close();
} catch(IOException ignored) {}
NIODispatcher.instance().getBufferCache().release(readBuffer);
observer.handleIOException(iox);
}
} else {
if(LOG.isDebugEnabled())
LOG.debug("Ignoring processing because machine is shutdown");
}
return false;
}
/**
* Moves to the next state.
* If there are no states left, we notify the observer that we're finished.
* Otherwise, we'll move to the next state and change interest on our channels
* depending on what we're currently doing and what's needed next.
*/
private void nextState(boolean reading, boolean writing) {
if(states.isEmpty()) {
LOG.debug("No more states, processing finished.");
readSink.interestRead(false);
writeSink.interestWrite(this, false);
observer.handleStatesFinished();
} else {
currentState = states.remove(0);
if(LOG.isDebugEnabled())
LOG.debug("Incrementing state to: " + currentState);
if(currentState.isReading() && !reading) {
writeSink.interestWrite(this, false);
if(readSink != null)
readSink.interestRead(true);
}
if(currentState.isWriting() && !writing) {
readSink.interestRead(false);
if(writeSink != null)
writeSink.interestWrite(this, true);
}
// Process reading immediately, else
// data already in the buffer may be
// ignored while waiting on more data
// in the socket.
if(currentState.isReading())
processCurrentState(currentState, true);
}
}
public InterestWritableByteChannel getWriteChannel() {
return writeSink;
}
public void setWriteChannel(InterestWritableByteChannel newChannel) {
this.writeSink = newChannel;
if(currentState != null)
writeSink.interestWrite(this, true);
}
public InterestReadableByteChannel getReadChannel() {
return readSink;
}
public void setReadChannel(InterestReadableByteChannel newChannel) {
this.readSink = newChannel;
if(currentState != null)
readSink.interestRead(true);
}
public boolean isOpen() {
return readSink != null && readSink.isOpen() &&
writeSink != null && writeSink.isOpen();
}
public void close() throws IOException {
if(readSink != null)
readSink.close();
if(writeSink != null)
writeSink.close();
}
/**
* Notification that this Handshaker is being shut down.
* This may be because message looping is taking over and is notifying us that
* we're done. If that's the case, we do not notify the observer that we were shutdown.
* Otherwise, we notify the observer that we were shut down.
*/
public void shutdown() {
synchronized(this) {
if(shutdown)
return;
shutdown = true;
}
// this may be called when transfer is switched to another observer
if(!isOpen()) {
observer.shutdown();
}
// This must be done on the NIO thread, else the NIO thread could
// currently be processing this buffer, and things may continue to
// process it after we release it.
NIODispatcher.instance().getScheduledExecutorService().execute(new Runnable() {
public void run() {
NIODispatcher.instance().getBufferCache().release(readBuffer);
}
});
}
public void interestRead(boolean status) {
if(currentState != null)
readSink.interestRead(status);
}
/**
* Allows another channel to read from this, passing any unread bytes to that channel.
* This is typically used for the <code>MessageReader</code> to read any bytes that this AsyncHandshaker
* read from the network but did not process during handshaking.
*/
public int read(ByteBuffer toBuffer) throws ClosedChannelException {
if(shutdown)
throw new ClosedChannelException();
return BufferUtils.transfer(readBuffer, toBuffer);
}
public long read(ByteBuffer []toBuffer) throws ClosedChannelException {
return read(toBuffer, 0, toBuffer.length);
}
public long read(ByteBuffer[] toBuffer, int offset, int num) throws ClosedChannelException {
if(shutdown)
throw new ClosedChannelException();
return BufferUtils.transfer(readBuffer, toBuffer, 0, toBuffer.length, true);
}
// unused.
public void handleIOException(IOException iox) {}
}