You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by jk...@apache.org on 2006/12/04 01:16:21 UTC

svn commit: r481996 - in /tapestry/tapestry4/trunk/tapestry-framework/src: descriptor/META-INF/ java/org/apache/tapestry/asset/ java/org/apache/tapestry/services/impl/ java/org/apache/tapestry/util/io/ test/org/apache/tapestry/asset/ test/org/apache/ta...

Author: jkuhnert
Date: Sun Dec  3 16:16:20 2006
New Revision: 481996

URL: http://svn.apache.org/viewvc?view=rev&rev=481996
Log:
Made a few more improvements to the gzip compression && asset caching related portions of the system. 

-) All of the correct logic needed to determine browser vendor/version is implemented to prevent issues from older 
IE releases with gzip content. 

-) Static assets now completely work off of the actual last modification time of the asset in question so 
that assets are properly cached even in development. 

-) Static asset content in raw byte or gzip form is now also cached in memory to prevent extraneous processing 
of resources for each request.

-) AJAX and JSON responses now appropriately compress their output when possible using the same shared 
logic that AssetService uses via GzipUtil.

-) Correctly implementing ~all~ caching header logic needed across the spectrum of options. This area will 
probably undergoe more refactoring as the types/specifics will need to be configurable on a finer grained basis. Either
way, implementing the right caching headers alone has made a huge difference.

Needless to say, this stuff is fast. At least compared to what was happening before. So very much faster. Almost 
enough to be shockingly fast. 200k dojo file pre-compressed to 50k and properly cached makes a difference.

Added:
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/asset/CachedAsset.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/util/io/GzipUtil.java
    tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/asset/TestAssetService.java
    tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/util/io/TestGzipUtil.java
