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