All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.myfaces.trinidad.resource.CachingResourceLoader Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.myfaces.trinidad.resource;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import java.util.concurrent.atomic.AtomicReference;

import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidad.util.Args;
import org.apache.myfaces.trinidad.util.URLUtils;

/**
 * Base class for resource loaders.  Resource loaders can lookup resources
 * as URLs from arbitrary locations, including JAR files.
 *
 */
public class CachingResourceLoader extends ResourceLoader
{
  /**
   * Constructs a new CachingResourceLoader.
   *
   * @param parent  the parent resource loader
   */
  public CachingResourceLoader(
    ResourceLoader parent)
  {
    super(parent);

    _cache = new ConcurrentHashMap();
  }

  

  /**
   * Returns the cached resource url if previously requested.  Otherwise,
   * fully reads the resource contents stores in the cache.
   *
   * @param path  the resource path
   *
   * @return the cached resource url
   *
   * @throws java.io.IOException  if an I/O error occurs
   */
  @Override
  protected URL findResource(
    String path
    ) throws IOException
  {
    URL url = _cache.get(path);

    if (url == null)
    {
      url = getParent().getResource(path);

      if (url != null)
      {
        url = new URL("cache", null, -1, path, new CachingURLStreamHandler(url));
        _cache.putIfAbsent(path, url);
      }
    }

    return url;
  }

  private final ConcurrentMap _cache;

  @Override
  public boolean isCachable()
  {
    return false;
  }

  /**
   * URLStreamHandler to cache URL contents and URLConnection headers.
   * 
   * The implementation is thread-safe.
   */
  static private final class CachingURLStreamHandler extends URLStreamHandler
  {
    public CachingURLStreamHandler(
      URL delegate)
    {
      _delegate = delegate;
      _contents = new AtomicReference();
    }

    /**
     * Compares a new content length against the length of the contents that
     * we have cached.  If these don't match, we need to dump the cache and reload
     * our contents.
     */
    public void validateContentLength(int newContentLength)
    {
      CachedContents contents = _contents.get();
      
      if ((contents != null) && !contents.validateContentLength(newContentLength))
      {
        // The new content length does not match the size of our cached
        // contents.  Clear out the cached contents and start over.
        _contents.compareAndSet(contents, null);
        _logResourceSizeChanged(newContentLength, contents);

      }
    }
    
    private void _logResourceSizeChanged(int newContentLength, CachedContents contents)
    {
      if (_LOG.isFine())
      {
        _LOG.fine("RESOURCE_SIZE_CHANGED",
                  new Object[]
                  {
                    newContentLength,
                    contents
                  });
      }      
    }

    @Override
    protected URLConnection openConnection(
      URL url
      ) throws IOException
    {
      return new URLConnectionImpl(url, _delegate.openConnection(), this);
    }

    protected InputStream getInputStream(URLConnection conn) throws IOException
    {
      CachedContents contents = _contents.get();
      
      if (contents == null || _isStale(contents, _delegate))
      {
        contents = _updateContents(conn);
        assert(contents != null);
      }

      return contents.toInputStream();
    }
    
    // Tests whether the CachedContents is stale based on the url's current lastModified time.
    private boolean _isStale(CachedContents contents, URL url) throws IOException
    {
      Args.notNull(contents, "contents");
      Args.notNull(url, "url");

      long lastModified = URLUtils.getLastModified(_delegate);
      return contents.isStale(lastModified);
    }

    private CachedContents _updateContents(URLConnection conn) throws IOException
    {
      CachedContents newContents = _createContents(conn);
      assert(newContents != null);

      // We're not doing a compareAndSet here because _contents may have
      // changed - eg. _contents may have been nulled out or set to a new
      // value by another request.  We're okay with replacing the current value
      // with our newly created instance.
      _contents.set(newContents);
      
      return newContents;
    }
    
    private CachedContents _createContents(URLConnection conn) throws IOException
    {
      // Note that the order of the following operations is intentional.
      // We get the last modified time before reading in the data in order
      // to protect against the possibility that the data is being modified
      // while read.  In this case, we want the earliest last modified time
      // to increase the chance that we will detect that our cached data
      // is stale on subsequent requests.
      long lastModified = URLUtils.getLastModified(conn);
      byte[] data = _readBytes(conn);
      int contentLength = conn.getContentLength();

      return new CachedContents(this._delegate, data, lastModified, contentLength);
    }

    @SuppressWarnings("oracle.jdeveloper.java.nested-assignment")
    private byte[] _readBytes(URLConnection conn) throws IOException
    {
      InputStream in = conn.getInputStream();
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      try
      {
        byte[] buffer = new byte[2048];
        int length;
        while ((length = (in.read(buffer))) >= 0)
        {
          out.write(buffer, 0, length);
        }
      }
      finally
      {
        in.close();
      }
      
      return out.toByteArray();
    }

