602 lines
20 KiB
Java
602 lines
20 KiB
Java
/*
|
|
* 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.IOException;
|
|
import java.io.InputStream;
|
|
import java.net.JarURLConnection;
|
|
import java.net.MalformedURLException;
|
|
import java.net.URL;
|
|
import java.net.URLConnection;
|
|
import java.net.URLStreamHandler;
|
|
import java.nio.charset.Charset;
|
|
import java.security.Permission;
|
|
import java.security.cert.Certificate;
|
|
import java.text.Collator;
|
|
import java.util.Arrays;
|
|
import java.util.Locale;
|
|
import java.util.jar.JarEntry;
|
|
import java.util.jar.JarFile;
|
|
import java.util.jar.Manifest;
|
|
|
|
import org.apache.catalina.WebResource;
|
|
import org.apache.catalina.WebResourceRoot;
|
|
import org.apache.juli.logging.Log;
|
|
import org.apache.juli.logging.LogFactory;
|
|
import org.apache.tomcat.util.res.StringManager;
|
|
|
|
/**
|
|
* 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 {
|
|
|
|
private static final Log log = LogFactory.getLog(CachedResource.class);
|
|
private static final StringManager sm = StringManager.getManager(CachedResource.class);
|
|
|
|
// 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() {
|
|
/*
|
|
* We don't want applications using this URL to access the resource
|
|
* directly as that could lead to inconsistent results when the resource
|
|
* is updated on the file system but the cache entry has not yet
|
|
* expired. We saw this, for example, in JSP compilation.
|
|
* - last modified time was obtained via
|
|
* ServletContext.getResource("path").openConnection().getLastModified()
|
|
* - JSP content was obtained via
|
|
* ServletContext.getResourceAsStream("path")
|
|
* The result was that the JSP modification was detected but the JSP
|
|
* content was read from the cache so the non-updated JSP page was
|
|
* used to generate the .java and .class file
|
|
*
|
|
* One option to resolve this issue is to use a custom URL scheme for
|
|
* resource URLs. This would allow us, via registration of a
|
|
* URLStreamHandlerFactory, to control how the resources are accessed
|
|
* and ensure that all access go via the cache We took this approach for
|
|
* war: URLs so we can use jar:war:file: URLs to reference resources in
|
|
* unpacked WAR files. However, because URL.setURLStreamHandlerFactory()
|
|
* may only be caused once, this can cause problems when using other
|
|
* libraries that also want to use a custom URL scheme.
|
|
*
|
|
* The approach below allows us to insert a custom URLStreamHandler
|
|
* without registering a custom protocol. The only limitation (compared
|
|
* to registering a custom protocol) is that if the application
|
|
* constructs the same URL from a String, they will access the resource
|
|
* directly and not via the cache.
|
|
*/
|
|
URL resourceURL = webResource.getURL();
|
|
if (resourceURL == null) {
|
|
return null;
|
|
}
|
|
try {
|
|
CachedResourceURLStreamHandler handler =
|
|
new CachedResourceURLStreamHandler(resourceURL, root, webAppPath, usesClassLoaderResources);
|
|
URL result = new URL(null, resourceURL.toExternalForm(), handler);
|
|
handler.setAssociatedURL(result);
|
|
return result;
|
|
} catch (MalformedURLException e) {
|
|
log.error(sm.getString("cachedResource.invalidURL", resourceURL.toExternalForm()), e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@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;
|
|
}
|
|
|
|
|
|
/*
|
|
* Mimics the behaviour of FileURLConnection.getInputStream for a directory.
|
|
* Deliberately uses default locale.
|
|
*/
|
|
private static InputStream buildInputStream(String[] files) {
|
|
Arrays.sort(files, Collator.getInstance(Locale.getDefault()));
|
|
StringBuilder result = new StringBuilder();
|
|
for (String file : files) {
|
|
result.append(file);
|
|
// Every entry is followed by \n including the last
|
|
result.append('\n');
|
|
}
|
|
return new ByteArrayInputStream(result.toString().getBytes(Charset.defaultCharset()));
|
|
}
|
|
|
|
|
|
private static class CachedResourceURLStreamHandler extends URLStreamHandler {
|
|
|
|
private final URL resourceURL;
|
|
private final StandardRoot root;
|
|
private final String webAppPath;
|
|
private final boolean usesClassLoaderResources;
|
|
|
|
private URL associatedURL = null;
|
|
|
|
public CachedResourceURLStreamHandler(URL resourceURL, StandardRoot root, String webAppPath,
|
|
boolean usesClassLoaderResources) {
|
|
this.resourceURL = resourceURL;
|
|
this.root = root;
|
|
this.webAppPath = webAppPath;
|
|
this.usesClassLoaderResources = usesClassLoaderResources;
|
|
}
|
|
|
|
protected void setAssociatedURL(URL associatedURL) {
|
|
this.associatedURL = associatedURL;
|
|
}
|
|
|
|
@Override
|
|
protected URLConnection openConnection(URL u) throws IOException {
|
|
// This deliberately uses ==. If u isn't the URL object this
|
|
// URLStreamHandler was constructed for we do not want to use this
|
|
// URLStreamHandler to create a connection.
|
|
if (associatedURL != null && u == associatedURL) {
|
|
if ("jar".equals(associatedURL.getProtocol())) {
|
|
return new CachedResourceJarURLConnection(resourceURL, root, webAppPath, usesClassLoaderResources);
|
|
} else {
|
|
return new CachedResourceURLConnection(resourceURL, root, webAppPath, usesClassLoaderResources);
|
|
}
|
|
} else {
|
|
// The stream handler has been inherited by a URL that was
|
|
// constructed from a cache URL. We need to break that link.
|
|
URL constructedURL = new URL(u.toExternalForm());
|
|
return constructedURL.openConnection();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Keep this in sync with CachedResourceJarURLConnection.
|
|
*/
|
|
private static class CachedResourceURLConnection extends URLConnection {
|
|
|
|
private final StandardRoot root;
|
|
private final String webAppPath;
|
|
private final boolean usesClassLoaderResources;
|
|
private final URL resourceURL;
|
|
|
|
protected CachedResourceURLConnection(URL resourceURL, StandardRoot root, String webAppPath,
|
|
boolean usesClassLoaderResources) {
|
|
super(resourceURL);
|
|
this.root = root;
|
|
this.webAppPath = webAppPath;
|
|
this.usesClassLoaderResources = usesClassLoaderResources;
|
|
this.resourceURL = resourceURL;
|
|
}
|
|
|
|
@Override
|
|
public void connect() throws IOException {
|
|
// NO-OP
|
|
}
|
|
|
|
@Override
|
|
public InputStream getInputStream() throws IOException {
|
|
WebResource resource = getResource();
|
|
if (resource.isDirectory()) {
|
|
return buildInputStream(resource.getWebResourceRoot().list(webAppPath));
|
|
} else {
|
|
return getResource().getInputStream();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Permission getPermission() throws IOException {
|
|
// Doesn't trigger a call to connect for file:// URLs
|
|
return resourceURL.openConnection().getPermission();
|
|
}
|
|
|
|
@Override
|
|
public long getLastModified() {
|
|
return getResource().getLastModified();
|
|
}
|
|
|
|
@Override
|
|
public long getContentLengthLong() {
|
|
return getResource().getContentLength();
|
|
}
|
|
|
|
private WebResource getResource() {
|
|
return root.getResource(webAppPath, false, usesClassLoaderResources);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Keep this in sync with CachedResourceURLConnection.
|
|
*/
|
|
private static class CachedResourceJarURLConnection extends JarURLConnection {
|
|
|
|
private final StandardRoot root;
|
|
private final String webAppPath;
|
|
private final boolean usesClassLoaderResources;
|
|
private final URL resourceURL;
|
|
|
|
protected CachedResourceJarURLConnection(URL resourceURL, StandardRoot root, String webAppPath,
|
|
boolean usesClassLoaderResources) throws IOException {
|
|
super(resourceURL);
|
|
this.root = root;
|
|
this.webAppPath = webAppPath;
|
|
this.usesClassLoaderResources = usesClassLoaderResources;
|
|
this.resourceURL = resourceURL;
|
|
}
|
|
|
|
@Override
|
|
public void connect() throws IOException {
|
|
// NO-OP
|
|
}
|
|
|
|
@Override
|
|
public InputStream getInputStream() throws IOException {
|
|
WebResource resource = getResource();
|
|
if (resource.isDirectory()) {
|
|
return buildInputStream(resource.getWebResourceRoot().list(webAppPath));
|
|
} else {
|
|
return getResource().getInputStream();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Permission getPermission() throws IOException {
|
|
// Doesn't trigger a call to connect for jar:// URLs
|
|
return resourceURL.openConnection().getPermission();
|
|
}
|
|
|
|
@Override
|
|
public long getLastModified() {
|
|
return getResource().getLastModified();
|
|
}
|
|
|
|
@Override
|
|
public long getContentLengthLong() {
|
|
return getResource().getContentLength();
|
|
}
|
|
|
|
private WebResource getResource() {
|
|
return root.getResource(webAppPath, false, usesClassLoaderResources);
|
|
}
|
|
|
|
@Override
|
|
public JarFile getJarFile() throws IOException {
|
|
return ((JarURLConnection) resourceURL.openConnection()).getJarFile();
|
|
}
|
|
|
|
@Override
|
|
public JarEntry getJarEntry() throws IOException {
|
|
if (getEntryName() == null) {
|
|
return null;
|
|
} else {
|
|
return super.getJarEntry();
|
|
}
|
|
}
|
|
}
|
|
}
|