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