/*
 * 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.catalina.webresources;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.net.URL;
import java.security.cert.Certificate;
import java.util.jar.Manifest;

import org.apache.catalina.WebResource;
import org.apache.catalina.WebResourceRoot;

This class is designed to wrap a 'raw' WebResource and providing caching for expensive operations. Inexpensive operations may be passed through to the underlying resource.
/** * This class is designed to wrap a 'raw' WebResource and providing caching for * expensive operations. Inexpensive operations may be passed through to the * underlying resource. */
public class CachedResource implements WebResource { // Estimate (on high side to be safe) of average size excluding content // based on profiler data. private static final long CACHE_ENTRY_SIZE = 500; private final Cache cache; private final StandardRoot root; private final String webAppPath; private final long ttl; private final int objectMaxSizeBytes; private final boolean usesClassLoaderResources; private volatile WebResource webResource; private volatile WebResource[] webResources; private volatile long nextCheck; private volatile Long cachedLastModified = null; private volatile String cachedLastModifiedHttp = null; private volatile byte[] cachedContent = null; private volatile Boolean cachedIsFile = null; private volatile Boolean cachedIsDirectory = null; private volatile Boolean cachedExists = null; private volatile Boolean cachedIsVirtual = null; private volatile Long cachedContentLength = null; public CachedResource(Cache cache, StandardRoot root, String path, long ttl, int objectMaxSizeBytes, boolean usesClassLoaderResources) { this.cache = cache; this.root = root; this.webAppPath = path; this.ttl = ttl; this.objectMaxSizeBytes = objectMaxSizeBytes; this.usesClassLoaderResources = usesClassLoaderResources; } protected boolean validateResource(boolean useClassLoaderResources) { // It is possible that some resources will only be visible for a given // value of useClassLoaderResources. Therefore, if the lookup is made // with a different value of useClassLoaderResources than was used when // creating the cache entry, invalidate the entry. This should have // minimal performance impact as it would be unusual for a resource to // be looked up both as a static resource and as a class loader // resource. if (usesClassLoaderResources != useClassLoaderResources) { return false; } long now = System.currentTimeMillis(); if (webResource == null) { synchronized (this) { if (webResource == null) { webResource = root.getResourceInternal( webAppPath, useClassLoaderResources); getLastModified(); getContentLength(); nextCheck = ttl + now; // exists() is a relatively expensive check for a file so // use the fact that we know if it exists at this point if (webResource instanceof EmptyResource) { cachedExists = Boolean.FALSE; } else { cachedExists = Boolean.TRUE; } return true; } } } if (now < nextCheck) { return true; } // Assume resources inside WARs will not change if (!root.isPackedWarFile()) { WebResource webResourceInternal = root.getResourceInternal( webAppPath, useClassLoaderResources); if (!webResource.exists() && webResourceInternal.exists()) { return false; } // If modified date or length change - resource has changed / been // removed etc. if (webResource.getLastModified() != getLastModified() || webResource.getContentLength() != getContentLength()) { return false; } // Has a resource been inserted / removed in a different resource set if (webResource.getLastModified() != webResourceInternal.getLastModified() || webResource.getContentLength() != webResourceInternal.getContentLength()) { return false; } } nextCheck = ttl + now; return true; } protected boolean validateResources(boolean useClassLoaderResources) { long now = System.currentTimeMillis(); if (webResources == null) { synchronized (this) { if (webResources == null) { webResources = root.getResourcesInternal( webAppPath, useClassLoaderResources); nextCheck = ttl + now; return true; } } } if (now < nextCheck) { return true; } // Assume resources inside WARs will not change if (root.isPackedWarFile()) { nextCheck = ttl + now; return true; } else { // At this point, always expire the entry and re-populating it is // likely to be as expensive as validating it. return false; } } protected long getNextCheck() { return nextCheck; } @Override public long getLastModified() { Long cachedLastModified = this.cachedLastModified; if (cachedLastModified == null) { cachedLastModified = Long.valueOf(webResource.getLastModified()); this.cachedLastModified = cachedLastModified; } return cachedLastModified.longValue(); } @Override public String getLastModifiedHttp() { String cachedLastModifiedHttp = this.cachedLastModifiedHttp; if (cachedLastModifiedHttp == null) { cachedLastModifiedHttp = webResource.getLastModifiedHttp(); this.cachedLastModifiedHttp = cachedLastModifiedHttp; } return cachedLastModifiedHttp; } @Override public boolean exists() { Boolean cachedExists = this.cachedExists; if (cachedExists == null) { cachedExists = Boolean.valueOf(webResource.exists()); this.cachedExists = cachedExists; } return cachedExists.booleanValue(); } @Override public boolean isVirtual() { Boolean cachedIsVirtual = this.cachedIsVirtual; if (cachedIsVirtual == null) { cachedIsVirtual = Boolean.valueOf(webResource.isVirtual()); this.cachedIsVirtual = cachedIsVirtual; } return cachedIsVirtual.booleanValue(); } @Override public boolean isDirectory() { Boolean cachedIsDirectory = this.cachedIsDirectory; if (cachedIsDirectory == null) { cachedIsDirectory = Boolean.valueOf(webResource.isDirectory()); this.cachedIsDirectory = cachedIsDirectory; } return cachedIsDirectory.booleanValue(); } @Override public boolean isFile() { Boolean cachedIsFile = this.cachedIsFile; if (cachedIsFile == null) { cachedIsFile = Boolean.valueOf(webResource.isFile()); this.cachedIsFile = cachedIsFile; } return cachedIsFile.booleanValue(); } @Override public boolean delete() { boolean deleteResult = webResource.delete(); if (deleteResult) { cache.removeCacheEntry(webAppPath); } return deleteResult; } @Override public String getName() { return webResource.getName(); } @Override public long getContentLength() { Long cachedContentLength = this.cachedContentLength; if (cachedContentLength == null) { long result = 0; if (webResource != null) { result = webResource.getContentLength(); cachedContentLength = Long.valueOf(result); this.cachedContentLength = cachedContentLength; } return result; } return cachedContentLength.longValue(); } @Override public String getCanonicalPath() { return webResource.getCanonicalPath(); } @Override public boolean canRead() { return webResource.canRead(); } @Override public String getWebappPath() { return webAppPath; } @Override public String getETag() { return webResource.getETag(); } @Override public void setMimeType(String mimeType) { webResource.setMimeType(mimeType); } @Override public String getMimeType() { return webResource.getMimeType(); } @Override public InputStream getInputStream() { byte[] content = getContent(); if (content == null) { // Can't cache InputStreams return webResource.getInputStream(); } return new ByteArrayInputStream(content); } @Override public byte[] getContent() { byte[] cachedContent = this.cachedContent; if (cachedContent == null) { if (getContentLength() > objectMaxSizeBytes) { return null; } cachedContent = webResource.getContent(); this.cachedContent = cachedContent; } return cachedContent; } @Override public long getCreation() { return webResource.getCreation(); } @Override public URL getURL() { return webResource.getURL(); } @Override public URL getCodeBase() { return webResource.getCodeBase(); } @Override public Certificate[] getCertificates() { return webResource.getCertificates(); } @Override public Manifest getManifest() { return webResource.getManifest(); } @Override public WebResourceRoot getWebResourceRoot() { return webResource.getWebResourceRoot(); } WebResource getWebResource() { return webResource; } WebResource[] getWebResources() { return webResources; } // Assume that the cache entry will always include the content unless the // resource content is larger than objectMaxSizeBytes. This isn't always the // case but it makes tracking the current cache size easier. long getSize() { long result = CACHE_ENTRY_SIZE; if (getContentLength() <= objectMaxSizeBytes) { result += getContentLength(); } return result; } }