View Javadoc

1   /*
2    *  jDTAUS Core Utilities
3    *  Copyright (C) 2005 Christian Schulte
4    *  <cs@schulte.it>
5    *
6    *  This library is free software; you can redistribute it and/or
7    *  modify it under the terms of the GNU Lesser General Public
8    *  License as published by the Free Software Foundation; either
9    *  version 2.1 of the License, or any later version.
10   *
11   *  This library is distributed in the hope that it will be useful,
12   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   *  Lesser General Public License for more details.
15   *
16   *  You should have received a copy of the GNU Lesser General Public
17   *  License along with this library; if not, write to the Free Software
18   *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
19   *
20   */
21  package org.jdtaus.core.io.util;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.util.Iterator;
27  import java.util.Locale;
28  import java.util.Map;
29  import java.util.TreeMap;
30  import org.jdtaus.core.container.ContainerFactory;
31  import org.jdtaus.core.io.FileOperations;
32  import org.jdtaus.core.lang.spi.MemoryManager;
33  import org.jdtaus.core.logging.spi.Logger;
34  
35  /**
36   * Coalescing {@code FileOperations} cache.
37   * <p>This implementation implements a coalescing cache for
38   * {@code FileOperations} implementations. The cache is controlled by
39   * configuration property {@code blockSize}. By default property
40   * {@code blockSize} is initialized to {@code 2097152} leading to a cache
41   * size of 10 MB (multiplied by property {@code cacheSize} which defaults to
42   * {@code 5}). All memory is allocated during instantiation so that an
43   * {@code OutOfMemoryError} may be thrown when constructing the cache but not
44   * when working with the instance.</p>
45   *
46   * <p><b>Note:</b><br>
47   * This implementation is not thread-safe and concurrent changes to the
48   * underlying {@code FileOperations} implementation are not supported.</p>
49   *
50   * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
51   * @version $JDTAUS: CoalescingFileOperations.java 8743 2012-10-07 03:06:20Z schulte $
52   */
53  public final class CoalescingFileOperations implements FlushableFileOperations
54  {
55      //--Dependencies------------------------------------------------------------
56  
57  // <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausDependencies
58      // This section is managed by jdtaus-container-mojo.
59  
60      /**
61       * Gets the configured <code>MemoryManager</code> implementation.
62       *
63       * @return The configured <code>MemoryManager</code> implementation.
64       */
65      private MemoryManager getMemoryManager()
66      {
67          return (MemoryManager) ContainerFactory.getContainer().
68              getDependency( this, "MemoryManager" );
69  
70      }
71  
72      /**
73       * Gets the configured <code>Locale</code> implementation.
74       *
75       * @return The configured <code>Locale</code> implementation.
76       */
77      private Locale getLocale()
78      {
79          return (Locale) ContainerFactory.getContainer().
80              getDependency( this, "Locale" );
81  
82      }
83  
84      /**
85       * Gets the configured <code>Logger</code> implementation.
86       *
87       * @return The configured <code>Logger</code> implementation.
88       */
89      private Logger getLogger()
90      {
91          return (Logger) ContainerFactory.getContainer().
92              getDependency( this, "Logger" );
93  
94      }
95  
96  // </editor-fold>//GEN-END:jdtausDependencies
97  
98      //------------------------------------------------------------Dependencies--
99      //--Properties--------------------------------------------------------------
100 
101 // <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausProperties
102     // This section is managed by jdtaus-container-mojo.
103 
104     /**
105      * Gets the value of property <code>defaultCacheBlocks</code>.
106      *
107      * @return Default number of cache blocks.
108      */
109     private java.lang.Integer getDefaultCacheBlocks()
110     {
111         return (java.lang.Integer) ContainerFactory.getContainer().
112             getProperty( this, "defaultCacheBlocks" );
113 
114     }
115 
116     /**
117      * Gets the value of property <code>defaultBlockSize</code>.
118      *
119      * @return Default size of one cache block in byte.
120      */
121     private java.lang.Integer getDefaultBlockSize()
122     {
123         return (java.lang.Integer) ContainerFactory.getContainer().
124             getProperty( this, "defaultBlockSize" );
125 
126     }
127 
128 // </editor-fold>//GEN-END:jdtausProperties
129 
130     //--------------------------------------------------------------Properties--
131     //--FileOperations----------------------------------------------------------
132 
133     public long getLength() throws IOException
134     {
135         this.assertNotClosed();
136 
137         return this.getFileOperations().getLength();
138     }
139 
140     public void setLength( final long newLength ) throws IOException
141     {
142         this.assertNotClosed();
143 
144         // Update the length of any cache nodes involved in the operation.
145         final long oldLength = this.getLength();
146         if ( newLength > oldLength )
147         {
148             final long delta = newLength - oldLength;
149 
150             assert delta <= Integer.MAX_VALUE :
151                 "Unexpected implementation limit reached.";
152 
153             final Node[] nodes =
154                 this.getCacheNodesForLength( oldLength, (int) delta );
155 
156             for ( int i = 0; i < nodes.length; i++ )
157             {
158                 final long startPos = nodes[i].block * this.getBlockSize();
159                 final long blockDelta = newLength - startPos;
160 
161                 assert blockDelta <= Integer.MAX_VALUE :
162                     "Unexpected implementation limit reached.";
163 
164                 nodes[i].length = blockDelta >= this.getBlockSize()
165                                   ? this.getBlockSize()
166                                   : (int) blockDelta;
167 
168             }
169         }
170         else if ( newLength < oldLength )
171         {
172             final long delta = oldLength - newLength;
173 
174             assert delta <= Integer.MAX_VALUE :
175                 "Unexpected implementation limit reached.";
176 
177             final Node[] nodes =
178                 this.getCacheNodesForLength( newLength, (int) delta );
179 
180             for ( int i = 0; i < nodes.length; i++ )
181             {
182                 final long startPos = nodes[i].block * this.getBlockSize();
183                 if ( startPos > newLength )
184                 { // Discard the block.
185                     this.root.remove( new Long( nodes[i].block ) );
186                 }
187                 else
188                 { // Update the blocks length.
189                     final long blockDelta = newLength - startPos;
190 
191                     assert blockDelta <= Integer.MAX_VALUE :
192                         "Unexpected implementation limit reached.";
193 
194                     nodes[i].length = blockDelta >= this.getBlockSize()
195                                       ? this.getBlockSize()
196                                       : (int) blockDelta;
197 
198                 }
199             }
200         }
201 
202         this.getFileOperations().setLength( newLength );
203 
204         if ( this.filePointer > newLength )
205         {
206             this.filePointer = newLength;
207         }
208     }
209 
210     public long getFilePointer() throws IOException
211     {
212         this.assertNotClosed();
213 
214         return this.filePointer;
215     }
216 
217     public void setFilePointer( final long pos ) throws IOException
218     {
219         this.assertNotClosed();
220 
221         this.filePointer = pos;
222     }
223 
224     public int read( final byte[] buf, int off, int len ) throws IOException
225     {
226         if ( buf == null )
227         {
228             throw new NullPointerException( "buf" );
229         }
230         if ( off < 0 )
231         {
232             throw new IndexOutOfBoundsException( Integer.toString( off ) );
233         }
234         if ( len < 0 )
235         {
236             throw new IndexOutOfBoundsException( Integer.toString( len ) );
237         }
238         if ( off + len > buf.length )
239         {
240             throw new IndexOutOfBoundsException( Integer.toString( off + len ) );
241         }
242 
243         this.assertNotClosed();
244 
245         int read = FileOperations.EOF;
246 
247         if ( len == 0 )
248         {
249             read = 0;
250         }
251         else if ( this.filePointer < this.getLength() )
252         { // End of file not reached.
253             final Node[] nodes =
254                 this.getCacheNodesForLength( this.filePointer, len );
255 
256             // Ensure cache holds the data of the involved blocks.
257             this.fillCache( nodes );
258 
259             int copied = 0;
260             for ( int i = 0; i < nodes.length; i++ )
261             {
262                 if ( nodes[i].length == FileOperations.EOF )
263                 { // Skip any end of file nodes.
264                     continue;
265                 }
266 
267                 if ( nodes[i].cacheIndex != Node.NO_CACHEINDEX )
268                 { // Node is associated with cache memory; cache is used.
269 
270                     // Use the current file pointer as the starting index.
271                     final long delta =
272                         nodes[i].cacheIndex +
273                         ( this.filePointer - nodes[i].block *
274                                              this.getBlockSize() );
275 
276                     assert delta <= Integer.MAX_VALUE :
277                         "Unexpected implementation limit reached.";
278 
279                     final int blockOffset = (int) delta;
280                     final int blockDelta = nodes[i].length -
281                                            ( blockOffset - nodes[i].cacheIndex );
282 
283                     final int copyLength = len > blockDelta
284                                            ? blockDelta
285                                            : len;
286 
287                     System.arraycopy( this.getCache(), blockOffset, buf, off,
288                                       copyLength );
289 
290                     off += copyLength;
291                     len -= copyLength;
292                     copied += copyLength;
293                     this.filePointer += copyLength;
294                 }
295                 else
296                 { // Node is not associated with cache memory; read directly.
297                     this.getFileOperations().setFilePointer( this.filePointer );
298                     copied += this.getFileOperations().read( buf, off, len );
299                     this.filePointer += len;
300 
301                     this.getLogger().debug(
302                         this.getReadBypassesCacheMessage(
303                         this.getLocale(),
304                         new Integer( this.getBlockSize() ),
305                         new Integer( this.getCacheBlocks() ),
306                         new Integer( len ) ) );
307 
308 
309                     break;
310                 }
311             }
312 
313             read = copied;
314         }
315 
316         return read;
317     }
318 
319     public void write( final byte[] buf, int off, int len ) throws IOException
320     {
321         if ( buf == null )
322         {
323             throw new NullPointerException( "buf" );
324         }
325         if ( off < 0 )
326         {
327             throw new IndexOutOfBoundsException( Integer.toString( off ) );
328         }
329         if ( len < 0 )
330         {
331             throw new IndexOutOfBoundsException( Integer.toString( len ) );
332         }
333         if ( off + len > buf.length )
334         {
335             throw new IndexOutOfBoundsException( Integer.toString( off + len ) );
336         }
337 
338         this.assertNotClosed();
339 
340         if ( this.filePointer + len > this.getLength() )
341         { // Expand the file of the backing instance.
342             this.setLength( this.filePointer + len );
343         }
344 
345         final Node[] nodes =
346             this.getCacheNodesForLength( this.filePointer, len );
347 
348         // Ensure cache holds the data of the involved blocks.
349         this.fillCache( nodes );
350 
351         for ( int i = 0; i < nodes.length; i++ )
352         {
353             // Check for correct file length update.
354             assert nodes[i].length != FileOperations.EOF :
355                 "Unexpected cache state.";
356 
357             if ( nodes[i].cacheIndex != Node.NO_CACHEINDEX )
358             { // Node is associated with cache memory; cache is used.
359 
360                 // Use the current file pointer as the starting index.
361                 final long delta = nodes[i].cacheIndex +
362                                    ( this.filePointer - nodes[i].block *
363                                                         this.getBlockSize() );
364 
365                 assert delta <= Integer.MAX_VALUE :
366                     "Unexpected implementation limit reached.";
367 
368                 final int blockOffset = (int) delta;
369                 final int blockDelta = nodes[i].length -
370                                        ( blockOffset - nodes[i].cacheIndex );
371 
372                 final int copyLength = len > blockDelta
373                                        ? blockDelta
374                                        : len;
375 
376                 System.arraycopy( buf, off, this.getCache(), blockOffset,
377                                   copyLength );
378 
379                 off += copyLength;
380                 len -= copyLength;
381                 this.filePointer += copyLength;
382                 nodes[i].dirty = true;
383             }
384             else
385             { // Node is not associated with cache memory; write out directly.
386                 this.getFileOperations().setFilePointer( this.filePointer );
387                 this.getFileOperations().write( buf, off, len );
388                 this.filePointer += len;
389 
390                 this.getLogger().debug(
391                     this.getWriteBypassesCacheMessage(
392                     this.getLocale(),
393                     new Integer( this.getBlockSize() ),
394                     new Integer( this.getCacheBlocks() ),
395                     new Integer( len ) ) );
396 
397 
398                 break;
399             }
400         }
401     }
402 
403     public void read( final OutputStream out ) throws IOException
404     {
405         this.assertNotClosed();
406 
407         this.getFileOperations().read( out );
408         this.filePointer = this.getFileOperations().getFilePointer();
409     }
410 
411     public void write( final InputStream in ) throws IOException
412     {
413         this.assertNotClosed();
414 
415         this.getFileOperations().write( in );
416         this.filePointer = this.getFileOperations().getFilePointer();
417     }
418 
419     /**
420      * {@inheritDoc}
421      * Flushes the cache and closes the {@code FileOperations} implementation
422      * backing the instance.
423      *
424      * @throws IOException if flushing or closing the {@code FileOperations}
425      * implementation backing the instance fails, or if the instance already
426      * is closed.
427      */
428     public void close() throws IOException
429     {
430         this.assertNotClosed();
431 
432         this.flush();
433         this.getFileOperations().close();
434         this.closed = true;
435     }
436 
437     //----------------------------------------------------------FileOperations--
438     //--FlushableFileOperations-------------------------------------------------
439 
440     /**
441      * {@inheritDoc}
442      * This method calls the {@code flush()} method of an underlying
443      * {@code FlushableFileOperations} implementation, if any.
444      *
445      * @throws IOException if writing any pending changes fails or if the
446      * instance is closed.
447      */
448     public void flush() throws IOException
449     {
450         this.assertNotClosed();
451 
452         this.defragmentCache();
453 
454         long startPos = FileOperations.EOF;
455         int startIndex = FileOperations.EOF;
456         int length = FileOperations.EOF;
457         Node previous = null;
458         boolean dirty = false;
459 
460         for ( final Iterator it = this.root.entrySet().iterator();
461               it.hasNext(); )
462         {
463             final Map.Entry entry = (Map.Entry) it.next();
464             final long block = ( (Long) entry.getKey() ).longValue();
465             final Node current = (Node) entry.getValue();
466 
467             // Skip any end of file nodes and nodes not associated with memory.
468             if ( current.length == FileOperations.EOF ||
469                  current.cacheIndex == Node.NO_CACHEINDEX )
470             {
471                 continue;
472             }
473 
474             assert current.block == block : "Unexpected cache state.";
475 
476             if ( previous == null )
477             { // Start the first chunk.
478                 previous = current;
479                 startPos = current.block * this.getBlockSize();
480                 startIndex = current.cacheIndex;
481                 length = current.length;
482                 dirty = current.dirty;
483             }
484             else if ( current.block == previous.block + 1L )
485             { // Expand the current chunk.
486 
487                 assert current.cacheIndex == previous.cacheIndex +
488                                              this.getBlockSize() :
489                     "Unexpected cache state.";
490 
491                 previous = current;
492                 length += current.length;
493                 if ( !dirty )
494                 {
495                     dirty = current.dirty;
496                 }
497             }
498             else
499             { // Write out the current chunk and start a new one.
500                 if ( dirty )
501                 {
502                     this.getFileOperations().setFilePointer( startPos );
503                     this.getFileOperations().write(
504                         this.getCache(), startIndex, length );
505 
506                 }
507 
508                 previous = current;
509                 startPos = current.block * this.getBlockSize();
510                 startIndex = current.cacheIndex;
511                 length = current.length;
512                 dirty = current.dirty;
513             }
514         }
515 
516         if ( dirty )
517         { // Write the remaining chunk.
518             this.getFileOperations().setFilePointer( startPos );
519             this.getFileOperations().write(
520                 this.getCache(), startIndex, length );
521 
522         }
523 
524         // Reset cache state.
525         for ( final Iterator it = this.root.entrySet().iterator();
526               it.hasNext(); )
527         {
528             final Map.Entry entry = (Map.Entry) it.next();
529             final Node current = (Node) entry.getValue();
530 
531             current.cacheIndex = Node.NO_CACHEINDEX;
532             current.dirty = false;
533         }
534 
535         this.nextCacheIndex = 0;
536 
537         if ( this.getFileOperations() instanceof FlushableFileOperations )
538         { // Cache of the backing instance also needs to get flushed.
539             ( (FlushableFileOperations) this.getFileOperations() ).flush();
540         }
541     }
542 
543     //-------------------------------------------------FlushableFileOperations--
544     //--CoalescingFileOperations------------------------------------------------
545 
546     /** Node describing a cache block. */
547     private static final class Node
548     {
549 
550         private static final int NO_CACHEINDEX = Integer.MIN_VALUE;
551 
552         private Node()
553         {
554             super();
555         }
556 
557         private long block;
558 
559         private int cacheIndex = NO_CACHEINDEX;
560 
561         private int length;
562 
563         private boolean dirty;
564 
565     }
566 
567     /** The {@code FileOperations} backing the instance. */
568     private final FileOperations fileOperations;
569 
570     /** Cached blocks. */
571     private byte[] cache;
572 
573     /** Second cache memory used during defragmentation. */
574     private byte[] defragCache;
575 
576     /** Index of the next free cached block. */
577     private int nextCacheIndex;
578 
579     /** Maps blocks to corresponding {@code Node}s. */
580     private final Map root = new TreeMap();
581 
582     /** File pointer. */
583     private long filePointer;
584 
585     /** Caches the value returned by method {@code getFilePointerBlock}. */
586     private long cachedFilePointerBlock = NO_FILEPOINTERBLOCK;
587 
588     private long cachedFilePointerBlockStart = FileOperations.EOF;
589 
590     private static final long NO_FILEPOINTERBLOCK = Long.MIN_VALUE;
591 
592     /** Flags the instance as beeing closed. */
593     private boolean closed;
594 
595     /** The number of bytes occupied by one cache block. */
596     private Integer blockSize;
597 
598     /** The number of cache blocks. */
599     private Integer cacheBlocks;
600 
601     /**
602      * Creates a new {@code CoalescingFileOperations} instance taking the
603      * {@code FileOperations} backing the instance.
604      *
605      * @param fileOperations the {@code FileOperations} backing the instance.
606      *
607      * @throws NullPointerException if {@code fileOperations} is {@code null}.
608      * @throws IOException if reading fails.
609      */
610     public CoalescingFileOperations( final FileOperations fileOperations )
611         throws IOException
612     {
613         super();
614 
615         if ( fileOperations == null )
616         {
617             throw new NullPointerException( "fileOperations" );
618         }
619 
620         this.fileOperations = fileOperations;
621         this.filePointer = fileOperations.getFilePointer();
622     }
623 
624     /**
625      * Creates a new {@code CoalescingFileOperations} instance taking the
626      * {@code FileOperations} backing the instance and the number of bytes
627      * occupied by one cache block.
628      *
629      * @param fileOperations the {@code FileOperations} backing the instance.
630      * @param blockSize the number of bytes occupied by one cache block.
631      *
632      * @throws NullPointerException if {@code fileOperations} is {@code null}.
633      * @throws IOException if reading fails.
634      */
635     public CoalescingFileOperations( final FileOperations fileOperations,
636                                      final int blockSize ) throws IOException
637     {
638         this( fileOperations );
639         if ( blockSize > 0 )
640         {
641             this.blockSize = new Integer( blockSize );
642         }
643     }
644 
645     /**
646      * Creates a new {@code CoalescingFileOperations} instance taking the
647      * {@code FileOperations} backing the instance, the number of bytes
648      * occupied by one cache block and the number of cache blocks.
649      *
650      * @param fileOperations the {@code FileOperations} backing the instance.
651      * @param blockSize the number of bytes occupied by one cache block.
652      * @param cacheBlocks number of cache blocks.
653      *
654      * @throws NullPointerException if {@code fileOperations} is {@code null}.
655      * @throws IOException if reading fails.
656      */
657     public CoalescingFileOperations( final FileOperations fileOperations,
658                                      final int blockSize,
659                                      final int cacheBlocks )
660         throws IOException
661     {
662         this( fileOperations, blockSize );
663         if ( cacheBlocks > 0 )
664         {
665             this.cacheBlocks = new Integer( cacheBlocks );
666         }
667     }
668 
669     /**
670      * Gets the {@code FileOperations} implementation operations are performed
671      * with.
672      *
673      * @return the {@code FileOperations} implementation operations are
674      * performed with.
675      */
676     public FileOperations getFileOperations()
677     {
678         return this.fileOperations;
679     }
680 
681     /**
682      * Gets the number of bytes occupied by one cache block.
683      *
684      * @return the number of bytes occupied by one cache block.
685      */
686     public int getBlockSize()
687     {
688         if ( this.blockSize == null )
689         {
690             this.blockSize = this.getDefaultBlockSize();
691         }
692 
693         return this.blockSize.intValue();
694     }
695 
696     /**
697      * Gets the number of blocks in the cache.
698      *
699      * @return the number of blocks in the cache.
700      */
701     public int getCacheBlocks()
702     {
703         if ( this.cacheBlocks == null )
704         {
705             this.cacheBlocks = this.getDefaultCacheBlocks();
706         }
707 
708         return this.cacheBlocks.intValue();
709     }
710 
711     /**
712      * Gets the cache buffer.
713      *
714      * @return the cache buffer.
715      */
716     private byte[] getCache()
717     {
718         if ( this.cache == null )
719         {
720             this.cache = this.getMemoryManager().allocateBytes(
721                 this.getBlockSize() * this.getCacheBlocks() );
722 
723         }
724 
725         return this.cache;
726     }
727 
728     /**
729      * Gets the buffer used during defragmentation of the cache.
730      *
731      * @return the buffer used during defragmentation of the cache.
732      */
733     private byte[] getDefragCache()
734     {
735         if ( this.defragCache == null )
736         {
737             this.defragCache = this.getMemoryManager().allocateBytes(
738                 this.getBlockSize() * this.getCacheBlocks() );
739 
740         }
741 
742         return this.defragCache;
743     }
744 
745     /**
746      * Gets the block pointed to by a given file pointer value.
747      *
748      * @param filePointer the file pointer value for which to return the
749      * corresponding block.
750      *
751      * @return the block pointed to by {@code filePointer}.
752      */
753     private long getFilePointerBlock( final long filePointer )
754     {
755         if ( this.cachedFilePointerBlock == NO_FILEPOINTERBLOCK )
756         {
757             this.cachedFilePointerBlock =
758                 ( filePointer / this.getBlockSize() ) -
759                 ( ( filePointer % this.getBlockSize() ) / this.getBlockSize() );
760 
761             this.cachedFilePointerBlockStart =
762                 this.cachedFilePointerBlock * this.getBlockSize();
763 
764         }
765         else
766         {
767             if ( !( filePointer >= this.cachedFilePointerBlockStart &&
768                     filePointer <= this.cachedFilePointerBlockStart +
769                                    this.getBlockSize() ) )
770             {
771                 this.cachedFilePointerBlock =
772                     ( filePointer / this.getBlockSize() ) -
773                     ( ( filePointer % this.getBlockSize() ) /
774                       this.getBlockSize() );
775 
776                 this.cachedFilePointerBlockStart =
777                     this.cachedFilePointerBlock * this.getBlockSize();
778 
779             }
780         }
781 
782         return this.cachedFilePointerBlock;
783     }
784 
785     /**
786      * Gets the cache nodes for all blocks involved in a read or write operation
787      * of a given length for a given file pointer value.
788      *
789      * @param filePointer the file pointer value to use for computing the
790      * number of involved blocks for a read or write operation of
791      * {@code length}.
792      * @param length the length of the operation to perform.
793      *
794      * @return an array of cache nodes for all blocks involved in the
795      * operation in the order corresponding to the operation's needs.
796      */
797     private Node[] getCacheNodesForLength( final long filePointer,
798                                            final int length )
799     {
800         final long startingBlock = this.getFilePointerBlock( filePointer );
801         final long endingBlock =
802             this.getFilePointerBlock( filePointer + length );
803 
804         assert endingBlock - startingBlock <= Integer.MAX_VALUE :
805             "Unexpected implementation limit reached.";
806 
807         final Node[] nodes =
808             new Node[ (int) ( endingBlock - startingBlock + 1L ) ];
809 
810         if ( startingBlock == endingBlock )
811         {
812             nodes[0] = this.getCacheNode( startingBlock );
813         }
814         else
815         {
816             int i;
817             long block;
818 
819             for ( block = startingBlock, i = 0; block <= endingBlock;
820                   block++, i++ )
821             {
822                 nodes[i] = this.getCacheNode( block );
823             }
824         }
825 
826         return nodes;
827     }
828 
829     /**
830      * Fills the cache for a given set of cache nodes.
831      * <p>This method ensures that each given node gets associated with
832      * corresponding cache memory possibly flushing the cache before
833      * reading.</p>
834      *
835      * @param nodes the nodes to fill the cache for.
836      *
837      * @throws NullPointerException if {@code nodes} is {@code null}.
838      * @throws IOException if reading fails.
839      */
840     private void fillCache( final Node[] nodes ) throws IOException
841     {
842         if ( nodes == null )
843         {
844             throw new NullPointerException( "nodes" );
845         }
846 
847         // Calculate the amount of bytes needed to be available in the cache
848         // and flush the cache if nodes would not fit.
849         long neededBytes = 0L;
850         for ( int i = nodes.length - 1; i >= 0; i-- )
851         {
852             if ( nodes[i].cacheIndex == Node.NO_CACHEINDEX )
853             { // Node's block needs to be read.
854                 neededBytes += this.getBlockSize();
855             }
856         }
857 
858         if ( this.nextCacheIndex + neededBytes > this.getCache().length )
859         { // Cache cannot hold the needed blocks so needs flushing.
860             this.flush();
861         }
862 
863         // Associate each node with cache memory for nodes not already
864         // associated with cache memory and fill these nodes' cache memory.
865         for ( int i = nodes.length - 1; i >= 0; i-- )
866         {
867             if ( nodes[i].cacheIndex == Node.NO_CACHEINDEX &&
868                  this.nextCacheIndex < this.getCache().length )
869             { // Node is not associated with any cache memory and can be read.
870 
871                 // Update the length field of the node for the block checking
872                 // for a possible end of file condition.
873                 final long pos = nodes[i].block * this.getBlockSize();
874                 if ( pos > this.getLength() )
875                 { // Node is behind the end of the file.
876                     nodes[i].length = FileOperations.EOF;
877                     continue;
878                 }
879                 else if ( pos + this.getBlockSize() > this.getLength() )
880                 {
881                     final long delta = this.getLength() - pos;
882 
883                     assert delta <= Integer.MAX_VALUE :
884                         "Unexpected implementation limit reached.";
885 
886                     nodes[i].length = (int) delta;
887                 }
888                 else
889                 {
890                     nodes[i].length = this.getBlockSize();
891                 }
892 
893                 // Associated the node with cache memory.
894                 nodes[i].cacheIndex = this.nextCacheIndex;
895                 this.nextCacheIndex += this.getBlockSize();
896 
897                 // Read the node's block into cache.
898                 int read = FileOperations.EOF;
899                 int totalRead = 0;
900                 int toRead = nodes[i].length;
901                 this.getFileOperations().setFilePointer( pos );
902 
903                 do
904                 {
905                     read = this.getFileOperations().read(
906                         this.getCache(), nodes[i].cacheIndex + totalRead,
907                         toRead );
908 
909                     assert read != FileOperations.EOF :
910                         "Unexpected end of file.";
911 
912                     totalRead += read;
913                     toRead -= read;
914 
915                 }
916                 while ( totalRead < nodes[i].length );
917             }
918         }
919     }
920 
921     /** Defragments the cache. */
922     private void defragmentCache()
923     {
924         int defragIndex = 0;
925 
926         // Step through the cached blocks and defragment the cache.
927         for ( final Iterator it = this.root.entrySet().iterator();
928               it.hasNext(); )
929         {
930             final Map.Entry entry = (Map.Entry) it.next();
931             final long block = ( (Long) entry.getKey() ).longValue();
932             final Node current = (Node) entry.getValue();
933 
934             // Skip any end of file nodes and nodes not associated with memory.
935             if ( current.length == FileOperations.EOF ||
936                  current.cacheIndex == Node.NO_CACHEINDEX )
937             {
938                 continue;
939             }
940 
941             assert current.block == block : "Unexpected cache state.";
942 
943             System.arraycopy( this.getCache(), current.cacheIndex,
944                               this.getDefragCache(), defragIndex,
945                               this.getBlockSize() );
946 
947             current.cacheIndex = defragIndex;
948             defragIndex += this.getBlockSize();
949         }
950 
951         System.arraycopy( this.getDefragCache(), 0, this.getCache(), 0,
952                           this.getCache().length );
953 
954     }
955 
956     /**
957      * Gets the cache node for a given block.
958      *
959      * @param block the block to return the corresponding cache node for.
960      *
961      * @return the cache node for {@code block}.
962      */
963     private Node getCacheNode( final long block )
964     {
965         final Long key = new Long( block );
966         Node node = (Node) this.root.get( key );
967         if ( node == null )
968         {
969             node = new Node();
970             node.block = block;
971             this.root.put( key, node );
972         }
973 
974         return node;
975     }
976 
977     /**
978      * Checks that the instance is not closed.
979      *
980      * @throws IOException if the instance is closed.
981      */
982     private void assertNotClosed() throws IOException
983     {
984         if ( this.closed )
985         {
986             throw new IOException( this.getAlreadyClosedMessage(
987                 this.getLocale() ) );
988 
989         }
990     }
991 
992     //------------------------------------------------CoalescingFileOperations--
993     //--Messages----------------------------------------------------------------
994 
995 // <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausMessages
996     // This section is managed by jdtaus-container-mojo.
997 
998     /**
999      * Gets the text of message <code>readBypassesCache</code>.
1000      * <blockquote><pre>Eine Lese-Operation umging den Cache. Cache zu klein dimensioniert. Aktuelle Blockgröße ist {0,number} und Cache umfaßt {1,number} Blöcke. {2,number} Bytes konnten nicht zwischengespeichert werden.</pre></blockquote>
1001      * <blockquote><pre>A read operation bypassed the cache. Consider increasing the cache. Current block size is {0,number} and current number of cache blocks is {1,number}. {2,number} bytes could not be cached.</pre></blockquote>
1002      *
1003      * @param locale The locale of the message instance to return.
1004      * @param blockSize The current block size in use.
1005      * @param cacheBlocks The current number of blocks in use.
1006      * @param uncachedBytes The number of bytes bypassing caching.
1007      *
1008      * @return Information about a misconfigured cache.
1009      */
1010     private String getReadBypassesCacheMessage( final Locale locale,
1011             final java.lang.Number blockSize,
1012             final java.lang.Number cacheBlocks,
1013             final java.lang.Number uncachedBytes )
1014     {
1015         return ContainerFactory.getContainer().
1016             getMessage( this, "readBypassesCache", locale,
1017                 new Object[]
1018                 {
1019                     blockSize,
1020                     cacheBlocks,
1021                     uncachedBytes
1022                 });
1023 
1024     }
1025 
1026     /**
1027      * Gets the text of message <code>writeBypassesCache</code>.
1028      * <blockquote><pre>Eine Schreib-Operation umging den Cache. Cache zu klein dimensioniert. Aktuelle Blockgröße ist {0,number} und Cache umfaßt {1,number} Blöcke. {2,number} Bytes konnten nicht zwischengespeichert werden.</pre></blockquote>
1029      * <blockquote><pre>A write operation bypassed the cache. Consider increasing the cache. Current block size is {0,number} and current number of cache blocks is {1,number}. {2,number} bytes could not be cached.</pre></blockquote>
1030      *
1031      * @param locale The locale of the message instance to return.
1032      * @param blockSize The current block size in use.
1033      * @param cacheBlocks The current number of blocks in use.
1034      * @param uncachedBytes The number of bytes bypassing caching.
1035      *
1036      * @return Information about a misconfigured cache.
1037      */
1038     private String getWriteBypassesCacheMessage( final Locale locale,
1039             final java.lang.Number blockSize,
1040             final java.lang.Number cacheBlocks,
1041             final java.lang.Number uncachedBytes )
1042     {
1043         return ContainerFactory.getContainer().
1044             getMessage( this, "writeBypassesCache", locale,
1045                 new Object[]
1046                 {
1047                     blockSize,
1048                     cacheBlocks,
1049                     uncachedBytes
1050                 });
1051 
1052     }
1053 
1054     /**
1055      * Gets the text of message <code>alreadyClosed</code>.
1056      * <blockquote><pre>Instanz geschlossen - keine E/A-Operationen möglich.</pre></blockquote>
1057      * <blockquote><pre>Instance closed - cannot perform I/O.</pre></blockquote>
1058      *
1059      * @param locale The locale of the message instance to return.
1060      *
1061      * @return Message stating that an instance is already closed.
1062      */
1063     private String getAlreadyClosedMessage( final Locale locale )
1064     {
1065         return ContainerFactory.getContainer().
1066             getMessage( this, "alreadyClosed", locale, null );
1067 
1068     }
1069 
1070 // </editor-fold>//GEN-END:jdtausMessages
1071 
1072     //----------------------------------------------------------------Messages--
1073 }