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 */ 17 package org.apache.commons.io; 18 19 import java.io.File; 20 import java.lang.ref.PhantomReference; 21 import java.lang.ref.ReferenceQueue; 22 import java.nio.file.Path; 23 import java.util.ArrayList; 24 import java.util.Collection; 25 import java.util.Collections; 26 import java.util.HashSet; 27 import java.util.List; 28 import java.util.Objects; 29 30 /** 31 * Keeps track of files awaiting deletion, and deletes them when an associated 32 * marker object is reclaimed by the garbage collector. 33 * <p> 34 * This utility creates a background thread to handle file deletion. 35 * Each file to be deleted is registered with a handler object. 36 * When the handler object is garbage collected, the file is deleted. 37 * </p> 38 * <p> 39 * In an environment with multiple class loaders (a servlet container, for 40 * example), you should consider stopping the background thread if it is no 41 * longer needed. This is done by invoking the method 42 * {@link #exitWhenFinished}, typically in 43 * {@code javax.servlet.ServletContextListener.contextDestroyed(javax.servlet.ServletContextEvent)} or similar. 44 * </p> 45 */ 46 public class FileCleaningTracker { 47 48 // Note: fields are package protected to allow use by test cases 49 50 /** 51 * The reaper thread. 52 */ 53 private final class Reaper extends Thread { 54 /** Constructs a new Reaper */ 55 Reaper() { 56 super("File Reaper"); 57 setPriority(MAX_PRIORITY); 58 setDaemon(true); 59 } 60 61 /** 62 * Runs the reaper thread that will delete files as their associated 63 * marker objects are reclaimed by the garbage collector. 64 */ 65 @Override 66 public void run() { 67 // thread exits when exitWhenFinished is true and there are no more tracked objects 68 while (!exitWhenFinished || !trackers.isEmpty()) { 69 try { 70 // Wait for a tracker to remove. 71 final Tracker tracker = (Tracker) q.remove(); // cannot return null 72 trackers.remove(tracker); 73 if (!tracker.delete()) { 74 deleteFailures.add(tracker.getPath()); 75 } 76 tracker.clear(); 77 } catch (final InterruptedException e) { 78 continue; 79 } 80 } 81 } 82 } 83 84 /** 85 * Inner class which acts as the reference for a file pending deletion. 86 */ 87 private static final class Tracker extends PhantomReference<Object> { 88 89 /** 90 * The full path to the file being tracked. 91 */ 92 private final String path; 93 94 /** 95 * The strategy for deleting files. 96 */ 97 private final FileDeleteStrategy deleteStrategy; 98 99 /** 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 }