View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.shared.util;
20  
21  import java.io.EOFException;
22  import java.io.Externalizable;
23  import java.io.IOException;
24  import java.io.ObjectInput;
25  import java.io.ObjectOutput;
26  import java.io.Reader;
27  import java.io.Writer;
28  import java.lang.ref.SoftReference;
29  import java.util.ArrayList;
30  import java.util.HashMap;
31  import java.util.HashSet;
32  import java.util.Iterator;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Set;
36  
37  /**
38   * <p>
39   * StreamCharBuffer is a multipurpose in-memory buffer that can replace JDK
40   * in-memory buffers (StringBuffer, StringBuilder, StringWriter).
41   * </p>
42   *
43   * <p>
44   * Grails GSP rendering uses this class as a buffer that is optimized for performance.
45   * </p>
46   *
47   * <p>
48   * StreamCharBuffer keeps the buffer in a linked list of "chunks". The main
49   * difference compared to JDK in-memory buffers (StringBuffer, StringBuilder &amp;
50   * StringWriter) is that the buffer can be held in several smaller buffers
51   * ("chunks" here). In JDK in-memory buffers, the buffer has to be expanded
52   * whenever it gets filled up. The old buffer's data is copied to the new one
53   * and the old one is discarded. In StreamCharBuffer, there are several ways to
54   * prevent unnecessary allocation &amp; copy operations. The StreamCharBuffer
55   * contains a linked list of different type of chunks: char arrays,
56   * java.lang.String chunks and other StreamCharBuffers as sub chunks. A
57   * StringChunk is appended to the linked list whenever a java.lang.String of a
58   * length that exceeds the "stringChunkMinSize" value is written to the buffer.
59   * </p>
60   *
61   * <p>
62   * Grails tag libraries also use a StreamCharBuffer to "capture" the output of
63   * the taglib and return it to the caller. The buffer can be appended to it's
64   * parent buffer directly without extra object generation (like converting to
65   * java.lang.String in between).
66   *
67   * for example this line of code in a taglib would just append the buffer
68   * returned from the body closure evaluation to the buffer of the taglib:</p>
69   * <code>
70   * out &lt;&lt; body()
71   * </code><p>
72   * other example:</p>
73   * <code>
74   * out &lt;&lt; g.render(template: '/some/template', model:[somebean: somebean])
75   * </code><p>
76   * There's no extra java.lang.String generation overhead.
77   *
78   * </p>
79   *
80   * <p>
81   * There's a java.io.Writer interface for appending character data to the buffer
82   * and a java.io.Reader interface for reading data.
83   * </p>
84   *
85   * <p>
86   * Each {@link #getReader()} call will create a new reader instance that keeps
87   * it own state.</p><p>
88   * There is a alternative method {@link #getReader(boolean)} for creating the
89   * reader. When reader is created by calling getReader(true), the reader will
90   * remove already read characters from the buffer. In this mode only a single
91   * reader instance is supported.
92   * </p>
93   *
94   * <p>
95   * There's also several other options for reading data:</p><ul>
96   * <li>{@link #readAsCharArray()} reads the buffer to a char[] array</li>
97   * <li>{@link #readAsString()} reads the buffer and wraps the char[] data as a
98   * String</li>
99   * <li>{@link #writeTo(Writer)} writes the buffer to a java.io.Writer</li>
100  * <li>{@link #toCharArray()} returns the buffer as a char[] array, caches the
101  * return value internally so that this method can be called several times.</li>
102  * <li>{@link #toString()} returns the buffer as a String, caches the return value
103  * internally</li></ul>
104  *
105  * <p>
106  * By using the "connectTo" method, one can connect the buffer directly to a
107  * target java.io.Writer. The internal buffer gets flushed automaticly to the
108  * target whenever the buffer gets filled up. See connectTo(Writer).
109  * </p>
110  *
111  * <p>
112  * <b>This class is not thread-safe.</b> Object instances of this class are
113  * intended to be used by a single Thread. The Reader and Writer interfaces can
114  * be open simultaneous and the same Thread can write and read several times.
115  * </p>
116  *
117  * <p>
118  * Main operation principle:
119  * </p>
120  * <p>
121  * StreamCharBuffer keeps the buffer in a linked link of "chunks".
122  * The main difference compared to JDK in-memory buffers (StringBuffer,
123  * StringBuilder &amp; StringWriter) is that the buffer can be held in several
124  * smaller buffers ("chunks" here).
125  * In JDK in-memory buffers, the buffer has to be expanded whenever it gets
126  * filled up. The old buffer's data is copied to the new one and the old one is
127  * discarded.
128  * In StreamCharBuffer, there are several ways to prevent unnecessary allocation
129  * &amp; copy operations.
130  * </p>
131  * <p>
132  * There can be several different type of chunks: char arrays (
133  * {@code CharBufferChunk}), String chunks ({@code StringChunk}) and other
134  * StreamCharBuffers as sub chunks ({@code StreamCharBufferSubChunk}).
135  * </p>
136  * <p>
137  * Child StreamCharBuffers can be changed after adding to parent buffer. The
138  * flush() method must be called on the child buffer's Writer to notify the
139  * parent that the child buffer's content has been changed (used for calculating
140  * total size).
141  * </p>
142  * <p>
143  * A StringChunk is appended to the linked list whenever a java.lang.String of a
144  * length that exceeds the "stringChunkMinSize" value is written to the buffer.
145  * </p>
146  * <p>
147  * If the buffer is in "connectTo" mode, any String or char[] that's length is
148  * over writeDirectlyToConnectedMinSize gets written directly to the target. The
149  * buffer content will get fully flushed to the target before writing the String
150  * or char[].
151  * </p>
152  * <p>
153  * There can be several targets "listening" the buffer in "connectTo" mode. The
154  * same content will be written to all targets.
155  * <p>
156  * <p>
157  * Growable chunksize: By default, a newly allocated chunk's size will grow
158  * based on the total size of all written chunks.
159  * The default growProcent value is 100. If the total size is currently 1024,
160  * the newly created chunk will have a internal buffer that's size is 1024.
161  * Growable chunksize can be turned off by setting the growProcent to 0.
162  * There's a default maximum chunksize of 1MB by default. The minimum size is
163  * the initial chunksize size.
164  * </p>
165  *
166  * 
167  * <table>
168  * <caption>System properties to change default configuration parameters</caption>
169  * <tr>
170  * <th>System Property name</th>
171  * <th>Description</th>
172  * <th>Default value</th>
173  * </tr>
174  * <tr>
175  * <td>streamcharbuffer.chunksize</td>
176  * <td>default chunk size - the size the first allocated buffer</td>
177  * <td>512</td>
178  * </tr>
179  * <tr>
180  * <td>streamcharbuffer.maxchunksize</td>
181  * <td>maximum chunk size - the maximum size of the allocated buffer</td>
182  * <td>1048576</td>
183  * </tr>
184  * <tr>
185  * <td>streamcharbuffer.growprocent</td>
186  * <td>growing buffer percentage - the newly allocated buffer is defined by
187  * total_size * (growpercent/100)</td>
188  * <td>100</td>
189  * </tr>
190  * <tr>
191  * <td>streamcharbuffer.subbufferchunkminsize</td>
192  * <td>minimum size of child StreamCharBuffer chunk - if the size is smaller,
193  * the content is copied to the parent buffer</td>
194  * <td>512</td>
195  * </tr>
196  * <tr>
197  * <td>streamcharbuffer.substringchunkminsize</td>
198  * <td>minimum size of String chunks - if the size is smaller, the content is
199  * copied to the buffer</td>
200  * <td>512</td>
201  * </tr>
202  * <tr>
203  * <td>streamcharbuffer.chunkminsize</td>
204  * <td>minimum size of chunk that gets written directly to the target in
205  * connected mode.</td>
206  * <td>256</td>
207  * </tr>
208  * </table>
209  * <p>
210  * Configuration values can also be changed for each instance of
211  * StreamCharBuffer individually. Default values are defined with System
212  * Properties.
213  *
214  * </p>
215  *
216  * See org.codehaus.groovy.grails.web.util.StreamCharBuffer
217  *      file licensed under ASL v2.0 
218  *      Copyright 2009 the original author or authors.
219  * Author Lari Hotari, Sagire Software Oy
220  */
221 public class StreamCharBuffer implements /*Writable,*/CharSequence,
222         Externalizable
223 {
224     static final long serialVersionUID = 5486972234419632945L;
225     //private static final Log log=LogFactory.getLog(StreamCharBuffer.class);
226 
227     private static final int DEFAULT_CHUNK_SIZE = Integer.getInteger(
228             "oam.streamcharbuffer.chunksize", 512);
229     private static final int DEFAULT_MAX_CHUNK_SIZE = Integer.getInteger(
230             "oam.streamcharbuffer.maxchunksize", 1024 * 1024);
231     private static final int DEFAULT_CHUNK_SIZE_GROW_PROCENT = Integer
232             .getInteger("oam.streamcharbuffer.growprocent", 100);
233     private static final int SUB_BUFFERCHUNK_MIN_SIZE = Integer.getInteger(
234             "oam.streamcharbuffer.subbufferchunkminsize", 512);
235     private static final int SUB_STRINGCHUNK_MIN_SIZE = Integer.getInteger(
236             "oam.streamcharbuffer.substringchunkminsize", 512);
237     private static final int WRITE_DIRECT_MIN_SIZE = Integer.getInteger(
238             "oam.streamcharbuffer.writedirectminsize", 1024);
239     private static final int CHUNK_MIN_SIZE = Integer.getInteger(
240             "oam.streamcharbuffer.chunkminsize", 256);
241 
242     private final int firstChunkSize;
243     private final int growProcent;
244     private final int maxChunkSize;
245     private int subStringChunkMinSize = SUB_STRINGCHUNK_MIN_SIZE;
246     private int subBufferChunkMinSize = SUB_BUFFERCHUNK_MIN_SIZE;
247     private int writeDirectlyToConnectedMinSize = WRITE_DIRECT_MIN_SIZE;
248     private int chunkMinSize = CHUNK_MIN_SIZE;
249 
250     private int chunkSize;
251     private int totalChunkSize;
252 
253     private final StreamCharBufferWriter writer;
254     private List<ConnectedWriter> connectedWriters;
255     private Writer connectedWritersWriter;
256 
257     boolean preferSubChunkWhenWritingToOtherBuffer = false;
258 
259     private AllocatedBuffer allocBuffer;
260     private AbstractChunk firstChunk;
261     private AbstractChunk lastChunk;
262     private int totalCharsInList;
263     private int totalCharsInDynamicChunks;
264     private int sizeAtLeast;
265     private StreamCharBufferKey bufferKey = new StreamCharBufferKey();
266     private Map<StreamCharBufferKey, StreamCharBufferSubChunk> dynamicChunkMap;
267 
268     private Set<SoftReference<StreamCharBufferKey>> parentBuffers;
269     int allocatedBufferIdSequence = 0;
270     int readerCount = 0;
271     boolean hasReaders = false;
272 
273     public StreamCharBuffer()
274     {
275         this(DEFAULT_CHUNK_SIZE, DEFAULT_CHUNK_SIZE_GROW_PROCENT,
276                 DEFAULT_MAX_CHUNK_SIZE);
277     }
278 
279     public StreamCharBuffer(int chunkSize)
280     {
281         this(chunkSize, DEFAULT_CHUNK_SIZE_GROW_PROCENT, DEFAULT_MAX_CHUNK_SIZE);
282     }
283 
284     public StreamCharBuffer(int chunkSize, int growProcent)
285     {
286         this(chunkSize, growProcent, DEFAULT_MAX_CHUNK_SIZE);
287     }
288 
289     public StreamCharBuffer(int chunkSize, int growProcent, int maxChunkSize)
290     {
291         this.firstChunkSize = chunkSize;
292         this.growProcent = growProcent;
293         this.maxChunkSize = maxChunkSize;
294         writer = new StreamCharBufferWriter();
295         reset(true);
296     }
297 
298     private class StreamCharBufferKey
299     {
300         StreamCharBuffer getBuffer()
301         {
302             return StreamCharBuffer.this;
303         }
304     }
305 
306     public boolean isPreferSubChunkWhenWritingToOtherBuffer()
307     {
308         return preferSubChunkWhenWritingToOtherBuffer;
309     }
310 
311     public void setPreferSubChunkWhenWritingToOtherBuffer(
312             boolean preferSubChunkWhenWritingToOtherBuffer)
313     {
314         this.preferSubChunkWhenWritingToOtherBuffer = preferSubChunkWhenWritingToOtherBuffer;
315     }
316 
317     public final void reset()
318     {
319         reset(true);
320     }
321 
322     /**
323      * resets the state of this buffer (empties it)
324      *
325      * @param resetChunkSize
326      */
327     public final void reset(boolean resetChunkSize)
328     {
329         firstChunk = null;
330         lastChunk = null;
331         totalCharsInList = 0;
332         totalCharsInDynamicChunks = -1;
333         sizeAtLeast = -1;
334         if (resetChunkSize)
335         {
336             chunkSize = firstChunkSize;
337             totalChunkSize = 0;
338         }
339         allocBuffer = new AllocatedBuffer(chunkSize);
340         dynamicChunkMap = new HashMap<StreamCharBufferKey, StreamCharBufferSubChunk>();
341     }
342 
343     /**
344      * Clears the buffer and notifies the parents of this buffer of the change
345      * 
346      */
347     public final void clear()
348     {
349         reset();
350         notifyBufferChange();
351     }
352 
353     /**
354      * Connect this buffer to a target Writer.
355      *
356      * When the buffer (a chunk) get filled up, it will automaticly write it's content to the Writer
357      *
358      * @param w
359      */
360     public final void connectTo(Writer w)
361     {
362         connectTo(w, true);
363     }
364 
365     public final void connectTo(Writer w, boolean autoFlush)
366     {
367         initConnected();
368         connectedWriters.add(new ConnectedWriter(w, autoFlush));
369         initConnectedWritersWriter();
370     }
371 
372     private void initConnectedWritersWriter()
373     {
374         if (connectedWriters.size() > 1)
375         {
376             connectedWritersWriter = new MultiOutputWriter(connectedWriters);
377         }
378         else
379         {
380             connectedWritersWriter = new SingleOutputWriter(
381                     connectedWriters.get(0));
382         }
383     }
384 
385     public final void connectTo(LazyInitializingWriter w)
386     {
387         connectTo(w, true);
388     }
389 
390     public final void connectTo(LazyInitializingWriter w, boolean autoFlush)
391     {
392         initConnected();
393         connectedWriters.add(new ConnectedWriter(w, autoFlush));
394         initConnectedWritersWriter();
395     }
396 
397     public final void removeConnections()
398     {
399         if (connectedWriters != null)
400         {
401             connectedWriters.clear();
402             connectedWritersWriter = null;
403         }
404     }
405 
406     private void initConnected()
407     {
408         if (connectedWriters == null)
409         {
410             connectedWriters = new ArrayList<ConnectedWriter>(2);
411         }
412     }
413 
414     public int getSubStringChunkMinSize()
415     {
416         return subStringChunkMinSize;
417     }
418 
419     /**
420      * Minimum size for a String to be added as a StringChunk instead of copying content to 
421      * the char[] buffer of the current StreamCharBufferChunk
422      *
423      * @param stringChunkMinSize
424      */
425     public void setSubStringChunkMinSize(int stringChunkMinSize)
426     {
427         this.subStringChunkMinSize = stringChunkMinSize;
428     }
429 
430     public int getSubBufferChunkMinSize()
431     {
432         return subBufferChunkMinSize;
433     }
434 
435     public void setSubBufferChunkMinSize(int subBufferChunkMinSize)
436     {
437         this.subBufferChunkMinSize = subBufferChunkMinSize;
438     }
439 
440     public int getWriteDirectlyToConnectedMinSize()
441     {
442         return writeDirectlyToConnectedMinSize;
443     }
444 
445     /**
446      * Minimum size for a String or char[] to get written directly to connected writer (in "connectTo" mode).
447      *
448      * @param writeDirectlyToConnectedMinSize
449      */
450     public void setWriteDirectlyToConnectedMinSize(
451             int writeDirectlyToConnectedMinSize)
452     {
453         this.writeDirectlyToConnectedMinSize = writeDirectlyToConnectedMinSize;
454     }
455 
456     public int getChunkMinSize()
457     {
458         return chunkMinSize;
459     }
460 
461     public void setChunkMinSize(int chunkMinSize)
462     {
463         this.chunkMinSize = chunkMinSize;
464     }
465 
466     /**
467      * Writer interface for adding/writing data to the buffer.
468      *
469      * @return the Writer
470      */
471     public Writer getWriter()
472     {
473         return writer;
474     }
475 
476     /**
477      * Creates a new Reader instance for reading/consuming data from the buffer.
478      * Each call creates a new instance that will keep it's reading state. There can be several readers on
479      * the buffer. (single thread only supported)
480      *
481      * @return the Reader
482      */
483     public Reader getReader()
484     {
485         return getReader(false);
486     }
487 
488     /**
489      * Like getReader(), but when removeAfterReading is true, the read data will be removed from the buffer.
490      *
491      * @param removeAfterReading
492      * @return the Reader
493      */
494     public Reader getReader(boolean removeAfterReading)
495     {
496         readerCount++;
497         hasReaders = true;
498         return new StreamCharBufferReader(removeAfterReading);
499     }
500 
501     /**
502      * Writes the buffer content to a target java.io.Writer
503      *
504      * @param target
505      * @throws IOException
506      */
507     public Writer writeTo(Writer target) throws IOException
508     {
509         writeTo(target, false, false);
510         return getWriter();
511     }
512 
513     /**
514      * Writes the buffer content to a target java.io.Writer
515      *
516      * @param target Writer
517      * @param flushTarget calls target.flush() before finishing
518      * @param emptyAfter empties the buffer if true
519      * @throws IOException
520      */
521     public void writeTo(Writer target, boolean flushTarget, boolean emptyAfter)
522             throws IOException
523     {
524         //if (target instanceof GrailsWrappedWriter) {
525         //    target = ((GrailsWrappedWriter)target).unwrap();
526         //}
527         if (target instanceof StreamCharBufferWriter)
528         {
529             if (target == writer)
530             {
531                 throw new IllegalArgumentException(
532                         "Cannot write buffer to itself.");
533             }
534             ((StreamCharBufferWriter) target).write(this);
535             return;
536         }
537         writeToImpl(target, flushTarget, emptyAfter);
538     }
539 
540     private void writeToImpl(Writer target, boolean flushTarget,
541             boolean emptyAfter) throws IOException
542     {
543         AbstractChunk current = firstChunk;
544         while (current != null)
545         {
546             current.writeTo(target);
547             current = current.next;
548         }
549         if (emptyAfter)
550         {
551             firstChunk = null;
552             lastChunk = null;
553             totalCharsInList = 0;
554             totalCharsInDynamicChunks = -1;
555             sizeAtLeast = -1;
556             dynamicChunkMap.clear();
557         }
558         allocBuffer.writeTo(target);
559         if (emptyAfter)
560         {
561             allocBuffer.reuseBuffer();
562         }
563         if (flushTarget)
564         {
565             target.flush();
566         }
567     }
568 
569     /**
570      * Reads the buffer to a char[].
571      *
572      * @return the chars
573      */
574     public char[] readAsCharArray()
575     {
576         int currentSize = size();
577         if (currentSize == 0)
578         {
579             return new char[0];
580         }
581 
582         FixedCharArrayWriter target = new FixedCharArrayWriter(currentSize);
583         try
584         {
585             writeTo(target);
586         }
587         catch (IOException e)
588         {
589             throw new RuntimeException("Unexpected IOException", e);
590         }
591         return target.getCharArray();
592     }
593 
594     /**
595      * Reads the buffer to a String.
596      *
597      * @return the String
598      */
599     public String readAsString()
600     {
601         char[] buf = readAsCharArray();
602         if (buf.length > 0)
603         {
604             return StringCharArrayAccessor.createString(buf);
605         }
606 
607         return "";
608     }
609 
610     /**
611      * {@inheritDoc}
612      *
613      * Reads (and empties) the buffer to a String, but caches the return value for subsequent calls.
614      * If more content has been added between 2 calls, the returned value will be joined from the previously 
615      * cached value and the data read from the buffer.
616      *
617      * @see java.lang.Object#toString()
618      */
619     @Override
620     public String toString()
621     {
622         if (firstChunk == lastChunk && firstChunk instanceof StringChunk
623                 && allocBuffer.charsUsed() == 0
624                 && ((StringChunk) firstChunk).isSingleBuffer())
625         {
626             return ((StringChunk) firstChunk).str;
627         }
628 
629         int initialReaderCount = readerCount;
630         String str = readAsString();
631         if (initialReaderCount == 0)
632         {
633             // if there are no readers, the result can be cached
634             reset();
635             if (str.length() > 0)
636             {
637                 addChunk(new StringChunk(str, 0, str.length()));
638             }
639         }
640         return str;
641     }
642 
643     /**
644      * {@inheritDoc}
645      *
646      * Uses String's hashCode to support compatibility with String instances in maps, sets, etc.
647      *
648      * @see java.lang.Object#hashCode()
649      */
650     @Override
651     public int hashCode()
652     {
653         return toString().hashCode();
654     }
655 
656     /**
657      * equals uses String.equals to check for equality to support compatibility with String instances 
658      * in maps, sets, etc.
659      *
660      * @see java.lang.Object#equals(java.lang.Object)
661      */
662     @Override
663     public boolean equals(Object o)
664     {
665         if (o == this)
666         {
667             return true;
668         }
669 
670         if (!(o instanceof CharSequence))
671         {
672             return false;
673         }
674 
675         CharSequence other = (CharSequence) o;
676 
677         return toString().equals(other.toString());
678     }
679 
680     public String plus(String value)
681     {
682         return toString() + value;
683     }
684 
685     public String plus(Object value)
686     {
687         return toString() + String.valueOf(value);
688     }
689 
690     /**
691      * Reads the buffer to a char[].
692      *
693      * Caches the result if there aren't any readers.
694      *
695      * @return the chars
696      */
697     public char[] toCharArray()
698     {
699         // check if there is a cached single charbuffer
700         if (firstChunk == lastChunk && firstChunk instanceof CharBufferChunk
701                 && allocBuffer.charsUsed() == 0
702                 && ((CharBufferChunk) firstChunk).isSingleBuffer())
703         {
704             return ((CharBufferChunk) firstChunk).buffer;
705         }
706 
707         int initialReaderCount = readerCount;
708         char[] buf = readAsCharArray();
709         if (initialReaderCount == 0)
710         {
711             // if there are no readers, the result can be cached
712             reset();
713             if (buf.length > 0)
714             {
715                 addChunk(new CharBufferChunk(-1, buf, 0, buf.length));
716             }
717         }
718         return buf;
719     }
720 
721     public int size()
722     {
723         int total = totalCharsInList;
724         if (totalCharsInDynamicChunks == -1)
725         {
726             totalCharsInDynamicChunks = 0;
727             for (StreamCharBufferSubChunk chunk : dynamicChunkMap.values())
728             {
729                 totalCharsInDynamicChunks += chunk.size();
730             }
731         }
732         total += totalCharsInDynamicChunks;
733         total += allocBuffer.charsUsed();
734         sizeAtLeast = total;
735         return total;
736     }
737 
738     public boolean isEmpty()
739     {
740         return !isNotEmpty();
741     }
742 
743     boolean isNotEmpty()
744     {
745         if (totalCharsInList > 0)
746         {
747             return true;
748         }
749         if (totalCharsInDynamicChunks > 0)
750         {
751             return true;
752         }
753         if (allocBuffer.charsUsed() > 0)
754         {
755             return true;
756         }
757         if (totalCharsInDynamicChunks == -1)
758         {
759             for (StreamCharBufferSubChunk chunk : dynamicChunkMap.values())
760             {
761                 if (chunk.getSubBuffer().isNotEmpty())
762                 {
763                     return true;
764                 }
765             }
766         }
767         return false;
768     }
769 
770     boolean isSizeLarger(int minSize)
771     {
772         if (minSize <= sizeAtLeast)
773         {
774             return true;
775         }
776 
777         boolean retval = calculateIsSizeLarger(minSize);
778         if (retval && minSize > sizeAtLeast)
779         {
780             sizeAtLeast = minSize;
781         }
782         return retval;
783     }
784 
785     private boolean calculateIsSizeLarger(int minSize)
786     {
787         int total = totalCharsInList;
788         total += allocBuffer.charsUsed();
789         if (total > minSize)
790         {
791             return true;
792         }
793         if (totalCharsInDynamicChunks != -1)
794         {
795             total += totalCharsInDynamicChunks;
796             if (total > minSize)
797             {
798                 return true;
799             }
800         }
801         else
802         {
803             for (StreamCharBufferSubChunk chunk : dynamicChunkMap.values())
804             {
805                 if (!chunk.hasCachedSize()
806                         && chunk.getSubBuffer().isSizeLarger(minSize - total))
807                 {
808                     return true;
809                 }
810                 total += chunk.size();
811                 if (total > minSize)
812                 {
813                     return true;
814                 }
815             }
816         }
817         return false;
818     }
819 
820     int allocateSpace() throws IOException
821     {
822         int spaceLeft = allocBuffer.spaceLeft();
823         if (spaceLeft == 0)
824         {
825             spaceLeft = appendCharBufferChunk(true);
826         }
827         return spaceLeft;
828     }
829 
830     private int appendCharBufferChunk(boolean flushInConnected)
831             throws IOException
832     {
833         int spaceLeft = 0;
834         if (flushInConnected && isConnectedMode())
835         {
836             flushToConnected();
837             if (!isChunkSizeResizeable())
838             {
839                 allocBuffer.reuseBuffer();
840                 spaceLeft = allocBuffer.spaceLeft();
841             }
842             else
843             {
844                 spaceLeft = 0;
845             }
846         }
847         else
848         {
849             if (allocBuffer.hasChunk())
850             {
851                 addChunk(allocBuffer.createChunk());
852             }
853             spaceLeft = allocBuffer.spaceLeft();
854         }
855         if (spaceLeft == 0)
856         {
857             totalChunkSize += allocBuffer.chunkSize();
858             resizeChunkSizeAsProcentageOfTotalSize();
859             allocBuffer = new AllocatedBuffer(chunkSize);
860             spaceLeft = allocBuffer.spaceLeft();
861         }
862         return spaceLeft;
863     }
864 
865     void appendStringChunk(String str, int off, int len) throws IOException
866     {
867         appendCharBufferChunk(false);
868         addChunk(new StringChunk(str, off, len));
869     }
870 
871     public void appendStreamCharBufferChunk(StreamCharBuffer subBuffer)
872             throws IOException
873     {
874         appendCharBufferChunk(false);
875         addChunk(new StreamCharBufferSubChunk(subBuffer));
876     }
877 
878     void addChunk(AbstractChunk newChunk)
879     {
880         if (lastChunk != null)
881         {
882             lastChunk.next = newChunk;
883             if (hasReaders)
884             {
885                 // double link only if there are active readers since backwards iterating is only required 
886                 //for simultaneous writer & reader
887                 newChunk.prev = lastChunk;
888             }
889         }
890         lastChunk = newChunk;
891         if (firstChunk == null)
892         {
893             firstChunk = newChunk;
894         }
895         if (newChunk instanceof StreamCharBufferSubChunk)
896         {
897             StreamCharBufferSubChunk bufSubChunk = (StreamCharBufferSubChunk) newChunk;
898             dynamicChunkMap.put(bufSubChunk.streamCharBuffer.bufferKey,
899                     bufSubChunk);
900         }
901         else
902         {
903             totalCharsInList += newChunk.size();
904         }
905     }
906 
907     public boolean isConnectedMode()
908     {
909         return connectedWriters != null && !connectedWriters.isEmpty();
910     }
911 
912     private void flushToConnected() throws IOException
913     {
914         writeTo(connectedWritersWriter, true, true);
915     }
916 
917     protected boolean isChunkSizeResizeable()
918     {
919         return (growProcent > 0);
920     }
921 
922     protected void resizeChunkSizeAsProcentageOfTotalSize()
923     {
924         if (growProcent == 0)
925         {
926             return;
927         }
928 
929         if (growProcent == 100)
930         {
931             chunkSize = Math.min(totalChunkSize, maxChunkSize);
932         }
933         else if (growProcent == 200)
934         {
935             chunkSize = Math.min(totalChunkSize << 1, maxChunkSize);
936         }
937         else if (growProcent > 0)
938         {
939             chunkSize = Math.max(Math.min((totalChunkSize * growProcent) / 100,
940                     maxChunkSize), firstChunkSize);
941         }
942     }
943 
944     protected static final void arrayCopy(char[] src, int srcPos, char[] dest,
945             int destPos, int length)
946     {
947         if (length == 1)
948         {
949             dest[destPos] = src[srcPos];
950         }
951         else
952         {
953             System.arraycopy(src, srcPos, dest, destPos, length);
954         }
955     }
956 
957     /**
958      * This is the java.io.Writer implementation for StreamCharBuffer
959      *
960      * @author Lari Hotari, Sagire Software Oy
961      */
962     public final class StreamCharBufferWriter extends Writer
963     {
964         boolean closed = false;
965         int writerUsedCounter = 0;
966         boolean increaseCounter = true;
967 
968         @Override
969         public final void write(final char[] b, final int off, final int len)
970                 throws IOException
971         {
972             if (b == null)
973             {
974                 throw new NullPointerException();
975             }
976 
977             if ((off < 0) || (off > b.length) || (len < 0)
978                     || ((off + len) > b.length) || ((off + len) < 0))
979             {
980                 throw new IndexOutOfBoundsException();
981             }
982 
983             if (len == 0)
984             {
985                 return;
986             }
987 
988             markUsed();
989             if (shouldWriteDirectly(len))
990             {
991                 appendCharBufferChunk(true);
992                 connectedWritersWriter.write(b, off, len);
993             }
994             else
995             {
996                 int charsLeft = len;
997                 int currentOffset = off;
998                 while (charsLeft > 0)
999                 {
1000                     int spaceLeft = allocateSpace();
1001                     int writeChars = Math.min(spaceLeft, charsLeft);
1002                     allocBuffer.write(b, currentOffset, writeChars);
1003                     charsLeft -= writeChars;
1004                     currentOffset += writeChars;
1005                 }
1006             }
1007         }
1008 
1009         private final boolean shouldWriteDirectly(final int len)
1010         {
1011             if (!isConnectedMode())
1012             {
1013                 return false;
1014             }
1015 
1016             if (!(writeDirectlyToConnectedMinSize >= 0 && len >= writeDirectlyToConnectedMinSize))
1017             {
1018                 return false;
1019             }
1020 
1021             return isNextChunkBigEnough(len);
1022         }
1023 
1024         private final boolean isNextChunkBigEnough(final int len)
1025         {
1026             return (len > getNewChunkMinSize());
1027         }
1028 
1029         private final int getDirectChunkMinSize()
1030         {
1031             if (!isConnectedMode())
1032             {
1033                 return -1;
1034             }
1035             if (writeDirectlyToConnectedMinSize >= 0)
1036             {
1037                 return writeDirectlyToConnectedMinSize;
1038             }
1039 
1040             return getNewChunkMinSize();
1041         }
1042 
1043         private final int getNewChunkMinSize()
1044         {
1045             if (chunkMinSize <= 0 || allocBuffer.charsUsed() == 0
1046                     || allocBuffer.charsUsed() >= chunkMinSize)
1047             {
1048                 return 0;
1049             }
1050             return allocBuffer.spaceLeft();
1051         }
1052 
1053         @Override
1054         public final void write(final String str) throws IOException
1055         {
1056             write(str, 0, str.length());
1057         }
1058 
1059         @Override
1060         public final void write(final String str, final int off, final int len)
1061                 throws IOException
1062         {
1063             if (len == 0)
1064             {
1065                 return;
1066             }
1067             markUsed();
1068             if (shouldWriteDirectly(len))
1069             {
1070                 appendCharBufferChunk(true);
1071                 connectedWritersWriter.write(str, off, len);
1072             }
1073             else if (len >= subStringChunkMinSize && isNextChunkBigEnough(len))
1074             {
1075                 appendStringChunk(str, off, len);
1076             }
1077             else
1078             {
1079                 int charsLeft = len;
1080                 int currentOffset = off;
1081                 while (charsLeft > 0)
1082                 {
1083                     int spaceLeft = allocateSpace();
1084                     int writeChars = Math.min(spaceLeft, charsLeft);
1085                     allocBuffer.writeString(str, currentOffset, writeChars);
1086                     charsLeft -= writeChars;
1087                     currentOffset += writeChars;
1088                 }
1089             }
1090         }
1091 
1092         public final void write(StreamCharBuffer subBuffer) throws IOException
1093         {
1094             markUsed();
1095             int directChunkMinSize = getDirectChunkMinSize();
1096             if (directChunkMinSize != -1
1097                     && subBuffer.isSizeLarger(directChunkMinSize))
1098             {
1099                 appendCharBufferChunk(true);
1100                 subBuffer.writeToImpl(connectedWritersWriter, false, false);
1101             }
1102             else if (subBuffer.preferSubChunkWhenWritingToOtherBuffer
1103                     || subBuffer.isSizeLarger(Math.max(subBufferChunkMinSize,
1104                             getNewChunkMinSize())))
1105             {
1106                 if (subBuffer.preferSubChunkWhenWritingToOtherBuffer)
1107                 {
1108                     StreamCharBuffer.this.preferSubChunkWhenWritingToOtherBuffer = true;
1109                 }
1110                 appendStreamCharBufferChunk(subBuffer);
1111                 subBuffer.addParentBuffer(StreamCharBuffer.this);
1112             }
1113             else
1114             {
1115                 subBuffer.writeToImpl(this, false, false);
1116             }
1117         }
1118 
1119         @Override
1120         public final Writer append(final CharSequence csq, final int start,
1121                 final int end) throws IOException
1122         {
1123             markUsed();
1124             if (csq == null)
1125             {
1126                 write("null");
1127             }
1128             else
1129             {
1130                 if (csq instanceof String || csq instanceof StringBuffer
1131                         || csq instanceof StringBuilder)
1132                 {
1133                     int len = end - start;
1134                     int charsLeft = len;
1135                     int currentOffset = start;
1136                     while (charsLeft > 0)
1137                     {
1138                         int spaceLeft = allocateSpace();
1139                         int writeChars = Math.min(spaceLeft, charsLeft);
1140                         if (csq instanceof String)
1141                         {
1142                             allocBuffer.writeString((String) csq,
1143                                     currentOffset, writeChars);
1144                         }
1145                         else if (csq instanceof StringBuffer)
1146                         {
1147                             allocBuffer.writeStringBuffer((StringBuffer) csq,
1148                                     currentOffset, writeChars);
1149                         }
1150                         else if (csq instanceof StringBuilder)
1151                         {
1152                             allocBuffer.writeStringBuilder((StringBuilder) csq,
1153                                     currentOffset, writeChars);
1154                         }
1155                         charsLeft -= writeChars;
1156                         currentOffset += writeChars;
1157                     }
1158                 }
1159                 else
1160                 {
1161                     write(csq.subSequence(start, end).toString());
1162                 }
1163             }
1164             return this;
1165         }
1166 
1167         @Override
1168         public final Writer append(final CharSequence csq) throws IOException
1169         {
1170             markUsed();
1171             if (csq == null)
1172             {
1173                 write("null");
1174             }
1175             else
1176             {
1177                 append(csq, 0, csq.length());
1178 
1179             }
1180             return this;
1181         }
1182 
1183         @Override
1184         public void close() throws IOException
1185         {
1186             closed = true;
1187             flush();
1188         }
1189 
1190         public boolean isClosed()
1191         {
1192             return closed;
1193         }
1194 
1195         public boolean isUsed()
1196         {
1197             return writerUsedCounter > 0;
1198         }
1199 
1200         public final void markUsed()
1201         {
1202             if (increaseCounter)
1203             {
1204                 writerUsedCounter++;
1205                 if (!hasReaders)
1206                 {
1207                     increaseCounter = false;
1208                 }
1209             }
1210         }
1211 
1212         public int resetUsed()
1213         {
1214             int prevUsed = writerUsedCounter;
1215             writerUsedCounter = 0;
1216             increaseCounter = true;
1217             return prevUsed;
1218         }
1219 
1220         @Override
1221         public void write(final int b) throws IOException
1222         {
1223             markUsed();
1224             allocateSpace();
1225             allocBuffer.write((char) b);
1226         }
1227 
1228         @Override
1229         public void flush() throws IOException
1230         {
1231             if (isConnectedMode())
1232             {
1233                 flushToConnected();
1234             }
1235             notifyBufferChange();
1236         }
1237 
1238         public final StreamCharBuffer getBuffer()
1239         {
1240             return StreamCharBuffer.this;
1241         }
1242     }
1243 
1244     /**
1245      * This is the java.io.Reader implementation for StreamCharBuffer
1246      *
1247      * @author Lari Hotari, Sagire Software Oy
1248      */
1249 
1250     final public class StreamCharBufferReader extends Reader
1251     {
1252         boolean eofException = false;
1253         int eofReachedCounter = 0;
1254         ChunkReader chunkReader;
1255         ChunkReader lastChunkReader;
1256         boolean removeAfterReading;
1257 
1258         public StreamCharBufferReader(boolean removeAfterReading)
1259         {
1260             this.removeAfterReading = removeAfterReading;
1261         }
1262 
1263         private int prepareRead(int len)
1264         {
1265             if (hasReaders && eofReachedCounter != 0)
1266             {
1267                 if (eofReachedCounter != writer.writerUsedCounter)
1268                 {
1269                     eofReachedCounter = 0;
1270                     eofException = false;
1271                     repositionChunkReader();
1272                 }
1273             }
1274             if (chunkReader == null && eofReachedCounter == 0)
1275             {
1276                 if (firstChunk != null)
1277                 {
1278                     chunkReader = firstChunk.getChunkReader(removeAfterReading);
1279                     if (removeAfterReading)
1280                     {
1281                         firstChunk.subtractFromTotalCount();
1282                     }
1283                 }
1284                 else
1285                 {
1286                     chunkReader = new AllocatedBufferReader(allocBuffer,
1287                             removeAfterReading);
1288                 }
1289             }
1290             int available = 0;
1291             if (chunkReader != null)
1292             {
1293                 available = chunkReader.getReadLenLimit(len);
1294                 while (available == 0 && chunkReader != null)
1295                 {
1296                     chunkReader = chunkReader.next();
1297                     if (chunkReader != null)
1298                     {
1299                         available = chunkReader.getReadLenLimit(len);
1300                     }
1301                     else
1302                     {
1303                         available = 0;
1304                     }
1305                 }
1306             }
1307             if (chunkReader == null)
1308             {
1309                 if (hasReaders)
1310                 {
1311                     eofReachedCounter = writer.writerUsedCounter;
1312                 }
1313                 else
1314                 {
1315                     eofReachedCounter = 1;
1316                 }
1317             }
1318             else if (hasReaders)
1319             {
1320                 lastChunkReader = chunkReader;
1321             }
1322             return available;
1323         }
1324 
1325         /* adds support for reading and writing simultaneously in the same thread */
1326         private void repositionChunkReader()
1327         {
1328             if (lastChunkReader instanceof AllocatedBufferReader)
1329             {
1330                 if (lastChunkReader.isValid())
1331                 {
1332                     chunkReader = lastChunkReader;
1333                 }
1334                 else
1335                 {
1336                     AllocatedBufferReader allocBufferReader = (AllocatedBufferReader) lastChunkReader;
1337                     // find out what is the CharBufferChunk that was read by the AllocatedBufferReader already
1338                     int currentPosition = allocBufferReader.position;
1339                     AbstractChunk chunk = lastChunk;
1340                     while (chunk != null
1341                             && chunk.writerUsedCounter >= lastChunkReader
1342                                     .getWriterUsedCounter())
1343                     {
1344                         if (chunk instanceof CharBufferChunk)
1345                         {
1346                             CharBufferChunk charBufChunk = (CharBufferChunk) chunk;
1347                             if (charBufChunk.allocatedBufferId == allocBufferReader.parent.id)
1348                             {
1349                                 if (currentPosition >= charBufChunk.offset
1350                                         && currentPosition <= charBufChunk.lastposition)
1351                                 {
1352                                     CharBufferChunkReader charBufChunkReader = (CharBufferChunkReader) charBufChunk
1353                                             .getChunkReader(removeAfterReading);
1354                                     int oldpointer = charBufChunkReader.pointer;
1355                                     // skip the already chars
1356                                     charBufChunkReader.pointer = currentPosition;
1357                                     if (removeAfterReading)
1358                                     {
1359                                         int diff = charBufChunkReader.pointer
1360                                                 - oldpointer;
1361                                         totalCharsInList -= diff;
1362                                         charBufChunk.subtractFromTotalCount();
1363                                     }
1364                                     chunkReader = charBufChunkReader;
1365                                     break;
1366                                 }
1367                             }
1368                         }
1369                         chunk = chunk.prev;
1370                     }
1371                 }
1372             }
1373         }
1374 
1375         @Override
1376         public boolean ready() throws IOException
1377         {
1378             return true;
1379         }
1380 
1381         @Override
1382         public final int read(final char[] b, final int off, final int len)
1383                 throws IOException
1384         {
1385             return readImpl(b, off, len);
1386         }
1387 
1388         final int readImpl(final char[] b, final int off, final int len)
1389                 throws IOException
1390         {
1391             if (b == null)
1392             {
1393                 throw new NullPointerException();
1394             }
1395 
1396             if ((off < 0) || (off > b.length) || (len < 0)
1397                     || ((off + len) > b.length) || ((off + len) < 0))
1398             {
1399                 throw new IndexOutOfBoundsException();
1400             }
1401 
1402             if (len == 0)
1403             {
1404                 return 0;
1405             }
1406 
1407             int charsLeft = len;
1408             int currentOffset = off;
1409             int readChars = prepareRead(charsLeft);
1410             if (eofException)
1411             {
1412                 throw new EOFException();
1413             }
1414 
1415             int totalCharsRead = 0;
1416             while (charsLeft > 0 && readChars > 0)
1417             {
1418                 chunkReader.read(b, currentOffset, readChars);
1419                 charsLeft -= readChars;
1420                 currentOffset += readChars;
1421                 totalCharsRead += readChars;
1422                 if (charsLeft > 0)
1423                 {
1424                     readChars = prepareRead(charsLeft);
1425                 }
1426             }
1427 
1428             if (totalCharsRead > 0)
1429             {
1430                 return totalCharsRead;
1431             }
1432 
1433             eofException = true;
1434             return -1;
1435         }
1436 
1437         @Override
1438         public void close() throws IOException
1439         {
1440             // do nothing
1441         }
1442 
1443         public final StreamCharBuffer getBuffer()
1444         {
1445             return StreamCharBuffer.this;
1446         }
1447 
1448         public int getReadLenLimit(int askedAmount)
1449         {
1450             return prepareRead(askedAmount);
1451         }
1452     }
1453 
1454     abstract class AbstractChunk
1455     {
1456         AbstractChunk next;
1457         AbstractChunk prev;
1458         int writerUsedCounter;
1459 
1460         public AbstractChunk()
1461         {
1462             if (hasReaders)
1463             {
1464                 writerUsedCounter = writer.writerUsedCounter;
1465             }
1466             else
1467             {
1468                 writerUsedCounter = 1;
1469             }
1470         }
1471 
1472         public abstract void writeTo(Writer target) throws IOException;
1473 
1474         public abstract ChunkReader getChunkReader(boolean removeAfterReading);
1475 
1476         public abstract int size();
1477 
1478         public int getWriterUsedCounter()
1479         {
1480             return writerUsedCounter;
1481         }
1482 
1483         public void subtractFromTotalCount()
1484         {
1485             totalCharsInList -= size();
1486         }
1487     }
1488 
1489     // keep read state in this class
1490     static abstract class ChunkReader
1491     {
1492         public abstract int read(char[] ch, int off, int len)
1493                 throws IOException;
1494 
1495         public abstract int getReadLenLimit(int askedAmount);
1496 
1497         public abstract ChunkReader next();
1498 
1499         public abstract int getWriterUsedCounter();
1500 
1501         public abstract boolean isValid();
1502     }
1503 
1504     final class AllocatedBuffer
1505     {
1506         private int id = allocatedBufferIdSequence++;
1507         private int size;
1508         private char[] buffer;
1509         private int used = 0;
1510         private int chunkStart = 0;
1511 
1512         public AllocatedBuffer(int size)
1513         {
1514             this.size = size;
1515             buffer = new char[size];
1516         }
1517 
1518         public int charsUsed()
1519         {
1520             return used - chunkStart;
1521         }
1522 
1523         public void writeTo(Writer target) throws IOException
1524         {
1525             if (used - chunkStart > 0)
1526             {
1527                 target.write(buffer, chunkStart, used - chunkStart);
1528             }
1529         }
1530 
1531         public void reuseBuffer()
1532         {
1533             used = 0;
1534             chunkStart = 0;
1535         }
1536 
1537         public int chunkSize()
1538         {
1539             return buffer.length;
1540         }
1541 
1542         public int spaceLeft()
1543         {
1544             return size - used;
1545         }
1546 
1547         public boolean write(final char ch)
1548         {
1549             if (used < size)
1550             {
1551                 buffer[used++] = ch;
1552                 return true;
1553             }
1554 
1555             return false;
1556         }
1557 
1558         public final void write(final char[] ch, final int off, final int len)
1559         {
1560             arrayCopy(ch, off, buffer, used, len);
1561             used += len;
1562         }
1563 
1564         public final void writeString(final String str, final int off,
1565                 final int len)
1566         {
1567             str.getChars(off, off + len, buffer, used);
1568             used += len;
1569         }
1570 
1571         public final void writeStringBuilder(final StringBuilder stringBuilder,
1572                 final int off, final int len)
1573         {
1574             stringBuilder.getChars(off, off + len, buffer, used);
1575             used += len;
1576         }
1577 
1578         public final void writeStringBuffer(final StringBuffer stringBuffer,
1579                 final int off, final int len)
1580         {
1581             stringBuffer.getChars(off, off + len, buffer, used);
1582             used += len;
1583         }
1584 
1585         /**
1586          * Creates a new chunk from the content written to the buffer 
1587          * (used before adding StringChunk or StreamCharBufferChunk).
1588          *
1589          * @return the chunk
1590          */
1591         public CharBufferChunk createChunk()
1592         {
1593             CharBufferChunk chunk = new CharBufferChunk(id, buffer, chunkStart,
1594                     used - chunkStart);
1595             chunkStart = used;
1596             return chunk;
1597         }
1598 
1599         public boolean hasChunk()
1600         {
1601             return (used > chunkStart);
1602         }
1603 
1604     }
1605 
1606     /**
1607      * The data in the buffer is stored in a linked list of StreamCharBufferChunks.
1608      *
1609      * This class contains data & read/write state for the "chunk level".
1610      * It contains methods for reading & writing to the chunk level.
1611      *
1612      * Underneath the chunk is one more level, the StringChunkGroup + StringChunk.
1613      * StringChunk makes it possible to directly store the java.lang.String objects.
1614      *
1615      * @author Lari Hotari
1616      *
1617      */
1618     final class CharBufferChunk extends AbstractChunk
1619     {
1620         int allocatedBufferId;
1621         char[] buffer;
1622         int offset;
1623         int lastposition;
1624         int length;
1625 
1626         public CharBufferChunk(int allocatedBufferId, char[] buffer,
1627                 int offset, int len)
1628         {
1629             super();
1630             this.allocatedBufferId = allocatedBufferId;
1631             this.buffer = buffer;
1632             this.offset = offset;
1633             this.lastposition = offset + len;
1634             this.length = len;
1635         }
1636 
1637         @Override
1638         public void writeTo(final Writer target) throws IOException
1639         {
1640             target.write(buffer, offset, length);
1641         }
1642 
1643         @Override
1644         public ChunkReader getChunkReader(boolean removeAfterReading)
1645         {
1646             return new CharBufferChunkReader(this, removeAfterReading);
1647         }
1648 
1649         @Override
1650         public int size()
1651         {
1652             return length;
1653         }
1654 
1655         public boolean isSingleBuffer()
1656         {
1657             return offset == 0 && length == buffer.length;
1658         }
1659     }
1660 
1661     abstract class AbstractChunkReader extends ChunkReader
1662     {
1663         private AbstractChunk parentChunk;
1664         private boolean removeAfterReading;
1665 
1666         public AbstractChunkReader(AbstractChunk parentChunk,
1667                 boolean removeAfterReading)
1668         {
1669             this.parentChunk = parentChunk;
1670             this.removeAfterReading = removeAfterReading;
1671         }
1672 
1673         @Override
1674         public boolean isValid()
1675         {
1676             return true;
1677         }
1678 
1679         @Override
1680         public ChunkReader next()
1681         {
1682             if (removeAfterReading)
1683             {
1684                 if (firstChunk == parentChunk)
1685                 {
1686                     firstChunk = null;
1687                 }
1688                 if (lastChunk == parentChunk)
1689                 {
1690                     lastChunk = null;
1691                 }
1692             }
1693             AbstractChunk nextChunk = parentChunk.next;
1694             if (nextChunk != null)
1695             {
1696                 if (removeAfterReading)
1697                 {
1698                     if (firstChunk == null)
1699                     {
1700                         firstChunk = nextChunk;
1701                     }
1702                     if (lastChunk == null)
1703                     {
1704                         lastChunk = nextChunk;
1705                     }
1706                     nextChunk.prev = null;
1707                     nextChunk.subtractFromTotalCount();
1708                 }
1709                 return nextChunk.getChunkReader(removeAfterReading);
1710             }
1711 
1712             return new AllocatedBufferReader(allocBuffer, removeAfterReading);
1713         }
1714 
1715         @Override
1716         public int getWriterUsedCounter()
1717         {
1718             return parentChunk.getWriterUsedCounter();
1719         }
1720     }
1721 
1722     final class CharBufferChunkReader extends AbstractChunkReader
1723     {
1724         CharBufferChunk parent;
1725         int pointer;
1726 
1727         public CharBufferChunkReader(CharBufferChunk parent,
1728                 boolean removeAfterReading)
1729         {
1730             super(parent, removeAfterReading);
1731             this.parent = parent;
1732             this.pointer = parent.offset;
1733         }
1734 
1735         @Override
1736         public int read(final char[] ch, final int off, final int len)
1737                 throws IOException
1738         {
1739             arrayCopy(parent.buffer, pointer, ch, off, len);
1740             pointer += len;
1741             return len;
1742         }
1743 
1744         @Override
1745         public int getReadLenLimit(int askedAmount)
1746         {
1747             return Math.min(parent.lastposition - pointer, askedAmount);
1748         }
1749     }
1750 
1751     /**
1752      * StringChunk is a wrapper for java.lang.String.
1753      *
1754      * It also keeps state of the read offset and the number of unread characters.
1755      *
1756      * There's methods that StringChunkGroup uses for reading data.
1757      *
1758      * @author Lari Hotari
1759      *
1760      */
1761     final class StringChunk extends AbstractChunk
1762     {
1763         String str;
1764         int offset;
1765         int lastposition;
1766         int length;
1767 
1768         public StringChunk(String str, int offset, int length)
1769         {
1770             this.str = str;
1771             this.offset = offset;
1772             this.length = length;
1773             this.lastposition = offset + length;
1774         }
1775 
1776         @Override
1777         public ChunkReader getChunkReader(boolean removeAfterReading)
1778         {
1779             return new StringChunkReader(this, removeAfterReading);
1780         }
1781 
1782         @Override
1783         public void writeTo(Writer target) throws IOException
1784         {
1785             target.write(str, offset, length);
1786         }
1787 
1788         @Override
1789         public int size()
1790         {
1791             return length;
1792         }
1793 
1794         public boolean isSingleBuffer()
1795         {
1796             return offset == 0 && length == str.length();
1797         }
1798     }
1799 
1800     final class StringChunkReader extends AbstractChunkReader
1801     {
1802         StringChunk parent;
1803         int position;
1804 
1805         public StringChunkReader(StringChunk parent, boolean removeAfterReading)
1806         {
1807             super(parent, removeAfterReading);
1808             this.parent = parent;
1809             this.position = parent.offset;
1810         }
1811 
1812         @Override
1813         public int read(final char[] ch, final int off, final int len)
1814         {
1815             parent.str.getChars(position, (position + len), ch, off);
1816             position += len;
1817             return len;
1818         }
1819 
1820         @Override
1821         public int getReadLenLimit(int askedAmount)
1822         {
1823             return Math.min(parent.lastposition - position, askedAmount);
1824         }
1825     }
1826 
1827     final class StreamCharBufferSubChunk extends AbstractChunk
1828     {
1829         StreamCharBuffer streamCharBuffer;
1830         int cachedSize;
1831 
1832         public StreamCharBufferSubChunk(StreamCharBuffer streamCharBuffer)
1833         {
1834             this.streamCharBuffer = streamCharBuffer;
1835             if (totalCharsInDynamicChunks != -1)
1836             {
1837                 cachedSize = streamCharBuffer.size();
1838                 totalCharsInDynamicChunks += cachedSize;
1839             }
1840             else
1841             {
1842                 cachedSize = -1;
1843             }
1844         }
1845 
1846         @Override
1847         public void writeTo(Writer target) throws IOException
1848         {
1849             streamCharBuffer.writeTo(target);
1850         }
1851 
1852         @Override
1853         public ChunkReader getChunkReader(boolean removeAfterReading)
1854         {
1855             return new StreamCharBufferSubChunkReader(this, removeAfterReading);
1856         }
1857 
1858         @Override
1859         public int size()
1860         {
1861             if (cachedSize == -1)
1862             {
1863                 cachedSize = streamCharBuffer.size();
1864             }
1865             return cachedSize;
1866         }
1867 
1868         public boolean hasCachedSize()
1869         {
1870             return (cachedSize != -1);
1871         }
1872 
1873         public StreamCharBuffer getSubBuffer()
1874         {
1875             return this.streamCharBuffer;
1876         }
1877 
1878         public boolean resetSize()
1879         {
1880             if (cachedSize != -1)
1881             {
1882                 cachedSize = -1;
1883                 return true;
1884             }
1885             return false;
1886         }
1887 
1888         @Override
1889         public void subtractFromTotalCount()
1890         {
1891             if (totalCharsInDynamicChunks != -1)
1892             {
1893                 totalCharsInDynamicChunks -= size();
1894             }
1895             dynamicChunkMap.remove(streamCharBuffer.bufferKey);
1896         }
1897     }
1898 
1899     final class StreamCharBufferSubChunkReader extends AbstractChunkReader
1900     {
1901         StreamCharBufferSubChunk parent;
1902         private StreamCharBufferReader reader;
1903 
1904         public StreamCharBufferSubChunkReader(StreamCharBufferSubChunk parent,
1905                 boolean removeAfterReading)
1906         {
1907             super(parent, removeAfterReading);
1908             this.parent = parent;
1909             reader = (StreamCharBufferReader) parent.streamCharBuffer
1910                     .getReader();
1911         }
1912 
1913         @Override
1914         public int getReadLenLimit(int askedAmount)
1915         {
1916             return reader.getReadLenLimit(askedAmount);
1917         }
1918 
1919         @Override
1920         public int read(char[] ch, int off, int len) throws IOException
1921         {
1922             return reader.read(ch, off, len);
1923         }
1924     }
1925 
1926     final class AllocatedBufferReader extends ChunkReader
1927     {
1928         AllocatedBuffer parent;
1929         int position;
1930         int writerUsedCounter;
1931         boolean removeAfterReading;
1932 
1933         public AllocatedBufferReader(AllocatedBuffer parent,
1934                 boolean removeAfterReading)
1935         {
1936             this.parent = parent;
1937             this.position = parent.chunkStart;
1938             if (hasReaders)
1939             {
1940                 writerUsedCounter = writer.writerUsedCounter;
1941             }
1942             else
1943             {
1944                 writerUsedCounter = 1;
1945             }
1946             this.removeAfterReading = removeAfterReading;
1947         }
1948 
1949         @Override
1950         public int getReadLenLimit(int askedAmount)
1951         {
1952             return Math.min(parent.used - position, askedAmount);
1953         }
1954 
1955         @Override
1956         public int read(char[] ch, int off, int len) throws IOException
1957         {
1958             arrayCopy(parent.buffer, position, ch, off, len);
1959             position += len;
1960             if (removeAfterReading)
1961             {
1962                 parent.chunkStart = position;
1963             }
1964             return len;
1965         }
1966 
1967         @Override
1968         public ChunkReader next()
1969         {
1970             return null;
1971         }
1972 
1973         @Override
1974         public int getWriterUsedCounter()
1975         {
1976             return writerUsedCounter;
1977         }
1978 
1979         @Override
1980         public boolean isValid()
1981         {
1982             return (allocBuffer == parent && (lastChunk == null || lastChunk.writerUsedCounter < writerUsedCounter));
1983         }
1984     }
1985 
1986     /**
1987      * Simplified version of a CharArrayWriter used internally in readAsCharArray method.
1988      *
1989      * Doesn't do any bound checks since size shouldn't change during writing in readAsCharArray.
1990      */
1991     private static final class FixedCharArrayWriter extends Writer
1992     {
1993         char buf[];
1994         int count = 0;
1995 
1996         public FixedCharArrayWriter(int fixedSize)
1997         {
1998             buf = new char[fixedSize];
1999         }
2000 
2001         @Override
2002         public void write(char[] cbuf, int off, int len) throws IOException
2003         {
2004             arrayCopy(cbuf, off, buf, count, len);
2005             count += len;
2006         }
2007 
2008         @Override
2009         public void write(char[] cbuf) throws IOException
2010         {
2011             write(cbuf, 0, cbuf.length);
2012         }
2013 
2014         @Override
2015         public void write(String str, int off, int len) throws IOException
2016         {
2017             str.getChars(off, off + len, buf, count);
2018             count += len;
2019         }
2020 
2021         @Override
2022         public void write(String str) throws IOException
2023         {
2024             write(str, 0, str.length());
2025         }
2026 
2027         @Override
2028         public void close() throws IOException
2029         {
2030             // do nothing
2031         }
2032 
2033         @Override
2034         public void flush() throws IOException
2035         {
2036             // do nothing
2037         }
2038 
2039         public char[] getCharArray()
2040         {
2041             return buf;
2042         }
2043     }
2044 
2045     /**
2046      * Interface for a Writer that gets initialized if it is used
2047      * Can be used for passing in to "connectTo" method of StreamCharBuffer
2048      *
2049      * @author Lari Hotari
2050      *
2051      */
2052     public static interface LazyInitializingWriter
2053     {
2054         public Writer getWriter() throws IOException;
2055     }
2056 
2057     /**
2058      * Simple holder class for the connected writer
2059      *
2060      * @author Lari Hotari
2061      *
2062      */
2063     static final class ConnectedWriter
2064     {
2065         Writer writer;
2066         LazyInitializingWriter lazyInitializingWriter;
2067         final boolean autoFlush;
2068 
2069         ConnectedWriter(final Writer writer, final boolean autoFlush)
2070         {
2071             this.writer = writer;
2072             this.autoFlush = autoFlush;
2073         }
2074 
2075         ConnectedWriter(final LazyInitializingWriter lazyInitializingWriter,
2076                 final boolean autoFlush)
2077         {
2078             this.lazyInitializingWriter = lazyInitializingWriter;
2079             this.autoFlush = autoFlush;
2080         }
2081 
2082         Writer getWriter() throws IOException
2083         {
2084             if (writer == null && lazyInitializingWriter != null)
2085             {
2086                 writer = lazyInitializingWriter.getWriter();
2087             }
2088             return writer;
2089         }
2090 
2091         public void flush() throws IOException
2092         {
2093             if (writer != null && isAutoFlush())
2094             {
2095                 writer.flush();
2096             }
2097         }
2098 
2099         public boolean isAutoFlush()
2100         {
2101             return autoFlush;
2102         }
2103     }
2104 
2105     static final class SingleOutputWriter extends Writer
2106     {
2107         private ConnectedWriter writer;
2108 
2109         public SingleOutputWriter(ConnectedWriter writer)
2110         {
2111             this.writer = writer;
2112         }
2113 
2114         @Override
2115         public void close() throws IOException
2116         {
2117             // do nothing
2118         }
2119 
2120         @Override
2121         public void flush() throws IOException
2122         {
2123             writer.flush();
2124         }
2125 
2126         @Override
2127         public void write(final char[] cbuf, final int off, final int len)
2128                 throws IOException
2129         {
2130             writer.getWriter().write(cbuf, off, len);
2131         }
2132 
2133         @Override
2134         public Writer append(final CharSequence csq, final int start,
2135                 final int end) throws IOException
2136         {
2137             writer.getWriter().append(csq, start, end);
2138             return this;
2139         }
2140 
2141         @Override
2142         public void write(String str, int off, int len) throws IOException
2143         {
2144             StringCharArrayAccessor.writeStringAsCharArray(writer.getWriter(),
2145                     str, off, len);
2146         }
2147     }
2148 
2149     /**
2150      * delegates to several writers, used in "connectTo" mode.
2151      *
2152      */
2153     static final class MultiOutputWriter extends Writer
2154     {
2155         final List<ConnectedWriter> writers;
2156 
2157         public MultiOutputWriter(final List<ConnectedWriter> writers)
2158         {
2159             this.writers = writers;
2160         }
2161 
2162         @Override
2163         public void close() throws IOException
2164         {
2165             // do nothing
2166         }
2167 
2168         @Override
2169         public void flush() throws IOException
2170         {
2171             for (ConnectedWriter writer : writers)
2172             {
2173                 writer.flush();
2174             }
2175         }
2176 
2177         @Override
2178         public void write(final char[] cbuf, final int off, final int len)
2179                 throws IOException
2180         {
2181             for (ConnectedWriter writer : writers)
2182             {
2183                 writer.getWriter().write(cbuf, off, len);
2184             }
2185         }
2186 
2187         @Override
2188         public Writer append(final CharSequence csq, final int start,
2189                 final int end) throws IOException
2190         {
2191             for (ConnectedWriter writer : writers)
2192             {
2193                 writer.getWriter().append(csq, start, end);
2194             }
2195             return this;
2196         }
2197 
2198         @Override
2199         public void write(String str, int off, int len) throws IOException
2200         {
2201             for (ConnectedWriter writer : writers)
2202             {
2203                 StringCharArrayAccessor.writeStringAsCharArray(
2204                         writer.getWriter(), str, off, len);
2205             }
2206         }
2207     }
2208 
2209     /* Compatibility methods so that StreamCharBuffer will behave more like java.lang.String in groovy code */
2210 
2211     public char charAt(int index)
2212     {
2213         return toString().charAt(index);
2214     }
2215 
2216     public int length()
2217     {
2218         return size();
2219     }
2220 
2221     public CharSequence subSequence(int start, int end)
2222     {
2223         return toString().subSequence(start, end);
2224     }
2225 
2226     public boolean asBoolean()
2227     {
2228         return isNotEmpty();
2229     }
2230 
2231     /* methods for notifying child (sub) StreamCharBuffer changes to the parent StreamCharBuffer */
2232 
2233     void addParentBuffer(StreamCharBuffer parent)
2234     {
2235         if (parentBuffers == null)
2236         {
2237             parentBuffers = new HashSet<SoftReference<StreamCharBufferKey>>();
2238         }
2239         parentBuffers.add(new SoftReference<StreamCharBufferKey>(
2240                 parent.bufferKey));
2241     }
2242 
2243     boolean bufferChanged(StreamCharBuffer buffer)
2244     {
2245         StreamCharBufferSubChunk subChunk = dynamicChunkMap
2246                 .get(buffer.bufferKey);
2247         if (subChunk == null)
2248         {
2249             // buffer isn't a subchunk in this buffer any more
2250             return false;
2251         }
2252         // reset cached size;
2253         if (subChunk.resetSize())
2254         {
2255             totalCharsInDynamicChunks = -1;
2256             sizeAtLeast = -1;
2257             // notify parents too
2258             notifyBufferChange();
2259         }
2260         return true;
2261     }
2262 
2263     void notifyBufferChange()
2264     {
2265         if (parentBuffers == null)
2266         {
2267             return;
2268         }
2269 
2270         for (Iterator<SoftReference<StreamCharBufferKey>> i = parentBuffers
2271                 .iterator(); i.hasNext();)
2272         {
2273             SoftReference<StreamCharBufferKey> ref = i.next();
2274             final StreamCharBuffer.StreamCharBufferKey parentKey = ref.get();
2275             boolean removeIt = true;
2276             if (parentKey != null)
2277             {
2278                 StreamCharBuffer parent = parentKey.getBuffer();
2279                 removeIt = !parent.bufferChanged(this);
2280             }
2281             if (removeIt)
2282             {
2283                 i.remove();
2284             }
2285         }
2286     }
2287 
2288     public void readExternal(ObjectInput in) throws IOException,
2289             ClassNotFoundException
2290     {
2291         String str = in.readUTF();
2292         reset();
2293         if (str.length() > 0)
2294         {
2295             addChunk(new StringChunk(str, 0, str.length()));
2296         }
2297     }
2298 
2299     public void writeExternal(ObjectOutput out) throws IOException
2300     {
2301         String str = toString();
2302         out.writeUTF(str);
2303     }
2304 
2305 }