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 }