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