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