    private final URL    _delegate;
    private final AtomicReference _contents;
  }
  
  // An immutable class that holds the data and metadadta for a single cached resource.
  // Note that we do not override equals() or hashCode() since we do not (yet) need
  // to hash or check for equality, but keep this in mind if we ever need to expand the
  // usage of this class.
  static private final class CachedContents
  {
    public CachedContents(
      URL resourceURL,
      byte[] data,
      long lastModified,
      int contentLength
      )
    {
      Args.notNull(data, "data");
      Args.notNull(resourceURL, "resourceURL");
      _ensureValidSize(resourceURL, data, contentLength);

      this._url = resourceURL;
      this._data = data;
      _lastModified = lastModified;
      _contentLength = contentLength;
    }

    public InputStream toInputStream()
    {
      return new ByteArrayInputStream(_data);       
    }

    /**
     * Tests whether this CacheContents instance contains stale data.
     * 
     * @return true if the specified lastModified time is new than
     *   the lastModified time that was recorded when this CachedContents
     *   instance was created.
     */
    public boolean isStale(long lastModified)
    {
      return (lastModified > _lastModified);
    }

    /**
     * Tests whether the specified content length is consistent with size of the
     * data held by this CachedContents.
     *
     * @return true if the newContentLength matches the current data size, false otherwise.
     */
    public boolean validateContentLength(int newContentLength)
    {
      return _isValidSize(_data, newContentLength);
    }

    /**
     * The string representation of this internal class is unspecified.
     * There is no reason anyone should need to parse this string representation,
     * but if that ever becomes an issue, listen to Joshua Bloch and add accessors
     * instead.  (See Item 10 in Effective Java, 2nd ed.)
     */
    @Override
    public String toString()
    {
      String urlString = _url.toString();
      String sizeString = Integer.toString(_data.length);
      int builderLength = urlString.length() + sizeString.length() + 13;
      
      StringBuilder builder = new StringBuilder(builderLength);
      builder.append("[url=");
      builder.append(urlString);
      builder.append(", size=");
      builder.append(sizeString);
      builder.append("]");
      
      return builder.toString();
    }

    private void _ensureValidSize(
      URL resourceURL,
      byte[] data,
      int contentLength
      ) throws IllegalStateException
    {
      assert(data != null);
      
      if (!_isValidSize(data, contentLength))
      {
        String messageKey = "INVALID_RESOURCE_SIZE";
        String message = _LOG.getMessage(messageKey,
                                         new Object[]
                                         {
                                           resourceURL.toString(),
                                           data.length,
                                           contentLength
                                         });
        
        _LOG.severe(message);
        
        // The message contains potentially sensitive data (eg. file system paths).
        // In order to make sure that this doesn't escape to the client, we don't
        // include the message in the exception.  The message key should be sufficient.
        throw new IllegalStateException(messageKey);
      }
    }
    
    private boolean _isValidSize(byte[] data, int contentLength)
    {
      assert(data != null);
      return ((contentLength < 0) || (contentLength == data.length));      
    }

    private final URL _url;
    private final byte[] _data;
    private final long _lastModified;
    private final int _contentLength;
  }

  /**
   * URLConnection to cache URL contents and header fields.
   */
  static private class URLConnectionImpl extends URLConnection
  {
    /**
     * Creates a new URLConnectionImpl.
     *
     * @param url      the cached url
     * @param handler  the caching stream handler
     */
    public URLConnectionImpl(
      URL                  url,
      URLConnection        conn,
      CachingURLStreamHandler handler)
    {
      super(url);
      _conn = conn;
      _handler = handler;
    }

    @Override
    public void connect() throws IOException
    {
      // cache: no-op
    }

    @Override
    public String getContentType()
    {
      return _conn.getContentType();
    }

    @Override
    public int getContentLength()
    {
      int contentLength = _conn.getContentLength();
      _handler.validateContentLength(contentLength);

      return contentLength;
    }

    @Override
    public long getLastModified()
    {
      try
      {
        return URLUtils.getLastModified(_conn);
      }
      catch (IOException exception)
      {
        return -1;
      }
    }

    @Override
    public String getHeaderField(
      String name)
    {
      return _conn.getHeaderField(name);
    }

    @Override
    public InputStream getInputStream() throws IOException
    {
      return _handler.getInputStream(_conn);
    }

    private final URLConnection        _conn;
    private final CachingURLStreamHandler _handler;
  }

  static private final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(CachingResourceLoader.class);
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy