    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.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(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     * Construct a new instance.
    161     */
    162    public FileCleaningTracker() {
    163        // empty
    164    }
    165
    166    /**
    167     * Adds a tracker to the list of trackers.
    168     *
    169     * @param path  the full path to the file to be tracked, not null
    170     * @param marker  the marker object used to track the file, not null
    171     * @param deleteStrategy  the strategy to delete the file, null means normal
    172     */
    173    private synchronized void addTracker(final String path, final Object marker, final FileDeleteStrategy
    174            deleteStrategy) {
    175        // synchronized block protects reaper
    176        if (exitWhenFinished) {
    177            throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called");
    178        }
    179        if (reaper == null) {
    180            reaper = new Reaper();
    181            reaper.start();
    182        }
    183        trackers.add(new Tracker(path, deleteStrategy, marker, q));
    184    }
    185
    186    /**
    187     * Call this method to cause the file cleaner thread to terminate when
    188     * there are no more objects being tracked for deletion.
    189     * <p>
    190     * In a simple environment, you don't need this method as the file cleaner
    191     * thread will simply exit when the JVM exits. In a more complex environment,
    192     * with multiple class loaders (such as an application server), you should be
    193     * aware that the file cleaner thread will continue running even if the class
    194     * loader it was started from terminates. This can constitute a memory leak.
    195     * <p>
    196     * For example, suppose that you have developed a web application, which
    197     * contains the commons-io jar file in your WEB-INF/lib directory. In other
    198     * words, the FileCleaner class is loaded through the class loader of your
    199     * web application. If the web application is terminated, but the servlet
    200     * container is still running, then the file cleaner thread will still exist,
    201     * posing a memory leak.
    202     * <p>
    203     * This method allows the thread to be terminated. Simply call this method
    204     * in the resource cleanup code, such as
    205     * {@code javax.servlet.ServletContextListener.contextDestroyed(javax.servlet.ServletContextEvent)}.
    206     * Once called, no new objects can be tracked by the file cleaner.
    207     */
    208    public synchronized void exitWhenFinished() {
    209        // synchronized block protects reaper
    210        exitWhenFinished = true;
    211        if (reaper != null) {
    212            synchronized (reaper) {
    213                reaper.interrupt();
    214            }
    215        }
    216    }
    217
    218    /**
    219     * Gets a copy of the file paths that failed to delete.
    220     *
    221     * @return a copy of the file paths that failed to delete
    222     * @since 2.0
    223     */
    224    public List<String> getDeleteFailures() {
    225        return new ArrayList<>(deleteFailures);
    226    }
    227
    228    /**
    229     * Gets the number of files currently being tracked, and therefore
    230     * awaiting deletion.
    231     *
    232     * @return the number of files being tracked
    233     */
    234    public int getTrackCount() {
    235        return trackers.size();
    236    }
    237
    238    /**
    239     * Tracks the specified file, using the provided marker, deleting the file
    240     * when the marker instance is garbage collected.
    241     * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
    242     *
    243     * @param file  the file to be tracked, not null
    244     * @param marker  the marker object used to track the file, not null
    245     * @throws NullPointerException if the file is null
    246     */
    247    public void track(final File file, final Object marker) {
    248        track(file, marker, null);
    249    }
    250
    251    /**
    252     * Tracks the specified file, using the provided marker, deleting the file
    253     * when the marker instance is garbage collected.
    254     * The specified deletion strategy is used.
    255     *
    256     * @param file  the file to be tracked, not null
    257     * @param marker  the marker object used to track the file, not null
    258     * @param deleteStrategy  the strategy to delete the file, null means normal
    259     * @throws NullPointerException if the file is null
    260     */
    261    public void track(final File file, final Object marker, final FileDeleteStrategy deleteStrategy) {
    262        Objects.requireNonNull(file, "file");
    263        addTracker(file.getPath(), marker, deleteStrategy);
    264    }
    265
    266    /**
    267     * Tracks the specified file, using the provided marker, deleting the file
    268     * when the marker instance is garbage collected.
    269     * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
    270     *
    271     * @param file  the file to be tracked, not null
    272     * @param marker  the marker object used to track the file, not null
    273     * @throws NullPointerException if the file is null
    274     * @since 2.14.0
    275     */
    276    public void track(final Path file, final Object marker) {
    277        track(file, marker, null);
    278    }
    279
    280    /**
    281     * Tracks the specified file, using the provided marker, deleting the file
    282     * when the marker instance is garbage collected.
    283     * The specified deletion strategy is used.
    284     *
    285     * @param file  the file to be tracked, not null
    286     * @param marker  the marker object used to track the file, not null
    287     * @param deleteStrategy  the strategy to delete the file, null means normal
    288     * @throws NullPointerException if the file is null
    289     * @since 2.14.0
    290     */
    291    public void track(final Path file, final Object marker, final FileDeleteStrategy deleteStrategy) {
    292        Objects.requireNonNull(file, "file");
    293        addTracker(file.toAbsolutePath().toString(), marker, deleteStrategy);
    294    }
    295
    296    /**
    297     * Tracks the specified file, using the provided marker, deleting the file
    298     * when the marker instance is garbage collected.
    299     * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
    300     *
    301     * @param path  the full path to the file to be tracked, not null
    302     * @param marker  the marker object used to track the file, not null
    303     * @throws NullPointerException if the path is null
    304     */
    305    public void track(final String path, final Object marker) {
    306        track(path, marker, null);
    307    }
    308
    309    /**
    310     * Tracks the specified file, using the provided marker, deleting the file
    311     * when the marker instance is garbage collected.
    312     * The specified deletion strategy is used.
    313     *
    314     * @param path  the full path to the file to be tracked, not null
    315     * @param marker  the marker object used to track the file, not null
    316     * @param deleteStrategy  the strategy to delete the file, null means normal
    317     * @throws NullPointerException if the path is null
    318     */
    319    public void track(final String path, final Object marker, final FileDeleteStrategy deleteStrategy) {
    320        Objects.requireNonNull(path, "path");
    321        addTracker(path, marker, deleteStrategy);
    322    }
    323
    324}