diff --git a/examples/src/main/java/net/sourceforge/stripes/examples/async/AsyncActionBean.java b/examples/src/main/java/net/sourceforge/stripes/examples/async/AsyncActionBean.java index bf62d7fe0..072e7eee6 100644 --- a/examples/src/main/java/net/sourceforge/stripes/examples/async/AsyncActionBean.java +++ b/examples/src/main/java/net/sourceforge/stripes/examples/async/AsyncActionBean.java @@ -43,81 +43,63 @@ public Resolution display() { * asynchronously fetch data from a remote web service (github) * and set instance fields for use in the view. */ - public Resolution asyncEvent() { - - // we return an AsyncResolution : this triggers the asynchronous servlet mode... - return new AsyncResolution() { - - // only this method to implement. you must complete() or dispatch() yourself. - @Override - protected void executeAsync() throws Exception { - - // we use an Async Http Client in order to call the github web service as a demo. - // the async http client calls on of the lambdas when he's done, and - // then we dispatch to a JSP, completing the async request. - - HttpHost host = new HttpHost("api.github.com", 443, "https"); - new AsyncHttpClient(host) - .buildRequest("/repos/StripesFramework/stripes/commits") - .completed(result -> { - - // response is returned, deserialize result - status = result.getStatusLine().getStatusCode(); - if (status == 200) { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try { - result.getEntity().writeTo(bos); - bos.close(); - ghResponse = bos.toString("UTF-8"); - } catch (Exception e) { - clientException = e; - } - dispatch(JSP_PATH); - } else { - ghResponse = result.getStatusLine().getReasonPhrase(); - dispatch(JSP_PATH); - } - - }) - .failed(ex -> { - - // http client failure - clientException = ex; - dispatch(JSP_PATH); - - }) - .cancelled(() -> { - - // just for demo, we never call it... - cancelled = true; - dispatch(JSP_PATH); - - }) - .get(); // trigger async request - } - }; + public void asyncEvent(AsyncResolution async) { + + // we use an Async Http Client in order to call the github web service as a demo. + // the async http client calls back one of the lambdas when it's done, and + // then we complete the async request. + + final Resolution forwardResolution = new ForwardResolution(JSP_PATH); + HttpHost host = new HttpHost("api.github.com", 443, "https"); + new AsyncHttpClient(host) + .buildRequest("/repos/StripesFramework/stripes/commits") + .completed(result -> { + + // response is returned, deserialize result + status = result.getStatusLine().getStatusCode(); + if (status == 200) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + result.getEntity().writeTo(bos); + bos.close(); + ghResponse = bos.toString("UTF-8"); + } catch (Exception e) { + clientException = e; + } + async.complete(forwardResolution); + } else { + ghResponse = result.getStatusLine().getReasonPhrase(); + async.complete(forwardResolution); + } + + }) + .failed(ex -> { + + // http client failure + clientException = ex; + async.complete(forwardResolution); + + }) + .cancelled(() -> { + + // just for demo, we never call it... + cancelled = true; + async.complete(forwardResolution); + + }) + .get(); // trigger async request } @DontValidate - public Resolution asyncEventThatTimeouts() { - return new AsyncResolution() { - @Override - protected void executeAsync() throws Exception { - getAsyncContext().setTimeout(1000); - getResponse().getWriter().write("OK"); - // never call complete/dispatch... - } - }; + public void asyncEventThatTimeouts(AsyncResolution r) throws Exception { + r.getAsyncContext().setTimeout(1000); + r.getResponse().getWriter().write("OK"); + // never call complete/dispatch... } @DontValidate - public Resolution asyncEventThatThrows() { - return new AsyncResolution() { - @Override - protected void executeAsync() throws Exception { - throw new RuntimeException("WTF"); - } - }; + public void asyncEventThatThrows(AsyncResolution r) { + throw new RuntimeException("BOOM"); } // getters for instance fields that have been set by event method diff --git a/stripes/src/main/java/net/sourceforge/stripes/action/AsyncResolution.java b/stripes/src/main/java/net/sourceforge/stripes/action/AsyncResolution.java index c836e4e39..768c265c8 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/action/AsyncResolution.java +++ b/stripes/src/main/java/net/sourceforge/stripes/action/AsyncResolution.java @@ -3,13 +3,23 @@ import javax.servlet.AsyncContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.util.function.Consumer; -import java.util.function.Supplier; +import java.lang.reflect.Method; -public abstract class AsyncResolution implements Resolution { +public class AsyncResolution implements Resolution { + private final HttpServletRequest request; + private final HttpServletResponse response; + private final Object bean; + private final Method handler; private AsyncContext asyncContext; + public AsyncResolution(HttpServletRequest request, HttpServletResponse response, Object bean, Method handler) { + this.request = request; + this.response = response; + this.bean = bean; + this.handler = handler; + } + public AsyncContext getAsyncContext() { return asyncContext; } @@ -28,8 +38,6 @@ public void setContext(ActionBeanContext context) { this.context = context; } - private HttpServletRequest request; - private HttpServletResponse response; public HttpServletRequest getRequest() { return request; @@ -40,23 +48,20 @@ public HttpServletResponse getResponse() { } @Override - public final void execute(HttpServletRequest request, HttpServletResponse response) throws Exception { - this.request = request; - this.response = response; - executeAsync(); + public void execute(HttpServletRequest request, HttpServletResponse response) throws Exception { + // invoke the handler (start async has been done already) and let it complete... + handler.invoke(bean, this); } - protected abstract void executeAsync() throws Exception; - - protected void dispatch(String path) { + public void dispatch(String path) { getAsyncContext().dispatch(path); } - protected void complete() { + public void complete() { getAsyncContext().complete(); } - protected void complete(Resolution resolution) { + public void complete(Resolution resolution) { try { resolution.execute(getRequest(), getResponse()); } catch (Exception e) { @@ -64,5 +69,4 @@ protected void complete(Resolution resolution) { } } - } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherHelper.java b/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherHelper.java index 75e9be9ee..cb307bee2 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherHelper.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherHelper.java @@ -15,11 +15,8 @@ package net.sourceforge.stripes.controller; import java.lang.annotation.Annotation; -import net.sourceforge.stripes.action.ActionBean; -import net.sourceforge.stripes.action.ActionBeanContext; -import net.sourceforge.stripes.action.DontBind; -import net.sourceforge.stripes.action.DontValidate; -import net.sourceforge.stripes.action.Resolution; + +import net.sourceforge.stripes.action.*; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.exception.StripesServletException; import net.sourceforge.stripes.util.HtmlUtil; @@ -49,15 +46,6 @@ import java.util.TreeSet; import java.util.WeakHashMap; import javax.servlet.http.HttpServletResponse; -import net.sourceforge.stripes.action.DELETE; -import net.sourceforge.stripes.action.ErrorResolution; -import net.sourceforge.stripes.action.GET; -import net.sourceforge.stripes.action.HEAD; -import net.sourceforge.stripes.action.HttpRequestMethod; -import net.sourceforge.stripes.action.JsonBuilder; -import net.sourceforge.stripes.action.POST; -import net.sourceforge.stripes.action.PUT; -import net.sourceforge.stripes.action.RestActionBean; /** * Helper class that contains much of the logic used when dispatching requests @@ -657,7 +645,17 @@ public Resolution intercept(ExecutionContext ctx) throws Exception { // a exception to be handled differently for RestActionBeans that regular // ActionBeans, they will need to write this code accordingly in their // ExceptionHandler class. - Object returnValue = handler.invoke(bean); + final Object returnValue; + if (NameBasedActionResolver.isAsyncEventHandler(handler)) { + returnValue = new AsyncResolution( + ctx.getActionBeanContext().getRequest(), + ctx.getActionBeanContext().getResponse(), + bean, + handler + ); + } else { + returnValue = handler.invoke(bean); + } fillInValidationErrors(ctx); diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherServlet.java b/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherServlet.java index 9ebb6978c..d555ca3ca 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherServlet.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/DispatcherServlet.java @@ -186,6 +186,9 @@ protected void service(final HttpServletRequest request, final HttpServletRespon // start async processing log.debug("Starting async processing from action ", ctx.getActionBean()); AsyncContext asyncContext = request.startAsync(request, response); + AsyncResolution asyncResolution = (AsyncResolution)resolution; + asyncResolution.setAsyncContext(asyncContext); + asyncResolution.setContext(ctx.getActionBeanContext()); final PageContext pc = pageContext; // register listener for finalizing the async processing asyncContext.addListener(new AsyncListener() { @@ -237,9 +240,6 @@ public void onStartAsync(AsyncEvent event) throws IOException { "response=", event.getSuppliedResponse()); } }); - AsyncResolution asyncResolution = (AsyncResolution)resolution; - asyncResolution.setAsyncContext(asyncContext); - asyncResolution.setContext(ctx.getActionBeanContext()); } executeResolution(ctx, resolution); } diff --git a/stripes/src/main/java/net/sourceforge/stripes/controller/NameBasedActionResolver.java b/stripes/src/main/java/net/sourceforge/stripes/controller/NameBasedActionResolver.java index 6d5f6a69f..7aece56e7 100644 --- a/stripes/src/main/java/net/sourceforge/stripes/controller/NameBasedActionResolver.java +++ b/stripes/src/main/java/net/sourceforge/stripes/controller/NameBasedActionResolver.java @@ -14,10 +14,7 @@ */ package net.sourceforge.stripes.controller; -import net.sourceforge.stripes.action.ActionBean; -import net.sourceforge.stripes.action.ActionBeanContext; -import net.sourceforge.stripes.action.Resolution; -import net.sourceforge.stripes.action.ForwardResolution; +import net.sourceforge.stripes.action.*; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.exception.StripesServletException; import net.sourceforge.stripes.util.Literal; @@ -26,6 +23,7 @@ import javax.servlet.ServletContext; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Collections; @@ -243,9 +241,24 @@ public String getHandledEvent(Method handler) { name = handler.getName(); } + if (name == null && isAsyncEventHandler(handler)) { + name = handler.getName(); + } + return name; } + public static boolean isAsyncEventHandler(Method handler) { + if (!Modifier.isAbstract(handler.getModifiers()) + && handler.getReturnType().equals(Void.TYPE) + && handler.getParameterCount() == 1) { + // look at arg type + Parameter p = handler.getParameters()[0]; + return AsyncResolution.class.isAssignableFrom(p.getType()); + } + return false; + } + /** *

Overridden to trap the exception that is thrown when a URL cannot be mapped to an * ActionBean and then attempt to construct a dummy ActionBean that will forward the diff --git a/stripes/src/test/java/net/sourceforge/stripes/mock/TestMockAsync.java b/stripes/src/test/java/net/sourceforge/stripes/mock/TestMockAsync.java index 3920b1d9e..91176869c 100644 --- a/stripes/src/test/java/net/sourceforge/stripes/mock/TestMockAsync.java +++ b/stripes/src/test/java/net/sourceforge/stripes/mock/TestMockAsync.java @@ -11,20 +11,26 @@ public class TestMockAsync extends FilterEnabledTestBase { - @Test - public void testSuccess() throws Exception { + private AsyncActionBean execute(String eventName) throws Exception { MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), AsyncActionBean.class); - trip.execute(); + trip.execute(eventName); AsyncActionBean bean = trip.getActionBean(AsyncActionBean.class); assertNotNull(bean); + assertEquals(eventName, bean.getContext().getEventName()); + assertTrue(bean.completed); + return bean; + } + + @Test + public void testSuccess() throws Exception { + AsyncActionBean bean = execute("doAsync"); + assertNotNull(bean); assertTrue(bean.isCompleted()); } @Test public void testReallyAsync() throws Exception { - MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), AsyncActionBean.class); - trip.execute("doReallyAsync"); - AsyncActionBean bean = trip.getActionBean(AsyncActionBean.class); + AsyncActionBean bean = execute("doReallyAsync"); assertNotNull(bean); assertTrue(bean.isCompleted()); } @@ -75,17 +81,18 @@ public void testAsyncException() throws Exception { @Test public void testCompleteWithForwardResolution() throws Exception { - MockRoundtrip trip = new MockRoundtrip(getMockServletContext(), AsyncActionBean.class); - trip.execute("doAsyncAndCompleteWithForwardResolution"); - AsyncActionBean bean = trip.getActionBean(AsyncActionBean.class); - assertNotNull(bean); + execute("doAsyncAndCompleteWithForwardResolution"); + } + + @Test + public void testAsyncClassy() throws Exception { + execute("doAsyncClassy"); } @UrlBinding("/async") public static class AsyncActionBean implements ActionBean { private boolean completed = false; - private boolean executedForwardResolution; private ActionBeanContext context; public ActionBeanContext getContext() { @@ -97,70 +104,52 @@ public void setContext(ActionBeanContext context) { } @DefaultHandler - public Resolution doAsync() { - return new AsyncResolution() { - @Override - protected void executeAsync() throws Exception { - System.out.println("Not Really Async..."); - getResponse().getWriter().write("DONE"); - completed = true; - complete(); - } - }; + public void doAsync(AsyncResolution r) throws Exception { + System.out.println("Not Really Async..."); + r.getResponse().getWriter().write("DONE"); + completed = true; + r.complete(); } - public Resolution doReallyAsync() { - return new AsyncResolution() { - @Override - protected void executeAsync() throws Exception { - new Thread(() -> { - System.out.println("Really Async !"); - try { - getResponse().getWriter().write("DONE"); - completed = true; - complete(); - } catch (IOException e) { - // will timeout... - e.printStackTrace(); - } - }).start(); + public void doReallyAsync(AsyncResolution r) throws Exception { + new Thread(() -> { + System.out.println("Really Async !"); + try { + r.getResponse().getWriter().write("DONE"); + completed = true; + r.complete(); + } catch (IOException e) { + // will timeout... + e.printStackTrace(); } - }; + }).start(); } - public Resolution doAsyncTimeout() { - return new AsyncResolution() { - @Override - protected void executeAsync() throws Exception { - getAsyncContext().setTimeout(1000); - // we never complete ! - } - }; + public void doAsyncTimeout(AsyncResolution r) { + r.getAsyncContext().setTimeout(1000); + // we never complete ! } public Resolution doRegularException() { throw new RuntimeException("boom"); } - public Resolution doAsyncException() { - return new AsyncResolution() { - @Override - protected void executeAsync() throws Exception { - throw new RuntimeException("Async boom"); - } - }; + public void doAsyncException(AsyncResolution r) { + throw new RuntimeException("Async boom"); } - public Resolution doAsyncAndCompleteWithForwardResolution() { - return new AsyncResolution() { - @Override - protected void executeAsync() throws Exception { - System.out.println("hiya, I'm forwarding..."); - complete(new ForwardResolution("/foo/bar.jsp")); - } - }; + public void doAsyncAndCompleteWithForwardResolution(AsyncResolution r) { + System.out.println("hiya, I'm forwarding..."); + completed = true; + r.complete(new ForwardResolution("/foo/bar.jsp")); } + public void doAsyncClassy(AsyncResolution callback) { + new Thread(() -> { + completed = true; + callback.complete(new ForwardResolution("/foo/bar")); + }).start(); + } public boolean isCompleted() { return completed; @@ -170,3 +159,4 @@ public boolean isCompleted() { } +