    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    }