/* * Copyright 2009 the original author or authors. * * 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.grails.buffer; import grails.util.GrailsArrayUtils; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; import java.util.Iterator; import java.util.LinkedList; /** * An in-memory buffer that provides OutputStream and InputStream interfaces. * * This is more efficient than using ByteArrayOutputStream/ByteArrayInputStream * * This is not thread-safe, it is intended to be used by a single Thread. * * @author Lari Hotari, Sagire Software Oy */ public class StreamByteBuffer { private static final int DEFAULT_CHUNK_SIZE = 8192; private LinkedList<StreamByteBufferChunk> chunks = new LinkedList<StreamByteBufferChunk>(); private StreamByteBufferChunk currentWriteChunk; private StreamByteBufferChunk currentReadChunk = null; private int chunkSize; private StreamByteBufferOutputStream output; private StreamByteBufferInputStream input; private int totalBytesUnreadInList = 0; private int totalBytesUnreadInIterator = 0; private ReadMode readMode; private Iterator<StreamByteBufferChunk> readIterator; public enum ReadMode { REMOVE_AFTER_READING, RETAIN_AFTER_READING } public StreamByteBuffer() { this(DEFAULT_CHUNK_SIZE); } public StreamByteBuffer(int chunkSize) { this(chunkSize, ReadMode.REMOVE_AFTER_READING); } public StreamByteBuffer(int chunkSize, ReadMode readMode) { this.chunkSize = chunkSize; this.readMode = readMode; currentWriteChunk = new StreamByteBufferChunk(chunkSize); output = new StreamByteBufferOutputStream(); input = new StreamByteBufferInputStream(); } public OutputStream getOutputStream() { return output; } public InputStream getInputStream() { return input; } public void writeTo(OutputStream target) throws IOException { while (prepareRead() != -1) { currentReadChunk.writeTo(target); } } public byte[] readAsByteArray() { byte[] buf = new byte[totalBytesUnread()]; input.readImpl(buf, 0, buf.length); return buf; } public String readAsString(String encoding) throws CharacterCodingException { Charset charset = Charset.forName(encoding); return readAsString(charset); } public String readAsString(Charset charset) throws CharacterCodingException { int unreadSize = totalBytesUnread(); if (unreadSize > 0) { CharsetDecoder decoder = charset.newDecoder().onMalformedInput( CodingErrorAction.REPLACE).onUnmappableCharacter( CodingErrorAction.REPLACE); CharBuffer charbuffer = CharBuffer.allocate(unreadSize); ByteBuffer buf = null; while (prepareRead() != -1) { buf = currentReadChunk.readToNioBuffer(); boolean endOfInput = (prepareRead() == -1); CoderResult result = decoder.decode(buf, charbuffer, endOfInput); if (endOfInput) { if (!result.isUnderflow()) { result.throwException(); } } } CoderResult result = decoder.flush(charbuffer); if (buf.hasRemaining()) { throw new IllegalStateException("There's a bug here, buffer wasn't read fully."); } if (!result.isUnderflow()) { result.throwException(); } charbuffer.flip(); String str; if (charbuffer.hasArray()) { int len = charbuffer.remaining(); char[] ch = charbuffer.array(); if (len != ch.length) { ch = (char[])GrailsArrayUtils.subarray(ch, 0, len); } str = StringCharArrayAccessor.createString(ch); } else { str = charbuffer.toString(); } return str; } return null; } public int totalBytesUnread() { int total = 0; if (readMode == ReadMode.REMOVE_AFTER_READING) { total = totalBytesUnreadInList; } else if (readMode == ReadMode.RETAIN_AFTER_READING) { prepareRetainAfterReading(); total = totalBytesUnreadInIterator; } if (currentReadChunk != null) { total += currentReadChunk.bytesUnread(); } if (currentWriteChunk != currentReadChunk && currentWriteChunk != null) { if (readMode == ReadMode.REMOVE_AFTER_READING) { total += currentWriteChunk.bytesUnread(); } else if (readMode == ReadMode.RETAIN_AFTER_READING) { total += currentWriteChunk.bytesUsed(); } } return total; } protected int allocateSpace() { int spaceLeft = currentWriteChunk.spaceLeft(); if (spaceLeft == 0) { chunks.add(currentWriteChunk); totalBytesUnreadInList += currentWriteChunk.bytesUnread(); currentWriteChunk = new StreamByteBufferChunk(chunkSize); spaceLeft = currentWriteChunk.spaceLeft(); } return spaceLeft; } protected int prepareRead() { prepareRetainAfterReading(); int bytesUnread = (currentReadChunk != null) ? currentReadChunk.bytesUnread() : 0; if (bytesUnread == 0) { if (readMode == ReadMode.REMOVE_AFTER_READING && !chunks.isEmpty()) { currentReadChunk = chunks.removeFirst(); bytesUnread = currentReadChunk.bytesUnread(); totalBytesUnreadInList -= bytesUnread; } else if (readMode == ReadMode.RETAIN_AFTER_READING && readIterator.hasNext()) { currentReadChunk = readIterator.next(); currentReadChunk.reset(); bytesUnread = currentReadChunk.bytesUnread(); totalBytesUnreadInIterator -= bytesUnread; } else if (currentReadChunk != currentWriteChunk) { currentReadChunk = currentWriteChunk; bytesUnread = currentReadChunk.bytesUnread(); } else { bytesUnread = -1; } } return bytesUnread; } public void reset() { if (readMode == ReadMode.RETAIN_AFTER_READING) { readIterator = null; prepareRetainAfterReading(); if (currentWriteChunk != null) { currentWriteChunk.reset(); } } } private void prepareRetainAfterReading() { if (readMode == ReadMode.RETAIN_AFTER_READING && readIterator == null) { readIterator = chunks.iterator(); totalBytesUnreadInIterator = totalBytesUnreadInList; currentReadChunk = null; } } public ReadMode getReadMode() { return readMode; } public void setReadMode(ReadMode readMode) { this.readMode = readMode; } public void retainAfterReadingMode() { setReadMode(ReadMode.RETAIN_AFTER_READING); } class StreamByteBufferChunk { private int pointer = 0; private byte[] buffer; private int size; private int used = 0; public StreamByteBufferChunk(int size) { this.size = size; buffer = new byte[size]; } public ByteBuffer readToNioBuffer() { if (pointer < used) { ByteBuffer result; if (pointer > 0 || used < size) { result = ByteBuffer.wrap(buffer, pointer, used - pointer); } else { result = ByteBuffer.wrap(buffer); } pointer = used; return result; } return null; } public boolean write(byte b) { if (used < size) { buffer[used++] = b; return true; } return false; } public void write(byte[] b, int off, int len) { System.arraycopy(b, off, buffer, used, len); used = used + len; } public void read(byte[] b, int off, int len) { System.arraycopy(buffer, pointer, b, off, len); pointer = pointer + len; } public void writeTo(OutputStream target) throws IOException { if (pointer < used) { target.write(buffer, pointer, used - pointer); pointer = used; } } public void reset() { pointer = 0; } public int bytesUsed() { return used; } public int bytesUnread() { return used - pointer; } public int read() { if (pointer < used) { return buffer[pointer++] & 0xff; } return -1; } public int spaceLeft() { return size - used; } } class StreamByteBufferOutputStream extends OutputStream { private boolean closed = false; @Override public void write(byte[] b, int off, int len) throws IOException { if (b == null) { throw new NullPointerException(); } if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } if (len == 0) { return; } int bytesLeft = len; int currentOffset = off; while (bytesLeft > 0) { int spaceLeft = allocateSpace(); int writeBytes = Math.min(spaceLeft, bytesLeft); currentWriteChunk.write(b, currentOffset, writeBytes); bytesLeft -= writeBytes; currentOffset += writeBytes; } } @Override public void close() throws IOException { closed = true; } public boolean isClosed() { return closed; } @Override public void write(int b) throws IOException { allocateSpace(); currentWriteChunk.write((byte) b); } public StreamByteBuffer getBuffer() { return StreamByteBuffer.this; } } class StreamByteBufferInputStream extends InputStream { @Override public int read() throws IOException { prepareRead(); return currentReadChunk.read(); } @Override public int read(byte[] b, int off, int len) throws IOException { return readImpl(b, off, len); } int readImpl(byte[] b, int off, int len) { if (b == null) { throw new NullPointerException(); } if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } if (len == 0) { return 0; } int bytesLeft = len; int currentOffset = off; int bytesUnread = prepareRead(); int totalBytesRead = 0; while (bytesLeft > 0 && bytesUnread != -1) { int readBytes = Math.min(bytesUnread, bytesLeft); currentReadChunk.read(b, currentOffset, readBytes); bytesLeft -= readBytes; currentOffset += readBytes; totalBytesRead += readBytes; bytesUnread = prepareRead(); } if (totalBytesRead > 0) { return totalBytesRead; } return -1; } @Override public synchronized void reset() throws IOException { if (readMode==ReadMode.RETAIN_AFTER_READING) { StreamByteBuffer.this.reset(); } else { // reset isn't supported in ReadMode.REMOVE_AFTER_READING super.reset(); } } @Override public int available() throws IOException { return totalBytesUnread(); } public StreamByteBuffer getBuffer() { return StreamByteBuffer.this; } } public void clear() { chunks.clear(); currentReadChunk = null; totalBytesUnreadInList = 0; totalBytesUnreadInIterator = 0; currentWriteChunk = new StreamByteBufferChunk(chunkSize); readIterator = null; } }