Merge pull request #688 from JCgH4164838Gh792C124B5/localS2_62_ExecWaitCleanup

[WW-5312] Attempt to fix ExecuteAndWaitInterceptor inconsistent processing
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/SessionMap.java b/core/src/main/java/org/apache/struts2/dispatcher/SessionMap.java
index dac8404..8fe5777 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/SessionMap.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/SessionMap.java
@@ -42,7 +42,7 @@
 
 
     /**
-     * Creates a new session map given a http servlet request. Note, ths enumeration of request
+     * Creates a new session map given a http servlet request. Note, the enumeration of request
      * attributes will occur when the map entries are asked for.
      *
      * @param request the http servlet request object.
@@ -82,7 +82,7 @@
 
         synchronized (session.getId().intern()) {
             entries = null;
-            Enumeration<String> attributeNamesEnum = session.getAttributeNames();
+            final Enumeration<String> attributeNamesEnum = session.getAttributeNames();
             while (attributeNamesEnum.hasMoreElements()) {
                 session.removeAttribute(attributeNamesEnum.nextElement());
             }
@@ -105,7 +105,7 @@
             if (entries == null) {
                 entries = new HashSet<>();
 
-                Enumeration<String> enumeration = session.getAttributeNames();
+                final Enumeration<String> enumeration = session.getAttributeNames();
 
                 while (enumeration.hasMoreElements()) {
                     final String key = enumeration.nextElement();
@@ -127,17 +127,21 @@
 
     /**
      * Returns the session attribute associated with the given key or <tt>null</tt> if it doesn't exist.
+     * 
+     * <b>Note:</b> Must use the same signature as {@link java.util.AbstractMap#get(java.lang.Object)} to ensure the
+     *   expected specialized behaviour is performed here (and not the generic ancestor behaviour).
      *
      * @param key the name of the session attribute.
      * @return the session attribute or <tt>null</tt> if it doesn't exist.
      */
