All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.myfaces.shared_tomahawk.util.StreamCharBuffer Maven / Gradle / Ivy

There is a newer version: 4.2.9
Show newest version
/*
 * 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 nameDescriptionDefault value
streamcharbuffer.chunksizedefault chunk size - the size the first allocated buffer512
streamcharbuffer.maxchunksizemaximum chunk size - the maximum size of the allocated buffer1048576
streamcharbuffer.growprocentgrowing buffer percentage - the newly allocated buffer is defined by * total_size * (growpercent/100)100
streamcharbuffer.subbufferchunkminsizeminimum size of child StreamCharBuffer chunk - if the size is smaller, * the content is copied to the parent buffer512
streamcharbuffer.substringchunkminsizeminimum size of String chunks - if the size is smaller, the content is * copied to the buffer512
streamcharbuffer.chunkminsizeminimum 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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy