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