com.microsoft.tfs.util.temp.FastTempOutputStream.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.util.temp.FastTempOutputStream.java

Source

// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See License.txt in the repository root.

package com.microsoft.tfs.util.temp;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.MessageFormat;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.microsoft.tfs.util.StringUtil;

/**
 * <p>
 * An {@link OutputStream} that writes bytes to temporary storage and can be
 * read from multiple times before being disposed. Internally the class first
 * writes to heap memory, then switches to a temporary file once a threshold is
 * exceeded. The temporary file is obtained through {@link TempStorageService}
 * so temp files can be removed automatically during JVM shutdown (if they were
 * not already disposed of manually before).
 * </p>
 * <p>
 * Common usage steps for this class:
 * <ol>
 * <li>Intantiate {@link FastTempOutputStream}</li>
 * <li>Write data to it via {@link #write(byte[])}, {@link #write(int)}, and
 * {@link #write(byte[], int, int)}</li>
 * <li>Call {@link #close()} when done writing all data</li>
 * <li>Call {@link #getInputStream()} to get an {@link InputStream} (this can be
 * done multiple times)</li>
 * <li>Read data via the {@link InputStream}</li>
 * <li>Close each {@link InputStream} via {@link InputStream#close()}</li>
 * <li>Call {@link #dispose()} on {@link FastTempOutputStream} to free resources
 * </li>
 * </ol>
 * Data cannot be read (via {@link #getInputStream()}) before
 * {@link InputStream#close()} is called (the behavior for concurrent open
 * writer and reader is undefined). You can open multiple {@link InputStream}s,
 * but they all must be closed before calling {@link #dispose()}.
 * </p>
 * <p>
 * Call {@link #dispose()} to free temp storage when finished with an instance
 * and all of its {@link InputStream}s have been closed.
 * </p>
 * <p>
 * This class is useful because creating temp files on some operating systems
 * (Windows) is very slow compared to others (most Unixes), especially when they
 * only store a small amount of data (a few hundred bytes) and are used for a
 * short time (tens or hundreds of milliseconds). This class is designed for
 * these scenarios, when the eventual size of the temp file is unknown
 * (streaming from the network, for instance) but are often small enough to fit
 * in main memory.
 * </p>
 *
 * @threadsafety thread-safe
 * @since TEE-SDK-10.1
 */
public class FastTempOutputStream extends OutputStream {
    private final static Log log = LogFactory.getLog(FastTempOutputStream.class);

    /**
     * Extends {@link ByteArrayOutputStream} so {@link FastTempOutputStream} can
     * get at the underlying byte array for efficiency.
     */
    private static class DirectAccessByteArrayOutputStream extends ByteArrayOutputStream {
        public DirectAccessByteArrayOutputStream(final int size) {
            super(size);
        }

        /**
         * @return the byte array this {@link DirectAccessByteArrayOutputStream}
         *         stores its data in. The array may be larger than the contents
         *         ({@link #size()}) of the stream
         */
        protected byte[] getByteArray() {
            return super.buf;
        }
    }

    /**
     * The default initial size for heap storage when the user didn't specify a
     * hint during construction. This is just the default for efficiency, it
     * will grow automatically until the configured heap storage limit is
     * reached.
     */
    public static final int DEFAULT_HEAP_STORAGE_INITIAL_SIZE_BYTES = 4096;

    /**
     * The default limit on heap storage, after this size file storage is used.
     */
    public static final int DEFAULT_HEAP_STORAGE_LIMIT_BYTES = getDefaultHeapStorageLimit();

    /**
     * Holds the limit on heap storage we can't exceed. We track this
     * independent of the heap storage object because we don't wish to
     * pre-allocate all of it (this is just the configured limit for this
     * instance).
     */
    private final int heapStorageLimitBytes;

    /**
     * The actual heap storage. Allocated during construction, nulled out when
     * {@link #adjustStorage(int)} decides to switch to file storage.
     */
    private DirectAccessByteArrayOutputStream heapStream;

    /**
     * Actual file storage. Only allocated when {@link #adjustStorage(int)}
     * decides to switch to file storage (which can be during construction).
     */
    private FileOutputStream fileStream;

