/** * */ package com.trendrr.oss.networking; import java.io.IOException; import java.net.SocketException; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.AsynchronousCloseException; import java.nio.channels.ClosedByInterruptException; import java.nio.channels.ClosedChannelException; import java.nio.channels.NotYetConnectedException; import java.nio.channels.ReadableByteChannel; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.util.ArrayList; import java.util.List; import java.util.Stack; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.trendrr.oss.exceptions.TrendrrDisconnectedException; import com.trendrr.oss.exceptions.TrendrrException; /** * @author Dustin Norlander * @created Mar 10, 2011 * */ public class AsynchBuffer { protected static Log log = LogFactory.getLog(AsynchBuffer.class); AtomicInteger bytesPerRead = new AtomicInteger(8192); int bufferPoolSize = 10; //max (empty) buffer size is buffersize * bytesPerRead //buffers ready to be written to. Stack<ByteBuffer> bufferPool = new Stack<ByteBuffer>(); //these hold the actual data that has been read from the socket. List<ByteBuffer> databuffers = new ArrayList<ByteBuffer>(); ConcurrentLinkedQueue<ChannelCallback> callbacks = new ConcurrentLinkedQueue<ChannelCallback>(); /** * Reads from the socketchannel until the delimiter is encountered. this method returns immediately, the * callback is invoked once the data is available. * * @param delimiter * @param callback * @param charset */ public synchronized void readUntil(String delimiter, Charset charset, boolean stripDelimiter, StringReadCallback callback) { this.callbacks.add(new StringReadRequest(delimiter, charset, callback, stripDelimiter)); } /** * reads until the requested delimiter. this method blocks until data is available. * @param dilimiter * @param charset */ public String readUntil(String delimiter, Charset charset, boolean stripDelimiter) throws TrendrrException{ SynchronousReadCallback callback = new SynchronousReadCallback(); this.readUntil(delimiter, charset, stripDelimiter, callback); callback.awaitResponse(); if (callback.exception != null) { throw callback.exception; } return callback.stringResult; } /** * reads the specified number of bytes from the channel, calls the callback when * requested bytes are available. * @param numBytes * @param callback */ public synchronized void readBytes(int numBytes, ByteReadCallback callback) { this.callbacks.add(new ByteReadRequest(numBytes, callback)); } public byte[] readBytes(int numBytes) throws TrendrrException{ SynchronousReadCallback callback = new SynchronousReadCallback(); this.readBytes(numBytes, callback); callback.awaitResponse(); if (callback.exception != null) { throw callback.exception; } return callback.byteResult; } /** * returns the next available buffer from the pool. * @return */ private ByteBuffer getBuffer() { if (bufferPool.isEmpty()) { return ByteBuffer.allocate(this.getBytesPerRead()); } return bufferPool.pop(); } /** * returns a buffer to the pool. * this will automatically clear the buffer, so no need to do that. * * if the pool is full, then this buffer will just be dropped * @param buf */ private void returnBuffer(ByteBuffer buf) { if (this.bufferPool.size() >= this.bufferPoolSize) { return; } buf.clear(); this.bufferPool.push(buf); } /** * reads one buffer (or less) worth of bytes from the channel. * channel settings. * * after calling read it is up to the caller to call process, in order * to process the bytes. * * @param channel * @return * @throws TrendrrException * @throws TrendrrDisconnectedException */ public synchronized int read(ReadableByteChannel channel) throws TrendrrDisconnectedException, TrendrrException { int numRead = this.getBytesPerRead(); int totalRead = 0; try { ByteBuffer buf = this.getBuffer(); numRead = channel.read(buf); totalRead += numRead; if (numRead < 0) { this.returnBuffer(buf); throw new TrendrrDisconnectedException("EOF reached!"); } else if (numRead == 0) { //just return the buffer, we didn't get any data. this.returnBuffer(buf); } else { buf.flip(); this.databuffers.add(buf); } } catch (Exception x) { this.throwException(x); } return totalRead; } public int getBytesPerRead() { return bytesPerRead.get(); } public void setBytesPerRead(int bytesPerRead) { this.bytesPerRead.set(bytesPerRead); } /** * * * attempts to read the requested data from the buffers. * * appropriate callback will be called if requested data is available. * * Exceptions are sent to the callbacks. * * */ public synchronized void process() { while(!this.callbacks.isEmpty()) { if (!this.process(this.callbacks.peek())) break; } } /** * closes the buffer. clears and discards all buffers. * Sends a TrendrrDisconnectedException to all waiting callbacks. */ public synchronized void close() { this.databuffers.clear(); this.bufferPool.clear(); while(!this.callbacks.isEmpty()) { ChannelCallback cb = this.callbacks.poll(); if (cb instanceof ByteReadRequest) { ((ByteReadRequest)cb).flush(); } this.callbacks.poll().onError(new TrendrrDisconnectedException("Buffer is closed, no more data available")); } this.callbacks.clear(); } public boolean hasCallbacksWaiting() { return !this.callbacks.isEmpty(); } private boolean process(ChannelCallback callback){ if (this.databuffers.isEmpty()) { return false; } try { if (callback instanceof StringReadRequest) { try { return this.readString((StringReadRequest)callback); } catch (CharacterCodingException e) { throw new TrendrrException("Error trying to read string", e); } } else if (callback instanceof ByteReadRequest) { return this.readBytes((ByteReadRequest)callback); } else { throw new TrendrrException("UNknown callback type: " + callback); } } catch (TrendrrException x) { callback.onError(x); } this.callbacks.poll(); return true; //unknown type } /** * attempts to read the requested bytes from the current buffer. * * callback is called if read was successfull, this request can then be * discarded. * * @return */ private boolean readBytes(ByteReadRequest request) { List<ByteBuffer> databufs = new ArrayList<ByteBuffer>(); for (ByteBuffer buf : this.databuffers) { if (request.getBuf().hasRemaining()) { try { request.getBuf().put(buf); this.returnBuffer(buf); } catch (BufferOverflowException x) { // means that b has more bytes then allocated in outbuf. while(request.getBuf().hasRemaining()) { request.getBuf().put(buf.get()); } databufs.add(buf); } } else { databufs.add(buf); } } this.databuffers = databufs; if (!request.getBuf().hasRemaining()) { // log.info("GOT The requested # of bytes!"); this.callbacks.poll(); //remove from the queue, must be done BEFORE the callback is called. request.getCallback().byteResult(request.getBuf().array()); return true; } return false; } /** * reads characters until the requested string is found, or buffers are exhausted. * @throws CharacterCodingException */ private boolean readString(StringReadRequest request) throws CharacterCodingException { String delimiter = request.getDelimiter(); StringBuilder builder = request.getBuf(); int fromIndex = Math.max(0, builder.length()-delimiter.length()); List<ByteBuffer> databufs = new ArrayList<ByteBuffer>(); String retVal = null; CharsetDecoder decoder = request.getCharset().newDecoder(); CharBuffer charBuf = CharBuffer.allocate(this.getBytesPerRead()); for (ByteBuffer buf : this.databuffers) { if (retVal != null) { databufs.add(buf); } else { //decode as many bytes into characters as we can. decoder.decode(buf, charBuf, false); charBuf.flip(); // log.info(charBuf.toString()); request.getBuf().append(charBuf); // log.info(this.outstringbuf.length() + " :" + this.outstringbuf.toString() + ": end"); charBuf.clear(); //check that there is at least the possibility of another delimiter. if (request.getBuf().length() >= request.getDelimiter().length() + fromIndex) { int found = builder.indexOf(delimiter, fromIndex); if (found != -1) { String val = builder.toString(); // log.info(val); int endIndex = found; if (!request.isStripDelimiter()) { endIndex += delimiter.length(); } retVal = val.substring(0, endIndex); String remaining = val.substring(found+delimiter.length()); // log.info(remaining); //now need to add this back to the ByteBuffer.. ByteBuffer remainingAsBuf = this.getBuffer().put(remaining.getBytes(request.getCharset())); remainingAsBuf.flip(); databufs.add(remainingAsBuf); } else { fromIndex += delimiter.length(); } } if (buf.hasRemaining()) { databufs.add(buf); if (retVal == null) { //we reached the end of the encodeable data and still haven't found our delimiter throw new CharacterCodingException(); } } else { this.returnBuffer(buf); } } } this.databuffers = databufs; if (retVal != null) { // log.info("GOT The requested String!"); // log.info(retVal); this.callbacks.poll(); //remove from the queue, must be done BEFORE the callback is called. request.getCallback().stringResult(retVal); return true; } return false; } private void throwException(Exception x) throws TrendrrDisconnectedException, TrendrrException{ if (x instanceof NotYetConnectedException) { throw new TrendrrDisconnectedException(x); } if (x instanceof ClosedChannelException) { throw new TrendrrDisconnectedException(x); } if (x instanceof SocketException) { throw new TrendrrDisconnectedException(x); } if (x instanceof AsynchronousCloseException) { throw new TrendrrDisconnectedException(x); } if (x instanceof ClosedByInterruptException) { throw new TrendrrDisconnectedException(x); } if (x instanceof TrendrrException) { throw (TrendrrException)x; } if (x instanceof IOException) { throw new TrendrrDisconnectedException(x); } throw new TrendrrException(x); } }