001// Copyright 2006, 2007, 2008, 2010 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.ioc.internal.util; 016 017import org.apache.tapestry5.ioc.internal.services.ClasspathURLConverterImpl; 018import org.apache.tapestry5.ioc.services.ClasspathURLConverter; 019 020import java.io.File; 021import java.io.IOException; 022import java.net.URISyntaxException; 023import java.net.URL; 024import java.util.Map; 025 026/** 027 * Given a (growing) set of URLs, can periodically check to see if any of the underlying resources has changed. This 028 * class is capable of using either millisecond-level granularity or second-level granularity. Millisecond-level 029 * granularity is used by default. Second-level granularity is provided for compatibility with browsers vis-a-vis 030 * resource caching -- that's how granular they get with their "If-Modified-Since", "Last-Modified" and "Expires" 031 * headers. 032 */ 033public class URLChangeTracker 034{ 035 private static final long FILE_DOES_NOT_EXIST_TIMESTAMP = -1L; 036 037 private final Map<File, Long> fileToTimestamp = CollectionFactory.newConcurrentMap(); 038 039 private final boolean granularitySeconds; 040 041 private final boolean trackFolderChanges; 042 043 private final ClasspathURLConverter classpathURLConverter; 044 045 public static final ClasspathURLConverter DEFAULT_CONVERTER = new ClasspathURLConverterImpl(); 046 047 /** 048 * Creates a tracker using the default (does nothing) URL converter, with default (millisecond) 049 * granularity and folder tracking disabled. 050 * 051 * @since 5.2.1 052 */ 053 public URLChangeTracker() 054 { 055 this(DEFAULT_CONVERTER, false, false); 056 } 057 058 /** 059 * Creates a new URL change tracker with millisecond-level granularity and folder checking enabled. 060 * 061 * @param classpathURLConverter 062 * used to convert URLs from one protocol to another 063 */ 064 public URLChangeTracker(ClasspathURLConverter classpathURLConverter) 065 { 066 this(classpathURLConverter, false); 067 068 } 069 070 /** 071 * Creates a new URL change tracker, using either millisecond-level granularity or second-level granularity and 072 * folder checking enabled. 073 * 074 * @param classpathURLConverter 075 * used to convert URLs from one protocol to another 076 * @param granularitySeconds 077 * whether or not to use second granularity (as opposed to millisecond granularity) 078 */ 079 public URLChangeTracker(ClasspathURLConverter classpathURLConverter, boolean granularitySeconds) 080 { 081 this(classpathURLConverter, granularitySeconds, true); 082 } 083 084 /** 085 * Creates a new URL change tracker, using either millisecond-level granularity or second-level granularity. 086 * 087 * @param classpathURLConverter 088 * used to convert URLs from one protocol to another 089 * @param granularitySeconds 090 * whether or not to use second granularity (as opposed to millisecond granularity) 091 * @param trackFolderChanges 092 * if true, then adding a file URL will also track the folder containing the file (this 093 * is useful when concerned about additions to a folder) 094 * @since 5.2.1 095 */ 096 public URLChangeTracker(ClasspathURLConverter classpathURLConverter, boolean granularitySeconds, 097 boolean trackFolderChanges) 098 { 099 this.granularitySeconds = granularitySeconds; 100 this.classpathURLConverter = classpathURLConverter; 101 this.trackFolderChanges = trackFolderChanges; 102 } 103 104 /** 105 * Converts a URL with protocol "file" to a File instance. 106 * 107 * @since 5.2.0 108 */ 109 public static File toFileFromFileProtocolURL(URL url) 110 { 111 assert url != null; 112 113 if (!url.getProtocol().equals("file")) 114 throw new IllegalArgumentException(String.format("URL %s does not use the 'file' protocol.", url)); 115 116 // http://weblogs.java.net/blog/kohsuke/archive/2007/04/how_to_convert.html 117 118 try 119 { 120 return new File(url.toURI()); 121 } catch (URISyntaxException ex) 122 { 123 return new File(url.getPath()); 124 } 125 } 126 127 /** 128 * Stores a new URL into the tracker, or returns the previous time stamp for a previously added URL. Filters out all 129 * non-file URLs. 130 * 131 * @param url 132 * of the resource to add, or null if not known 133 * @return the current timestamp for the URL (possibly rounded off for granularity reasons), or 0 if the URL is 134 * null 135 */ 136 public long add(URL url) 137 { 138 if (url == null) 139 return 0; 140 141 URL converted = classpathURLConverter.convert(url); 142 143 if (!converted.getProtocol().equals("file")) 144 return timestampForNonFileURL(converted); 145 146 File resourceFile = toFileFromFileProtocolURL(converted); 147 148 if (fileToTimestamp.containsKey(resourceFile)) 149 return fileToTimestamp.get(resourceFile); 150 151 long timestamp = readTimestamp(resourceFile); 152 153 // A quick and imperfect fix for TAPESTRY-1918. When a file 154 // is added, add the directory containing the file as well. 155 156 fileToTimestamp.put(resourceFile, timestamp); 157 158 if (trackFolderChanges) 159 { 160 File dir = resourceFile.getParentFile(); 161 162 if (!fileToTimestamp.containsKey(dir)) 163 { 164 long dirTimestamp = readTimestamp(dir); 165 fileToTimestamp.put(dir, dirTimestamp); 166 } 167 } 168 169 return timestamp; 170 } 171 172 private long timestampForNonFileURL(URL url) 173 { 174 long timestamp; 175 176 try 177 { 178 timestamp = url.openConnection().getLastModified(); 179 } 180 catch (IOException ex) 181 { 182 throw new RuntimeException(ex); 183 } 184 185 return applyGranularity(timestamp); 186 } 187 188 /** 189 * Clears all URL and timestamp data stored in the tracker. 190 */ 191 public void clear() 192 { 193 fileToTimestamp.clear(); 194 } 195 196 /** 197 * Re-acquires the last updated timestamp for each URL and returns true if any timestamp has changed. 198 */ 199 public boolean containsChanges() 200 { 201 boolean result = false; 202 203 // This code would be highly suspect if this method was expected to be invoked 204 // concurrently, but CheckForUpdatesFilter ensures that it will be invoked 205 // synchronously. 206 207 for (Map.Entry<File, Long> entry : fileToTimestamp.entrySet()) 208 { 209 long newTimestamp = readTimestamp(entry.getKey()); 210 long current = entry.getValue(); 211 212 if (current == newTimestamp) 213 continue; 214 215 result = true; 216 entry.setValue(newTimestamp); 217 } 218 219 return result; 220 } 221 222 /** 223 * Returns the time that the specified file was last modified, possibly rounded down to the nearest second. 224 */ 225 private long readTimestamp(File file) 226 { 227 if (!file.exists()) 228 return FILE_DOES_NOT_EXIST_TIMESTAMP; 229 230 return applyGranularity(file.lastModified()); 231 } 232 233 private long applyGranularity(long timestamp) 234 { 235 // For coarse granularity (accurate only to the last second), remove the milliseconds since 236 // the last full second. This is for compatibility with client HTTP requests, which 237 // are only accurate to one second. The extra level of detail creates false positives 238 // for changes, and undermines HTTP response caching in the client. 239 240 if (granularitySeconds) 241 return timestamp - (timestamp % 1000); 242 243 return timestamp; 244 } 245 246 /** 247 * Needed for testing; changes file timestamps so that a change will be detected by {@link #containsChanges()}. 248 */ 249 public void forceChange() 250 { 251 for (Map.Entry<File, Long> e : fileToTimestamp.entrySet()) 252 { 253 e.setValue(0l); 254 } 255 } 256 257 /** 258 * Needed for testing. 259 */ 260 int trackedFileCount() 261 { 262 return fileToTimestamp.size(); 263 } 264 265}