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.nio.file.Path; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.HashSet; 027import java.util.List; 028import java.util.Objects; 029 030/** 031 * Keeps track of 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 /** Constructs a new Reaper */ 055 Reaper() { 056 super("File Reaper"); 057 setPriority(MAX_PRIORITY); 058 setDaemon(true); 059 } 060 061 /** 062 * Runs the reaper thread that will delete files as their associated 063 * marker objects are reclaimed by the garbage collector. 064 */ 065 @Override 066 public void run() { 067 // thread exits when exitWhenFinished is true and there are no more tracked objects 068 while (!exitWhenFinished || !trackers.isEmpty()) { 069 try { 070 // Wait for a tracker to remove. 071 final Tracker tracker = (Tracker) q.remove(); // cannot return null 072 trackers.remove(tracker); 073 if (!tracker.delete()) { 074 deleteFailures.add(tracker.getPath()); 075 } 076 tracker.clear(); 077 } catch (final InterruptedException e) { 078 continue; 079 } 080 } 081 } 082 } 083 084 /** 085 * Inner class which acts as the reference for a file pending deletion. 086 */ 087 private static final class Tracker extends PhantomReference<Object> { 088 089 /** 090 * The full path to the file being tracked. 091 */ 092 private final String path; 093 094 /** 095 * The strategy for deleting files. 096 */ 097 private final FileDeleteStrategy deleteStrategy; 098 099 /** 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}