package com.limegroup.gnutella.udpconnect;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Handle writing to a udp connection via a stream. Internally, the writes
* are broken up into chunks of a convenient size for UDP packets. Blocking
* occurs when the internal list is full. This means that the
* UDPConnectionProcessor can't send the data currently.
*/
public class UDPBufferedOutputStream extends OutputStream {
private static final Log LOG =
LogFactory.getLog(UDPBufferedOutputStream.class);
/**
* The maximum blocking time of a write.
*/
private static final int FOREVER = 10 * 60 * 60 * 1000;
/**
* The list of buffered chunks.
*/
private ArrayList chunks;
/**
* The current chunk getting written to.
*/
private byte[] activeChunk;
/**
* The written progress on the activeChunk.
*/
private int activeCount;
/**
* The reader of information coming into this output stream.
*/
private UDPConnectionProcessor _processor;
private boolean _connectionActive;
/**
* Creates an output stream front end to a UDP Connection.
*/
public UDPBufferedOutputStream(UDPConnectionProcessor p) {
_processor = p;
_connectionActive = true;
chunks = new ArrayList(5);
allocateNewChunk();
}
/**
* Writes the specified byte to the activeChunk if room.
* Block if necessary.
*/
public synchronized void write(int b) throws IOException {
// If there was no data before this, then ensure a writer is awake
if ( _connectionActive && getPendingChunks() == 0 )
_processor.wakeupWriteEvent();
while (true) {
if ( !_connectionActive )
throw new IOException("Connection Closed");
// If there is room within current chunk
else if ( activeCount < UDPConnectionProcessor.DATA_CHUNK_SIZE ) {
// Add to the current chunk
activeChunk[activeCount] = (byte) b;
activeCount++;
return;
} else {
// If there is room for more chunks
if ( chunks.size() < _processor.getChunkLimit() ) {
// Allocate a new chunk
chunks.add(activeChunk);
allocateNewChunk();
} else {
// Wait for room for a new chunk
waitOnReader();
// Again, If there was no data before this,
// then ensure a writer is awake
if ( getPendingChunks() == 0 )
_processor.wakeupWriteEvent();
}
}
}
}
/**
* Do a partial write from the byte array. Block if necessary.
*/
public synchronized void write(byte b[], int off, int len)
throws IOException {
if(LOG.isDebugEnabled()) {
LOG.debug("writing len: "+len+" bytes");
}
int space; // The space available within the active chunk
int wlength; // The length of data to be written to the active chunk
// If there was no data before this, then ensure a writer is awake
if ( _connectionActive && getPendingChunks() == 0 )
_processor.wakeupWriteEvent();
while (true) {
if ( !_connectionActive )
throw new IOException("Connection Closed");
// If there is room within current chunk
else if ( activeCount < activeChunk.length ) {
// Fill up the current chunk
space = activeChunk.length - activeCount;
wlength = Math.min(space, len);
System.arraycopy(b, off, activeChunk, activeCount, wlength);
space -= wlength;
activeCount += wlength;
// If the data length was less than the available space
if ( space > 0 ) {
return;
}
len -= wlength;
off += wlength;
// If the data length matched the space available
if ( len <= 0 )
return;
} else {
// If there is room for more chunks
if ( chunks.size() < _processor.getChunkLimit() ) {
// Allocate a new chunk
chunks.add(activeChunk);
allocateNewChunk();
} else {
// Wait for room for a new chunk
waitOnReader();
// Again, If there was no data before this,
// then ensure a writer is awake
if ( getPendingChunks() == 0 )
_processor.wakeupWriteEvent();
}
}
}
}
/**
* Closing output stream has no effect.
*/
public synchronized void close() throws IOException {
if (!_connectionActive)
throw new IOException("already closed");
_processor.close();
}
/**
* Flushing currently does nothing.
* TODO: If needed, it can wait for all data to be read.
*/
public void flush() throws IOException {
}
/**
* Allocates a chunk for writing to and reset written amount.
*/
private void allocateNewChunk() {
activeChunk = new byte[UDPConnectionProcessor.DATA_CHUNK_SIZE];
activeCount = 0;
}
/**
* Package accessor for retrieving and freeing up chunks of data.
* Returns null if no data.
*/
synchronized Chunk getChunk() {
Chunk rChunk;
if ( chunks.size() > 0 ) {
// Return the oldest chunk
rChunk = new Chunk();
rChunk.data = (byte[]) chunks.remove(0);
rChunk.start = 0; // Keep this to zero here
rChunk.length = rChunk.data.length;
} else if (activeCount > 0) {
// Return a partial chunk and allocate a fresh one
rChunk = new Chunk();
rChunk.data = activeChunk;
rChunk.start = 0; // Keep this to zero here
rChunk.length = activeCount;
allocateNewChunk();
} else {
// If no data currently, return null
return null;
}
// Wakeup any write operation waiting for space
notify();
return rChunk;
}
/**
* Wait for some chunks to be read
*/
private void waitOnReader() throws IOException {
try { wait(FOREVER); } catch(InterruptedException e) {}
if ( !_connectionActive )
throw new IOException("Connection Closed");
}
/**
* Package accessor for erroring out and waking up any further activity.
*/
synchronized void connectionClosed() {
LOG.debug("connection closed");
_connectionActive=false;
notify();
}
/**
* Return how many pending chunks are waiting.
*/
synchronized int getPendingChunks() {
// Add the number of list blocks
int count = chunks.size();
// Add one for the current block if data available.
if (activeCount > 0)
count++;
return count;
}
}