Java tutorial
/* * Copyright 2009 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.codehaus.groovy.grails.web.util; import groovy.lang.GroovyObjectSupport; import groovy.lang.Writable; import java.io.EOFException; import java.io.Externalizable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectInput; import java.io.ObjectOutput; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.groovy.grails.commons.DefaultGrailsCodecClass; import org.codehaus.groovy.grails.support.encoding.AbstractEncodedAppender; import org.codehaus.groovy.grails.support.encoding.CharArrayAccessible; import org.codehaus.groovy.grails.support.encoding.CodecIdentifier; import org.codehaus.groovy.grails.support.encoding.DefaultCodecIdentifier; import org.codehaus.groovy.grails.support.encoding.Encodeable; import org.codehaus.groovy.grails.support.encoding.EncodedAppender; import org.codehaus.groovy.grails.support.encoding.EncodedAppenderFactory; import org.codehaus.groovy.grails.support.encoding.EncodedAppenderWriter; import org.codehaus.groovy.grails.support.encoding.EncodedAppenderWriterFactory; import org.codehaus.groovy.grails.support.encoding.Encoder; import org.codehaus.groovy.grails.support.encoding.EncoderAware; import org.codehaus.groovy.grails.support.encoding.EncodingState; import org.codehaus.groovy.grails.support.encoding.EncodingStateImpl; import org.codehaus.groovy.grails.support.encoding.EncodingStateRegistry; import org.codehaus.groovy.grails.support.encoding.EncodingStateRegistryLookup; import org.codehaus.groovy.grails.support.encoding.StreamEncodeable; /** * <p> * StreamCharBuffer is a multipurpose in-memory buffer that can replace JDK * in-memory buffers (StringBuffer, StringBuilder, StringWriter). * </p> * * <p> * Grails GSP rendering uses this class as a buffer that is optimized for performance. * </p> * * <p> * 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. * </p> * * <p> * 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:<br> * <code> * out << body() * </code><br> * other example:<br> * <code> * out << g.render(template: '/some/template', model:[somebean: somebean]) * </code><br> * There's no extra java.lang.String generation overhead. * * </p> * * <p> * There's a java.io.Writer interface for appending character data to the buffer * and a java.io.Reader interface for reading data. * </p> * * <p> * Each {@link #getReader()} call will create a new reader instance that keeps * it own state.<br> * 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. * </p> * * <p> * There's also several other options for reading data:<br> * {@link #readAsCharArray()} reads the buffer to a char[] array<br> * {@link #readAsString()} reads the buffer and wraps the char[] data as a * String<br> * {@link #writeTo(Writer)} writes the buffer to a java.io.Writer<br> * {@link #toCharArray()} returns the buffer as a char[] array, caches the * return value internally so that this method can be called several times.<br> * {@link #toString()} returns the buffer as a String, caches the return value * internally<br> * </p> * * <p> * 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). * </p> * * <p> * <b>This class is not thread-safe.</b> 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. * </p> * * <p> * Main operation principle:<br> * </p> * <p> * StreamCharBuffer keeps the buffer in a linked link of "chunks".<br> * 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).<br> * 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.<br> * In StreamCharBuffer, there are several ways to prevent unnecessary allocation * & copy operations. * </p> * <p> * There can be several different type of chunks: char arrays ( * {@code CharBufferChunk}), String chunks ({@code StringChunk}) and other * StreamCharBuffers as sub chunks ({@code StreamCharBufferSubChunk}). * </p> * <p> * 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). * </p> * <p> * 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. * </p> * <p> * 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[]. * </p> * <p> * There can be several targets "listening" the buffer in "connectTo" mode. The * same content will be written to all targets. * <p> * <p> * Growable chunksize: By default, a newly allocated chunk's size will grow * based on the total size of all written chunks.<br> * 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.<br> * Growable chunksize can be turned off by setting the growProcent to 0.<br> * There's a default maximum chunksize of 1MB by default. The minimum size is * the initial chunksize size.<br> * </p> * * <p> * System properties to change default configuration parameters:<br> * <table> * <tr> * <th>System Property name</th> * <th>Description</th> * <th>Default value</th> * </tr> * <tr> * <td>streamcharbuffer.chunksize</td> * <td>default chunk size - the size the first allocated buffer</td> * <td>512</td> * </tr> * <tr> * <td>streamcharbuffer.maxchunksize</td> * <td>maximum chunk size - the maximum size of the allocated buffer</td> * <td>1048576</td> * </tr> * <tr> * <td>streamcharbuffer.growprocent</td> * <td>growing buffer percentage - the newly allocated buffer is defined by * total_size * (growpercent/100)</td> * <td>100</td> * </tr> * <tr> * <td>streamcharbuffer.subbufferchunkminsize</td> * <td>minimum size of child StreamCharBuffer chunk - if the size is smaller, * the content is copied to the parent buffer</td> * <td>512</td> * </tr> * <tr> * <td>streamcharbuffer.substringchunkminsize</td> * <td>minimum size of String chunks - if the size is smaller, the content is * copied to the buffer</td> * <td>512</td> * </tr> * <tr> * <td>streamcharbuffer.chunkminsize</td> * <td>minimum size of chunk that gets written directly to the target in * connected mode.</td> * <td>256</td> * </tr> * </table> * * Configuration values can also be changed for each instance of * StreamCharBuffer individually. Default values are defined with System * Properties. * * </p> * * @author Lari Hotari, Sagire Software Oy */ public class StreamCharBuffer extends GroovyObjectSupport implements Writable, CharSequence, Externalizable, Encodeable, StreamEncodeable, EncodedAppenderWriterFactory, Cloneable { private static final int EXTERNALIZABLE_VERSION = 2; static final long serialVersionUID = EXTERNALIZABLE_VERSION; private static final Log log = LogFactory.getLog(StreamCharBuffer.class); private static final int DEFAULT_CHUNK_SIZE = Integer.getInteger("streamcharbuffer.chunksize", 512); private static final int DEFAULT_MAX_CHUNK_SIZE = Integer.getInteger("streamcharbuffer.maxchunksize", 1024 * 1024); private static final int DEFAULT_CHUNK_SIZE_GROW_PROCENT = Integer.getInteger("streamcharbuffer.growprocent", 100); private static final int SUB_BUFFERCHUNK_MIN_SIZE = Integer.getInteger("streamcharbuffer.subbufferchunkminsize", 512); private static final int SUB_STRINGCHUNK_MIN_SIZE = Integer.getInteger("streamcharbuffer.substringchunkminsize", 512); private static final int WRITE_DIRECT_MIN_SIZE = Integer.getInteger("streamcharbuffer.writedirectminsize", 1024); private static final int CHUNK_MIN_SIZE = Integer.getInteger("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<ConnectToWriter> connectToWriters; private ConnectedWritersWriter connectedWritersWriter; private Boolean notConnectedToEncodeAwareWriters = null; 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<StreamCharBufferKey, StreamCharBufferSubChunk> dynamicChunkMap; private Set<SoftReference<StreamCharBufferKey>> parentBuffers; int allocatedBufferIdSequence = 0; int readerCount = 0; boolean hasReaders = false; boolean notifyParentBuffersEnabled = true; 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 prefer) { preferSubChunkWhenWritingToOtherBuffer = prefer; } 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<StreamCharBufferKey, StreamCharBufferSubChunk>(); } /** * 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(); connectToWriters.add(new ConnectToWriter(w, autoFlush)); initConnectedWritersWriter(); } public final void encodeInStreamingModeTo(final EncoderAware encoderLookup, final EncodingStateRegistryLookup encodingStateRegistryLookup, boolean autoFlush, final Writer w) { encodeInStreamingModeTo(encoderLookup, encodingStateRegistryLookup, autoFlush, new LazyInitializingWriter() { public Writer getWriter() throws IOException { return w; } }); } public final void encodeInStreamingModeTo(final EncoderAware encoderLookup, final EncodingStateRegistryLookup encodingStateRegistryLookup, final boolean autoFlush, final LazyInitializingWriter... writers) { LazyInitializingWriter encodingWriterInitializer = createEncodingInitializer(encoderLookup, encodingStateRegistryLookup, writers); connectTo(encodingWriterInitializer, autoFlush); } public LazyInitializingWriter createEncodingInitializer(final EncoderAware encoderLookup, final EncodingStateRegistryLookup encodingStateRegistryLookup, final LazyInitializingWriter... writers) { LazyInitializingWriter encodingWriterInitializer = new LazyInitializingMultipleWriter() { Writer lazyWriter; public Writer getWriter() throws IOException { return lazyWriter; } public LazyInitializingWriter[] initializeMultiple(StreamCharBuffer buffer, boolean autoFlushMode) throws IOException { Encoder encoder = encoderLookup.getEncoder(); if (encoder != null) { EncodingStateRegistry encodingStateRegistry = encodingStateRegistryLookup.lookup(); StreamCharBuffer encodeBuffer = new StreamCharBuffer(chunkSize, growProcent, maxChunkSize); lazyWriter = encodeBuffer.getWriterForEncoder(encoder, encodingStateRegistry); for (LazyInitializingWriter w : writers) { encodeBuffer.connectTo(w, autoFlushMode); } return new LazyInitializingWriter[] { this }; } else { return writers; } } }; return encodingWriterInitializer; } private void initConnectedWritersWriter() { notConnectedToEncodeAwareWriters = null; connectedWritersWriter = null; setNotifyParentBuffersEnabled(false); } private void startUsingConnectedWritersWriter() throws IOException { if (connectedWritersWriter == null) { List<ConnectedWriter> connectedWriters = new ArrayList<ConnectedWriter>(); for (ConnectToWriter connectToWriter : connectToWriters) { for (Writer writer : connectToWriter.getWriters()) { Writer target = writer; if (target instanceof GrailsWrappedWriter) { target = ((GrailsWrappedWriter) target).unwrap(); } if (target == null) { throw new NullPointerException("target is null"); } connectedWriters.add(new ConnectedWriter(target, connectToWriter.isAutoFlush())); } } 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(); connectToWriters.add(new ConnectToWriter(w, autoFlush)); initConnectedWritersWriter(); } public final void removeConnections() { if (connectToWriters != null) { connectToWriters = null; connectedWritersWriter = null; notConnectedToEncodeAwareWriters = null; } } private void initConnected() { if (connectToWriters == null) { connectToWriters = new ArrayList<ConnectToWriter>(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 size */ public void setSubStringChunkMinSize(int size) { subStringChunkMinSize = size; } public int getSubBufferChunkMinSize() { return subBufferChunkMinSize; } public void setSubBufferChunkMinSize(int size) { subBufferChunkMinSize = size; } public int getWriteDirectlyToConnectedMinSize() { return writeDirectlyToConnectedMinSize; } /** * Minimum size for a String or char[] to get written directly to connected writer (in "connectTo" mode). * * @param size */ public void setWriteDirectlyToConnectedMinSize(int size) { writeDirectlyToConnectedMinSize = size; } public int getChunkMinSize() { return chunkMinSize; } public void setChunkMinSize(int size) { chunkMinSize = size; } /** * 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 target; } /** * 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 */ @SuppressWarnings("resource") public void writeTo(Writer target, boolean flushTarget, boolean emptyAfter) throws IOException { if (target instanceof GrailsWrappedWriter) { GrailsWrappedWriter wrappedWriter = ((GrailsWrappedWriter) target); if (wrappedWriter.isAllowUnwrappingOut()) { target = wrappedWriter.unwrap(); } } if (target == writer) { throw new IllegalArgumentException("Cannot write buffer to itself."); } if (!emptyAfter && target instanceof StreamCharBufferWriter) { ((StreamCharBufferWriter) target).write(this); return; } else if (target instanceof EncodedAppenderFactory) { EncodedAppenderFactory eaw = (EncodedAppenderFactory) target; EncodedAppender appender = eaw.getEncodedAppender(); if (appender != null) { if (appender == writer.getEncodedAppender()) { throw new IllegalArgumentException("Cannot write buffer to itself."); } Encoder encoder = null; if (target instanceof EncoderAware) { encoder = ((EncoderAware) target).getEncoder(); } if (encoder == null && appender instanceof EncoderAware) { encoder = ((EncoderAware) appender).getEncoder(); } encodeTo(appender, encoder); if (emptyAfter) { emptyAfterReading(); } if (flushTarget) { target.flush(); } 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; } allocBuffer.writeTo(target); if (emptyAfter) { emptyAfterReading(); } if (flushTarget) { target.flush(); } } protected void emptyAfterReading() { firstChunk = null; lastChunk = null; totalCharsInList = 0; totalCharsInDynamicChunks = -1; sizeAtLeast = -1; dynamicChunkMap.clear(); allocBuffer.reuseBuffer(null); } /** * Reads the buffer to a char[]. * * @return the chars * @deprecated use toCharArray() directly */ @Deprecated public char[] readAsCharArray() { return toCharArray(); } /** * Reads the buffer to a String. * * @return the String * @deprecated Use toString() directly */ @Deprecated public String readAsString() { return toString(); } /** * {@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() { StringChunk stringChunk = readToSingleStringChunk(true); if (stringChunk != null) { return stringChunk.str; } else { return ""; } } public StringChunk readToSingleStringChunk(boolean registerEncodingState) { if (firstChunk == lastChunk && firstChunk instanceof StringChunk && allocBuffer.charsUsed() == 0 && ((StringChunk) firstChunk).isSingleBuffer()) { StringChunk chunk = ((StringChunk) firstChunk); if (registerEncodingState) { markEncoded(chunk); } return chunk; } int initialReaderCount = readerCount; MultipartCharBufferChunk chunk = readToSingleChunk(); MultipartStringChunk stringChunk = (chunk != null) ? chunk.asStringChunk() : null; if (initialReaderCount == 0) { // if there are no readers, the result can be cached reset(); if (stringChunk != null) { addChunk(stringChunk); } } if (registerEncodingState) { markEncoded(stringChunk); } return stringChunk; } public void markEncoded(StringChunk strChunk) { if (strChunk instanceof MultipartStringChunk) { MultipartStringChunk stringChunk = (MultipartStringChunk) strChunk; if (stringChunk.isSingleEncoding()) { EncodingState encodingState = stringChunk.firstPart.encodingState; if (encodingState != null && encodingState.getEncoders() != null && encodingState.getEncoders().size() > 0) { Encoder encoder = encodingState.getEncoders().iterator().next(); if (encoder != null) encoder.markEncoded(stringChunk.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() + 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; MultipartCharBufferChunk chunk = readToSingleChunk(); if (initialReaderCount == 0) { // if there are no readers, the result can be cached reset(); if (chunk != null) { addChunk(chunk); } } return chunk.buffer; } public static final class EncodedPart { private final EncodingState encodingState; private final String part; public EncodedPart(EncodingState encodingState, String part) { this.encodingState = encodingState; this.part = part; } public EncodingState getEncodingState() { return encodingState; } public String getPart() { return part; } @Override public String toString() { return "EncodedPart [encodingState='" + encodingState + "', part='" + part + "']"; } } public List<EncodedPart> dumpEncodedParts() { List<EncodedPart> encodedParts = new ArrayList<StreamCharBuffer.EncodedPart>(); MultipartStringChunk mpStringChunk = readToSingleChunk().asStringChunk(); if (mpStringChunk.firstPart != null) { EncodingStatePart current = mpStringChunk.firstPart; int offset = 0; char[] buf = StringCharArrayAccessor.getValue(mpStringChunk.str); while (current != null) { encodedParts.add(new EncodedPart(current.encodingState, new String(buf, offset, current.len))); offset += current.len; current = current.next; } } return encodedParts; } private MultipartCharBufferChunk readToSingleChunk() { int currentSize = size(); if (currentSize == 0) { return null; } FixedCharArrayEncodedAppender appender = new FixedCharArrayEncodedAppender(currentSize); try { encodeTo(appender, null); } catch (IOException e) { throw new RuntimeException("Unexpected IOException", e); } appender.finish(); return appender.chunk; } 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(EncodingState encodingState) throws IOException { int spaceLeft = allocBuffer.spaceLeft(encodingState); if (spaceLeft == 0) { spaceLeft = appendCharBufferChunk(encodingState, true, true); } return spaceLeft; } private int appendCharBufferChunk(EncodingState encodingState, boolean flushInConnected, boolean allocate) throws IOException { int spaceLeft = 0; if (flushInConnected && isConnectedMode()) { flushToConnected(false); if (!isChunkSizeResizeable()) { allocBuffer.reuseBuffer(encodingState); } } else { if (allocBuffer.hasChunk()) { addChunk(allocBuffer.createChunk()); } } spaceLeft = allocBuffer.spaceLeft(encodingState); if (allocate && spaceLeft == 0) { totalChunkSize += allocBuffer.chunkSize(); resizeChunkSizeAsProcentageOfTotalSize(); allocBuffer = new AllocatedBuffer(chunkSize); spaceLeft = allocBuffer.spaceLeft(encodingState); } return spaceLeft; } void appendStringChunk(EncodingState encodingState, String str, int off, int len) throws IOException { appendCharBufferChunk(encodingState, false, false); addChunk(new StringChunk(str, off, len)).setEncodingState(encodingState); } public void appendStreamCharBufferChunk(StreamCharBuffer subBuffer) throws IOException { appendCharBufferChunk(null, false, false); addChunk(new StreamCharBufferSubChunk(subBuffer)); } AbstractChunk 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(); } return newChunk; } public boolean isConnectedMode() { return connectToWriters != null && !connectToWriters.isEmpty(); } private void flushToConnected(boolean forceFlush) throws IOException { startUsingConnectedWritersWriter(); if (notConnectedToEncodeAwareWriters == null) { notConnectedToEncodeAwareWriters = !connectedWritersWriter.isEncoderAware(); } writeTo(connectedWritersWriter, true, true); if (forceFlush) { connectedWritersWriter.forceFlush(); } } protected boolean isChunkSizeResizeable() { return (growProcent > 0 && chunkSize < maxChunkSize); } 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 implements EncodedAppenderFactory, EncodedAppenderWriterFactory { boolean closed = false; int writerUsedCounter = 0; boolean increaseCounter = true; EncodedAppender encodedAppender; @Override public final void write(final char[] b, final int off, final int len) throws IOException { write(null, b, off, len); } private final void write(EncodingState encodingState, 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(encodingState, true, true); startUsingConnectedWritersWriter(); connectedWritersWriter.write(b, off, len); } else { int charsLeft = len; int currentOffset = off; while (charsLeft > 0) { int spaceLeft = allocateSpace(encodingState); 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(null); } @Override public final void write(final String str) throws IOException { write(null, str, 0, str.length()); } @Override public final void write(final String str, final int off, final int len) throws IOException { write(null, str, off, len); } private final void write(EncodingState encodingState, final String str, final int off, final int len) throws IOException { if (len == 0) return; markUsed(); if (shouldWriteDirectly(len)) { appendCharBufferChunk(encodingState, true, false); startUsingConnectedWritersWriter(); connectedWritersWriter.write(str, off, len); } else if (len >= subStringChunkMinSize && isNextChunkBigEnough(len)) { appendStringChunk(encodingState, str, off, len); } else { int charsLeft = len; int currentOffset = off; while (charsLeft > 0) { int spaceLeft = allocateSpace(encodingState); 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 == 0 || (directChunkMinSize != -1 && subBuffer.isSizeLarger(directChunkMinSize))) { appendCharBufferChunk(null, true, false); startUsingConnectedWritersWriter(); 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.encodeTo(this.getEncodedAppender(), null); } } @Override public final Writer append(final CharSequence csq, final int start, final int end) throws IOException { markUsed(); if (csq == null) { write("null"); } else { appendCharSequence(null, csq, start, end); } return this; } protected void appendCharSequence(final EncodingState encodingState, final CharSequence csq, final int start, final int end) throws IOException { if (csq instanceof String || csq instanceof StringBuffer || csq instanceof StringBuilder || csq instanceof CharArrayAccessible) { int len = end - start; int charsLeft = len; int currentOffset = start; while (charsLeft > 0) { int spaceLeft = allocateSpace(encodingState); 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); } else if (csq instanceof CharArrayAccessible) { allocBuffer.writeCharArrayAccessible((CharArrayAccessible) csq, currentOffset, writeChars); } charsLeft -= writeChars; currentOffset += writeChars; } } else { String str = csq.subSequence(start, end).toString(); write(encodingState, str, 0, str.length()); } } @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; flushWriter(true); } 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(null); allocBuffer.write((char) b); } void flushWriter(boolean forceFlush) throws IOException { if (isConnectedMode()) { flushToConnected(forceFlush); } notifyBufferChange(); } public final StreamCharBuffer getBuffer() { return StreamCharBuffer.this; } public void append(Encoder encoder, char character) throws IOException { markUsed(); allocateSpace(isNotConnectedToEncoderAwareWriters() ? EncodingStateImpl.UNDEFINED_ENCODING_STATE : new EncodingStateImpl(Collections.singleton(encoder))); allocBuffer.write(character); } public Writer getWriterForEncoder(Encoder encoder, EncodingStateRegistry encodingStateRegistry) { return StreamCharBuffer.this.getWriterForEncoder(encoder, encodingStateRegistry); } public EncodedAppender getEncodedAppender() { if (encodedAppender == null) { encodedAppender = new StreamCharBufferEncodedAppender(this); } return encodedAppender; } @Override public void flush() throws IOException { flushWriter(false); } } private boolean isNotConnectedToEncoderAwareWriters() { return notConnectedToEncodeAwareWriters != null && notConnectedToEncodeAwareWriters; } private final static class StreamCharBufferEncodedAppender extends AbstractEncodedAppender { StreamCharBufferWriter writer; StreamCharBufferEncodedAppender(StreamCharBufferWriter writer) { this.writer = writer; } public StreamCharBufferWriter getWriter() { return writer; } @Override public void flush() throws IOException { writer.flush(); } @Override protected void write(EncodingState encodingState, char[] b, int off, int len) throws IOException { writer.write(encodingState, b, off, len); } @Override protected void write(EncodingState encodingState, String str, int off, int len) throws IOException { writer.write(encodingState, str, off, len); } @Override protected void appendCharSequence(EncodingState encodingState, CharSequence str, int start, int end) throws IOException { writer.appendCharSequence(encodingState, str, start, end); } @Override public void append(Encoder encoder, char character) throws IOException { writer.append(encoder, character); } public void close() throws IOException { writer.close(); } } /** * 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 remove) { removeAfterReading = remove; } 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 implements StreamEncodeable { AbstractChunk next; AbstractChunk prev; int writerUsedCounter; EncodingState encodingState; 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(); } public abstract void encodeTo(EncodedAppender appender, Encoder encoder) throws IOException; public EncodingState getEncodingState() { return encodingState; } public void setEncodingState(EncodingState encodingState) { this.encodingState = encodingState; } } // 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; private EncodingState encodingState; private EncodingState nextEncoders; 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(EncodingState encodingState) { used = 0; chunkStart = 0; this.encodingState = null; this.nextEncoders = encodingState; } public int chunkSize() { return buffer.length; } public int spaceLeft(EncodingState encodingState) { if (this.encodingState != null && (encodingState == null || !this.encodingState.equals(encodingState)) && hasChunk() && !isNotConnectedToEncoderAwareWriters()) { addChunk(allocBuffer.createChunk()); this.encodingState = null; } this.nextEncoders = encodingState; return size - used; } private final void applyEncoders() throws IOException { if (encodingState == nextEncoders) { return; } if (encodingState != null && !isNotConnectedToEncoderAwareWriters() && (nextEncoders == null || !encodingState.equals(nextEncoders))) { throw new IOException("Illegal operation in AllocatedBuffer"); } encodingState = nextEncoders; } public boolean write(final char ch) throws IOException { if (used < size) { applyEncoders(); buffer[used++] = ch; return true; } return false; } public final void write(final char[] ch, final int off, final int len) throws IOException { applyEncoders(); arrayCopy(ch, off, buffer, used, len); used += len; } public final void writeString(final String str, final int off, final int len) throws IOException { applyEncoders(); str.getChars(off, off + len, buffer, used); used += len; } public final void writeStringBuilder(final StringBuilder stringBuilder, final int off, final int len) throws IOException { applyEncoders(); stringBuilder.getChars(off, off + len, buffer, used); used += len; } public final void writeStringBuffer(final StringBuffer stringBuffer, final int off, final int len) throws IOException { applyEncoders(); stringBuffer.getChars(off, off + len, buffer, used); used += len; } public final void writeCharArrayAccessible(final CharArrayAccessible charArrayAccessible, final int off, final int len) throws IOException { applyEncoders(); charArrayAccessible.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); chunk.setEncodingState(encodingState); chunkStart = used; return chunk; } public boolean hasChunk() { return (used > chunkStart); } public void encodeTo(EncodedAppender appender, Encoder encoder) throws IOException { if (used - chunkStart > 0) { appender.append(encoder, encodingState, buffer, chunkStart, used - chunkStart); } } public EncodingState getEncodingState() { return encodingState; } } /** * 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 * */ 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; } @Override public void encodeTo(EncodedAppender appender, Encoder encoder) throws IOException { appender.append(encoder, getEncodingState(), buffer, offset, length); } } class MultipartStringChunk extends StringChunk { EncodingStatePart firstPart = null; EncodingStatePart lastPart = null; public MultipartStringChunk(String str) { super(str, 0, str.length()); } @Override public void encodeTo(EncodedAppender appender, Encoder encoder) throws IOException { if (firstPart != null) { EncodingStatePart current = firstPart; int offset = 0; char[] buf = StringCharArrayAccessor.getValue(str); while (current != null) { appender.append(encoder, current.encodingState, buf, offset, current.len); offset += current.len; current = current.next; } } else { super.encodeTo(appender, encoder); } } public boolean isSingleEncoding() { return (firstPart == lastPart); } public int partCount() { int partCount = 0; EncodingStatePart current = firstPart; while (current != null) { partCount++; current = current.next; } return partCount; } public void appendEncodingStatePart(EncodingStatePart current) { if (firstPart == null) { firstPart = current; lastPart = current; } else { lastPart.next = current; lastPart = current; } } } class MultipartCharBufferChunk extends CharBufferChunk { EncodingStatePart firstPart = null; EncodingStatePart lastPart = null; public MultipartCharBufferChunk(char[] buffer) { super(-1, buffer, 0, buffer.length); } @Override public void encodeTo(EncodedAppender appender, Encoder encoder) throws IOException { if (firstPart != null) { EncodingStatePart current = firstPart; int offset = 0; while (current != null) { appender.append(encoder, current.encodingState, buffer, offset, current.len); offset += current.len; current = current.next; } } else { super.encodeTo(appender, encoder); } } public MultipartStringChunk asStringChunk() { String str = StringCharArrayAccessor.createString(buffer); MultipartStringChunk chunk = new MultipartStringChunk(str); chunk.firstPart = firstPart; chunk.lastPart = lastPart; return chunk; } } static final class EncodingStatePart { EncodingStatePart next; EncodingState encodingState; int len = -1; } 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; 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 * */ 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(); } @Override public void encodeTo(EncodedAppender appender, Encoder encoder) throws IOException { appender.append(encoder, getEncodingState(), str, offset, 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 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); } @Override public void encodeTo(EncodedAppender appender, Encoder encoder) throws IOException { appender.append(encoder, getSubBuffer()); } } 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; 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)); } } private final class FixedCharArrayEncodedAppender extends AbstractEncodedAppender { char buf[]; int count = 0; int currentStart = 0; EncodingState currentState; MultipartCharBufferChunk chunk; public FixedCharArrayEncodedAppender(int fixedSize) { buf = new char[fixedSize]; chunk = new MultipartCharBufferChunk(buf); } private void checkEncodingChange(EncodingState encodingState) { if (currentState != null && (encodingState == null || !currentState.equals(encodingState))) { addPart(); } if (currentState == null) { currentState = encodingState; } } public void finish() { addPart(); } private void addPart() { if (count - currentStart > 0) { EncodingStatePart newPart = new EncodingStatePart(); newPart.encodingState = currentState; newPart.len = count - currentStart; if (chunk.lastPart == null) { chunk.firstPart = newPart; chunk.lastPart = newPart; } else { chunk.lastPart.next = newPart; chunk.lastPart = newPart; } currentState = null; currentStart = count; } } @Override protected void write(EncodingState encodingState, char[] b, int off, int len) throws IOException { checkEncodingChange(encodingState); arrayCopy(b, off, buf, count, len); count += len; } @Override protected void write(EncodingState encodingState, String str, int off, int len) throws IOException { checkEncodingChange(encodingState); str.getChars(off, off + len, buf, count); count += len; } @Override protected void appendCharSequence(EncodingState encodingState, CharSequence csq, int start, int end) throws IOException { checkEncodingChange(encodingState); if (csq instanceof String) { write(encodingState, (String) csq, start, end - start); } else if (csq instanceof StringBuffer) { ((StringBuffer) csq).getChars(start, end, buf, count); count += end - start; } else if (csq instanceof StringBuilder) { ((StringBuilder) csq).getChars(start, end, buf, count); count += end - start; } else { String str = csq.subSequence(start, end).toString(); write(encodingState, str, 0, str.length()); } } @Override public void append(Encoder encoder, char character) throws IOException { EncodingState encodingState = new EncodingStateImpl( encoder != null ? Collections.singleton(encoder) : null); checkEncodingChange(encodingState); buf[count++] = character; } public void close() throws IOException { finish(); } } /** * 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; } public static interface LazyInitializingMultipleWriter extends LazyInitializingWriter { /** * initialize underlying writer * * @return false if this writer entry should be removed after calling this callback method */ public LazyInitializingWriter[] initializeMultiple(StreamCharBuffer buffer, boolean autoFlush) throws IOException; } final class ConnectToWriter { final Writer writer; final LazyInitializingWriter lazyInitializingWriter; final boolean autoFlush; Boolean encoderAware; ConnectToWriter(final Writer writer, final boolean autoFlush) { this.writer = writer; this.lazyInitializingWriter = null; this.autoFlush = autoFlush; } ConnectToWriter(final LazyInitializingWriter lazyInitializingWriter, final boolean autoFlush) { this.lazyInitializingWriter = lazyInitializingWriter; this.writer = null; this.autoFlush = autoFlush; } Writer[] getWriters() throws IOException { if (writer != null) { return new Writer[] { writer }; } else { Set<Writer> writerList = resolveLazyInitializers(new HashSet<Integer>(), lazyInitializingWriter); return writerList.toArray(new Writer[writerList.size()]); } } private Set<Writer> resolveLazyInitializers(Set<Integer> resolved, LazyInitializingWriter lazyInitializingWriter) throws IOException { Set<Writer> writerList = Collections.emptySet(); Integer identityHashCode = System.identityHashCode(lazyInitializingWriter); if (!resolved.contains(identityHashCode) && lazyInitializingWriter instanceof LazyInitializingMultipleWriter) { resolved.add(identityHashCode); writerList = new LinkedHashSet<Writer>(); LazyInitializingWriter[] writers = ((LazyInitializingMultipleWriter) lazyInitializingWriter) .initializeMultiple(StreamCharBuffer.this, autoFlush); for (LazyInitializingWriter writer : writers) { writerList.addAll(resolveLazyInitializers(resolved, writer)); } } else { writerList = Collections.singleton(lazyInitializingWriter.getWriter()); } return writerList; } public boolean isAutoFlush() { return autoFlush; } } /** * Simple holder class for the connected writer * * @author Lari Hotari * */ static final class ConnectedWriter { final Writer writer; final boolean autoFlush; final boolean encoderAware; ConnectedWriter(final Writer writer, final boolean autoFlush) { this.writer = writer; this.autoFlush = autoFlush; this.encoderAware = (writer instanceof EncodedAppenderFactory || writer instanceof EncodedAppenderWriterFactory); } Writer getWriter() { return writer; } public void flush() throws IOException { if (autoFlush) { writer.flush(); } } public boolean isEncoderAware() { return encoderAware; } } static final class SingleOutputWriter extends ConnectedWritersWriter implements GrailsWrappedWriter { private final ConnectedWriter connectedWriter; private final Writer writer; private final boolean encoderAware; public SingleOutputWriter(ConnectedWriter connectedWriter) { this.connectedWriter = connectedWriter; this.writer = connectedWriter.getWriter(); this.encoderAware = connectedWriter.isEncoderAware(); } @Override public void close() throws IOException { // do nothing } @Override public void flush() throws IOException { connectedWriter.flush(); } @Override public void write(final char[] cbuf, final int off, final int len) throws IOException { writer.write(cbuf, off, len); } @Override public Writer append(final CharSequence csq, final int start, final int end) throws IOException { writer.append(csq, start, end); return this; } @Override public void write(String str, int off, int len) throws IOException { if (!encoderAware) { StringCharArrayAccessor.writeStringAsCharArray(writer, str, off, len); } else { writer.write(str, off, len); } } @Override public boolean isEncoderAware() throws IOException { return encoderAware; } public boolean isAllowUnwrappingOut() { return true; } public Writer unwrap() { return writer; } public void markUsed() { } @Override public void forceFlush() throws IOException { writer.flush(); } } static abstract class ConnectedWritersWriter extends Writer { public abstract boolean isEncoderAware() throws IOException; public abstract void forceFlush() throws IOException; } /** * delegates to several writers, used in "connectTo" mode. */ static final class MultiOutputWriter extends ConnectedWritersWriter { final List<ConnectedWriter> connectedWriters; final List<Writer> writers; public MultiOutputWriter(final List<ConnectedWriter> connectedWriters) { this.connectedWriters = connectedWriters; this.writers = new ArrayList<Writer>(connectedWriters.size()); for (ConnectedWriter connectedWriter : connectedWriters) { writers.add(connectedWriter.getWriter()); } } @Override public void close() throws IOException { // do nothing } @Override public void flush() throws IOException { for (ConnectedWriter connectedWriter : connectedWriters) { connectedWriter.flush(); } } @Override public void write(final char[] cbuf, final int off, final int len) throws IOException { for (Writer writer : writers) { writer.write(cbuf, off, len); } } @Override public Writer append(final CharSequence csq, final int start, final int end) throws IOException { for (Writer writer : writers) { writer.append(csq, start, end); } return this; } @Override public void write(String str, int off, int len) throws IOException { if (isEncoderAware()) { for (ConnectedWriter connectedWriter : connectedWriters) { if (!connectedWriter.isEncoderAware()) { StringCharArrayAccessor.writeStringAsCharArray(connectedWriter.getWriter(), str, off, len); } else { connectedWriter.getWriter().write(str, off, len); } } } else { for (Writer writer : writers) { writer.write(str, off, len); } } } Boolean encoderAware; @Override public boolean isEncoderAware() throws IOException { if (encoderAware == null) { encoderAware = false; for (ConnectedWriter writer : connectedWriters) { if (writer.isEncoderAware()) { encoderAware = true; break; } } } return encoderAware; } @Override public void forceFlush() throws IOException { for (Writer writer : writers) { writer.flush(); } } } /* 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 (!notifyParentBuffersEnabled) return; if (parentBuffers == null) { parentBuffers = new HashSet<SoftReference<StreamCharBufferKey>>(); } parentBuffers.add(new SoftReference<StreamCharBufferKey>(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 (!notifyParentBuffersEnabled) return; if (parentBuffers == null) { return; } for (Iterator<SoftReference<StreamCharBufferKey>> i = parentBuffers.iterator(); i.hasNext();) { SoftReference<StreamCharBufferKey> 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(); } } } @Override public StreamCharBuffer clone() { StreamCharBuffer cloned = new StreamCharBuffer(); cloned.setNotifyParentBuffersEnabled(false); if (this.size() > 0) { cloned.addChunk(readToSingleChunk()); } return cloned; } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { int version = in.readInt(); if (version != EXTERNALIZABLE_VERSION) { throw new IOException("Uncompatible version in serialization stream."); } reset(); int len = in.readInt(); if (len > 0) { char[] buf = new char[len]; Reader reader = new InputStreamReader((InputStream) in, "UTF-8"); reader.read(buf); String str = StringCharArrayAccessor.createString(buf); MultipartStringChunk mpStringChunk = new MultipartStringChunk(str); int partCount = in.readInt(); for (int i = 0; i < partCount; i++) { EncodingStatePart current = new EncodingStatePart(); mpStringChunk.appendEncodingStatePart(current); current.len = in.readInt(); int encodersSize = in.readInt(); Set<Encoder> encoders = null; if (encodersSize > 0) { encoders = new LinkedHashSet<Encoder>(); for (int j = 0; j < encodersSize; j++) { String codecName = in.readUTF(); boolean safe = in.readBoolean(); encoders.add(new SavedEncoder(codecName, safe)); } } current.encodingState = new EncodingStateImpl(encoders); } addChunk(mpStringChunk); } } private static final class SavedEncoder implements Encoder { private CodecIdentifier codecIdentifier; private boolean safe; public SavedEncoder(String codecName, boolean safe) { this.codecIdentifier = new DefaultCodecIdentifier(codecName); this.safe = safe; } public CodecIdentifier getCodecIdentifier() { return codecIdentifier; } public boolean isSafe() { return safe; } public Object encode(Object o) { throw new UnsupportedOperationException("encode isn't supported for SavedEncoder"); } public void markEncoded(CharSequence string) { throw new UnsupportedOperationException("markEncoded isn't supported for SavedEncoder"); } public boolean isApplyToSafelyEncoded() { return false; } } public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(EXTERNALIZABLE_VERSION); StringChunk stringChunk = readToSingleStringChunk(false); if (stringChunk != null && stringChunk.str.length() > 0) { char[] buf = StringCharArrayAccessor.getValue(stringChunk.str); out.writeInt(buf.length); Writer writer = new OutputStreamWriter((OutputStream) out, "UTF-8"); writer.write(buf); writer.flush(); if (stringChunk instanceof MultipartStringChunk) { MultipartStringChunk mpStringChunk = (MultipartStringChunk) stringChunk; out.writeInt(mpStringChunk.partCount()); EncodingStatePart current = mpStringChunk.firstPart; while (current != null) { out.writeInt(current.len); if (current.encodingState != null && current.encodingState.getEncoders() != null && current.encodingState.getEncoders().size() > 0) { out.writeInt(current.encodingState.getEncoders().size()); for (Encoder encoder : current.encodingState.getEncoders()) { out.writeUTF(encoder.getCodecIdentifier().getCodecName()); out.writeBoolean(encoder.isSafe()); } } else { out.writeInt(0); } current = current.next; } } else { out.writeInt(0); } } else { out.writeInt(0); } } public StreamCharBuffer encodeToBuffer(Encoder encoder) { StreamCharBuffer coded = new StreamCharBuffer( Math.min(Math.max(totalChunkSize, chunkSize) * 12 / 10, maxChunkSize)); coded.setNotifyParentBuffersEnabled(false); EncodedAppender codedWriter = coded.writer.getEncodedAppender(); try { encodeTo(codedWriter, encoder); } catch (IOException e) { // Should not ever happen log.error("IOException in StreamCharBuffer.encodeToBuffer", e); } return coded; } public void encodeTo(EncodedAppender appender, Encoder encoder) throws IOException { AbstractChunk current = firstChunk; while (current != null) { current.encodeTo(appender, encoder); current = current.next; } allocBuffer.encodeTo(appender, encoder); } public CharSequence encode(Encoder encoder) { return encodeToBuffer(encoder); } public Writer getWriterForEncoder() { return getWriterForEncoder(null); } public Writer getWriterForEncoder(Encoder encoder) { return getWriterForEncoder(encoder, DefaultGrailsCodecClass.getEncodingStateRegistryLookup() != null ? DefaultGrailsCodecClass.getEncodingStateRegistryLookup().lookup() : null); } public Writer getWriterForEncoder(Encoder encoder, EncodingStateRegistry encodingStateRegistry) { return new EncodedAppenderWriter(writer.getEncodedAppender(), encoder, encodingStateRegistry); } public boolean isNotifyParentBuffersEnabled() { return notifyParentBuffersEnabled; } /** * By default the parent buffers (a buffer where this buffer has been appended to) get notified of changed to this buffer. * * You can control the notification behavior with this property. * Setting this property to false will also clear the references to parent buffers if there are any. * * @param notifyParentBuffersEnabled */ public void setNotifyParentBuffersEnabled(boolean notifyParentBuffersEnabled) { this.notifyParentBuffersEnabled = notifyParentBuffersEnabled; if (!notifyParentBuffersEnabled && parentBuffers != null) { parentBuffers.clear(); } } }