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(Thread.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}