    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.commons.util.CollectionFactory;
    018import org.apache.tapestry5.ioc.internal.services.ClasspathURLConverterImpl;
    019import org.apache.tapestry5.ioc.services.ClasspathURLConverter;
    020
    021import java.io.File;
    022import java.io.IOException;
    023import java.net.URISyntaxException;
    024import java.net.URL;
    025import java.util.Map;
    026
    027/**
    028 * Given a (growing) set of URLs, can periodically check to see if any of the underlying resources has changed. This
    029 * class is capable of using either millisecond-level granularity or second-level granularity. Millisecond-level
    030 * granularity is used by default. Second-level granularity is provided for compatibility with browsers vis-a-vis
    031 * resource caching -- that's how granular they get with their "If-Modified-Since", "Last-Modified" and "Expires"
    032 * headers.
    033 */
    034public class URLChangeTracker
    035{
    036    private static final long FILE_DOES_NOT_EXIST_TIMESTAMP = -1L;
    037
    038    private final Map<File, Long> fileToTimestamp = CollectionFactory.newConcurrentMap();
    039
    040    private final boolean granularitySeconds;
    041
    042    private final boolean trackFolderChanges;
    043
    044    private final ClasspathURLConverter classpathURLConverter;
    045
    046    public static final ClasspathURLConverter DEFAULT_CONVERTER = new ClasspathURLConverterImpl();
    047
    048    /**
    049     * Creates a tracker using the default (does nothing) URL converter, with default (millisecond)
    050     * granularity and folder tracking disabled.
    051     *
    052     * @since 5.2.1
    053     */
    054    public URLChangeTracker()
    055    {
    056        this(DEFAULT_CONVERTER, false, false);
    057    }
    058
    059    /**
    060     * Creates a new URL change tracker with millisecond-level granularity and folder checking enabled.
    061     *
    062     * @param classpathURLConverter
    063     *            used to convert URLs from one protocol to another
    064     */
    065    public URLChangeTracker(ClasspathURLConverter classpathURLConverter)
    066    {
    067        this(classpathURLConverter, false);
    068
    069    }
    070
    071    /**
    072     * Creates a new URL change tracker, using either millisecond-level granularity or second-level granularity and
    073     * folder checking enabled.
    074     *
    075     * @param classpathURLConverter
    076     *            used to convert URLs from one protocol to another
    077     * @param granularitySeconds
    078     *            whether or not to use second granularity (as opposed to millisecond granularity)
    079     */
    080    public URLChangeTracker(ClasspathURLConverter classpathURLConverter, boolean granularitySeconds)
    081    {
    082        this(classpathURLConverter, granularitySeconds, true);
    083    }
    084
    085    /**
    086     * Creates a new URL change tracker, using either millisecond-level granularity or second-level granularity.
    087     *
    088     * @param classpathURLConverter
    089     *            used to convert URLs from one protocol to another
    090     * @param granularitySeconds
    091     *            whether or not to use second granularity (as opposed to millisecond granularity)
    092     * @param trackFolderChanges
    093     *            if true, then adding a file URL will also track the folder containing the file (this
    094     *            is useful when concerned about additions to a folder)
    095     * @since 5.2.1
    096     */
    097    public URLChangeTracker(ClasspathURLConverter classpathURLConverter, boolean granularitySeconds,
    098            boolean trackFolderChanges)
    099    {
    100        this.granularitySeconds = granularitySeconds;
    101        this.classpathURLConverter = classpathURLConverter;
    102        this.trackFolderChanges = trackFolderChanges;
    103    }
    104
    105    /**
    106     * Converts a URL with protocol "file" to a File instance.
    107     *
    108     * @since 5.2.0
    109     */
    110    public static File toFileFromFileProtocolURL(URL url)
    111    {
    112        assert url != null;
    113
    114        if (!url.getProtocol().equals("file"))
    115            throw new IllegalArgumentException(String.format("URL %s does not use the 'file' protocol.", url));
    116
    117        // http://weblogs.java.net/blog/kohsuke/archive/2007/04/how_to_convert.html
    118
    119        try
    120        {
    121            return new File(url.toURI());
    122        } catch (URISyntaxException ex)
    123        {
    124            return new File(url.getPath());
    125        }
    126    }
    127
    128    /**
    129     * Stores a new URL into the tracker, or returns the previous time stamp for a previously added URL. Filters out all
    130     * non-file URLs.
    131     *
    132     * @param url
    133     *            of the resource to add, or null if not known
    134     * @return the current timestamp for the URL (possibly rounded off for granularity reasons), or 0 if the URL is
    135     *         null
    136     */
    137    public long add(URL url)
    138    {
    139        if (url == null)
    140            return 0;
    141
    142        URL converted = classpathURLConverter.convert(url);
    143
    144        if (!converted.getProtocol().equals("file"))
    145            return timestampForNonFileURL(converted);
    146
    147        File resourceFile = toFileFromFileProtocolURL(converted);
    148
    149        if (fileToTimestamp.containsKey(resourceFile))
    150            return fileToTimestamp.get(resourceFile);
    151
    152        long timestamp = readTimestamp(resourceFile);
    153
    154        // A quick and imperfect fix for TAPESTRY-1918. When a file
    155        // is added, add the directory containing the file as well.
    156
    157        fileToTimestamp.put(resourceFile, timestamp);
    158
    159        if (trackFolderChanges)
    160        {
    161            File dir = resourceFile.getParentFile();
    162
    163            if (!fileToTimestamp.containsKey(dir))
    164            {
    165                long dirTimestamp = readTimestamp(dir);
    166                fileToTimestamp.put(dir, dirTimestamp);
    167            }
    168        }
    169
    170        return timestamp;
    171    }
    172
    173    private long timestampForNonFileURL(URL url)
    174    {
    175        long timestamp;
    176
    177        try
    178        {
    179            timestamp = url.openConnection().getLastModified();
    180        }
    181        catch (IOException ex)
    182        {
    183            throw new RuntimeException(ex);
    184        }
    185
    186        return applyGranularity(timestamp);
    187    }
    188
    189    /**
    190     * Clears all URL and timestamp data stored in the tracker.
    191     */
    192    public void clear()
    193    {
    194        fileToTimestamp.clear();
    195    }
    196
    197    /**
    198     * Re-acquires the last updated timestamp for each URL and returns true if any timestamp has changed.
    199     */
    200    public boolean containsChanges()
    201    {
    202        boolean result = false;
    203
    204        // This code would be highly suspect if this method was expected to be invoked
    205        // concurrently, but CheckForUpdatesFilter ensures that it will be invoked
    206        // synchronously.
    207
    208        for (Map.Entry<File, Long> entry : fileToTimestamp.entrySet())
    209        {
    210            long newTimestamp = readTimestamp(entry.getKey());
    211            long current = entry.getValue();
    212
    213            if (current == newTimestamp)
    214                continue;
    215
    216            result = true;
    217            entry.setValue(newTimestamp);
    218        }
    219
    220        return result;
    221    }
    222
    223    /**
    224     * Returns the time that the specified file was last modified, possibly rounded down to the nearest second.
    225     */
    226    private long readTimestamp(File file)
    227    {
    228        if (!file.exists())
    229            return FILE_DOES_NOT_EXIST_TIMESTAMP;
    230
    231        return applyGranularity(file.lastModified());
    232    }
    233
    234    private long applyGranularity(long timestamp)
    235    {
    236        // For coarse granularity (accurate only to the last second), remove the milliseconds since
    237        // the last full second. This is for compatibility with client HTTP requests, which
    238        // are only accurate to one second. The extra level of detail creates false positives
    239        // for changes, and undermines HTTP response caching in the client.
    240
    241        if (granularitySeconds)
    242            return timestamp - (timestamp % 1000);
    243
    244        return timestamp;
    245    }
    246
    247    /**
    248     * Needed for testing; changes file timestamps so that a change will be detected by {@link #containsChanges()}.
    249     */
    250    public void forceChange()
    251    {
    252        for (Map.Entry<File, Long> e : fileToTimestamp.entrySet())
    253        {
    254            e.setValue(0l);
    255        }
    256    }
    257
    258    /**
    259     * Needed for testing.
    260     */
    261    int trackedFileCount()
    262    {
    263        return fileToTimestamp.size();
    264    }
    265
    266}