You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2017/05/25 20:05:55 UTC

svn commit: r1796186 - in /tomcat/trunk: java/javax/servlet/http/ java/org/apache/catalina/connector/ java/org/apache/coyote/ java/org/apache/coyote/http11/ java/org/apache/coyote/http11/filters/ java/org/apache/coyote/http2/ test/org/apache/catalina/f...

Author: markt
Date: Thu May 25 20:05:55 2017
New Revision: 1796186

URL: http://svn.apache.org/viewvc?rev=1796186&view=rev
Log:
Servlet 4.0
Implement writing trailer headers where the protocol supports it

Added:
    tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/
    tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java   (with props)
Modified:
    tomcat/trunk/java/javax/servlet/http/HttpServletResponse.java
    tomcat/trunk/java/javax/servlet/http/HttpServletResponseWrapper.java
    tomcat/trunk/java/org/apache/catalina/connector/Response.java
    tomcat/trunk/java/org/apache/catalina/connector/ResponseFacade.java
    tomcat/trunk/java/org/apache/coyote/AbstractProcessor.java
    tomcat/trunk/java/org/apache/coyote/ActionCode.java
    tomcat/trunk/java/org/apache/coyote/LocalStrings.properties
    tomcat/trunk/java/org/apache/coyote/Response.java
    tomcat/trunk/java/org/apache/coyote/http11/Http11OutputBuffer.java
    tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java
    tomcat/trunk/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java
    tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java
    tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java
    tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties
    tomcat/trunk/java/org/apache/coyote/http2/Stream.java
    tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java
    tomcat/trunk/test/org/apache/catalina/filters/TesterHttpServletResponse.java
    tomcat/trunk/test/org/apache/coyote/http2/TestStream.java
    tomcat/trunk/webapps/docs/changelog.xml
    tomcat/trunk/webapps/examples/WEB-INF/web.xml
    tomcat/trunk/webapps/examples/servlets/index.html

Modified: tomcat/trunk/java/javax/servlet/http/HttpServletResponse.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/javax/servlet/http/HttpServletResponse.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/javax/servlet/http/HttpServletResponse.java (original)
+++ tomcat/trunk/java/javax/servlet/http/HttpServletResponse.java Thu May 25 20:05:55 2017
@@ -18,6 +18,8 @@ package javax.servlet.http;
 
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Map;
+import java.util.function.Supplier;
 
 import javax.servlet.ServletResponse;
 
@@ -340,6 +342,25 @@ public interface HttpServletResponse ext
      */
     public Collection<String> getHeaderNames();
 
+    /**
+     * Configure the supplier of the trailer headers. The supplier will be
+     * called in the scope of the thread that completes the response.
+     * <b>
+     * Trailers that don't meet the requirements of RFC 7230, section 4.1.2 will
+     * be ignored.
+     *
+     * @param supplier The supplier for the trailer headers
+     *
+     * @throws IllegalStateException if this method is called when the
+     *         underlying protocol does not support trailer headers or if using
+     *         HTTP/1.1 and the response has already been committed
+     *
+     * @since Servlet 4.0
+     */
+    public default void setTrailerFields(Supplier<Map<String, String>> supplier) {
+        // NO-OP
+    }
+
     /*
      * Server status codes; see RFC 2068.
      */

Modified: tomcat/trunk/java/javax/servlet/http/HttpServletResponseWrapper.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/javax/servlet/http/HttpServletResponseWrapper.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/javax/servlet/http/HttpServletResponseWrapper.java (original)
+++ tomcat/trunk/java/javax/servlet/http/HttpServletResponseWrapper.java Thu May 25 20:05:55 2017
@@ -18,6 +18,8 @@ package javax.servlet.http;
 
 import java.io.IOException;
 import java.util.Collection;
+import java.util.Map;
+import java.util.function.Supplier;
 
 import javax.servlet.ServletResponseWrapper;
 
@@ -266,4 +268,18 @@ public class HttpServletResponseWrapper
     public Collection<String> getHeaderNames() {
         return this._getHttpServletResponse().getHeaderNames();
     }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The default implementation is to call
+     * {@link HttpServletResponse#setTrailerFields(Supplier)}
+     * on the wrapper {@link HttpServletResponse}.
+     *
+     * @since Servlet 4.0
+     */
+    @Override
+    public void setTrailerFields(Supplier<Map<String, String>> supplier) {
+        this._getHttpServletResponse().setTrailerFields(supplier);
+    }
 }

