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}