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 * 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}