    /**
     * The file storage location (non-null whenever {@link #fileStream} is being
     * used).
     */
    private File file;

    /**
     * Updated by {@link #adjustStorage(int)} to always point to the currently
     * used stream (heap or file).
     */
    private OutputStream currentStream;

    /**
     * When true, the write methods can be called. Once {@link #close()} is
     * called, {@link #writable} is set to false and only then can
     * {@link #getInputStream()} be called (possibly multiple times).
     */
    private boolean writable = true;

    /**
     * Creates a {@link FastTempOutputStream} with default values for the size
     * hint and heap limit.
     */
    public FastTempOutputStream() {
        this(DEFAULT_HEAP_STORAGE_LIMIT_BYTES, DEFAULT_HEAP_STORAGE_INITIAL_SIZE_BYTES);
    }

    /**
     * Creates a {@link FastTempOutputStream} that will not use more than the
     * given limit for heap storage (before transparently switch to file
     * storage), and starts with the given heap storage pre-allocated (for more
     * efficient writing of smaller files).
     *
     * @param heapStorageLimitBytes
     *        the (approximate) limit on heap storage before file storage is
     *        used instead. If negative, default values are used.
     * @param initialHeapStorageSizeBytes
     *        the amount of heap storage to initially allocate. Using numbers
     *        near the ultimate size of the content written helps reduce heap
     *        allocations and fragmentation (at the expense of more initial
     *        memory used). If negative, default values are used.
     */
    public FastTempOutputStream(int heapStorageLimitBytes, int initialHeapStorageSizeBytes) {
        super();

        if (heapStorageLimitBytes < 0) {
            heapStorageLimitBytes = DEFAULT_HEAP_STORAGE_LIMIT_BYTES;
        }

        if (initialHeapStorageSizeBytes < 0) {
            initialHeapStorageSizeBytes = DEFAULT_HEAP_STORAGE_INITIAL_SIZE_BYTES;
        }

        final String messageFormat = "New instance with heap limit of {0} bytes, initial heap size {1}"; //$NON-NLS-1$
        final String message = MessageFormat.format(messageFormat, heapStorageLimitBytes,
                initialHeapStorageSizeBytes);
        log.trace(message);

        synchronized (this) {
            this.heapStorageLimitBytes = heapStorageLimitBytes;

            heapStream = new DirectAccessByteArrayOutputStream(initialHeapStorageSizeBytes);
            currentStream = heapStream;
        }
    }

    private static int getDefaultHeapStorageLimit() {
        final String propertyName = "com.microsoft.tfs.fasttempstream.heaplimit"; //$NON-NLS-1$
        final int defaultValue = 8 * 1024 * 1024;
        final String value = System.getProperty(propertyName);
        if (StringUtil.isNullOrEmpty(value)) {
            return defaultValue;
        } else {
            try {
                final int number = StringUtil.toInt(value);
                return number;
            } catch (final NumberFormatException e) {
                final String message = MessageFormat.format("Incorrect value of the system property {0} = {1}", //$NON-NLS-1$
                        propertyName, value);
                log.error(message, e);

                return defaultValue;
            }
        }
    }

