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.util.ArrayList;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashSet;
026import java.util.List;
027import java.util.Objects;
028
029/**
030 * Keeps track of files awaiting deletion, and deletes them when an associated
031 * marker object is reclaimed by the garbage collector.
032 * <p>
033 * This utility creates a background thread to handle file deletion.
034 * Each file to be deleted is registered with a handler object.
035 * When the handler object is garbage collected, the file is deleted.
036 * </p>
037 * <p>
038 * In an environment with multiple class loaders (a servlet container, for
039 * example), you should consider stopping the background thread if it is no
040 * longer needed. This is done by invoking the method
041 * {@link #exitWhenFinished}, typically in
042 * {@code javax.servlet.ServletContextListener.contextDestroyed(javax.servlet.ServletContextEvent)} or similar.
043 * </p>
044 */
045public class FileCleaningTracker {
046
047    // Note: fields are package protected to allow use by test cases
048
049    /**
050     * The reaper thread.
051     */
052    private final class Reaper extends Thread {
053        /** Constructs a new Reaper */
054        Reaper() {
055            super("File Reaper");
056            setPriority(Thread.MAX_PRIORITY);
057            setDaemon(true);
058        }
059
060        /**
061         * Runs the reaper thread that will delete files as their associated
062         * marker objects are reclaimed by the garbage collector.
063         */
064        @Override
065        public void run() {
066            // thread exits when exitWhenFinished is true and there are no more tracked objects
067            while (!exitWhenFinished || !trackers.isEmpty()) {
068                try {
069                    // Wait for a tracker to remove.
070                    final Tracker tracker = (Tracker) q.remove(); // cannot return null
071                    trackers.remove(tracker);
072                    if (!tracker.delete()) {
073                        deleteFailures.add(tracker.getPath());
074                    }
075                    tracker.clear();
076                } catch (final InterruptedException e) {
077                    continue;
078                }
079            }
080        }
081    }
082
083    /**
084     * Inner class which acts as the reference for a file pending deletion.
085     */
086    private static final class Tracker extends PhantomReference<Object> {
087
088        /**
089         * The full path to the file being tracked.
090         */
091        private final String path;
092
093        /**
094         * The strategy for deleting files.
095         */
096        private final FileDeleteStrategy deleteStrategy;
097
098        /**
099         * Constructs an instance of this class from the supplied parameters.
100         *
101         * @param path  the full path to the file to be tracked, not null
102         * @param deleteStrategy  the strategy to delete the file, null means normal
103         * @param marker  the marker object used to track the file, not null
104         * @param queue  the queue on to which the tracker will be pushed, not null
105         */
106        Tracker(final String path, final FileDeleteStrategy deleteStrategy, final Object marker,
107                final ReferenceQueue<? super Object> queue) {
108            super(marker, queue);
109            this.path = path;
110            this.deleteStrategy = deleteStrategy == null ? FileDeleteStrategy.NORMAL : deleteStrategy;
111        }
112
113        /**
114         * Deletes the file associated with this tracker instance.
115         *
116         * @return {@code true} if the file was deleted successfully;
117         *         {@code false} otherwise.
118         */
119        public boolean delete() {
120            return deleteStrategy.deleteQuietly(new File(path));
121        }
122
123        /**
124         * Return the path.
125         *
126         * @return the path
127         */
128        public String getPath() {
129            return path;
130        }
131    }
132
133    /**
134     * Queue of {@link Tracker} instances being watched.
135     */
136    ReferenceQueue<Object> q = new ReferenceQueue<>();
137
138    /**
139     * Collection of {@link Tracker} instances in existence.
140     */
141    final Collection<Tracker> trackers = Collections.synchronizedSet(new HashSet<>()); // synchronized
142
143    /**
144     * Collection of File paths that failed to delete.
145     */
146    final List<String> deleteFailures = Collections.synchronizedList(new ArrayList<>());
147
148    /**
149     * Whether to terminate the thread when the tracking is complete.
150     */
151    volatile boolean exitWhenFinished;
152
153    /**
154     * The thread that will clean up registered files.
155     */
156    Thread reaper;
157
158    /**
159     * Adds a tracker to the list of trackers.
160     *
161     * @param path  the full path to the file to be tracked, not null
162     * @param marker  the marker object used to track the file, not null
163     * @param deleteStrategy  the strategy to delete the file, null means normal
164     */
165    private synchronized void addTracker(final String path, final Object marker, final FileDeleteStrategy
166            deleteStrategy) {
167        // synchronized block protects reaper
168        if (exitWhenFinished) {
169            throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called");
170        }
171        if (reaper == null) {
172            reaper = new Reaper();
173            reaper.start();
174        }
175        trackers.add(new Tracker(path, deleteStrategy, marker, q));
176    }
177
178    /**
179     * Call this method to cause the file cleaner thread to terminate when
180     * there are no more objects being tracked for deletion.
181     * <p>
182     * In a simple environment, you don't need this method as the file cleaner
183     * thread will simply exit when the JVM exits. In a more complex environment,
184     * with multiple class loaders (such as an application server), you should be
185     * aware that the file cleaner thread will continue running even if the class
186     * loader it was started from terminates. This can constitute a memory leak.
187     * <p>
188     * For example, suppose that you have developed a web application, which
189     * contains the commons-io jar file in your WEB-INF/lib directory. In other
190     * words, the FileCleaner class is loaded through the class loader of your
191     * web application. If the web application is terminated, but the servlet
192     * container is still running, then the file cleaner thread will still exist,
193     * posing a memory leak.
194     * <p>
195     * This method allows the thread to be terminated. Simply call this method
196     * in the resource cleanup code, such as
197     * {@code javax.servlet.ServletContextListener.contextDestroyed(javax.servlet.ServletContextEvent)}.
198     * Once called, no new objects can be tracked by the file cleaner.
199     */
200    public synchronized void exitWhenFinished() {
201        // synchronized block protects reaper
202        exitWhenFinished = true;
203        if (reaper != null) {
204            synchronized (reaper) {
205                reaper.interrupt();
206            }
207        }
208    }
209
210    /**
211     * Gets the file paths that failed to delete.
212     *
213     * @return the file paths that failed to delete
214     * @since 2.0
215     */
216    public List<String> getDeleteFailures() {
217        return deleteFailures;
218    }
219
220    /**
221     * Gets the number of files currently being tracked, and therefore
222     * awaiting deletion.
223     *
224     * @return the number of files being tracked
225     */
226    public int getTrackCount() {
227        return trackers.size();
228    }
229
230    /**
231     * Track the specified file, using the provided marker, deleting the file
232     * when the marker instance is garbage collected.
233     * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
234     *
235     * @param file  the file to be tracked, not null
236     * @param marker  the marker object used to track the file, not null
237     * @throws NullPointerException if the file is null
238     */
239    public void track(final File file, final Object marker) {
240        track(file, marker, null);
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 specified deletion strategy is 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     * @param deleteStrategy  the strategy to delete the file, null means normal
251     * @throws NullPointerException if the file is null
252     */
253    public void track(final File file, final Object marker, final FileDeleteStrategy deleteStrategy) {
254        Objects.requireNonNull(file, "file");
255        addTracker(file.getPath(), marker, deleteStrategy);
256    }
257
258    /**
259     * Tracks the specified file, using the provided marker, deleting the file
260     * when the marker instance is garbage collected.
261     * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
262     *
263     * @param path  the full path to the file to be tracked, not null
264     * @param marker  the marker object used to track the file, not null
265     * @throws NullPointerException if the path is null
266     */
267    public void track(final String path, final Object marker) {
268        track(path, marker, null);
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 specified deletion strategy is used.
275     *
276     * @param path  the full path to the file to be tracked, not null
277     * @param marker  the marker object used to track the file, not null
278     * @param deleteStrategy  the strategy to delete the file, null means normal
279     * @throws NullPointerException if the path is null
280     */
281    public void track(final String path, final Object marker, final FileDeleteStrategy deleteStrategy) {
282        Objects.requireNonNull(path, "path");
283        addTracker(path, marker, deleteStrategy);
284    }
285
286}