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

Java tutorial

Introduction

Here is the source code for com.microsoft.tfs.util.temp.TempStorageService.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.File;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

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

import com.microsoft.tfs.util.Check;
import com.microsoft.tfs.util.GUID;
import com.microsoft.tfs.util.IOUtils;
import com.microsoft.tfs.util.Messages;
import com.microsoft.tfs.util.shutdown.ShutdownEventListener;
import com.microsoft.tfs.util.shutdown.ShutdownManager;
import com.microsoft.tfs.util.shutdown.ShutdownManager.Priority;

/**
 * <p>
 * Creates temporary files and directories with more flexibility than the
 * {@link File} class. Temp items can be deleted immediately with
 * {@link #cleanUpItem(File)}. By default items the service creates will be
 * deleted when the JVM shuts down, but this behavior can be changed per-item
 * with {@link #forgetItem(File)}.
 * </p>
 * <p>
 * This class is a singleton.
 * </p>
 *
 * @since TEE-SDK-10.1
 * @threadsafety thread-safe
 */
public final class TempStorageService {
    private final static Log log = LogFactory.getLog(TempStorageService.class);

    private final static int MAX_RENAME_ATTEMPTS = 5;
    private final static int RENAME_ATTEMPTS_DELAY = 500; // in milliseconds

    private static boolean nioClassesLoadable = true;
    private static boolean nioClassesLoaded = false;
    private static Class<?> filesClass;
    private static Class<?> pathInterface;
    private static Class<?> pathsClass;
    private static Class<?> copyOptionInterfaceArray;
    private static Class<?> copyOptionInterface;
    private static Method getMethod;
    private static Method moveMethod;
    private static Object copyOptions;

    /**
     * The singleton.
     */
    private static TempStorageService instance;

    /**
     * The default extension for files we create.
     */
    public final static String DEFAULT_EXTENSION = ".tmp"; //$NON-NLS-1$

    /**
     * A counter used tag {@link CleanUpItem}s in the order they are created.
     * When we process clean up items up we sort the collection by serial number
     * so we delete earlier items first.
     */
    private final AtomicLong currentSerialNumber = new AtomicLong(0);

    /**
     * When the user does not specify a directory where temp files should be
     * created, they go here. Lazily initialized with what Java thinks is the
     * system's temp dir. Synchronized on this.
     */
    private File systemTempDir;

    /**
     * Maps a {@link File} to a {@link CleanUpItem} that we can delete in the
     * future (unless its deletion is canceled by the user). Synchronized on
     * this.
     */
    private final Map<File, CleanUpItem> cleanUpItems = new HashMap<File, CleanUpItem>();

    /**
     * Describes a temporary file or directory that was allocated by
     * {@link TempStorageService} and needs to be deleted by it in the future.
     *
     * @threadsafety immutable
     */
    private static class CleanUpItem implements Comparable<CleanUpItem> {
        /**
         * The file that gets cleaned up for this item. May be the actual temp
         * item created but could be a parent if the parent was created by
         * {@link TempStorageService}.
         */
        private final File cleanUpFile;

        /**
         * Used to order {@link CleanUpItem}s during a sort. A
         * {@link TempStorageService} instance increments a counter each time it
         * creates a new {@link CleanUpItem} so it can sort them for correct
         * deletion.
         */
        private final long serialNumber;

        /**
         * Creates a {@link CleanUpItem} that is managed by
         * {@link TempStorageService}.
         *
         * @param cleanUpFile
         *        the local file or directory that should be cleaned up
         *        (recursive) for this item (must not be <code>null</code>)
         * @param serialNumber
         *        a number that represents the order in which this
         *        {@link CleanUpItem} was created relative to others created by
         *        the same {@link TempStorageService}
         */
        public CleanUpItem(final File cleanUpFile, final long serialNumber) {
            Check.notNull(cleanUpFile, "cleanUpFile"); //$NON-NLS-1$

            this.cleanUpFile = cleanUpFile;
            this.serialNumber = serialNumber;
        }

