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