-    public Object get(final String key) {
+    @Override
+    public Object get(final Object key) {
         if (session == null) {
             return null;
         }
 
         synchronized (session.getId().intern()) {
-            return session.getAttribute(key);
+            return session.getAttribute(key != null ? key.toString() : null);
         }
     }
 
@@ -156,7 +160,7 @@
             }
         }
         synchronized (session.getId().intern()) {
-            Object oldValue = get(key);
+            final Object oldValue = get(key);
             entries = null;
             session.setAttribute(key, value);
             return oldValue;
@@ -166,10 +170,14 @@
     /**
      * Removes the specified session attribute.
      *
+     * <b>Note:</b> Must use the same signature as {@link java.util.AbstractMap#remove(java.lang.Object)} to ensure the
+     *   expected specialized behaviour is performed here (and not the generic ancestor behaviour).
+     * 
      * @param key the name of the attribute to remove.
      * @return the value that was removed or <tt>null</tt> if the value was not found (and hence, not removed).
      */
-    public Object remove(final String key) {
+    @Override
+    public Object remove(final Object key) {
         if (session == null) {
             return null;
         }
@@ -177,8 +185,9 @@
         synchronized (session.getId().intern()) {
             entries = null;
 
-            Object value = get(key);
-            session.removeAttribute(key);
+            final String keyAsString = (key != null ? key.toString() : null);
+            final Object value = get(keyAsString);
+            session.removeAttribute(keyAsString);
 
             return value;
         }
@@ -188,16 +197,21 @@
     /**
      * Checks if the specified session attribute with the given key exists.
      *
+     * <b>Note:</b> Must use the same signature as {@link java.util.AbstractMap#containsKey(java.lang.Object)} to ensure the
+     *   expected specialized behaviour is performed here (and not the generic ancestor behaviour).
+     * 
      * @param key the name of the session attribute.
      * @return <tt>true</tt> if the session attribute exits or <tt>false</tt> if it doesn't exist.
      */
-    public boolean containsKey(final String key) {
+    @Override
+    public boolean containsKey(final Object key) {
         if (session == null) {
             return false;
         }
 
         synchronized (session.getId().intern()) {
-            return (session.getAttribute(key.toString()) != null);
+            final String keyAsString = (key != null ? key.toString() : null);
+            return (session.getAttribute(keyAsString) != null);
         }
     }
 }
diff --git a/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
index 7a022c9..8d48766 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
@@ -228,6 +228,7 @@
     /* (non-Javadoc)
      * @see com.opensymphony.xwork2.interceptor.MethodFilterInterceptor#doIntercept(com.opensymphony.xwork2.ActionInvocation)
      */
+    @Override
     protected String doIntercept(ActionInvocation actionInvocation) throws Exception {
         ActionProxy proxy = actionInvocation.getProxy();
         String name = getBackgroundProcessName(proxy);
@@ -235,34 +236,40 @@
         Map<String, Object> session = context.getSession();
         HttpSession httpSession = ServletActionContext.getRequest().getSession(true);
 
-        Boolean secondTime = true;
-        if (executeAfterValidationPass) {
-            secondTime = (Boolean) context.get(KEY);
-            if (secondTime == null) {
-                context.put(KEY, true);
-                secondTime = false;
-            } else {
-                secondTime = true;
-                context.put(KEY, null);
-            }
-        }
-
         //sync on the real HttpSession as the session from the context is a wrap that is created
         //on every request
         synchronized (httpSession) {
-            BackgroundProcess bp = (BackgroundProcess) session.get(KEY + name);
+            // State flag processing moved within the synchronization block, to ensure consistency.
+            Boolean secondTime = true;
+            if (executeAfterValidationPass) {
+                secondTime = (Boolean) context.get(KEY);
+                if (secondTime == null) {
+                    context.put(KEY, true);
+                    secondTime = false;
+                } else {
+                    secondTime = true;
+                    context.put(KEY, null);
+                }
+            }
+
+            final String bp_SessionKey = KEY + name;
+            BackgroundProcess bp = (BackgroundProcess) session.get(bp_SessionKey);
+
+            LOG.debug("Intercepting invocation for BackgroundProcess - session key: {}, value: {}", bp_SessionKey, bp);
 
             //WW-4900 Checks if from a de-serialized session? so background thread missed, let's start a new one.
             if (bp != null && bp.getInvocation() == null) {
-                session.remove(KEY + name);
+                LOG.trace("BackgroundProcess invocation is null (remove key, clear instance)");
+                session.remove(bp_SessionKey);
                 bp = null;
             }
 
             if ((!executeAfterValidationPass || secondTime) && bp == null) {
+                LOG.trace("BackgroundProcess instance is null (create new instance) - executeAfterValidationPass: {}, secondTime: {}.", executeAfterValidationPass, secondTime);
                 bp = getNewBackgroundProcess(name, actionInvocation, threadPriority).prepare();
-                session.put(KEY + name, bp);
-                if (executor.isShutdown()) {
-                    LOG.warn("Executor is shutting down, cannot execute a new process");
+                session.put(bp_SessionKey, bp);
+                if (executor == null || executor.isShutdown()) {
+                    LOG.warn("Executor is shutting down (or null), cannot execute a new process, invoke next ActionInvocation step and return.");
                     return actionInvocation.invoke();
                 }
                 executor.execute(bp);
@@ -271,6 +278,7 @@
             }
 
             if ((!executeAfterValidationPass || !secondTime) && bp != null && !bp.isDone()) {
+                LOG.trace("BackgroundProcess instance is not done (wait processing) - executeAfterValidationPass: {}, secondTime: {}.", executeAfterValidationPass, secondTime);
                 actionInvocation.getStack().push(bp.getAction());
 
                 final String token = TokenHelper.getToken();
@@ -297,7 +305,8 @@
 
                 return WAIT;
             } else if ((!executeAfterValidationPass || !secondTime) && bp != null && bp.isDone()) {
-                session.remove(KEY + name);
+                LOG.trace("BackgroundProcess instance is done (remove key, return result) - executeAfterValidationPass: {}, secondTime: {}.", executeAfterValidationPass, secondTime);
+                session.remove(bp_SessionKey);
                 actionInvocation.getStack().push(bp.getAction());
 
                 // if an exception occurred during action execution, throw it here
@@ -307,6 +316,7 @@
 
                 return bp.getResult();
             } else {
+                LOG.trace("BackgroundProcess state fall-through (first instance, pass through), invoke next ActionInvocation step and return - executeAfterValidationPass: {}, secondTime: {}.", executeAfterValidationPass, secondTime);
                 // this is the first instance of the interceptor and there is no existing action
                 // already run in the background, so let's just let this pass through. We assume
                 // the action invocation will be run in the background on the subsequent pass through
@@ -394,7 +404,10 @@
 
     @Override
     public void destroy() {
-        super.destroy();
-        executor.shutdown();
+        try {
+          executor.shutdown();
+        } finally {
+          super.destroy();
+        }
     }
 }
diff --git a/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcess.java b/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcess.java
index 8f56391..4223726 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcess.java
+++ b/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcess.java
@@ -33,10 +33,10 @@
     private final String threadName;
     private final int threadPriority;
 
-    private transient  Thread processThread;
+    private transient Thread processThread;
     //WW-4900 transient since 2.5.15
     protected transient ActionInvocation invocation;
-    protected transient  Exception exception;
+    protected transient Exception exception;
 
     protected String result;
     protected boolean done;
@@ -64,9 +64,9 @@
                     afterInvocation();
                 } catch (Exception e) {
                     exception = e;
+                } finally {
+                  done = true;
                 }
-
-                done = true;
             });
             processThread.setName(threadName);
             processThread.setPriority(threadPriority);
diff --git a/core/src/test/java/org/apache/struts2/dispatcher/SessionMapTest.java b/core/src/test/java/org/apache/struts2/dispatcher/SessionMapTest.java
index 8c292cd..6dc597e 100644
--- a/core/src/test/java/org/apache/struts2/dispatcher/SessionMapTest.java
+++ b/core/src/test/java/org/apache/struts2/dispatcher/SessionMapTest.java
@@ -36,6 +36,7 @@
 import com.mockobjects.constraint.IsAnything;
 import com.mockobjects.constraint.IsEqual;
 import com.mockobjects.dynamic.Mock;
+import java.util.AbstractMap;
 
 
 /**
@@ -47,24 +48,18 @@
 
 
     public void testClearInvalidatesTheSession() throws Exception {
-        List<String> attributeNames = new ArrayList<String>();
+        List<String> attributeNames = new ArrayList<>();
         attributeNames.add("test");
         attributeNames.add("anotherTest");
         Enumeration<String> attributeNamesEnum = Collections.enumeration(attributeNames);
 
         MockSessionMap sessionMap = new MockSessionMap((HttpServletRequest) requestMock.proxy());
-        sessionMock.expect("getAttribute",
-                new Constraint[] {
-                    new IsEqual("test")
-                });
+        // Note: getAttribute() calls no longer expected after fix to ensure descendant (not ancestor) calls are made for
+        //   the SessionMap Map methods (i.e. the overrides are called, as expected).
         sessionMock.expect("setAttribute",
                 new Constraint[] {
                     new IsEqual("test"), new IsEqual("test value")
                 });
-        sessionMock.expect("getAttribute",
-                new Constraint[] {
-                    new IsEqual("anotherTest")
-                });
         sessionMock.expect("setAttribute",
                 new Constraint[] {
                     new IsEqual("anotherTest"), new IsEqual("another test value")
@@ -78,14 +73,6 @@
                 new Constraint[]{
                     new IsEqual("anotherTest")
                 });
-        sessionMock.expect("getAttribute",
-                new Constraint[] {
-                    new IsEqual("test")
-                });
-        sessionMock.expect("getAttribute",
-                new Constraint[] {
-                    new IsEqual("anotherTest")
-                });
         sessionMap.put("test", "test value");
         sessionMap.put("anotherTest", "another test value");
         sessionMap.clear();
@@ -121,7 +108,7 @@
         String key = "theKey";
         Object value = new Object();
         sessionMock.expectAndReturn("getAttribute", new Constraint[]{
-                new IsEqual(key.toString())
+                new IsEqual(key)
         }, value);
 
         SessionMap sessionMap = new SessionMap((HttpServletRequest) requestMock.proxy());
@@ -134,7 +121,7 @@
         Object value = new Object();
         sessionMock.expect("getAttribute", new Constraint[]{new IsAnything()});
         sessionMock.expect("setAttribute", new Constraint[]{
-                new IsEqual(key.toString()), new IsEqual(value)
+                new IsEqual(key), new IsEqual(value)
         });
 
         SessionMap sessionMap = new SessionMap((HttpServletRequest) requestMock.proxy());
@@ -159,7 +146,7 @@
     	MockHttpServletRequest request = new MockHttpServletRequest();
     	
         String key = "theKey";
-        Object someOtherKey = "someOtherKey";
+        String someOtherKey = "someOtherKey";
         Object value = new Object();
         
         SessionMap sessionMap = new SessionMap(request);
@@ -218,6 +205,79 @@
         sessionMock.verify();
     }
 
+    /** 
+     * Attempt to detect any changes that would make the attribute handling for puts produce different results
+     * for the SessionMap and underlying HttpSession attributes.
+     * 
+     * @throws Exception 
+     */
+    public void testPutResultInSessionAttributes() throws Exception {
+        Object value = new Object();
+
+        //HttpSession httpSessionMock = ((HttpServletRequest) requestMock.proxy()).getSession(false);
+        HttpSession httpSessionMock = (HttpSession) sessionMock.proxy();
+        sessionMock.expectAndReturn("getAttribute", new Constraint[]{
+                new IsEqual("KEY")
+        }, null);
+        sessionMock.expect("setAttribute", new Constraint[]{
+                new IsEqual("KEY"), new IsEqual(value)
+        });
+        sessionMock.expectAndReturn("getAttribute", new Constraint[]{
+                new IsEqual("KEY")
+        }, value);
+        sessionMock.expectAndReturn("getAttribute", new Constraint[]{
+                new IsEqual("KEY")
+        }, value);
+
+        SessionMap sessionMap = new SessionMap((HttpServletRequest) requestMock.proxy());
+        AbstractMap<String, Object> abstractMap = (AbstractMap<String, Object>) sessionMap;
+        abstractMap.put("KEY", value);
+        assertEquals("Underlying HttpSession attribute does not match after SessionMap put ?", abstractMap.get("KEY"), httpSessionMock.getAttribute("KEY"));
+        sessionMock.verify();
+    }
+
+    /**
+     * Attempt to detect any changes that would make the attribute handling for removes produce different results
+     * for the SessionMap and underlying HttpSession attributes.
+     * 
+     * @throws Exception 
+     */
+    public void testRemoveResultInSessionAttributes() throws Exception {
+        Object value = new Object();
+        Object removedValue;
+
+        //HttpSession httpSessionMock = ((HttpServletRequest) requestMock.proxy()).getSession(false);
+        HttpSession httpSessionMock = (HttpSession) sessionMock.proxy();
+        sessionMock.expectAndReturn("getAttribute", new Constraint[]{
+                new IsEqual("KEY")
+        }, null);
+        sessionMock.expect("setAttribute", new Constraint[]{
+                new IsEqual("KEY"), new IsEqual(value)
+        });
+        sessionMock.expectAndReturn("getAttribute", new Constraint[]{
+                new IsEqual("KEY")
+        }, value);
+        sessionMock.expect("removeAttribute", new Constraint[]{
+                new IsEqual("KEY")
+        });
+        sessionMock.expectAndReturn("getAttribute", new Constraint[]{
+                new IsEqual("KEY")
+        }, null);
+        sessionMock.expectAndReturn("getAttribute", new Constraint[]{
+                new IsEqual("KEY")
+        }, null);
+
+        SessionMap sessionMap = new SessionMap((HttpServletRequest) requestMock.proxy());
+        AbstractMap<String, Object> abstractMap = (AbstractMap<String, Object>) sessionMap;
+        abstractMap.put("KEY", value);
+        removedValue = abstractMap.remove("KEY");
+        assertEquals("Removed attribute not equal to put attribute ?", value, removedValue);
+        assertNull("Removed attribute still present in SessionMap ?", abstractMap.get("KEY"));
+        assertNull("Removed attribute still present in HttpSessionMock ?", httpSessionMock.getAttribute("KEY"));
+        sessionMock.verify();
+    }
+
+    @Override
     protected void setUp() throws Exception {
         sessionMock = new Mock(HttpSession.class);
         sessionMock.matchAndReturn("getId", "1");
@@ -235,16 +295,19 @@
 
         private static final long serialVersionUID = 8783604360786273764L;
 
-        private Map<String, Object> map = new HashMap<>();
+        private final Map<String, Object> map;
 
         public MockSessionMap(HttpServletRequest request) {
             super(request);
+            this.map = new HashMap<>();
         }
 
+        @Override
         public Object get(Object key) {
             return map.get(key);
         }
         
+        @Override
         public Object put(String key, Object value) {
             Object originalValue = super.put(key, value);
             map.put(key, value); //put the value into our map after putting it in the superclass map to avoid polluting the get call.
@@ -252,6 +315,7 @@
             return originalValue;
         }
 
+        @Override
         public void clear() {
             super.clear();
             map.clear();