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 java.io.File; 018import java.io.IOException; 019import java.net.URISyntaxException; 020import java.net.URL; 021import java.util.HashSet; 022import java.util.Map; 023import java.util.Set; 024 025import org.apache.tapestry5.commons.util.CollectionFactory; 026import org.apache.tapestry5.ioc.internal.services.ClasspathURLConverterImpl; 027import org.apache.tapestry5.ioc.services.ClasspathURLConverter; 028 029/** 030 * Given a (growing) set of URLs, can periodically check to see if any of the underlying resources has changed. This 031 * class is capable of using either millisecond-level granularity or second-level granularity. Millisecond-level 032 * granularity is used by default. Second-level granularity is provided for compatibility with browsers vis-a-vis 033 * resource caching -- that's how granular they get with their "If-Modified-Since", "Last-Modified" and "Expires" 034 * headers. 035 * 036 * @param <T> The type of the optional information about the tracked resource. This type should 037 * implement <code>equals()</code> and <code>hashCode()</code>. 038 */ 039public class URLChangeTracker<T> 040{ 041 private static final long FILE_DOES_NOT_EXIST_TIMESTAMP = -1L; 042 043 private final Map<File, TrackingInfo> fileToTimestamp = CollectionFactory.newConcurrentMap(); 044 045 private final boolean granularitySeconds; 046 047 private final boolean trackFolderChanges; 048 049 private final ClasspathURLConverter classpathURLConverter; 050 051 public static final ClasspathURLConverter DEFAULT_CONVERTER = new ClasspathURLConverterImpl(); 052 053 /** 054 * Creates a tracker using the default (does nothing) URL converter, with default (millisecond) 055 * granularity and folder tracking disabled. 056 * 057 * @since 5.2.1 058 */ 059 public URLChangeTracker() 060 { 061 this(DEFAULT_CONVERTER, false, false); 062 } 063 064 /** 065 * Creates a new URL change tracker with millisecond-level granularity and folder checking enabled. 066 * 067 * @param classpathURLConverter 068 * used to convert URLs from one protocol to another 069 */ 070 public URLChangeTracker(ClasspathURLConverter classpathURLConverter) 071 { 072 this(classpathURLConverter, false); 073 074 } 075 076 /** 077 * Creates a new URL change tracker, using either millisecond-level granularity or second-level granularity and 078 * folder checking enabled. 079 * 080 * @param classpathURLConverter 081 * used to convert URLs from one protocol to another 082 * @param granularitySeconds 083 * whether or not to use second granularity (as opposed to millisecond granularity) 084 */ 085 public URLChangeTracker(ClasspathURLConverter classpathURLConverter, boolean granularitySeconds) 086 { 087 this(classpathURLConverter, granularitySeconds, true); 088 } 089 090 /** 091 * Creates a new URL change tracker, using either millisecond-level granularity or second-level granularity. 092 * 093 * @param classpathURLConverter 094 * used to convert URLs from one protocol to another 095 * @param granularitySeconds 096 * whether or not to use second granularity (as opposed to millisecond granularity) 097 * @param trackFolderChanges 098 * if true, then adding a file URL will also track the folder containing the file (this 099 * is useful when concerned about additions to a folder) 100 * @since 5.2.1 101 */ 102 public URLChangeTracker(ClasspathURLConverter classpathURLConverter, boolean granularitySeconds, 103 boolean trackFolderChanges) 104 { 105 this.granularitySeconds = granularitySeconds; 106 this.classpathURLConverter = classpathURLConverter; 107 this.trackFolderChanges = trackFolderChanges; 108 } 109 110 /** 111 * Converts a URL with protocol "file" to a File instance. 112 * 113 * @since 5.2.0 114 */ 115 public static File toFileFromFileProtocolURL(URL url) 116 { 117 assert url != null; 118 119 if (!url.getProtocol().equals("file")) 120 throw new IllegalArgumentException(String.format("URL %s does not use the 'file' protocol.", url)); 121 122 // http://weblogs.java.net/blog/kohsuke/archive/2007/04/how_to_convert.html 123 124 try 125 { 126 return new File(url.toURI()); 127 } catch (URISyntaxException ex) 128 { 129 return new File(url.getPath()); 130 } 131 } 132 133 /** 134 * Stores a new URL into the tracker, or returns the previous time stamp for a previously added URL. Filters out all 135 * non-file URLs. 136 * 137 * @param url 138 * of the resource to add, or null if not known 139 * @return the current timestamp for the URL (possibly rounded off for granularity reasons), or 0 if the URL is 140 * null 141 */ 142 public long add(URL url) 143 { 144 return add(url, null); 145 } 146 /** 147 * Stores a new URL and associated memo (most probably a related class name) 148 * into the tracker, or returns the previous time stamp for a previously added URL. Filters out all 149 * non-file URLs. 150 * 151 * @param url 152 * of the resource to add, or null if not known 153 * @param resourceInfo 154 * an optional object containing information about the tracked URL. It's 155 * returned in the {@link #getChangedResourcesInfo()} method. 156 * @return the current timestamp for the URL (possibly rounded off for granularity reasons), or 0 if the URL is 157 * null 158 * @since 5.8.3 159 */ 160 public long add(URL url, T resourceInfo) 161 { 162 if (url == null) 163 return 0; 164 165 URL converted = classpathURLConverter.convert(url); 166 167 if (!converted.getProtocol().equals("file")) 168 return timestampForNonFileURL(converted); 169 170 File resourceFile = toFileFromFileProtocolURL(converted); 171 172 if (fileToTimestamp.containsKey(resourceFile)) 173 return fileToTimestamp.get(resourceFile).timestamp; 174 175 long timestamp = readTimestamp(resourceFile); 176 177 // A quick and imperfect fix for TAPESTRY-1918. When a file 178 // is added, add the directory containing the file as well. 179 180 fileToTimestamp.put(resourceFile, new TrackingInfo(timestamp, resourceInfo)); 181 182 if (trackFolderChanges) 183 { 184 File dir = resourceFile.getParentFile(); 185 186 if (!fileToTimestamp.containsKey(dir)) 187 { 188 long dirTimestamp = readTimestamp(dir); 189 fileToTimestamp.put(dir, new TrackingInfo(dirTimestamp, null)); 190 } 191 } 192 193 return timestamp; 194 } 195 196 private long timestampForNonFileURL(URL url) 197 { 198 long timestamp; 199 200 try 201 { 202 timestamp = url.openConnection().getLastModified(); 203 } 204 catch (IOException ex) 205 { 206 throw new RuntimeException(ex); 207 } 208 209 return applyGranularity(timestamp); 210 } 211 212 /** 213 * Clears all URL and timestamp data stored in the tracker. 214 */ 215 public void clear() 216 { 217 fileToTimestamp.clear(); 218 } 219 220 /** 221 * Re-acquires the last updated timestamp for each URL and returns true if any timestamp has changed. 222 */ 223 public boolean containsChanges() 224 { 225 boolean result = false; 226 227 // This code would be highly suspect if this method was expected to be invoked 228 // concurrently, but CheckForUpdatesFilter ensures that it will be invoked 229 // synchronously. 230 231 for (Map.Entry<File, TrackingInfo> entry : fileToTimestamp.entrySet()) 232 { 233 long newTimestamp = readTimestamp(entry.getKey()); 234 long current = entry.getValue().timestamp; 235 236 if (current == newTimestamp) 237 continue; 238 239 result = true; 240 entry.getValue().timestamp = newTimestamp; 241 } 242 243 return result; 244 } 245 246 /** 247 * Re-acquires the last updated timestamp for each URL and returns the non-null resource information for all files with a changed timestamp. 248 */ 249 public Set<T> getChangedResourcesInfo() 250 { 251 252 Set<T> changedResourcesInfo = new HashSet<>(); 253 254 for (Map.Entry<File, TrackingInfo> entry : fileToTimestamp.entrySet()) 255 { 256 long newTimestamp = readTimestamp(entry.getKey()); 257 final TrackingInfo value = entry.getValue(); 258 long current = value.timestamp; 259 260 if (current != newTimestamp) 261 { 262 if (value.resourceInfo != null) 263 { 264 changedResourcesInfo.add(value.resourceInfo); 265 } 266 value.timestamp = newTimestamp; 267 } 268 } 269 270 return changedResourcesInfo; 271 } 272 273 274 /** 275 * Returns the time that the specified file was last modified, possibly rounded down to the nearest second. 276 */ 277 private long readTimestamp(File file) 278 { 279 if (!file.exists()) 280 return FILE_DOES_NOT_EXIST_TIMESTAMP; 281 282 return applyGranularity(file.lastModified()); 283 } 284 285 private long applyGranularity(long timestamp) 286 { 287 // For coarse granularity (accurate only to the last second), remove the milliseconds since 288 // the last full second. This is for compatibility with client HTTP requests, which 289 // are only accurate to one second. The extra level of detail creates false positives 290 // for changes, and undermines HTTP response caching in the client. 291 292 if (granularitySeconds) 293 return timestamp - (timestamp % 1000); 294 295 return timestamp; 296 } 297 298 /** 299 * Needed for testing; changes file timestamps so that a change will be detected by {@link #containsChanges()}. 300 */ 301 public void forceChange() 302 { 303 for (Map.Entry<File, TrackingInfo> e : fileToTimestamp.entrySet()) 304 { 305 e.getValue().timestamp = 0l; 306 } 307 } 308 309 /** 310 * Needed for testing. 311 */ 312 int trackedFileCount() 313 { 314 return fileToTimestamp.size(); 315 } 316 317 private final class TrackingInfo 318 { 319 320 private long timestamp; 321 private T resourceInfo; 322 323 public TrackingInfo(long timestamp, T resourceInfo) 324 { 325 this.timestamp = timestamp; 326 this.resourceInfo = resourceInfo; 327 } 328 329 @Override 330 public String toString() 331 { 332 return "Info [timestamp=" + timestamp + ", resourceInfo=" + resourceInfo + "]"; 333 } 334 335 } 336 337}