        /**
         * @return the local item to delete
         */
        public File getCleanUpFile() {
            return this.cleanUpFile;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public int compareTo(final CleanUpItem other) {
            if (serialNumber < other.serialNumber) {
                return -1;
            } else if (serialNumber > other.serialNumber) {
                return 1;
            } else {
                return 0;
            }
        }
    }

    private TempStorageService() {
        /*
         * Register for late invocation because deleting temp files doesn't
         * depend on other resources but they may depend on this.
         */
        ShutdownManager.getInstance().addShutdownEventListener(new ShutdownEventListener() {
            @Override
            public void onShutdown() {
                /*
                 * Normally a class which implements ShutdownEventListener would
                 * remove itself from the ShutdownManager's listeners.
                 * Singletons don't really fit this pattern, and there's no harm
                 * in calling cleanUpAllItems() multiple times, so we don't
                 * bother.
                 */
                cleanUpAllItems();
            }
        }, Priority.LATE);
    }

    /**
     * @return the single instance of {@link TempStorageService}.
     */
    public static synchronized TempStorageService getInstance() {
        if (instance == null) {
            instance = new TempStorageService();
        }

        return instance;
    }

    /**
     * <p>
     * Creates a new empty file in a new directory inside the system's default
     * temporary directory. See {@link #createTempFile(File, String)} for how to
     * delete the file.
     * </p>
     *
     * @return the empty temp file that was created
     * @throws IOException
     *         if a filesystem error occurred creating the temporary file
     */
    public File createTempFile() throws IOException {
        return createTempFile(null);
    }

    /**
     * <p>
     * Creates a new empty file with the specified extension in a new directory
     * inside the system's default temporary directory. See
     * {@link #createTempFile(File, String)} for how to delete the file.
     * </p>
     *
     * @return the empty temp file that was created
     * @param extension
     *        the extension (including the '.') to use for the temporary file's
     *        name (pass <code>null</code> or empty to use the default,
     *        {@link #DEFAULT_EXTENSION})
     * @throws IOException
     *         if a filesystem error occurred creating the temporary file
     */
    public File createTempFile(final String extension) throws IOException {
        return createTempFile(null, extension);
    }

    /**
     * <p>
     * Creates a new empty file with the specified extension in the specified
     * directory.
     * </p>
     * <p>
     * When done using the file, do one of:
     * <ul>
     * <li>Call {@link #cleanUpItem(File)} with the file;
     * {@link TempStorageService} deletes it immediately</li>
     * <li>Do nothing; {@link TempStorageService} will delete it on JVM shutdown
     * </li>
     * <li>Call {@link #forgetItem(File)} with the file to prevent deletion on
     * JVM shutdown; delete it yourself if desired</li>
     * <li>Call {@link #cleanUpAllItems()} to delete all items created by
     * {@link TempStorageService}</li>
     * </ul>
     * </p>
     *
     * @return the temp file that was created
     * @param userDirectory
     *        the directory in which to create this file (which must already
     *        exist). If <code>null</code> or empty, a new directory is created
     *        inside the system's temporary location and the file is created
     *        there and that directory is automatically cleaned up when the file
     *        is cleaned up
     * @param extension
     *        the extension (including the '.') to use for the temporary file
     *        (pass <code>null</code> or empty to use the default).
     * @throws IOException
     *         if a filesystem error occurred creating the temporary file.
     */
    public File createTempFile(final File userDirectory, String extension) throws IOException {
        if (extension == null || extension.length() == 0) {
            extension = DEFAULT_EXTENSION;
        }

        final boolean createDirectory = userDirectory == null || userDirectory.length() == 0;

        /*
         * Create a temp directory, not remembered for clean up.
         */
        final File dir = (createDirectory) ? createTempDirectoryInternal() : userDirectory;

        // Create a file.
        final File file = File.createTempFile("tfs", extension, dir); //$NON-NLS-1$

        /*
         * Remember a clean up item for the temp directory we created if we
         * created one, otherwise for the file itself (the user specified a
         * directory).
         */
        cleanUpItems.put(file,
                new CleanUpItem((createDirectory) ? dir : file, currentSerialNumber.getAndIncrement()));

        if (createDirectory) {
            log.debug(MessageFormat.format("remembered directory ''{0}'' for clean up (parent of ''{1}'')", dir, //$NON-NLS-1$
                    file));
        } else {
            log.debug(MessageFormat.format("remembered file ''{0}'' for clean up", file)); //$NON-NLS-1$
        }

        return file;
    }

