You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by th...@apache.org on 2021/12/11 20:06:53 UTC

[tapestry-5] 01/01: TAP5-2698: Support for Servlet API 3.0+ asynchronous requests

This is an automated email from the ASF dual-hosted git repository.

thiagohp pushed a commit to branch async
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git

commit fe8b89250807d5ed1d43d2ca6b221b27da2b8bdc
Author: Thiago H. de Paula Figueiredo <th...@arsmachina.com.br>
AuthorDate: Sat Dec 11 17:06:27 2021 -0300

    TAP5-2698: Support for Servlet API 3.0+ asynchronous requests
---
 .../apache/tapestry5/http/AsyncRequestHandler.java |  53 ++++++++
 .../http/AsyncRequestHandlerResponse.java          | 145 +++++++++++++++++++++
 .../org/apache/tapestry5/http/TapestryFilter.java  |  64 ++++++++-
 .../http/internal/AsyncRequestService.java         |  33 +++++
 .../internal/services/AsyncRequestServiceImpl.java |  54 ++++++++
 .../tapestry5/http/modules/TapestryHttpModule.java |   4 +
 6 files changed, 352 insertions(+), 1 deletion(-)

diff --git a/tapestry-http/src/main/java/org/apache/tapestry5/http/AsyncRequestHandler.java b/tapestry-http/src/main/java/org/apache/tapestry5/http/AsyncRequestHandler.java
new file mode 100644
index 0000000..9271375
--- /dev/null
+++ b/tapestry-http/src/main/java/org/apache/tapestry5/http/AsyncRequestHandler.java
@@ -0,0 +1,53 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.http;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javax.servlet.AsyncListener;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.tapestry5.http.internal.AsyncRequestService;
+
+/**
+ * <p>
+ * Service whose implementations define whether a given request should be handled
+ * asynchronously or not and, if yes, which {@link Executor} (usually, a thread pool,
+ * but not necessarily) should handle it, possibly different {@link HttpServletRequest}
+ * and {@link HttpServletResponse} objects to be used when calling 
+ * {@linkplain} HttpServletRequest#startAsync()} and an optional {@linkplain AsyncListener}.
+ * <p>
+ * <p>
+ * If one {@link AsyncRequestHandler} doesn't tells the request should be asynchronous,
+ * the next one contributed to {@link AsyncRequestService} will be called
+ * and so on until one says the request should be asynchronous or all of them
+ * were called and the request will be synchronous.
+ * </p>
+ * @see AsyncRequestService
+ * @see Executor
+ * @see ExecutorService
+ * @see Executors
+ */
+public interface AsyncRequestHandler 
+{
+    
+    /**
+     * Returns whether this request is handled by this handler. If not,
+     * it should return {@link AsyncRequestHandlerResponse#notHandled()}. 
+     * @return a non-null {@linkplain AsyncRequestHandlerResponse}.
+     */
+    AsyncRequestHandlerResponse handle(HttpServletRequest request, HttpServletResponse response);
+
+}
diff --git a/tapestry-http/src/main/java/org/apache/tapestry5/http/AsyncRequestHandlerResponse.java b/tapestry-http/src/main/java/org/apache/tapestry5/http/AsyncRequestHandlerResponse.java
new file mode 100644
index 0000000..9bd3040
--- /dev/null
+++ b/tapestry-http/src/main/java/org/apache/tapestry5/http/AsyncRequestHandlerResponse.java
@@ -0,0 +1,145 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.http;
+
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+import javax.servlet.AsyncListener;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Class used by {@linkplain AsyncRequestHandler} to return information on how to handle
+ * a request.
+ * @see AsyncRequestHandler
+ */
+public class AsyncRequestHandlerResponse 
+{
+    
+    private static final AsyncRequestHandlerResponse NOT_HANDLED = 
+            new AsyncRequestHandlerResponse(false);
+    
+    final private boolean async;
+    
+    final private Executor executor;
+    
+    private HttpServletRequest request;
+    
+    private HttpServletResponse response;
+    
+    private AsyncListener listener;
+    
+    /**
+     * Creates an instance with a given {@link Executor}. It cannot be null.
+     * If you want an instance with a non-async response, use {@link #notHandled()} instead.
+     * @param executor a non-null {@link Executor}.
+     */
+    public AsyncRequestHandlerResponse(Executor executor)
+    {
+        this(true, executor);
+    }
+    
+    private AsyncRequestHandlerResponse(boolean async, Executor executor)
+    {
+        Objects.requireNonNull(executor, "Parameter executor cannot be null");
+        this.async = async;
+        this.executor = executor;
+    }
+    
+    private AsyncRequestHandlerResponse(boolean async)
+    {
+        this.async = async;
+        executor = null;
+    }
+
+    /**
+     * Defines a different request and response to be passed to {@link HttpServletRequest#startAsync(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}.
+     * Both cannot be null.
+     */
+    public AsyncRequestHandlerResponse with(HttpServletRequest request, HttpServletResponse response)
+    {
+        Objects.requireNonNull(request, "Parameter request cannot be null");
+        Objects.requireNonNull(response, "Parameter response cannot be null");
+        this.request = request;
+        this.response = response;
+        return this;
+    }
+
+    /**
+     * Defines a listener to be added to the asynchronous request. It cannot be null.
+     */
+    public AsyncRequestHandlerResponse with(AsyncListener listener)
+    {
+        Objects.requireNonNull(listener, "Parameter listener cannot be null");
+        this.listener = listener;
+        return this;
+    }
+    
+    /**
+     * Returns a response saying this {@linkplain AsyncRequestHandler} doesn't handle this request.
+     * @return an {@link AsyncRequestHandlerResponse}.
+     */
+    public static AsyncRequestHandlerResponse notHandled()
+    {
+        return NOT_HANDLED;
+    }
+
+    /**
+     * Returns whether the request should be processed asynchronously or not.
+     */
+    public boolean isAsync() 
+    {
+        return async;
+    }
+
+    /**    
+     * Returns the {@link Executor} to be used to process the request.
+     */
+    public Executor getExecutor() 
+    {
+        return executor;
+    }
+
+    /**
+     * Returns the request to be used with {@link HttpServletRequest#startAsync()} or null.
+     */
+    public HttpServletRequest getRequest() 
+    {
+        return request;
+    }
+
+    /**
+     * Returns the response to be used with {@link HttpServletRequest#startAsync()} or null.
+     */
+    public HttpServletResponse getResponse() 
+    {
+        return response;
+    }
+
+    /**
+     * Returns the listener to be added to the asynchronous request or null.
+     */
+    public AsyncListener getListener() 
+    {
+        return listener;
+    }
+    
+    /**
+     * Returns whether a request and a response were set in this object.
+     */
+    public boolean isHasRequestAndResponse()
+    {
+        return request != null && response != null;
+    }
+    
+}
diff --git a/tapestry-http/src/main/java/org/apache/tapestry5/http/TapestryFilter.java b/tapestry-http/src/main/java/org/apache/tapestry5/http/TapestryFilter.java
index 7aea81f..5be696a 100644
--- a/tapestry-http/src/main/java/org/apache/tapestry5/http/TapestryFilter.java
+++ b/tapestry-http/src/main/java/org/apache/tapestry5/http/TapestryFilter.java
@@ -14,6 +14,7 @@ package org.apache.tapestry5.http;
 
 import java.io.IOException;
 
