org.apache.myfaces.shared_tomahawk.util.StreamCharBuffer Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.myfaces.shared_tomahawk.util;
import java.io.EOFException;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Reader;
import java.io.Writer;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
*
* StreamCharBuffer is a multipurpose in-memory buffer that can replace JDK
* in-memory buffers (StringBuffer, StringBuilder, StringWriter).
*
*
*
* Grails GSP rendering uses this class as a buffer that is optimized for performance.
*
*
*
* StreamCharBuffer keeps the buffer in a linked list of "chunks". The main
* difference compared to JDK in-memory buffers (StringBuffer, StringBuilder &
* StringWriter) is that the buffer can be held in several smaller buffers
* ("chunks" here). In JDK in-memory buffers, the buffer has to be expanded
* whenever it gets filled up. The old buffer's data is copied to the new one
* and the old one is discarded. In StreamCharBuffer, there are several ways to
* prevent unnecessary allocation & copy operations. The StreamCharBuffer
* contains a linked list of different type of chunks: char arrays,
* java.lang.String chunks and other StreamCharBuffers as sub chunks. A
* StringChunk is appended to the linked list whenever a java.lang.String of a
* length that exceeds the "stringChunkMinSize" value is written to the buffer.
*
*
*
* Grails tag libraries also use a StreamCharBuffer to "capture" the output of
* the taglib and return it to the caller. The buffer can be appended to it's
* parent buffer directly without extra object generation (like converting to
* java.lang.String in between).
*
* for example this line of code in a taglib would just append the buffer
* returned from the body closure evaluation to the buffer of the taglib:
*
* out << body()
*
* other example:
*
* out << g.render(template: '/some/template', model:[somebean: somebean])
*
* There's no extra java.lang.String generation overhead.
*
*
*
*
* There's a java.io.Writer interface for appending character data to the buffer
* and a java.io.Reader interface for reading data.
*
*
*
* Each {@link #getReader()} call will create a new reader instance that keeps
* it own state.
* There is a alternative method {@link #getReader(boolean)} for creating the
* reader. When reader is created by calling getReader(true), the reader will
* remove already read characters from the buffer. In this mode only a single
* reader instance is supported.
*
*
*
* There's also several other options for reading data:
* {@link #readAsCharArray()} reads the buffer to a char[] array
* {@link #readAsString()} reads the buffer and wraps the char[] data as a
* String
* {@link #writeTo(Writer)} writes the buffer to a java.io.Writer
* {@link #toCharArray()} returns the buffer as a char[] array, caches the
* return value internally so that this method can be called several times.
* {@link #toString()} returns the buffer as a String, caches the return value
* internally
*
*
*
* By using the "connectTo" method, one can connect the buffer directly to a
* target java.io.Writer. The internal buffer gets flushed automaticly to the
* target whenever the buffer gets filled up. See connectTo(Writer).
*
*
*
* This class is not thread-safe. Object instances of this class are
* intended to be used by a single Thread. The Reader and Writer interfaces can
* be open simultaneous and the same Thread can write and read several times.
*
*
*
* Main operation principle:
*
*
* StreamCharBuffer keeps the buffer in a linked link of "chunks".
* The main difference compared to JDK in-memory buffers (StringBuffer,
* StringBuilder & StringWriter) is that the buffer can be held in several
* smaller buffers ("chunks" here).
* In JDK in-memory buffers, the buffer has to be expanded whenever it gets
* filled up. The old buffer's data is copied to the new one and the old one is
* discarded.
* In StreamCharBuffer, there are several ways to prevent unnecessary allocation
* & copy operations.
*
*
* There can be several different type of chunks: char arrays (
* {@code CharBufferChunk}), String chunks ({@code StringChunk}) and other
* StreamCharBuffers as sub chunks ({@code StreamCharBufferSubChunk}).
*
*
* Child StreamCharBuffers can be changed after adding to parent buffer. The
* flush() method must be called on the child buffer's Writer to notify the
* parent that the child buffer's content has been changed (used for calculating
* total size).
*
*
* A StringChunk is appended to the linked list whenever a java.lang.String of a
* length that exceeds the "stringChunkMinSize" value is written to the buffer.
*
*
* If the buffer is in "connectTo" mode, any String or char[] that's length is
* over writeDirectlyToConnectedMinSize gets written directly to the target. The
* buffer content will get fully flushed to the target before writing the String
* or char[].
*
*
* There can be several targets "listening" the buffer in "connectTo" mode. The
* same content will be written to all targets.
*
*
* Growable chunksize: By default, a newly allocated chunk's size will grow
* based on the total size of all written chunks.
* The default growProcent value is 100. If the total size is currently 1024,
* the newly created chunk will have a internal buffer that's size is 1024.
* Growable chunksize can be turned off by setting the growProcent to 0.
* There's a default maximum chunksize of 1MB by default. The minimum size is
* the initial chunksize size.
*
*
*
* System properties to change default configuration parameters:
*
*
* System Property name
* Description
* Default value
*
*
* streamcharbuffer.chunksize
* default chunk size - the size the first allocated buffer
* 512
*
*
* streamcharbuffer.maxchunksize
* maximum chunk size - the maximum size of the allocated buffer
* 1048576
*
*
* streamcharbuffer.growprocent
* growing buffer percentage - the newly allocated buffer is defined by
* total_size * (growpercent/100)
* 100
*
*
* streamcharbuffer.subbufferchunkminsize
* minimum size of child StreamCharBuffer chunk - if the size is smaller,
* the content is copied to the parent buffer
* 512
*
*
* streamcharbuffer.substringchunkminsize
* minimum size of String chunks - if the size is smaller, the content is
* copied to the buffer
* 512
*
*
* streamcharbuffer.chunkminsize
* minimum size of chunk that gets written directly to the target in
* connected mode.
* 256
*
*
*
* Configuration values can also be changed for each instance of
* StreamCharBuffer individually. Default values are defined with System
* Properties.
*
*
*
* @author Lari Hotari, Sagire Software Oy
* @see org.codehaus.groovy.grails.web.util.StreamCharBuffer
* file licensed under ASL v2.0
* Copyright 2009 the original author or authors.
*/
public class StreamCharBuffer implements /*Writable,*/CharSequence,
Externalizable
{
static final long serialVersionUID = 5486972234419632945L;
//private static final Log log=LogFactory.getLog(StreamCharBuffer.class);
private static final int DEFAULT_CHUNK_SIZE = Integer.getInteger(
"oam.streamcharbuffer.chunksize", 512);
private static final int DEFAULT_MAX_CHUNK_SIZE = Integer.getInteger(
"oam.streamcharbuffer.maxchunksize", 1024 * 1024);
private static final int DEFAULT_CHUNK_SIZE_GROW_PROCENT = Integer
.getInteger("oam.streamcharbuffer.growprocent", 100);
private static final int SUB_BUFFERCHUNK_MIN_SIZE = Integer.getInteger(
"oam.streamcharbuffer.subbufferchunkminsize", 512);
private static final int SUB_STRINGCHUNK_MIN_SIZE = Integer.getInteger(
"oam.streamcharbuffer.substringchunkminsize", 512);
private static final int WRITE_DIRECT_MIN_SIZE = Integer.getInteger(
"oam.streamcharbuffer.writedirectminsize", 1024);
private static final int CHUNK_MIN_SIZE = Integer.getInteger(
"oam.streamcharbuffer.chunkminsize", 256);
private final int firstChunkSize;
private final int growProcent;
private final int maxChunkSize;
private int subStringChunkMinSize = SUB_STRINGCHUNK_MIN_SIZE;
private int subBufferChunkMinSize = SUB_BUFFERCHUNK_MIN_SIZE;
private int writeDirectlyToConnectedMinSize = WRITE_DIRECT_MIN_SIZE;
private int chunkMinSize = CHUNK_MIN_SIZE;
private int chunkSize;
private int totalChunkSize;
private final StreamCharBufferWriter writer;
private List connectedWriters;
private Writer connectedWritersWriter;
boolean preferSubChunkWhenWritingToOtherBuffer = false;
private AllocatedBuffer allocBuffer;
private AbstractChunk firstChunk;
private AbstractChunk lastChunk;
private int totalCharsInList;
private int totalCharsInDynamicChunks;
private int sizeAtLeast;
private StreamCharBufferKey bufferKey = new StreamCharBufferKey();
private Map dynamicChunkMap;
private Set> parentBuffers;
int allocatedBufferIdSequence = 0;
int readerCount = 0;
boolean hasReaders = false;
public StreamCharBuffer()
{
this(DEFAULT_CHUNK_SIZE, DEFAULT_CHUNK_SIZE_GROW_PROCENT,
DEFAULT_MAX_CHUNK_SIZE);
}
public StreamCharBuffer(int chunkSize)
{
this(chunkSize, DEFAULT_CHUNK_SIZE_GROW_PROCENT, DEFAULT_MAX_CHUNK_SIZE);
}
public StreamCharBuffer(int chunkSize, int growProcent)
{
this(chunkSize, growProcent, DEFAULT_MAX_CHUNK_SIZE);
}
public StreamCharBuffer(int chunkSize, int growProcent, int maxChunkSize)
{
this.firstChunkSize = chunkSize;
this.growProcent = growProcent;
this.maxChunkSize = maxChunkSize;
writer = new StreamCharBufferWriter();
reset(true);
}
private class StreamCharBufferKey
{
StreamCharBuffer getBuffer()
{
return StreamCharBuffer.this;
}
}
public boolean isPreferSubChunkWhenWritingToOtherBuffer()
{
return preferSubChunkWhenWritingToOtherBuffer;
}
public void setPreferSubChunkWhenWritingToOtherBuffer(
boolean preferSubChunkWhenWritingToOtherBuffer)
{
this.preferSubChunkWhenWritingToOtherBuffer = preferSubChunkWhenWritingToOtherBuffer;
}
public final void reset()
{
reset(true);
}
/**
* resets the state of this buffer (empties it)
*
* @param resetChunkSize
*/
public final void reset(boolean resetChunkSize)
{
firstChunk = null;
lastChunk = null;
totalCharsInList = 0;
totalCharsInDynamicChunks = -1;
sizeAtLeast = -1;
if (resetChunkSize)
{
chunkSize = firstChunkSize;
totalChunkSize = 0;
}
allocBuffer = new AllocatedBuffer(chunkSize);
dynamicChunkMap = new HashMap();
}
/**
* Clears the buffer and notifies the parents of this buffer of the change
*
*/
public final void clear()
{
reset();
notifyBufferChange();
}
/**
* Connect this buffer to a target Writer.
*
* When the buffer (a chunk) get filled up, it will automaticly write it's content to the Writer
*
* @param w
*/
public final void connectTo(Writer w)
{
connectTo(w, true);
}
public final void connectTo(Writer w, boolean autoFlush)
{
initConnected();
connectedWriters.add(new ConnectedWriter(w, autoFlush));
initConnectedWritersWriter();
}
private void initConnectedWritersWriter()
{
if (connectedWriters.size() > 1)
{
connectedWritersWriter = new MultiOutputWriter(connectedWriters);
}
else
{
connectedWritersWriter = new SingleOutputWriter(
connectedWriters.get(0));
}
}
public final void connectTo(LazyInitializingWriter w)
{
connectTo(w, true);
}
public final void connectTo(LazyInitializingWriter w, boolean autoFlush)
{
initConnected();
connectedWriters.add(new ConnectedWriter(w, autoFlush));
initConnectedWritersWriter();
}
public final void removeConnections()
{
if (connectedWriters != null)
{
connectedWriters.clear();
connectedWritersWriter = null;
}
}
private void initConnected()
{
if (connectedWriters == null)
{
connectedWriters = new ArrayList(2);
}
}
public int getSubStringChunkMinSize()
{
return subStringChunkMinSize;
}
/**
* Minimum size for a String to be added as a StringChunk instead of copying content to
* the char[] buffer of the current StreamCharBufferChunk
*
* @param stringChunkMinSize
*/
public void setSubStringChunkMinSize(int stringChunkMinSize)
{
this.subStringChunkMinSize = stringChunkMinSize;
}
public int getSubBufferChunkMinSize()
{
return subBufferChunkMinSize;
}
public void setSubBufferChunkMinSize(int subBufferChunkMinSize)
{
this.subBufferChunkMinSize = subBufferChunkMinSize;
}
public int getWriteDirectlyToConnectedMinSize()
{
return writeDirectlyToConnectedMinSize;
}
/**
* Minimum size for a String or char[] to get written directly to connected writer (in "connectTo" mode).
*
* @param writeDirectlyToConnectedMinSize
*/
public void setWriteDirectlyToConnectedMinSize(
int writeDirectlyToConnectedMinSize)
{
this.writeDirectlyToConnectedMinSize = writeDirectlyToConnectedMinSize;
}
public int getChunkMinSize()
{
return chunkMinSize;
}
public void setChunkMinSize(int chunkMinSize)
{
this.chunkMinSize = chunkMinSize;
}
/**
* Writer interface for adding/writing data to the buffer.
*
* @return the Writer
*/
public Writer getWriter()
{
return writer;
}
/**
* Creates a new Reader instance for reading/consuming data from the buffer.
* Each call creates a new instance that will keep it's reading state. There can be several readers on
* the buffer. (single thread only supported)
*
* @return the Reader
*/
public Reader getReader()
{
return getReader(false);
}
/**
* Like getReader(), but when removeAfterReading is true, the read data will be removed from the buffer.
*
* @param removeAfterReading
* @return the Reader
*/
public Reader getReader(boolean removeAfterReading)
{
readerCount++;
hasReaders = true;
return new StreamCharBufferReader(removeAfterReading);
}
/**
* Writes the buffer content to a target java.io.Writer
*
* @param target
* @throws IOException
*/
public Writer writeTo(Writer target) throws IOException
{
writeTo(target, false, false);
return getWriter();
}
/**
* Writes the buffer content to a target java.io.Writer
*
* @param target Writer
* @param flushTarget calls target.flush() before finishing
* @param emptyAfter empties the buffer if true
* @throws IOException
*/
public void writeTo(Writer target, boolean flushTarget, boolean emptyAfter)
throws IOException
{
//if (target instanceof GrailsWrappedWriter) {
// target = ((GrailsWrappedWriter)target).unwrap();
//}
if (target instanceof StreamCharBufferWriter)
{
if (target == writer)
{
throw new IllegalArgumentException(
"Cannot write buffer to itself.");
}
((StreamCharBufferWriter) target).write(this);
return;
}
writeToImpl(target, flushTarget, emptyAfter);
}
private void writeToImpl(Writer target, boolean flushTarget,
boolean emptyAfter) throws IOException
{
AbstractChunk current = firstChunk;
while (current != null)
{
current.writeTo(target);
current = current.next;
}
if (emptyAfter)
{
firstChunk = null;
lastChunk = null;
totalCharsInList = 0;
totalCharsInDynamicChunks = -1;
sizeAtLeast = -1;
dynamicChunkMap.clear();
}
allocBuffer.writeTo(target);
if (emptyAfter)
{
allocBuffer.reuseBuffer();
}
if (flushTarget)
{
target.flush();
}
}
/**
* Reads the buffer to a char[].
*
* @return the chars
*/
public char[] readAsCharArray()
{
int currentSize = size();
if (currentSize == 0)
{
return new char[0];
}
FixedCharArrayWriter target = new FixedCharArrayWriter(currentSize);
try
{
writeTo(target);
}
catch (IOException e)
{
throw new RuntimeException("Unexpected IOException", e);
}
return target.getCharArray();
}
/**
* Reads the buffer to a String.
*
* @return the String
*/
public String readAsString()
{
char[] buf = readAsCharArray();
if (buf.length > 0)
{
return StringCharArrayAccessor.createString(buf);
}
return "";
}
/**
* {@inheritDoc}
*
* Reads (and empties) the buffer to a String, but caches the return value for subsequent calls.
* If more content has been added between 2 calls, the returned value will be joined from the previously
* cached value and the data read from the buffer.
*
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
if (firstChunk == lastChunk && firstChunk instanceof StringChunk
&& allocBuffer.charsUsed() == 0
&& ((StringChunk) firstChunk).isSingleBuffer())
{
return ((StringChunk) firstChunk).str;
}
int initialReaderCount = readerCount;
String str = readAsString();
if (initialReaderCount == 0)
{
// if there are no readers, the result can be cached
reset();
if (str.length() > 0)
{
addChunk(new StringChunk(str, 0, str.length()));
}
}
return str;
}
/**
* {@inheritDoc}
*
* Uses String's hashCode to support compatibility with String instances in maps, sets, etc.
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode()
{
return toString().hashCode();
}
/**
* equals uses String.equals to check for equality to support compatibility with String instances
* in maps, sets, etc.
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object o)
{
if (o == this)
{
return true;
}
if (!(o instanceof CharSequence))
{
return false;
}
CharSequence other = (CharSequence) o;
return toString().equals(other.toString());
}
public String plus(String value)
{
return toString() + value;
}
public String plus(Object value)
{
return toString() + String.valueOf(value);
}
/**
* Reads the buffer to a char[].
*
* Caches the result if there aren't any readers.
*
* @return the chars
*/
public char[] toCharArray()
{
// check if there is a cached single charbuffer
if (firstChunk == lastChunk && firstChunk instanceof CharBufferChunk
&& allocBuffer.charsUsed() == 0
&& ((CharBufferChunk) firstChunk).isSingleBuffer())
{
return ((CharBufferChunk) firstChunk).buffer;
}
int initialReaderCount = readerCount;
char[] buf = readAsCharArray();
if (initialReaderCount == 0)
{
// if there are no readers, the result can be cached
reset();
if (buf.length > 0)
{
addChunk(new CharBufferChunk(-1, buf, 0, buf.length));
}
}
return buf;
}
public int size()
{
int total = totalCharsInList;
if (totalCharsInDynamicChunks == -1)
{
totalCharsInDynamicChunks = 0;
for (StreamCharBufferSubChunk chunk : dynamicChunkMap.values())
{
totalCharsInDynamicChunks += chunk.size();
}
}
total += totalCharsInDynamicChunks;
total += allocBuffer.charsUsed();
sizeAtLeast = total;
return total;
}
public boolean isEmpty()
{
return !isNotEmpty();
}
boolean isNotEmpty()
{
if (totalCharsInList > 0)
{
return true;
}
if (totalCharsInDynamicChunks > 0)
{
return true;
}
if (allocBuffer.charsUsed() > 0)
{
return true;
}
if (totalCharsInDynamicChunks == -1)
{
for (StreamCharBufferSubChunk chunk : dynamicChunkMap.values())
{
if (chunk.getSubBuffer().isNotEmpty())
{
return true;
}
}
}
return false;
}
boolean isSizeLarger(int minSize)
{
if (minSize <= sizeAtLeast)
{
return true;
}
boolean retval = calculateIsSizeLarger(minSize);
if (retval && minSize > sizeAtLeast)
{
sizeAtLeast = minSize;
}
return retval;
}
private boolean calculateIsSizeLarger(int minSize)
{
int total = totalCharsInList;
total += allocBuffer.charsUsed();
if (total > minSize)
{
return true;
}
if (totalCharsInDynamicChunks != -1)
{
total += totalCharsInDynamicChunks;
if (total > minSize)
{
return true;
}
}
else
{
for (StreamCharBufferSubChunk chunk : dynamicChunkMap.values())
{
if (!chunk.hasCachedSize()
&& chunk.getSubBuffer().isSizeLarger(minSize - total))
{
return true;
}
total += chunk.size();
if (total > minSize)
{
return true;
}
}
}
return false;
}
int allocateSpace() throws IOException
{
int spaceLeft = allocBuffer.spaceLeft();
if (spaceLeft == 0)
{
spaceLeft = appendCharBufferChunk(true);
}
return spaceLeft;
}
private int appendCharBufferChunk(boolean flushInConnected)
throws IOException
{
int spaceLeft = 0;
if (flushInConnected && isConnectedMode())
{
flushToConnected();
if (!isChunkSizeResizeable())
{
allocBuffer.reuseBuffer();
spaceLeft = allocBuffer.spaceLeft();
}
else
{
spaceLeft = 0;
}
}
else
{
if (allocBuffer.hasChunk())
{
addChunk(allocBuffer.createChunk());
}
spaceLeft = allocBuffer.spaceLeft();
}
if (spaceLeft == 0)
{
totalChunkSize += allocBuffer.chunkSize();
resizeChunkSizeAsProcentageOfTotalSize();
allocBuffer = new AllocatedBuffer(chunkSize);
spaceLeft = allocBuffer.spaceLeft();
}
return spaceLeft;
}
void appendStringChunk(String str, int off, int len) throws IOException
{
appendCharBufferChunk(false);
addChunk(new StringChunk(str, off, len));
}
public void appendStreamCharBufferChunk(StreamCharBuffer subBuffer)
throws IOException
{
appendCharBufferChunk(false);
addChunk(new StreamCharBufferSubChunk(subBuffer));
}
void addChunk(AbstractChunk newChunk)
{
if (lastChunk != null)
{
lastChunk.next = newChunk;
if (hasReaders)
{
// double link only if there are active readers since backwards iterating is only required
//for simultaneous writer & reader
newChunk.prev = lastChunk;
}
}
lastChunk = newChunk;
if (firstChunk == null)
{
firstChunk = newChunk;
}
if (newChunk instanceof StreamCharBufferSubChunk)
{
StreamCharBufferSubChunk bufSubChunk = (StreamCharBufferSubChunk) newChunk;
dynamicChunkMap.put(bufSubChunk.streamCharBuffer.bufferKey,
bufSubChunk);
}
else
{
totalCharsInList += newChunk.size();
}
}
public boolean isConnectedMode()
{
return connectedWriters != null && !connectedWriters.isEmpty();
}
private void flushToConnected() throws IOException
{
writeTo(connectedWritersWriter, true, true);
}
protected boolean isChunkSizeResizeable()
{
return (growProcent > 0);
}
protected void resizeChunkSizeAsProcentageOfTotalSize()
{
if (growProcent == 0)
{
return;
}
if (growProcent == 100)
{
chunkSize = Math.min(totalChunkSize, maxChunkSize);
}
else if (growProcent == 200)
{
chunkSize = Math.min(totalChunkSize << 1, maxChunkSize);
}
else if (growProcent > 0)
{
chunkSize = Math.max(Math.min((totalChunkSize * growProcent) / 100,
maxChunkSize), firstChunkSize);
}
}
protected static final void arrayCopy(char[] src, int srcPos, char[] dest,
int destPos, int length)
{
if (length == 1)
{
dest[destPos] = src[srcPos];
}
else
{
System.arraycopy(src, srcPos, dest, destPos, length);
}
}
/**
* This is the java.io.Writer implementation for StreamCharBuffer
*
* @author Lari Hotari, Sagire Software Oy
*/
public final class StreamCharBufferWriter extends Writer
{
boolean closed = false;
int writerUsedCounter = 0;
boolean increaseCounter = true;
@Override
public final void write(final char[] b, final int off, final 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;
}
markUsed();
if (shouldWriteDirectly(len))
{
appendCharBufferChunk(true);
connectedWritersWriter.write(b, off, len);
}
else
{
int charsLeft = len;
int currentOffset = off;
while (charsLeft > 0)
{
int spaceLeft = allocateSpace();
int writeChars = Math.min(spaceLeft, charsLeft);
allocBuffer.write(b, currentOffset, writeChars);
charsLeft -= writeChars;
currentOffset += writeChars;
}
}
}
private final boolean shouldWriteDirectly(final int len)
{
if (!isConnectedMode())
{
return false;
}
if (!(writeDirectlyToConnectedMinSize >= 0 && len >= writeDirectlyToConnectedMinSize))
{
return false;
}
return isNextChunkBigEnough(len);
}
private final boolean isNextChunkBigEnough(final int len)
{
return (len > getNewChunkMinSize());
}
private final int getDirectChunkMinSize()
{
if (!isConnectedMode())
{
return -1;
}
if (writeDirectlyToConnectedMinSize >= 0)
{
return writeDirectlyToConnectedMinSize;
}
return getNewChunkMinSize();
}
private final int getNewChunkMinSize()
{
if (chunkMinSize <= 0 || allocBuffer.charsUsed() == 0
|| allocBuffer.charsUsed() >= chunkMinSize)
{
return 0;
}
return allocBuffer.spaceLeft();
}
@Override
public final void write(final String str) throws IOException
{
write(str, 0, str.length());
}
@Override
public final void write(final String str, final int off, final int len)
throws IOException
{
if (len == 0)
{
return;
}
markUsed();
if (shouldWriteDirectly(len))
{
appendCharBufferChunk(true);
connectedWritersWriter.write(str, off, len);
}
else if (len >= subStringChunkMinSize && isNextChunkBigEnough(len))
{
appendStringChunk(str, off, len);
}
else
{
int charsLeft = len;
int currentOffset = off;
while (charsLeft > 0)
{
int spaceLeft = allocateSpace();
int writeChars = Math.min(spaceLeft, charsLeft);
allocBuffer.writeString(str, currentOffset, writeChars);
charsLeft -= writeChars;
currentOffset += writeChars;
}
}
}
public final void write(StreamCharBuffer subBuffer) throws IOException
{
markUsed();
int directChunkMinSize = getDirectChunkMinSize();
if (directChunkMinSize != -1
&& subBuffer.isSizeLarger(directChunkMinSize))
{
appendCharBufferChunk(true);
subBuffer.writeToImpl(connectedWritersWriter, false, false);
}
else if (subBuffer.preferSubChunkWhenWritingToOtherBuffer
|| subBuffer.isSizeLarger(Math.max(subBufferChunkMinSize,
getNewChunkMinSize())))
{
if (subBuffer.preferSubChunkWhenWritingToOtherBuffer)
{
StreamCharBuffer.this.preferSubChunkWhenWritingToOtherBuffer = true;
}
appendStreamCharBufferChunk(subBuffer);
subBuffer.addParentBuffer(StreamCharBuffer.this);
}
else
{
subBuffer.writeToImpl(this, false, false);
}
}
@Override
public final Writer append(final CharSequence csq, final int start,
final int end) throws IOException
{
markUsed();
if (csq == null)
{
write("null");
}
else
{
if (csq instanceof String || csq instanceof StringBuffer
|| csq instanceof StringBuilder)
{
int len = end - start;
int charsLeft = len;
int currentOffset = start;
while (charsLeft > 0)
{
int spaceLeft = allocateSpace();
int writeChars = Math.min(spaceLeft, charsLeft);
if (csq instanceof String)
{
allocBuffer.writeString((String) csq,
currentOffset, writeChars);
}
else if (csq instanceof StringBuffer)
{
allocBuffer.writeStringBuffer((StringBuffer) csq,
currentOffset, writeChars);
}
else if (csq instanceof StringBuilder)
{
allocBuffer.writeStringBuilder((StringBuilder) csq,
currentOffset, writeChars);
}
charsLeft -= writeChars;
currentOffset += writeChars;
}
}
else
{
write(csq.subSequence(start, end).toString());
}
}
return this;
}
@Override
public final Writer append(final CharSequence csq) throws IOException
{
markUsed();
if (csq == null)
{
write("null");
}
else
{
append(csq, 0, csq.length());
}
return this;
}
@Override
public void close() throws IOException
{
closed = true;
flush();
}
public boolean isClosed()
{
return closed;
}
public boolean isUsed()
{
return writerUsedCounter > 0;
}
public final void markUsed()
{
if (increaseCounter)
{
writerUsedCounter++;
if (!hasReaders)
{
increaseCounter = false;
}
}
}
public int resetUsed()
{
int prevUsed = writerUsedCounter;
writerUsedCounter = 0;
increaseCounter = true;
return prevUsed;
}
@Override
public void write(final int b) throws IOException
{
markUsed();
allocateSpace();
allocBuffer.write((char) b);
}
@Override
public void flush() throws IOException
{
if (isConnectedMode())
{
flushToConnected();
}
notifyBufferChange();
}
public final StreamCharBuffer getBuffer()
{
return StreamCharBuffer.this;
}
}
/**
* This is the java.io.Reader implementation for StreamCharBuffer
*
* @author Lari Hotari, Sagire Software Oy
*/
final public class StreamCharBufferReader extends Reader
{
boolean eofException = false;
int eofReachedCounter = 0;
ChunkReader chunkReader;
ChunkReader lastChunkReader;
boolean removeAfterReading;
public StreamCharBufferReader(boolean removeAfterReading)
{
this.removeAfterReading = removeAfterReading;
}
private int prepareRead(int len)
{
if (hasReaders && eofReachedCounter != 0)
{
if (eofReachedCounter != writer.writerUsedCounter)
{
eofReachedCounter = 0;
eofException = false;
repositionChunkReader();
}
}
if (chunkReader == null && eofReachedCounter == 0)
{
if (firstChunk != null)
{
chunkReader = firstChunk.getChunkReader(removeAfterReading);
if (removeAfterReading)
{
firstChunk.subtractFromTotalCount();
}
}
else
{
chunkReader = new AllocatedBufferReader(allocBuffer,
removeAfterReading);
}
}
int available = 0;
if (chunkReader != null)
{
available = chunkReader.getReadLenLimit(len);
while (available == 0 && chunkReader != null)
{
chunkReader = chunkReader.next();
if (chunkReader != null)
{
available = chunkReader.getReadLenLimit(len);
}
else
{
available = 0;
}
}
}
if (chunkReader == null)
{
if (hasReaders)
{
eofReachedCounter = writer.writerUsedCounter;
}
else
{
eofReachedCounter = 1;
}
}
else if (hasReaders)
{
lastChunkReader = chunkReader;
}
return available;
}
/* adds support for reading and writing simultaneously in the same thread */
private void repositionChunkReader()
{
if (lastChunkReader instanceof AllocatedBufferReader)
{
if (lastChunkReader.isValid())
{
chunkReader = lastChunkReader;
}
else
{
AllocatedBufferReader allocBufferReader = (AllocatedBufferReader) lastChunkReader;
// find out what is the CharBufferChunk that was read by the AllocatedBufferReader already
int currentPosition = allocBufferReader.position;
AbstractChunk chunk = lastChunk;
while (chunk != null
&& chunk.writerUsedCounter >= lastChunkReader
.getWriterUsedCounter())
{
if (chunk instanceof CharBufferChunk)
{
CharBufferChunk charBufChunk = (CharBufferChunk) chunk;
if (charBufChunk.allocatedBufferId == allocBufferReader.parent.id)
{
if (currentPosition >= charBufChunk.offset
&& currentPosition <= charBufChunk.lastposition)
{
CharBufferChunkReader charBufChunkReader = (CharBufferChunkReader) charBufChunk
.getChunkReader(removeAfterReading);
int oldpointer = charBufChunkReader.pointer;
// skip the already chars
charBufChunkReader.pointer = currentPosition;
if (removeAfterReading)
{
int diff = charBufChunkReader.pointer
- oldpointer;
totalCharsInList -= diff;
charBufChunk.subtractFromTotalCount();
}
chunkReader = charBufChunkReader;
break;
}
}
}
chunk = chunk.prev;
}
}
}
}
@Override
public boolean ready() throws IOException
{
return true;
}
@Override
public final int read(final char[] b, final int off, final int len)
throws IOException
{
return readImpl(b, off, len);
}
final int readImpl(final char[] b, final int off, final 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 0;
}
int charsLeft = len;
int currentOffset = off;
int readChars = prepareRead(charsLeft);
if (eofException)
{
throw new EOFException();
}
int totalCharsRead = 0;
while (charsLeft > 0 && readChars > 0)
{
chunkReader.read(b, currentOffset, readChars);
charsLeft -= readChars;
currentOffset += readChars;
totalCharsRead += readChars;
if (charsLeft > 0)
{
readChars = prepareRead(charsLeft);
}
}
if (totalCharsRead > 0)
{
return totalCharsRead;
}
eofException = true;
return -1;
}
@Override
public void close() throws IOException
{
// do nothing
}
public final StreamCharBuffer getBuffer()
{
return StreamCharBuffer.this;
}
public int getReadLenLimit(int askedAmount)
{
return prepareRead(askedAmount);
}
}
abstract class AbstractChunk
{
AbstractChunk next;
AbstractChunk prev;
int writerUsedCounter;
public AbstractChunk()
{
if (hasReaders)
{
writerUsedCounter = writer.writerUsedCounter;
}
else
{
writerUsedCounter = 1;
}
}
public abstract void writeTo(Writer target) throws IOException;
public abstract ChunkReader getChunkReader(boolean removeAfterReading);
public abstract int size();
public int getWriterUsedCounter()
{
return writerUsedCounter;
}
public void subtractFromTotalCount()
{
totalCharsInList -= size();
}
}
// keep read state in this class
static abstract class ChunkReader
{
public abstract int read(char[] ch, int off, int len)
throws IOException;
public abstract int getReadLenLimit(int askedAmount);
public abstract ChunkReader next();
public abstract int getWriterUsedCounter();
public abstract boolean isValid();
}
final class AllocatedBuffer
{
private int id = allocatedBufferIdSequence++;
private int size;
private char[] buffer;
private int used = 0;
private int chunkStart = 0;
public AllocatedBuffer(int size)
{
this.size = size;
buffer = new char[size];
}
public int charsUsed()
{
return used - chunkStart;
}
public void writeTo(Writer target) throws IOException
{
if (used - chunkStart > 0)
{
target.write(buffer, chunkStart, used - chunkStart);
}
}
public void reuseBuffer()
{
used = 0;
chunkStart = 0;
}
public int chunkSize()
{
return buffer.length;
}
public int spaceLeft()
{
return size - used;
}
public boolean write(final char ch)
{
if (used < size)
{
buffer[used++] = ch;
return true;
}
return false;
}
public final void write(final char[] ch, final int off, final int len)
{
arrayCopy(ch, off, buffer, used, len);
used += len;
}
public final void writeString(final String str, final int off,
final int len)
{
str.getChars(off, off + len, buffer, used);
used += len;
}
public final void writeStringBuilder(final StringBuilder stringBuilder,
final int off, final int len)
{
stringBuilder.getChars(off, off + len, buffer, used);
used += len;
}
public final void writeStringBuffer(final StringBuffer stringBuffer,
final int off, final int len)
{
stringBuffer.getChars(off, off + len, buffer, used);
used += len;
}
/**
* Creates a new chunk from the content written to the buffer
* (used before adding StringChunk or StreamCharBufferChunk).
*
* @return the chunk
*/
public CharBufferChunk createChunk()
{
CharBufferChunk chunk = new CharBufferChunk(id, buffer, chunkStart,
used - chunkStart);
chunkStart = used;
return chunk;
}
public boolean hasChunk()
{
return (used > chunkStart);
}
}
/**
* The data in the buffer is stored in a linked list of StreamCharBufferChunks.
*
* This class contains data & read/write state for the "chunk level".
* It contains methods for reading & writing to the chunk level.
*
* Underneath the chunk is one more level, the StringChunkGroup + StringChunk.
* StringChunk makes it possible to directly store the java.lang.String objects.
*
* @author Lari Hotari
*
*/
final class CharBufferChunk extends AbstractChunk
{
int allocatedBufferId;
char[] buffer;
int offset;
int lastposition;
int length;
public CharBufferChunk(int allocatedBufferId, char[] buffer,
int offset, int len)
{
super();
this.allocatedBufferId = allocatedBufferId;
this.buffer = buffer;
this.offset = offset;
this.lastposition = offset + len;
this.length = len;
}
@Override
public void writeTo(final Writer target) throws IOException
{
target.write(buffer, offset, length);
}
@Override
public ChunkReader getChunkReader(boolean removeAfterReading)
{
return new CharBufferChunkReader(this, removeAfterReading);
}
@Override
public int size()
{
return length;
}
public boolean isSingleBuffer()
{
return offset == 0 && length == buffer.length;
}
}
abstract class AbstractChunkReader extends ChunkReader
{
private AbstractChunk parentChunk;
private boolean removeAfterReading;
public AbstractChunkReader(AbstractChunk parentChunk,
boolean removeAfterReading)
{
this.parentChunk = parentChunk;
this.removeAfterReading = removeAfterReading;
}
@Override
public boolean isValid()
{
return true;
}
@Override
public ChunkReader next()
{
if (removeAfterReading)
{
if (firstChunk == parentChunk)
{
firstChunk = null;
}
if (lastChunk == parentChunk)
{
lastChunk = null;
}
}
AbstractChunk nextChunk = parentChunk.next;
if (nextChunk != null)
{
if (removeAfterReading)
{
if (firstChunk == null)
{
firstChunk = nextChunk;
}
if (lastChunk == null)
{
lastChunk = nextChunk;
}
nextChunk.prev = null;
nextChunk.subtractFromTotalCount();
}
return nextChunk.getChunkReader(removeAfterReading);
}
return new AllocatedBufferReader(allocBuffer, removeAfterReading);
}
@Override
public int getWriterUsedCounter()
{
return parentChunk.getWriterUsedCounter();
}
}
final class CharBufferChunkReader extends AbstractChunkReader
{
CharBufferChunk parent;
int pointer;
public CharBufferChunkReader(CharBufferChunk parent,
boolean removeAfterReading)
{
super(parent, removeAfterReading);
this.parent = parent;
this.pointer = parent.offset;
}
@Override
public int read(final char[] ch, final int off, final int len)
throws IOException
{
arrayCopy(parent.buffer, pointer, ch, off, len);
pointer += len;
return len;
}
@Override
public int getReadLenLimit(int askedAmount)
{
return Math.min(parent.lastposition - pointer, askedAmount);
}
}
/**
* StringChunk is a wrapper for java.lang.String.
*
* It also keeps state of the read offset and the number of unread characters.
*
* There's methods that StringChunkGroup uses for reading data.
*
* @author Lari Hotari
*
*/
final class StringChunk extends AbstractChunk
{
String str;
int offset;
int lastposition;
int length;
public StringChunk(String str, int offset, int length)
{
this.str = str;
this.offset = offset;
this.length = length;
this.lastposition = offset + length;
}
@Override
public ChunkReader getChunkReader(boolean removeAfterReading)
{
return new StringChunkReader(this, removeAfterReading);
}
@Override
public void writeTo(Writer target) throws IOException
{
target.write(str, offset, length);
}
@Override
public int size()
{
return length;
}
public boolean isSingleBuffer()
{
return offset == 0 && length == str.length();
}
}
final class StringChunkReader extends AbstractChunkReader
{
StringChunk parent;
int position;
public StringChunkReader(StringChunk parent, boolean removeAfterReading)
{
super(parent, removeAfterReading);
this.parent = parent;
this.position = parent.offset;
}
@Override
public int read(final char[] ch, final int off, final int len)
{
parent.str.getChars(position, (position + len), ch, off);
position += len;
return len;
}
@Override
public int getReadLenLimit(int askedAmount)
{
return Math.min(parent.lastposition - position, askedAmount);
}
}
final class StreamCharBufferSubChunk extends AbstractChunk
{
StreamCharBuffer streamCharBuffer;
int cachedSize;
public StreamCharBufferSubChunk(StreamCharBuffer streamCharBuffer)
{
this.streamCharBuffer = streamCharBuffer;
if (totalCharsInDynamicChunks != -1)
{
cachedSize = streamCharBuffer.size();
totalCharsInDynamicChunks += cachedSize;
}
else
{
cachedSize = -1;
}
}
@Override
public void writeTo(Writer target) throws IOException
{
streamCharBuffer.writeTo(target);
}
@Override
public ChunkReader getChunkReader(boolean removeAfterReading)
{
return new StreamCharBufferSubChunkReader(this, removeAfterReading);
}
@Override
public int size()
{
if (cachedSize == -1)
{
cachedSize = streamCharBuffer.size();
}
return cachedSize;
}
public boolean hasCachedSize()
{
return (cachedSize != -1);
}
public StreamCharBuffer getSubBuffer()
{
return this.streamCharBuffer;
}
public boolean resetSize()
{
if (cachedSize != -1)
{
cachedSize = -1;
return true;
}
return false;
}
@Override
public void subtractFromTotalCount()
{
if (totalCharsInDynamicChunks != -1)
{
totalCharsInDynamicChunks -= size();
}
dynamicChunkMap.remove(streamCharBuffer.bufferKey);
}
}
final class StreamCharBufferSubChunkReader extends AbstractChunkReader
{
StreamCharBufferSubChunk parent;
private StreamCharBufferReader reader;
public StreamCharBufferSubChunkReader(StreamCharBufferSubChunk parent,
boolean removeAfterReading)
{
super(parent, removeAfterReading);
this.parent = parent;
reader = (StreamCharBufferReader) parent.streamCharBuffer
.getReader();
}
@Override
public int getReadLenLimit(int askedAmount)
{
return reader.getReadLenLimit(askedAmount);
}
@Override
public int read(char[] ch, int off, int len) throws IOException
{
return reader.read(ch, off, len);
}
}
final class AllocatedBufferReader extends ChunkReader
{
AllocatedBuffer parent;
int position;
int writerUsedCounter;
boolean removeAfterReading;
public AllocatedBufferReader(AllocatedBuffer parent,
boolean removeAfterReading)
{
this.parent = parent;
this.position = parent.chunkStart;
if (hasReaders)
{
writerUsedCounter = writer.writerUsedCounter;
}
else
{
writerUsedCounter = 1;
}
this.removeAfterReading = removeAfterReading;
}
@Override
public int getReadLenLimit(int askedAmount)
{
return Math.min(parent.used - position, askedAmount);
}
@Override
public int read(char[] ch, int off, int len) throws IOException
{
arrayCopy(parent.buffer, position, ch, off, len);
position += len;
if (removeAfterReading)
{
parent.chunkStart = position;
}
return len;
}
@Override
public ChunkReader next()
{
return null;
}
@Override
public int getWriterUsedCounter()
{
return writerUsedCounter;
}
@Override
public boolean isValid()
{
return (allocBuffer == parent && (lastChunk == null || lastChunk.writerUsedCounter < writerUsedCounter));
}
}
/**
* Simplified version of a CharArrayWriter used internally in readAsCharArray method.
*
* Doesn't do any bound checks since size shouldn't change during writing in readAsCharArray.
*/
private static final class FixedCharArrayWriter extends Writer
{
char buf[];
int count = 0;
public FixedCharArrayWriter(int fixedSize)
{
buf = new char[fixedSize];
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException
{
arrayCopy(cbuf, off, buf, count, len);
count += len;
}
@Override
public void write(char[] cbuf) throws IOException
{
write(cbuf, 0, cbuf.length);
}
@Override
public void write(String str, int off, int len) throws IOException
{
str.getChars(off, off + len, buf, count);
count += len;
}
@Override
public void write(String str) throws IOException
{
write(str, 0, str.length());
}
@Override
public void close() throws IOException
{
// do nothing
}
@Override
public void flush() throws IOException
{
// do nothing
}
public char[] getCharArray()
{
return buf;
}
}
/**
* Interface for a Writer that gets initialized if it is used
* Can be used for passing in to "connectTo" method of StreamCharBuffer
*
* @author Lari Hotari
*
*/
public static interface LazyInitializingWriter
{
public Writer getWriter() throws IOException;
}
/**
* Simple holder class for the connected writer
*
* @author Lari Hotari
*
*/
static final class ConnectedWriter
{
Writer writer;
LazyInitializingWriter lazyInitializingWriter;
final boolean autoFlush;
ConnectedWriter(final Writer writer, final boolean autoFlush)
{
this.writer = writer;
this.autoFlush = autoFlush;
}
ConnectedWriter(final LazyInitializingWriter lazyInitializingWriter,
final boolean autoFlush)
{
this.lazyInitializingWriter = lazyInitializingWriter;
this.autoFlush = autoFlush;
}
Writer getWriter() throws IOException
{
if (writer == null && lazyInitializingWriter != null)
{
writer = lazyInitializingWriter.getWriter();
}
return writer;
}
public void flush() throws IOException
{
if (writer != null && isAutoFlush())
{
writer.flush();
}
}
public boolean isAutoFlush()
{
return autoFlush;
}
}
static final class SingleOutputWriter extends Writer
{
private ConnectedWriter writer;
public SingleOutputWriter(ConnectedWriter writer)
{
this.writer = writer;
}
@Override
public void close() throws IOException
{
// do nothing
}
@Override
public void flush() throws IOException
{
writer.flush();
}
@Override
public void write(final char[] cbuf, final int off, final int len)
throws IOException
{
writer.getWriter().write(cbuf, off, len);
}
@Override
public Writer append(final CharSequence csq, final int start,
final int end) throws IOException
{
writer.getWriter().append(csq, start, end);
return this;
}
@Override
public void write(String str, int off, int len) throws IOException
{
StringCharArrayAccessor.writeStringAsCharArray(writer.getWriter(),
str, off, len);
}
}
/**
* delegates to several writers, used in "connectTo" mode.
*
*/
static final class MultiOutputWriter extends Writer
{
final List writers;
public MultiOutputWriter(final List writers)
{
this.writers = writers;
}
@Override
public void close() throws IOException
{
// do nothing
}
@Override
public void flush() throws IOException
{
for (ConnectedWriter writer : writers)
{
writer.flush();
}
}
@Override
public void write(final char[] cbuf, final int off, final int len)
throws IOException
{
for (ConnectedWriter writer : writers)
{
writer.getWriter().write(cbuf, off, len);
}
}
@Override
public Writer append(final CharSequence csq, final int start,
final int end) throws IOException
{
for (ConnectedWriter writer : writers)
{
writer.getWriter().append(csq, start, end);
}
return this;
}
@Override
public void write(String str, int off, int len) throws IOException
{
for (ConnectedWriter writer : writers)
{
StringCharArrayAccessor.writeStringAsCharArray(
writer.getWriter(), str, off, len);
}
}
}
/* Compatibility methods so that StreamCharBuffer will behave more like java.lang.String in groovy code */
public char charAt(int index)
{
return toString().charAt(index);
}
public int length()
{
return size();
}
public CharSequence subSequence(int start, int end)
{
return toString().subSequence(start, end);
}
public boolean asBoolean()
{
return isNotEmpty();
}
/* methods for notifying child (sub) StreamCharBuffer changes to the parent StreamCharBuffer */
void addParentBuffer(StreamCharBuffer parent)
{
if (parentBuffers == null)
{
parentBuffers = new HashSet>();
}
parentBuffers.add(new SoftReference(
parent.bufferKey));
}
boolean bufferChanged(StreamCharBuffer buffer)
{
StreamCharBufferSubChunk subChunk = dynamicChunkMap
.get(buffer.bufferKey);
if (subChunk == null)
{
// buffer isn't a subchunk in this buffer any more
return false;
}
// reset cached size;
if (subChunk.resetSize())
{
totalCharsInDynamicChunks = -1;
sizeAtLeast = -1;
// notify parents too
notifyBufferChange();
}
return true;
}
void notifyBufferChange()
{
if (parentBuffers == null)
{
return;
}
for (Iterator> i = parentBuffers
.iterator(); i.hasNext();)
{
SoftReference ref = i.next();
final StreamCharBuffer.StreamCharBufferKey parentKey = ref.get();
boolean removeIt = true;
if (parentKey != null)
{
StreamCharBuffer parent = parentKey.getBuffer();
removeIt = !parent.bufferChanged(this);
}
if (removeIt)
{
i.remove();
}
}
}
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException
{
String str = in.readUTF();
reset();
if (str.length() > 0)
{
addChunk(new StringChunk(str, 0, str.length()));
}
}
public void writeExternal(ObjectOutput out) throws IOException
{
String str = toString();
out.writeUTF(str);
}
}