    /**
     * Creates a temporary directory (named after a new GUID) inside the
     * system's default temporary directory and returns its full path. When
     * you're done with the object, you may call {@link #cleanUpItem(File)} to
     * have it deleted.
     * <p>
     * If you do not call {@link #cleanUpItem(File)} or
     * {@link #forgetItem(File)} in the future, any future call to
     * {@link #cleanUpAllItems()} will clean it up for you.
     *
     * @return the temp directory that was created
     * @throws IOException
     *         if a filesystem error occurred creating the temporary directory.
     */
    public File createTempDirectory() throws IOException {
        final File dir = createTempDirectoryInternal();

        cleanUpItems.put(dir, new CleanUpItem(dir, currentSerialNumber.getAndIncrement()));

        log.debug(MessageFormat.format("remembered directory ''{0}'' for clean up", dir)); //$NON-NLS-1$

        return dir;
    }

    /**
     * Prevents the {@link TempStorageService} from cleaning up (deleting) this
     * item automatically in the future.
     *
     * @param item
     *        the file or directory created by {@link TempStorageService} that
     *        should not be cleaned up (deleted) by {@link TempStorageService}
     *        in the future (must not be <code>null</code>)
     */
    public synchronized void forgetItem(final File item) {
        Check.notNull(item, "item"); //$NON-NLS-1$

        final CleanUpItem removed = cleanUpItems.remove(item);

        if (removed != null) {
            log.debug(MessageFormat.format("forgot clean up item ''{0}'' for temp item ''{1}''", //$NON-NLS-1$
                    removed.getCleanUpFile(), item));
        } else {
            log.debug(MessageFormat
                    .format("could not forget clean up item for ''{0}'': not found (this is harmless)", item)); //$NON-NLS-1$
        }
    }

    /**
     * Sometime a file cannot be renamed because it's locked by a file system
     * scanning process like anti-viruses software, user virtualization client,
     * Google console, etc. This tries to rename with several retries. If rename
     * fails, the source file copies to the target destination and registers
     * with TempStorageService for future deletion.
     *
     * @param sourceItem
     *        the file or directory to rename (must not be <code>null</code>)
     * @param targetItem
     *        the new file or directory (must not be <code>null</code> and not
     *        exist in the file system)
     */
    public synchronized void renameItem(final File sourceItem, final File targetItem) {
        Check.notNull(sourceItem, "sourceItem"); //$NON-NLS-1$
        Check.notNull(targetItem, "targetItem"); //$NON-NLS-1$

        if (!sourceItem.exists()) {
            throw new RuntimeException(MessageFormat.format(
                    Messages.getString("TempStorageService.RenameErrorSourceDoesNotExistFormat"), //$NON-NLS-1$
                    sourceItem.getAbsolutePath(), targetItem.getAbsolutePath()));
        }
        if (targetItem.exists()) {
            throw new RuntimeException(
                    MessageFormat.format(Messages.getString("TempStorageService.RenameErrorTargetExistsFormat"), //$NON-NLS-1$
                            sourceItem.getAbsolutePath(), targetItem.getAbsolutePath()));
        }

        for (int k = 0; k < MAX_RENAME_ATTEMPTS; k++) {
            if (k > 0) {
                log.debug(MessageFormat.format(
                        "delaying attempt {0} to rename ''{1}'' to ''{2}'' for {3} milliseconds.", //$NON-NLS-1$
                        k + 1, sourceItem.getAbsolutePath(), targetItem.getAbsolutePath(), RENAME_ATTEMPTS_DELAY));

                try {
                    synchronized (Thread.currentThread()) {
                        Thread.currentThread().wait(RENAME_ATTEMPTS_DELAY);
                    }
                } catch (final Exception e) {
                    log.debug(MessageFormat.format("the renaming delay cancelled before attempt {0}.", k + 11)); //$NON-NLS-1$
                    return;
                }
            }

            if (renameInternal(sourceItem, targetItem)) {
                log.debug(MessageFormat.format("attempt {0} to rename ''{1}'' to ''{2}'' succeeded.", //$NON-NLS-1$
                        k + 1, sourceItem.getAbsolutePath(), targetItem.getAbsolutePath()));
                return;
            } else {
                log.debug(MessageFormat.format("attempt {0} to rename ''{1}'' to ''{2}'' failed.", //$NON-NLS-1$
                        k + 1, sourceItem.getAbsolutePath(), targetItem.getAbsolutePath()));
            }
        }

        if (IOUtils.copy(sourceItem, targetItem)) {
            log.debug(MessageFormat.format("copy ''{1}'' to ''{2}'' succeeded.", //$NON-NLS-1$
                    sourceItem.getAbsolutePath(), targetItem.getAbsolutePath()));

        } else {
            log.debug(MessageFormat.format("copy ''{1}'' to ''{2}'' failed.", //$NON-NLS-1$
                    sourceItem.getAbsolutePath(), targetItem.getAbsolutePath()));
        }

        deleteItem(sourceItem);
    }