+import javax.servlet.AsyncContext;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -24,6 +25,7 @@ import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.tapestry5.http.internal.AsyncRequestService;
 import org.apache.tapestry5.http.internal.ServletContextSymbolProvider;
 import org.apache.tapestry5.http.internal.SingleKeySymbolProvider;
 import org.apache.tapestry5.http.internal.TapestryAppInitializer;
@@ -67,6 +69,8 @@ public class TapestryFilter implements Filter
     private Registry registry;
 
     private HttpServletRequestHandler handler;
+    
+    private AsyncRequestService asyncRequestService;
 
     /**
      * Key under which the Tapestry IoC {@link org.apache.tapestry5.ioc.Registry} is stored in the
@@ -117,6 +121,7 @@ public class TapestryFilter implements Filter
         registry.performRegistryStartup();
 
         handler = registry.getService("HttpServletRequestHandler", HttpServletRequestHandler.class);
+        asyncRequestService = registry.getService("AsyncRequestService", AsyncRequestService.class);
 
         init(registry);
 
@@ -165,7 +170,7 @@ public class TapestryFilter implements Filter
         return new Class[0];
     }
 
-    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+    public final void runFilter(ServletRequest request, ServletResponse response, FilterChain chain)
             throws IOException, ServletException
     {
         try
@@ -182,6 +187,63 @@ public class TapestryFilter implements Filter
             registry.cleanupThread();
         }
     }
+    
+    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException
+    {
+
+        AsyncRequestHandlerResponse handlerResponse = asyncRequestService.handle(
+                (HttpServletRequest) request, (HttpServletResponse) response);
+        
+        if (handlerResponse.isAsync())
+        {
+            AsyncContext asyncContext;
+            if (handlerResponse.isHasRequestAndResponse())
+            {
+                asyncContext = request.startAsync(handlerResponse.getRequest(), handlerResponse.getResponse());
+            }
+            else
+            {
+                asyncContext = request.startAsync();
+            }
+            if (handlerResponse.getListener() != null)
+            {
+                asyncContext.addListener(handlerResponse.getListener());
+            }
+            handlerResponse.getExecutor().execute(
+                    new ExceptionCatchingRunnable(() -> runFilter(request, response, chain)));
+        }
+        else
+        {
+            runFilter(request, response, chain);
+        }
+    }
+    
+    private static interface ExceptionRunnable
+    {
+        void run() throws Exception;
+    }
+    
+    private final class ExceptionCatchingRunnable implements Runnable
+    {
+        private final ExceptionRunnable runnable;
+        
+        public ExceptionCatchingRunnable(ExceptionRunnable runnable) 
+        {
+            this.runnable = runnable;
+        }
+        public void run() 
+        {
+            try 
+            {
+                runnable.run();
+            }
+            catch (Exception e)
+            {
+                throw new RuntimeException(e);
+            }
+        }
+    }
 
     /**
      * Shuts down and discards the registry. Invokes
diff --git a/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/AsyncRequestService.java b/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/AsyncRequestService.java
new file mode 100644
index 0000000..a7f4d52
--- /dev/null
+++ b/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/AsyncRequestService.java
@@ -0,0 +1,33 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.http.internal;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.tapestry5.http.AsyncRequestHandler;
+import org.apache.tapestry5.http.AsyncRequestHandlerResponse;
+import org.apache.tapestry5.ioc.annotations.UsesOrderedConfiguration;
+
+/**
+ * Service that handles Tapestry's support for asynchronous Servlet API requests.
+ * @see AsyncRequestHandler
+ */
+@UsesOrderedConfiguration(AsyncRequestHandler.class)
+public interface AsyncRequestService 
+{
+    /**
+     * Returns an {@linkplain AsyncRequestHandlerResponse} describing how to handle this
+     * request concering being async or not.
+     */
+    AsyncRequestHandlerResponse handle(HttpServletRequest request, HttpServletResponse response);
+}
diff --git a/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/AsyncRequestServiceImpl.java b/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/AsyncRequestServiceImpl.java
new file mode 100644
index 0000000..964aa60
--- /dev/null
+++ b/tapestry-http/src/main/java/org/apache/tapestry5/http/internal/services/AsyncRequestServiceImpl.java
@@ -0,0 +1,54 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry5.http.internal.services;
+
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.tapestry5.http.AsyncRequestHandler;
+import org.apache.tapestry5.http.AsyncRequestHandlerResponse;
+import org.apache.tapestry5.http.internal.AsyncRequestService;
+
+/**
+ * Service that handles Tapestry's support for asynchronous Servlet API requests.
+ */
+public class AsyncRequestServiceImpl implements AsyncRequestService
+{
+    
+    final private List<AsyncRequestHandler> handlers;
+
+    public AsyncRequestServiceImpl(List<AsyncRequestHandler> handlers) 
+    {
+        super();
+        this.handlers = handlers;
+    }
+
+    public AsyncRequestHandlerResponse handle(HttpServletRequest request, HttpServletResponse response)  
+    {
+        
+        AsyncRequestHandlerResponse handlerResponse = AsyncRequestHandlerResponse.notHandled();
+        
+        for (AsyncRequestHandler asyncRequestHandler : handlers) 
+        {
+            handlerResponse = asyncRequestHandler.handle(request, response);
+            if (handlerResponse.isAsync())
+            {
+                break;
+            }
+        }
+        
+        return handlerResponse;
+    }
+    
+}
diff --git a/tapestry-http/src/main/java/org/apache/tapestry5/http/modules/TapestryHttpModule.java b/tapestry-http/src/main/java/org/apache/tapestry5/http/modules/TapestryHttpModule.java
index 87f500a..8e39574 100644
--- a/tapestry-http/src/main/java/org/apache/tapestry5/http/modules/TapestryHttpModule.java
+++ b/tapestry-http/src/main/java/org/apache/tapestry5/http/modules/TapestryHttpModule.java
@@ -30,9 +30,11 @@ import org.apache.tapestry5.commons.internal.util.TapestryException;
 import org.apache.tapestry5.commons.services.CoercionTuple;
 import org.apache.tapestry5.http.OptimizedSessionPersistedObject;
 import org.apache.tapestry5.http.TapestryHttpSymbolConstants;
