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 <[email protected]> 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);