    private boolean renameInternal(final File sourceItem, final File targetItem) {
        if (nioClassesLoadable && nioClassesLoaded) {
            /*
             * We have already tried reflection and it worked out. Let's
             * continue the same way.
             */
            return renameInternalUsingReflection(sourceItem, targetItem);
        }

        /*
         * We never tried reflection or JDK 1.7 is not available. Let's try
         * first to rename using JDK 1.0
         */
        if (sourceItem.renameTo(targetItem)) {
            return true;
        } else if (nioClassesLoadable) {
            /*
             * Let's try to load JDK 1.7 classes and use reflection
             */
            tryLoadNioClasses();
            return renameInternalUsingReflection(sourceItem, targetItem);
        } else {
            /*
             * We did our best, but were not able neither to rename files nor to
             * provide extended error diagnostic.
             */
            return false;
        }
    }

    private boolean renameInternalUsingReflection(final File sourceItem, final File targetItem) {
        if (nioClassesLoaded) {
            try {
                final Object sourcePath = getMethod.invoke(null, sourceItem.getAbsolutePath(), new String[0]);
                final Object targetPath = getMethod.invoke(null, targetItem.getAbsolutePath(), new String[0]);
                moveMethod.invoke(null, sourcePath, targetPath, copyOptions);

                return true;
            } catch (final Exception e) {
                log.warn(e.getMessage());
                return false;
            }
        } else {
            return false;
        }
    }

    private void tryLoadNioClasses() {
        if (nioClassesLoadable && !nioClassesLoaded) {
            try {
                filesClass = Class.forName("java.nio.file.Files"); //$NON-NLS-1$
                pathInterface = Class.forName("java.nio.file.Path"); //$NON-NLS-1$
                pathsClass = Class.forName("java.nio.file.Paths"); //$NON-NLS-1$
                copyOptionInterfaceArray = Class.forName("[Ljava.nio.file.CopyOption;"); //$NON-NLS-1$
                copyOptionInterface = Class.forName("java.nio.file.CopyOption"); //$NON-NLS-1$

                moveMethod = filesClass.getMethod("move", new Class<?>[] { //$NON-NLS-1$
                        pathInterface, pathInterface, copyOptionInterfaceArray });
                getMethod = pathsClass.getMethod("get", new Class<?>[] { //$NON-NLS-1$
                        String.class, String[].class });

                copyOptions = Array.newInstance(copyOptionInterface, 0);

                nioClassesLoaded = true;
            } catch (final Exception e) {
                log.warn("Cannot load java.nio.file classes: " + e.getMessage()); //$NON-NLS-1$
                nioClassesLoadable = false;
            }
        }
    }

    public synchronized void deleteItem(final File item) {
        log.debug(MessageFormat.format("Trying to delete item ''{1}''.", item.getAbsolutePath())); //$NON-NLS-1$
        if (!item.delete()) {
            log.debug(MessageFormat.format("Remember a clean up item later for ''{1}''.", item.getAbsolutePath())); //$NON-NLS-1$
            cleanUpItems.put(item, new CleanUpItem(item, currentSerialNumber.getAndIncrement()));
        }
    }

