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