From 1c0976a8693519f03b3796215bb9140a3c3160fc Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Sat, 28 Oct 2017 16:39:55 +0330 Subject: [PATCH 1/9] WW-4874 Introduces Servlet3 plugin by adding support for async action methods --- .../com/opensymphony/xwork2/AsyncManager.java | 35 ++++ .../xwork2/DefaultActionInvocation.java | 87 ++++++---- .../apache/struts2/dispatcher/Dispatcher.java | 12 +- .../struts2/dispatcher/PrepareOperations.java | 9 +- plugins/pom.xml | 1 + plugins/servlet3/pom.xml | 46 ++++++ .../struts2/servlet3/async/AsyncAction.java | 65 ++++++++ .../servlet3/async/Servlet3AsyncManager.java | 149 ++++++++++++++++++ .../src/main/resources/struts-plugin.xml | 29 ++++ 9 files changed, 396 insertions(+), 37 deletions(-) create mode 100644 core/src/main/java/com/opensymphony/xwork2/AsyncManager.java create mode 100644 plugins/servlet3/pom.xml create mode 100644 plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/AsyncAction.java create mode 100644 plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java create mode 100644 plugins/servlet3/src/main/resources/struts-plugin.xml diff --git a/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java b/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java new file mode 100644 index 000000000..fb6563859 --- /dev/null +++ b/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java @@ -0,0 +1,35 @@ +/* + * 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 com.opensymphony.xwork2; + +import java.util.concurrent.Callable; + +/** + * Adds support for invoke async actions. This allows us to support action methods that return {@link Callable} + * as well as invoking them in separate not-container thread then executing the result in another container thread. + * + * @since 2.5.14 + */ +public interface AsyncManager { + boolean hasAsyncActionResult(); + + Object getAsyncActionResult(); + + void invokeAsyncAction(Callable asyncAction); +} diff --git a/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java b/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java index 5777007c1..da38e4eb5 100644 --- a/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java +++ b/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java @@ -40,6 +40,7 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.Callable; /** * The Default ActionInvocation implementation @@ -71,6 +72,8 @@ public class DefaultActionInvocation implements ActionInvocation { protected Container container; protected UnknownHandlerManager unknownHandlerManager; protected OgnlUtil ognlUtil; + protected AsyncManager asyncManager; + protected Callable asyncAction; protected WithLazyParams.LazyParamInjector lazyParamInjector; public DefaultActionInvocation(final Map extraContext, final boolean pushAction) { @@ -108,6 +111,11 @@ public class DefaultActionInvocation implements ActionInvocation { this.ognlUtil = ognlUtil; } + @Inject(required=false) + public void setAsyncManager(AsyncManager asyncManager) { + this.asyncManager = asyncManager; + } + public Object getAction() { return action; } @@ -237,49 +245,61 @@ public class DefaultActionInvocation implements ActionInvocation { throw new IllegalStateException("Action has already executed"); } - if (interceptors.hasNext()) { - final InterceptorMapping interceptorMapping = interceptors.next(); - String interceptorMsg = "interceptorMapping: " + interceptorMapping.getName(); - UtilTimerStack.push(interceptorMsg); - try { - Interceptor interceptor = interceptorMapping.getInterceptor(); - if (interceptor instanceof WithLazyParams) { - interceptor = lazyParamInjector.injectParams(interceptor, interceptorMapping.getParams(), invocationContext); + if (asyncManager == null || !asyncManager.hasAsyncActionResult()) { + if (interceptors.hasNext()) { + final InterceptorMapping interceptorMapping = interceptors.next(); + String interceptorMsg = "interceptorMapping: " + interceptorMapping.getName(); + UtilTimerStack.push(interceptorMsg); + try { + Interceptor interceptor = interceptorMapping.getInterceptor(); + if (interceptor instanceof WithLazyParams) { + interceptor = lazyParamInjector.injectParams(interceptor, interceptorMapping.getParams(), invocationContext); + } + resultCode = interceptor.intercept(DefaultActionInvocation.this); + } finally { + UtilTimerStack.pop(interceptorMsg); } - resultCode = interceptor.intercept(DefaultActionInvocation.this); - } finally { - UtilTimerStack.pop(interceptorMsg); + } else { + resultCode = invokeActionOnly(); } } else { - resultCode = invokeActionOnly(); + Object asyncActionResult = asyncManager.getAsyncActionResult(); + if (asyncActionResult instanceof Throwable) { + throw new Exception((Throwable) asyncActionResult); + } + asyncAction = null; + resultCode = saveResult(proxy.getConfig(), asyncActionResult); } - // this is needed because the result will be executed, then control will return to the Interceptor, which will - // return above and flow through again - if (!executed) { - if (preResultListeners != null) { - LOG.trace("Executing PreResultListeners for result [{}]", result); + if (asyncManager == null || asyncAction == null) { + // this is needed because the result will be executed, then control will return to the Interceptor, which will + // return above and flow through again + if (!executed) { + if (preResultListeners != null) { + LOG.trace("Executing PreResultListeners for result [{}]", result); - for (Object preResultListener : preResultListeners) { - PreResultListener listener = (PreResultListener) preResultListener; + for (Object preResultListener : preResultListeners) { + PreResultListener listener = (PreResultListener) preResultListener; - String _profileKey = "preResultListener: "; - try { - UtilTimerStack.push(_profileKey); - listener.beforeResult(this, resultCode); - } - finally { - UtilTimerStack.pop(_profileKey); + String _profileKey = "preResultListener: "; + try { + UtilTimerStack.push(_profileKey); + listener.beforeResult(this, resultCode); + } finally { + UtilTimerStack.pop(_profileKey); + } } } - } - // now execute the result, if we're supposed to - if (proxy.getExecuteResult()) { - executeResult(); - } + // now execute the result, if we're supposed to + if (proxy.getExecuteResult()) { + executeResult(); + } - executed = true; + executed = true; + } + } else { + asyncManager.invokeAsyncAction(asyncAction); } return resultCode; @@ -495,6 +515,9 @@ public class DefaultActionInvocation implements ActionInvocation { // Wire the result automatically container.inject(explicitResult); return null; + } else if (methodResult instanceof Callable) { + asyncAction = (Callable) methodResult; + return null; } else { return (String) methodResult; } diff --git a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java index dcc5fe72a..1672f9ead 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java @@ -561,8 +561,16 @@ public class Dispatcher { String name = mapping.getName(); String method = mapping.getMethod(); - ActionProxy proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy( - namespace, name, method, extraContext, true, false); + ActionProxy proxy; + + //check if we are probably in an async resuming + ActionInvocation inv = ActionContext.getContext().getActionInvocation(); + if (inv == null || inv.isExecuted()) { + proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy(namespace, name, method, + extraContext, true, false); + } else { + proxy = inv.getProxy(); + } request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack()); diff --git a/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java b/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java index 354cad7fa..c216cdfb0 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java @@ -79,9 +79,12 @@ public class PrepareOperations { // detected existing context, so we are probably in a forward ctx = new ActionContext(new HashMap<>(oldContext.getContextMap())); } else { - ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack(); - stack.getContext().putAll(dispatcher.createContextMap(request, response, null)); - ctx = new ActionContext(stack.getContext()); + ctx = ServletActionContext.getActionContext(request); //checks if we are probably in an async + if (ctx == null) { + ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack(); + stack.getContext().putAll(dispatcher.createContextMap(request, response, null)); + ctx = new ActionContext(stack.getContext()); + } } request.setAttribute(CLEANUP_RECURSION_COUNTER, counter); ActionContext.setContext(ctx); diff --git a/plugins/pom.xml b/plugins/pom.xml index c336d767e..4a9c7647d 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -56,6 +56,7 @@ spring testng tiles + servlet3 diff --git a/plugins/servlet3/pom.xml b/plugins/servlet3/pom.xml new file mode 100644 index 000000000..36de62611 --- /dev/null +++ b/plugins/servlet3/pom.xml @@ -0,0 +1,46 @@ + + + + 4.0.0 + + org.apache.struts + struts2-plugins + 2.5.14-SNAPSHOT + + + struts2-servlet3-plugin + Struts 2 Servlet3 Plugin + jar + + + UTF-8 + + + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + diff --git a/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/AsyncAction.java b/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/AsyncAction.java new file mode 100644 index 000000000..912cfe765 --- /dev/null +++ b/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/AsyncAction.java @@ -0,0 +1,65 @@ +/* + * 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 org.apache.struts2.servlet3.async; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; + +/** + * A {@link Callable} with a timeout value and an {@link Executor}. + * + * @since 2.5.14 + */ +public class AsyncAction implements Callable { + private Callable callable; + private Long timeout; + private Executor executor; + + public AsyncAction(Callable callable) { + this.callable = callable; + } + + public AsyncAction(long timeout, Callable callable) { + this(callable); + this.timeout = timeout; + } + + public AsyncAction(Executor executor, Callable callable) { + this(callable); + this.executor = executor; + } + + public AsyncAction(long timeout, Executor executor, Callable callable) { + this(timeout, callable); + this.executor = executor; + } + + public Long getTimeout() { + return timeout; + } + + public Executor getExecutor() { + return executor; + } + + @Override + public Object call() throws Exception { + return callable.call(); + } +} diff --git a/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java b/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java new file mode 100644 index 000000000..f72f58036 --- /dev/null +++ b/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java @@ -0,0 +1,149 @@ +/* + * 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 org.apache.struts2.servlet3.async; + +import com.opensymphony.xwork2.AsyncManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ServletActionContext; + +import javax.servlet.AsyncContext; +import javax.servlet.AsyncEvent; +import javax.servlet.AsyncListener; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Implements {@link AsyncManager} to add support for invoke async actions via Servlet 3's API. + * + * @since 2.5.14 + */ +public class Servlet3AsyncManager implements AsyncManager, AsyncListener { + private static final Logger LOG = LogManager.getLogger(Servlet3AsyncManager.class); + private static final AtomicInteger threadCount = new AtomicInteger(0); + + private AsyncContext asyncContext; + private boolean asyncActionStarted; + private Boolean asyncCompleted; + private Object asyncActionResult; + + @Override + public void invokeAsyncAction(final Callable asyncAction) { + if (asyncActionStarted) { + return; + } + + Long timeout = null; + Executor executor = null; + if (asyncAction instanceof AsyncAction) { + AsyncAction customAsyncAction = (AsyncAction) asyncAction; + timeout = customAsyncAction.getTimeout(); + executor = customAsyncAction.getExecutor(); + } + + HttpServletRequest req = ServletActionContext.getRequest(); + asyncActionResult = null; + asyncCompleted = false; + + if (asyncContext == null || !req.isAsyncStarted()) { + asyncContext = req.startAsync(req, ServletActionContext.getResponse()); + asyncContext.addListener(this); + if (timeout != null) { + asyncContext.setTimeout(timeout); + } + } + asyncActionStarted = true; + LOG.debug("Async processing started for " + asyncContext); + + final Runnable task = new Runnable() { + @Override + public void run() { + try { + setAsyncActionResultAndDispatch(asyncAction.call()); + } catch (Throwable e) { + setAsyncActionResultAndDispatch(e); + } + } + }; + if (executor != null) { + executor.execute(task); + } else { + final Thread thread = new Thread(new Runnable() { + @Override + public void run() { + try { + task.run(); + } finally { + threadCount.decrementAndGet(); + } + } + }, this.getClass().getSimpleName() + "-" + threadCount.incrementAndGet()); + thread.start(); + } + } + + private void setAsyncActionResultAndDispatch(Object asyncActionResult) { + this.asyncActionResult = asyncActionResult; + + String log = "Async result [" + asyncActionResult + "] of " + asyncContext; + if (asyncCompleted) { + LOG.error(log + " - could not complete result executing due to timeout or network error"); + } else { + LOG.debug(log + " - dispatching request to execute result in container"); + asyncContext.dispatch(); + } + } + + @Override + public boolean hasAsyncActionResult() { + return asyncActionResult != null; + } + + @Override + public Object getAsyncActionResult() { + return asyncActionResult; + } + + @Override + public void onComplete(AsyncEvent asyncEvent) throws IOException { + asyncContext = null; + asyncCompleted = true; + } + + @Override + public void onTimeout(AsyncEvent asyncEvent) throws IOException { + LOG.debug("Processing timeout for " + asyncEvent.getAsyncContext()); + setAsyncActionResultAndDispatch("timeout"); + } + + @Override + public void onError(AsyncEvent asyncEvent) throws IOException { + Throwable e = asyncEvent.getThrowable(); + LOG.error("Processing error for " + asyncEvent.getAsyncContext(), e); + setAsyncActionResultAndDispatch(e); + } + + @Override + public void onStartAsync(AsyncEvent asyncEvent) throws IOException { + + } +} diff --git a/plugins/servlet3/src/main/resources/struts-plugin.xml b/plugins/servlet3/src/main/resources/struts-plugin.xml new file mode 100644 index 000000000..9c6a75208 --- /dev/null +++ b/plugins/servlet3/src/main/resources/struts-plugin.xml @@ -0,0 +1,29 @@ + + + + + + + From eaed709f1d314f02b3318667c434e5addc922154 Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Sat, 25 Nov 2017 10:11:26 +0330 Subject: [PATCH 2/9] WW-4874 Refactors technology name, servlet3, to asset name, async --- plugins/{servlet3 => async}/pom.xml | 4 ++-- .../main/java/org/apache/struts2}/async/AsyncAction.java | 2 +- .../java/org/apache/struts2/async/DefaultAsyncManager.java} | 6 +++--- .../src/main/resources/struts-plugin.xml | 2 +- plugins/pom.xml | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) rename plugins/{servlet3 => async}/pom.xml (94%) rename plugins/{servlet3/src/main/java/org/apache/struts2/servlet3 => async/src/main/java/org/apache/struts2}/async/AsyncAction.java (97%) rename plugins/{servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java => async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java} (95%) rename plugins/{servlet3 => async}/src/main/resources/struts-plugin.xml (92%) diff --git a/plugins/servlet3/pom.xml b/plugins/async/pom.xml similarity index 94% rename from plugins/servlet3/pom.xml rename to plugins/async/pom.xml index 36de62611..6ed07cff8 100644 --- a/plugins/servlet3/pom.xml +++ b/plugins/async/pom.xml @@ -27,8 +27,8 @@ 2.5.14-SNAPSHOT - struts2-servlet3-plugin - Struts 2 Servlet3 Plugin + struts2-async-plugin + Struts 2 Async Plugin jar diff --git a/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/AsyncAction.java b/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java similarity index 97% rename from plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/AsyncAction.java rename to plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java index 912cfe765..27729d56b 100644 --- a/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/AsyncAction.java +++ b/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.struts2.servlet3.async; +package org.apache.struts2.async; import java.util.concurrent.Callable; import java.util.concurrent.Executor; diff --git a/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java b/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java similarity index 95% rename from plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java rename to plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java index f72f58036..4617dca3b 100644 --- a/plugins/servlet3/src/main/java/org/apache/struts2/servlet3/async/Servlet3AsyncManager.java +++ b/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.struts2.servlet3.async; +package org.apache.struts2.async; import com.opensymphony.xwork2.AsyncManager; import org.apache.logging.log4j.LogManager; @@ -37,8 +37,8 @@ import java.util.concurrent.atomic.AtomicInteger; * * @since 2.5.14 */ -public class Servlet3AsyncManager implements AsyncManager, AsyncListener { - private static final Logger LOG = LogManager.getLogger(Servlet3AsyncManager.class); +public class DefaultAsyncManager implements AsyncManager, AsyncListener { + private static final Logger LOG = LogManager.getLogger(DefaultAsyncManager.class); private static final AtomicInteger threadCount = new AtomicInteger(0); private AsyncContext asyncContext; diff --git a/plugins/servlet3/src/main/resources/struts-plugin.xml b/plugins/async/src/main/resources/struts-plugin.xml similarity index 92% rename from plugins/servlet3/src/main/resources/struts-plugin.xml rename to plugins/async/src/main/resources/struts-plugin.xml index 9c6a75208..fb71372d9 100644 --- a/plugins/servlet3/src/main/resources/struts-plugin.xml +++ b/plugins/async/src/main/resources/struts-plugin.xml @@ -25,5 +25,5 @@ + class="org.apache.struts2.async.DefaultAsyncManager" scope="prototype" /> diff --git a/plugins/pom.xml b/plugins/pom.xml index 4a9c7647d..d94046cd7 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -56,7 +56,7 @@ spring testng tiles - servlet3 + async From 4ac287abd7da73273b162b2dc203c3636e9cb44f Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Sat, 25 Nov 2017 19:26:02 +0330 Subject: [PATCH 3/9] WW-4874 Adds unit tests --- .../xwork2/DefaultActionInvocationTest.java | 92 +++++++++++++- .../struts2/dispatcher/DispatcherTest.java | 30 +++++ .../dispatcher/PrepareOperationsTest.java | 42 +++++++ plugins/async/pom.xml | 18 +++ .../async/DefaultAsyncManagerTest.java | 113 ++++++++++++++++++ 5 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 core/src/test/java/org/apache/struts2/dispatcher/PrepareOperationsTest.java create mode 100644 plugins/async/src/test/java/org/apache/struts2/async/DefaultAsyncManagerTest.java diff --git a/core/src/test/java/com/opensymphony/xwork2/DefaultActionInvocationTest.java b/core/src/test/java/com/opensymphony/xwork2/DefaultActionInvocationTest.java index 334839082..4226ef83a 100644 --- a/core/src/test/java/com/opensymphony/xwork2/DefaultActionInvocationTest.java +++ b/core/src/test/java/com/opensymphony/xwork2/DefaultActionInvocationTest.java @@ -22,10 +22,10 @@ import com.opensymphony.xwork2.config.entities.ActionConfig; import com.opensymphony.xwork2.config.entities.InterceptorMapping; import com.opensymphony.xwork2.config.entities.ResultConfig; import com.opensymphony.xwork2.config.providers.XmlConfigurationProvider; +import com.opensymphony.xwork2.interceptor.PreResultListener; import com.opensymphony.xwork2.mock.MockActionProxy; import com.opensymphony.xwork2.mock.MockContainer; import com.opensymphony.xwork2.mock.MockInterceptor; -import com.opensymphony.xwork2.mock.MockLazyInterceptor; import com.opensymphony.xwork2.ognl.OgnlUtil; import com.opensymphony.xwork2.util.ValueStack; import com.opensymphony.xwork2.util.ValueStackFactory; @@ -34,7 +34,9 @@ import org.apache.struts2.dispatcher.HttpParameters; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; /** @@ -340,6 +342,92 @@ public class DefaultActionInvocationTest extends XWorkTestCase { assertEquals("this is blah", action.getName()); } + public void testInvokeWithAsyncManager() throws Exception { + DefaultActionInvocation dai = new DefaultActionInvocation(new HashMap(), false); + dai.stack = container.getInstance(ValueStackFactory.class).createValueStack(); + + final Semaphore lock = new Semaphore(1); + lock.acquire(); + dai.setAsyncManager(new AsyncManager() { + Object asyncActionResult; + @Override + public boolean hasAsyncActionResult() { + return asyncActionResult != null; + } + + @Override + public Object getAsyncActionResult() { + return asyncActionResult; + } + + @Override + public void invokeAsyncAction(Callable asyncAction) { + try { + asyncActionResult = asyncAction.call(); + } catch (Exception e) { + asyncActionResult = e; + } + lock.release(); + } + }); + + dai.action = new Callable>() { + @Override + public Callable call() throws Exception { + return new Callable() { + @Override + public String call() throws Exception { + return "success"; + } + }; + } + }; + + MockActionProxy actionProxy = new MockActionProxy(); + actionProxy.setMethod("call"); + dai.proxy = actionProxy; + + final boolean[] preResultExecuted = new boolean[1]; + dai.addPreResultListener(new PreResultListener() { + @Override + public void beforeResult(ActionInvocation invocation, String resultCode) { + preResultExecuted[0] = true; + } + }); + + List interceptorMappings = new ArrayList<>(); + MockInterceptor mockInterceptor1 = new MockInterceptor(); + mockInterceptor1.setFoo("test1"); + mockInterceptor1.setExpectedFoo("test1"); + interceptorMappings.add(new InterceptorMapping("test1", mockInterceptor1)); + dai.interceptors = interceptorMappings.iterator(); + + dai.ognlUtil = new OgnlUtil(); + + dai.invoke(); + + assertTrue("interceptor1 should be executed", mockInterceptor1.isExecuted()); + assertFalse("preResultListener should no be executed", preResultExecuted[0]); + assertNotNull("an async action should be saved", dai.asyncAction); + assertFalse("invocation should not be executed", dai.executed); + assertNull("a null result should be passed to upper and wait for the async result", dai.resultCode); + + if(lock.tryAcquire(1500L, TimeUnit.MILLISECONDS)) { + try { + dai.invoke(); + assertTrue("preResultListener should be executed", preResultExecuted[0]); + assertNull("async action should be cleared", dai.asyncAction); + assertTrue("invocation should be executed", dai.executed); + assertEquals("success", dai.resultCode); + } finally { + lock.release(); + } + } else { + lock.release(); + fail("async result did not received on timeout!"); + } + } + @Override protected void setUp() throws Exception { super.setUp(); diff --git a/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java b/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java index 7e25fb11f..6ff918653 100644 --- a/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java +++ b/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java @@ -20,7 +20,9 @@ package org.apache.struts2.dispatcher; import com.mockobjects.dynamic.C; import com.mockobjects.dynamic.Mock; +import com.opensymphony.xwork2.ActionContext; import com.opensymphony.xwork2.ObjectFactory; +import com.opensymphony.xwork2.StubValueStack; import com.opensymphony.xwork2.XWorkConstants; import com.opensymphony.xwork2.config.Configuration; import com.opensymphony.xwork2.config.ConfigurationManager; @@ -30,8 +32,12 @@ import com.opensymphony.xwork2.config.entities.PackageConfig; import com.opensymphony.xwork2.inject.Container; import com.opensymphony.xwork2.interceptor.Interceptor; import com.opensymphony.xwork2.LocalizedTextProvider; +import com.opensymphony.xwork2.mock.MockActionInvocation; +import com.opensymphony.xwork2.mock.MockActionProxy; +import org.apache.struts2.ServletActionContext; import org.apache.struts2.StrutsConstants; import org.apache.struts2.StrutsInternalTestCase; +import org.apache.struts2.dispatcher.mapper.ActionMapping; import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper; import org.apache.struts2.util.ObjectFactoryDestroyable; import org.springframework.mock.web.MockHttpServletRequest; @@ -321,6 +327,30 @@ public class DispatcherTest extends StrutsInternalTestCase { assertTrue(du.isMultipartRequest(req)); } + public void testServiceActionResumePreviousProxy() throws Exception { + Dispatcher du = initDispatcher(Collections.emptyMap()); + + MockActionInvocation mai = new MockActionInvocation(); + ActionContext.getContext().setActionInvocation(mai); + + MockActionProxy actionProxy = new MockActionProxy(); + actionProxy.setInvocation(mai); + mai.setProxy(actionProxy); + + mai.setStack(new StubValueStack()); + + HttpServletRequest req = new MockHttpServletRequest(); + req.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, mai.getStack()); + + assertFalse(actionProxy.isExecutedCalled()); + + du.setDevMode("false"); + du.setHandleException("false"); + du.serviceAction(req, null, new ActionMapping()); + + assertTrue("should execute previous proxy", actionProxy.isExecutedCalled()); + } + class InternalConfigurationManager extends ConfigurationManager { public boolean destroyConfiguration = false; diff --git a/core/src/test/java/org/apache/struts2/dispatcher/PrepareOperationsTest.java b/core/src/test/java/org/apache/struts2/dispatcher/PrepareOperationsTest.java new file mode 100644 index 000000000..02b705b11 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/dispatcher/PrepareOperationsTest.java @@ -0,0 +1,42 @@ +/* + * 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 org.apache.struts2.dispatcher; + +import com.opensymphony.xwork2.ActionContext; +import com.opensymphony.xwork2.StubValueStack; +import org.apache.struts2.ServletActionContext; +import org.apache.struts2.StrutsInternalTestCase; +import org.springframework.mock.web.MockHttpServletRequest; + +import javax.servlet.http.HttpServletRequest; + +public class PrepareOperationsTest extends StrutsInternalTestCase { + public void testCreateActionContextWhenRequestHasOne() { + HttpServletRequest req = new MockHttpServletRequest(); + StubValueStack stack = new StubValueStack(); + req.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack); + + PrepareOperations prepare = new PrepareOperations(null); + + ActionContext.setContext(null); + ActionContext actionContext = prepare.createActionContext(req, null); + + assertEquals(stack.getContext(), actionContext.getContextMap()); + } +} diff --git a/plugins/async/pom.xml b/plugins/async/pom.xml index 6ed07cff8..b5e5ae02b 100644 --- a/plugins/async/pom.xml +++ b/plugins/async/pom.xml @@ -42,5 +42,23 @@ 3.0.1 provided + + + mockobjects + mockobjects-core + test + + + + org.springframework + spring-test + test + + + + org.springframework + spring-web + test + diff --git a/plugins/async/src/test/java/org/apache/struts2/async/DefaultAsyncManagerTest.java b/plugins/async/src/test/java/org/apache/struts2/async/DefaultAsyncManagerTest.java new file mode 100644 index 000000000..f4bc21bef --- /dev/null +++ b/plugins/async/src/test/java/org/apache/struts2/async/DefaultAsyncManagerTest.java @@ -0,0 +1,113 @@ +/* + * 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 org.apache.struts2.async; + +import com.opensymphony.xwork2.XWorkTestCase; +import org.apache.struts2.ServletActionContext; +import org.springframework.mock.web.MockAsyncContext; +import org.springframework.mock.web.MockHttpServletRequest; + +import java.util.concurrent.Callable; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +public class DefaultAsyncManagerTest extends XWorkTestCase { + public void testInvokeAsyncAction() throws Exception { + final MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAsyncSupported(true); + + ServletActionContext.setRequest(request); + + final Semaphore lock = new Semaphore(1); + lock.acquire(); + + AsyncAction asyncAction = new AsyncAction(new Callable() { + @Override + public Object call() throws Exception { + final MockAsyncContext mockAsyncContext = (MockAsyncContext) request.getAsyncContext(); + mockAsyncContext.addDispatchHandler(new Runnable() { + @Override + public void run() { + mockAsyncContext.complete(); + lock.release(); + } + }); + + return "success"; + } + }); + + DefaultAsyncManager asyncManager = new DefaultAsyncManager(); + asyncManager.invokeAsyncAction(asyncAction); + asyncManager.invokeAsyncAction(asyncAction); // duplicate invoke should not raise any problem + + if (lock.tryAcquire(1500L, TimeUnit.MILLISECONDS)) { + try { + assertTrue("an async result is expected", asyncManager.hasAsyncActionResult()); + assertEquals("success", asyncManager.getAsyncActionResult()); + } finally { + lock.release(); + } + } else { + lock.release(); + fail("async result did not received on timeout!"); + } + } + + public void testInvokeAsyncActionException() throws Exception { + final MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAsyncSupported(true); + + ServletActionContext.setRequest(request); + + final Semaphore lock = new Semaphore(1); + lock.acquire(); + + final Exception expected = new Exception(); + AsyncAction asyncAction = new AsyncAction(new Callable() { + @Override + public Object call() throws Exception { + final MockAsyncContext mockAsyncContext = (MockAsyncContext) request.getAsyncContext(); + mockAsyncContext.addDispatchHandler(new Runnable() { + @Override + public void run() { + mockAsyncContext.complete(); + lock.release(); + } + }); + + throw expected; + } + }); + + DefaultAsyncManager asyncManager = new DefaultAsyncManager(); + asyncManager.invokeAsyncAction(asyncAction); + + if (lock.tryAcquire(1500L, TimeUnit.MILLISECONDS)) { + try { + assertTrue("an async result is expected", asyncManager.hasAsyncActionResult()); + assertEquals(expected, asyncManager.getAsyncActionResult()); + } finally { + lock.release(); + } + } else { + fail("async result did not received on timeout!"); + } + } +} From e65d77bae1757b7306bfac8deb38704335d43779 Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Sat, 25 Nov 2017 19:40:25 +0330 Subject: [PATCH 4/9] WW-4874 Updates parent version --- core/src/main/java/com/opensymphony/xwork2/AsyncManager.java | 2 +- plugins/async/pom.xml | 2 +- .../src/main/java/org/apache/struts2/async/AsyncAction.java | 2 +- .../main/java/org/apache/struts2/async/DefaultAsyncManager.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java b/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java index fb6563859..5bada9c77 100644 --- a/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java +++ b/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java @@ -24,7 +24,7 @@ import java.util.concurrent.Callable; * Adds support for invoke async actions. This allows us to support action methods that return {@link Callable} * as well as invoking them in separate not-container thread then executing the result in another container thread. * - * @since 2.5.14 + * @since 2.6 */ public interface AsyncManager { boolean hasAsyncActionResult(); diff --git a/plugins/async/pom.xml b/plugins/async/pom.xml index b5e5ae02b..b92e8ad60 100644 --- a/plugins/async/pom.xml +++ b/plugins/async/pom.xml @@ -24,7 +24,7 @@ org.apache.struts struts2-plugins - 2.5.14-SNAPSHOT + 2.6-SNAPSHOT struts2-async-plugin diff --git a/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java b/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java index 27729d56b..3baf78fbd 100644 --- a/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java +++ b/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java @@ -24,7 +24,7 @@ import java.util.concurrent.Executor; /** * A {@link Callable} with a timeout value and an {@link Executor}. * - * @since 2.5.14 + * @since 2.6 */ public class AsyncAction implements Callable { private Callable callable; diff --git a/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java b/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java index 4617dca3b..1e84f27f0 100644 --- a/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java +++ b/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java @@ -35,7 +35,7 @@ import java.util.concurrent.atomic.AtomicInteger; /** * Implements {@link AsyncManager} to add support for invoke async actions via Servlet 3's API. * - * @since 2.5.14 + * @since 2.6 */ public class DefaultAsyncManager implements AsyncManager, AsyncListener { private static final Logger LOG = LogManager.getLogger(DefaultAsyncManager.class); From fb8f139b069dd32ccecf9abe6b4c110a666ca860 Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Thu, 21 Dec 2017 10:13:36 +0330 Subject: [PATCH 5/9] WW-4874 Renames local variable inv to invocation --- .../main/java/org/apache/struts2/dispatcher/Dispatcher.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java index 1672f9ead..dd2f76bea 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java @@ -564,12 +564,12 @@ public class Dispatcher { ActionProxy proxy; //check if we are probably in an async resuming - ActionInvocation inv = ActionContext.getContext().getActionInvocation(); - if (inv == null || inv.isExecuted()) { + ActionInvocation invocation = ActionContext.getContext().getActionInvocation(); + if (invocation == null || invocation.isExecuted()) { proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy(namespace, name, method, extraContext, true, false); } else { - proxy = inv.getProxy(); + proxy = invocation.getProxy(); } request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack()); From bffa47acf14b47133d8a8dff925fee8acf4823e8 Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Sun, 24 Dec 2017 12:10:50 +0330 Subject: [PATCH 6/9] define constant for timeout result --- .../src/main/java/org/apache/struts2/async/AsyncAction.java | 6 ++++++ .../java/org/apache/struts2/async/DefaultAsyncManager.java | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java b/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java index 3baf78fbd..1edf5d45a 100644 --- a/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java +++ b/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java @@ -27,6 +27,12 @@ import java.util.concurrent.Executor; * @since 2.6 */ public class AsyncAction implements Callable { + + /** + * The action invocation was successful but did not return the result before timeout. + */ + public static final String TIMEOUT = "timeout"; + private Callable callable; private Long timeout; private Executor executor; diff --git a/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java b/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java index 1e84f27f0..ed69548fc 100644 --- a/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java +++ b/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java @@ -132,7 +132,7 @@ public class DefaultAsyncManager implements AsyncManager, AsyncListener { @Override public void onTimeout(AsyncEvent asyncEvent) throws IOException { LOG.debug("Processing timeout for " + asyncEvent.getAsyncContext()); - setAsyncActionResultAndDispatch("timeout"); + setAsyncActionResultAndDispatch(AsyncAction.TIMEOUT); } @Override From 8251dd836f845c360f2c9a9571c517c95a6cbb5e Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Sun, 24 Dec 2017 13:26:15 +0330 Subject: [PATCH 7/9] decrease log level for timeout --- .../main/java/org/apache/struts2/async/DefaultAsyncManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java b/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java index ed69548fc..8543590bb 100644 --- a/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java +++ b/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java @@ -106,7 +106,7 @@ public class DefaultAsyncManager implements AsyncManager, AsyncListener { String log = "Async result [" + asyncActionResult + "] of " + asyncContext; if (asyncCompleted) { - LOG.error(log + " - could not complete result executing due to timeout or network error"); + LOG.debug(log + " - could not complete result executing due to timeout or network error"); } else { LOG.debug(log + " - dispatching request to execute result in container"); asyncContext.dispatch(); From b0270eb16f95e77ee9070bf43ebba0ccbc101e5b Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Mon, 25 Dec 2017 10:54:19 +0330 Subject: [PATCH 8/9] add async plugin to dependency management --- pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index d1acf4086..add1d6794 100644 --- a/pom.xml +++ b/pom.xml @@ -532,6 +532,11 @@ struts2-gxp-plugin ${project.version} + + org.apache.struts + struts2-async-plugin + ${project.version} + org.apache.struts struts2-osgi-admin-bundle From aee171c3b8ad401006612c4df44ed540fb2ed7e3 Mon Sep 17 00:00:00 2001 From: Yasser Zamani Date: Tue, 26 Dec 2017 17:10:52 +0330 Subject: [PATCH 9/9] add showcase for async plugin --- apps/showcase/pom.xml | 5 + .../struts2/showcase/async/AsyncFilter.java | 53 ++++++++ .../showcase/async/ChatRoomAction.java | 68 ++++++++++ .../src/main/resources/struts-async.xml | 49 ++++++++ apps/showcase/src/main/resources/struts.xml | 2 + .../main/webapp/WEB-INF/decorators/main.jsp | 1 + apps/showcase/src/main/webapp/WEB-INF/web.xml | 25 +++- .../showcase/src/main/webapp/async/index.html | 119 ++++++++++++++++++ .../apache/struts2/showcase/AsyncTest.java | 30 +++++ 9 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 apps/showcase/src/main/java/org/apache/struts2/showcase/async/AsyncFilter.java create mode 100644 apps/showcase/src/main/java/org/apache/struts2/showcase/async/ChatRoomAction.java create mode 100644 apps/showcase/src/main/resources/struts-async.xml create mode 100644 apps/showcase/src/main/webapp/async/index.html create mode 100644 apps/showcase/src/test/java/it/org/apache/struts2/showcase/AsyncTest.java diff --git a/apps/showcase/pom.xml b/apps/showcase/pom.xml index 7f277124e..c64fe5693 100644 --- a/apps/showcase/pom.xml +++ b/apps/showcase/pom.xml @@ -89,6 +89,11 @@ struts2-bean-validation-plugin + + org.apache.struts + struts2-async-plugin + + javax.servlet servlet-api diff --git a/apps/showcase/src/main/java/org/apache/struts2/showcase/async/AsyncFilter.java b/apps/showcase/src/main/java/org/apache/struts2/showcase/async/AsyncFilter.java new file mode 100644 index 000000000..95d98ca53 --- /dev/null +++ b/apps/showcase/src/main/java/org/apache/struts2/showcase/async/AsyncFilter.java @@ -0,0 +1,53 @@ +/* + * 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 org.apache.struts2.showcase.async; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * Filters async actions directly to Struts servlet + */ +public class AsyncFilter implements Filter { + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + String requestURI = ((HttpServletRequest) servletRequest).getRequestURI(); + if (!requestURI.contains("/async/receiveNewMessages")) { + filterChain.doFilter(servletRequest, servletResponse); // Just continue chain. + } else { + servletRequest.getRequestDispatcher("/async/receiveNewMessages").forward(servletRequest, servletResponse); + } + } + + @Override + public void destroy() { + + } +} diff --git a/apps/showcase/src/main/java/org/apache/struts2/showcase/async/ChatRoomAction.java b/apps/showcase/src/main/java/org/apache/struts2/showcase/async/ChatRoomAction.java new file mode 100644 index 000000000..5877fe6e1 --- /dev/null +++ b/apps/showcase/src/main/java/org/apache/struts2/showcase/async/ChatRoomAction.java @@ -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 org.apache.struts2.showcase.async; + +import com.opensymphony.xwork2.ActionSupport; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; + +/** + * Example to illustrate the async plugin. + */ +public class ChatRoomAction extends ActionSupport { + private String message; + private Integer lastIndex; + private List newMessages; + + private static final List messages = new ArrayList<>(); + + public void setMessage(String message) { + this.message = message; + } + + public void setLastIndex(Integer lastIndex) { + this.lastIndex = lastIndex; + } + + public List getNewMessages() { + return newMessages; + } + + public Callable receiveNewMessages() throws Exception { + return new Callable() { + @Override + public String call() throws Exception { + while (lastIndex >= messages.size()) { + Thread.sleep(3000); + } + newMessages = messages.subList(lastIndex, messages.size()); + return SUCCESS; + } + }; + } + + public String sendMessage() { + synchronized (messages) { + messages.add(message); + } + return SUCCESS; + } +} diff --git a/apps/showcase/src/main/resources/struts-async.xml b/apps/showcase/src/main/resources/struts-async.xml new file mode 100644 index 000000000..faa38656c --- /dev/null +++ b/apps/showcase/src/main/resources/struts-async.xml @@ -0,0 +1,49 @@ + + + + + + + + + + newMessages + + + newMessages + + + + + + newMessages + + + newMessages + + + + + + diff --git a/apps/showcase/src/main/resources/struts.xml b/apps/showcase/src/main/resources/struts.xml index 46611f9cd..ee7dbcee3 100644 --- a/apps/showcase/src/main/resources/struts.xml +++ b/apps/showcase/src/main/resources/struts.xml @@ -74,6 +74,8 @@ + + diff --git a/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp b/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp index 727b28c5b..bcbe36113 100644 --- a/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp +++ b/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp @@ -235,6 +235,7 @@
  • Token
  • Model Driven
  • +
  • Async