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    }