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(Thread.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 * Adds a tracker to the list of trackers. 161 * 162 * @param path the full path to the file to be tracked, not null 163 * @param marker the marker object used to track the file, not null 164 * @param deleteStrategy the strategy to delete the file, null means normal 165 */ 166 private synchronized void addTracker(final String path, final Object marker, final FileDeleteStrategy 167 deleteStrategy) { 168 // synchronized block protects reaper 169 if (exitWhenFinished) { 170 throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called"); 171 } 172 if (reaper == null) { 173 reaper = new Reaper(); 174 reaper.start(); 175 } 176 trackers.add(new Tracker(path, deleteStrategy, marker, q)); 177 } 178 179 /** 180 * Call this method to cause the file cleaner thread to terminate when 181 * there are no more objects being tracked for deletion. 182 * <p> 183 * In a simple environment, you don't need this method as the file cleaner 184 * thread will simply exit when the JVM exits. In a more complex environment, 185 * with multiple class loaders (such as an application server), you should be 186 * aware that the file cleaner thread will continue running even if the class 187 * loader it was started from terminates. This can constitute a memory leak. 188 * <p> 189 * For example, suppose that you have developed a web application, which 190 * contains the commons-io jar file in your WEB-INF/lib directory. In other 191 * words, the FileCleaner class is loaded through the class loader of your 192 * web application. If the web application is terminated, but the servlet 193 * container is still running, then the file cleaner thread will still exist, 194 * posing a memory leak. 195 * <p> 196 * This method allows the thread to be terminated. Simply call this method 197 * in the resource cleanup code, such as 198 * {@code javax.servlet.ServletContextListener.contextDestroyed(javax.servlet.ServletContextEvent)}. 199 * Once called, no new objects can be tracked by the file cleaner. 200 */ 201 public synchronized void exitWhenFinished() { 202 // synchronized block protects reaper 203 exitWhenFinished = true; 204 if (reaper != null) { 205 synchronized (reaper) { 206 reaper.interrupt(); 207 } 208 } 209 } 210 211 /** 212 * Gets a copy of the file paths that failed to delete. 213 * 214 * @return a copy of the file paths that failed to delete 215 * @since 2.0 216 */ 217 public List<String> getDeleteFailures() { 218 return new ArrayList<>(deleteFailures); 219 } 220 221 /** 222 * Gets the number of files currently being tracked, and therefore 223 * awaiting deletion. 224 * 225 * @return the number of files being tracked 226 */ 227 public int getTrackCount() { 228 return trackers.size(); 229 } 230 231 /** 232 * Tracks the specified file, using the provided marker, deleting the file 233 * when the marker instance is garbage collected. 234 * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used. 235 * 236 * @param file the file to be tracked, not null 237 * @param marker the marker object used to track the file, not null 238 * @throws NullPointerException if the file is null 239 */ 240 public void track(final File file, final Object marker) { 241 track(file, marker, null); 242 } 243 244 /** 245 * Tracks the specified file, using the provided marker, deleting the file 246 * when the marker instance is garbage collected. 247 * The specified deletion strategy is used. 248 * 249 * @param file the file to be tracked, not null 250 * @param marker the marker object used to track the file, not null 251 * @param deleteStrategy the strategy to delete the file, null means normal 252 * @throws NullPointerException if the file is null 253 */ 254 public void track(final File file, final Object marker, final FileDeleteStrategy deleteStrategy) { 255 Objects.requireNonNull(file, "file"); 256 addTracker(file.getPath(), marker, deleteStrategy); 257 } 258 259 /** 260 * Tracks the specified file, using the provided marker, deleting the file 261 * when the marker instance is garbage collected. 262 * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used. 263 * 264 * @param file the file to be tracked, not null 265 * @param marker the marker object used to track the file, not null 266 * @throws NullPointerException if the file is null 267 * @since 2.14.0 268 */ 269 public void track(final Path file, final Object marker) { 270 track(file, marker, null); 271 } 272 273 /** 274 * Tracks the specified file, using the provided marker, deleting the file 275 * when the marker instance is garbage collected. 276 * The specified deletion strategy is used. 277 * 278 * @param file the file to be tracked, not null 279 * @param marker the marker object used to track the file, not null 280 * @param deleteStrategy the strategy to delete the file, null means normal 281 * @throws NullPointerException if the file is null 282 * @since 2.14.0 283 */ 284 public void track(final Path file, final Object marker, final FileDeleteStrategy deleteStrategy) { 285 Objects.requireNonNull(file, "file"); 286 addTracker(file.toAbsolutePath().toString(), marker, deleteStrategy); 287 } 288 289 /** 290 * Tracks the specified file, using the provided marker, deleting the file 291 * when the marker instance is garbage collected. 292 * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used. 293 * 294 * @param path the full path to the file to be tracked, not null 295 * @param marker the marker object used to track the file, not null 296 * @throws NullPointerException if the path is null 297 */ 298 public void track(final String path, final Object marker) { 299 track(path, marker, null); 300 } 301 302 /** 303 * Tracks the specified file, using the provided marker, deleting the file 304 * when the marker instance is garbage collected. 305 * The specified deletion strategy is used. 306 * 307 * @param path the full path to the file to be tracked, not null 308 * @param marker the marker object used to track the file, not null 309 * @param deleteStrategy the strategy to delete the file, null means normal 310 * @throws NullPointerException if the path is null 311 */ 312 public void track(final String path, final Object marker, final FileDeleteStrategy deleteStrategy) { 313 Objects.requireNonNull(path, "path"); 314 addTracker(path, marker, deleteStrategy); 315 } 316 317 }