Coverage Report - org.apache.commons.io.FileCleaningTracker
 
Classes in this File Line Coverage Branch Coverage Complexity
FileCleaningTracker
96%
32/33
90%
9/10
2
FileCleaningTracker$Reaper
100%
15/15
100%
6/6
2
FileCleaningTracker$Tracker
100%
6/6
100%
2/2
2
 
 1  
 /*
 2  
  * Licensed to the Apache Software Foundation (ASF) under one or more
 3  
  * contributor license agreements.  See the NOTICE file distributed with
 4  
  * this work for additional information regarding copyright ownership.
 5  
  * The ASF licenses this file to You under the Apache License, Version 2.0
 6  
  * (the "License"); you may not use this file except in compliance with
 7  
  * the License.  You may obtain a copy of the License at
 8  
  * 
 9  
  *      http://www.apache.org/licenses/LICENSE-2.0
 10  
  * 
 11  
  * Unless required by applicable law or agreed to in writing, software
 12  
  * distributed under the License is distributed on an "AS IS" BASIS,
 13  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  
  * See the License for the specific language governing permissions and
 15  
  * limitations under the License.
 16  
  */
 17  
 package org.apache.commons.io;
 18  
 
 19  
 import java.io.File;
 20  
 import java.lang.ref.PhantomReference;
 21  
 import java.lang.ref.ReferenceQueue;
 22  
 import java.util.ArrayList;
 23  
 import java.util.Collection;
 24  
 import java.util.Collections;
 25  
 import java.util.HashSet;
 26  
 import java.util.List;
 27  
 
 28  
 /**
 29  
  * Keeps track of files awaiting deletion, and deletes them when an associated
 30  
  * marker object is reclaimed by the garbage collector.
 31  
  * <p>
 32  
  * This utility creates a background thread to handle file deletion.
 33  
  * Each file to be deleted is registered with a handler object.
 34  
  * When the handler object is garbage collected, the file is deleted.
 35  
  * <p>
 36  
  * In an environment with multiple class loaders (a servlet container, for
 37  
  * example), you should consider stopping the background thread if it is no
 38  
  * longer needed. This is done by invoking the method
 39  
  * {@link #exitWhenFinished}, typically in
 40  
  * {@code javax.servlet.ServletContextListener.contextDestroyed(javax.servlet.ServletContextEvent)} or similar.
 41  
  *
 42  
  */
 43  20
 public class FileCleaningTracker {
 44  
 
 45  
     // Note: fields are package protected to allow use by test cases
 46  
 
 47  
     /**
 48  
      * Queue of <code>Tracker</code> instances being watched.
 49  
      */
 50  20
     ReferenceQueue<Object> q = new ReferenceQueue<>();
 51  
     /**
 52  
      * Collection of <code>Tracker</code> instances in existence.
 53  
      */
 54  20
     final Collection<Tracker> trackers = Collections.synchronizedSet(new HashSet<Tracker>()); // synchronized
 55  
     /**
 56  
      * Collection of File paths that failed to delete.
 57  
      */
 58  20
     final List<String> deleteFailures = Collections.synchronizedList(new ArrayList<String>());
 59  
     /**
 60  
      * Whether to terminate the thread when the tracking is complete.
 61  
      */
 62  20
     volatile boolean exitWhenFinished = false;
 63  
     /**
 64  
      * The thread that will clean up registered files.
 65  
      */
 66  
     Thread reaper;
 67  
 
 68  
     //-----------------------------------------------------------------------
 69  
     /**
 70  
      * Track the specified file, using the provided marker, deleting the file
 71  
      * when the marker instance is garbage collected.
 72  
      * The {@link FileDeleteStrategy#NORMAL normal} deletion strategy will be used.
 73  
      *
 74  
      * @param file  the file to be tracked, not null
 75  
      * @param marker  the marker object used to track the file, not null
 76  
      * @throws NullPointerException if the file is null
 77  
      */
 78  
     public void track(final File file, final Object marker) {
 79  8
         track(file, marker, null);
 80  4
     }
 81  
 
 82  
     /**
 83  
      * Track the specified file, using the provided marker, deleting the file
 84  
      * when the marker instance is garbage collected.
 85  
      * The specified deletion strategy is used.
 86  
      *
 87  
      * @param file  the file to be tracked, not null
 88  
      * @param marker  the marker object used to track the file, not null
 89  
      * @param deleteStrategy  the strategy to delete the file, null means normal
 90  
      * @throws NullPointerException if the file is null
 91  
      */
 92  
     public void track(final File file, final Object marker, final FileDeleteStrategy deleteStrategy) {
 93  20
         if (file == null) {
 94  8
             throw new NullPointerException("The file must not be null");
 95  
         }
 96  12
         addTracker(file.getPath(), marker, deleteStrategy);
 97  12
     }
 98  
 
 99  
     /**
 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  20
         track(path, marker, null);
 110  12
     }
 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  24
         if (path == null) {
 124  8
             throw new NullPointerException("The path must not be null");
 125  
         }
 126  16
         addTracker(path, marker, deleteStrategy);
 127  12
     }
 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  28
         if (exitWhenFinished) {
 140  4
             throw new IllegalStateException("No new trackers can be added once exitWhenFinished() is called");
 141  
         }
 142  24
         if (reaper == null) {
 143  24
             reaper = new Reaper();
 144  24
             reaper.start();
 145  
         }
 146  24
         trackers.add(new Tracker(path, deleteStrategy, marker, q));
 147  24
     }
 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  132
         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  0
         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  16
         exitWhenFinished = true;
 195  16
         if (reaper != null) {
 196  8
             synchronized (reaper) {
 197  8
                 reaper.interrupt();
 198  8
             }
 199  
         }
 200  16
     }
 201  
 
 202  
     //-----------------------------------------------------------------------
 203  
     /**
 204  
      * The reaper thread.
 205  
      */
 206  
     private final class Reaper extends Thread {
 207  
         /** Construct a new Reaper */
 208  24
         Reaper() {
 209  24
             super("File Reaper");
 210  24
             setPriority(Thread.MAX_PRIORITY);
 211  24
             setDaemon(true);
 212  24
         }
 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  56
             while (exitWhenFinished == false || trackers.size() > 0) {
 222  
                 try {
 223  
                     // Wait for a tracker to remove.
 224  48
                     final Tracker tracker = (Tracker) q.remove(); // cannot return null
 225  24
                     trackers.remove(tracker);
 226  24
                     if (!tracker.delete()) {
 227  8
                         deleteFailures.add(tracker.getPath());
 228  
                     }
 229  24
                     tracker.clear();
 230  8
                 } catch (final InterruptedException e) {
 231  8
                     continue;
 232  24
                 }
 233  
             }
 234  8
         }
 235  
     }
 236  
 
 237  
     //-----------------------------------------------------------------------
 238  
     /**
 239  
      * Inner class which acts as the reference for a file pending deletion.
 240  
      */
 241  20
     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  24
             super(marker, queue);
 263  24
             this.path = path;
 264  24
             this.deleteStrategy = deleteStrategy == null ? FileDeleteStrategy.NORMAL : deleteStrategy;
 265  24
         }
 266  
 
 267  
         /**
 268  
          * Return the path.
 269  
          *
 270  
          * @return the path
 271  
          */
 272  
         public String getPath() {
 273  8
             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  24
             return deleteStrategy.deleteQuietly(new File(path));
 284  
         }
 285  
     }
 286  
 
 287  
 }