Modified:
    tapestry/tapestry4/trunk/tapestry-framework/src/descriptor/META-INF/tapestry.services.xml
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/asset/AssetService.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/DojoAjaxResponseBuilder.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/DojoAjaxResponseContributorImpl.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/JSONResponseBuilder.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/JSONResponseContributorImpl.java
    tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/services/impl/DojoAjaxResponseBuilderTest.java
    tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/services/impl/TestDisableCachingFilter.java

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/descriptor/META-INF/tapestry.services.xml
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/descriptor/META-INF/tapestry.services.xml?view=diff&rev=481996&r1=481995&r2=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/descriptor/META-INF/tapestry.services.xml (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/descriptor/META-INF/tapestry.services.xml Sun Dec  3 16:16:20 2006
@@ -76,10 +76,8 @@
                 <set-object property="linkFactory" value="infrastructure:linkFactory" />
                 <set-service property="context" service-id="tapestry.globals.WebContext" />
                 <set-object property="response" value="service:tapestry.globals.WebResponse" />
-                <set-service property="digestSource"
-                    service-id="tapestry.asset.ResourceDigestSource" />
+                <set-service property="digestSource" service-id="tapestry.asset.ResourceDigestSource" />
                 <set-service property="unprotectedMatcher" service-id="tapestry.asset.UnprotectedResourceMatcher" />
-                <event-listener service-id="tapestry.ResetEventHub"/>
             </construct>
         </invoke-factory>
     </service-point>

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/asset/AssetService.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/asset/AssetService.java?view=diff&rev=481996&r1=481995&r2=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/asset/AssetService.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/asset/AssetService.java Sun Dec  3 16:16:20 2006
@@ -41,10 +41,10 @@
 import org.apache.tapestry.engine.IEngineService;
 import org.apache.tapestry.engine.ILink;
 import org.apache.tapestry.error.RequestExceptionReporter;
-import org.apache.tapestry.event.ResetEventListener;
 import org.apache.tapestry.services.LinkFactory;
 import org.apache.tapestry.services.ServiceConstants;
 import org.apache.tapestry.util.ContentType;
+import org.apache.tapestry.util.io.GzipUtil;
 import org.apache.tapestry.web.WebContext;
 import org.apache.tapestry.web.WebRequest;
 import org.apache.tapestry.web.WebResponse;
@@ -63,7 +63,7 @@
  * @author Howard Lewis Ship
  */
 
-public class AssetService implements IEngineService, ResetEventListener
+public class AssetService implements IEngineService
 {
     /**
      * Query parameter that stores the path to the resource (with a leading slash).
@@ -82,6 +82,8 @@
 
     public static final String DIGEST = "digest";
     
+    static final DateFormat CACHED_FORMAT = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
+    
     /**
      * Defaults MIME types, by extension, used when the servlet container doesn't provide MIME
      * types. ServletExec Debugger, for example, fails to provide these.
@@ -100,8 +102,6 @@
         _mimeTypes.put("htm", "text/html");
         _mimeTypes.put("html", "text/html");
     }
-
-    private static final DateFormat CACHED_FORMAT = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
     
     /** Represents a month of time in seconds. */
     private static final long MONTH_SECONDS = 60 * 60 * 24 * 30;
@@ -148,19 +148,9 @@
     /** @since 4.0 */
 
     private RequestExceptionReporter _exceptionReporter;
-
-    /** Used to prevent caching of resources when in disabled caching mode. */
     
-    private long _lastResetTime = -1;
+    private Map _cache = new HashMap();
     
-    /** 
-     * {@inheritDoc}
-     */
-    public void resetEventDidOccur()
-    {
-        _lastResetTime = System.currentTimeMillis();
-    }
-
     /**
      * Builds a {@link ILink}for a {@link PrivateAsset}.
      * <p>
@@ -345,8 +335,7 @@
                 modify = CACHED_FORMAT.parse(header).getTime();
         } catch (ParseException e) { e.printStackTrace(); }
         
-        if (resourceURL.getLastModified() > modify
-                || (_lastResetTime > modify))
+        if (resourceURL.getLastModified() > modify)
             return false;
         
         _response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
@@ -356,89 +345,119 @@
     
     /** @since 2.2 */
 
-    private void writeAssetContent(IRequestCycle cycle, String resourcePath,
-            URLConnection resourceConnection) throws IOException
+    private void writeAssetContent(IRequestCycle cycle, String resourcePath, URLConnection resourceConnection) 
+    throws IOException
     {
-        InputStream input = null;
+        // Getting the content type and length is very dependant
+        // on support from the application server (represented
+        // here by the servletContext).
+        
+        String contentType = getMimeType(resourcePath);
 
-        try
-        {
-            // Getting the content type and length is very dependant
-            // on support from the application server (represented
-            // here by the servletContext).
+        long lastModified = resourceConnection.getLastModified();
+        if (lastModified <= 0)
+            lastModified = _startupTime;
+        
+        _response.setDateHeader("Last-Modified", lastModified);
+        
+        // write out expiration/cache info
 
-            String contentType = getMimeType(resourcePath);
-            int contentLength = resourceConnection.getContentLength();
-            
-            if (contentLength > 0)
-                _response.setContentLength(contentLength);
+        _response.setDateHeader("Expires", _expireTime);
+        _response.setHeader("Cache-Control", "public, max-age=" + (MONTH_SECONDS * 3));
+        
+        // ie won't cache javascript with etag attached
+        if (_request.getHeader("User-Agent").indexOf("MSIE") < 0 
+                || contentType.indexOf("javascript") < 0)
+            _response.setHeader("ETag", String.valueOf(resourcePath.hashCode()));
             
-            long lastModified = _startupTime;
-            if (_lastResetTime > 0)
-                lastModified = _lastResetTime;
-            else
-                lastModified = resourceConnection.getLastModified();
+        
+        // Set the content type. If the servlet container doesn't
+        // provide it, try and guess it by the extension.
+        
+        if (contentType == null || contentType.length() == 0)
+            contentType = getMimeType(resourcePath);
+        
+        byte[] data = getAssetData(cycle, resourcePath, resourceConnection, contentType);
+        
+        // force image(or other) caching when detected, esp helps with ie related things
+        // see http://mir.aculo.us/2005/08/28/internet-explorer-and-ajax-image-caching-woes
+        
+        _response.setContentLength(data.length);
+        
+        OutputStream output = _response.getOutputStream(new ContentType(contentType));
+        
+        output.write(data);
+    }
+    
+    byte[] getAssetData(IRequestCycle cycle, String resourcePath,
+            URLConnection resourceConnection, String contentType) 
+    throws IOException
+    {
+        InputStream input = null;
+
+        try {
             
-            _response.setDateHeader("Last-Modified", lastModified);
+            CachedAsset cache = null;
+            byte[] data = null;
             
-            // write out expiration/cache info
+            // check cache first
             
-            if (_lastResetTime <= 0) {
+            if (_cache.get(resourcePath) != null) {
+                
+                cache = (CachedAsset)_cache.get(resourcePath);
+                
+                if (cache.getLastModified() < resourceConnection.getLastModified())
+                    cache.clear(resourceConnection.getLastModified());
                 
-                _response.setDateHeader("Expires", _expireTime);
-                _response.setHeader("Cache-Control", "max-age=" + (MONTH_SECONDS * 3));
-                _response.setHeader("ETag", String.valueOf(resourcePath.hashCode()));
+                data = cache.getData();
+            } else {
+                
+                cache = new CachedAsset(resourcePath, resourceConnection.getLastModified(), null, null);
+                
+                _cache.put(resourcePath, cache);
             }
             
-            // Set the content type. If the servlet container doesn't
-            // provide it, try and guess it by the extension.
-            
-            if (contentType == null || contentType.length() == 0)
-                contentType = getMimeType(resourcePath);
-            
-            input = resourceConnection.getInputStream();
-            
-            byte[] data = IOUtils.toByteArray(input);
+            if (data == null) {
+                
+                input = resourceConnection.getInputStream();
+                data = IOUtils.toByteArray(input);
+                
+                cache.setData(data);
+            }
             
             // compress javascript responses when possible
             
-            if (contentType.indexOf("javascript") > -1
-                    || contentType.indexOf("css") > -1 
-                    || contentType.indexOf("html") > -1
-                    || contentType.indexOf("text") > -1) {
-                String encoding = _request.getHeader("Accept-Encoding");
-                if (encoding != null && encoding.indexOf("gzip") > -1) {
+            if (GzipUtil.shouldCompressContentType(contentType) && GzipUtil.isGzipCapable(_request)) {
+                
+                if (cache.getGzipData() == null) {
                     
-                    ByteArrayOutputStream bo = 
-                        new ByteArrayOutputStream();
-                    GZIPOutputStream gzip = 
-                        new GZIPOutputStream(bo);
+                    ByteArrayOutputStream bo = new ByteArrayOutputStream();
+                    GZIPOutputStream gzip = new GZIPOutputStream(bo);
                     
                     gzip.write(data);
                     gzip.close();
                     
                     data = bo.toByteArray();
+                    cache.setGzipData(data);
+                } else {
                     
-                    _response.setHeader("Content-Encoding", "gzip");
+                    data = cache.getGzipData();
                 }
+                
+                _response.setHeader("Content-Encoding", "gzip");
             }
             
-            // force image(or other) caching when detected, esp helps with ie related things
-            // see http://mir.aculo.us/2005/08/28/internet-explorer-and-ajax-image-caching-woes
-            
-            _response.setContentLength(data.length);
+            return data;
             
-            OutputStream output = _response.getOutputStream(new ContentType(contentType));
+        } finally {
             
-            output.write(data);
-        }
-        finally
-        {
-            IOUtils.closeQuietly(input);
-            input = null;
+            if (input != null) {
+                IOUtils.closeQuietly(input);
+                input = null;
+            }
         }
     }
-
+    
     /** @since 4.0 */
 
     public void setExceptionReporter(RequestExceptionReporter exceptionReporter)

Added: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/asset/CachedAsset.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/asset/CachedAsset.java?view=auto&rev=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/asset/CachedAsset.java (added)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/asset/CachedAsset.java Sun Dec  3 16:16:20 2006
@@ -0,0 +1,174 @@
+// Copyright 2004, 2005 The Apache Software Foundation
+//
+// Licensed 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.tapestry.asset;
+
+
+/**
+ * Wrapper around cached asset resource.
+ * 
+ * @author jkuhnert
+ */
+public class CachedAsset
+{
+    
+    /**
+     * The raw data for this resource.
+     */
+    private byte[] _data;
+    
+    /**
+     * The gzipped version of the raw data.
+     */
+    private byte[] _gzipData;
+    
+    /**
+     * Path to the resource.
+     */
+    private String _path;
+    
+    /**
+     * The last known modification time of the data this cached object
+     * represents. Is used to invalidate cache entries.
+     */
+    private long _lastModified;
+    
+    /**
+     * Creates a new cachable asset entry. 
+     * 
+     * @param path
+     *          The path string of the resource.
+     * @param lastModified
+     *          The last known modification time of the data this cached object
+     *          represents. Is used to invalidate cache entries.
+     * @param data
+     *          The data representation to cache.
+     * @param gzipData
+     *          The optional gzip'ed data.
+     */
+    public CachedAsset(String path, long lastModified, byte[] data, byte[] gzipData)
+    {
+        _path = path;
+        _lastModified = lastModified;
+        _data = data;
+        _gzipData = gzipData;
+    }
+    
+    /**
+     * @return Returns the data.
+     */
+    public byte[] getData()
+    {
+        return _data;
+    }
+    
+    /**
+     * @param data The data to set.
+     */
+    public void setData(byte[] data)
+    {
+        _data = data;
+    }
+
+    
+    /**
+     * @return Returns the gzipData.
+     */
+    public byte[] getGzipData()
+    {
+        return _gzipData;
+    }
+
+    
+    /**
+     * @param gzipData The gzipData to set.
+     */
+    public void setGzipData(byte[] gzipData)
+    {
+        _gzipData = gzipData;
+    }
+
+    
+    /**
+     * @return Returns the path.
+     */
+    public String getPath()
+    {
+        return _path;
+    }
+    
+    /**
+     * @return Returns the lastModified.
+     */
+    public long getLastModified()
+    {
+        return _lastModified;
+    }
+    
+    /**
+     * Clears the currently cached data and resets the last modified time.
+     * 
+     * @param lastModified The lastModified to set.
+     */
+    public void clear(long lastModified)
+    {
+        _lastModified = lastModified;
+        _data = null;
+        _gzipData = null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode()
+    {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((_path == null) ? 0 : _path.hashCode());
+        return result;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(Object obj)
+    {
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
+        final CachedAsset other = (CachedAsset) obj;
+        if (_path == null) {
+            if (other._path != null) return false;
+        } else if (!_path.equals(other._path)) return false;
+        return true;
+    }
+    
+    /**
+     * {@inheritDoc}
+     */
+    public String toString()
+    {
+        String ret = "CachedAsset [path: " + _path;
+        
+        if (_data != null)
+            ret += ", data size(bytes): " + _data.length;
+        if (_gzipData != null)
+            ret += ", gzip data size(bytes): " + _gzipData.length;
+        
+        ret += ", lastModified(ms): " + _lastModified + "]";
+        
+        return ret;
+    }
+}

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/DojoAjaxResponseBuilder.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/DojoAjaxResponseBuilder.java?view=diff&rev=481996&r1=481995&r2=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/DojoAjaxResponseBuilder.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/DojoAjaxResponseBuilder.java Sun Dec  3 16:16:20 2006
@@ -13,13 +13,16 @@
 // limitations under the License.
 package org.apache.tapestry.services.impl;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.zip.GZIPOutputStream;
 
 import org.apache.hivemind.Resource;
 import org.apache.hivemind.util.Defense;
@@ -42,6 +45,8 @@
 import org.apache.tapestry.util.ContentType;
 import org.apache.tapestry.util.PageRenderSupportImpl;
 import org.apache.tapestry.util.ScriptUtils;
+import org.apache.tapestry.util.io.GzipUtil;
+import org.apache.tapestry.web.WebRequest;
 import org.apache.tapestry.web.WebResponse;
 
 
@@ -64,9 +69,13 @@
     // used to create IMarkupWriter
     private RequestLocaleManager _localeManager;
     private MarkupWriterSource _markupWriterSource;
-    private WebResponse _webResponse;
+    private WebRequest _request;
+    private WebResponse _response;
     private List _errorPages;
     
+    private ByteArrayOutputStream _output;
+    private ContentType _contentType;
+    
     // our response writer
     private IMarkupWriter _writer;
     // Parts that will be updated.
@@ -123,7 +132,7 @@
     public DojoAjaxResponseBuilder(IRequestCycle cycle, 
             RequestLocaleManager localeManager, 
             MarkupWriterSource markupWriterSource,
-            WebResponse webResponse, List errorPages, 
+            WebResponse webResponse, WebRequest request, List errorPages, 
             AssetFactory assetFactory, String namespace, IEngineService service)
     {
         Defense.notNull(cycle, "cycle");
@@ -132,7 +141,8 @@
         _cycle = cycle;
         _localeManager = localeManager;
         _markupWriterSource = markupWriterSource;
-        _webResponse = webResponse;
+        _response = webResponse;
+        _request = request;
         _errorPages = errorPages;
         _pageService = service;
         
@@ -158,21 +168,24 @@
     {
         _localeManager.persistLocale();
         
-        ContentType contentType = new ContentType(CONTENT_TYPE
+        _contentType = new ContentType(CONTENT_TYPE
                 + ";charset=" + cycle.getInfrastructure().getOutputEncoding());
         
-        String encoding = contentType.getParameter(ENCODING_KEY);
+        String encoding = _contentType.getParameter(ENCODING_KEY);
         
         if (encoding == null)
         {
             encoding = cycle.getEngine().getOutputEncoding();
             
-            contentType.setParameter(ENCODING_KEY, encoding);
+            _contentType.setParameter(ENCODING_KEY, encoding);
         }
         
-        PrintWriter printWriter = _webResponse.getPrintWriter(contentType);
+        _output = new ByteArrayOutputStream();
+        
+        // PrintWriter printWriter = _response.getPrintWriter(_contentType);
+        PrintWriter printWriter = new PrintWriter(_output, true);
         
-        _writer = _markupWriterSource.newMarkupWriter(printWriter, contentType);
+        _writer = _markupWriterSource.newMarkupWriter(printWriter, _contentType);
         
         parseParameters(cycle);
         
@@ -191,6 +204,41 @@
         endResponse();
         
         _writer.close();
+        
+        writeResponse();
+    }
+    
+    /**
+     * Causes the actual / real response to be written to the output stream.
+     */
+    void writeResponse()
+    throws IOException
+    {
+        byte[] data = _output.toByteArray();
+        
+        if (GzipUtil.isGzipCapable(_request)) {
+            
+            // reset data buffer
+            _output.reset();
+            
+            GZIPOutputStream gzip = new GZIPOutputStream(_output);
+            
+            gzip.write(data);
+            gzip.close();
+            
+            data = _output.toByteArray();
+            
+            _response.setHeader("Content-Encoding", "gzip");
+        }
+        
+        _response.setContentLength(data.length);
+        
+        OutputStream output = _response.getOutputStream(_contentType);
+        
+        output.write(data);
+        
+        output.flush();
+        output.close();
     }
     
     /** 

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/DojoAjaxResponseContributorImpl.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/DojoAjaxResponseContributorImpl.java?view=diff&rev=481996&r1=481995&r2=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/DojoAjaxResponseContributorImpl.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/DojoAjaxResponseContributorImpl.java Sun Dec  3 16:16:20 2006
@@ -68,7 +68,7 @@
         
         return new DojoAjaxResponseBuilder(cycle, _localeManager, 
                 _markupWriterSource,
-                _webResponse, errorPages, _assetFactory, 
+                _webResponse, _webRequest, errorPages, _assetFactory, 
                 _webResponse.getNamespace(), _pageService);
     }
     

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/JSONResponseBuilder.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/JSONResponseBuilder.java?view=diff&rev=481996&r1=481995&r2=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/JSONResponseBuilder.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/JSONResponseBuilder.java Sun Dec  3 16:16:20 2006
@@ -13,11 +13,14 @@
 // limitations under the License.
 package org.apache.tapestry.services.impl;
 
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.zip.GZIPOutputStream;
 
 import org.apache.hivemind.Resource;
 import org.apache.hivemind.util.Defense;
@@ -37,6 +40,8 @@
 import org.apache.tapestry.services.ServiceConstants;
 import org.apache.tapestry.util.ContentType;
 import org.apache.tapestry.util.PageRenderSupportImpl;
+import org.apache.tapestry.util.io.GzipUtil;
+import org.apache.tapestry.web.WebRequest;
 import org.apache.tapestry.web.WebResponse;
 
 /**
@@ -56,10 +61,12 @@
     protected List _parts = new ArrayList();
     
     protected RequestLocaleManager _localeManager;
-    
     protected MarkupWriterSource _markupWriterSource;
-
-    protected WebResponse _webResponse;
+    private WebRequest _request;
+    private WebResponse _response;
+    
+    private ByteArrayOutputStream _output;
+    private ContentType _contentType;
     
     private final AssetFactory _assetFactory;
     
@@ -82,14 +89,15 @@
      */
     public JSONResponseBuilder(IRequestCycle cycle, RequestLocaleManager localeManager, 
             MarkupWriterSource markupWriterSource,
-            WebResponse webResponse, AssetFactory assetFactory, String namespace)
+            WebResponse webResponse, WebRequest request, AssetFactory assetFactory, String namespace)
     {
         Defense.notNull(cycle, "cycle");
         
         _cycle = cycle;
         _localeManager = localeManager;
         _markupWriterSource = markupWriterSource;
-        _webResponse = webResponse;
+        _response = webResponse;
+        _request = request;
         
         // Used by PageRenderSupport
         _assetFactory = assetFactory;
@@ -115,20 +123,23 @@
         
         IPage page = cycle.getPage();
         
-        ContentType contentType = page.getResponseContentType();
+        _contentType = page.getResponseContentType();
         
-        String encoding = contentType.getParameter(ENCODING_KEY);
+        String encoding = _contentType.getParameter(ENCODING_KEY);
         
         if (encoding == null)
         {
             encoding = cycle.getEngine().getOutputEncoding();
             
-            contentType.setParameter(ENCODING_KEY, encoding);
+            _contentType.setParameter(ENCODING_KEY, encoding);
         }
         
-        PrintWriter printWriter = _webResponse.getPrintWriter(contentType);
+        _output = new ByteArrayOutputStream();
         
-        _writer = _markupWriterSource.newJSONWriter(printWriter, contentType);
+        // PrintWriter printWriter = _webResponse.getPrintWriter(_contentType);
+        PrintWriter printWriter = new PrintWriter(_output, true);
+        
+        _writer = _markupWriterSource.newJSONWriter(printWriter, _contentType);
         
         // render response
         
@@ -143,6 +154,41 @@
         TapestryUtils.removePageRenderSupport(cycle);
         
         _writer.close();
+        
+        writeResponse();
+    }
+    
+    /**
+     * Causes the actual / real response to be written to the output stream.
+     */
+    void writeResponse()
+    throws IOException
+    {
+        byte[] data = _output.toByteArray();
+        
+        if (GzipUtil.isGzipCapable(_request)) {
+            
+            // reset data buffer
+            _output.reset();
+            
+            GZIPOutputStream gzip = new GZIPOutputStream(_output);
+            
+            gzip.write(data);
+            gzip.close();
+            
+            data = _output.toByteArray();
+            
+            _response.setHeader("Content-Encoding", "gzip");
+        }
+        
+        _response.setContentLength(data.length);
+        
+        OutputStream output = _response.getOutputStream(_contentType);
+        
+        output.write(data);
+        
+        output.flush();
+        output.close();
     }
     
     /**

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/JSONResponseContributorImpl.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/JSONResponseContributorImpl.java?view=diff&rev=481996&r1=481995&r2=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/JSONResponseContributorImpl.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/services/impl/JSONResponseContributorImpl.java Sun Dec  3 16:16:20 2006
@@ -52,7 +52,7 @@
     throws IOException
     {
         return new JSONResponseBuilder(cycle, _localeManager, _markupWriterSource,
-                _webResponse, _assetFactory, _webResponse.getNamespace());
+                _webResponse, _webRequest, _assetFactory, _webResponse.getNamespace());
     }
     
     /**

Added: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/util/io/GzipUtil.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/util/io/GzipUtil.java?view=auto&rev=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/util/io/GzipUtil.java (added)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/util/io/GzipUtil.java Sun Dec  3 16:16:20 2006
@@ -0,0 +1,92 @@
+// Copyright 2004, 2005 The Apache Software Foundation
+//
+// Licensed 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.tapestry.util.io;
+
+import org.apache.tapestry.web.WebRequest;
+
+
+/**
+ * Encapsulates logic related to various gzip compression schemes and their rules
+ * as they apply to different browsers.
+ * 
+ * @author jkuhnert
+ */
+public final class GzipUtil
+{
+    private static final float MIN_IE_VERSION = 7.0f;
+    
+    private static final String MSIE_6_COMPATIBLE_STRING = "SV1";
+    
+    /* defeat instantiation */
+    private GzipUtil() { }
+    
+    /**
+     * Determines if gzip compression is appropriate/possible based on the User Agent and 
+     * other limiting factors. IE versions &lt; 6.1 are known to not work with gzip compression reliably. 
+     * 
+     * @return True, if this request can be served in gzip format. False otherwise.
+     */
+    public static boolean isGzipCapable(WebRequest request)
+    {
+        String encoding = request.getHeader("Accept-Encoding");
+        if (encoding == null || encoding.indexOf("gzip") < 0)
+            return false;
+        
+        // Handle IE specific hacks
+        
+        String userAgent = request.getHeader("User-Agent");
+        int ieIndex = (userAgent != null) ? userAgent.indexOf("MSIE") : -1;
+        if (ieIndex > -1) {
+            
+            float version = -1;
+            
+            try {
+             version = Float.parseFloat(userAgent.substring(ieIndex + 4, ieIndex + 8));
+            } catch (NumberFormatException nf) {nf.printStackTrace();}
+            
+            if (version >= MIN_IE_VERSION)
+                return true;
+            
+            if (userAgent.indexOf(MSIE_6_COMPATIBLE_STRING) > -1)
+                return true;
+            
+            // else false
+            
+            return false;
+        }
+        
+        return true;
+    }
+    
+    /**
+     * Based on the given type of content, determines if compression is appropriate. The biggest
+     * thing it does is make sure that image content isn't compressed as that kind of content
+     * is already compressed fairly well.
+     * 
+     * @param contentType
+     *          The content type to check. (ie "text/javascript","text/html", etc..)
+     *          
+     * @return True if compression is appropriate for the content specified, false otherwise.
+     */
+    public static boolean shouldCompressContentType(String contentType)
+    {
+        if (contentType == null)
+            return false;
+        
+        return contentType.indexOf("javascript") > -1
+        || contentType.indexOf("css") > -1 
+        || contentType.indexOf("html") > -1
+        || contentType.indexOf("text") > -1;
+    }
+}

Added: tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/asset/TestAssetService.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/asset/TestAssetService.java?view=auto&rev=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/asset/TestAssetService.java (added)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/asset/TestAssetService.java Sun Dec  3 16:16:20 2006
@@ -0,0 +1,130 @@
+// Copyright 2004, 2005 The Apache Software Foundation
+//
+// Licensed 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.tapestry.asset;
+
+import static org.easymock.EasyMock.*;
+
+import java.net.URLConnection;
+
+import org.apache.tapestry.web.WebRequest;
+import org.apache.tapestry.web.WebResponse;
+import org.testng.annotations.Test;
+
+import com.javaforge.tapestry.testng.TestBase;
+
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * Tests functionality of {@link AssetService}. 
+ *
+ * @author jkuhnert
+ */
+@Test
+public class TestAssetService extends TestBase
+{   
+    
+    public void test_Cached_Resource_Null_Modified()
+    {
+        WebRequest request = newMock(WebRequest.class);
+        checkOrder(request, false);
+        
+        AssetService service = new AssetService();
+        service.setRequest(request);
+        
+        URLConnection url = org.easymock.classextension.EasyMock.createMock(URLConnection.class);
+        
+        expect(request.getHeader("If-Modified-Since")).andReturn(null);
+        expect(url.getLastModified()).andReturn(System.currentTimeMillis());
+        
+        replay();
+        org.easymock.classextension.EasyMock.replay(url);
+        
+        assertFalse(service.cachedResource(url));
+        
+        verify();
+        org.easymock.classextension.EasyMock.verify(url);
+    }
+    
+    public void test_Cached_Resource_Malformed_Modified()
+    {
+        WebRequest request = newMock(WebRequest.class);
+        checkOrder(request, false);
+        
+        AssetService service = new AssetService();
+        service.setRequest(request);
+        
+        URLConnection url = org.easymock.classextension.EasyMock.createMock(URLConnection.class);
+        
+        expect(request.getHeader("If-Modified-Since")).andReturn("Woopedy woopedy");
+        expect(url.getLastModified()).andReturn(System.currentTimeMillis());
+        
+        replay();
+        org.easymock.classextension.EasyMock.replay(url);
+        
+        assertFalse(service.cachedResource(url));
+        
+        verify();
+        org.easymock.classextension.EasyMock.verify(url);
+    }
+    
+    public void test_Cached_Resource_Stale()
+    {
+        WebRequest request = newMock(WebRequest.class);
+        checkOrder(request, false);
+        
+        AssetService service = new AssetService();
+        service.setRequest(request);
+        
+        URLConnection url = org.easymock.classextension.EasyMock.createMock(URLConnection.class);
+        
+        expect(request.getHeader("If-Modified-Since")).andReturn("Sat, 29 Oct 1994 19:43:31 GMT");
+        expect(url.getLastModified()).andReturn(System.currentTimeMillis());
+        
+        replay();
+        org.easymock.classextension.EasyMock.replay(url);
+        
+        assertFalse(service.cachedResource(url));
+        
+        verify();
+        org.easymock.classextension.EasyMock.verify(url);
+    }
+    
+    public void test_Cached_Resource_Good()
+    throws Exception
+    {
+        WebRequest request = newMock(WebRequest.class);
+        checkOrder(request, false);
+        WebResponse response = newMock(WebResponse.class);
+        
+        AssetService service = new AssetService();
+        service.setRequest(request);
+        service.setResponse(response);
+        
+        URLConnection url = org.easymock.classextension.EasyMock.createMock(URLConnection.class);
+        
+        expect(request.getHeader("If-Modified-Since")).andReturn("Sat, 29 Oct 1994 19:43:31 GMT");
+        expect(url.getLastModified()).andReturn(AssetService.CACHED_FORMAT.parse("Sat, 1 Dec 1991 19:43:31 GMT").getTime());
+        
+        response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+        
+        replay();
+        org.easymock.classextension.EasyMock.replay(url);
+        
+        assertTrue(service.cachedResource(url));
+        
+        verify();
+        org.easymock.classextension.EasyMock.verify(url);
+    }
+}

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/services/impl/DojoAjaxResponseBuilderTest.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/services/impl/DojoAjaxResponseBuilderTest.java?view=diff&rev=481996&r1=481995&r2=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/services/impl/DojoAjaxResponseBuilderTest.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/services/impl/DojoAjaxResponseBuilderTest.java Sun Dec  3 16:16:20 2006
@@ -40,6 +40,7 @@
 import org.apache.tapestry.services.RequestLocaleManager;
 import org.apache.tapestry.services.ResponseBuilder;
 import org.apache.tapestry.services.ServiceConstants;
+import org.apache.tapestry.web.WebRequest;
 import org.apache.tapestry.web.WebResponse;
 import org.testng.annotations.AfterClass;
 import org.testng.annotations.Test;
@@ -181,6 +182,7 @@
         RequestLocaleManager rlm = newMock(RequestLocaleManager.class);
         MarkupWriterSource mrs = newMock(MarkupWriterSource.class);
         WebResponse resp = newMock(WebResponse.class);
+        WebRequest req = newMock(WebRequest.class);
         AssetFactory assetFactory = newMock(AssetFactory.class);
         IEngineService pageService = newEngineService();
         
@@ -190,7 +192,7 @@
         parts.add("id1");
         
         DojoAjaxResponseBuilder builder = 
-            new DojoAjaxResponseBuilder(cycle, rlm, mrs, resp, 
+            new DojoAjaxResponseBuilder(cycle, rlm, mrs, resp, req,
                     errorPages, assetFactory, "", pageService);
         
         expect(page.getPageName()).andReturn("RequestPage").anyTimes();
@@ -224,6 +226,7 @@
         RequestLocaleManager rlm = newMock(RequestLocaleManager.class);
         MarkupWriterSource mrs = newMock(MarkupWriterSource.class);
         WebResponse resp = newMock(WebResponse.class);
+        WebRequest req = newMock(WebRequest.class);
         AssetFactory assetFactory = newMock(AssetFactory.class);
         IEngineService pageService = newEngineService();
         
@@ -233,7 +236,7 @@
         parts.add("id1");
         
         DojoAjaxResponseBuilder builder = 
-            new DojoAjaxResponseBuilder(cycle, rlm, mrs, resp, 
+            new DojoAjaxResponseBuilder(cycle, rlm, mrs, resp, req,
                     errorPages, assetFactory, "", pageService);
         
         builder.setWriter(writer);

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/services/impl/TestDisableCachingFilter.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/services/impl/TestDisableCachingFilter.java?view=diff&rev=481996&r1=481995&r2=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/services/impl/TestDisableCachingFilter.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/services/impl/TestDisableCachingFilter.java Sun Dec  3 16:16:20 2006
@@ -32,7 +32,7 @@
  * @author Howard M. Lewis Ship
  * @since 4.0
  */
-@Test()
+@Test(sequential=true)
 public class TestDisableCachingFilter extends BaseComponentTestCase
 {
     private WebResponse newResponse()

Added: tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/util/io/TestGzipUtil.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/util/io/TestGzipUtil.java?view=auto&rev=481996
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/util/io/TestGzipUtil.java (added)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/util/io/TestGzipUtil.java Sun Dec  3 16:16:20 2006
@@ -0,0 +1,97 @@
+// Copyright 2004, 2005 The Apache Software Foundation
+//
+// Licensed 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.tapestry.util.io;
+
+import static org.easymock.EasyMock.checkOrder;
+import static org.easymock.EasyMock.expect;
+
+import org.apache.tapestry.web.WebRequest;
+import org.testng.annotations.Test;
+
+import com.javaforge.tapestry.testng.TestBase;
+
+
+/**
+ * Tests functionality of {@link GzipUtil} .
+ *
+ * @author jkuhnert
+ */
+@Test
+public class TestGzipUtil extends TestBase
+{
+    
+    // for more see http://en.wikipedia.org/wiki/User_agent
+    static final String MSIE_5_SUNOS = "Mozilla/4.0 (compatible; MSIE 5.0; SunOS 5.9 sun4u; X11)";
+    static final String MSIE_5_5 = "Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0)";
+    static final String MSIE_6 = "Mozilla/4.0 (compatible; MSIE 6.0; MSN 2.5; Windows 98)";
+    static final String MSIE_6_SP2 = "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)";
+    static final String MSIE_7_BETA_1 = "Mozilla/4.0 (compatible; MSIE 7.0b; Win32)";
+    
+    static final String MOZ_1_5 = "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.0.2) Gecko/20060308 Firefox/1.5.0.2";
+    static final String MOZ_2_WIN98 = "Mozilla/5.0 (Windows; U; Win98; en-US; rv:1.8.1) Gecko/20061010 Firefox/2.0";
+    
+    static final String OPERA_7 = "Opera/7.23 (Windows 98; U) [en]";
+    
+    static final String SAFARI_125 = "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/124 (KHTML, like Gecko)";
+    static final String SAFARI_204 = "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/418.9 (KHTML, like Gecko) Safari/419.3";
+    
+    public void test_Gzip_Capable()
+    {
+        WebRequest request = newMock(WebRequest.class);
+        checkOrder(request, false);
+        
+        expect(request.getHeader("Accept-Encoding")).andReturn("gzip").anyTimes();
+        
+        expect(request.getHeader("User-Agent")).andReturn(MSIE_5_SUNOS);
+        expect(request.getHeader("User-Agent")).andReturn(MSIE_5_5);
+        expect(request.getHeader("User-Agent")).andReturn(MSIE_6);
+        
+        expect(request.getHeader("User-Agent")).andReturn(MSIE_6_SP2);
+        expect(request.getHeader("User-Agent")).andReturn(MSIE_7_BETA_1);
+        
+        expect(request.getHeader("User-Agent")).andReturn(MOZ_1_5);
+        expect(request.getHeader("User-Agent")).andReturn(MOZ_2_WIN98);
+        expect(request.getHeader("User-Agent")).andReturn(OPERA_7);
+        expect(request.getHeader("User-Agent")).andReturn(SAFARI_125);
+        expect(request.getHeader("User-Agent")).andReturn(SAFARI_204);
+        
+        replay();
+        
+        assertFalse(GzipUtil.isGzipCapable(request));
+        assertFalse(GzipUtil.isGzipCapable(request));
+        assertFalse(GzipUtil.isGzipCapable(request));
+        
+        assertTrue(GzipUtil.isGzipCapable(request));
+        assertTrue(GzipUtil.isGzipCapable(request));
+        assertTrue(GzipUtil.isGzipCapable(request));
+        assertTrue(GzipUtil.isGzipCapable(request));
+        assertTrue(GzipUtil.isGzipCapable(request));
+        assertTrue(GzipUtil.isGzipCapable(request));
+        assertTrue(GzipUtil.isGzipCapable(request));
+        
+        verify();
+    }
+    
+    public void test_Compress_Content_Type()
+    {
+        assertFalse(GzipUtil.shouldCompressContentType(null));
+        assertTrue(GzipUtil.shouldCompressContentType("javascript"));
+        assertTrue(GzipUtil.shouldCompressContentType("html"));
+        assertTrue(GzipUtil.shouldCompressContentType("css"));
+        
+        assertFalse(GzipUtil.shouldCompressContentType("jpeg"));
+        assertFalse(GzipUtil.shouldCompressContentType("image"));
+        assertFalse(GzipUtil.shouldCompressContentType("png"));
+    }
+}