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.Locale;
027import org.jdtaus.core.container.ContainerFactory;
028import org.jdtaus.core.io.FileOperations;
029import org.jdtaus.core.lang.spi.MemoryManager;
030
031/**
032 * Read-ahead {@code FileOperations} cache.
033 * <p>This implementation implements a read-ahead cache for
034 * {@code FileOperations} implementations. The cache is controlled by
035 * configuration property {@code cacheSize} holding the number of bytes
036 * to read-ahead. By default property {@code cacheSize} is initialized to
037 * {@code 16384} leading to a cache size of 16 kB. All memory is allocated
038 * during instantiation so that an {@code OutOfMemoryError} may be thrown
039 * when constructing the cache but not when working with the instance.</p>
040 *
041 * <p><b>Note:</b><br>
042 * This implementation is not thread-safe and concurrent changes to the
043 * underlying {@code FileOperations} implementation are not supported.</p>
044 *
045 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
046 * @version $JDTAUS: ReadAheadFileOperations.java 8641 2012-09-27 06:45:17Z schulte $
047 */
048public final class ReadAheadFileOperations implements FlushableFileOperations
049{
050    //--Dependencies------------------------------------------------------------
051
052// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausDependencies
053    // This section is managed by jdtaus-container-mojo.
054
055    /**
056     * Gets the configured <code>MemoryManager</code> implementation.
057     *
058     * @return The configured <code>MemoryManager</code> implementation.
059     */
060    private MemoryManager getMemoryManager()
061    {
062        return (MemoryManager) ContainerFactory.getContainer().
063            getDependency( this, "MemoryManager" );
064
065    }
066
067    /**
068     * Gets the configured <code>Locale</code> implementation.
069     *
070     * @return The configured <code>Locale</code> implementation.
071     */
072    private Locale getLocale()
073    {
074        return (Locale) ContainerFactory.getContainer().
075            getDependency( this, "Locale" );
076
077    }
078
079// </editor-fold>//GEN-END:jdtausDependencies
080
081    //------------------------------------------------------------Dependencies--
082    //--Properties--------------------------------------------------------------
083
084// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausProperties
085    // This section is managed by jdtaus-container-mojo.
086
087    /**
088     * Gets the value of property <code>defaultCacheSize</code>.
089     *
090     * @return Default cache size in byte.
091     */
092    private java.lang.Integer getDefaultCacheSize()
093    {
094        return (java.lang.Integer) ContainerFactory.getContainer().
095            getProperty( this, "defaultCacheSize" );
096
097    }
098
099// </editor-fold>//GEN-END:jdtausProperties
100
101    //--------------------------------------------------------------Properties--
102    //--FileOperations----------------------------------------------------------
103
104    public long getLength() throws IOException
105    {
106        this.assertNotClosed();
107
108        return this.fileOperations.getLength();
109    }
110
111    public void setLength( final long newLength ) throws IOException
112    {
113        this.assertNotClosed();
114
115        final long oldLength = this.getLength();
116        this.fileOperations.setLength( newLength );
117        if ( this.filePointer > newLength )
118        {
119            this.filePointer = newLength;
120        }
121
122        if ( oldLength > newLength && this.cachePosition != NO_CACHEPOSITION &&
123             this.cachePosition + this.cacheLength >= newLength )
124        { // Discard the end of file cache.
125            this.cachePosition = NO_CACHEPOSITION;
126        }
127    }
128
129    public long getFilePointer() throws IOException
130    {
131        this.assertNotClosed();
132
133        return this.filePointer;
134    }
135
136    public void setFilePointer( final long pos ) throws IOException
137    {
138        this.assertNotClosed();
139
140        this.filePointer = pos;
141    }
142
143    public int read( final byte[] buf, int off, int len )
144        throws IOException
145    {
146        if ( buf == null )
147        {
148            throw new NullPointerException( "buf" );
149        }
150        if ( off < 0 )
151        {
152            throw new IndexOutOfBoundsException( Integer.toString( off ) );
153        }
154        if ( len < 0 )
155        {
156            throw new IndexOutOfBoundsException( Integer.toString( len ) );
157        }
158        if ( off + len > buf.length )
159        {
160            throw new IndexOutOfBoundsException( Integer.toString( off + len ) );
161        }
162
163        this.assertNotClosed();
164
165        int read = FileOperations.EOF;
166
167        final long fileLength = this.getLength();
168
169        if ( len == 0 )
170        {
171            read = 0;
172        }
173        else if ( this.filePointer < fileLength )
174        {
175            if ( this.cachePosition == NO_CACHEPOSITION ||
176                 !( this.filePointer >= this.cachePosition &&
177                    this.filePointer < this.cachePosition + this.cacheLength ) )
178            { // Cache not initialized or file pointer outside the cached area.
179                this.fillCache();
180            }
181
182            final long cacheStart = this.filePointer - this.cachePosition;
183
184            assert cacheStart <= Integer.MAX_VALUE :
185                "Unexpected implementation limit reached.";
186
187            final int cachedLength = len > this.cacheLength -
188                                           (int) cacheStart
189                                     ? this.cacheLength - (int) cacheStart
190                                     : len;
191
192            System.arraycopy( this.getCache(), (int) cacheStart, buf, off,
193                              cachedLength );
194
195            len -= cachedLength;
196            off += cachedLength;
197            read = cachedLength;
198            this.filePointer += cachedLength;
199        }
200
201        return read;
202    }
203
204    public void write( final byte[] buf, final int off, final int len )
205        throws IOException
206    {
207        if ( buf == null )
208        {
209            throw new NullPointerException( "buf" );
210        }
211        if ( off < 0 )
212        {
213            throw new IndexOutOfBoundsException( Integer.toString( off ) );
214        }
215        if ( len < 0 )
216        {
217            throw new IndexOutOfBoundsException( Integer.toString( len ) );
218        }
219        if ( off + len > buf.length )
220        {
221            throw new IndexOutOfBoundsException( Integer.toString( off + len ) );
222        }
223
224        this.assertNotClosed();
225
226        if ( this.cachePosition != NO_CACHEPOSITION &&
227             this.filePointer >= this.cachePosition &&
228             this.filePointer < this.cachePosition + this.cacheLength )
229        { // Cache needs updating.
230            final long cacheStart = this.filePointer - this.cachePosition;
231
232            assert cacheStart <= Integer.MAX_VALUE :
233                "Unexpected implementation limit reached.";
234
235            final int cachedLength = len > this.cacheLength -
236                                           (int) cacheStart
237                                     ? this.cacheLength - (int) cacheStart
238                                     : len;
239
240            System.arraycopy( buf, off, this.getCache(), (int) cacheStart,
241                              cachedLength );
242
243        }
244
245        this.fileOperations.setFilePointer( this.filePointer );
246        this.fileOperations.write( buf, off, len );
247        this.filePointer += len;
248    }
249
250    public void read( final OutputStream out ) throws IOException
251    {
252        this.assertNotClosed();
253
254        this.fileOperations.read( out );
255        this.filePointer = this.fileOperations.getFilePointer();
256    }
257
258    public void write( final InputStream in ) throws IOException
259    {
260        this.assertNotClosed();
261
262        this.fileOperations.write( in );
263        this.filePointer = this.fileOperations.getFilePointer();
264    }
265
266    /**
267     * {@inheritDoc}
268     * Flushes the cache and closes the {@code FileOperations} implementation
269     * backing the instance.
270     *
271     * @throws IOException if closing the {@code FileOperations} implementation
272     * backing the instance fails or if the instance already is closed.
273     */
274    public void close() throws IOException
275    {
276        this.assertNotClosed();
277
278        this.flush();
279        this.getFileOperations().close();
280        this.closed = true;
281    }
282
283    //----------------------------------------------------------FileOperations--
284    //--FlushableFileOperations-------------------------------------------------
285
286    /**
287     * {@inheritDoc}
288     * This method calls the {@code flush()} method of an underlying
289     * {@code FlushableFileOperations} implementation, if any.
290     */
291    public void flush() throws IOException
292    {
293        this.assertNotClosed();
294
295        if ( this.fileOperations instanceof FlushableFileOperations )
296        {
297            ( (FlushableFileOperations) this.fileOperations ).flush();
298        }
299    }
300
301    //-------------------------------------------------FlushableFileOperations--
302    //--ReadAheadFileOperations-------------------------------------------------
303
304    /** The {@code FileOperations} backing the instance. */
305    private final FileOperations fileOperations;
306
307    /** Cached bytes. */
308    private byte[] cache;
309
310    /** Position in the file {@code cache} starts. */
311    private long cachePosition;
312
313    private static final long NO_CACHEPOSITION = Long.MIN_VALUE;
314
315    /** Length of the cached data. */
316    private int cacheLength;
317
318    /** File pointer value. */
319    private long filePointer;
320
321    /** Flags the instance as beeing closed. */
322    private boolean closed;
323
324    /** Cache size in byte. */
325    private Integer cacheSize;
326
327    /**
328     * Creates a new {@code ReadAheadFileOperations} instance taking the
329     * {@code FileOperations} backing the instance.
330     *
331     * @param fileOperations the {@code FileOperations} backing the instance.
332     *
333     * @throws NullPointerException if {@code fileOperations} is {@code null}.
334     * @throws IOException if reading fails.
335     */
336    public ReadAheadFileOperations( final FileOperations fileOperations )
337        throws IOException
338    {
339        super();
340
341        if ( fileOperations == null )
342        {
343            throw new NullPointerException( "fileOperations" );
344        }
345
346        this.fileOperations = fileOperations;
347        this.filePointer = fileOperations.getFilePointer();
348    }
349
350    /**
351     * Creates a new {@code ReadAheadFileOperations} instance taking the
352     * {@code FileOperations} backing the instance and the size of the cache.
353     *
354     * @param fileOperations the {@code FileOperations} backing the instance.
355     * @param cacheSize the number of bytes to read-ahead.
356     *
357     * @throws NullPointerException if {@code fileOperations} is {@code null}.
358     * @throws IOException if reading fails.
359     */
360    public ReadAheadFileOperations( final FileOperations fileOperations,
361                                    final int cacheSize ) throws IOException
362    {
363        this( fileOperations );
364
365        if ( cacheSize > 0 )
366        {
367            this.cacheSize = new Integer( cacheSize );
368        }
369    }
370
371    /**
372     * Gets the {@code FileOperations} implementation operations are performed
373     * with.
374     *
375     * @return the {@code FileOperations} implementation operations are
376     * performed with.
377     */
378    public FileOperations getFileOperations()
379    {
380        return this.fileOperations;
381    }
382
383    /**
384     * Gets the size of the cache in byte.
385     *
386     * @return the size of the cache in byte.
387     */
388    public int getCacheSize()
389    {
390        if ( this.cacheSize == null )
391        {
392            this.cacheSize = this.getDefaultCacheSize();
393        }
394
395        return this.cacheSize.intValue();
396    }
397
398    /**
399     * Gets the cache buffer.
400     *
401     * @return the cache buffer.
402     */
403    private byte[] getCache()
404    {
405        if ( this.cache == null )
406        {
407            this.cache =
408                this.getMemoryManager().allocateBytes( this.getCacheSize() );
409
410        }
411
412        return this.cache;
413    }
414
415    /**
416     * Checks that the instance is not closed.
417     *
418     * @throws IOException if the instance is closed.
419     */
420    private void assertNotClosed() throws IOException
421    {
422        if ( this.closed )
423        {
424            throw new IOException( this.getAlreadyClosedMessage(
425                this.getLocale() ) );
426
427        }
428    }
429
430    /**
431     * Fills the cache starting at the current file pointer value.
432     *
433     * @throws IOException if reading fails.
434     */
435    private void fillCache() throws IOException
436    {
437        final long delta = this.getLength() - this.filePointer;
438        final int toRead = delta > this.getCache().length
439                           ? this.getCache().length
440                           : (int) delta;
441
442        this.cachePosition = this.filePointer;
443
444        int totalRead = 0;
445        int readLength = toRead;
446
447        do
448        {
449            this.fileOperations.setFilePointer( this.filePointer );
450            final int read = this.fileOperations.read(
451                this.getCache(), totalRead, readLength );
452
453            assert read != FileOperations.EOF : "Unexpected end of file.";
454
455            totalRead += read;
456            readLength -= read;
457
458        }
459        while ( totalRead < toRead );
460
461        this.cacheLength = toRead;
462    }
463
464    //-------------------------------------------------ReadAheadFileOperations--
465    //--Messages----------------------------------------------------------------
466
467// <editor-fold defaultstate="collapsed" desc=" Generated Code ">//GEN-BEGIN:jdtausMessages
468    // This section is managed by jdtaus-container-mojo.
469
470    /**
471     * Gets the text of message <code>alreadyClosed</code>.
472     * <blockquote><pre>Instanz geschlossen - keine E/A-Operationen möglich.</pre></blockquote>
473     * <blockquote><pre>Instance closed - cannot perform I/O.</pre></blockquote>
474     *
475     * @param locale The locale of the message instance to return.
476     *
477     * @return Message stating that an instance is already closed.
478     */
479    private String getAlreadyClosedMessage( final Locale locale )
480    {
481        return ContainerFactory.getContainer().
482            getMessage( this, "alreadyClosed", locale, null );
483
484    }
485
486// </editor-fold>//GEN-END:jdtausMessages
487
488    //----------------------------------------------------------------Messages--
489}