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 * https://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.Collections;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Objects;
28 import java.util.Set;
29
30 /**
31 * Tracks 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
55 /** Constructs a new Reaper */
56 Reaper() {
57 super("commons-io-FileCleaningTracker-Reaper");
58 setPriority(MAX_PRIORITY);
59 setDaemon(true);
60 }
61
62 /**
63 * Runs the reaper thread that will delete files as their associated
64 * marker objects are reclaimed by the garbage collector.
65 */
66 @Override
67 public void run() {
68 // thread exits when exitWhenFinished is true and there are no more tracked objects
69 while (!(exitWhenFinished && trackers.isEmpty())) {
70 try {
71 // Wait for a tracker to remove.
72 final Tracker tracker = (Tracker) refQueue.remove(); // cannot return null
73 trackers.remove(tracker);
74 if (!tracker.delete()) {
75 deleteFailures.add(tracker.getPath());
76 }
77 tracker.clear();
78 } catch (final InterruptedException e) {
79 // interrupted removing from the queue.
80 interrupt();
81 continue;
82 }
83 }
84 }
85 }
86
87 /**
88 * Inner class which acts as the reference for a file pending deletion.
89 */
90 private static final class Tracker extends PhantomReference<Object> {
91
92 /**
93 * The full path to the file being tracked.
94 */
95 private final String path;
96
97 /**
98 * The strategy for deleting files.
99 */
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 }