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 8743 2012-10-07 03:06:20Z 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 ( 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}