Modified: tomcat/trunk/java/org/apache/catalina/connector/Response.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/connector/Response.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/connector/Response.java (original)
+++ tomcat/trunk/java/org/apache/catalina/connector/Response.java Thu May 25 20:05:55 2017
@@ -33,9 +33,11 @@ import java.util.Collection;
 import java.util.Enumeration;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.TimeZone;
 import java.util.Vector;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
 
 import javax.servlet.ServletOutputStream;
 import javax.servlet.ServletResponse;
@@ -1152,6 +1154,12 @@ public class Response implements HttpSer
     }
 
 
+    @Override
+    public void setTrailerFields(Supplier<Map<String, String>> supplier) {
+        getCoyoteResponse().setTrailerFields(supplier);
+    }
+
+
     /**
      * Encode the session identifier associated with this response
      * into the specified redirect URL, if necessary.

Modified: tomcat/trunk/java/org/apache/catalina/connector/ResponseFacade.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/connector/ResponseFacade.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/connector/ResponseFacade.java (original)
+++ tomcat/trunk/java/org/apache/catalina/connector/ResponseFacade.java Thu May 25 20:05:55 2017
@@ -24,6 +24,8 @@ import java.security.PrivilegedActionExc
 import java.security.PrivilegedExceptionAction;
 import java.util.Collection;
 import java.util.Locale;
+import java.util.Map;
+import java.util.function.Supplier;
 
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.Cookie;
@@ -640,4 +642,10 @@ public class ResponseFacade implements H
     public Collection<String> getHeaders(String name) {
         return response.getHeaders(name);
     }
+
+
+    @Override
+    public void setTrailerFields(Supplier<Map<String, String>> supplier) {
+        response.setTrailerFields(supplier);
+    }
 }

Modified: tomcat/trunk/java/org/apache/coyote/AbstractProcessor.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/AbstractProcessor.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/AbstractProcessor.java (original)
+++ tomcat/trunk/java/org/apache/coyote/AbstractProcessor.java Thu May 25 20:05:55 2017
@@ -492,6 +492,11 @@ public abstract class AbstractProcessor
             result.set(isTrailerFieldsReady());
             break;
         }
+        case IS_TRAILER_FIELDS_SUPPORTED: {
+            AtomicBoolean result = (AtomicBoolean) param;
+            result.set(isTrailerFieldsSupported());
+            break;
+        }
         }
     }
 
@@ -770,6 +775,18 @@ public abstract class AbstractProcessor
 
 
     /**
+     * Protocols that support trailer fields should override this method and
+     * return {@code true}.
+     *
+     * @return {@code true} if trailer fields are supported by this processor,
+     *         otherwise {@code false}.
+     */
+    protected boolean isTrailerFieldsSupported() {
+        return false;
+    }
+
+
+    /**
      * Flush any pending writes. Used during non-blocking writes to flush any
      * remaining data from a previous incomplete write.
      *

Modified: tomcat/trunk/java/org/apache/coyote/ActionCode.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/ActionCode.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/ActionCode.java (original)
+++ tomcat/trunk/java/org/apache/coyote/ActionCode.java Thu May 25 20:05:55 2017
@@ -252,5 +252,12 @@ public enum ActionCode {
      * true if it is known that request trailer fields are not supported so an
      * empty collection of trailers can then be read.
      */
-    IS_TRAILER_FIELDS_READY
+    IS_TRAILER_FIELDS_READY,
+
+    /**
+     * Are HTTP trailer fields supported for the current response? Note that
+     * once an HTTP/1.1 response has been committed, it will no longer support
+     * trailer fields.
+     */
+    IS_TRAILER_FIELDS_SUPPORTED
 }

Modified: tomcat/trunk/java/org/apache/coyote/LocalStrings.properties
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/LocalStrings.properties?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/LocalStrings.properties (original)
+++ tomcat/trunk/java/org/apache/coyote/LocalStrings.properties Thu May 25 20:05:55 2017
@@ -49,5 +49,6 @@ request.readListenerSet=The non-blocking
 response.encoding.invalid=The encoding [{0}] is not recognised by the JRE
 response.notAsync=It is only valid to switch to non-blocking IO within async processing or HTTP upgrade processing
 response.notNonBlocking=It is invalid to call isReady() when the response has not been put into non-blocking mode