    /**
     * Creates a new temp directory in the system's temp directory using a GUID
     * name. Does not remember the directory for clean up.
     *
     * @return the new temp directory (not remembered for clean up)
     */
    private File createTempDirectoryInternal() {
        final File dir = new File(getSystemTempFile(), GUID.newGUIDString());

        dir.mkdirs();

        return dir;
    }

    /**
     * <p>
     * Cleans up (deletes) a file or directory which was allocated by this
     * service. If the item is a file in a directory which was also created by
     * the service, that directory is cleaned up (recursively).
     * </p>
     * <p>
     * If the path was not allocated by this {@link TempStorageService} or was
     * already cleaned up, no exception is thrown and the item is ignored.
     * </p>
     *
     * @param item
     *        an item that was created by {@link #createTempFile()} or
     *        {@link #createTempDirectory()} (must not be <code>null</code>)
     */
    public synchronized void cleanUpItem(final File item) {
        Check.notNull(item, "item"); //$NON-NLS-1$

        final CleanUpItem tempItem = cleanUpItems.get(item);

        if (tempItem == null) {
            log.debug(MessageFormat.format("could not clean up for item ''{0}'': not found (this is harmless)", //$NON-NLS-1$
                    item));
            return;
        }

        cleanUpItemInternal(tempItem);

        if (!item.exists()) {
            cleanUpItems.remove(item);
        }
    }

    /**
     * Cleans up (deletes) files and directories allocated by this service. Can
     * be called multiple times on a single {@link TempStorageService} instance.
     */
    public synchronized void cleanUpAllItems() {
        final Collection<CleanUpItem> values = cleanUpItems.values();

        if (values.size() == 0) {
            return;
        }

        // Sort them so we delete them in the order created.
        final CleanUpItem[] items = values.toArray(new CleanUpItem[values.size()]);
        Arrays.sort(items);

        for (int i = 0; i < items.length; i++) {
            cleanUpItemInternal(items[i]);
        }

        cleanUpItems.clear();
    }

    /**
     * Cleans up one {@link CleanUpItem}.
     *
     * @param tempItem
     *        the item that should be deleted from disk (must not be
     *        <code>null</code>)
     */
    private void cleanUpItemInternal(final CleanUpItem tempItem) {
        Check.notNull(tempItem, "tempItem"); //$NON-NLS-1$

        final File cleanUpFile = tempItem.getCleanUpFile();

        log.debug(MessageFormat.format("deleting ''{0}'' (recursively)", cleanUpFile)); //$NON-NLS-1$

        deleteRecursive(cleanUpFile);
    }

    /**
     * Deletes a {@link File} (whether actually directory or file), recursing
     * down any directories it finds.
     *
     * @param fileOrDirectory
     *        the local disk file or directory to delete (if <code>null</code>,
     *        returns immediately)
     */
    private void deleteRecursive(final File fileOrDirectory) {
        if (fileOrDirectory == null) {
            return;
        }

        final File[] subFiles = fileOrDirectory.listFiles();

        if (subFiles == null) {
            // Object is a file, not a directory.
            if (fileOrDirectory.delete() == false) {
                log.warn(MessageFormat.format("could not delete ''{0}'' (no futher information available)", //$NON-NLS-1$
                        fileOrDirectory));
            }
        } else {
            // Recurse into each child directory.
            for (int i = 0; i < subFiles.length; i++) {
                deleteRecursive(subFiles[i]);
            }

            if (fileOrDirectory.delete() == false) {
                log.warn(
                        MessageFormat.format("could not delete directory ''{0}'' (no futher information available)", //$NON-NLS-1$
                                fileOrDirectory.getAbsolutePath()));
            }
        }
    }

    /**
     * @return the system's temporary directory (precisely, java.io.tmpdir
     *         property).
     */
    private synchronized File getSystemTempFile() {
        if (systemTempDir == null) {
            systemTempDir = new File(System.getProperty("java.io.tmpdir")); //$NON-NLS-1$
        }

        return systemTempDir;
    }
}