View Javadoc
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.util.ArrayList;
23  import java.util.Collection;
24  import java.util.Collections;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Objects;
28  
29  /**
30   * Keeps track of files awaiting deletion, and deletes them when an associated
31   * marker object is reclaimed by the garbage collector.
32   * <p>
33   * This utility creates a background thread to handle file deletion.
34   * Each file to be deleted is registered with a handler object.
35   * When the handler object is garbage collected, the file is deleted.
36   * <p>
37   * In an environment with multiple class loaders (a servlet container, for
38   * example), you should consider stopping the background thread if it is no
39   * longer needed. This is done by invoking the method
40   * {@link #exitWhenFinished}, typically in
41   * {@code javax.servlet.ServletContextListener.contextDestroyed(javax.servlet.ServletContextEvent)} or similar.
42   *
43   */
44  public class FileCleaningTracker {
45  
46      // Note: fields are package protected to allow use by test cases
47  
48      /**
49       * Queue of {@code Tracker} instances being watched.
50       */
51      ReferenceQueue<Object> q = new ReferenceQueue<>();
52      /**
53       * Collection of {@code Tracker} instances in existence.
54       */
55      final Collection<Tracker> trackers = Collections.synchronizedSet(new HashSet<>()); // synchronized
56      /**
57       * Collection of File paths that failed to delete.
58       */
59      final List<String> deleteFailures = Collections.synchronizedList(new ArrayList<>());
60      /**
61       * Whether to terminate the thread when the tracking is complete.
62       */
63      volatile boolean exitWhenFinished;
64      /**
65       * The thread that will clean up registered files.
66       */
67      Thread reaper;
68  
69      /**
70       * Track the specified file, using the provided marker, deleting the file
71       * when the marker instance is garbage collected.
72       * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
73       *
74       * @param file  the file to be tracked, not null
75       * @param marker  the marker object used to track the file, not null
76       * @throws NullPointerException if the file is null
77       */
78      public void track(final File file, final Object marker) {
79          track(file, marker, null);
80      }
81  
82      /**
83       * Track the specified file, using the provided marker, deleting the file
84       * when the marker instance is garbage collected.
85       * The specified deletion strategy is used.
86       *
87       * @param file  the file to be tracked, not null
88       * @param marker  the marker object used to track the file, not null
89       * @param deleteStrategy  the strategy to delete the file, null means normal
90       * @throws NullPointerException if the file is null
91       */
92      public void track(final File file, final Object marker, final FileDeleteStrategy deleteStrategy) {
93          Objects.requireNonNull(file, "file");
94          addTracker(file.getPath(), marker, deleteStrategy);
95      }
96  
97      /**
98       * Track the specified file, using the provided marker, deleting the file
99       * when the marker instance is garbage collected.
100      * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
101      *
102      * @param path  the full path to the file to be tracked, not null
103      * @param marker  the marker object used to track the file, not null
104      * @throws NullPointerException if the path is null
105      */
106     public void track(final String path, final Object marker) {
107         track(path, marker, null);
108     }
109 
110     /**
111      * Track the specified file, using the provided marker, deleting the file
112      * when the marker instance is garbage collected.
113      * The specified deletion strategy is used.
114      *
115      * @param path  the full path to the file to be tracked, not null
116      * @param marker  the marker object used to track the file, not null
117      * @param deleteStrategy  the strategy to delete the file, null means normal
118      * @throws NullPointerException if the path is null
119      */
120     public void track(final String path, final Object marker, final FileDeleteStrategy deleteStrategy) {
121         Objects.requireNonNull(path, "path");
122         addTracker(path, marker, deleteStrategy);
123     }
124 
125     /**
126      * Adds a tracker to the list of trackers.
127      *
128      * @param path  the full path to the file to be tracked, not null
129      * @param marker  the marker object used to track the file, not null
130      * @param deleteStrategy  the strategy to delete the file, null means normal
131      */
132     private synchronized void addTracker(final String path, final Object marker, final FileDeleteStrategy
133             deleteStrategy) {
134         // synchronized block protects reaper
135         if (exitWhenFinished) {
136             throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called");
137         }
138         if (reaper == null) {
139             reaper = new Reaper();
140             reaper.start();
141         }
142         trackers.add(new Tracker(path, deleteStrategy, marker, q));
143     }
144 
145     /**
146      * Retrieve the number of files currently being tracked, and therefore
147      * awaiting deletion.
148      *
149      * @return the number of files being tracked
150      */
151     public int getTrackCount() {
152         return trackers.size();
153     }
154 
155     /**
156      * Return the file paths that failed to delete.
157      *
158      * @return the file paths that failed to delete
159      * @since 2.0
160      */
161     public List<String> getDeleteFailures() {
162         return deleteFailures;
163     }
164 
165     /**
166      * Call this method to cause the file cleaner thread to terminate when
167      * there are no more objects being tracked for deletion.
168      * <p>
169      * In a simple environment, you don't need this method as the file cleaner
170      * thread will simply exit when the JVM exits. In a more complex environment,
171      * with multiple class loaders (such as an application server), you should be
172      * aware that the file cleaner thread will continue running even if the class
173      * loader it was started from terminates. This can constitute a memory leak.
174      * <p>
175      * For example, suppose that you have developed a web application, which
176      * contains the commons-io jar file in your WEB-INF/lib directory. In other
177      * words, the FileCleaner class is loaded through the class loader of your
178      * web application. If the web application is terminated, but the servlet
179      * container is still running, then the file cleaner thread will still exist,
180      * posing a memory leak.
181      * <p>
182      * This method allows the thread to be terminated. Simply call this method
183      * in the resource cleanup code, such as
184      * {@code javax.servlet.ServletContextListener.contextDestroyed(javax.servlet.ServletContextEvent)}.
185      * Once called, no new objects can be tracked by the file cleaner.
186      */
187     public synchronized void exitWhenFinished() {
188         // synchronized block protects reaper
189         exitWhenFinished = true;
190         if (reaper != null) {
191             synchronized (reaper) {
192                 reaper.interrupt();
193             }
194         }
195     }
196 
197     /**
198      * The reaper thread.
199      */
200     private final class Reaper extends Thread {
201         /** Construct a new Reaper */
202         Reaper() {
203             super("File Reaper");
204             setPriority(Thread.MAX_PRIORITY);
205             setDaemon(true);
206         }
207 
208         /**
209          * Run the reaper thread that will delete files as their associated
210          * marker objects are reclaimed by the garbage collector.
211          */
212         @Override
213         public void run() {
214             // thread exits when exitWhenFinished is true and there are no more tracked objects
215             while (!exitWhenFinished || !trackers.isEmpty()) {
216                 try {
217                     // Wait for a tracker to remove.
218                     final Tracker tracker = (Tracker) q.remove(); // cannot return null
219                     trackers.remove(tracker);
220                     if (!tracker.delete()) {
221                         deleteFailures.add(tracker.getPath());
222                     }
223                     tracker.clear();
224                 } catch (final InterruptedException e) {
225                     continue;
226                 }
227             }
228         }
229     }
230 
231     /**
232      * Inner class which acts as the reference for a file pending deletion.
233      */
234     private static final class Tracker extends PhantomReference<Object> {
235 
236         /**
237          * The full path to the file being tracked.
238          */
239         private final String path;
240         /**
241          * The strategy for deleting files.
242          */
243         private final FileDeleteStrategy deleteStrategy;
244 
245         /**
246          * Constructs an instance of this class from the supplied parameters.
247          *
248          * @param path  the full path to the file to be tracked, not null
249          * @param deleteStrategy  the strategy to delete the file, null means normal
250          * @param marker  the marker object used to track the file, not null
251          * @param queue  the queue on to which the tracker will be pushed, not null
252          */
253         Tracker(final String path, final FileDeleteStrategy deleteStrategy, final Object marker,
254                 final ReferenceQueue<? super Object> queue) {
255             super(marker, queue);
256             this.path = path;
257             this.deleteStrategy = deleteStrategy == null ? FileDeleteStrategy.NORMAL : deleteStrategy;
258         }
259 
260         /**
261          * Return the path.
262          *
263          * @return the path
264          */
265         public String getPath() {
266             return path;
267         }
268 
269         /**
270          * Deletes the file associated with this tracker instance.
271          *
272          * @return {@code true} if the file was deleted successfully;
273          *         {@code false} otherwise.
274          */
275         public boolean delete() {
276             return deleteStrategy.deleteQuietly(new File(path));
277         }
278     }
279 
280 }