001: // Copyright 2006, 2007 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.tapestry.internal.util;
016:
017: import static org.apache.tapestry.ioc.internal.util.CollectionFactory.newConcurrentMap;
018:
019: import java.io.File;
020: import java.net.URI;
021: import java.net.URISyntaxException;
022: import java.net.URL;
023: import java.util.Map;
024:
025: /**
026: * Given a (growing) set of URLs, can periodically check to see if any of the underlying resources
027: * has changed. This class is capable of using either millisecond-level granularity or second-level
028: * granularity. Millisecond-level granularity is used by default. Second-level granularity is
029: * provided for compatibility with browsers vis-a-vis resource caching -- that's how granular they
030: * get with their "If-Modified-Since", "Last-Modified" and "Expires" headers.
031: */
032: public class URLChangeTracker {
033: private static final long FILE_DOES_NOT_EXIST_TIMESTAMP = -1L;
034:
035: private final Map<File, Long> _fileToTimestamp = newConcurrentMap();
036:
037: private boolean _granularitySeconds;
038:
039: /**
040: * Creates a new URL change tracker with millisecond-level granularity.
041: */
042: public URLChangeTracker() {
043: this (false);
044: }
045:
046: /**
047: * Creates a new URL change tracker, using either millisecond-level granularity or second-level
048: * granularity.
049: *
050: * @param granularitySeconds
051: * whether or not to use second-level granularity
052: */
053: public URLChangeTracker(boolean granularitySeconds) {
054: _granularitySeconds = granularitySeconds;
055: }
056:
057: /**
058: * Stores a new URL into the tracker, or returns the previous time stamp for a previously added
059: * URL. Filters out all non-file URLs.
060: *
061: * @param url
062: * of the resource to add
063: * @return the current timestamp for the URL, or 0 if not a file URL
064: */
065: public long add(URL url) {
066: if (!url.getProtocol().equals("file"))
067: return 0;
068:
069: try {
070: URI resourceURI = url.toURI();
071: File resourceFile = new File(resourceURI);
072:
073: if (_fileToTimestamp.containsKey(resourceFile))
074: return _fileToTimestamp.get(resourceFile);
075:
076: long timestamp = readTimestamp(resourceFile);
077:
078: _fileToTimestamp.put(resourceFile, timestamp);
079:
080: return timestamp;
081: } catch (URISyntaxException ex) {
082: throw new RuntimeException(ex);
083: }
084: }
085:
086: /**
087: * Clears all URL and timestamp data stored in the tracker.
088: */
089: public void clear() {
090: _fileToTimestamp.clear();
091: }
092:
093: /**
094: * Re-acquires the last updated timestamp for each URL and returns true if any timestamp has
095: * changed.
096: */
097: public boolean containsChanges() {
098: boolean result = false;
099:
100: // This code would be highly suspect if this method was expected to be invoked
101: // concurrently, but CheckForUpdatesFilter ensures that it will be invoked
102: // synchronously.
103:
104: for (Map.Entry<File, Long> entry : _fileToTimestamp.entrySet()) {
105: long newTimestamp = readTimestamp(entry.getKey());
106: long current = entry.getValue();
107:
108: if (current == newTimestamp)
109: continue;
110:
111: result = true;
112: entry.setValue(newTimestamp);
113: }
114:
115: return result;
116: }
117:
118: /**
119: * Returns the time that the specified file was last modified, possibly rounded down to the
120: * nearest second.
121: */
122: private long readTimestamp(File file) {
123: if (!file.exists())
124: return FILE_DOES_NOT_EXIST_TIMESTAMP;
125:
126: long timestamp = file.lastModified();
127: if (_granularitySeconds)
128: timestamp -= timestamp % 1000;
129: return timestamp;
130: }
131:
132: /**
133: * Needed for testing; changes file timestamps so that a change will be detected by
134: * {@link #containsChanges()}.
135: */
136: public void forceChange() {
137: for (Map.Entry<File, Long> e : _fileToTimestamp.entrySet()) {
138: e.setValue(0l);
139: }
140: }
141:
142: /** Needed for testing. */
143: int trackedFileCount() {
144: return _fileToTimestamp.size();
145: }
146:
147: }
|