+import org.apache.tapestry5.http.internal.AsyncRequestService;
 import org.apache.tapestry5.http.internal.TypeCoercerHttpRequestBodyConverter;
 import org.apache.tapestry5.http.internal.gzip.GZipFilter;
 import org.apache.tapestry5.http.internal.services.ApplicationGlobalsImpl;
+import org.apache.tapestry5.http.internal.services.AsyncRequestServiceImpl;
 import org.apache.tapestry5.http.internal.services.BaseURLSourceImpl;
 import org.apache.tapestry5.http.internal.services.ContextImpl;
 import org.apache.tapestry5.http.internal.services.DefaultSessionPersistedObjectAnalyzer;
@@ -102,6 +104,7 @@ public final class TapestryHttpModule {
         binder.bind(BaseURLSource.class, BaseURLSourceImpl.class);
         binder.bind(ResponseCompressionAnalyzer.class, ResponseCompressionAnalyzerImpl.class);
         binder.bind(RestSupport.class, RestSupportImpl.class);
+        binder.bind(AsyncRequestService.class, AsyncRequestServiceImpl.class);
     }
     
     /**
@@ -316,6 +319,7 @@ public final class TapestryHttpModule {
         configuration.addInstance("TypeCoercer", TypeCoercerHttpRequestBodyConverter.class);
     }
     
+    @SuppressWarnings("rawtypes")
     public static void contributeTypeCoercer(MappedConfiguration<CoercionTuple.Key, CoercionTuple> configuration)
     {
         CoercionTuple.add(configuration, HttpServletRequest.class, String.class, TapestryHttpModule::toString);