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