You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@shindig.apache.org by jo...@apache.org on 2010/12/20 20:41:24 UTC

svn commit: r1051258 - in /shindig/trunk/java: common/src/main/java/org/apache/shindig/common/util/ gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ server/src/main/webapp/WEB-INF/

Author: johnh
Date: Mon Dec 20 19:41:24 2010
New Revision: 1051258

URL: http://svn.apache.org/viewvc?rev=1051258&view=rev
Log:
ETagging support.

Introduces an ETagFilter and the ETaggingHttpResponse wrapper that it utilizes under the hood. All Shindig APIs are presently batching, so installs this Filter in the default web.xml.

Patch provided by Jacobo Tarrio.


Added:
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETagFilter.java
    shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponse.java
    shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETagFilterTest.java
    shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponseTest.java
Modified:
    shindig/trunk/java/common/src/main/java/org/apache/shindig/common/util/HashUtil.java
    shindig/trunk/java/server/src/main/webapp/WEB-INF/web.xml

Modified: shindig/trunk/java/common/src/main/java/org/apache/shindig/common/util/HashUtil.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/common/src/main/java/org/apache/shindig/common/util/HashUtil.java?rev=1051258&r1=1051257&r2=1051258&view=diff
==============================================================================
--- shindig/trunk/java/common/src/main/java/org/apache/shindig/common/util/HashUtil.java (original)
+++ shindig/trunk/java/common/src/main/java/org/apache/shindig/common/util/HashUtil.java Mon Dec 20 19:41:24 2010
@@ -40,6 +40,16 @@ public final class HashUtil {
    */
   public static String checksum(byte[] data) {
     byte[] hashBytes = getMessageDigest().digest(Preconditions.checkNotNull(data));
+    return bytesToHex(hashBytes);
+  }
+  
+  /**
+   * Converts a byte array into a hex string.
+   * 
+   * @param hashBytes The byte array to convert.
+   * @return The hex string.
+   */
+  public static String bytesToHex(byte[] hashBytes) {
     char[] hex = new char[2 * hashBytes.length];
 
     // Convert to hex. possibly change to base64 in the future for smaller
@@ -63,7 +73,12 @@ public final class HashUtil {
     return new String(getMessageDigest().digest(Preconditions.checkNotNull(data)));
   }
 
-  private static MessageDigest getMessageDigest() {
+  /**
+   * Provides a {@link MessageDigest} object for calculating checksums.
+   * 
+   * @return A MessageDigest object.
+   */
+  public static MessageDigest getMessageDigest() {
     MessageDigest md;
     try {
       md = MessageDigest.getInstance("MD5");

Added: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETagFilter.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETagFilter.java?rev=1051258&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETagFilter.java (added)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETagFilter.java Mon Dec 20 19:41:24 2010
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * A servlet filter to generate and check ETags in HTTP responses.
+ *
+ * An ETag is calculated for the servlet's output. If its value matches the
+ * value provided in the request's "If-None-Match" header, a 304 Not Modified
+ * response is returned; otherwise, the value is added to the response's "ETag"
+ * header.
+ *
+ * Note that when this filter is applied, the response body cannot be streamed.
+ */
+public class ETagFilter implements Filter {
+
+  public void init(FilterConfig filterConfig) {
+  }
+
+  public void destroy() {
+  }
+
+  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+      throws IOException, ServletException {
+    if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
+      ETaggingHttpResponse taggingResponse =
+          new ETaggingHttpResponse((HttpServletRequest) request, (HttpServletResponse) response);
+      try {
+        chain.doFilter(request, taggingResponse);
+      } finally {
+        // Write to the output even if there was an exception, as it would have
+        // done without this filter.
+        taggingResponse.writeToOutput();
+      }
+    } else {
+      chain.doFilter(request, response);
+    }
+  }
+
+}

Added: shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponse.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponse.java?rev=1051258&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponse.java (added)
+++ shindig/trunk/java/gadgets/src/main/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponse.java Mon Dec 20 19:41:24 2010
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import org.apache.http.util.ByteArrayBuffer;
+import org.apache.shindig.common.util.HashUtil;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.security.MessageDigest;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+/**
+ * A class for generating and managing ETags for improved caching.
+ *
+ * Objects of this class have two modes: batching mode and streaming mode.
+ * 
+ * In batching mode, the response body is stored in a buffer and, at the end,
+ * its ETag is calculated and everything is written to the output at once.
+ * 
+ * In streaming mode, however, the response body is output as it's received
+ * from the servlet, and no ETag is calculated.
+ */
+public class ETaggingHttpResponse extends HttpServletResponseWrapper {
+
+  public static final String RESPONSE_HEADER = "ETag";
+  public static final String REQUEST_HEADER = "If-None-Match";
+
+  private final HttpServletRequest request;
+  private final BufferServletOutputStream stream;
+  private ServletOutputStream originalStream;
+  private PrintWriter writer;
+  private boolean batching;
+
+  public ETaggingHttpResponse(HttpServletRequest request, HttpServletResponse response) {
+    super(response);
+    this.request = request;
+    this.stream = new BufferServletOutputStream();
+    this.writer = null;
+    this.batching = true;
+  }
+
+  @Override
+  public ServletOutputStream getOutputStream() throws IOException {
+    if (originalStream == null) {
+      originalStream = getResponse().getOutputStream();
+    }
+    if (isCommitted()) {
+      batching = false;
+    }
+    return stream;
+  }
+
+  @Override
+  public PrintWriter getWriter() throws IOException {
+    if (writer == null) {
+      writer = new PrintWriter(new OutputStreamWriter(getOutputStream(), getCharacterEncoding()));
+    }
+    return writer;
+  }
+
+  /**
+   * {@inheritDoc}
+   * 
+   * The response object is also switched to streaming mode.
+   */
+  @Override
+  public void flushBuffer() throws IOException {
+    writeToOutput();
+    getResponse().flushBuffer();
+    batching = false;
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * The response object is switched to batching mode if the response has not
+   * been committed yet.
+   */
+  @Override
+  public void reset() {
+    super.reset();
+    writer = null;
+    stream.reset();
+    batching = !isCommitted();
+  }
+
+  /**
+   * {@inheritDoc}
+   *
+   * The response object is switched to batching mode if the response has not
+   * been committed yet.
+   */
+  @Override
+  public void resetBuffer() {
+    super.resetBuffer();
+    writer = null;
+    stream.reset();
+    batching = !isCommitted();
+  }
+
+  /**
+   * Switches this response object to streaming mode.
+   *
+   * The current buffer is written to the output, as are any subsequent writes.
+   *
+   * @throws IOException If flushing the buffer produced an exception.
+   */
+  public void startStreaming() throws IOException {
+    batching = false;
+    writeToOutput();
+  }
+
+  /**
+   * Outputs the response body.
+   * 
+   * In batching mode, it outputs the full contents of the buffer with its
+   * corresponding ETag, or a NOT_MODIFIED response if the ETag matches the
+   * request's "If-None-Match" header.
+   *
+   * In streaming mode, output is only generated if the buffer is not empty;
+   * in that case, the buffer is flushed to the output.
+   *
+   * @throws IOException If there was a problem writing to the output.
+   */
+  void writeToOutput() throws IOException {
+    if (writer != null) {
+      writer.flush();
+    }
+    byte[] bytes = stream.getBuffer().toByteArray();
+    if (batching) {
+      String etag = stream.getContentHash();
+      String reqEtag = request.getHeader(REQUEST_HEADER);
+      ((HttpServletResponse) getResponse()).setHeader(RESPONSE_HEADER, etag);
+      if (etag.equals(reqEtag)) {
+        emitETagMatchedResult();
+      } else {
+        getResponse().setContentLength(bytes.length);
+        getResponse().getOutputStream().write(bytes);
+      }
+    } else if (bytes.length != 0) {
+      originalStream.write(bytes);
+      stream.getBuffer().clear();
+    }
+  }
+
+  protected void emitETagMatchedResult() throws IOException {
+    ((HttpServletResponse) getResponse()).setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+    getResponse().setContentLength(0);
+  }
+
+  /**
+   * A ServletOutputStream that stores the data in a byte array buffer.
+   */
+  private class BufferServletOutputStream extends ServletOutputStream {
+    private static final int BUFFER_INITIAL_CAPACITY = 16384;
+
+    private MessageDigest digest = HashUtil.getMessageDigest();
+    private ByteArrayBuffer buffer = new ByteArrayBuffer(BUFFER_INITIAL_CAPACITY);
+
+    @Override
+    public void write(int b) throws IOException {
+      if (batching) {
+        updateDigest(b);
+        buffer.append(b);
+      } else {
+        originalStream.write(b);
+      }
+    }
+
+    public ByteArrayBuffer getBuffer() {
+      return buffer;
+    }
+
+    public void reset() {
+      buffer.clear();
+      digest.reset();
+    }
+
+    public String getContentHash() {
+      String hash = HashUtil.bytesToHex(digest.digest());
+      digest = null;
+      return hash;
+    }
+
+    private void updateDigest(int b) {
+      if (digest == null) {
+        digest = HashUtil.getMessageDigest();
+      }
+      digest.update((byte) b);
+    }
+  }
+}

Added: shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETagFilterTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETagFilterTest.java?rev=1051258&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETagFilterTest.java (added)
+++ shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETagFilterTest.java Mon Dec 20 19:41:24 2010
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.junit.Assert.*;
+
+import com.google.common.collect.Lists;
+
+import org.apache.http.util.ByteArrayBuffer;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Tests for {@link ETagFilter}.
+ */
+public class ETagFilterTest {
+
+  // Using Spanish string and UTF-8 encoding to check for encoding errors.
+  private static final String ENCODING = "UTF-8";
+  private static final byte[] RESPONSE_BODY_BYTES =
+      new byte[] {-62, -95, 72, 111, 108, 97, 44, 32, 110, 105, -61, -79, 111, 33};
+  private static final int RESPONSE_BODY_LENGTH = RESPONSE_BODY_BYTES.length;
+  private static final String GOOD_ETAG = "dae018f624d09423e7c4d7209fbea597";
+
+  private IMocksControl control;
+  private HttpServletRequest request;
+  private HttpServletResponse response;
+  private MockFilterChain chain;
+  private MockServletOutputStream stream;
+  private ETagFilter filter;
+
+  @Before
+  public void setUp() throws Exception {
+    control = EasyMock.createControl();
+    request = control.createMock(HttpServletRequest.class);
+    response = control.createMock(HttpServletResponse.class);
+    chain = new MockFilterChain();
+    stream = new MockServletOutputStream();
+    filter = new ETagFilter();
+
+    EasyMock.expect(response.isCommitted()).andReturn(false).anyTimes();
+    EasyMock.expect(response.getOutputStream()).andReturn(stream).anyTimes();
+    EasyMock.expect(response.getCharacterEncoding()).andReturn(ENCODING).anyTimes();
+    EasyMock.expect(request.getHeader(ETaggingHttpResponse.REQUEST_HEADER)).andReturn(null);
+    response.setHeader(ETaggingHttpResponse.RESPONSE_HEADER, GOOD_ETAG);
+    response.setContentLength(RESPONSE_BODY_LENGTH);
+  }
+
+  @Test
+  public void testTagContent() throws Exception {
+    chain.write(RESPONSE_BODY_BYTES);
+    control.replay();
+
+    filter.doFilter(request, response, chain);
+    assertArrayEquals(RESPONSE_BODY_BYTES, stream.getBuffer());
+    control.verify();
+  }
+
+  @Test
+  public void testTagContentOnException() throws Exception {
+    chain.write(RESPONSE_BODY_BYTES);
+    chain.throwException();
+    control.replay();
+
+    try {
+      filter.doFilter(request, response, chain);
+      fail("Should have thrown an IOException");
+    } catch (IOException e) {
+      // pass
+    }
+    assertArrayEquals(RESPONSE_BODY_BYTES, stream.getBuffer());
+    control.verify();
+  }
+
+  private class MockServletOutputStream extends ServletOutputStream {
+    private ByteArrayBuffer buffer = new ByteArrayBuffer(1024);
+
+    @Override
+    public void write(int b) {
+      buffer.append(b);
+    }
+
+    public byte[] getBuffer() {
+      return buffer.toByteArray();
+    }
+  }
+
+  private class MockFilterChain implements FilterChain {
+    private List<Object> commands = Lists.newArrayList();
+
+    public void write(byte[] s) {
+      commands.add(s);
+    }
+
+    public void throwException() {
+      commands.add(new IOException());
+    }
+
+    public void doFilter(ServletRequest request, ServletResponse response) throws IOException {
+      for (Object cmd : commands) {
+        if (cmd instanceof byte[]) {
+          response.getOutputStream().write((byte[]) cmd);
+        } else if (cmd instanceof IOException) {
+          throw (IOException) cmd;
+        }
+      }
+    }
+  }
+}

Added: shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponseTest.java
URL: http://svn.apache.org/viewvc/shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponseTest.java?rev=1051258&view=auto
==============================================================================
--- shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponseTest.java (added)
+++ shindig/trunk/java/gadgets/src/test/java/org/apache/shindig/gadgets/servlet/ETaggingHttpResponseTest.java Mon Dec 20 19:41:24 2010
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.apache.shindig.gadgets.servlet;
+
+import static org.junit.Assert.*;
+
+import org.apache.http.util.ByteArrayBuffer;
+import org.easymock.EasyMock;
+import org.easymock.IMocksControl;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Tests for {@link ETaggingHttpResponse}.
+ */
+public class ETaggingHttpResponseTest {
+
+  // Using Spanish string and UTF-8 encoding to check for encoding errors.
+  private static final String ENCODING = "UTF-8";
+  private static final String RESPONSE_BODY = "¡Hola, niño!";
+  private static final byte[] RESPONSE_BODY_BYTES =
+      new byte[] {-62, -95, 72, 111, 108, 97, 44, 32, 110, 105, -61, -79, 111, 33};
+  private static final String SECOND_RESPONSE_BODY = "你好";
+  private static final byte[] AFTER_SECOND_RESPONSE_BODY_BYTES =
+      new byte[] {-62, -95, 72, 111, 108, 97, 44, 32, 110, 105, -61, -79, 111, 33};
+  private static final int RESPONSE_BODY_LENGTH = RESPONSE_BODY_BYTES.length;
+  private static final String GOOD_ETAG = "dae018f624d09423e7c4d7209fbea597";
+  private static final String BAD_ETAG = "some bogus etag";
+  private static final String EMPTY_CONTENT_ETAG = "d41d8cd98f00b204e9800998ecf8427e";
+
+  private IMocksControl control;
+  private HttpServletRequest request;
+  private HttpServletResponse origResponse;
+  private MockServletOutputStream stream;
+  private ETaggingHttpResponse response;
+
+  @Before
+  public void setUp() throws Exception {
+    control = EasyMock.createControl();
+    request = control.createMock(HttpServletRequest.class);
+    origResponse = control.createMock(HttpServletResponse.class);
+    stream = new MockServletOutputStream();
+    response = new ETaggingHttpResponse(request, origResponse);
+
+    EasyMock.expect(origResponse.isCommitted()).andReturn(false).anyTimes();
+    EasyMock.expect(origResponse.getOutputStream()).andReturn(stream).anyTimes();
+    EasyMock.expect(origResponse.getCharacterEncoding()).andReturn(ENCODING).anyTimes();
+    origResponse.flushBuffer();
+    EasyMock.expectLastCall().anyTimes();
+  }
+
+  @Test
+  public void testTagContentWithPrint() throws Exception {
+    expectRequestETag(null);
+    expectFullResponse();
+    control.replay();
+    
+    response.getWriter().print(RESPONSE_BODY);
+    response.flushBuffer();
+
+    assertResponseHasBody();
+    control.verify();
+  }
+
+  @Test
+  public void testNotModifiedWithPrint() throws Exception {
+    expectRequestETag(GOOD_ETAG);
+    expectNotModifiedResponse();
+    control.replay();
+
+    response.getWriter().print(RESPONSE_BODY);
+    response.flushBuffer();
+
+    assertResponseBodyIsEmpty();
+    control.verify();
+  }
+
+  @Test
+  public void testNonMatchingETagWithPrint() throws Exception {
+    expectRequestETag(BAD_ETAG);
+    expectFullResponse();
+    control.replay();
+
+    response.getWriter().print(RESPONSE_BODY);
+    response.flushBuffer();
+
+    assertResponseHasBody();
+    control.verify();
+  }
+
+  @Test
+  public void testTagContentWithWrite() throws Exception {
+    expectRequestETag(null);
+    expectFullResponse();
+    control.replay();
+
+    response.getOutputStream().write(RESPONSE_BODY_BYTES);
+    response.flushBuffer();
+    
+    assertResponseHasBody();
+    control.verify();
+  }
+
+  @Test
+  public void testNotModifiedWithWrite() throws Exception {
+    expectRequestETag(GOOD_ETAG);
+    expectNotModifiedResponse();
+    control.replay();
+
+    response.getOutputStream().write(RESPONSE_BODY_BYTES);
+    response.flushBuffer();
+    
+    assertResponseBodyIsEmpty();
+    control.verify();
+  }
+
+  @Test
+  public void testNonMatchingETagWithWrite() throws Exception {
+    expectRequestETag(BAD_ETAG);
+    expectFullResponse();
+    control.replay();
+
+    response.getOutputStream().write(RESPONSE_BODY_BYTES);
+    response.flushBuffer();
+    
+    assertResponseHasBody();
+    control.verify();
+  }
+
+  @Test
+  public void testTagEmptyContent() throws Exception {
+    expectRequestETag(null);
+    origResponse.setHeader(ETaggingHttpResponse.RESPONSE_HEADER, EMPTY_CONTENT_ETAG);
+    origResponse.setContentLength(0);
+    control.replay();
+    
+    response.getOutputStream();
+    response.flushBuffer();
+
+    assertEquals(0, stream.getBuffer().length);
+    control.verify();
+  }
+
+  @Test
+  public void testStreamingMode() throws Exception {
+    expectRequestETag(null);
+    control.replay();
+    
+    response.getWriter().print(RESPONSE_BODY);
+    assertEquals(0, stream.getBuffer().length);
+    
+    response.startStreaming();
+    assertArrayEquals(RESPONSE_BODY_BYTES, stream.getBuffer());
+    
+    response.getWriter().print(SECOND_RESPONSE_BODY);
+    assertArrayEquals(AFTER_SECOND_RESPONSE_BODY_BYTES, stream.getBuffer());    
+  }
+
+  private void expectRequestETag(String eTag) {
+    EasyMock.expect(request.getHeader(ETaggingHttpResponse.REQUEST_HEADER)).andReturn(eTag);
+  }
+
+  private void expectFullResponse() {
+    origResponse.setHeader(ETaggingHttpResponse.RESPONSE_HEADER, GOOD_ETAG);
+    origResponse.setContentLength(RESPONSE_BODY_LENGTH);
+  }
+
+  private void expectNotModifiedResponse() {
+    origResponse.setHeader(ETaggingHttpResponse.RESPONSE_HEADER, GOOD_ETAG);
+    origResponse.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+    origResponse.setContentLength(0);
+  }
+
+  private void assertResponseHasBody() {
+    assertArrayEquals(RESPONSE_BODY_BYTES, stream.getBuffer());
+  }
+
+  private void assertResponseBodyIsEmpty() {
+    assertEquals(0, stream.getBuffer().length);
+  }
+
+  private class MockServletOutputStream extends ServletOutputStream {
+    private ByteArrayBuffer buffer = new ByteArrayBuffer(1024);
+
+    @Override
+    public void write(int b) {
+      buffer.append(b);
+    }
+
+    public byte[] getBuffer() {
+      return buffer.toByteArray();
+    }
+  }
+}

Modified: shindig/trunk/java/server/src/main/webapp/WEB-INF/web.xml
URL: http://svn.apache.org/viewvc/shindig/trunk/java/server/src/main/webapp/WEB-INF/web.xml?rev=1051258&r1=1051257&r2=1051258&view=diff
==============================================================================
--- shindig/trunk/java/server/src/main/webapp/WEB-INF/web.xml (original)
+++ shindig/trunk/java/server/src/main/webapp/WEB-INF/web.xml Mon Dec 20 19:41:24 2010
@@ -99,6 +99,11 @@
     <filter-name>authFilter</filter-name>
     <filter-class>org.apache.shindig.auth.AuthenticationServletFilter</filter-class>
   </filter>
+  
+  <filter>
+    <filter-name>etagFilter</filter-name>
+    <filter-class>org.apache.shindig.gadgets.servlet.ETagFilter</filter-class>
+  </filter>
 
 
   <filter-mapping>
@@ -122,6 +127,11 @@
     <url-pattern>/rest/*</url-pattern>
   </filter-mapping>
 
+  <filter-mapping>
+    <filter-name>etagFilter</filter-name>
+    <url-pattern>*</url-pattern>
+  </filter-mapping>
+
   <listener>
     <listener-class>org.apache.shindig.common.servlet.GuiceServletContextListener</listener-class>
   </listener>