Coverage Report - org.apache.tapestry5.internal.util.URLChangeTracker
 
Classes in this File Line Coverage Branch Coverage Complexity
URLChangeTracker
91%
42/46
100%
18/18
0
 
 1  
 // Copyright 2006, 2007, 2008 The Apache Software Foundation
 2  
 //
 3  
 // Licensed under the Apache License, Version 2.0 (the "License");
 4  
 // you may not use this file except in compliance with the License.
 5  
 // You may obtain a copy of the License at
 6  
 //
 7  
 //     http://www.apache.org/licenses/LICENSE-2.0
 8  
 //
 9  
 // Unless required by applicable law or agreed to in writing, software
 10  
 // distributed under the License is distributed on an "AS IS" BASIS,
 11  
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  
 // See the License for the specific language governing permissions and
 13  
 // limitations under the License.
 14  
 
 15  
 package org.apache.tapestry5.internal.util;
 16  
 
 17  
 import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
 18  
 import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
 19  
 
 20  
 import java.io.File;
 21  
 import java.io.IOException;
 22  
 import java.net.URISyntaxException;
 23  
 import java.net.URL;
 24  
 import java.util.Map;
 25  
 
 26  
 /**
 27  
  * Given a (growing) set of URLs, can periodically check to see if any of the underlying resources has changed. This
 28  
  * class is capable of using either millisecond-level granularity or second-level granularity. Millisecond-level
 29  
  * granularity is used by default. Second-level granularity is provided for compatibility with browsers vis-a-vis
 30  
  * resource caching -- that's how granular they get with their "If-Modified-Since", "Last-Modified" and "Expires"
 31  
  * headers.
 32  
  */
 33  
 public class URLChangeTracker
 34  
 {
 35  
     private static final long FILE_DOES_NOT_EXIST_TIMESTAMP = -1L;
 36  
 
 37  290
     private final Map<File, Long> fileToTimestamp = CollectionFactory.newConcurrentMap();
 38  
 
 39  
     private final boolean granularitySeconds;
 40  
     
 41  
     private ClasspathURLConverter classpathURLConverter; 
 42  
 
 43  
     /**
 44  
      * Creates a new URL change tracker with millisecond-level granularity.
 45  
      * 
 46  
      * @param classpathURLConverter used to convert URLs from one protocol to another
 47  
      */
 48  
     public URLChangeTracker(ClasspathURLConverter classpathURLConverter)
 49  
     {
 50  222
         this(classpathURLConverter, false);
 51  
         
 52  222
     }
 53  
 
 54  
     /**
 55  
      * Creates a new URL change tracker, using either millisecond-level granularity or second-level granularity.
 56  
      *
 57  
      * @param classpathURLConverter used to convert URLs from one protocol to another
 58  
      * @param granularitySeconds whether or not to use second granularity (as opposed to millisecond granularity)
 59  
      */
 60  
     public URLChangeTracker(ClasspathURLConverter classpathURLConverter, boolean granularitySeconds)
 61  290
     {
 62  290
         this.granularitySeconds = granularitySeconds;
 63  
         
 64  290
         this.classpathURLConverter = classpathURLConverter;
 65  290
     }
 66  
 
 67  
     /**
 68  
      * Stores a new URL into the tracker, or returns the previous time stamp for a previously added URL. Filters out all
 69  
      * non-file URLs.
 70  
      *
 71  
      * @param url of the resource to add, or null if not known
 72  
      * @return the current timestamp for the URL (possibly rounded off for granularity reasons), or 0 if the URL is
 73  
      *         null
 74  
      */
 75  
     public long add(URL url)
 76  
     {
 77  2364
         if (url == null) return 0;
 78  
         
 79  1950
         URL converted = classpathURLConverter.convert(url);
 80  
 
 81  1950
         if (!converted.getProtocol().equals("file")) return timestampForNonFileURL(converted);
 82  
 
 83  1948
         File resourceFile = toFile(converted);
 84  
 
 85  1948
         if (fileToTimestamp.containsKey(resourceFile)) return fileToTimestamp.get(resourceFile);
 86  
 
 87  1944
         long timestamp = readTimestamp(resourceFile);
 88  
 
 89  
         // A quick and imperfect fix for TAPESTRY-1918.  When a file
 90  
         // is added, add the directory containing the file as well.
 91  
 
 92  1944
         fileToTimestamp.put(resourceFile, timestamp);
 93  
 
 94  1944
         File dir = resourceFile.getParentFile();
 95  
 
 96  1944
         if (!fileToTimestamp.containsKey(dir))
 97  
         {
 98  488
             long dirTimestamp = readTimestamp(dir);
 99  488
             fileToTimestamp.put(dir, dirTimestamp);
 100  
         }
 101  
 
 102  
 
 103  1944
         return timestamp;
 104  
     }
 105  
 
 106  
     private long timestampForNonFileURL(URL url)
 107  
     {
 108  
         long timestamp;
 109  
 
 110  
         try
 111  
         {
 112  2
             timestamp = url.openConnection().getLastModified();
 113  
         }
 114  0
         catch (IOException ex)
 115  
         {
 116  0
             throw new RuntimeException(ex);
 117  2
         }
 118  
 
 119  2
         return applyGranularity(timestamp);
 120  
     }
 121  
 
 122  
     private File toFile(URL url)
 123  
     {
 124  
         // http://weblogs.java.net/blog/kohsuke/archive/2007/04/how_to_convert.html
 125  
 
 126  
         try
 127  
         {
 128  1948
             return new File(url.toURI());
 129  
         }
 130  0
         catch (URISyntaxException ex)
 131  
         {
 132  0
             return new File(url.getPath());
 133  
         }
 134  
     }
 135  
 
 136  
     /**
 137  
      * Clears all URL and timestamp data stored in the tracker.
 138  
      */
 139  
     public void clear()
 140  
     {
 141  22
         fileToTimestamp.clear();
 142  22
     }
 143  
 
 144  
     /**
 145  
      * Re-acquires the last updated timestamp for each URL and returns true if any timestamp has changed.
 146  
      */
 147  
     public boolean containsChanges()
 148  
     {
 149  1647
         boolean result = false;
 150  
 
 151  
         // This code would be highly suspect if this method was expected to be invoked
 152  
         // concurrently, but CheckForUpdatesFilter ensures that it will be invoked
 153  
         // synchronously.
 154  
 
 155  1647
         for (Map.Entry<File, Long> entry : fileToTimestamp.entrySet())
 156  
         {
 157  130125
             long newTimestamp = readTimestamp(entry.getKey());
 158  130125
             long current = entry.getValue();
 159  
 
 160  130125
             if (current == newTimestamp) continue;
 161  
 
 162  50
             result = true;
 163  50
             entry.setValue(newTimestamp);
 164  50
         }
 165  
 
 166  1647
         return result;
 167  
     }
 168  
 
 169  
     /**
 170  
      * Returns the time that the specified file was last modified, possibly rounded down to the nearest second.
 171  
      */
 172  
     private long readTimestamp(File file)
 173  
     {
 174  132557
         if (!file.exists()) return FILE_DOES_NOT_EXIST_TIMESTAMP;
 175  
 
 176  132555
         return applyGranularity(file.lastModified());
 177  
     }
 178  
 
 179  
     private long applyGranularity(long timestamp)
 180  
     {
 181  
         // For coarse granularity (accurate only to the last second), remove the milliseconds since
 182  
         // the last full second. This is for compatibility with client HTTP requests, which
 183  
         // are only accurate to one second. The extra level of detail creates false positives
 184  
         // for changes, and undermines HTTP response caching in the client.
 185  
 
 186  132557
         if (granularitySeconds) return timestamp - (timestamp % 1000);
 187  
 
 188  118380
         return timestamp;
 189  
     }
 190  
 
 191  
     /**
 192  
      * Needed for testing; changes file timestamps so that a change will be detected by {@link #containsChanges()}.
 193  
      */
 194  
     public void forceChange()
 195  
     {
 196  12
         for (Map.Entry<File, Long> e : fileToTimestamp.entrySet())
 197  
         {
 198  28
             e.setValue(0l);
 199  
         }
 200  12
     }
 201  
 
 202  
     /**
 203  
      * Needed for testing.
 204  
      */
 205  
     int trackedFileCount()
 206  
     {
 207  6
         return fileToTimestamp.size();
 208  
     }
 209  
 
 210  
 }