blob: 43ece0fa7aea5d5e40f6206fd4da5944a041565e [file] [log] [blame]
// Copyright 2006, 2007, 2008, 2010 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry5.ioc.internal.util;
import org.apache.tapestry5.ioc.internal.services.ClasspathURLConverterImpl;
import org.apache.tapestry5.ioc.services.ClassFabUtils;
import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Map;
/**
* Given a (growing) set of URLs, can periodically check to see if any of the underlying resources has changed. This
* class is capable of using either millisecond-level granularity or second-level granularity. Millisecond-level
* granularity is used by default. Second-level granularity is provided for compatibility with browsers vis-a-vis
* resource caching -- that's how granular they get with their "If-Modified-Since", "Last-Modified" and "Expires"
* headers.
*/
public class URLChangeTracker
{
private static final long FILE_DOES_NOT_EXIST_TIMESTAMP = -1L;
private final Map<File, Long> fileToTimestamp = CollectionFactory.newConcurrentMap();
private final boolean granularitySeconds;
private final boolean trackFolderChanges;
private final ClasspathURLConverter classpathURLConverter;
public static final ClasspathURLConverter DEFAULT_CONVERTER = new ClasspathURLConverterImpl();
/**
* Creates a tracker using the default (does nothing) URL converter, with default (millisecond)
* granularity and folder tracking disabled.
*
* @since 5.2.1
*/
public URLChangeTracker()
{
this(DEFAULT_CONVERTER, false, false);
}
/**
* Creates a new URL change tracker with millisecond-level granularity and folder checking enabled.
*
* @param classpathURLConverter
* used to convert URLs from one protocol to another
*/
public URLChangeTracker(ClasspathURLConverter classpathURLConverter)
{
this(classpathURLConverter, false);
}
/**
* Creates a new URL change tracker, using either millisecond-level granularity or second-level granularity and
* folder checking enabled.
*
* @param classpathURLConverter
* used to convert URLs from one protocol to another
* @param granularitySeconds
* whether or not to use second granularity (as opposed to millisecond granularity)
*/
public URLChangeTracker(ClasspathURLConverter classpathURLConverter, boolean granularitySeconds)
{
this(classpathURLConverter, granularitySeconds, true);
}
/**
* Creates a new URL change tracker, using either millisecond-level granularity or second-level granularity.
*
* @param classpathURLConverter
* used to convert URLs from one protocol to another
* @param granularitySeconds
* whether or not to use second granularity (as opposed to millisecond granularity)
* @param trackFolderChanges
* if true, then adding a file URL will also track the folder containing the file (this
* is useful when concerned about additions to a folder)
* @since 5.2.1
*/
public URLChangeTracker(ClasspathURLConverter classpathURLConverter, boolean granularitySeconds,
boolean trackFolderChanges)
{
this.granularitySeconds = granularitySeconds;
this.classpathURLConverter = classpathURLConverter;
this.trackFolderChanges = trackFolderChanges;
}
/**
* Stores a new URL into the tracker, or returns the previous time stamp for a previously added URL. Filters out all
* non-file URLs.
*
* @param url
* of the resource to add, or null if not known
* @return the current timestamp for the URL (possibly rounded off for granularity reasons), or 0 if the URL is
* null
*/
public long add(URL url)
{
if (url == null)
return 0;
URL converted = classpathURLConverter.convert(url);
if (!converted.getProtocol().equals("file"))
return timestampForNonFileURL(converted);
File resourceFile = ClassFabUtils.toFileFromFileProtocolURL(converted);
if (fileToTimestamp.containsKey(resourceFile))
return fileToTimestamp.get(resourceFile);
long timestamp = readTimestamp(resourceFile);
// A quick and imperfect fix for TAPESTRY-1918. When a file
// is added, add the directory containing the file as well.
fileToTimestamp.put(resourceFile, timestamp);
if (trackFolderChanges)
{
File dir = resourceFile.getParentFile();
if (!fileToTimestamp.containsKey(dir))
{
long dirTimestamp = readTimestamp(dir);
fileToTimestamp.put(dir, dirTimestamp);
}
}
return timestamp;
}
private long timestampForNonFileURL(URL url)
{
long timestamp;
try
{
timestamp = url.openConnection().getLastModified();
}
catch (IOException ex)
{
throw new RuntimeException(ex);
}
return applyGranularity(timestamp);
}
/**
* Clears all URL and timestamp data stored in the tracker.
*/
public void clear()
{
fileToTimestamp.clear();
}
/**
* Re-acquires the last updated timestamp for each URL and returns true if any timestamp has changed.
*/
public boolean containsChanges()
{
boolean result = false;
// This code would be highly suspect if this method was expected to be invoked
// concurrently, but CheckForUpdatesFilter ensures that it will be invoked
// synchronously.
for (Map.Entry<File, Long> entry : fileToTimestamp.entrySet())
{
long newTimestamp = readTimestamp(entry.getKey());
long current = entry.getValue();
if (current == newTimestamp)
continue;
result = true;
entry.setValue(newTimestamp);
}
return result;
}
/**
* Returns the time that the specified file was last modified, possibly rounded down to the nearest second.
*/
private long readTimestamp(File file)
{
if (!file.exists())
return FILE_DOES_NOT_EXIST_TIMESTAMP;
return applyGranularity(file.lastModified());
}
private long applyGranularity(long timestamp)
{
// For coarse granularity (accurate only to the last second), remove the milliseconds since
// the last full second. This is for compatibility with client HTTP requests, which
// are only accurate to one second. The extra level of detail creates false positives
// for changes, and undermines HTTP response caching in the client.
if (granularitySeconds)
return timestamp - (timestamp % 1000);
return timestamp;
}
/**
* Needed for testing; changes file timestamps so that a change will be detected by {@link #containsChanges()}.
*/
public void forceChange()
{
for (Map.Entry<File, Long> e : fileToTimestamp.entrySet())
{
e.setValue(0l);
}
}
/**
* Needed for testing.
*/
int trackedFileCount()
{
return fileToTimestamp.size();
}
}