diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/GzipHttpContent.java b/jetty-http/src/main/java/org/eclipse/jetty/http/GzipHttpContent.java new file mode 100644 index 000000000000..ff70f3777f68 --- /dev/null +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/GzipHttpContent.java @@ -0,0 +1,177 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import org.eclipse.jetty.http.MimeTypes.Type; + +import org.eclipse.jetty.util.resource.Resource; + +/* ------------------------------------------------------------ */ +public class GzipHttpContent implements HttpContent +{ + private final HttpContent _content; + private final HttpContent _contentGz; + public final static String ETAG_GZIP="--gzip"; + public final static PreEncodedHttpField CONTENT_ENCODING_GZIP=new PreEncodedHttpField(HttpHeader.CONTENT_ENCODING,"gzip"); + + public GzipHttpContent(HttpContent content, HttpContent contentGz) + { + _content=content; + _contentGz=contentGz; + } + + @Override + public int hashCode() + { + return _content.hashCode(); + } + + @Override + public boolean equals(Object obj) + { + return _content.equals(obj); + } + + @Override + public Resource getResource() + { + return _content.getResource(); + } + + @Override + public HttpField getETag() + { + return new HttpField(HttpHeader.ETAG,getETagValue()); + } + + @Override + public String getETagValue() + { + return _content.getResource().getWeakETag(ETAG_GZIP); + } + + @Override + public HttpField getLastModified() + { + return _content.getLastModified(); + } + + @Override + public String getLastModifiedValue() + { + return _content.getLastModifiedValue(); + } + + @Override + public HttpField getContentType() + { + return _content.getContentType(); + } + + @Override + public String getContentTypeValue() + { + return _content.getContentTypeValue(); + } + + @Override + public HttpField getContentEncoding() + { + return CONTENT_ENCODING_GZIP; + } + + @Override + public String getContentEncodingValue() + { + return CONTENT_ENCODING_GZIP.getValue(); + } + + @Override + public String getCharacterEncoding() + { + return _content.getCharacterEncoding(); + } + + @Override + public Type getMimeType() + { + return _content.getMimeType(); + } + + @Override + public void release() + { + _content.release(); + } + + @Override + public ByteBuffer getIndirectBuffer() + { + return _contentGz.getIndirectBuffer(); + } + + @Override + public ByteBuffer getDirectBuffer() + { + return _contentGz.getDirectBuffer(); + } + + @Override + public HttpField getContentLength() + { + return _contentGz.getContentLength(); + } + + @Override + public long getContentLengthValue() + { + return _contentGz.getContentLengthValue(); + } + + @Override + public InputStream getInputStream() throws IOException + { + return _contentGz.getInputStream(); + } + + @Override + public ReadableByteChannel getReadableByteChannel() throws IOException + { + return _contentGz.getReadableByteChannel(); + } + + @Override + public String toString() + { + return String.format("GzipHttpContent@%x{r=%s|%s,lm=%s|%s,ct=%s}",hashCode(), + _content.getResource(),_contentGz.getResource(), + _content.getResource().lastModified(),_contentGz.getResource().lastModified(), + getContentType()); + } + + @Override + public HttpContent getGzipContent() + { + return null; + } +} \ No newline at end of file diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java index bf4f4cbc1e79..7ae190aab215 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpContent.java @@ -44,6 +44,9 @@ public interface HttpContent String getCharacterEncoding(); Type getMimeType(); + HttpField getContentEncoding(); + String getContentEncodingValue(); + HttpField getContentLength(); long getContentLengthValue(); @@ -60,4 +63,11 @@ public interface HttpContent ReadableByteChannel getReadableByteChannel() throws IOException; void release(); + HttpContent getGzipContent(); + + + public interface Factory + { + HttpContent getContent(String path) throws IOException; + } } diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/ResourceHttpContent.java b/jetty-http/src/main/java/org/eclipse/jetty/http/ResourceHttpContent.java index b30bbcf813cc..30d7ba5c261a 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/ResourceHttpContent.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/ResourceHttpContent.java @@ -39,33 +39,28 @@ public class ResourceHttpContent implements HttpContent final Resource _resource; final String _contentType; final int _maxBuffer; - final String _etag; + HttpContent _gzip; + String _etag; /* ------------------------------------------------------------ */ public ResourceHttpContent(final Resource resource, final String contentType) { - this(resource,contentType,-1,false); + this(resource,contentType,-1,null); } /* ------------------------------------------------------------ */ public ResourceHttpContent(final Resource resource, final String contentType, int maxBuffer) { - this(resource,contentType,maxBuffer,false); + this(resource,contentType,maxBuffer,null); } - - /* ------------------------------------------------------------ */ - public ResourceHttpContent(final Resource resource, final String contentType, boolean etag) - { - this(resource,contentType,-1,etag); - } - + /* ------------------------------------------------------------ */ - public ResourceHttpContent(final Resource resource, final String contentType, int maxBuffer, boolean etag) + public ResourceHttpContent(final Resource resource, final String contentType, int maxBuffer, HttpContent gzip) { _resource=resource; _contentType=contentType; _maxBuffer=maxBuffer; - _etag=etag?resource.getWeakETag():null; + _gzip=gzip; } /* ------------------------------------------------------------ */ @@ -82,6 +77,20 @@ public HttpField getContentType() return _contentType==null?null:new HttpField(HttpHeader.CONTENT_TYPE,_contentType); } + /* ------------------------------------------------------------ */ + @Override + public HttpField getContentEncoding() + { + return null; + } + + /* ------------------------------------------------------------ */ + @Override + public String getContentEncodingValue() + { + return null; + } + /* ------------------------------------------------------------ */ @Override public String getCharacterEncoding() @@ -132,14 +141,14 @@ public ByteBuffer getDirectBuffer() @Override public HttpField getETag() { - return _etag==null?null:new HttpField(HttpHeader.ETAG,_etag); + return new HttpField(HttpHeader.ETAG,getETagValue()); } /* ------------------------------------------------------------ */ @Override public String getETagValue() { - return _etag; + return _resource.getWeakETag(); } /* ------------------------------------------------------------ */ @@ -205,6 +214,14 @@ public void release() @Override public String toString() { - return String.format("%s@%x{r=%s}",this.getClass().getSimpleName(),hashCode(),_resource); + return String.format("%s@%x{r=%s,gz=%b}",this.getClass().getSimpleName(),hashCode(),_resource,_gzip!=null); } + + /* ------------------------------------------------------------ */ + @Override + public HttpContent getGzipContent() + { + return _gzip==null?null:new GzipHttpContent(this,_gzip); + } + } \ No newline at end of file diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java index b9e73bdd3f5f..a209bf109fba 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceCache.java @@ -32,6 +32,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.eclipse.jetty.http.DateGenerator; +import org.eclipse.jetty.http.GzipHttpContent; import org.eclipse.jetty.http.HttpContent; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; @@ -45,8 +46,8 @@ import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceFactory; - -public class ResourceCache +// TODO rename to ContentCache +public class ResourceCache implements HttpContent.Factory { private static final Logger LOG = Log.getLogger(ResourceCache.class); @@ -56,7 +57,8 @@ public class ResourceCache private final ResourceFactory _factory; private final ResourceCache _parent; private final MimeTypes _mimeTypes; - private final boolean _etagSupported; + private final boolean _etags; + private final boolean _gzip; private final boolean _useFileMappedBuffer; private int _maxCachedFileSize =128*1024*1024; @@ -70,8 +72,9 @@ public class ResourceCache * @param mimeTypes Mimetype to use for meta data * @param useFileMappedBuffer true to file memory mapped buffers * @param etags true to support etags + * @param gzip true to support gzip */ - public ResourceCache(ResourceCache parent, ResourceFactory factory, MimeTypes mimeTypes,boolean useFileMappedBuffer,boolean etags) + public ResourceCache(ResourceCache parent, ResourceFactory factory, MimeTypes mimeTypes,boolean useFileMappedBuffer,boolean etags,boolean gzip) { _factory = factory; _cache=new ConcurrentHashMap(); @@ -80,7 +83,8 @@ public ResourceCache(ResourceCache parent, ResourceFactory factory, MimeTypes mi _mimeTypes=mimeTypes; _parent=parent; _useFileMappedBuffer=useFileMappedBuffer; - _etagSupported=etags; + _etags=etags; + _gzip=gzip; } /* ------------------------------------------------------------ */ @@ -163,6 +167,14 @@ public void flushCache() } } + /* ------------------------------------------------------------ */ + @Deprecated + public HttpContent lookup(String pathInContext) + throws IOException + { + return getContent(pathInContext); + } + /* ------------------------------------------------------------ */ /** Get a Entry from the cache. * Get either a valid entry object or create a new one if possible. @@ -174,7 +186,8 @@ public void flushCache() * the resource does not exist, then null is returned. * @throws IOException Problem loading the resource */ - public HttpContent lookup(String pathInContext) + @Override + public HttpContent getContent(String pathInContext) throws IOException { // Is the content in this cache? @@ -206,6 +219,9 @@ public HttpContent lookup(String pathInContext) */ protected boolean isCacheable(Resource resource) { + if (_maxCachedFiles<=0) + return false; + long len = resource.length(); // Will it fit in the cache? @@ -216,16 +232,43 @@ protected boolean isCacheable(Resource resource) private HttpContent load(String pathInContext, Resource resource) throws IOException { - CachedHttpContent content=null; if (resource==null || !resource.exists()) return null; + if (resource.isDirectory()) + return new ResourceHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),getMaxCachedFileSize()); + // Will it fit in the cache? - if (!resource.isDirectory() && isCacheable(resource)) + if (isCacheable(resource)) { - // Create the Content (to increment the cache sizes before adding the content - content = new CachedHttpContent(pathInContext,resource); + CachedHttpContent content=null; + + // Look for a gzip resource + if (_gzip) + { + String pathInContextGz=pathInContext+".gz"; + CachedHttpContent contentGz = _cache.get(pathInContextGz); + if (contentGz==null || !contentGz.isValid()) + { + contentGz=null; + Resource resourceGz=_factory.getResource(pathInContextGz); + if (resourceGz.exists() && resourceGz.lastModified()>=resource.lastModified() && resourceGz.length()=resource.lastModified()) + return new ResourceHttpContent(resource,mt,getMaxCachedFileSize(),contentGz); + + // Is there a gzip resource? + Resource resourceGz=_factory.getResource(pathInContextGz); + if (resourceGz.exists() && resourceGz.lastModified()>=resource.lastModified() && resourceGz.length() _indirectBuffer=new AtomicReference(); AtomicReference _directBuffer=new AtomicReference(); /* ------------------------------------------------------------ */ - CachedHttpContent(String pathInContext,Resource resource) + CachedHttpContent(String pathInContext,Resource resource,CachedHttpContent gzipped) { _key=pathInContext; _resource=resource; @@ -365,9 +425,11 @@ public class CachedHttpContent implements HttpContent _cachedFiles.incrementAndGet(); _lastAccessed=System.currentTimeMillis(); - _etag=ResourceCache.this._etagSupported?new PreEncodedHttpField(HttpHeader.ETAG,resource.getWeakETag()):null; + _etag=ResourceCache.this._etags?new PreEncodedHttpField(HttpHeader.ETAG,resource.getWeakETag()):null; + + _gzipped=gzipped==null?null:new CachedGzipHttpContent(this,gzipped); } - + /* ------------------------------------------------------------ */ public String getKey() @@ -428,7 +490,7 @@ protected void invalidate() // Invalidate it _cachedSize.addAndGet(-_contentLengthValue); _cachedFiles.decrementAndGet(); - _resource.close(); + _resource.close(); } /* ------------------------------------------------------------ */ @@ -459,6 +521,20 @@ public String getContentTypeValue() { return _contentType==null?null:_contentType.getValue(); } + + /* ------------------------------------------------------------ */ + @Override + public HttpField getContentEncoding() + { + return null; + } + + /* ------------------------------------------------------------ */ + @Override + public String getContentEncodingValue() + { + return null; + } /* ------------------------------------------------------------ */ @Override @@ -479,7 +555,6 @@ public Type getMimeType() @Override public void release() { - // don't release while cached. Release when invalidated. } /* ------------------------------------------------------------ */ @@ -557,12 +632,65 @@ public ReadableByteChannel getReadableByteChannel() throws IOException return _resource.getReadableByteChannel(); } + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + return String.format("CachedContent@%x{r=%s,e=%b,lm=%s,ct=%s,gz=%b}",hashCode(),_resource,_resource.exists(),_lastModified,_contentType,_gzipped!=null); + } /* ------------------------------------------------------------ */ + @Override + public HttpContent getGzipContent() + { + return (_gzipped!=null && _gzipped.isValid())?_gzipped:null; + } + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + public class CachedGzipHttpContent extends GzipHttpContent + { + private final CachedHttpContent _content; + private final CachedHttpContent _contentGz; + private final HttpField _etag; + + CachedGzipHttpContent(CachedHttpContent content, CachedHttpContent contentGz) + { + super(content,contentGz); + _content=content; + _contentGz=contentGz; + + _etag=(ResourceCache.this._etags)?new PreEncodedHttpField(HttpHeader.ETAG,_content.getResource().getWeakETag("--gzip")):null; + } + + public boolean isValid() + { + return _contentGz.isValid() && _content.isValid() && _content.getResource().lastModified() <= _contentGz.getResource().lastModified(); + } + + @Override + public HttpField getETag() + { + if (_etag!=null) + return _etag; + return super.getETag(); + } + + @Override + public String getETagValue() + { + if (_etag!=null) + return _etag.getValue(); + return super.getETagValue(); + } + @Override public String toString() { - return String.format("CachedContent@%x{r=%s,e=%b,lm=%s,ct=%s}",hashCode(),_resource,_resource.exists(),_lastModified,_contentType); - } + return "Cached"+super.toString(); + } } + } diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java new file mode 100644 index 000000000000..2e0edde6730c --- /dev/null +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java @@ -0,0 +1,104 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.server; + +import java.io.IOException; + +import org.eclipse.jetty.http.HttpContent; +import org.eclipse.jetty.http.HttpContent.Factory; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.http.ResourceHttpContent; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceFactory; + +public class ResourceContentFactory implements Factory +{ + private final ResourceFactory _factory; + private final MimeTypes _mimeTypes; + private final int _maxBufferSize; + private final boolean _gzip; + + + /* ------------------------------------------------------------ */ + public ResourceContentFactory(ResourceFactory factory, MimeTypes mimeTypes, int maxBufferSize, boolean gzip) + { + _factory=factory; + _mimeTypes=mimeTypes; + _maxBufferSize=maxBufferSize; + _gzip=gzip; + } + + /* ------------------------------------------------------------ */ + /** Get a Entry from the cache. + * Get either a valid entry object or create a new one if possible. + * + * @param pathInContext The key into the cache + * @return The entry matching pathInContext, or a new entry + * if no matching entry was found. If the content exists but is not cachable, + * then a {@link ResourceHttpContent} instance is return. If + * the resource does not exist, then null is returned. + * @throws IOException Problem loading the resource + */ + @Override + public HttpContent getContent(String pathInContext) + throws IOException + { + + // try loading the content from our factory. + Resource resource=_factory.getResource(pathInContext); + HttpContent loaded = load(pathInContext,resource); + return loaded; + } + + + /* ------------------------------------------------------------ */ + private HttpContent load(String pathInContext, Resource resource) + throws IOException + { + if (resource==null || !resource.exists()) + return null; + + if (resource.isDirectory()) + return new ResourceHttpContent(resource,_mimeTypes.getMimeByExtension(resource.toString()),_maxBufferSize); + + // Look for a gzip resource or content + String mt = _mimeTypes.getMimeByExtension(pathInContext); + if (_gzip) + { + // Is there a gzip resource? + String pathInContextGz=pathInContext+".gz"; + Resource resourceGz=_factory.getResource(pathInContextGz); + if (resourceGz.exists() && resourceGz.lastModified()>=resource.lastModified() && resourceGz.length()0) _mimeType=content.getMimeType(); } + HttpField ce=content.getContentEncoding(); + if (ce!=null) + _fields.put(ce); + if (etag) { HttpField et = content.getETag(); @@ -1362,7 +1365,11 @@ public static void putHeaders(HttpServletResponse response, HttpContent content, String ct=content.getContentTypeValue(); if (ct!=null && response.getContentType()==null) - response.setContentType(content.getContentTypeValue()); + response.setContentType(ct); + + String ce=content.getContentEncodingValue(); + if (ce!=null) + response.setHeader(HttpHeader.CONTENT_ENCODING.asString(),ce); if (etag) { diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java index ba0d6d05a0b2..1c0198bd19c1 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHandler.java @@ -28,6 +28,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.http.GzipHttpContent; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.http.HttpMethod; @@ -61,10 +62,8 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory public final static String GZIP = "gzip"; public final static String DEFLATE = "deflate"; - public final static String ETAG_GZIP="--gzip"; public final static String ETAG = "o.e.j.s.Gzip.ETag"; public final static int DEFAULT_MIN_GZIP_SIZE=16; - private int _minGzipSize=DEFAULT_MIN_GZIP_SIZE; private int _compressionLevel=Deflater.DEFAULT_COMPRESSION; private boolean _checkGzExists = true; @@ -79,6 +78,7 @@ public class GzipHandler extends HandlerWrapper implements GzipFactory private HttpField _vary; + /* ------------------------------------------------------------ */ /** * Instantiates a new gzip handler. @@ -418,8 +418,8 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques String etag = request.getHeader("If-None-Match"); if (etag!=null) { - if (etag.contains(ETAG_GZIP)) - request.setAttribute(ETAG,etag.replace(ETAG_GZIP,"")); + if (etag.contains(GzipHttpContent.ETAG_GZIP)) + request.setAttribute(ETAG,etag.replace(GzipHttpContent.ETAG_GZIP,"")); } // install interceptor and handle diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java index 41ba3b9c65cf..0aa28d786b7f 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/handler/gzip/GzipHttpOutputInterceptor.java @@ -24,6 +24,7 @@ import java.util.zip.CRC32; import java.util.zip.Deflater; +import org.eclipse.jetty.http.GzipHttpContent; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeader; @@ -41,7 +42,6 @@ public class GzipHttpOutputInterceptor implements HttpOutput.Interceptor { public static Logger LOG = Log.getLogger(GzipHttpOutputInterceptor.class); - private final static PreEncodedHttpField CONTENT_ENCODING_GZIP=new PreEncodedHttpField(HttpHeader.CONTENT_ENCODING,"gzip"); private final static byte[] GZIP_HEADER = new byte[] { (byte)0x1f, (byte)0x8b, Deflater.DEFLATED, 0, 0, 0, 0, 0, 0, 0 }; public final static HttpField VARY_ACCEPT_ENCODING_USER_AGENT=new PreEncodedHttpField(HttpHeader.VARY,HttpHeader.ACCEPT_ENCODING+", "+HttpHeader.USER_AGENT); @@ -202,7 +202,7 @@ protected void commit(ByteBuffer content, boolean complete, Callback callback) return; } - fields.put(CONTENT_ENCODING_GZIP); + fields.put(GzipHttpContent.CONTENT_ENCODING_GZIP); _crc.reset(); _buffer=_channel.getByteBufferPool().acquire(_bufferSize,false); BufferUtil.fill(_buffer,GZIP_HEADER,0,GZIP_HEADER.length); @@ -213,7 +213,7 @@ protected void commit(ByteBuffer content, boolean complete, Callback callback) if (etag!=null) { int end = etag.length()-1; - etag=(etag.charAt(end)=='"')?etag.substring(0,end)+GzipHandler.ETAG_GZIP+'"':etag+GzipHandler.ETAG_GZIP; + etag=(etag.charAt(end)=='"')?etag.substring(0,end)+GzipHttpContent.ETAG_GZIP+'"':etag+GzipHttpContent.ETAG_GZIP; fields.put(HttpHeader.ETAG,etag); } diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/ResourceCacheTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/ResourceCacheTest.java index a21e45d7732a..67c6e2447f57 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/ResourceCacheTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/ResourceCacheTest.java @@ -49,9 +49,9 @@ public void testMutlipleSources1() throws Exception Resource[] r = rc.getResources(); MimeTypes mime = new MimeTypes(); - ResourceCache rc3 = new ResourceCache(null,r[2],mime,false,false); - ResourceCache rc2 = new ResourceCache(rc3,r[1],mime,false,false); - ResourceCache rc1 = new ResourceCache(rc2,r[0],mime,false,false); + ResourceCache rc3 = new ResourceCache(null,r[2],mime,false,false,false); + ResourceCache rc2 = new ResourceCache(rc3,r[1],mime,false,false,false); + ResourceCache rc1 = new ResourceCache(rc2,r[0],mime,false,false,false); assertEquals("1 - one", getContent(rc1, "1.txt")); assertEquals("2 - two", getContent(rc1, "2.txt")); @@ -79,8 +79,8 @@ public void testUncacheable() throws Exception Resource[] r = rc.getResources(); MimeTypes mime = new MimeTypes(); - ResourceCache rc3 = new ResourceCache(null,r[2],mime,false,false); - ResourceCache rc2 = new ResourceCache(rc3,r[1],mime,false,false) + ResourceCache rc3 = new ResourceCache(null,r[2],mime,false,false,false); + ResourceCache rc2 = new ResourceCache(rc3,r[1],mime,false,false,false) { @Override public boolean isCacheable(Resource resource) @@ -89,7 +89,7 @@ public boolean isCacheable(Resource resource) } }; - ResourceCache rc1 = new ResourceCache(rc2,r[0],mime,false,false); + ResourceCache rc1 = new ResourceCache(rc2,r[0],mime,false,false,false); assertEquals("1 - one", getContent(rc1, "1.txt")); assertEquals("2 - two", getContent(rc1, "2.txt")); @@ -130,7 +130,7 @@ public void testResourceCache() throws Exception directory=Resource.newResource(files[0].getParentFile().getAbsolutePath()); - cache=new ResourceCache(null,directory,new MimeTypes(),false,false); + cache=new ResourceCache(null,directory,new MimeTypes(),false,false,false); cache.setMaxCacheSize(95); cache.setMaxCachedFileSize(85); @@ -243,7 +243,7 @@ public void testNoextension() throws Exception Resource[] resources = rc.getResources(); MimeTypes mime = new MimeTypes(); - ResourceCache cache = new ResourceCache(null,resources[0],mime,false,false); + ResourceCache cache = new ResourceCache(null,resources[0],mime,false,false,false); assertEquals("4 - four", getContent(cache, "four.txt")); assertEquals("4 - four (no extension)", getContent(cache, "four")); diff --git a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java index 1942359252cf..8041eb0687c4 100644 --- a/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java +++ b/jetty-servlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java @@ -54,6 +54,7 @@ import org.eclipse.jetty.server.InclusiveByteRange; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.ResourceCache; +import org.eclipse.jetty.server.ResourceContentFactory; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.BufferUtil; @@ -161,6 +162,7 @@ public class DefaultServlet extends HttpServlet implements ResourceFactory private Resource _resourceBase; private ResourceCache _cache; + private HttpContent.Factory _contentFactory; private MimeTypes _mimeTypes; private String[] _welcomes; @@ -243,7 +245,7 @@ public void init() String cc=getInitParameter("cacheControl"); if (cc!=null) _cacheControl=new PreEncodedHttpField(HttpHeader.CACHE_CONTROL, cc); - + String resourceCache = getInitParameter("resourceCache"); int max_cache_size=getInitInt("maxCacheSize", -2); int max_cached_file_size=getInitInt("maxCachedFileSize", -2); @@ -257,23 +259,23 @@ public void init() _cache=(ResourceCache)_servletContext.getAttribute(resourceCache); if (LOG.isDebugEnabled()) - LOG.debug("Cache {}={}",resourceCache,_cache); + LOG.debug("Cache {}={}",resourceCache,_contentFactory); } _etags = getInitBoolean("etags",_etags); - + try { if (_cache==null && (max_cached_files!=-2 || max_cache_size!=-2 || max_cached_file_size!=-2)) { - _cache= new ResourceCache(null,this,_mimeTypes,_useFileMappedBuffer,_etags); - + _cache = new ResourceCache(null,this,_mimeTypes,_useFileMappedBuffer,_etags,_gzip); if (max_cache_size>=0) _cache.setMaxCacheSize(max_cache_size); if (max_cached_file_size>=-1) _cache.setMaxCachedFileSize(max_cached_file_size); if (max_cached_files>=-1) _cache.setMaxCachedFiles(max_cached_files); + _servletContext.setAttribute(resourceCache==null?"resourceCache":resourceCache,_cache); } } catch (Exception e) @@ -281,33 +283,34 @@ public void init() LOG.warn(Log.EXCEPTION,e); throw new UnavailableException(e.toString()); } - - _gzipEquivalentFileExtensions = new ArrayList(); - String otherGzipExtensions = getInitParameter("otherGzipFileExtensions"); - if (otherGzipExtensions != null) - { - //comma separated list - StringTokenizer tok = new StringTokenizer(otherGzipExtensions,",",false); - while (tok.hasMoreTokens()) - { - String s = tok.nextToken().trim(); - _gzipEquivalentFileExtensions.add((s.charAt(0)=='.'?s:"."+s)); - } - } - else - { - //.svgz files are gzipped svg files and must be served with Content-Encoding:gzip - _gzipEquivalentFileExtensions.add(".svgz"); - } - - _servletHandler= _contextHandler.getChildHandlerByClass(ServletHandler.class); - for (ServletHolder h :_servletHandler.getServlets()) - if (h.getServletInstance()==this) - _defaultHolder=h; - - - if (LOG.isDebugEnabled()) - LOG.debug("resource base = "+_resourceBase); + + _contentFactory=_cache==null?new ResourceContentFactory(this,_mimeTypes,-1,_gzip):_cache; // TODO pass a buffer size + + _gzipEquivalentFileExtensions = new ArrayList(); + String otherGzipExtensions = getInitParameter("otherGzipFileExtensions"); + if (otherGzipExtensions != null) + { + //comma separated list + StringTokenizer tok = new StringTokenizer(otherGzipExtensions,",",false); + while (tok.hasMoreTokens()) + { + String s = tok.nextToken().trim(); + _gzipEquivalentFileExtensions.add((s.charAt(0)=='.'?s:"."+s)); + } + } + else + { + //.svgz files are gzipped svg files and must be served with Content-Encoding:gzip + _gzipEquivalentFileExtensions.add(".svgz"); + } + + _servletHandler= _contextHandler.getChildHandlerByClass(ServletHandler.class); + for (ServletHolder h :_servletHandler.getServlets()) + if (h.getServletInstance()==this) + _defaultHolder=h; + + if (LOG.isDebugEnabled()) + LOG.debug("resource base = "+_resourceBase); } /** @@ -422,8 +425,8 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) String servletPath=null; String pathInfo=null; Enumeration reqRanges = null; - Boolean included =request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI)!=null; - if (included!=null && included.booleanValue()) + boolean included =request.getAttribute(RequestDispatcher.INCLUDE_REQUEST_URI)!=null; + if (included) { servletPath=(String)request.getAttribute(RequestDispatcher.INCLUDE_SERVLET_PATH); pathInfo=(String)request.getAttribute(RequestDispatcher.INCLUDE_PATH_INFO); @@ -435,7 +438,6 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) } else { - included = Boolean.FALSE; servletPath = _pathInfoOnly?"/":request.getServletPath(); pathInfo = request.getPathInfo(); @@ -447,155 +449,72 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) String pathInContext=URIUtil.addPaths(servletPath,pathInfo); boolean endsWithSlash=(pathInfo==null?request.getServletPath():pathInfo).endsWith(URIUtil.SLASH); - - - // Find the resource and content - Resource resource=null; + boolean gzippable=_gzip && !endsWithSlash && !included && reqRanges==null; + HttpContent content=null; - boolean close_content=true; + boolean release_content=true; try { - // is gzip enabled? - String pathInContextGz=null; - boolean gzip=false; - if (!included.booleanValue() && _gzip && reqRanges==null && !endsWithSlash ) - { - // Look for a gzip resource - pathInContextGz=pathInContext+".gz"; - if (_cache==null) - resource=getResource(pathInContextGz); - else - { - content=_cache.lookup(pathInContextGz); - resource=(content==null)?null:content.getResource(); - } - - // Does a gzip resource exist? - if (resource!=null && resource.exists() && !resource.isDirectory()) - { - // Tell caches that response may vary by accept-encoding - response.addHeader(HttpHeader.VARY.asString(),HttpHeader.ACCEPT_ENCODING.asString()); - - // Does the client accept gzip? - String accept=request.getHeader(HttpHeader.ACCEPT_ENCODING.asString()); - if (accept!=null && accept.indexOf("gzip")>=0) - gzip=true; - } - } - - // find resource - if (!gzip) - { - if (_cache==null) - resource=getResource(pathInContext); - else - { - content=_cache.lookup(pathInContext); - resource=content==null?null:content.getResource(); - } - } - + // Find the content + content=_contentFactory.getContent(pathInContext); if (LOG.isDebugEnabled()) - LOG.debug(String.format("uri=%s, resource=%s, content=%s",request.getRequestURI(),resource,content)); - - // Handle resource - if (resource==null || !resource.exists()) + LOG.info("content={}",content); + + // Not found? + if (content==null || !content.getResource().exists()) { if (included) throw new FileNotFoundException("!" + pathInContext); response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; } - else if (!resource.isDirectory()) + + // Directory? + if (content.getResource().isDirectory()) { - if (endsWithSlash && pathInContext.length()>1) - { - String q=request.getQueryString(); - pathInContext=pathInContext.substring(0,pathInContext.length()-1); - if (q!=null&&q.length()!=0) - pathInContext+="?"+q; - response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(),pathInContext))); - } - else - { - // ensure we have content - if (content==null) - content=new ResourceHttpContent(resource,_mimeTypes.getMimeByExtension(pathInContext),response.getBufferSize(),_etags); - - if (included.booleanValue() || passConditionalHeaders(request,response, resource,content)) - { - if (gzip || isGzippedContent(pathInContext)) - { - response.setHeader(HttpHeader.CONTENT_ENCODING.asString(),"gzip"); - String mt=_servletContext.getMimeType(pathInContext); - if (mt!=null) - response.setContentType(mt); - } - close_content=sendData(request,response,included.booleanValue(),resource,content,reqRanges); - } - } + sendWelcome(content,pathInContext,endsWithSlash,included,request,response); + return; } - else + + // Strip slash? + if (endsWithSlash && pathInContext.length()>1) { - String welcome=null; - - if (!endsWithSlash || (pathInContext.length()==1 && request.getAttribute("org.eclipse.jetty.server.nullPathInfo")!=null)) - { - StringBuffer buf=request.getRequestURL(); - synchronized(buf) - { - int param=buf.lastIndexOf(";"); - if (param<0) - buf.append('/'); - else - buf.insert(param,'/'); - String q=request.getQueryString(); - if (q!=null&&q.length()!=0) - { - buf.append('?'); - buf.append(q); - } - response.setContentLength(0); - response.sendRedirect(response.encodeRedirectURL(buf.toString())); - } - } - // else look for a welcome file - else if (null!=(welcome=getWelcomeFile(pathInContext))) + String q=request.getQueryString(); + pathInContext=pathInContext.substring(0,pathInContext.length()-1); + if (q!=null&&q.length()!=0) + pathInContext+="?"+q; + response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths(_servletContext.getContextPath(),pathInContext))); + return; + } + + // Conditional response? + if (!included && !passConditionalHeaders(request,response,content)) + return; + + // Gzip? + HttpContent gzip_content = gzippable?content.getGzipContent():null; + if (gzip_content!=null) + { + // Tell caches that response may vary by accept-encoding + response.addHeader(HttpHeader.VARY.asString(),HttpHeader.ACCEPT_ENCODING.asString()); + + // Does the client accept gzip? + String accept=request.getHeader(HttpHeader.ACCEPT_ENCODING.asString()); + if (accept!=null && accept.indexOf("gzip")>=0) { if (LOG.isDebugEnabled()) - LOG.debug("welcome={}",welcome); - if (_redirectWelcome) - { - // Redirect to the index - response.setContentLength(0); - String q=request.getQueryString(); - if (q!=null&&q.length()!=0) - response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)+"?"+q)); - else - response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome))); - } - else - { - // Forward to the index - RequestDispatcher dispatcher=request.getRequestDispatcher(welcome); - if (dispatcher!=null) - { - if (included.booleanValue()) - dispatcher.include(request,response); - else - { - request.setAttribute("org.eclipse.jetty.server.welcome",welcome); - dispatcher.forward(request,response); - } - } - } - } - else - { - content=new ResourceHttpContent(resource,_mimeTypes.getMimeByExtension(pathInContext),_etags); - if (included.booleanValue() || passConditionalHeaders(request,response, resource,content)) - sendDirectory(request,response,resource,pathInContext); + LOG.debug("gzip={}",gzip_content); + content=gzip_content; } } + + // TODO this should be done by HttpContent#getContentEncoding + if (isGzippedContent(pathInContext)) + response.setHeader(HttpHeader.CONTENT_ENCODING.asString(),"gzip"); + + // Send the data + release_content=sendData(request,response,included,content,reqRanges); + } catch(IllegalArgumentException e) { @@ -605,17 +524,80 @@ else if (null!=(welcome=getWelcomeFile(pathInContext))) } finally { - if (close_content) + if (release_content) { if (content!=null) content.release(); - else if (resource!=null) - resource.close(); } } } + protected void sendWelcome(HttpContent content, String pathInContext, boolean endsWithSlash, boolean included, HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException + { + // Redirect to directory + if (!endsWithSlash || (pathInContext.length()==1 && request.getAttribute("org.eclipse.jetty.server.nullPathInfo")!=null)) + { + StringBuffer buf=request.getRequestURL(); + synchronized(buf) + { + int param=buf.lastIndexOf(";"); + if (param<0) + buf.append('/'); + else + buf.insert(param,'/'); + String q=request.getQueryString(); + if (q!=null&&q.length()!=0) + { + buf.append('?'); + buf.append(q); + } + response.setContentLength(0); + response.sendRedirect(response.encodeRedirectURL(buf.toString())); + } + return; + } + + // look for a welcome file + String welcome=getWelcomeFile(pathInContext); + if (welcome!=null) + { + if (LOG.isDebugEnabled()) + LOG.debug("welcome={}",welcome); + if (_redirectWelcome) + { + // Redirect to the index + response.setContentLength(0); + String q=request.getQueryString(); + if (q!=null&&q.length()!=0) + response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome)+"?"+q)); + else + response.sendRedirect(response.encodeRedirectURL(URIUtil.addPaths( _servletContext.getContextPath(),welcome))); + } + else + { + // Forward to the index + RequestDispatcher dispatcher=request.getRequestDispatcher(welcome); + if (dispatcher!=null) + { + if (included) + dispatcher.include(request,response); + else + { + request.setAttribute("org.eclipse.jetty.server.welcome",welcome); + dispatcher.forward(request,response); + } + } + } + return; + } + + if (included || passConditionalHeaders(request,response, content)) + sendDirectory(request,response,content.getResource(),pathInContext); + } + + /* ------------------------------------------------------------ */ protected boolean isGzippedContent(String path) { if (path == null) return false; @@ -699,7 +681,7 @@ private String getWelcomeFile(String pathInContext) throws MalformedURLException /* ------------------------------------------------------------ */ /* Check modification date headers. */ - protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletResponse response, Resource resource, HttpContent content) + protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletResponse response, HttpContent content) throws IOException { try @@ -820,7 +802,7 @@ protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletR } long ifmsl=request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.asString()); - if (ifmsl!=-1 && resource.lastModified()/1000 <= ifmsl/1000) + if (ifmsl!=-1 && content.getResource().lastModified()/1000 <= ifmsl/1000) { response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); if (_etags) @@ -831,7 +813,7 @@ protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletR } // Parse the if[un]modified dates and compare to resource - if (ifums!=-1 && resource.lastModified()/1000 > ifums/1000) + if (ifums!=-1 && content.getResource().lastModified()/1000 > ifums/1000) { response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); return false; @@ -894,12 +876,11 @@ else if (_contextHandler.getBaseResource() instanceof ResourceCollection) protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, - Resource resource, final HttpContent content, Enumeration reqRanges) throws IOException { - final long content_length = (content==null)?resource.length():content.getContentLengthValue(); + final long content_length = content.getContentLengthValue(); // Get the output stream (or writer) OutputStream out =null; @@ -908,7 +889,7 @@ protected boolean sendData(HttpServletRequest request, { out = response.getOutputStream(); - // has a filter already written to the response? + // has something already written to the response? written = out instanceof HttpOutput ? ((HttpOutput)out).isWritten() : true; @@ -928,18 +909,18 @@ protected boolean sendData(HttpServletRequest request, if (include) { // write without headers - resource.writeTo(out,0,content_length); + content.getResource().writeTo(out,0,content_length); } // else if we can't do a bypass write because of wrapping - else if (content==null || written || !(out instanceof HttpOutput)) + else if (written || !(out instanceof HttpOutput)) { // write normally putHeaders(response,content,written?-1:0); - ByteBuffer buffer = (content==null)?null:content.getIndirectBuffer(); + ByteBuffer buffer = content.getIndirectBuffer(); if (buffer!=null) BufferUtil.writeTo(buffer,out); else - resource.writeTo(out,0,content_length); + content.getResource().writeTo(out,0,content_length); } // else do a bypass write else @@ -983,7 +964,6 @@ public String toString() } // otherwise write content blocking ((HttpOutput)out).sendContent(content); - } } else @@ -998,7 +978,7 @@ public String toString() response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); response.setHeader(HttpHeader.CONTENT_RANGE.asString(), InclusiveByteRange.to416HeaderRangeString(content_length)); - resource.writeTo(out,0,content_length); + content.getResource().writeTo(out,0,content_length); return true; } @@ -1014,7 +994,7 @@ public String toString() response.addDateHeader(HttpHeader.DATE.asString(),System.currentTimeMillis()); response.setHeader(HttpHeader.CONTENT_RANGE.asString(), singleSatisfiableRange.toHeaderRangeString(content_length)); - resource.writeTo(out,singleSatisfiableRange.getFirst(content_length),singleLength); + content.getResource().writeTo(out,singleSatisfiableRange.getFirst(content_length),singleLength); return true; } @@ -1041,7 +1021,7 @@ public String toString() ctp = "multipart/byteranges; boundary="; response.setContentType(ctp+multi.getBoundary()); - InputStream in=resource.getInputStream(); + InputStream in=content.getResource().getInputStream(); long pos=0; // calculate the content-length @@ -1075,7 +1055,7 @@ public String toString() if (start