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