001/*
002 *  jDTAUS Core Utilities
003 *  Copyright (C) 2005 Christian Schulte
004 *  <cs@schulte.it>
005 *
006 *  This library is free software; you can redistribute it and/or
007 *  modify it under the terms of the GNU Lesser General Public
008 *  License as published by the Free Software Foundation; either
009 *  version 2.1 of the License, or any later version.
010 *
011 *  This library is distributed in the hope that it will be useful,
012 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
013 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 *  Lesser General Public License for more details.
015 *
016 *  You should have received a copy of the GNU Lesser General Public
017 *  License along with this library; if not, write to the Free Software
018 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
019 *
020 */
021package org.jdtaus.core.io.util;
022
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.util.Iterator;
027import java.util.Locale;
028import java.util.Map;
029import java.util.TreeMap;
030import org.jdtaus.core.container.ContainerFactory;
031import org.jdtaus.core.io.FileOperations;
032import org.jdtaus.core.lang.spi.MemoryManager;
033import org.jdtaus.core.logging.spi.Logger;
034
035/**
036 * Coalescing {@code FileOperations} cache.
037 * <p>This implementation implements a coalescing cache for
038 * {@code FileOperations} implementations. The cache is controlled by
039 * configuration property {@code blockSize}. By default property
040 * {@code blockSize} is initialized to {@code 2097152} leading to a cache
041 * size of 10 MB (multiplied by property {@code cacheSize} which defaults to
042 * {@code 5}). All memory is allocated during instantiation so that an
043 * {@code OutOfMemoryError} may be thrown when constructing the cache but not
044 * when working with the instance.</p>
045 *
046 * <p><b>Note:</b><br>
047 * This implementation is not thread-safe and concurrent changes to the
048 * underlying {@code FileOperations} implementation are not supported.</p>
049 *
050 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
051 * @version $JDTAUS: CoalescingFileOperations.java 8641 2012-09-27 06:45:17Z schulte $
052 */
053public final class CoalescingFileOperations implements FlushableFileOperations
054{
055    //--Dependencies------------------------------------------------------------
056
057// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausDependencies
058    // This section is managed by jdtaus-container-mojo.
059
060    /**
061     * Gets the configured <code>MemoryManager</code> implementation.
062     *
063     * @return The configured <code>MemoryManager</code> implementation.
064     */
065    private MemoryManager getMemoryManager()
066    {
067        return (MemoryManager) ContainerFactory.getContainer().
068            getDependency( this, "MemoryManager" );
069
070    }
071
072    /**
073     * Gets the configured <code>Locale</code> implementation.
074     *
075     * @return The configured <code>Locale</code> implementation.
076     */
077    private Locale getLocale()
078    {
079        return (Locale) ContainerFactory.getContainer().
080            getDependency( this, "Locale" );
081
082    }
083
084    /**
085     * Gets the configured <code>Logger</code> implementation.
086     *
087     * @return The configured <code>Logger</code> implementation.
088     */
089    private Logger getLogger()
090    {
091        return (Logger) ContainerFactory.getContainer().
092            getDependency( this, "Logger" );
093
094    }
095
096// </editor-fold>//GEN-END:jdtausDependencies
097
098    //------------------------------------------------------------Dependencies--
099    //--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}