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 015 package org.apache.tapestry5.ioc.internal.util; 016 017 import org.apache.tapestry5.ioc.internal.services.ClasspathURLConverterImpl; 018 import org.apache.tapestry5.ioc.services.ClassFabUtils; 019 import org.apache.tapestry5.ioc.services.ClasspathURLConverter; 020 021 import java.io.File; 022 import java.io.IOException; 023 import java.net.URL; 024 import 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 */ 033 public 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 * Stores a new URL into the tracker, or returns the previous time stamp for a previously added URL. Filters out all 106 * non-file URLs. 107 * 108 * @param url 109 * of the resource to add, or null if not known 110 * @return the current timestamp for the URL (possibly rounded off for granularity reasons), or 0 if the URL is 111 * null 112 */ 113 public long add(URL url) 114 { 115 if (url == null) 116 return 0; 117 118 URL converted = classpathURLConverter.convert(url); 119 120 if (!converted.getProtocol().equals("file")) 121 return timestampForNonFileURL(converted); 122 123 File resourceFile = ClassFabUtils.toFileFromFileProtocolURL(converted); 124 125 if (fileToTimestamp.containsKey(resourceFile)) 126 return fileToTimestamp.get(resourceFile); 127 128 long timestamp = readTimestamp(resourceFile); 129 130 // A quick and imperfect fix for TAPESTRY-1918. When a file 131 // is added, add the directory containing the file as well. 132 133 fileToTimestamp.put(resourceFile, timestamp); 134 135 if (trackFolderChanges) 136 { 137 File dir = resourceFile.getParentFile(); 138 139 if (!fileToTimestamp.containsKey(dir)) 140 { 141 long dirTimestamp = readTimestamp(dir); 142 fileToTimestamp.put(dir, dirTimestamp); 143 } 144 } 145 146 return timestamp; 147 } 148 149 private long timestampForNonFileURL(URL url) 150 { 151 long timestamp; 152 153 try 154 { 155 timestamp = url.openConnection().getLastModified(); 156 } 157 catch (IOException ex) 158 { 159 throw new RuntimeException(ex); 160 } 161 162 return applyGranularity(timestamp); 163 } 164 165 /** 166 * Clears all URL and timestamp data stored in the tracker. 167 */ 168 public void clear() 169 { 170 fileToTimestamp.clear(); 171 } 172 173 /** 174 * Re-acquires the last updated timestamp for each URL and returns true if any timestamp has changed. 175 */ 176 public boolean containsChanges() 177 { 178 boolean result = false; 179 180 // This code would be highly suspect if this method was expected to be invoked 181 // concurrently, but CheckForUpdatesFilter ensures that it will be invoked 182 // synchronously. 183 184 for (Map.Entry<File, Long> entry : fileToTimestamp.entrySet()) 185 { 186 long newTimestamp = readTimestamp(entry.getKey()); 187 long current = entry.getValue(); 188 189 if (current == newTimestamp) 190 continue; 191 192 result = true; 193 entry.setValue(newTimestamp); 194 } 195 196 return result; 197 } 198 199 /** 200 * Returns the time that the specified file was last modified, possibly rounded down to the nearest second. 201 */ 202 private long readTimestamp(File file) 203 { 204 if (!file.exists()) 205 return FILE_DOES_NOT_EXIST_TIMESTAMP; 206 207 return applyGranularity(file.lastModified()); 208 } 209 210 private long applyGranularity(long timestamp) 211 { 212 // For coarse granularity (accurate only to the last second), remove the milliseconds since 213 // the last full second. This is for compatibility with client HTTP requests, which 214 // are only accurate to one second. The extra level of detail creates false positives 215 // for changes, and undermines HTTP response caching in the client. 216 217 if (granularitySeconds) 218 return timestamp - (timestamp % 1000); 219 220 return timestamp; 221 } 222 223 /** 224 * Needed for testing; changes file timestamps so that a change will be detected by {@link #containsChanges()}. 225 */ 226 public void forceChange() 227 { 228 for (Map.Entry<File, Long> e : fileToTimestamp.entrySet()) 229 { 230 e.setValue(0l); 231 } 232 } 233 234 /** 235 * Needed for testing. 236 */ 237 int trackedFileCount() 238 { 239 return fileToTimestamp.size(); 240 } 241 242 }