Merge pull request #179 from yasserzamani/WW-4874

WW-4874 Introduces Async plugin (adds support for async methods)
This commit is contained in:
Lukasz Lenart
2018-03-20 14:59:12 +01:00
committed by GitHub
23 changed files with 1051 additions and 38 deletions
+5
View File
@@ -89,6 +89,11 @@
<artifactId>struts2-bean-validation-plugin</artifactId>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-async-plugin</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
@@ -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() {
}
}
@@ -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 <code>async</code> plugin.
*/
public class ChatRoomAction extends ActionSupport {
private String message;
private Integer lastIndex;
private List<String> newMessages;
private static final List<String> messages = new ArrayList<>();
public void setMessage(String message) {
this.message = message;
}
public void setLastIndex(Integer lastIndex) {
this.lastIndex = lastIndex;
}
public List<String> getNewMessages() {
return newMessages;
}
public Callable<String> receiveNewMessages() throws Exception {
return new Callable<String>() {
@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;
}
}
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
/*
* 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.
*/
-->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<package name="async" extends="json-default" namespace="/async">
<action name="receiveNewMessages" class="org.apache.struts2.showcase.async.ChatRoomAction" method="receiveNewMessages">
<result name="success" type="json">
<param name="root">newMessages</param>
</result>
<result name="timeout" type="json">
<param name="root">newMessages</param>
</result>
</action>
<action name="sendMessage" class="org.apache.struts2.showcase.async.ChatRoomAction" method="sendMessage">
<result name="success" type="json">
<param name="root">newMessages</param>
</result>
<result name="timeout" type="json">
<param name="root">newMessages</param>
</result>
</action>
</package>
</struts>
@@ -74,6 +74,8 @@
<include file="struts-xslt.xml" />
<include file="struts-async.xml" />
<package name="default" extends="struts-default">
<interceptors>
<interceptor-stack name="crudStack">
@@ -235,6 +235,7 @@
<li><s:a value="/token/index.html">Token</s:a></li>
<li><s:url var="url" namespace="/modelDriven" action="modelDriven"/><s:a
href="%{url}">Model Driven</s:a></li>
<li><s:a value="/async/index.html">Async</s:a></li>
</ul>
</li>
<li class="dropdown">
+24 -1
View File
@@ -23,7 +23,13 @@
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>Struts Showcase Application</display-name>
<filter>
<filter-name>async</filter-name>
<filter-class>org.apache.struts2.showcase.async.AsyncFilter</filter-class>
<async-supported>true</async-supported>
</filter>
<filter>
<filter-name>struts-prepare</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareFilter</filter-class>
@@ -40,6 +46,11 @@
<filter-class>com.opensymphony.sitemesh.webapp.SiteMeshFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>async</filter-name>
<url-pattern>/async/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>struts-prepare</filter-name>
<url-pattern>/*</url-pattern>
@@ -113,6 +124,13 @@
<load-on-startup>1</load-on-startup>
</servlet>
<servlet>
<servlet-name>strutsServlet</servlet-name>
<servlet-class>org.apache.struts2.dispatcher.servlet.StrutsServlet</servlet-class>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>dwr</servlet-name>
<url-pattern>/dwr/*</url-pattern>
@@ -128,6 +146,11 @@
<url-pattern>*.vm</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>strutsServlet</servlet-name>
<url-pattern>/async/receiveNewMessages</url-pattern>
</servlet-mapping>
<!-- END SNIPPET: dwr -->
<!-- SNIPPET START: example.velocity.filter.chain
@@ -0,0 +1,119 @@
<!--
/*
* 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.
*/
-->
<html>
<head>
<title>Struts2 Showcase - Async Example</title>
<script>
function User () {
this.lastIndex = 0;
this.sendMessage = function(message) {
$.ajax({
url: "sendMessage",
data: {
message: message
}
});
};
this.receiveNewMessages = function() {
$.ajax({
url: "receiveNewMessages",
context: this,
data: {
lastIndex: this.lastIndex
},
success: function (result) {
if (result != null) {//result is null on timeout
var msgs = $('#msgs');
var messages = msgs.val();
msgs.val(messages + result + '\n');
this.lastIndex += result.length;
}
this.receiveNewMessages();
}
});
};
this.receiveNewMessages();
}
var user;
function sendMessage() {
var msg = $('#msg');
var message = msg.val();
if (message.length > 0) {
user.sendMessage(message);
msg.val('');
}
return false;
}
$(function() {
user = new User();
});
</script>
</head>
<body>
<div class="page-header">
<h1>Async Example</h1>
</div>
<div class="container-fluid">
<div class="row">
<div class="col-md-12" style="text-align: center;">
<p>
These examples illustrate Struts build in support for async request processing.
</p>
<p>
When you have a process that takes a long time, it can make your app not scalable under heavy load conditions.
Scalability limitations include running out of memory or exhausting the pool of container threads.
To create scalable web applications, you must ensure that no threads associated with a request
are sitting idle, so the container can use them to process new requests.
Asynchronous processing refers to assigning these blocking operations to a new thread and returning
the thread associated with the request immediately to the container.
<br/> Reference: <a href="https://docs.oracle.com/javaee/7/tutorial/servlets012.htm">Asynchronous Processing</a>
<br/> An interesting and vital use case for the async request processing is server push.
A good solution is to use the Servlet 3.0+ asynchronous feature.
</p>
<br/>
<h2>Example: A minimal chat room using server push</h2>
<h3>Open current page in different tabs, browsers and computers then send messages.</h3>
<h4>This is a minimal chat room which uses server push to retrieve new messages.
It doesn't poll the server frequently to check if a new message is available to display.
Instead it waits for the server to push back new messages. This approach has two obvious advantages:
low-lag communication without requests being sent, and no waste of server resources and network bandwidth.</h4>
<h5>Reference: <a href="https://www.javaworld.com/article/2077995/java-concurrency/java-concurrency-asynchronous-processing-support-in-servlet-3-0.html">
Asynchronous processing support in Servlet 3.0</a></h5>
<textarea id="msgs" cols="40" rows="5" title="messages" readonly></textarea><br/>
<form>
<input name="msg" id="msg" type="text" title="message" required />
<input type="submit" value="Send" onclick="return sendMessage();" />
</form>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,30 @@
/*
* 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 it.org.apache.struts2.showcase;
public class AsyncTest extends ITBaseTest {
public void testChatRoom() throws InterruptedException {
beginAt("/async/index.html");
setTextField("msg", "hello");
submit();
Thread.sleep(4000);
assertTextInElement("msgs", "hello");
}
}
@@ -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.6
*/
public interface AsyncManager {
boolean hasAsyncActionResult();
Object getAsyncActionResult();
void invokeAsyncAction(Callable asyncAction);
}
@@ -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<String, Object> 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;
}
@@ -588,8 +588,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 invocation = ActionContext.getContext().getActionInvocation();
if (invocation == null || invocation.isExecuted()) {
proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy(namespace, name, method,
extraContext, true, false);
} else {
proxy = invocation.getProxy();
}
request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack());
@@ -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);
@@ -22,6 +22,7 @@ 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.MockInterceptor;
import com.opensymphony.xwork2.mock.MockResult;
@@ -33,6 +34,9 @@ import org.apache.struts2.dispatcher.HttpParameters;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
/**
@@ -322,6 +326,92 @@ public class DefaultActionInvocationTest extends XWorkTestCase {
assertEquals("this is blah", action.getName());
}
public void testInvokeWithAsyncManager() throws Exception {
DefaultActionInvocation dai = new DefaultActionInvocation(new HashMap<String, Object>(), 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<Callable<String>>() {
@Override
public Callable<String> call() throws Exception {
return new Callable<String>() {
@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<InterceptorMapping> 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!");
}
}
public void testActionEventListener() throws Exception {
ActionProxy actionProxy = actionProxyFactory.createActionProxy("",
"ExceptionFoo", "exceptionMethod", new HashMap<String, Object>());
@@ -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.<String, String>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;
@@ -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());
}
}
+64
View File
@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
/*
* 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.
*/
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-plugins</artifactId>
<version>2.6-SNAPSHOT</version>
</parent>
<artifactId>struts2-async-plugin</artifactId>
<name>Struts 2 Async Plugin</name>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.0.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>mockobjects</groupId>
<artifactId>mockobjects-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,71 @@
/*
* 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 java.util.concurrent.Callable;
import java.util.concurrent.Executor;
/**
* A {@link Callable} with a timeout value and an {@link 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;
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();
}
}
@@ -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.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.6
*/
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;
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.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();
}
}
@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(AsyncAction.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 {
}
}
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
/*
* 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.
*/
-->
<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
"http://struts.apache.org/dtds/struts-2.5.dtd">
<struts>
<bean type="com.opensymphony.xwork2.AsyncManager" name="default"
class="org.apache.struts2.async.DefaultAsyncManager" scope="prototype" />
</struts>
@@ -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!");
}
}
}
+1
View File
@@ -56,6 +56,7 @@
<module>spring</module>
<module>testng</module>
<module>tiles</module>
<module>async</module>
</modules>
<dependencies>
+5
View File
@@ -592,6 +592,11 @@
<artifactId>struts2-gxp-plugin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-async-plugin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-osgi-admin-bundle</artifactId>