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