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