View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.trinidad.resource;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.net.URL;
26  import java.net.URLConnection;
27  import java.net.URLStreamHandler;
28  import java.util.concurrent.ConcurrentHashMap;
29  import java.util.concurrent.ConcurrentMap;
30  
31  import java.util.concurrent.atomic.AtomicReference;
32  
33  import org.apache.myfaces.trinidad.logging.TrinidadLogger;
34  import org.apache.myfaces.trinidad.util.Args;
35  import org.apache.myfaces.trinidad.util.URLUtils;
36  
37  /**
38   * Base class for resource loaders.  Resource loaders can lookup resources
39   * as URLs from arbitrary locations, including JAR files.
40   *
41   */
42  public class CachingResourceLoader extends ResourceLoader
43  {
44    /**
45     * Constructs a new CachingResourceLoader.
46     *
47     * @param parent  the parent resource loader
48     */
49    public CachingResourceLoader(
50      ResourceLoader parent)
51    {
52      super(parent);
53  
54      _cache = new ConcurrentHashMap<String, URL>();
55    }
56  
57    
58  
59    /**
60     * Returns the cached resource url if previously requested.  Otherwise,
61     * fully reads the resource contents stores in the cache.
62     *
63     * @param path  the resource path
64     *
65     * @return the cached resource url
66     *
67     * @throws java.io.IOException  if an I/O error occurs
68     */
69    @Override
70    protected URL findResource(
71      String path
72      ) throws IOException
73    {
74      URL url = _cache.get(path);
75  
76      if (url == null)
77      {
78        url = getParent().getResource(path);
79  
80        if (url != null)
81        {
82          url = new URL("cache", null, -1, path, new CachingURLStreamHandler(url));
83          _cache.putIfAbsent(path, url);
84        }
85      }
86  
87      return url;
88    }
89  
90    private final ConcurrentMap<String, URL> _cache;
91  
92    @Override
93    public boolean isCachable()
94    {
95      return false;
96    }
97  
98    /**
99     * URLStreamHandler to cache URL contents and URLConnection headers.
100    * 
101    * The implementation is thread-safe.
102    */
103   static private final class CachingURLStreamHandler extends URLStreamHandler
104   {
105     public CachingURLStreamHandler(
106       URL delegate)
107     {
108       _delegate = delegate;
109       _contents = new AtomicReference<CachedContents>();
110     }
111 
112     /**
113      * Compares a new content length against the length of the contents that
114      * we have cached.  If these don't match, we need to dump the cache and reload
115      * our contents.
116      */
117     public void validateContentLength(int newContentLength)
118     {
119       CachedContents contents = _contents.get();
120       
121       if ((contents != null) && !contents.validateContentLength(newContentLength))
122       {
123         // The new content length does not match the size of our cached
124         // contents.  Clear out the cached contents and start over.
125         _contents.compareAndSet(contents, null);
126         _logResourceSizeChanged(newContentLength, contents);
127 
128       }
129     }
130     
131     private void _logResourceSizeChanged(int newContentLength, CachedContents contents)
132     {
133       if (_LOG.isFine())
134       {
135         _LOG.fine("RESOURCE_SIZE_CHANGED",
136                   new Object[]
137                   {
138                     newContentLength,
139                     contents
140                   });
141       }      
142     }
143 
144     @Override
145     protected URLConnection openConnection(
146       URL url
147       ) throws IOException
148     {
149       return new URLConnectionImpl(url, _delegate.openConnection(), this);
150     }
151 
152     protected InputStream getInputStream(URLConnection conn) throws IOException
153     {
154       CachedContents contents = _contents.get();
155       
156       if (contents == null || _isStale(contents, _delegate))
157       {
158         contents = _updateContents(conn);
159         assert(contents != null);
160       }
161 
162       return contents.toInputStream();
163     }
164     
165     // Tests whether the CachedContents is stale based on the url's current lastModified time.
166     private boolean _isStale(CachedContents contents, URL url) throws IOException
167     {
168       Args.notNull(contents, "contents");
169       Args.notNull(url, "url");
170 
171       long lastModified = URLUtils.getLastModified(_delegate);
172       return contents.isStale(lastModified);
173     }
174 
175     private CachedContents _updateContents(URLConnection conn) throws IOException
176     {
177       CachedContents newContents = _createContents(conn);
178       assert(newContents != null);
179 
180       // We're not doing a compareAndSet here because _contents may have
181       // changed - eg. _contents may have been nulled out or set to a new
182       // value by another request.  We're okay with replacing the current value
183       // with our newly created instance.
184       _contents.set(newContents);
185       
186       return newContents;
187     }
188     
189     private CachedContents _createContents(URLConnection conn) throws IOException
190     {
191       // Note that the order of the following operations is intentional.
192       // We get the last modified time before reading in the data in order
193       // to protect against the possibility that the data is being modified
194       // while read.  In this case, we want the earliest last modified time
195       // to increase the chance that we will detect that our cached data
196       // is stale on subsequent requests.
197       long lastModified = URLUtils.getLastModified(conn);
198       byte[] data = _readBytes(conn);
199       int contentLength = conn.getContentLength();
200 
201       return new CachedContents(this._delegate, data, lastModified, contentLength);
202     }
203 
204     @SuppressWarnings("oracle.jdeveloper.java.nested-assignment")
205     private byte[] _readBytes(URLConnection conn) throws IOException
206     {
207       InputStream in = conn.getInputStream();
208       ByteArrayOutputStream out = new ByteArrayOutputStream();
209       try
210       {
211         byte[] buffer = new byte[2048];
212         int length;
213         while ((length = (in.read(buffer))) >= 0)
214         {
215           out.write(buffer, 0, length);
216         }
217       }
218       finally
219       {
220         in.close();
221       }
222       
223       return out.toByteArray();
224     }
225 
226     private final URL    _delegate;
227     private final AtomicReference<CachedContents> _contents;
228   }
229   
230   // An immutable class that holds the data and metadadta for a single cached resource.
231   // Note that we do not override equals() or hashCode() since we do not (yet) need
232   // to hash or check for equality, but keep this in mind if we ever need to expand the
233   // usage of this class.
234   static private final class CachedContents
235   {
236     public CachedContents(
237       URL resourceURL,
238       byte[] data,
239       long lastModified,
240       int contentLength
241       )
242     {
243       Args.notNull(data, "data");
244       Args.notNull(resourceURL, "resourceURL");
245       _ensureValidSize(resourceURL, data, contentLength);
246 
247       this._url = resourceURL;
248       this._data = data;
249       _lastModified = lastModified;
250       _contentLength = contentLength;
251     }
252 
253     public InputStream toInputStream()
254     {
255       return new ByteArrayInputStream(_data);       
256     }
257 
258     /**
259      * Tests whether this CacheContents instance contains stale data.
260      * 
261      * @return true if the specified lastModified time is new than
262      *   the lastModified time that was recorded when this CachedContents
263      *   instance was created.
264      */
265     public boolean isStale(long lastModified)
266     {
267       return (lastModified > _lastModified);
268     }
269 
270     /**
271      * Tests whether the specified content length is consistent with size of the
272      * data held by this CachedContents.
273      *
274      * @return true if the newContentLength matches the current data size, false otherwise.
275      */
276     public boolean validateContentLength(int newContentLength)
277     {
278       return _isValidSize(_data, newContentLength);
279     }
280 
281     /**
282      * The string representation of this internal class is unspecified.
283      * There is no reason anyone should need to parse this string representation,
284      * but if that ever becomes an issue, listen to Joshua Bloch and add accessors
285      * instead.  (See Item 10 in Effective Java, 2nd ed.)
286      */
287     @Override
288     public String toString()
289     {
290       String urlString = _url.toString();
291       String sizeString = Integer.toString(_data.length);
292       int builderLength = urlString.length() + sizeString.length() + 13;
293       
294       StringBuilder builder = new StringBuilder(builderLength);
295       builder.append("[url=");
296       builder.append(urlString);
297       builder.append(", size=");
298       builder.append(sizeString);
299       builder.append("]");
300       
301       return builder.toString();
302     }
303 
304     private void _ensureValidSize(
305       URL resourceURL,
306       byte[] data,
307       int contentLength
308       ) throws IllegalStateException
309     {
310       assert(data != null);
311       
312       if (!_isValidSize(data, contentLength))
313       {
314         String messageKey = "INVALID_RESOURCE_SIZE";
315         String message = _LOG.getMessage(messageKey,
316                                          new Object[]
317                                          {
318                                            resourceURL.toString(),
319                                            data.length,
320                                            contentLength
321                                          });
322         
323         _LOG.severe(message);
324         
325         // The message contains potentially sensitive data (eg. file system paths).
326         // In order to make sure that this doesn't escape to the client, we don't
327         // include the message in the exception.  The message key should be sufficient.
328         throw new IllegalStateException(messageKey);
329       }
330     }
331     
332     private boolean _isValidSize(byte[] data, int contentLength)
333     {
334       assert(data != null);
335       return ((contentLength < 0) || (contentLength == data.length));      
336     }
337 
338     private final URL _url;
339     private final byte[] _data;
340     private final long _lastModified;
341     private final int _contentLength;
342   }
343 
344   /**
345    * URLConnection to cache URL contents and header fields.
346    */
347   static private class URLConnectionImpl extends URLConnection
348   {
349     /**
350      * Creates a new URLConnectionImpl.
351      *
352      * @param url      the cached url
353      * @param handler  the caching stream handler
354      */
355     public URLConnectionImpl(
356       URL                  url,
357       URLConnection        conn,
358       CachingURLStreamHandler handler)
359     {
360       super(url);
361       _conn = conn;
362       _handler = handler;
363     }
364 
365     @Override
366     public void connect() throws IOException
367     {
368       // cache: no-op
369     }
370 
371     @Override
372     public String getContentType()
373     {
374       return _conn.getContentType();
375     }
376 
377     @Override
378     public int getContentLength()
379     {
380       int contentLength = _conn.getContentLength();
381       _handler.validateContentLength(contentLength);
382 
383       return contentLength;
384     }
385 
386     @Override
387     public long getLastModified()
388     {
389       try
390       {
391         return URLUtils.getLastModified(_conn);
392       }
393       catch (IOException exception)
394       {
395         return -1;
396       }
397     }
398 
399     @Override
400     public String getHeaderField(
401       String name)
402     {
403       return _conn.getHeaderField(name);
404     }
405 
406     @Override
407     public InputStream getInputStream() throws IOException
408     {
409       return _handler.getInputStream(_conn);
410     }
411 
412     private final URLConnection        _conn;
413     private final CachingURLStreamHandler _handler;
414   }
415 
416   static private final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(CachingResourceLoader.class);
417 }