1/*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements.  See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License.  You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17package org.apache.commons.io;
18
19import java.io.File;
20import java.lang.ref.PhantomReference;
21import java.lang.ref.ReferenceQueue;
22import java.util.Collection;
23import java.util.Vector;
24
25/**
26 * Keeps track of files awaiting deletion, and deletes them when an associated
27 * marker object is reclaimed by the garbage collector.
28 * <p>
29 * This utility creates a background thread to handle file deletion.
30 * Each file to be deleted is registered with a handler object.
31 * When the handler object is garbage collected, the file is deleted.
32 * <p>
33 * In an environment with multiple class loaders (a servlet container, for
34 * example), you should consider stopping the background thread if it is no
35 * longer needed. This is done by invoking the method
36 * {@link #exitWhenFinished}, typically in
37 * {@link javax.servlet.ServletContextListener#contextDestroyed} or similar.
38 *
39 * @author Noel Bergman
40 * @author Martin Cooper
41 * @version $Id: FileCleaner.java 490987 2006-12-29 12:11:48Z scolebourne $
42 */
43public class FileCleaningTracker {
44    /**
45     * Queue of <code>Tracker</code> instances being watched.
46     */
47    ReferenceQueue<Object> /* Tracker */ q = new ReferenceQueue<Object>();
48    /**
49     * Collection of <code>Tracker</code> instances in existence.
50     */
51    final Collection<Tracker> /* Tracker */ trackers = new Vector<Tracker>();  // synchronized
52    /**
53     * Whether to terminate the thread when the tracking is complete.
54     */
55    volatile boolean exitWhenFinished = false;
56    /**
57     * The thread that will clean up registered files.
58     */
59    Thread reaper;
60
61    //-----------------------------------------------------------------------
62    /**
63     * Track the specified file, using the provided marker, deleting the file
64     * when the marker instance is garbage collected.
65     * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
66     *
67     * @param file  the file to be tracked, not null
68     * @param marker  the marker object used to track the file, not null
69     * @throws NullPointerException if the file is null
70     */
71    public void track(File file, Object marker) {
72        track(file, marker, (FileDeleteStrategy) null);
73    }
74
75    /**
76     * Track the specified file, using the provided marker, deleting the file
77     * when the marker instance is garbage collected.
78     * The speified deletion strategy is used.
79     *
80     * @param file  the file to be tracked, not null
81     * @param marker  the marker object used to track the file, not null
82     * @param deleteStrategy  the strategy to delete the file, null means normal
83     * @throws NullPointerException if the file is null
84     */
85    public void track(File file, Object marker, FileDeleteStrategy deleteStrategy) {
86        if (file == null) {
87            throw new NullPointerException("The file must not be null");
88        }
89        addTracker(file.getPath(), marker, deleteStrategy);
90    }
91
92    /**
93     * Track the specified file, using the provided marker, deleting the file
94     * when the marker instance is garbage collected.
95     * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
96     *
97     * @param path  the full path to the file to be tracked, not null
98     * @param marker  the marker object used to track the file, not null
99     * @throws NullPointerException if the path is null
100     */
101    public void track(String path, Object marker) {
102        track(path, marker, (FileDeleteStrategy) null);
103    }
104
105    /**
106     * Track the specified file, using the provided marker, deleting the file
107     * when the marker instance is garbage collected.
108     * The speified deletion strategy is used.
109     *
110     * @param path  the full path to the file to be tracked, not null
111     * @param marker  the marker object used to track the file, not null
112     * @param deleteStrategy  the strategy to delete the file, null means normal
113     * @throws NullPointerException if the path is null
114     */
115    public void track(String path, Object marker, FileDeleteStrategy deleteStrategy) {
116        if (path == null) {
117            throw new NullPointerException("The path must not be null");
118        }
119        addTracker(path, marker, deleteStrategy);
120    }
121
122    /**
123     * Adds a tracker to the list of trackers.
124     *
125     * @param path  the full path to the file to be tracked, not null
126     * @param marker  the marker object used to track the file, not null
127     * @param deleteStrategy  the strategy to delete the file, null means normal
128     */
129    private synchronized void addTracker(String path, Object marker, FileDeleteStrategy deleteStrategy) {
130        // synchronized block protects reaper
131        if (exitWhenFinished) {
132            throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called");
133        }
134        if (reaper == null) {
135            reaper = new Reaper();
136            reaper.start();
137        }
138        trackers.add(new Tracker(path, deleteStrategy, marker, q));
139    }
140
141    //-----------------------------------------------------------------------
142    /**
143     * Retrieve the number of files currently being tracked, and therefore
144     * awaiting deletion.
145     *
146     * @return the number of files being tracked
147     */
148    public int getTrackCount() {
149        return trackers.size();
150    }
151
152    /**
153     * Call this method to cause the file cleaner thread to terminate when
154     * there are no more objects being tracked for deletion.
155     * <p>
156     * In a simple environment, you don't need this method as the file cleaner
157     * thread will simply exit when the JVM exits. In a more complex environment,
158     * with multiple class loaders (such as an application server), you should be
159     * aware that the file cleaner thread will continue running even if the class
160     * loader it was started from terminates. This can consitute a memory leak.
161     * <p>
162     * For example, suppose that you have developed a web application, which
163     * contains the commons-io jar file in your WEB-INF/lib directory. In other
164     * words, the FileCleaner class is loaded through the class loader of your
165     * web application. If the web application is terminated, but the servlet
166     * container is still running, then the file cleaner thread will still exist,
167     * posing a memory leak.
168     * <p>
169     * This method allows the thread to be terminated. Simply call this method
170     * in the resource cleanup code, such as {@link javax.servlet.ServletContextListener#contextDestroyed}.
171     * One called, no new objects can be tracked by the file cleaner.
172     */
173    public synchronized void exitWhenFinished() {
174        // synchronized block protects reaper
175        exitWhenFinished = true;
176        if (reaper != null) {
177            synchronized (reaper) {
178                reaper.interrupt();
179            }
180        }
181    }
182
183    //-----------------------------------------------------------------------
184    /**
185     * The reaper thread.
186     */
187    private final class Reaper extends Thread {
188        /** Construct a new Reaper */
189        Reaper() {
190            super("File Reaper");
191            setPriority(Thread.MAX_PRIORITY);
192            setDaemon(true);
193        }
194
195        /**
196         * Run the reaper thread that will delete files as their associated
197         * marker objects are reclaimed by the garbage collector.
198         */
199        @Override
200        public void run() {
201            // thread exits when exitWhenFinished is true and there are no more tracked objects
202            while (exitWhenFinished == false || trackers.size() > 0) {
203                Tracker tracker = null;
204                try {
205                    // Wait for a tracker to remove.
206                    tracker = (Tracker) q.remove();
207                } catch (Exception e) {
208                    continue;
209                }
210                if (tracker != null) {
211                    tracker.delete();
212                    tracker.clear();
213                    trackers.remove(tracker);
214                }
215            }
216        }
217    }
218
219    //-----------------------------------------------------------------------
220    /**
221     * Inner class which acts as the reference for a file pending deletion.
222     */
223    private static final class Tracker extends PhantomReference<Object> {
224
225        /**
226         * The full path to the file being tracked.
227         */
228        private final String path;
229        /**
230         * The strategy for deleting files.
231         */
232        private final FileDeleteStrategy deleteStrategy;
233
234        /**
235         * Constructs an instance of this class from the supplied parameters.
236         *
237         * @param path  the full path to the file to be tracked, not null
238         * @param deleteStrategy  the strategy to delete the file, null means normal
239         * @param marker  the marker object used to track the file, not null
240         * @param queue  the queue on to which the tracker will be pushed, not null
241         */
242        Tracker(String path, FileDeleteStrategy deleteStrategy, Object marker, ReferenceQueue<Object> queue) {
243            super(marker, queue);
244            this.path = path;
245            this.deleteStrategy = (deleteStrategy == null ? FileDeleteStrategy.NORMAL : deleteStrategy);
246        }
247
248        /**
249         * Deletes the file associated with this tracker instance.
250         *
251         * @return <code>true</code> if the file was deleted successfully;
252         *         <code>false</code> otherwise.
253         */
254        public boolean delete() {
255            return deleteStrategy.deleteQuietly(new File(path));
256        }
257    }
258
259}
260