]> source.dussan.org Git - vaadin-framework.git/commitdiff
Merged test case and fix for #3184 - "TransactionListener addition/iteration can...
authorArtur Signell <artur.signell@itmill.com>
Mon, 16 Nov 2009 10:51:48 +0000 (10:51 +0000)
committerArtur Signell <artur.signell@itmill.com>
Mon, 16 Nov 2009 10:51:48 +0000 (10:51 +0000)
svn changeset:9813/svn branch:6.2

src/com/vaadin/service/ApplicationContext.java
src/com/vaadin/terminal/gwt/server/WebApplicationContext.java
tests/src/com/vaadin/tests/server/TransactionListenersConcurrency.java [new file with mode: 0644]

index c5bafa8a6d05fad259698ed792030fc1ace352e9..308a936335f4a10b229604de05cfd574321d7286 100644 (file)
@@ -13,7 +13,8 @@ import com.vaadin.Application;
 /**
  * <code>ApplicationContext</code> provides information about the running
  * context of the application. Each context is shared by all applications that
- * are open for one user. In web-environment this corresponds to HttpSession.
+ * are open for one user. In a web-environment this corresponds to a
+ * HttpSession.
  * 
  * @author IT Mill Ltd.
  * @version
@@ -25,28 +26,28 @@ public interface ApplicationContext extends Serializable {
     /**
      * Returns application context base directory.
      * 
-     * Typically an application is deployed in a such way that is has
+     * Typically an application is deployed in a such way that is has an
      * application directory. For web applications this directory is the root
-     * directory of the web applications. In some cases application might not
-     * have application directory (for example web applications running inside
-     * of war).
+     * directory of the web applications. In some cases applications might not
+     * have an application directory (for example web applications running
+     * inside a war).
      * 
-     * @return The application base directory
+     * @return The application base directory or null if the application has no
+     *         base directory.
      */
     public File getBaseDirectory();
 
     /**
-     * Gets the applications in this context.
+     * Returns a collection of all the applications in this context.
      * 
-     * Gets all applications in this context. Each application context contains
-     * all applications that are open for one user.
+     * Each application context contains all active applications for one user.
      * 
-     * @return Collection containing all applications in this context
+     * @return A collection containing all the applications in this context.
      */
     public Collection getApplications();
 
     /**
-     * Adds transaction listener to this context.
+     * Adds transaction listener to this context.
      * 
      * @param listener
      *            the listener to be added.
@@ -55,7 +56,7 @@ public interface ApplicationContext extends Serializable {
     public void addTransactionListener(TransactionListener listener);
 
     /**
-     * Removes transaction listener from this context.
+     * Removes transaction listener from this context.
      * 
      * @param listener
      *            the listener to be removed.
@@ -64,9 +65,8 @@ public interface ApplicationContext extends Serializable {
     public void removeTransactionListener(TransactionListener listener);
 
     /**
-     * Interface for listening the application transaction events.
-     * Implementations of this interface can be used to listen all transactions
-     * between the client and the application.
+     * Interface for listening to transaction events. Implement this interface
+     * to listen to all transactions between the client and the application.
      * 
      */
     public interface TransactionListener extends Serializable {
@@ -74,6 +74,11 @@ public interface ApplicationContext extends Serializable {
         /**
          * Invoked at the beginning of every transaction.
          * 
+         * The transaction is linked to the context, not the application so if
+         * you have multiple applications running in the same context you need
+         * to check that the request is associated with the application you are
+         * interested in. This can be done looking at the application parameter.
+         * 
          * @param application
          *            the Application object.
          * @param transactionData
@@ -85,6 +90,11 @@ public interface ApplicationContext extends Serializable {
         /**
          * Invoked at the end of every transaction.
          * 
+         * The transaction is linked to the context, not the application so if
+         * you have multiple applications running in the same context you need
+         * to check that the request is associated with the application you are
+         * interested in. This can be done looking at the application parameter.
+         * 
          * @param applcation
          *            the Application object.
          * @param transactionData
index 304282fd9f4d50bc61eb7cafbbeb30c164ac2381..e49c95d905b751db1288b55bdddd8837cf6a83ed 100644 (file)
@@ -36,7 +36,8 @@ import com.vaadin.service.ApplicationContext;
 public class WebApplicationContext implements ApplicationContext,
         HttpSessionBindingListener, Serializable {
 
-    protected List<TransactionListener> listeners;
+    protected List<TransactionListener> listeners = Collections
+            .synchronizedList(new LinkedList<TransactionListener>());
 
     protected transient HttpSession session;
 
@@ -87,7 +88,7 @@ public class WebApplicationContext implements ApplicationContext,
     }
 
     /**
-     * Gets the application context for HttpSession.
+     * Gets the application context for an HttpSession.
      * 
      * @param session
      *            the HTTP session.
@@ -108,70 +109,68 @@ public class WebApplicationContext implements ApplicationContext,
     }
 
     /**
-     * Adds the transaction listener to this context.
+     * Adds a transaction listener to this context. The transaction listener is
+     * called before and after each each HTTP request related to this session
+     * except when serving static resources.
+     * 
      * 
      * @see com.vaadin.service.ApplicationContext#addTransactionListener(com.vaadin.service.ApplicationContext.TransactionListener)
      */
     public void addTransactionListener(TransactionListener listener) {
-        if (listeners == null) {
-            listeners = new LinkedList<TransactionListener>();
-        }
         listeners.add(listener);
     }
 
     /**
-     * Removes the transaction listener from this context.
+     * Removes a transaction listener from this context. The transaction
+     * listener is called before and after each each HTTP request related to
+     * this session except when serving static resources.
      * 
      * @see com.vaadin.service.ApplicationContext#removeTransactionListener(com.vaadin.service.ApplicationContext.TransactionListener)
      */
     public void removeTransactionListener(TransactionListener listener) {
-        if (listeners != null) {
-            listeners.remove(listener);
-        }
+        listeners.remove(listener);
 
     }
 
     /**
-     * Notifies the transaction start.
+     * Sends a notification that a transaction is starting.
      * 
      * @param application
+     *            The application associated with the transaction.
      * @param request
-     *            the HTTP request.
+     *            the HTTP request that triggered the transaction.
      */
     protected void startTransaction(Application application,
             HttpServletRequest request) {
-        if (listeners == null) {
-            return;
-        }
-        for (final Iterator i = listeners.iterator(); i.hasNext();) {
-            ((ApplicationContext.TransactionListener) i.next())
-                    .transactionStart(application, request);
+        synchronized (listeners) {
+            for (TransactionListener listener : listeners) {
+                listener.transactionStart(application, request);
+            }
         }
     }
 
     /**
-     * Notifies the transaction end.
+     * Sends a notification that a transaction has ended.
      * 
      * @param application
+     *            The application associated with the transaction.
      * @param request
-     *            the HTTP request.
+     *            the HTTP request that triggered the transaction.
      */
     protected void endTransaction(Application application,
             HttpServletRequest request) {
-        if (listeners == null) {
-            return;
-        }
-
         LinkedList<Exception> exceptions = null;
-        for (final Iterator i = listeners.iterator(); i.hasNext();) {
-            try {
-                ((ApplicationContext.TransactionListener) i.next())
-                        .transactionEnd(application, request);
-            } catch (final RuntimeException t) {
-                if (exceptions == null) {
-                    exceptions = new LinkedList<Exception>();
+
+        synchronized (listeners) {
+            for (TransactionListener listener : listeners) {
+                try {
+                    listener.transactionEnd(application, request);
+                } catch (final RuntimeException t) {
+                    if (exceptions == null) {
+                        exceptions = new LinkedList<Exception>();
+                    }
+                    exceptions.add(t);
                 }
-                exceptions.add(t);
             }
         }
 
diff --git a/tests/src/com/vaadin/tests/server/TransactionListenersConcurrency.java b/tests/src/com/vaadin/tests/server/TransactionListenersConcurrency.java
new file mode 100644 (file)
index 0000000..d33163d
--- /dev/null
@@ -0,0 +1,185 @@
+package com.vaadin.tests.server;\r
+\r
+import static org.easymock.EasyMock.createMock;\r
+\r
+import java.lang.Thread.UncaughtExceptionHandler;\r
+import java.lang.reflect.InvocationTargetException;\r
+import java.lang.reflect.Method;\r
+import java.net.MalformedURLException;\r
+import java.net.URL;\r
+import java.util.ArrayList;\r
+import java.util.Iterator;\r
+import java.util.List;\r
+import java.util.Properties;\r
+import java.util.Random;\r
+\r
+import javax.servlet.http.HttpServletRequest;\r
+import javax.servlet.http.HttpSession;\r
+\r
+import junit.framework.TestCase;\r
+\r
+import org.easymock.EasyMock;\r
+\r
+import com.vaadin.Application;\r
+import com.vaadin.service.ApplicationContext.TransactionListener;\r
+import com.vaadin.terminal.gwt.server.WebApplicationContext;\r
+\r
+public class TransactionListenersConcurrency extends TestCase {\r
+\r
+    /**\r
+     * This test starts N threads concurrently. Each thread creates an\r
+     * application which adds a transaction listener to the context. A\r
+     * transaction is then started for each application. Some semi-random delays\r
+     * are included so that calls to addTransactionListener and\r
+     * WebApplicationContext.startTransaction are mixed.\r
+     * \r
+     */\r
+    public void testTransactionListeners() throws Exception {\r
+        final List<Throwable> exceptions = new ArrayList<Throwable>();\r
+\r
+        HttpSession session = createSession();\r
+        final WebApplicationContext context = WebApplicationContext\r
+                .getApplicationContext(session);\r
+        List<Thread> threads = new ArrayList<Thread>();\r
+\r
+        for (int i = 0; i < 5; i++) {\r
+            Thread t = new Thread(new Runnable() {\r
+\r
+                public void run() {\r
+                    Application app = new Application() {\r
+\r
+                        @Override\r
+                        public void init() {\r
+                            // Sleep 0-1000ms so another transaction has time to\r
+                            // start before we add the transaction listener.\r
+                            try {\r
+                                Thread.sleep((long) (1000 * new Random()\r
+                                        .nextDouble()));\r
+                            } catch (InterruptedException e) {\r
+                                // TODO Auto-generated catch block\r
+                                e.printStackTrace();\r
+                            }\r
+\r
+                            getContext().addTransactionListener(\r
+                                    new DelayTransactionListener(2000));\r
+                        }\r
+\r
+                    };\r
+\r
+                    // Start the application so the transaction listener is\r
+                    // called later on.\r
+                    try {\r
+\r
+                        app.start(new URL("http://localhost/"),\r
+                                new Properties(), context);\r
+                    } catch (MalformedURLException e) {\r
+                        // TODO Auto-generated catch block\r
+                        e.printStackTrace();\r
+                    }\r
+\r
+                    try {\r
+                        // Call the transaction listener using reflection as\r
+                        // startTransaction is protected.\r
+\r
+                        Method m = context.getClass().getDeclaredMethod(\r
+                                "startTransaction", Application.class,\r
+                                HttpServletRequest.class);\r
+                        m.setAccessible(true);\r
+                        m.invoke(context, app, null);\r
+                    } catch (Exception e) {\r
+                        throw new RuntimeException(e);\r
+                    }\r
+                }\r
+\r
+            });\r
+\r
+            threads.add(t);\r
+            t.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {\r
+\r
+                public void uncaughtException(Thread t, Throwable e) {\r
+                    e = e.getCause();\r
+                    exceptions.add(e);\r
+                }\r
+            });\r
+        }\r
+\r
+        // Start the threads and wait for all of them to finish\r
+        for (Thread t : threads) {\r
+            t.start();\r
+        }\r
+        int running = threads.size();\r
+\r
+        while (running > 0) {\r
+            for (Iterator<Thread> i = threads.iterator(); i.hasNext();) {\r
+                Thread t = i.next();\r
+                if (!t.isAlive()) {\r
+                    running--;\r
+                    i.remove();\r
+                }\r
+            }\r
+        }\r
+\r
+        for (Throwable t : exceptions) {\r
+            if (t instanceof InvocationTargetException) {\r
+                t = t.getCause();\r
+            }\r
+            t.printStackTrace(System.err);\r
+            fail(t.getClass().getName());\r
+        }\r
+\r
+        System.out.println("Done, all ok");\r
+\r
+    }\r
+\r
+    /**\r
+     * Creates a HttpSession mock\r
+     * \r
+     */\r
+    private static HttpSession createSession() {\r
+        HttpSession session = createMock(HttpSession.class);\r
+        EasyMock.expect(\r
+                session.getAttribute(WebApplicationContext.class.getName()))\r
+                .andReturn(null).anyTimes();\r
+        session.setAttribute(\r
+                EasyMock.eq(WebApplicationContext.class.getName()), EasyMock\r
+                        .anyObject());\r
+\r
+        EasyMock.replay(session);\r
+        return session;\r
+    }\r
+\r
+    /**\r
+     * A transaction listener that just sleeps for the given amount of time in\r
+     * transactionStart and transactionEnd.\r
+     * \r
+     */\r
+    public static class DelayTransactionListener implements TransactionListener {\r
+\r
+        private int delay;\r
+\r
+        public DelayTransactionListener(int delay) {\r
+            this.delay = delay;\r
+        }\r
+\r
+        public void transactionStart(Application application,\r
+                Object transactionData) {\r
+            try {\r
+                Thread.sleep(delay);\r
+            } catch (InterruptedException e) {\r
+                e.printStackTrace();\r
+            }\r
+\r
+        }\r
+\r
+        public void transactionEnd(Application application,\r
+                Object transactionData) {\r
+            try {\r
+                Thread.sleep(delay);\r
+            } catch (InterruptedException e) {\r
+                e.printStackTrace();\r
+            }\r
+\r
+        }\r
+    }\r
+\r
+}\r