Author: markt Date: Wed Oct 14 14:29:31 2015 New Revision: 1708605 URL: http://svn.apache.org/viewvc?rev=1708605&view=rev Log: Implement a very basic (you can only specify the path at the moment) server push mechanism. Add an example to the examples web app that shows how to use it.
Added: tomcat/trunk/webapps/examples/WEB-INF/classes/http2/ tomcat/trunk/webapps/examples/WEB-INF/classes/http2/SimpleImagePush.java (with props) Modified: tomcat/trunk/java/javax/servlet/http/PushBuilder.java tomcat/trunk/java/org/apache/catalina/core/ApplicationPushBuilder.java tomcat/trunk/java/org/apache/catalina/core/LocalStrings.properties tomcat/trunk/java/org/apache/coyote/ActionCode.java tomcat/trunk/java/org/apache/coyote/ajp/AjpProcessor.java tomcat/trunk/java/org/apache/coyote/ajp/LocalStrings.properties tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java tomcat/trunk/java/org/apache/coyote/http11/LocalStrings.properties 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/webapps/examples/WEB-INF/web.xml tomcat/trunk/webapps/examples/servlets/index.html Modified: tomcat/trunk/java/javax/servlet/http/PushBuilder.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/javax/servlet/http/PushBuilder.java?rev=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/javax/servlet/http/PushBuilder.java (original) +++ tomcat/trunk/java/javax/servlet/http/PushBuilder.java Wed Oct 14 14:29:31 2015 @@ -18,9 +18,32 @@ package javax.servlet.http; /** * Builds a push request based on the {@link HttpServletRequest} from which this - * builder was obtained. + * builder was obtained. The push request will be constructed on the following + * basis: + * <ul> + * <li>The request method is set to <code>GET</code></li> + * <li>The path will not be set. This must be set explicitly via a call to + * {@link #setPath(String)}</li> + * </ul> * * @since Servlet 4.0 */ public interface PushBuilder { + + /** + * Sets the URI path to be used for the push request. This must be called + * before every call to {@link #push()}. If the path includes a query + * string, the query string will be appended to the existing query string + * (if any) and no de-duplication will occur. + * + * @param path Paths beginning with '/' are treated as absolute paths. All + * other paths are treated as relative to the context path of + * the request used to create this builder instance. The path + * may include a query string. + * + * @return This builder instance + */ + PushBuilder setPath(String path); + + void push(); } Modified: tomcat/trunk/java/org/apache/catalina/core/ApplicationPushBuilder.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/core/ApplicationPushBuilder.java?rev=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/core/ApplicationPushBuilder.java (original) +++ tomcat/trunk/java/org/apache/catalina/core/ApplicationPushBuilder.java Wed Oct 14 14:29:31 2015 @@ -16,14 +16,73 @@ */ package org.apache.catalina.core; +import javax.servlet.ServletRequest; +import javax.servlet.ServletRequestWrapper; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.PushBuilder; +import org.apache.catalina.connector.Request; +import org.apache.coyote.ActionCode; +import org.apache.tomcat.util.res.StringManager; + public class ApplicationPushBuilder implements PushBuilder { + private static final StringManager sm = StringManager.getManager(ApplicationPushBuilder.class); + private final HttpServletRequest baseRequest; + private final org.apache.coyote.Request coyoteRequest; + + private String path; public ApplicationPushBuilder(HttpServletRequest request) { baseRequest = request; + // Need a reference to the CoyoteRequest in order to process the push + ServletRequest current = request; + while (current instanceof ServletRequestWrapper) { + current = ((ServletRequestWrapper) current).getRequest(); + } + if (current instanceof Request) { + coyoteRequest = ((Request) current).getCoyoteRequest(); + } else { + throw new UnsupportedOperationException(sm.getString( + "applicationPushBuilder.noCoyoteRequest", current.getClass().getName())); + } + } + + + @Override + public PushBuilder setPath(String path) { + if (path.startsWith("/")) { + this.path = path; + } else { + String contextPath = baseRequest.getContextPath(); + int len = contextPath.length() + path.length() + 1; + StringBuilder sb = new StringBuilder(len); + sb.append(contextPath); + sb.append('/'); + sb.append(path); + this.path = sb.toString(); + } + return this; + } + + + @Override + public void push() { + org.apache.coyote.Request pushTarget = new org.apache.coyote.Request(); + + pushTarget.method().setString("GET"); + // The next three are implied by the Javadoc getPath() + pushTarget.serverName().setString(baseRequest.getServerName()); + pushTarget.setServerPort(baseRequest.getServerPort()); + pushTarget.scheme().setString(baseRequest.getScheme()); + + pushTarget.requestURI().setString(path); + pushTarget.decodedURI().setString(path); + + // TODO Copy across / set other required attributes + + coyoteRequest.action(ActionCode.PUSH_REQUEST, pushTarget); + pushTarget = null; } } Modified: tomcat/trunk/java/org/apache/catalina/core/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/core/LocalStrings.properties?rev=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/catalina/core/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/catalina/core/LocalStrings.properties Wed Oct 14 14:29:31 2015 @@ -48,6 +48,9 @@ applicationFilterConfig.jmxUnregisterFai applicationFilterConfig.release=Failed to destroy the filter named [{0}] of type [{1}] applicationFilterRegistration.nullInitParam=Unable to set initialisation parameter for filter due to null name and/or value. Name [{0}], Value [{1}] applicationFilterRegistration.nullInitParams=Unable to set initialisation parameters for filter due to null name and/or value. Name [{0}], Value [{1}] + +applicationPushBuilder.noCoyoteRequest=Unable to find the underlying Coyote request object (which is required to create a push request) from the request of type [{0}] + applicationServletRegistration.setServletSecurity.iae=Null constraint specified for servlet [{0}] deployed to context with name [{1}] applicationServletRegistration.setServletSecurity.ise=Security constraints can't be added to servlet [{0}] deployed to context with name [{1}] as the context has already been initialised applicationSessionCookieConfig.ise=Property {0} cannot be added to SessionCookieConfig for context {1} as the context has been initialised Modified: tomcat/trunk/java/org/apache/coyote/ActionCode.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/ActionCode.java?rev=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/ActionCode.java (original) +++ tomcat/trunk/java/org/apache/coyote/ActionCode.java Wed Oct 14 14:29:31 2015 @@ -236,5 +236,10 @@ public enum ActionCode { * Trigger end of request processing (remaining input swallowed, write any * remaining parts of the response etc.). */ - END_REQUEST + END_REQUEST, + + /** + * Push a request on behalf of the client of the current request. + */ + PUSH_REQUEST } Modified: tomcat/trunk/java/org/apache/coyote/ajp/AjpProcessor.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/ajp/AjpProcessor.java?rev=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/ajp/AjpProcessor.java (original) +++ tomcat/trunk/java/org/apache/coyote/ajp/AjpProcessor.java Wed Oct 14 14:29:31 2015 @@ -594,6 +594,13 @@ public class AjpProcessor extends Abstra // NO-OP for AJP break; } + + // Servlet 4.0 Push requests + case PUSH_REQUEST: { + // HTTP2 connections only. Unsupported for AJP. + throw new UnsupportedOperationException( + sm.getString("ajpprocessor.pushrequest.notsupported")); + } } } Modified: tomcat/trunk/java/org/apache/coyote/ajp/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/ajp/LocalStrings.properties?rev=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/ajp/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/coyote/ajp/LocalStrings.properties Wed Oct 14 14:29:31 2015 @@ -27,6 +27,7 @@ ajpprocessor.request.prepare=Error prepa ajpprocessor.request.process=Error processing request ajpprocessor.certs.fail=Certificate conversion failed ajpprocessor.httpupgrade.notsupported=HTTP upgrade is not supported by the AJP protocol +ajpprocessor.pushrequest.notsupported=Server push requests are not supported by the AJP protocol ajpmessage.null=Cannot append null value ajpmessage.overflow=Overflow error for buffer adding {0} bytes at position {1} 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=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java (original) +++ tomcat/trunk/java/org/apache/coyote/http11/Http11Processor.java Wed Oct 14 14:29:31 2015 @@ -904,6 +904,13 @@ public class Http11Processor extends Abs endRequest(); break; } + + // Servlet 4.0 Push requests + case PUSH_REQUEST: { + // HTTP2 connections only. Unsupported for AJP. + throw new UnsupportedOperationException( + sm.getString("http11processor.pushrequest.notsupported")); + } } } Modified: tomcat/trunk/java/org/apache/coyote/http11/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/LocalStrings.properties?rev=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/coyote/http11/LocalStrings.properties Wed Oct 14 14:29:31 2015 @@ -20,6 +20,7 @@ abstractHttp11Protocol.httpUpgradeConfig http11processor.fallToDebug=\n Note: further occurrences of HTTP header parsing errors will be logged at DEBUG level. http11processor.header.parse=Error parsing HTTP request header http11processor.neverused=This method should never be used +http11processor.pushrequest.notsupported=Server push requests are not supported by the HTTP/1.1 protocol http11processor.request.prepare=Error preparing request http11processor.request.process=Error processing request http11processor.request.finish=Error finishing request 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=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java (original) +++ tomcat/trunk/java/org/apache/coyote/http2/Http2UpgradeHandler.java Wed Oct 14 14:29:31 2015 @@ -129,6 +129,7 @@ public class Http2UpgradeHandler extends // Start at -1 so the 'add 2' logic in closeIdleStreams() works private volatile int maxActiveRemoteStreamId = -1; private volatile int maxProcessedStreamId; + private final AtomicInteger nextLocalStreamId = new AtomicInteger(2); private final PingManager pingManager = new PingManager(); private volatile int newStreamsSinceLastPrune = 0; // Tracking for when the connection is blocked (windowSize < 1) @@ -499,6 +500,46 @@ public class Http2UpgradeHandler extends } + void writePushHeaders(Stream stream, int pushedStreamId, Request coyoteRequest, int payloadSize) + throws IOException { + if (log.isDebugEnabled()) { + log.debug(sm.getString("upgradeHandler.writePushHeaders", connectionId, + stream.getIdentifier())); + } + // This ensures the Stream processing thread has control of the socket. + synchronized (socketWrapper) { + byte[] header = new byte[9]; + ByteBuffer target = ByteBuffer.allocate(payloadSize); + boolean first = true; + State state = null; + byte[] pushedStreamIdBytes = new byte[4]; + ByteUtil.set31Bits(pushedStreamIdBytes, 0, pushedStreamId); + target.put(pushedStreamIdBytes); + while (state != State.COMPLETE) { + state = getHpackEncoder().encode(coyoteRequest.getMimeHeaders(), target); + target.flip(); + ByteUtil.setThreeBytes(header, 0, target.limit()); + if (first) { + first = false; + header[3] = FrameType.PUSH_PROMISE.getIdByte(); + } else { + header[3] = FrameType.CONTINUATION.getIdByte(); + } + if (state == State.COMPLETE) { + header[4] += FLAG_END_OF_HEADERS; + } + if (log.isDebugEnabled()) { + log.debug(target.limit() + " bytes"); + } + ByteUtil.set31Bits(header, 5, stream.getIdentifier().intValue()); + socketWrapper.write(true, header, 0, header.length); + socketWrapper.write(true, target.array(), target.arrayOffset(), target.limit()); + socketWrapper.flush(true); + } + } + } + + private HpackEncoder getHpackEncoder() { if (hpackEncoder == null) { hpackEncoder = new HpackEncoder(localSettings.getHeaderTableSize()); @@ -598,6 +639,10 @@ public class Http2UpgradeHandler extends synchronized (stream) { do { synchronized (this) { + if (!stream.canWrite()) { + throw new IOException("TODO i18n: Stream not writeable"); + } + long windowSize = getWindowSize(); if (windowSize < 1 || backLogSize > 0) { // Has this stream been granted an allocation @@ -808,6 +853,18 @@ public class Http2UpgradeHandler extends } + private Stream createLocalStream(Request request) { + int streamId = nextLocalStreamId.getAndAdd(2); + + Integer key = Integer.valueOf(streamId); + + Stream result = new Stream(key, this, request); + streams.put(key, result); + maxRemoteStreamId = streamId; + return result; + } + + private void close() { connectionState.set(ConnectionState.CLOSED); try { @@ -890,6 +947,21 @@ public class Http2UpgradeHandler extends } + void push(Request request, Stream associatedStream) throws IOException { + Stream pushStream = createLocalStream(request); + + // TODO: Is 1k the optimal value? + writePushHeaders(associatedStream, pushStream.getIdentifier().intValue(), request, 1024); + + pushStream.sentPushPromise(); + + // Process this stream on a container thread + StreamProcessor streamProcessor = new StreamProcessor(pushStream, adapter, socketWrapper); + streamProcessor.setSslSupport(sslSupport); + socketWrapper.getEndpoint().getExecutor().execute(streamProcessor); + } + + String getProperty(String key) { return socketWrapper.getEndpoint().getProperty(key); } @@ -968,7 +1040,11 @@ public class Http2UpgradeHandler extends return false; } } else if (thisRead == -1) { - throw new EOFException(); + if (connectionState.get().isNewStreamAllowed()) { + throw new EOFException(); + } else { + return false; + } } else { pos += thisRead; len -= thisRead; 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=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/coyote/http2/LocalStrings.properties Wed Oct 14 14:29:31 2015 @@ -117,6 +117,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}] 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=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/Stream.java (original) +++ tomcat/trunk/java/org/apache/coyote/http2/Stream.java Wed Oct 14 14:29:31 2015 @@ -145,6 +145,9 @@ public class Stream extends AbstractStre private synchronized int reserveWindowSize(int reservation, boolean block) throws IOException { long windowSize = getWindowSize(); while (windowSize < 1) { + if (!canWrite()) { + throw new IOException("TODO i18n: Stream not writeable"); + } try { if (block) { wait(); @@ -329,11 +332,21 @@ public class Stream extends AbstractStre } + void sentPushPromise() { + state.sentPushPromise(); + } + + boolean isActive() { return state.isActive(); } + boolean canWrite() { + return state.canWrite(); + } + + boolean isClosedFinal() { return state.isClosedFinal(); } @@ -365,6 +378,19 @@ public class Stream extends AbstractStre } + void push(Request request) throws IOException { + // Set the special HTTP/2 headers + request.getMimeHeaders().addValue(":method").duplicate(request.method()); + request.getMimeHeaders().addValue(":scheme").duplicate(request.scheme()); + // TODO: Query string + request.getMimeHeaders().addValue(":path").duplicate(request.decodedURI()); + // TODO: Handle default ports + request.getMimeHeaders().addValue(":authority").setString( + request.serverName().getString() + ":" + request.getServerPort()); + handler.push(request, this); + } + + class StreamOutputBuffer implements OutputBuffer { private final ByteBuffer buffer = ByteBuffer.allocate(8 * 1024); 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=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java (original) +++ tomcat/trunk/java/org/apache/coyote/http2/StreamProcessor.java Wed Oct 14 14:29:31 2015 @@ -31,6 +31,7 @@ import org.apache.coyote.Adapter; import org.apache.coyote.AsyncContextCallback; import org.apache.coyote.ContainerThreadMarker; import org.apache.coyote.ErrorState; +import org.apache.coyote.Request; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.buf.ByteChunk; @@ -385,6 +386,17 @@ public class StreamProcessor extends Abs break; } + // Servlet 4.0 Push requests + case PUSH_REQUEST: { + try { + stream.push((Request) param); + } catch (IOException ioe) { + response.setErrorException(ioe); + setErrorState(ErrorState.CLOSE_CONNECTION_NOW, ioe); + } + break; + } + // Unsupported / illegal under HTTP/2 case UPGRADE: throw new UnsupportedOperationException( Added: tomcat/trunk/webapps/examples/WEB-INF/classes/http2/SimpleImagePush.java URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/examples/WEB-INF/classes/http2/SimpleImagePush.java?rev=1708605&view=auto ============================================================================== --- tomcat/trunk/webapps/examples/WEB-INF/classes/http2/SimpleImagePush.java (added) +++ tomcat/trunk/webapps/examples/WEB-INF/classes/http2/SimpleImagePush.java Wed Oct 14 14:29:31 2015 @@ -0,0 +1,50 @@ +/* + * 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 http2; + +import java.io.IOException; +import java.io.PrintWriter; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.PushBuilder; + +public class SimpleImagePush extends HttpServlet { + + private static final long serialVersionUID = 1L; + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + + PushBuilder pb = req.getPushBuilder().setPath("servlets/images/code.gif"); + pb.push(); + + resp.setCharacterEncoding("UTF-8"); + resp.setContentType("text/html"); + PrintWriter pw = resp.getWriter(); + pw.println("<html>"); + pw.println("<body>"); + pw.println("<p>The following image was provided via a push request.</p>"); + pw.println("<img src=\"" + req.getContextPath() + "/servlets/images/code.gif\"/>"); + pw.println("</body>"); + pw.println("</html>"); + pw.flush(); + } +} Propchange: tomcat/trunk/webapps/examples/WEB-INF/classes/http2/SimpleImagePush.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=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/webapps/examples/WEB-INF/web.xml (original) +++ tomcat/trunk/webapps/examples/WEB-INF/web.xml Wed Oct 14 14:29:31 2015 @@ -379,6 +379,16 @@ <url-pattern>/servlets/nonblocking/numberwriter</url-pattern> </servlet-mapping> + <!-- Server Push examples --> + <servlet> + <servlet-name>simpleimagepush</servlet-name> + <servlet-class>http2.SimpleImagePush</servlet-class> + </servlet> + <servlet-mapping> + <servlet-name>simpleimagepush</servlet-name> + <url-pattern>/servlets/serverpush/simpleimage</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=1708605&r1=1708604&r2=1708605&view=diff ============================================================================== --- tomcat/trunk/webapps/examples/servlets/index.html (original) +++ tomcat/trunk/webapps/examples/servlets/index.html Wed Oct 14 14:29:31 2015 @@ -166,6 +166,17 @@ for clarity.</p> <td style="width: 30%;"></td> </tr> +<tr> + <th colspan="3">Servlet 4.0 Server Push examples</th> +</tr> +<tr> + <td>Simple image push</td> + <td style="width: 30%;"> + <a href="serverpush/simpleimage"><img src="images/execute.gif" alt=""> Execute</a> + </td> + <td style="width: 30%;"></td> +</tr> + </table> </body> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org