+response.noTrailers.notSupported=A trailer fields supplier may not be set for this response. Either the underlying protocol does not support trailer fields or the protocol requires that the supplier is set before the response is committed
 response.nullWriteListener=The listener passed to setWriteListener() may not be null
 response.writeListenerSet=The non-blocking write listener has already been set

Modified: tomcat/trunk/java/org/apache/coyote/Response.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/Response.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/Response.java (original)
+++ tomcat/trunk/java/org/apache/coyote/Response.java Thu May 25 20:05:55 2017
@@ -22,7 +22,9 @@ import java.io.UnsupportedEncodingExcept
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
 import java.util.Locale;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
 
 import javax.servlet.WriteListener;
 
@@ -78,6 +80,8 @@ public final class Response {
     final MimeHeaders headers = new MimeHeaders();
 
 
+    private Supplier<Map<String,String>> trailerFieldsSupplier = null;
+
     /**
      * Associated output buffer.
      */
@@ -322,6 +326,22 @@ public final class Response {
     }
 
 
+    public void setTrailerFields(Supplier<Map<String, String>> supplier) {
+        AtomicBoolean trailerFieldsSupported = new AtomicBoolean(false);
+        action(ActionCode.IS_TRAILER_FIELDS_SUPPORTED, trailerFieldsSupported);
+        if (!trailerFieldsSupported.get()) {
+            throw new IllegalStateException(sm.getString("response.noTrailers.notSupported"));
+        }
+
+        this.trailerFieldsSupplier = supplier;
+    }
+
+
+    public Supplier<Map<String, String>> getTrailerFields() {
+        return trailerFieldsSupplier;
+    }
+
+
     /**
      * Set internal fields for special header names.
      * Called from set/addHeader.
@@ -530,6 +550,7 @@ public final class Response {
         commitTime = -1;
         errorException = null;
         headers.clear();
+        trailerFieldsSupplier = null;
         // Servlet 3.1 non-blocking write listener
         listener = null;
         fireListener = false;

Modified: tomcat/trunk/java/org/apache/coyote/http11/Http11OutputBuffer.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/Http11OutputBuffer.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http11/Http11OutputBuffer.java (original)
+++ tomcat/trunk/java/org/apache/coyote/http11/Http11OutputBuffer.java Thu May 25 20:05:55 2017
@@ -524,6 +524,16 @@ public class Http11OutputBuffer implemen
     }
 
 
+    boolean isChunking() {
+        for (int i = 0; i < lastActiveFilter; i++) {
+            if (activeFilters[i] == filterLibrary[Constants.CHUNKED_FILTER]) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
     // ------------------------------------------ SocketOutputBuffer Inner Class
 
     /**

Modified: tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java (original)
+++ tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java Thu May 25 20:05:55 2017
@@ -900,23 +900,25 @@ public class Http11Processor extends Abs
 
         long contentLength = response.getContentLengthLong();
         boolean connectionClosePresent = false;
-        if (contentLength != -1) {
+        if (http11 && response.getTrailerFields() != null) {
+            // If trailer fields are set, always use chunking
+            outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]);
+            contentDelimitation = true;
+            headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED);
+        } else if (contentLength != -1) {
             headers.setValue("Content-Length").setLong(contentLength);
-            outputBuffer.addActiveFilter
-                (outputFilters[Constants.IDENTITY_FILTER]);
+            outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]);
             contentDelimitation = true;
         } else {
             // If the response code supports an entity body and we're on
             // HTTP 1.1 then we chunk unless we have a Connection: close header
             connectionClosePresent = isConnectionClose(headers);
-            if (entityBody && http11 && !connectionClosePresent) {
-                outputBuffer.addActiveFilter
-                    (outputFilters[Constants.CHUNKED_FILTER]);
+            if (http11 && entityBody && !connectionClosePresent) {
+                outputBuffer.addActiveFilter(outputFilters[Constants.CHUNKED_FILTER]);
                 contentDelimitation = true;
                 headers.addValue(Constants.TRANSFERENCODING).setString(Constants.CHUNKED);
             } else {
-                outputBuffer.addActiveFilter
-                    (outputFilters[Constants.IDENTITY_FILTER]);
+                outputBuffer.addActiveFilter(outputFilters[Constants.IDENTITY_FILTER]);
             }
         }
 
@@ -1317,6 +1319,24 @@ public class Http11Processor extends Abs
     }
 
 
+    @Override
+    protected boolean isTrailerFieldsSupported() {
+        // Request must be HTTP/1.1 to support trailer fields
+        if (!http11) {
+            return false;
+        }
+
+        // If the response is not yet committed, chunked encoding can be used
+        // and the trailer fields sent
+        if (!response.isCommitted()) {
+            return true;
+        }
+
+        // Response has been committed - need to see if chunked is being used
+        return outputBuffer.isChunking();
+    }
+
+
     /**
      * Trigger sendfile processing if required.
      *

Modified: tomcat/trunk/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java (original)
+++ tomcat/trunk/java/org/apache/coyote/http11/filters/ChunkedOutputFilter.java Thu May 25 20:05:55 2017
@@ -17,12 +17,20 @@
 package org.apache.coyote.http11.filters;
 
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Supplier;
 
 import org.apache.coyote.OutputBuffer;
 import org.apache.coyote.Response;
 import org.apache.coyote.http11.OutputFilter;
 import org.apache.tomcat.util.buf.HexUtils;
+import org.apache.tomcat.util.http.fileupload.ByteArrayOutputStream;
 
 /**
  * Chunked output filter.
@@ -31,9 +39,30 @@ import org.apache.tomcat.util.buf.HexUti
  */
 public class ChunkedOutputFilter implements OutputFilter {
 
+    private static final byte[] LAST_CHUNK_BYTES = {(byte) '0', (byte) '\r', (byte) '\n'};
+    private static final byte[] CRLF_BYTES = {(byte) '\r', (byte) '\n'};
     private static final byte[] END_CHUNK_BYTES =
         {(byte) '0', (byte) '\r', (byte) '\n', (byte) '\r', (byte) '\n'};
 
+    private static final Set<String> disallowedTrailerFieldNames = new HashSet<>();
+
+    static {
+        // Always add these in lower case
+        disallowedTrailerFieldNames.add("age");
+        disallowedTrailerFieldNames.add("cache-control");
+        disallowedTrailerFieldNames.add("content-length");
+        disallowedTrailerFieldNames.add("content-encoding");
+        disallowedTrailerFieldNames.add("content-range");
+        disallowedTrailerFieldNames.add("content-type");
+        disallowedTrailerFieldNames.add("date");
+        disallowedTrailerFieldNames.add("expires");
+        disallowedTrailerFieldNames.add("location");
+        disallowedTrailerFieldNames.add("retry-after");
+        disallowedTrailerFieldNames.add("trailer");
+        disallowedTrailerFieldNames.add("transfer-encoding");
+        disallowedTrailerFieldNames.add("vary");
+        disallowedTrailerFieldNames.add("warning");
+    }
 
     /**
      * Next buffer in the pipeline.
@@ -47,12 +76,17 @@ public class ChunkedOutputFilter impleme
     protected final ByteBuffer chunkHeader = ByteBuffer.allocate(10);
 
 
+    protected final ByteBuffer lastChunk = ByteBuffer.wrap(LAST_CHUNK_BYTES);
+    protected final ByteBuffer crlfChunk = ByteBuffer.wrap(CRLF_BYTES);
     /**
      * End chunk.
      */
     protected final ByteBuffer endChunk = ByteBuffer.wrap(END_CHUNK_BYTES);
 
 
+    private Response response;
+
+
     public ChunkedOutputFilter() {
         chunkHeader.put(8, (byte) '\r');
         chunkHeader.put(9, (byte) '\n');
@@ -112,7 +146,7 @@ public class ChunkedOutputFilter impleme
      */
     @Override
     public void setResponse(Response response) {
-        // NOOP: No need for parameters from response in this filter
+        this.response = response;
     }
 
 
@@ -132,9 +166,40 @@ public class ChunkedOutputFilter impleme
     @Override
     public long end() throws IOException {
 
-        // Write end chunk
-        buffer.doWrite(endChunk);
-        endChunk.position(0).limit(endChunk.capacity());
+        Supplier<Map<String,String>> trailerFieldsSupplier = response.getTrailerFields();
+        Map<String,String> trailerFields = null;
+
+        if (trailerFieldsSupplier != null) {
+            trailerFields = trailerFieldsSupplier.get();
+        }
+
+        if (trailerFields == null) {
+            // Write end chunk
+            buffer.doWrite(endChunk);
+            endChunk.position(0).limit(endChunk.capacity());
+        } else {
+            buffer.doWrite(lastChunk);
+            lastChunk.position(0).limit(lastChunk.capacity());
+
+           ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
+           OutputStreamWriter osw = new OutputStreamWriter(baos, StandardCharsets.ISO_8859_1);
+            for (Map.Entry<String,String> trailerField : trailerFields.entrySet()) {
+                // Ignore disallowed headers
+                if (disallowedTrailerFieldNames.contains(
+                        trailerField.getKey().toLowerCase(Locale.ENGLISH))) {
+                    continue;
+                }
+                osw.write(trailerField.getKey());
+                osw.write(':');
+                osw.write(trailerField.getValue());
+                osw.write("\r\n");
+            }
+            osw.close();
+            buffer.doWrite(ByteBuffer.wrap(baos.toByteArray()));
+
+            buffer.doWrite(crlfChunk);
+            crlfChunk.position(0).limit(crlfChunk.capacity());
+        }
 
         return 0;
     }
@@ -145,6 +210,6 @@ public class ChunkedOutputFilter impleme
      */
     @Override
     public void recycle() {
-        // NOOP: Nothing to recycle
+        response = null;
     }
 }

Modified: tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java (original)
+++ tomcat/trunk/java/org/apache/coyote/http2/Http2AsyncUpgradeHandler.java Thu May 25 20:05:55 2017
@@ -136,6 +136,9 @@ public class Http2AsyncUpgradeHandler ex
     void writeHeaders(Stream stream, int pushedStreamId, MimeHeaders mimeHeaders,
             boolean endOfStream, int payloadSize) throws IOException {
         doWriteHeaders(stream, pushedStreamId, mimeHeaders, endOfStream, payloadSize);
+        if (endOfStream) {
+            stream.sentEndOfStream();
+        }
     }
 
 

Modified: tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java (original)
+++ tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java Thu May 25 20:05:55 2017
@@ -531,6 +531,9 @@ class Http2UpgradeHandler extends Abstra
         synchronized (socketWrapper) {
             doWriteHeaders(stream, pushedStreamId, mimeHeaders, endOfStream, payloadSize);
         }
+        if (endOfStream) {
+            stream.sentEndOfStream();
+        }
     }
 
 

Modified: tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties (original)
+++ tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties Thu May 25 20:05:55 2017
@@ -143,7 +143,7 @@ upgradeHandler.windowSizeTooBig=Connecti
 upgradeHandler.windowSizeReservationInterrupted=Connection [{0}], Stream [{1}], reservation for [{2}] bytes
 upgradeHandler.writeBody=Connection [{0}], Stream [{1}], Data length [{2}]
 upgradeHandler.writeHeaders=Connection [{0}], Stream [{1}]
-upgradeHandler.writePushHeaders=Connection [{0}], Stream [{1}], Pushed stream [{2}]
+upgradeHandler.writePushHeaders=Connection [{0}], Stream [{1}], Pushed stream [{2}], EndOfStream [{3}]
 
 writeStateMachine.endWrite.ise=It is illegal to specify [{0}] for the new state once a write has completed
 writeStateMachine.ise=It is illegal to call [{0}()] in state [{1}]
\ No newline at end of file

Modified: tomcat/trunk/java/org/apache/coyote/http2/Stream.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/Stream.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http2/Stream.java (original)
+++ tomcat/trunk/java/org/apache/coyote/http2/Stream.java Thu May 25 20:05:55 2017
@@ -22,8 +22,11 @@ import java.nio.charset.StandardCharsets
 import java.security.AccessController;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.Locale;
+import java.util.Map;
+import java.util.function.Supplier;
 
 import org.apache.coyote.ActionCode;
 import org.apache.coyote.CloseNowException;
@@ -35,6 +38,7 @@ import org.apache.coyote.http2.HpackDeco
 import org.apache.juli.logging.Log;
 import org.apache.juli.logging.LogFactory;
 import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.buf.MessageBytes;
 import org.apache.tomcat.util.http.FastHttpDateFormat;
 import org.apache.tomcat.util.http.MimeHeaders;
 import org.apache.tomcat.util.net.ApplicationBufferHandler;
@@ -392,11 +396,42 @@ class Stream extends AbstractStream impl
 
     final void writeHeaders() throws IOException {
         prepareHeaders(coyoteResponse);
-        boolean endOfStream = getOutputBuffer().hasNoBody();
+        boolean endOfStream = getOutputBuffer().hasNoBody() &&
+                coyoteResponse.getTrailerFields() == null;
         // TODO: Is 1k the optimal value?
         handler.writeHeaders(this, 0, coyoteResponse.getMimeHeaders(), endOfStream, 1024);
     }
 
+
+    final void writeTrailers() throws IOException {
+        Supplier<Map<String,String>> supplier = coyoteResponse.getTrailerFields();
+        if (supplier == null) {
+            // No supplier was set, end of stream will already have been sent
+            return;
+        }
+
+        // We can re-use the MimeHeaders from the response since they have
+        // already been processed by the encoder at this point
+        MimeHeaders mimeHeaders = coyoteResponse.getMimeHeaders();
+        mimeHeaders.recycle();
+
+        Map<String,String> headerMap = supplier.get();
+        if (headerMap == null) {
+            headerMap = Collections.emptyMap();
+        }
+
+        // Copy the contents of the Map to the MimeHeaders
+        // TODO: Is there benefit in refactoring this? Is MimeHeaders too
+        //       heavyweight? Can we reduce the copy/conversions?
+        for (Map.Entry<String, String> headerEntry : headerMap.entrySet()) {
+            MessageBytes mb = mimeHeaders.addValue(headerEntry.getKey());
+            mb.setString(headerEntry.getValue());
+        }
+
+        handler.writeHeaders(this, 0, mimeHeaders, true, 1024);
+    }
+
+
     final void writeAck() throws IOException {
         // TODO: Is 64 too big? Just the status header with compression
         handler.writeHeaders(this, 0, ACK_HEADERS, false, 64);
@@ -586,12 +621,17 @@ class Stream extends AbstractStream impl
     }
 
 
-    public boolean isTrailerFieldsReady() {
+    boolean isTrailerFieldsReady() {
         // Once EndOfStream has been received, canRead will be false
         return !state.canRead();
     }
 
 
+    boolean isTrailerFieldsSupported() {
+        return !getOutputBuffer().endOfStreamSent;
+    }
+
+
     private static void push(final Http2UpgradeHandler handler, final Request request,
             final Stream stream) throws IOException {
         if (org.apache.coyote.Constants.IS_SECURITY_ENABLED) {
@@ -716,7 +756,8 @@ class Stream extends AbstractStream impl
                 if (closed && !endOfStreamSent) {
                     // Handling this special case here is simpler than trying
                     // to modify the following code to handle it.
-                    handler.writeBody(Stream.this, buffer, 0, true);
+                    handler.writeBody(Stream.this, buffer, 0,
+                            coyoteResponse.getTrailerFields() == null);
                 }
                 // Buffer is empty. Nothing to do.
                 return false;
@@ -735,7 +776,8 @@ class Stream extends AbstractStream impl
                                 handler.reserveWindowSize(Stream.this, streamReservation);
                     // Do the write
                     handler.writeBody(Stream.this, buffer, connectionReservation,
-                            !writeInProgress && closed && left == connectionReservation);
+                            !writeInProgress && closed && left == connectionReservation &&
+                            coyoteResponse.getTrailerFields() == null);
                     streamReservation -= connectionReservation;
                     left -= connectionReservation;
                 }
@@ -760,6 +802,7 @@ class Stream extends AbstractStream impl
         final void close() throws IOException {
             closed = true;
             flushData();
+            writeTrailers();
         }
 
         /**

Modified: tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java (original)
+++ tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java Thu May 25 20:05:55 2017
@@ -190,7 +190,7 @@ class StreamProcessor extends AbstractPr
         synchronized (this) {
             /*
              * TODO Check if this sync is necessary.
-             *      Compare with superrclass that uses SocketWrapper
+             *      Compare with superclass that uses SocketWrapper
              */
             while (dispatches != null && dispatches.hasNext()) {
                 DispatchType dispatchType = dispatches.next();
@@ -223,6 +223,12 @@ class StreamProcessor extends AbstractPr
     }
 
 
+    @Override
+    protected boolean isTrailerFieldsSupported() {
+        return stream.isTrailerFieldsSupported();
+    }
+
+
     @Override
     public final void recycle() {
         // StreamProcessor instances are not re-used.

Modified: tomcat/trunk/test/org/apache/catalina/filters/TesterHttpServletResponse.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/filters/TesterHttpServletResponse.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/test/org/apache/catalina/filters/TesterHttpServletResponse.java (original)
+++ tomcat/trunk/test/org/apache/catalina/filters/TesterHttpServletResponse.java Thu May 25 20:05:55 2017
@@ -24,6 +24,8 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
+import java.util.function.Supplier;
 
 import javax.servlet.ServletOutputStream;
 import javax.servlet.ServletResponse;
@@ -247,4 +249,6 @@ public class TesterHttpServletResponse i
     public void setStatus(int status, String message) {/* NOOP */}
     @Override
     public void setContentLengthLong(long length) {/* NOOP */}
+    @Override
+    public void setTrailerFields(Supplier<Map<String, String>> supplier) { /* NOOP */ }
 }

Modified: tomcat/trunk/test/org/apache/coyote/http2/TestStream.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/coyote/http2/TestStream.java?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/test/org/apache/coyote/http2/TestStream.java (original)
+++ tomcat/trunk/test/org/apache/coyote/http2/TestStream.java Thu May 25 20:05:55 2017
@@ -30,6 +30,9 @@ import org.junit.Test;
 import org.apache.catalina.Context;
 import org.apache.catalina.startup.Tomcat;
 
+import trailers.ResponseTrailers;
+
+
 public class TestStream extends Http2TestBase {
 
     /*
@@ -73,6 +76,53 @@ public class TestStream extends Http2Tes
                 "3-EndOfStream\n", output.getTrace());
     }
 
+
+    @Test
+    public void testResponseTrailerFields() throws Exception {
+
+        enableHttp2();
+
+        Tomcat tomcat = getTomcatInstance();
+
+        Context ctxt = tomcat.addContext("", null);
+        Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
+        ctxt.addServletMappingDecoded("/simple", "simple");
+        Tomcat.addServlet(ctxt, "trailers", new ResponseTrailers());
+        ctxt.addServletMappingDecoded("/trailers", "trailers");
+
+        tomcat.start();
+
+        openClientConnection();
+        doHttpUpgrade();
+        sendClientPreface();
+        validateHttp2InitialResponse();
+
+        byte[] frameHeader = new byte[9];
+        ByteBuffer headersPayload = ByteBuffer.allocate(128);
+        buildGetRequest(frameHeader, headersPayload, null, 3, "/trailers");
+        writeFrame(frameHeader, headersPayload);
+
+        // Headers
+        parser.readFrame(true);
+        // Body
+        parser.readFrame(true);
+        // Trailers
+        parser.readFrame(true);
+
+        Assert.assertEquals(
+                "3-HeadersStart\n" +
+                "3-Header-[:status]-[200]\n" +
+                "3-Header-[content-type]-[text/plain;charset=UTF-8]\n" +
+                "3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n" +
+                "3-HeadersEnd\n" +
+                "3-Body-43\n" +
+                "3-HeadersStart\n" +
+                "3-Header-[x-trailer-2]-[Trailer value two]\n" +
+                "3-Header-[x-trailer-1]-[Trailer value one]\n" +
+                "3-HeadersEnd\n" +
+                "3-EndOfStream\n", output.getTrace());
+    }
+
 
     private static final class PathParam extends HttpServlet {
 

Modified: tomcat/trunk/webapps/docs/changelog.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/webapps/docs/changelog.xml (original)
+++ tomcat/trunk/webapps/docs/changelog.xml Thu May 25 20:05:55 2017
@@ -61,6 +61,10 @@
         <bug>61101</bug>: CORS filter should set Vary header in response.
         Submitted by Rick Riemer. (remm)
       </fix>
+      <update>
+        Update the Servlet 4.0 implementation to add support for setting
+        trailer fields for HTTP responses. (markt)
+      </update>
     </changelog>
   </subsection>
   <subsection name="Coyote">

Added: tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java?rev=1796186&view=auto
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java (added)
+++ tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java Thu May 25 20:05:55 2017
@@ -0,0 +1,68 @@
+/*
+ * 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 trailers;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Supplier;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * This example writes some trailer fields to the HTTP response.
+ */
+public class ResponseTrailers extends HttpServlet {
+
+    private static final long serialVersionUID = 1L;
+    private static final Supplier<Map<String,String>> TRAILER_FIELD_SUPPLIER =
+            new TrailerFieldSupplier();
+
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
+            throws ServletException, IOException {
+
+        resp.setTrailerFields(TRAILER_FIELD_SUPPLIER);
+        resp.setContentType("text/plain");
+        resp.setCharacterEncoding("UTF-8");
+
+        PrintWriter pw  = resp.getWriter();
+
+        pw.print("This reponse should include trailer fields.");
+    }
+
+
+    private static class TrailerFieldSupplier implements Supplier<Map<String,String>> {
+
+        private static final Map<String,String> trailerFields = new HashMap<>();
+
+        static {
+            trailerFields.put("x-trailer-1", "Trailer value one");
+            trailerFields.put("x-trailer-2", "Trailer value two");
+        }
+
+        @Override
+        public Map<String, String> get() {
+            return trailerFields;
+        }
+    }
+}

Propchange: tomcat/trunk/webapps/examples/WEB-INF/classes/trailers/ResponseTrailers.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: tomcat/trunk/webapps/examples/WEB-INF/web.xml
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/web.xml?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/webapps/examples/WEB-INF/web.xml (original)
+++ tomcat/trunk/webapps/examples/WEB-INF/web.xml Thu May 25 20:05:55 2017
@@ -389,6 +389,16 @@
       <url-pattern>/servlets/serverpush/simpleimage</url-pattern>
     </servlet-mapping>
 
+    <!-- Trailer examples -->
+    <servlet>
+      <servlet-name>responsetrailer</servlet-name>
+      <servlet-class>trailers.ResponseTrailers</servlet-class>
+    </servlet>
+    <servlet-mapping>
+      <servlet-name>responsetrailer</servlet-name>
+      <url-pattern>/servlets/trailers/response</url-pattern>
+    </servlet-mapping>
+
     <welcome-file-list>
         <welcome-file>index.html</welcome-file>
         <welcome-file>index.xhtml</welcome-file>

Modified: tomcat/trunk/webapps/examples/servlets/index.html
URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/servlets/index.html?rev=1796186&r1=1796185&r2=1796186&view=diff
==============================================================================
--- tomcat/trunk/webapps/examples/servlets/index.html (original)
+++ tomcat/trunk/webapps/examples/servlets/index.html Thu May 25 20:05:55 2017
@@ -176,6 +176,17 @@ for clarity.</p>
   <td style="width: 30%;"></td>
 </tr>
 
+<tr>
+  <th colspan="3">Servlet 4.0 Trailer Field examples</th>
+</tr>
+<tr>
+  <td>Response trailer fields</td>
+  <td style="width: 30%;">
+    <a href="trailers/response"><img src="images/execute.gif" alt=""> Execute</a>
+  </td>
+  <td style="width: 30%;"></td>
+</tr>
+
 </table>
 
 </body>



---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


Re: svn commit: r1796186 - in /tomcat/trunk: java/javax/servlet/http/ java/org/apache/catalina/connector/ java/org/apache/coyote/ java/org/apache/coyote/http11/ java/org/apache/coyote/http11/filters/ java/org/apache/coyote/http2/ test/org/apache/catalina/f...

Posted by Josh Soref <js...@gmail.com>.
mark wrote:
> -             *      Compare with superrclass that uses SocketWrapper
> +             *      Compare with superclass that uses SocketWrapper

Fwiw, I finally posted my spelling scripts at https://github.com/jsoref/spelling

If you wanted to, you could have a script that did:
```sh
mv repo.words repo.old;
f repo > repo.words
diff -U0 repo.old repo.words|perl -ne 'next unless s/^\+([^+])/$1/; print'
```
It doesn't really matter if you run the script after every commit or
between releases. The result will show you only the newly added
"tokens"

For what it's worth, if you ran that script against before/after this
commit, you'd see that `reponse` was added:
+        pw.print("This reponse should include trailer fields.");

---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org