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