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}