    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.io;
    018
    019import java.io.File;
    020import java.lang.ref.PhantomReference;
    021import java.lang.ref.ReferenceQueue;
    022import java.util.ArrayList;
    023import java.util.Collection;
    024import java.util.Collections;
    025import java.util.HashSet;
    026import java.util.List;
    027import java.util.Objects;
    028
    029/**
    030 * Keeps track of files awaiting deletion, and deletes them when an associated
    031 * marker object is reclaimed by the garbage collector.
    032 * <p>
    033 * This utility creates a background thread to handle file deletion.
    034 * Each file to be deleted is registered with a handler object.
    035 * When the handler object is garbage collected, the file is deleted.
    036 * <p>
    037 * In an environment with multiple class loaders (a servlet container, for
    038 * example), you should consider stopping the background thread if it is no
    039 * longer needed. This is done by invoking the method
    040 * {@link #exitWhenFinished}, typically in
    041 * {@code javax.servlet.ServletContextListener.contextDestroyed(javax.servlet.ServletContextEvent)} or similar.
    042 *
    043 */
    044public class FileCleaningTracker {
    045
    046    // Note: fields are package protected to allow use by test cases
    047
    048    /**
    049     * Queue of {@code Tracker} instances being watched.
    050     */
    051    ReferenceQueue<Object> q = new ReferenceQueue<>();
    052    /**
    053     * Collection of {@code Tracker} instances in existence.
    054     */
    055    final Collection<Tracker> trackers = Collections.synchronizedSet(new HashSet<>()); // synchronized
    056    /**
    057     * Collection of File paths that failed to delete.
    058     */
    059    final List<String> deleteFailures = Collections.synchronizedList(new ArrayList<>());
    060    /**
    061     * Whether to terminate the thread when the tracking is complete.
    062     */
    063    volatile boolean exitWhenFinished;
    064    /**
    065     * The thread that will clean up registered files.
    066     */
    067    Thread reaper;
    068
    069    /**
    070     * Track the specified file, using the provided marker, deleting the file
    071     * when the marker instance is garbage collected.
    072     * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
    073     *
    074     * @param file  the file to be tracked, not null
    075     * @param marker  the marker object used to track the file, not null
    076     * @throws NullPointerException if the file is null
    077     */
    078    public void track(final File file, final Object marker) {
    079        track(file, marker, null);
    080    }
    081
    082    /**
    083     * Track the specified file, using the provided marker, deleting the file
    084     * when the marker instance is garbage collected.
    085     * The specified deletion strategy is used.
    086     *
    087     * @param file  the file to be tracked, not null
    088     * @param marker  the marker object used to track the file, not null
    089     * @param deleteStrategy  the strategy to delete the file, null means normal
    090     * @throws NullPointerException if the file is null
    091     */
    092    public void track(final File file, final Object marker, final FileDeleteStrategy deleteStrategy) {
    093        Objects.requireNonNull(file, "file");
    094        addTracker(file.getPath(), marker, deleteStrategy);
    095    }
    096
    097    /**
    098     * Track the specified file, using the provided marker, deleting the file
    099     * when the marker instance is garbage collected.
    100     * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
    101     *
    102     * @param path  the full path to the file to be tracked, not null
    103     * @param marker  the marker object used to track the file, not null
    104     * @throws NullPointerException if the path is null
    105     */
    106    public void track(final String path, final Object marker) {
    107        track(path, marker, null);
    108    }
    109
    110    /**
    111     * Track the specified file, using the provided marker, deleting the file
    112     * when the marker instance is garbage collected.
    113     * The specified deletion strategy is used.
    114     *
    115     * @param path  the full path to the file to be tracked, not null
    116     * @param marker  the marker object used to track the file, not null
    117     * @param deleteStrategy  the strategy to delete the file, null means normal
    118     * @throws NullPointerException if the path is null
    119     */
    120    public void track(final String path, final Object marker, final FileDeleteStrategy deleteStrategy) {
    121        Objects.requireNonNull(path, "path");
    122        addTracker(path, marker, deleteStrategy);
    123    }
    124
    125    /**
    126     * Adds a tracker to the list of trackers.
    127     *
    128     * @param path  the full path to the file to be tracked, not null
    129     * @param marker  the marker object used to track the file, not null
    130     * @param deleteStrategy  the strategy to delete the file, null means normal
    131     */
    132    private synchronized void addTracker(final String path, final Object marker, final FileDeleteStrategy
    133            deleteStrategy) {
    134        // synchronized block protects reaper
    135        if (exitWhenFinished) {
    136            throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called");
    137        }
    138        if (reaper == null) {
    139            reaper = new Reaper();
    140            reaper.start();
    141        }
    142        trackers.add(new Tracker(path, deleteStrategy, marker, q));
    143    }
    144
    145    /**
    146     * Retrieve the number of files currently being tracked, and therefore
    147     * awaiting deletion.
    148     *
    149     * @return the number of files being tracked
    150     */
    151    public int getTrackCount() {
    152        return trackers.size();
    153    }
    154
    155    /**
    156     * Return the file paths that failed to delete.
    157     *
    158     * @return the file paths that failed to delete
    159     * @since 2.0
    160     */
    161    public List<String> getDeleteFailures() {
    162        return deleteFailures;
    163    }
    164
    165    /**
    166     * Call this method to cause the file cleaner thread to terminate when
    167     * there are no more objects being tracked for deletion.
    168     * <p>
    169     * In a simple environment, you don't need this method as the file cleaner
    170     * thread will simply exit when the JVM exits. In a more complex environment,
    171     * with multiple class loaders (such as an application server), you should be
    172     * aware that the file cleaner thread will continue running even if the class
    173     * loader it was started from terminates. This can constitute a memory leak.
    174     * <p>
    175     * For example, suppose that you have developed a web application, which
    176     * contains the commons-io jar file in your WEB-INF/lib directory. In other
    177     * words, the FileCleaner class is loaded through the class loader of your
    178     * web application. If the web application is terminated, but the servlet
    179     * container is still running, then the file cleaner thread will still exist,
    180     * posing a memory leak.
    181     * <p>
    182     * This method allows the thread to be terminated. Simply call this method
    183     * in the resource cleanup code, such as
    184     * {@code javax.servlet.ServletContextListener.contextDestroyed(javax.servlet.ServletContextEvent)}.
    185     * Once called, no new objects can be tracked by the file cleaner.
    186     */
    187    public synchronized void exitWhenFinished() {
    188        // synchronized block protects reaper
    189        exitWhenFinished = true;
    190        if (reaper != null) {
    191            synchronized (reaper) {
    192                reaper.interrupt();
    193            }
    194        }
    195    }
    196
    197    /**
    198     * The reaper thread.
    199     */
    200    private final class Reaper extends Thread {
    201        /** Construct a new Reaper */
    202        Reaper() {
    203            super("File Reaper");
    204            setPriority(Thread.MAX_PRIORITY);
    205            setDaemon(true);
    206        }
    207
    208        /**
    209         * Run the reaper thread that will delete files as their associated
    210         * marker objects are reclaimed by the garbage collector.
    211         */
    212        @Override
    213        public void run() {
    214            // thread exits when exitWhenFinished is true and there are no more tracked objects
    215            while (!exitWhenFinished || !trackers.isEmpty()) {
    216                try {
    217                    // Wait for a tracker to remove.
    218                    final Tracker tracker = (Tracker) q.remove(); // cannot return null
    219                    trackers.remove(tracker);
    220                    if (!tracker.delete()) {
    221                        deleteFailures.add(tracker.getPath());
    222                    }
    223                    tracker.clear();
    224                } catch (final InterruptedException e) {
    225                    continue;
    226                }
    227            }
    228        }
    229    }
    230
    231    /**
    232     * Inner class which acts as the reference for a file pending deletion.
    233     */
    234    private static final class Tracker extends PhantomReference<Object> {
    235
    236        /**
    237         * The full path to the file being tracked.
    238         */
    239        private final String path;
    240        /**
    241         * The strategy for deleting files.
    242         */
    243        private final FileDeleteStrategy deleteStrategy;
    244
    245        /**
    246         * Constructs an instance of this class from the supplied parameters.
    247         *
    248         * @param path  the full path to the file to be tracked, not null
    249         * @param deleteStrategy  the strategy to delete the file, null means normal
    250         * @param marker  the marker object used to track the file, not null
    251         * @param queue  the queue on to which the tracker will be pushed, not null
    252         */
    253        Tracker(final String path, final FileDeleteStrategy deleteStrategy, final Object marker,
    254                final ReferenceQueue<? super Object> queue) {
    255            super(marker, queue);
    256            this.path = path;
    257            this.deleteStrategy = deleteStrategy == null ? FileDeleteStrategy.NORMAL : deleteStrategy;
    258        }
    259
    260        /**
    261         * Return the path.
    262         *
    263         * @return the path
    264         */
    265        public String getPath() {
    266            return path;
    267        }
    268
    269        /**
    270         * Deletes the file associated with this tracker instance.
    271         *
    272         * @return {@code true} if the file was deleted successfully;
    273         *         {@code false} otherwise.
    274         */
    275        public boolean delete() {
    276            return deleteStrategy.deleteQuietly(new File(path));
    277        }
    278    }
    279
    280}