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 8641 2012-09-27 06:45:17Z 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 ( Iterator it = this.root.entrySet().iterator(); it.hasNext(); )
461         {
462             final Map.Entry entry = (Map.Entry) it.next();
463             final long block = ( (Long) entry.getKey() ).longValue();
464             final Node current = (Node) entry.getValue();
465 
466             // Skip any end of file nodes and nodes not associated with memory.
467             if ( current.length == FileOperations.EOF ||
468                  current.cacheIndex == Node.NO_CACHEINDEX )
469             {
470                 continue;
471             }
472 
473             assert current.block == block : "Unexpected cache state.";
474 
475             if ( previous == null )
476             { // Start the first chunk.
477                 previous = current;
478                 startPos = current.block * this.getBlockSize();
479                 startIndex = current.cacheIndex;
480                 length = current.length;
481                 dirty = current.dirty;
482             }
483             else if ( current.block == previous.block + 1L )
484             { // Expand the current chunk.
485 
486                 assert current.cacheIndex == previous.cacheIndex +
487                                              this.getBlockSize() :
488                     "Unexpected cache state.";
489 
490                 previous = current;
491                 length += current.length;
492                 if ( !dirty )
493                 {
494                     dirty = current.dirty;
495                 }
496             }
497             else
498             { // Write out the current chunk and start a new one.
499                 if ( dirty )
500                 {
501                     this.getFileOperations().setFilePointer( startPos );
502                     this.getFileOperations().write(
503                         this.getCache(), startIndex, length );
504 
505                 }
506 
507                 previous = current;
508                 startPos = current.block * this.getBlockSize();
509                 startIndex = current.cacheIndex;
510                 length = current.length;
511                 dirty = current.dirty;
512             }
513         }
514 
515         if ( dirty )
516         { // Write the remaining chunk.
517             this.getFileOperations().setFilePointer( startPos );
518             this.getFileOperations().write(
519                 this.getCache(), startIndex, length );
520 
521         }
522 
523         // Reset cache state.
524         for ( Iterator it = this.root.entrySet().iterator(); it.hasNext(); )
525         {
526             final Map.Entry entry = (Map.Entry) it.next();
527             final Node current = (Node) entry.getValue();
528 
529             current.cacheIndex = Node.NO_CACHEINDEX;
530             current.dirty = false;
531         }
532 
533         this.nextCacheIndex = 0;
534 
535         if ( this.getFileOperations() instanceof FlushableFileOperations )
536         { // Cache of the backing instance also needs to get flushed.
537             ( (FlushableFileOperations) this.getFileOperations() ).flush();
538         }
539     }
540 
541     //-------------------------------------------------FlushableFileOperations--
542     //--CoalescingFileOperations------------------------------------------------
543 
544     /** Node describing a cache block. */
545     private static final class Node
546     {
547 
548         private static final int NO_CACHEINDEX = Integer.MIN_VALUE;
549 
550         long block;
551 
552         int cacheIndex = NO_CACHEINDEX;
553 
554         int length;
555 
556         boolean dirty;
557 
558     }
559 
560     /** The {@code FileOperations} backing the instance. */
561     private final FileOperations fileOperations;
562 
563     /** Cached blocks. */
564     private byte[] cache;
565 
566     /** Second cache memory used during defragmentation. */
567     private byte[] defragCache;
568 
569     /** Index of the next free cached block. */
570     private int nextCacheIndex;
571 
572     /** Maps blocks to corresponding {@code Node}s. */
573     private final Map root = new TreeMap();
574 
575     /** File pointer. */
576     private long filePointer;
577 
578     /** Caches the value returned by method {@code getFilePointerBlock}. */
579     private long cachedFilePointerBlock = NO_FILEPOINTERBLOCK;
580 
581     private long cachedFilePointerBlockStart = FileOperations.EOF;
582 
583     private static final long NO_FILEPOINTERBLOCK = Long.MIN_VALUE;
584 
585     /** Flags the instance as beeing closed. */
586     private boolean closed;
587 
588     /** The number of bytes occupied by one cache block. */
589     private Integer blockSize;
590 
591     /** The number of cache blocks. */
592     private Integer cacheBlocks;
593 
594     /**
595      * Creates a new {@code CoalescingFileOperations} instance taking the
596      * {@code FileOperations} backing the instance.
597      *
598      * @param fileOperations the {@code FileOperations} backing the instance.
599      *
600      * @throws NullPointerException if {@code fileOperations} is {@code null}.
601      * @throws IOException if reading fails.
602      */
603     public CoalescingFileOperations( final FileOperations fileOperations )
604         throws IOException
605     {
606         super();
607 
608         if ( fileOperations == null )
609         {
610             throw new NullPointerException( "fileOperations" );
611         }
612 
613         this.fileOperations = fileOperations;
614         this.filePointer = fileOperations.getFilePointer();
615     }
616 
617     /**
618      * Creates a new {@code CoalescingFileOperations} instance taking the
619      * {@code FileOperations} backing the instance and the number of bytes
620      * occupied by one cache block.
621      *
622      * @param fileOperations the {@code FileOperations} backing the instance.
623      * @param blockSize the number of bytes occupied by one cache block.
624      *
625      * @throws NullPointerException if {@code fileOperations} is {@code null}.
626      * @throws IOException if reading fails.
627      */
628     public CoalescingFileOperations( final FileOperations fileOperations,
629                                      final int blockSize ) throws IOException
630     {
631         this( fileOperations );
632         if ( blockSize > 0 )
633         {
634             this.blockSize = new Integer( blockSize );
635         }
636     }
637 
638     /**
639      * Creates a new {@code CoalescingFileOperations} instance taking the
640      * {@code FileOperations} backing the instance, the number of bytes
641      * occupied by one cache block and the number of cache blocks.
642      *
643      * @param fileOperations the {@code FileOperations} backing the instance.
644      * @param blockSize the number of bytes occupied by one cache block.
645      * @param cacheBlocks number of cache blocks.
646      *
647      * @throws NullPointerException if {@code fileOperations} is {@code null}.
648      * @throws IOException if reading fails.
649      */
650     public CoalescingFileOperations( final FileOperations fileOperations,
651                                      final int blockSize,
652                                      final int cacheBlocks )
653         throws IOException
654     {
655         this( fileOperations, blockSize );
656         if ( cacheBlocks > 0 )
657         {
658             this.cacheBlocks = new Integer( cacheBlocks );
659         }
660     }
661 
662     /**
663      * Gets the {@code FileOperations} implementation operations are performed
664      * with.
665      *
666      * @return the {@code FileOperations} implementation operations are
667      * performed with.
668      */
669     public FileOperations getFileOperations()
670     {
671         return this.fileOperations;
672     }
673 
674     /**
675      * Gets the number of bytes occupied by one cache block.
676      *
677      * @return the number of bytes occupied by one cache block.
678      */
679     public int getBlockSize()
680     {
681         if ( this.blockSize == null )
682         {
683             this.blockSize = this.getDefaultBlockSize();
684         }
685 
686         return this.blockSize.intValue();
687     }
688 
689     /**
690      * Gets the number of blocks in the cache.
691      *
692      * @return the number of blocks in the cache.
693      */
694     public int getCacheBlocks()
695     {
696         if ( this.cacheBlocks == null )
697         {
698             this.cacheBlocks = this.getDefaultCacheBlocks();
699         }
700 
701         return this.cacheBlocks.intValue();
702     }
703 
704     /**
705      * Gets the cache buffer.
706      *
707      * @return the cache buffer.
708      */
709     private byte[] getCache()
710     {
711         if ( this.cache == null )
712         {
713             this.cache = this.getMemoryManager().allocateBytes(
714                 this.getBlockSize() * this.getCacheBlocks() );
715 
716         }
717 
718         return this.cache;
719     }
720 
721     /**
722      * Gets the buffer used during defragmentation of the cache.
723      *
724      * @return the buffer used during defragmentation of the cache.
725      */
726     private byte[] getDefragCache()
727     {
728         if ( this.defragCache == null )
729         {
730             this.defragCache = this.getMemoryManager().allocateBytes(
731                 this.getBlockSize() * this.getCacheBlocks() );
732 
733         }
734 
735         return this.defragCache;
736     }
737 
738     /**
739      * Gets the block pointed to by a given file pointer value.
740      *
741      * @param filePointer the file pointer value for which to return the
742      * corresponding block.
743      *
744      * @return the block pointed to by {@code filePointer}.
745      */
746     private long getFilePointerBlock( final long filePointer )
747     {
748         if ( this.cachedFilePointerBlock == NO_FILEPOINTERBLOCK )
749         {
750             this.cachedFilePointerBlock =
751                 ( filePointer / this.getBlockSize() ) -
752                 ( ( filePointer % this.getBlockSize() ) / this.getBlockSize() );
753 
754             this.cachedFilePointerBlockStart =
755                 this.cachedFilePointerBlock * this.getBlockSize();
756 
757         }
758         else
759         {
760             if ( !( filePointer >= this.cachedFilePointerBlockStart &&
761                     filePointer <= this.cachedFilePointerBlockStart +
762                                    this.getBlockSize() ) )
763             {
764                 this.cachedFilePointerBlock =
765                     ( filePointer / this.getBlockSize() ) -
766                     ( ( filePointer % this.getBlockSize() ) /
767                       this.getBlockSize() );
768 
769                 this.cachedFilePointerBlockStart =
770                     this.cachedFilePointerBlock * this.getBlockSize();
771 
772             }
773         }
774 
775         return this.cachedFilePointerBlock;
776     }
777 
778     /**
779      * Gets the cache nodes for all blocks involved in a read or write operation
780      * of a given length for a given file pointer value.
781      *
782      * @param filePointer the file pointer value to use for computing the
783      * number of involved blocks for a read or write operation of
784      * {@code length}.
785      * @param length the length of the operation to perform.
786      *
787      * @return an array of cache nodes for all blocks involved in the
788      * operation in the order corresponding to the operation's needs.
789      */
790     private Node[] getCacheNodesForLength( final long filePointer,
791                                            final int length )
792     {
793         final long startingBlock = this.getFilePointerBlock( filePointer );
794         final long endingBlock =
795             this.getFilePointerBlock( filePointer + length );
796 
797         assert endingBlock - startingBlock <= Integer.MAX_VALUE :
798             "Unexpected implementation limit reached.";
799 
800         final Node[] nodes =
801             new Node[ (int) ( endingBlock - startingBlock + 1L ) ];
802 
803         if ( startingBlock == endingBlock )
804         {
805             nodes[0] = this.getCacheNode( startingBlock );
806         }
807         else
808         {
809             int i;
810             long block;
811 
812             for ( block = startingBlock, i = 0; block <= endingBlock;
813                   block++, i++ )
814             {
815                 nodes[i] = this.getCacheNode( block );
816             }
817         }
818 
819         return nodes;
820     }
821 
822     /**
823      * Fills the cache for a given set of cache nodes.
824      * <p>This method ensures that each given node gets associated with
825      * corresponding cache memory possibly flushing the cache before
826      * reading.</p>
827      *
828      * @param nodes the nodes to fill the cache for.
829      *
830      * @throws NullPointerException if {@code nodes} is {@code null}.
831      * @throws IOException if reading fails.
832      */
833     private void fillCache( final Node[] nodes ) throws IOException
834     {
835         if ( nodes == null )
836         {
837             throw new NullPointerException( "nodes" );
838         }
839 
840         // Calculate the amount of bytes needed to be available in the cache
841         // and flush the cache if nodes would not fit.
842         long neededBytes = 0L;
843         for ( int i = nodes.length - 1; i >= 0; i-- )
844         {
845             if ( nodes[i].cacheIndex == Node.NO_CACHEINDEX )
846             { // Node's block needs to be read.
847                 neededBytes += this.getBlockSize();
848             }
849         }
850 
851         if ( this.nextCacheIndex + neededBytes > this.getCache().length )
852         { // Cache cannot hold the needed blocks so needs flushing.
853             this.flush();
854         }
855 
856         // Associate each node with cache memory for nodes not already
857         // associated with cache memory and fill these nodes' cache memory.
858         for ( int i = nodes.length - 1; i >= 0; i-- )
859         {
860             if ( nodes[i].cacheIndex == Node.NO_CACHEINDEX &&
861                  this.nextCacheIndex < this.getCache().length )
862             { // Node is not associated with any cache memory and can be read.
863 
864                 // Update the length field of the node for the block checking
865                 // for a possible end of file condition.
866                 long pos = nodes[i].block * this.getBlockSize();
867                 if ( pos > this.getLength() )
868                 { // Node is behind the end of the file.
869                     nodes[i].length = FileOperations.EOF;
870                     continue;
871                 }
872                 else if ( pos + this.getBlockSize() > this.getLength() )
873                 {
874                     final long delta = this.getLength() - pos;
875 
876                     assert delta <= Integer.MAX_VALUE :
877                         "Unexpected implementation limit reached.";
878 
879                     nodes[i].length = (int) delta;
880                 }
881                 else
882                 {
883                     nodes[i].length = this.getBlockSize();
884                 }
885 
886                 // Associated the node with cache memory.
887                 nodes[i].cacheIndex = this.nextCacheIndex;
888                 this.nextCacheIndex += this.getBlockSize();
889 
890                 // Read the node's block into cache.
891                 int read = FileOperations.EOF;
892                 int totalRead = 0;
893                 int toRead = nodes[i].length;
894                 this.getFileOperations().setFilePointer( pos );
895 
896                 do
897                 {
898                     read = this.getFileOperations().read(
899                         this.getCache(), nodes[i].cacheIndex + totalRead,
900                         toRead );
901 
902                     assert read != FileOperations.EOF :
903                         "Unexpected end of file.";
904 
905                     totalRead += read;
906                     toRead -= read;
907 
908                 }
909                 while ( totalRead < nodes[i].length );
910             }
911         }
912     }
913 
914     /** Defragments the cache. */
915     private void defragmentCache()
916     {
917         int defragIndex = 0;
918 
919         // Step through the cached blocks and defragment the cache.
920         for ( Iterator it = this.root.entrySet().iterator(); it.hasNext(); )
921         {
922             final Map.Entry entry = (Map.Entry) it.next();
923             final long block = ( (Long) entry.getKey() ).longValue();
924             final Node current = (Node) entry.getValue();
925 
926             // Skip any end of file nodes and nodes not associated with memory.
927             if ( current.length == FileOperations.EOF ||
928                  current.cacheIndex == Node.NO_CACHEINDEX )
929             {
930                 continue;
931             }
932 
933             assert current.block == block : "Unexpected cache state.";
934 
935             System.arraycopy( this.getCache(), current.cacheIndex,
936                               this.getDefragCache(), defragIndex,
937                               this.getBlockSize() );
938 
939             current.cacheIndex = defragIndex;
940             defragIndex += this.getBlockSize();
941         }
942 
943         System.arraycopy( this.getDefragCache(), 0, this.getCache(), 0,
944                           this.getCache().length );
945 
946     }
947 
948     /**
949      * Gets the cache node for a given block.
950      *
951      * @param block the block to return the corresponding cache node for.
952      *
953      * @return the cache node for {@code block}.
954      */
955     private Node getCacheNode( final long block )
956     {
957         final Long key = new Long( block );
958         Node node = (Node) this.root.get( key );
959         if ( node == null )
960         {
961             node = new Node();
962             node.block = block;
963             this.root.put( key, node );
964         }
965 
966         return node;
967     }
968 
969     /**
970      * Checks that the instance is not closed.
971      *
972      * @throws IOException if the instance is closed.
973      */
974     private void assertNotClosed() throws IOException
975     {
976         if ( this.closed )
977         {
978             throw new IOException( this.getAlreadyClosedMessage(
979                 this.getLocale() ) );
980 
981         }
982     }
983 
984     //------------------------------------------------CoalescingFileOperations--
985     //--Messages----------------------------------------------------------------
986 
987 // <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausMessages
988     // This section is managed by jdtaus-container-mojo.
989 
990     /**
991      * Gets the text of message <code>readBypassesCache</code>.
992      * <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>
993      * <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>
994      *
995      * @param locale The locale of the message instance to return.
996      * @param blockSize The current block size in use.
997      * @param cacheBlocks The current number of blocks in use.
998      * @param uncachedBytes The number of bytes bypassing caching.
999      *
1000      * @return Information about a misconfigured cache.
1001      */
1002     private String getReadBypassesCacheMessage( final Locale locale,
1003             final java.lang.Number blockSize,
1004             final java.lang.Number cacheBlocks,
1005             final java.lang.Number uncachedBytes )
1006     {
1007         return ContainerFactory.getContainer().
1008             getMessage( this, "readBypassesCache", locale,
1009                 new Object[]
1010                 {
1011                     blockSize,
1012                     cacheBlocks,
1013                     uncachedBytes
1014                 });
1015 
1016     }
1017 
1018     /**
1019      * Gets the text of message <code>writeBypassesCache</code>.
1020      * <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>
1021      * <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>
1022      *
1023      * @param locale The locale of the message instance to return.
1024      * @param blockSize The current block size in use.
1025      * @param cacheBlocks The current number of blocks in use.
1026      * @param uncachedBytes The number of bytes bypassing caching.
1027      *
1028      * @return Information about a misconfigured cache.
1029      */
1030     private String getWriteBypassesCacheMessage( final Locale locale,
1031             final java.lang.Number blockSize,
1032             final java.lang.Number cacheBlocks,
1033             final java.lang.Number uncachedBytes )
1034     {
1035         return ContainerFactory.getContainer().
1036             getMessage( this, "writeBypassesCache", locale,
1037                 new Object[]
1038                 {
1039                     blockSize,
1040                     cacheBlocks,
1041                     uncachedBytes
1042                 });
1043 
1044     }
1045 
1046     /**
1047      * Gets the text of message <code>alreadyClosed</code>.
1048      * <blockquote><pre>Instanz geschlossen - keine E/A-Operationen möglich.</pre></blockquote>
1049      * <blockquote><pre>Instance closed - cannot perform I/O.</pre></blockquote>
1050      *
1051      * @param locale The locale of the message instance to return.
1052      *
1053      * @return Message stating that an instance is already closed.
1054      */
1055     private String getAlreadyClosedMessage( final Locale locale )
1056     {
1057         return ContainerFactory.getContainer().
1058             getMessage( this, "alreadyClosed", locale, null );
1059 
1060     }
1061 
1062 // </editor-fold>//GEN-END:jdtausMessages
1063 
1064     //----------------------------------------------------------------Messages--
1065 }