    /**
     * Examines the current configuration of the buffer relative to the new size
     * and adjusts the storage method if required.
     * <p>
     * This method should be called <b>before</b> writing to the underlying
     * streams, so big writes can be aimed at the right stream (file).
     * <p>
     * This method always updates {@link #currentStream} before returning.
     *
     * @param increase
     *        the increase in size this buffer needs (usually the length of a
     *        write operation)
     */
    private synchronized void adjustStorage(final int increase) {
        /*
         * If we're already using a file, there's nothing more to adjust (and
         * the current stream is already correct).
         */
        if (increase <= 0 || fileStream != null) {
            return;
        }

        /*
         * If we're currently using a heap stream, and the new total size is
         * big, start using a file.
         */
        if (heapStream != null && heapStream.size() + increase > heapStorageLimitBytes) {
            String messageFormat = "adjustment for {0} total bytes (increase of {1}) exceeds threshold of {2}, switching to file storage"; //$NON-NLS-1$
            String message = MessageFormat.format(messageFormat, (heapStream.size() + increase), increase,
                    heapStorageLimitBytes);

            log.trace(message);

            /*
             * Big size, time to allocate a file.
             */
            try {
                file = TempStorageService.getInstance().createTempFile();

                messageFormat = "Created temporary file {0} (exceeded heap limit of {1} bytes)"; //$NON-NLS-1$
                message = MessageFormat.format(messageFormat, file.getAbsolutePath(), heapStorageLimitBytes);
                log.debug(message);

                fileStream = new FileOutputStream(file);
            } catch (final IOException e) {
                log.error("Error creating temp file", e); //$NON-NLS-1$

                if (file != null) {
                    file.delete();
                }

                throw new RuntimeException(e);
            }

            /*
             * Write the previous heap data to the file and free it.
             */
            try {
                messageFormat = "Copying {0} initial bytes from heap to file"; //$NON-NLS-1$
                message = MessageFormat.format(messageFormat, heapStream.size());
                log.trace(message);

                heapStream.writeTo(fileStream);
            } catch (final IOException e) {
                log.error("Error copying initial bytes", e); //$NON-NLS-1$

                if (file != null) {
                    file.delete();
                }

                throw new RuntimeException(e);
            }

            heapStream = null;
            currentStream = fileStream;

            log.trace("Now using file storage"); //$NON-NLS-1$
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void close() throws IOException {
        currentStream.close();
        writable = false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void flush() throws IOException {
        checkWritable();

        currentStream.flush();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void write(final byte[] b, final int off, final int len) throws IOException {
        checkWritable();

        adjustStorage(len);
        currentStream.write(b, off, len);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void write(final byte[] b) throws IOException {
        checkWritable();

        adjustStorage(b.length);
        currentStream.write(b);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void write(final int b) throws IOException {
        checkWritable();

        adjustStorage(1);
        currentStream.write(b);
    }

    /**
     * Tests whether this stream is still writable, and throws if it is not.
     *
     * @throws IOException
     *         an exception with a message about the stream being closed
     */
    private synchronized void checkWritable() throws IOException {
        if (writable == false) {
            throw new IOException("The stream has been closed"); //$NON-NLS-1$
        }
    }

    /**
     * Gets an {@link InputStream} that reads the buffer contents that were
     * previously written before {@link #close()} was called. This method may be
     * invoked multiple times.
     *
     * @return an {@link InputStream} that reads the data that was written to
     *         this buffer
     * @throws IOException
     *         if the stream is still writable, or if the temporary file (if a
     *         file is being used for storage) could not be opened
     */
    public synchronized InputStream getInputStream() throws IOException {
        if (writable) {
            throw new IOException(
                    "Cannot get an InputStream because the stream is still open for writing (call close())"); //$NON-NLS-1$
        }

        if (heapStream != null) {
            /*
             * The byte array in the heapStream won't change from here out.
             */
            log.trace("Creating new ByteArrayInputStream"); //$NON-NLS-1$
            return new ByteArrayInputStream(heapStream.getByteArray(), 0, heapStream.size());
        } else {
            final String messageFormat = "Creating new FileInputStream for {0}"; //$NON-NLS-1$
            final String message = MessageFormat.format(messageFormat, file.getAbsolutePath());
            log.trace(message);
            return new FileInputStream(file);
        }
    }

    /**
     * Disposes of temporary resources used by this {@link FastTempOutputStream}
     * . Users must call this when finished writing and reading from
     * {@link FastTempOutputStream} (all {@link InputStream}s retrieved via
     * {@link #getInputStream()} must be closed), otherwise temporary files may
     * remain on disk.
     *
     * @throws IOException
     *         if an error occurred closing the temporary file stream
     */
    public synchronized void dispose() throws IOException {
        log.trace("Disposing"); //$NON-NLS-1$

        close();

        if (heapStream != null) {
            heapStream = null;
            log.trace("Cleared heap storage"); //$NON-NLS-1$
        }

        if (fileStream != null) {
            fileStream = null;
        }

        if (file != null) {
            TempStorageService.getInstance().cleanUpItem(file);

            final String messageFormat = "Deleted file storage {0}"; //$NON-NLS-1$
            final String message = MessageFormat.format(messageFormat, file.getAbsolutePath());
            log.debug(message);
            file = null;
        }
    }
}