Anonymous avatar Anonymous committed 3d058eb

Client Dispatcher

git-svn-id: http://svn.opensymphony.com/svn/webwork/trunk@535 573baa09-0c28-0410-bef9-dab3c582ae83

Comments (0)

Files changed (32)

     <property name="build.java-test" value="${build}/java-test"/>
     <property name="build.java" value="${build}/java"/>
     <property name="build.example" value="${build}/example"/>
+    <property name="build.example.jars" value="${build}/example-jars"/>
     <property name="build.example-war" value="${build}/example-war"/>
+    <property name="build.example.client.jar" value="${build}/example-client.jar"/>
     <property name="build.config-browser" value="${build}/config-browser"/>
     <property name="build.config-browser.jar" value="${build}/webwork-config-browser.jar"/>
     <property name="build.migration" value="${build}/migration"/>
     <property name="build.dist" value="${build}/dist"/>
     <property name="build.webwork.tld" value="${src.webapp}/WEB-INF/webwork.tld"/>
 
+    <property name="demo.keystore.password" value="webwork"/>
+    <property name="demo.keystore" value="${src.etc}/security/demoKeystore"/>
+
     <property name="docs" value="docs"/>
 
     <target name="clean">
         <jar basedir="${build.migration}" jarfile="${build.migration.jar}"/>
     </target>
 
-    <target name="example-war" depends="java, jar, config-browser">
+    <target name="example-jar" depends="java, jar, config-browser">
         <mkdir dir="${build.example}"/>
         <javac srcdir="${src.example}" destdir="${build.example}" classpath="${build.java}" classpathref="cp" debug="on"/>
         <copy filtering="no" todir="${build.example}">
 
         <copy file="${src.etc}/taglib.tld" tofile="${build}/webwork.tld"/>
 
+        <jar basedir="${build.example}" jarfile="${build.example.client.jar}">
+                <include name="**/client/**"/>
+                <include name="**/log4j.properties"/>
+        </jar>
+
+        <jar basedir="${src.etc}/example/client" jarfile="${build.example.client.jar}" update="true">
+                <include name="*.xml"/>
+        </jar>
+    </target>
+
+    <target name="signjar" depends="example-jar">
+        <!-- Copy the jars for the client before signing them - we don't want the server jars signed -->
+        <mkdir dir="${build.example.jars}"/>
+        <copy todir="${build.example.jars}">
+            <fileset dir="${lib.core}">
+                <include name="cglib-full-2.0.2.jar"/>
+                <include name="commons-logging.jar"/>
+                <include name="ognl.jar"/>
+                <include name="oscore.jar"/>
+                <include name="xwork.jar"/>
+            </fileset>
+            <fileset file="${build}/${name}-${version}.jar"/>
+            <fileset file="${build.example.client.jar}"/>
+            <fileset file="${lib.build}/servlet.jar"/>
+            <fileset dir="${lib.optional}">
+                <include name="object-dispatcher.jar"/>
+                <include name="commons-digester.jar"/>
+                <include name="commons-collections.jar"/>
+                <include name="commons-beanutils.jar"/>
+                <include name="log4j-1.2.8.jar"/>
+            </fileset>
+        </copy>
+        <signjar
+           keystore="${demo.keystore}" alias="demo"
+              storepass="${demo.keystore.password}"
+           verbose="false"
+        >
+            <fileset dir="${build.example.jars}">
+                <include name="cglib-full-2.0.2.jar"/>
+                <include name="commons-logging.jar"/>
+                <include name="commons-digester.jar"/>
+                <include name="commons-collections.jar"/>
+                <include name="commons-beanutils.jar"/>
+                <include name="ognl.jar"/>
+                <include name="oscore.jar"/>
+                <include name="xwork.jar"/>
+                <include name="${name}-${version}.jar"/>
+                <include name="example-client.jar"/>
+                <include name="servlet.jar"/>
+                <include name="object-dispatcher.jar"/>
+                <include name="log4j-1.2.8.jar"/>
+            </fileset>
+        </signjar>
+    </target>
+
+    <target name="example-war" depends="signjar">
+
         <war destfile="${build}/${name}-example.war" webxml="${src.webapp}/WEB-INF/web.xml">
             <fileset dir="${src.webapp}">
                 <exclude name="**/web.xml"/>
             </fileset>
+            <fileset dir="${build.example.jars}">
+                <include name="cglib-full-2.0.2.jar"/>
+                <include name="commons-logging.jar"/>
+                <include name="commons-digester.jar"/>
+                <include name="commons-collections.jar"/>
+                <include name="commons-beanutils.jar"/>
+                <include name="ognl.jar"/>
+                <include name="oscore.jar"/>
+                <include name="xwork.jar"/>
+                <include name="${name}-${version}.jar"/>
+                <include name="example-client.jar"/>
+                <include name="servlet.jar"/>
+                <include name="object-dispatcher.jar"/>
+                <include name="log4j-1.2.8.jar"/>
+            </fileset>
             <lib dir="${lib.core}"/>
             <lib dir="${lib.optional}"/>
             <lib dir="${lib.example}"/>
             <lib file="${build}/${name}-${version}.jar"/>
             <lib file="${build.config-browser.jar}"/>
+            <lib file="${build.example.client.jar}"/>
             <webinf file="${build}/webwork.tld"/>
             <classes dir="${build.example}"/>
             <classes dir="${src.etc}/example"/>

src/example/com/opensymphony/webwork/example/client/Demo.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.example.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Properties;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+
+import com.opensymphony.webwork.dispatcher.client.ClientException;
+import com.opensymphony.webwork.dispatcher.client.TransportFactory;
+import com.opensymphony.webwork.dispatcher.client.TransportHttp;
+import com.opensymphony.xwork.dispatcher.MappingFactory;
+import com.opensymphony.xwork.dispatcher.ObjectDispatcher;
+import com.opensymphony.xwork.dispatcher.ResultObjectFactory;
+import com.opensymphony.xwork.dispatcher.XMLMappingFactory;
+import com.opensymphony.xwork.util.OgnlValueStack;
+import java.io.StringWriter;
+import java.io.PrintWriter;
+import com.opensymphony.xwork.ObjectFactory;
+import com.opensymphony.webwork.dispatcher.client.HTTPClientObjectFactory;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import com.opensymphony.xwork.ActionProxyFactory;
+import com.opensymphony.webwork.dispatcher.client.HTTPClientActionProxyFactory;
+
+
+/**
+ * SWING-based demonstration of Client Dispatcher and notification system.
+ * Requires <code>demo.properties</code> to be available on classpath.
+ *
+ * @version $Id$
+ * @author Ben Alex (<a href="mailto:ben.alex@acegi.com.au">ben.alex@acegi.com.au</a>)
+ */
+public class Demo extends JFrame {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private DemoResult taskOutput;
+    private TransportFactory transportFactory;
+    private int taskNumber = 1;
+
+    /** logger for this class */
+    private static final Log log = LogFactory.getLog(Demo.class);
+
+
+    //~ Constructors ///////////////////////////////////////////////////////////
+
+    /**
+     * Construct the demo object
+     * @param connectionURL The URL to connect to to process the remote action invocations. If this
+     * is null it will be searched for in the demo.properties file
+     * @throws ClientException
+     * @throws IOException
+     */
+    public Demo(String connectionURL) throws ClientException, IOException {
+        super("WebWork2 Client Dispatcher Demonstration");
+
+        // Load in the properties
+        URL url = this.getClass().getClassLoader().getResource("com/opensymphony/webwork/example/client/demo.properties");
+
+        if (url == null) {
+            throw new IOException("demo.properties not found on classpath");
+        }
+
+        InputStream inputStream = url.openStream();
+        Properties props = new Properties();
+        props.load(inputStream);
+
+        if (connectionURL != null) {
+            props.setProperty(TransportHttp.KEY_URL, connectionURL);
+        }
+
+        // Create the connection factory
+        transportFactory = new TransportHttp(props);
+
+        // Set up the action factory
+        ObjectFactory.setObjectFactory(new HTTPClientObjectFactory(transportFactory));
+        ActionProxyFactory.setFactory(new HTTPClientActionProxyFactory());
+
+        // Create a progress consumer and attach it to the connection factory
+        ProgressConsumerToolBar consumer = new ProgressConsumerToolBar(this);
+        transportFactory.setProgressConsumer(consumer);
+        consumer.setBorderPainted(true);
+        consumer.setFloatable(false);
+
+        //consumer.setToolTipText("Progress Monitor");
+        //Create the demo's UI.
+        JButton startButton = new JButton("Start New Client Request");
+        startButton.setActionCommand("start");
+        startButton.addActionListener(new ButtonListener());
+
+        taskOutput = new DemoResult();
+
+        JPanel panel = new JPanel();
+        panel.add(startButton);
+
+        JPanel contentPane = new JPanel();
+        contentPane.setLayout(new BorderLayout());
+        contentPane.add(panel, BorderLayout.NORTH);
+        contentPane.add(consumer, BorderLayout.SOUTH);
+        contentPane.add(new JScrollPane(taskOutput), BorderLayout.CENTER);
+        contentPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+        setContentPane(contentPane);
+    }
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public static void main(String[] args) throws ClientException, IOException {
+        JFrame frame = new Demo(args[0]);
+        frame.addWindowListener(new WindowAdapter() {
+                public void windowClosing(WindowEvent e) {
+                    System.exit(0);
+                }
+            });
+
+        frame.pack();
+        frame.setVisible(true);
+    }
+
+    //~ Inner Classes //////////////////////////////////////////////////////////
+
+    class ActualTask implements ResultObjectFactory {
+        ActualTask() {
+            int thisTaskNumber = ++taskNumber;
+            taskOutput.getInvocationNumber().setText(taskNumber + "");
+            taskOutput.getMessages().setText("");
+
+            OgnlValueStack result = null;
+            try {
+                MappingFactory mappingFactory = new XMLMappingFactory();
+                ObjectDispatcher objectDispatcher = new ObjectDispatcher(mappingFactory);
+                result = objectDispatcher.dispatch(taskOutput,
+                    "/clientDispatcher",
+                    "DemoAction",
+                    "demo-mappings",
+                    this);
+            } catch (Exception ex) {
+                StringWriter stackTrace = new StringWriter();
+
+                ex.printStackTrace(new PrintWriter(stackTrace, true));
+                taskOutput.getMessages().append("Error during invocation number: " + thisTaskNumber
+                                                + ": " + "\n");
+                taskOutput.getMessages().append(stackTrace + "\n");
+            }
+        }
+
+        /**
+         * Get a result object to copy the information to
+         * @param viewName nominally the name of the result object but we only have 1 in this case
+         * @return the task output panel
+         */
+        public Object getResultObject(String viewName) {
+            // only 1 possible object
+            log.debug("Returning result object: " + taskOutput);
+            if ("error".equals(viewName)) {
+                taskOutput.getMessages().append("The server reported an error whilst executing the action\n");
+            }
+            return taskOutput;
+        }
+    }
+
+    class ButtonListener implements ActionListener {
+        public void actionPerformed(ActionEvent evt) {
+            final DemoSwingWorker worker = new DemoSwingWorker() {
+                public Object construct() {
+                    return new ActualTask();
+                }
+            };
+
+            worker.start();
+        }
+    }
+}

src/example/com/opensymphony/webwork/example/client/DemoAction.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.example.client;
+
+import java.io.Serializable;
+
+import com.opensymphony.webwork.dispatcher.client.ClientRequest;
+import com.opensymphony.webwork.dispatcher.client.ClientRequestInvocation;
+import com.opensymphony.xwork.Action;
+
+
+/**
+ * WebWork Action used by the {@link Demo Demo} class.
+ *
+ * @version $Id$
+ * @author Ben Alex (<a href="mailto:ben.alex@acegi.com.au">ben.alex@acegi.com.au</a>)
+ */
+public class DemoAction implements Action, ClientRequest {
+    //~ Static fields/initializers /////////////////////////////////////////////
+
+    /**
+     * Variable solely to increase object size.
+     */
+    static Acl acl = new Acl();
+
+    /**
+     * Variable solely to increase object size.
+     */
+    static Bcl bcl = new Bcl(acl);
+
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    Bcl param2;
+    int param1;
+    int result1;
+
+    /**
+     * Variable solely to increase object size.
+     */
+    private Bcl junkHolder = null;
+
+    /**
+     * Required for {@link ClientRequest ClientRequest} interface.
+     */
+    private ClientRequestInvocation clientRequestInvocation;
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public void setClientRequestInvocation(ClientRequestInvocation clientRequestInvocation) {
+        this.clientRequestInvocation = clientRequestInvocation;
+    }
+
+    public ClientRequestInvocation getClientRequestInvocation() {
+        return clientRequestInvocation;
+    }
+
+    /**
+     * Set the number we would like squared.
+     */
+    public void setParam1(int param1) {
+        this.param1 = param1;
+    }
+
+    /**
+     * Getter for property param1.
+     * @return the number we would like squared
+     */
+    public int getParam1() {
+        return param1;
+    }
+
+    /**
+     * Set an object array to increase the size of the request or reply.
+     */
+    public void setParam2(Bcl param2) {
+        this.param2 = param2;
+    }
+
+    /**
+     * Get the result of squaring <code>param1</code>. You must call
+     * {@link DemoAction#execute() DemoAction.execute()} first, after first
+     * building a {@link ClientRequest ClientRequest} proxy using the
+     * {@link TransportFactory#createClientRequestProxy(ClientRequest)
+     * TransportFactory.createClientRequestProxy(ClientRequest)} method.
+     */
+    public int getResult1() {
+        return result1;
+    }
+
+    /**
+     * Execute method. We return an <code>ERROR</code> if number 7 is the
+     * <code>param1</code>, to demonstrate the operation of
+     * {@link ClientRequestInvocation#getResultCode()
+     * ClientRequestInvocation.getResultCode()}. We return <code>SUCCESS</code>
+     * if any number other than 7 was provided.
+     */
+    public String execute() throws Exception {
+        junkHolder = new Bcl(acl); // make return object bigger
+
+        if (param1 == 7) {
+            result1 = 7;
+
+            return ERROR;
+        } else {
+            result1 = param1 * param1;
+
+            return SUCCESS;
+        }
+    }
+
+    //~ Inner Classes //////////////////////////////////////////////////////////
+
+    /**
+     * Class that is used solely to increase the object's serialized size.
+     */
+    private static class Acl {
+        static String[][][] aclvar = new String[4][366][999];
+    }
+
+    /**
+     * Class that is used solely to increase the object's serialized size.
+     */
+    private static class Bcl implements Serializable {
+        String[][][] bclv = new String[4][366][999];
+
+        Bcl(Acl acl) {
+            for (int a = 0; a < 999; a++) {
+                for (int l = 0; l < 366; l++) {
+                    for (int w = 0; w < 4; w++) {
+                        bclv[w][l][a] = Acl.aclvar[w][l][a];
+                    }
+                }
+            }
+        }
+    }
+}

src/example/com/opensymphony/webwork/example/client/DemoSwingWorker.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+/*
+ * This class is freely available on the java.sun.com web site. In accordance
+ * with a statement on that web site, "Unless otherwise licensed, code in all
+ * technical manuals herein (including articles, FAQs, samples) is provided
+ * under this License" with "this License" being found at URL
+ * http://developer.java.sun.com/berkeley_license.html. A copy of the
+ * license at that URL is contained below:
+ *
+ * ----------------------------------------------------------------------------
+ *
+ * Copyright� 2003 Sun Microsystems, Inc. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * Redistribution of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * Redistribution in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of Sun Microsystems, Inc. or the names of contributors may
+ * be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * This software is provided "AS IS," without a warranty of any kind. ALL
+ * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
+ * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR
+ * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") AND
+ * ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A
+ * RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
+ * IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT
+ * OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR
+ * PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
+ * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS
+ * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+ *
+ * You acknowledge that this software is not designed, licensed or intended
+ * for use in the design, construction, operation or maintenance of any nuclear
+ * facility.
+ *
+ * ----------------------------------------------------------------------------
+ *
+ * The above was taken from the java.sun.com web site on 24 July 2003.
+ *
+ * In addition, the forum posting at
+ * http://forum.java.sun.com/thread.jsp?forum=57&thread=283051 appears to
+ * confirm the interpretation that the above license applies to this class.
+ */
+package com.opensymphony.webwork.example.client;
+
+import javax.swing.SwingUtilities;
+
+
+/**
+ * This is the 3rd version of SwingWorker (also known as
+ * SwingWorker 3), an abstract class that you subclass to
+ * perform GUI-related work in a dedicated thread.  For
+ * instructions on and examples of using this class, see:
+ *
+ * http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html
+ *
+ * Note that the API changed slightly in the 3rd version:
+ * You must now invoke start() on the SwingWorker after
+ * creating it.
+ */
+public abstract class DemoSwingWorker {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private Object value; // see getValue(), setValue()
+    private ThreadVar threadVar;
+
+    //~ Constructors ///////////////////////////////////////////////////////////
+
+    /**
+     * Start a thread that will call the <code>construct</code> method
+     * and then exit.
+     */
+    public DemoSwingWorker() {
+        final Runnable doFinished = new Runnable() {
+            public void run() {
+                finished();
+            }
+        };
+
+        Runnable doConstruct = new Runnable() {
+            public void run() {
+                try {
+                    setValue(construct());
+                } finally {
+                    threadVar.clear();
+                }
+
+                SwingUtilities.invokeLater(doFinished);
+            }
+        };
+
+        Thread t = new Thread(doConstruct);
+        threadVar = new ThreadVar(t);
+    }
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * Compute the value to be returned by the <code>get</code> method.
+     */
+    public abstract Object construct();
+
+    /**
+     * Called on the event dispatching thread (not on the worker thread)
+     * after the <code>construct</code> method has returned.
+     */
+    public void finished() {
+    }
+
+    /**
+     * Return the value created by the <code>construct</code> method.
+     * Returns null if either the constructing thread or the current
+     * thread was interrupted before a value was produced.
+     *
+     * @return the value created by the <code>construct</code> method
+     */
+    public Object get() {
+        while (true) {
+            Thread t = threadVar.get();
+
+            if (t == null) {
+                return getValue();
+            }
+
+            try {
+                t.join();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt(); // propagate
+
+                return null;
+            }
+        }
+    }
+
+    /**
+     * A new method that interrupts the worker thread.  Call this method
+     * to force the worker to stop what it's doing.
+     */
+    public void interrupt() {
+        Thread t = threadVar.get();
+
+        if (t != null) {
+            t.interrupt();
+        }
+
+        threadVar.clear();
+    }
+
+    /**
+     * Start the worker thread.
+     */
+    public void start() {
+        Thread t = threadVar.get();
+
+        if (t != null) {
+            t.start();
+        }
+    }
+
+    /**
+     * Get the value produced by the worker thread, or null if it
+     * hasn't been constructed yet.
+     */
+    protected synchronized Object getValue() {
+        return value;
+    }
+
+    /**
+     * Set the value produced by worker thread
+     */
+    private synchronized void setValue(Object x) {
+        value = x;
+    }
+
+    //~ Inner Classes //////////////////////////////////////////////////////////
+
+    /**
+     * Class to maintain reference to current worker thread
+     * under separate synchronization control.
+     */
+    private static class ThreadVar {
+        private Thread thread;
+
+        ThreadVar(Thread t) {
+            thread = t;
+        }
+
+        synchronized void clear() {
+            thread = null;
+        }
+
+        synchronized Thread get() {
+            return thread;
+        }
+    }
+}

src/example/com/opensymphony/webwork/example/client/ProgressConsumerToolBar.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.example.client;
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import java.net.URL;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+import javax.swing.JScrollPane;
+import javax.swing.JTabbedPane;
+import javax.swing.JTable;
+import javax.swing.JToolBar;
+import javax.swing.table.AbstractTableModel;
+import com.opensymphony.webwork.dispatcher.client.*;
+
+
+/**
+ * Provides a <code>JToolBar</code> that is also capable of acting as a
+ * {@link ProgressConsumer ProgressConsumer}. This class uses a series of
+ * <code>GIF</code> files to display various status and security levels. These
+ * <code>GIF</code> files must be located on the classpath.<BR><BR>
+ *
+ * We thank the Eclipse project for providing the <code>GIF</code> files
+ * shipped with the <code>ProgressConsumerToolBar</code>. The Eclipse project
+ * license can be found at <a href="http://www.eclipse.org/legal/cpl-v10.html">
+ * http://www.eclipse.org/legal/cpl-v10.html</a>.
+ *
+ * @version $Id$
+ * @author Ben Alex (<a href="mailto:ben.alex@acegi.com.au">ben.alex@acegi.com.au</a>)
+ */
+public class ProgressConsumerToolBar extends JToolBar implements ProgressConsumer {
+    //~ Static fields/initializers /////////////////////////////////////////////
+
+    private static String NONE = new String("NONE");
+    private static String ZERO = new String("0");
+
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private Component parent;
+    private ImageIcon securityEncrypted = new ImageIcon(getUrl("com/opensymphony/webwork/example/client/security-encrypted.gif"), "Encrypted Connection (no identity assurance)");
+    private ImageIcon securityIdentified = new ImageIcon(getUrl("com/opensymphony/webwork/example/client/security-identified.gif"), "Encrypted and Identity Assured Connection");
+    private ImageIcon securityNone = new ImageIcon(getUrl("com/opensymphony/webwork/example/client/security-none.gif"), "Insecure Connection");
+    private ImageIcon statusConnecting = new ImageIcon(getUrl("com/opensymphony/webwork/example/client/status-connecting.gif"), "Connecting to Remote");
+    private ImageIcon statusNone = new ImageIcon(getUrl("com/opensymphony/webwork/example/client/status-none.gif"), "");
+    private ImageIcon statusProcessing = new ImageIcon(getUrl("com/opensymphony/webwork/example/client/status-processing.gif"), "Remote is Processing");
+    private ImageIcon statusReceived = new ImageIcon(getUrl("com/opensymphony/webwork/example/client/status-received.gif"), "Received");
+    private ImageIcon statusReceiving = new ImageIcon(getUrl("com/opensymphony/webwork/example/client/status-receiving.gif"), "Receiving from Remote");
+    private ImageIcon statusRetryDelay = new ImageIcon(getUrl("com/opensymphony/webwork/example/client/status-retry-delay.gif"), "Retrying");
+    private ImageIcon statusSending = new ImageIcon(getUrl("com/opensymphony/webwork/example/client/status-sending.gif"), "Sending to Remote");
+    private JButton securityButton;
+    private JButton statusButton;
+    private JLabel outstandingLabel = new JLabel();
+    private JProgressBar progressBar = new JProgressBar();
+    private Map notifications = new HashMap();
+    private Properties lastFactoryInformation;
+    private Properties lastSecurityInformation;
+    private String currentProgressId;
+
+    //~ Constructors ///////////////////////////////////////////////////////////
+
+    /**
+     * Create a ProgressConsumerToolbar. The parent Component is required so
+     * a dialog can be created if a user clicks on the status or security icon.
+     */
+    public ProgressConsumerToolBar(Component parent) {
+        Properties props = new Properties();
+        props.setProperty("Status", "Unavailable (no connection attempted)");
+        lastFactoryInformation = props;
+        lastSecurityInformation = props;
+        this.parent = parent;
+        outstandingLabel.setFont(new Font("Monospaced", Font.PLAIN, 12));
+        outstandingLabel.setText(ZERO);
+        outstandingLabel.setToolTipText("Number of Pending Remote Actions");
+        statusButton = new JButton(statusNone);
+        statusButton.setBorderPainted(false);
+        statusButton.setFocusPainted(false);
+        statusButton.setMargin(new Insets(0, 0, 0, 0));
+        statusButton.addActionListener(new ButtonListener());
+        securityButton = new JButton(statusNone);
+        securityButton.setBorderPainted(false);
+        securityButton.setFocusPainted(false);
+        securityButton.setMargin(new Insets(0, 0, 0, 0));
+        securityButton.addActionListener(new ButtonListener());
+        add(outstandingLabel);
+        add(statusButton);
+        add(securityButton);
+        add(progressBar);
+        currentProgressId = NONE;
+    }
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * Method called by the connection factory to advise of a progress update.
+     */
+    public synchronized void notify(ProgressNotification progressNotification) {
+        if (progressNotification.getStatus() == ProgressNotification.STATUS_NONE) {
+            notifications.remove(progressNotification.getId());
+        } else {
+            notifications.put(progressNotification.getId(), progressNotification);
+
+            if (currentProgressId.equals(NONE)) {
+                currentProgressId = progressNotification.getId();
+            }
+        }
+
+        if (progressNotification.getId().equals(currentProgressId)) {
+            if (progressNotification.getStatus() == ProgressNotification.STATUS_NONE) {
+                if (notifications.size() > 0) {
+                    Iterator iterator = notifications.keySet().iterator();
+                    currentProgressId = (String) iterator.next();
+                    progressNotification = (ProgressNotification) notifications.get(currentProgressId);
+                } else {
+                    currentProgressId = NONE;
+                }
+            }
+        }
+
+        if (currentProgressId.equals(NONE)) {
+            outstandingLabel.setText(ZERO);
+            statusButton.setIcon(statusNone);
+            statusButton.setToolTipText("");
+            progressBar.setIndeterminate(false);
+            progressBar.setValue(0);
+            progressBar.setToolTipText("");
+        } else {
+            int remaining = notifications.size();
+            outstandingLabel.setText(new Integer(remaining).toString());
+            lastFactoryInformation = progressNotification.getFactoryInformation();
+            lastSecurityInformation = progressNotification.getSecurityInformation();
+
+            if (progressNotification.getSecurity() == ProgressNotification.SECURITY_NONE) {
+                securityButton.setIcon(securityNone);
+                securityButton.setToolTipText(securityNone.getDescription());
+            } else if (progressNotification.getSecurity() == ProgressNotification.SECURITY_ENCRYPTED) {
+                securityButton.setIcon(securityEncrypted);
+                securityButton.setToolTipText(securityEncrypted.getDescription());
+            } else if (progressNotification.getSecurity() == ProgressNotification.SECURITY_IDENTIFIED) {
+                securityButton.setIcon(securityIdentified);
+                securityButton.setToolTipText(securityIdentified.getDescription());
+            }
+
+            if (progressNotification.getStatus() == ProgressNotification.STATUS_NONE) {
+                statusButton.setIcon(statusNone);
+                statusButton.setToolTipText("");
+                progressBar.setIndeterminate(false);
+                progressBar.setValue(0);
+                progressBar.setToolTipText("");
+            } else if (progressNotification.getStatus() == ProgressNotification.STATUS_CONNECTING) {
+                statusButton.setIcon(statusConnecting);
+                statusButton.setToolTipText(statusConnecting.getDescription());
+                progressBar.setIndeterminate(true);
+                progressBar.setToolTipText("Trying...");
+            } else if (progressNotification.getStatus() == ProgressNotification.STATUS_PROCESSING) {
+                statusButton.setIcon(statusProcessing);
+                statusButton.setToolTipText(statusProcessing.getDescription());
+                progressBar.setIndeterminate(true);
+                progressBar.setToolTipText("Waiting...");
+            } else if (progressNotification.getStatus() == ProgressNotification.STATUS_RECEIVING) {
+                statusButton.setIcon(statusReceiving);
+                statusButton.setToolTipText(statusReceiving.getDescription());
+                progressBar.setIndeterminate(false);
+
+                float completed = (float) progressNotification.getInputReceived();
+                float ofTotal = (float) progressNotification.getInputSize();
+                progressBar.setValue((int) (completed / ofTotal * 100));
+                progressBar.setToolTipText("Receive Progress...");
+            } else if (progressNotification.getStatus() == ProgressNotification.STATUS_RECEIVED) {
+                statusButton.setIcon(statusReceived);
+                statusButton.setToolTipText(statusReceived.getDescription());
+                progressBar.setIndeterminate(false);
+                progressBar.setValue(0);
+                progressBar.setToolTipText("");
+            } else if (progressNotification.getStatus() == ProgressNotification.STATUS_RETRY_DELAY) {
+                statusButton.setIcon(statusRetryDelay);
+                statusButton.setToolTipText(statusRetryDelay.getDescription());
+                progressBar.setIndeterminate(true);
+                progressBar.setToolTipText("Retrying...");
+            } else if (progressNotification.getStatus() == ProgressNotification.STATUS_SENDING) {
+                statusButton.setIcon(statusSending);
+                statusButton.setToolTipText(statusSending.getDescription());
+                progressBar.setIndeterminate(false);
+
+                float completed = (float) progressNotification.getOutputTransmitted();
+                float ofTotal = (float) progressNotification.getOutputSize();
+                progressBar.setValue((int) (completed / ofTotal * 100));
+                progressBar.setToolTipText("Send Progress...");
+            }
+        }
+    }
+
+    private URL getUrl(String filename) {
+        URL url = this.getClass().getClassLoader().getResource(filename);
+
+        return url;
+    }
+
+    /**
+     * Create a <code>JPanel</code> containing the given
+     * <code>Properties</code>.
+     */
+    private Component makeTab(Properties info) {
+        JPanel panel = new JPanel();
+
+        PropertiesTableModel propsModel = new PropertiesTableModel();
+        propsModel.setupData(info);
+
+        JTable table = new JTable(propsModel);
+        table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+        table.setRowSelectionAllowed(true);
+        table.setColumnSelectionAllowed(false);
+        table.setCellSelectionEnabled(false);
+        table.setPreferredScrollableViewportSize(new Dimension(500, 400));
+        table.getColumnModel().getColumn(0).setPreferredWidth(150);
+        table.getColumnModel().getColumn(1).setPreferredWidth(1000);
+
+        JScrollPane scrollPane = new JScrollPane(table);
+        panel.add(scrollPane);
+
+        return panel;
+    }
+
+    //~ Inner Classes //////////////////////////////////////////////////////////
+
+    /**
+     * Responds to clicks on the security and status icons, creating a
+     * new dialog that displays connection related information.
+     */
+    private class ButtonListener implements ActionListener {
+        public void actionPerformed(ActionEvent evt) {
+            final DemoSwingWorker worker = new DemoSwingWorker() {
+                public Object construct() {
+                    JTabbedPane tabbedPane = new JTabbedPane();
+
+                    Component factoryInformation = makeTab(lastFactoryInformation);
+                    tabbedPane.addTab("Connection", factoryInformation);
+
+                    Component securityInformation = makeTab(lastSecurityInformation);
+                    tabbedPane.addTab("Security", securityInformation);
+
+                    JOptionPane.showMessageDialog(parent, tabbedPane, "Connection Information", JOptionPane.INFORMATION_MESSAGE);
+
+                    return "done";
+                }
+            };
+
+            worker.start();
+        }
+    }
+
+    /**
+     * Produces a <code>JTable</code> containing information from a
+     * <code>Properties</code> object.
+     */
+    private class PropertiesTableModel extends AbstractTableModel {
+        private Object[][] data;
+        private String[] headers = {"Property", "Value"};
+
+        public int getColumnCount() {
+            return headers.length;
+        }
+
+        public String getColumnName(int col) {
+            return headers[col];
+        }
+
+        public int getRowCount() {
+            return data.length;
+        }
+
+        public Object getValueAt(int row, int col) {
+            return (row < data.length) ? data[row][col] : null;
+        }
+
+        public void setupData(Properties properties) {
+            // Sort properties by key
+            List list = new ArrayList(20);
+            Iterator iterator = properties.keySet().iterator();
+
+            while (iterator.hasNext()) {
+                list.add(iterator.next());
+            }
+
+            Collections.sort(list);
+
+            // Add sorted properties to table data model
+            data = new Object[properties.size()][2];
+
+            int row = 0;
+            iterator = list.iterator();
+
+            while (iterator.hasNext()) {
+                String key = (String) iterator.next();
+                data[row][0] = key;
+                data[row][1] = properties.get(key);
+                row++;
+            }
+
+            updateData();
+        }
+
+        public void updateData() {
+            fireTableDataChanged();
+        }
+    }
+}

src/java/com/opensymphony/webwork/dispatcher/client/ClientException.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.dispatcher.client;
+
+
+/**
+ * Generic exception used by classes in the
+ * <code>com.opensymphony.webwork.dispatcher.client</code> package.
+ *
+ * @version $Id$
+ * @author Ben Alex (<a href="mailto:ben.alex@acegi.com.au">ben.alex@acegi.com.au</a>)
+ */
+public class ClientException extends Exception {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private String text;
+    private Throwable cause;
+
+    //~ Constructors ///////////////////////////////////////////////////////////
+
+    public ClientException(Throwable cause) {
+        super(cause.getMessage());
+        this.cause = cause;
+    }
+
+    public ClientException(String text) {
+        this.text = text;
+    }
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public Throwable getCause() {
+        return cause;
+    }
+}

src/java/com/opensymphony/webwork/dispatcher/client/ClientRequest.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.dispatcher.client;
+
+
+/**
+ * Interface that must be implemented by any client-side object that needs to
+ * be processed by the Client Dispatcher. Generally, normal XWork Action
+ * objects will implement this interface and thus enable them to be used on
+ * both the client and the server tiers.
+ *
+ * @version $Id$
+ * @author Ben Alex (<a href="mailto:ben.alex@acegi.com.au">ben.alex@acegi.com.au</a>)
+ */
+public interface ClientRequest {
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * Sets the {@link ClientRequestInvocation ClientRequestInvocation}
+     * that is specific to this <code>ClientRequest</code> proxy object. This
+     * method should only be called by
+     * {@link TransportFactory#createClientRequestProxy(ClientRequest)
+     * TransportFactory.createClientRequestProxy(ClientRequest)}.
+     */
+    public void setClientRequestInvocation(ClientRequestInvocation clientRequestInvocation);
+
+    /**
+     * Provides the {@link ClientRequestInvocation ClientRequestInvocation}
+     * that is specific to this <code>ClientRequest</code> proxy object.
+     */
+    public ClientRequestInvocation getClientRequestInvocation();
+}

src/java/com/opensymphony/webwork/dispatcher/client/ClientRequestInvocation.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.dispatcher.client;
+
+import com.opensymphony.xwork.Action;
+
+import java.io.Serializable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * Holds information specific to a unique {@link ClientRequest
+ * ClientRequest} proxy object.
+ *
+ * @version $Id$
+ * @author Ben Alex (<a href="mailto:ben.alex@acegi.com.au">ben.alex@acegi.com.au</a>)
+ * @author Philipp Meier (meier@meisterbohne.de)
+ */
+public class ClientRequestInvocation implements Serializable {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private transient TransportFactory transportFactory;
+    private Action resultAction;
+    private Map parameters;
+    private String remoteActionName;
+    private String resultCode;
+
+    //~ Constructors ///////////////////////////////////////////////////////////
+
+    /**
+     * Should not be called directly by users. A
+     * <code>ClientRequestInvocation</code> will be generated for a given
+     * {@link ClientRequest ClientRequest} whilst executing
+     * {@link TransportFactory#createClientRequestProxy(ClientRequest)
+     * TransportFactory.createClientRequestProxy(ClientRequest)}.
+     */
+    public ClientRequestInvocation(TransportFactory transportFactory, String remoteActionName) {
+        this.transportFactory = transportFactory;
+        this.remoteActionName = remoteActionName;
+        this.parameters = new HashMap();
+    }
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * Should not be called directly by users. The
+     * {@link ClientRequestProxy ClientRequestProxy} will record each parameter
+     * <code>set</code> performed against the associated
+     * {@link ClientRequest ClientRequest} proxy object. These are ultimately
+     * transferred to the server side of the Client Dispatcher.
+     */
+    public void setParameter(Object key, Object value) {
+        parameters.put(key, value);
+    }
+
+    /**
+     * Returns the list of parameters <code>set</code> against a
+     * {@link ClientRequest ClientRequest} proxy object.
+     * This method is not normally used by users, as the
+     * {@link TransportFactory#execute(ClientRequestInvocation)
+     * TransportFactory.execute(ClientRequestInvocation)}
+     * method will serialize this information to the remote side of the
+     * Client Dispatcher.
+     */
+    public Map getParameters() {
+        return parameters;
+    }
+
+    /**
+     * Sets the action name that will be executed on the remote side of the
+     * Client Dispatcher. This remote name must match an action defined in
+     * <code>xwork.xml</code> through a
+     * <code>&lt;action name="remoteActionName"
+     * class="com.some.company.class"&gt;</code>
+     * setting.<BR><BR>
+     *
+     * {@link TransportFactory TransportFactory} implementations usually set
+     * a remote action name to be equal to the class name of the
+     * {@link ClientRequest ClientRequest}.<BR><BR>
+     *
+     * Users should ensure this method is used to correctly set the intended
+     * remote action.
+     */
+    public void setRemoteActionName(String remoteActionName) {
+        this.remoteActionName = remoteActionName;
+    }
+
+    /**
+     * Return the action name that will be executed on the server side of the
+     * Client Dispatcher.
+     */
+    public String getRemoteActionName() {
+        return remoteActionName;
+    }
+
+    /**
+     * Should not be called by users. This sets the final remote action that
+     * was executed on the XWork Value Stack. This method is called by the
+     * {@link ClientRequestProxy#execute ClientRequestProxy.execute} method.
+     */
+    public void setResultAction(Action resultAction) {
+        this.resultAction = resultAction;
+    }
+
+    /**
+     * After the {@link ClientRequestInvocation#execute()
+     * ClientRequestInvocation.execute()} method has been called via the
+     * {@link ClientRequest ClientRequest} proxy object, this method returns
+     * the final action that was in the server's XWork Value Stack.
+     * This is a serialized copy of that Action and is therefore limited
+     * to normal serialization behaviour (eg objects marked
+     * <code>transient</code> or that do not implement <code>
+     * java.io.Serializable</code> will not be serialized).
+     */
+    public Action getResultAction() {
+        return resultAction;
+    }
+
+    /**
+     * Should not be called by users. This sets the final remote
+     * <code>String</code> result code. This method is called by the
+     * {@link ClientRequestProxy#execute ClientRequestProxy.execute} method.
+     */
+    public void setResultCode(String resultCode) {
+        this.resultCode = resultCode;
+    }
+
+    /**
+     * After the {@link ClientRequestInvocation#execute()
+     * ClientRequestInvocation.execute()} method has been called via the
+     * {@link ClientRequest ClientRequest} proxy object, this method returns
+     * the <code>String</code> result code from the remote action's
+     * <code>execute()</code> method. This is typically <code>SUCCESS</code>,
+     * <code>INPUT</code>, <code>ERROR</code> etc.
+     */
+    public String getResultCode() {
+        return resultCode;
+    }
+
+    /**
+     * Should not be called directly by users. The
+     * {@link ClientRequestProxy ClientRequestProxy} will call this
+     * method as part of its interception of the <code>execute</code> method
+     * of the {@link ClientRequest ClientRequest} proxy object.
+     */
+    public RemoteResult execute() throws ClientException {
+        return transportFactory.execute(this);
+    }
+}

src/java/com/opensymphony/webwork/dispatcher/client/ClientRequestProxy.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.dispatcher.client;
+
+import net.sf.cglib.proxy.MethodInterceptor;
+import net.sf.cglib.proxy.MethodProxy;
+
+import java.lang.reflect.Method;
+
+
+/**
+ * This class represents an interceptor added to {@link ClientRequest
+ * ClientRequest} proxy objects. This class is added via the
+ * {@link TransportFactory#createClientRequestProxy(ClientRequest)
+ * TransportFactory.createClientRequestProxy(ClientRequest)}
+ * method.<BR><BR>
+ *
+ * This class is responsible for ensuring the {@link ClientRequestInvocation
+ * ClientRequestInvocation} associated with a given {@link ClientRequest
+ * ClientRequest} proxy object is properly notified whenever calls are made to
+ * <code>set</code> methods and the <code>execute</code> method.<BR><BR>
+ *
+ * Users should not need to interact directly with this class.
+ *
+ * @version $Id$
+ * @author Ben Alex (<a href="mailto:ben.alex@acegi.com.au">ben.alex@acegi.com.au</a>)
+ */
+public class ClientRequestProxy implements MethodInterceptor {
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
+        ClientRequest clientAction = (ClientRequest) obj;
+        Object result = null;
+
+        if (method.getName().equals("execute")) {
+            RemoteResult car = clientAction.getClientRequestInvocation().execute();
+            clientAction.getClientRequestInvocation().setResultAction(car.getAction());
+            clientAction.getClientRequestInvocation().setResultCode(car.getResultCode());
+            result = car.getResultCode();
+        } else if (method.getName().startsWith("set")) {
+            String propertyName = method.getName().substring(3, method.getName().length());
+            result = proxy.invokeSuper(obj, args);
+            clientAction.getClientRequestInvocation().setParameter(propertyName, args[0]);
+        }
+
+        return result;
+    }
+}

src/java/com/opensymphony/webwork/dispatcher/client/HTTPClientActionProxyFactory.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.dispatcher.client;
+
+import com.opensymphony.xwork.ActionInvocation;
+import com.opensymphony.xwork.ActionProxy;
+import com.opensymphony.xwork.DefaultActionProxyFactory;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.Map;
+
+
+/**
+ * This class is intended for use in conjunction with {@link HTTPClientObjectFactory HTTPClientObjectFactory}
+ * to allow the remote action invocation to be placed on the value stack on the client rather
+ * than the local one.
+ * @author Peter Kelley
+ */
+public class HTTPClientActionProxyFactory extends DefaultActionProxyFactory {
+    //~ Static fields/initializers /////////////////////////////////////////////
+
+    /** logger for this class */
+    private static final Log log = LogFactory.getLog(HTTPClientActionProxyFactory.class);
+
+    //~ Constructors ///////////////////////////////////////////////////////////
+
+    /**
+     * Default constructor
+     */
+    public HTTPClientActionProxyFactory() {
+        super();
+    }
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * This method adds a pre result listener to allow replacement of the object at the top of the
+     * value stack with the one returned from the server..
+     *
+     * @param actionProxy The action proxy to create the invocation for
+     * @return a {@link com.opensymphony.xwork.DefaultActionInvocation DefaultActionInvocation} with
+     * a {link HTTPClientPreResultListener HTTPClientPreResultListener} added.
+     * @throws Exception if something goes wrong
+     */
+    public ActionInvocation createActionInvocation(ActionProxy actionProxy) throws Exception {
+        ActionInvocation result = super.createActionInvocation(actionProxy);
+
+        if (log.isDebugEnabled()) {
+            log.debug("Adding pre result listener");
+        }
+
+        result.addPreResultListener(new HTTPClientPreResultListener());
+
+        return result;
+    }
+
+    /**
+     * This method adds a pre result listener to allow replacement of the object at the top of the
+     * value stack with the one returned from the server..
+     *
+     * @param actionProxy The action proxy to create the invocation for
+     * @param extraContext Extra information needed for this action invocation
+     * @return a {@link com.opensymphony.xwork.DefaultActionInvocation DefaultActionInvocation}
+     *   with a {link HTTPClientPreResultListener HTTPClientPreResultListener} added.
+     * @throws Exception if something goes wrong
+     */
+    public ActionInvocation createActionInvocation(ActionProxy actionProxy, Map extraContext) throws Exception {
+        ActionInvocation result = super.createActionInvocation(actionProxy, extraContext);
+
+        if (log.isDebugEnabled()) {
+            log.debug("Adding pre result listener");
+        }
+
+        result.addPreResultListener(new HTTPClientPreResultListener());
+
+        return result;
+    }
+
+    /**
+     * This method adds a pre result listener to allow replacement of the object at the top of the
+     * value stack with the one returned from the server..
+     *
+     * @param actionProxy The action proxy to create the invocation for
+     * @param extraContext Extra information needed for this action invocation
+     * @param pushAction whether to oput this action on the value stack after invocation
+     * @return a {@link com.opensymphony.xwork.DefaultActionInvocation DefaultActionInvocation}
+     *   with a {link HTTPClientPreResultListener HTTPClientPreResultListener} added.
+     * @throws Exception if something goes wrong
+     */
+    public ActionInvocation createActionInvocation(ActionProxy actionProxy, Map extraContext, boolean pushAction) throws Exception {
+        ActionInvocation result = super.createActionInvocation(actionProxy, extraContext, pushAction);
+
+        if (pushAction) {
+            result.addPreResultListener(new HTTPClientPreResultListener());
+            log.debug("Adding pre result listener");
+        } else {
+            log.debug("Not adding pre result listener as action is not on the stack");
+        }
+
+        return result;
+    }
+}

src/java/com/opensymphony/webwork/dispatcher/client/HTTPClientObjectFactory.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.dispatcher.client;
+
+import com.opensymphony.xwork.Action;
+import com.opensymphony.xwork.ObjectFactory;
+import com.opensymphony.xwork.config.entities.ActionConfig;
+
+
+/**
+ * DOCUMENT ME!
+ *
+ * @author $author$
+ * @version $Revision$
+ */
+public class HTTPClientObjectFactory extends ObjectFactory {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    /** transport factory used to talk to the server */
+    private TransportFactory transportFactory;
+
+    //~ Constructors ///////////////////////////////////////////////////////////
+
+    /**
+ * Default constructor
+ * @param transport The transport factory implementation to use to communicate with the remote
+ * server
+ */
+    public HTTPClientObjectFactory(TransportFactory transport) {
+        if (transport == null) {
+            throw new IllegalArgumentException("Transport factory must not be null");
+        }
+
+        transportFactory = transport;
+    }
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+ * Build an action from the configuration
+ *
+ * @param config The action configuration to use to find the action. The action referenced
+ * must implement {@link ClientRequest ClientRequest} for this to work.
+ * @throws Exception If there is a problem creating the action. In particular the action must
+ * implement {@link ClientRequest ClientRequest} or a class cast execption will occur.
+ * @return An action set up to be called remotely
+ */
+    public Action buildAction(ActionConfig config) throws Exception {
+        Action localAction = (Action) buildBean(config.getClassName());
+        String namespace = config.getPackageName();
+        Action remoteAction = (Action) transportFactory.createClientRequestProxy((ClientRequest) localAction, namespace);
+
+        return remoteAction;
+    }
+}

src/java/com/opensymphony/webwork/dispatcher/client/HTTPClientPreResultListener.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.dispatcher.client;
+
+import com.opensymphony.xwork.ActionInvocation;
+import com.opensymphony.xwork.interceptor.PreResultListener;
+import com.opensymphony.xwork.util.OgnlValueStack;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+
+/**
+ * Class to replace the action at the top of the stack with the one returned from the server.
+ * @author Peter Kelley
+ */
+public class HTTPClientPreResultListener implements PreResultListener {
+    //~ Static fields/initializers /////////////////////////////////////////////
+
+    /** logger for this class */
+    private static final Log log = LogFactory.getLog(HTTPClientPreResultListener.class);
+
+    //~ Constructors ///////////////////////////////////////////////////////////
+
+    /**
+     * Default constructor
+     */
+    public HTTPClientPreResultListener() {
+    }
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * Replace the action at the top of the stack with the one returned from the server. This
+     * method assumes that the object at the top of the stack is a
+     * {@link ClientRequestInvocation ClientRequestInvocation}.
+     * This callback method will be called after the Action execution and before the Result execution.
+     *
+     * @param invocation The action invocation that is being processed
+     * @param resultCode The result of the action invocation
+     */
+    public void beforeResult(ActionInvocation invocation, String resultCode) {
+        OgnlValueStack stack = invocation.getStack();
+
+        if (log.isDebugEnabled()) {
+            log.debug("Object at top of stack was: " + stack.peek());
+        }
+
+        ClientRequest localAction = (ClientRequest) stack.pop();
+        ClientRequestInvocation clientRequestInvocation = (localAction).getClientRequestInvocation();
+        Object resultAction = clientRequestInvocation.getResultAction();
+
+        if (log.isDebugEnabled()) {
+            log.debug("Replacing " + localAction + " with " + resultAction);
+        }
+
+        stack.push(resultAction);
+    }
+}

src/java/com/opensymphony/webwork/dispatcher/client/ProgressConsumer.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.dispatcher.client;
+
+
+/**
+ * Interface this is implemented by classes wishing to receive
+ * progress notifications whilst
+ * {@link TransportFactory#execute(ClientRequestInvocation)
+ * TransportFactory.execute(ClientRequestInvocation)} is running.
+ *
+ * Users should use the {@link
+ * TransportFactory#setProgressConsumer(ProgressConsumer)
+ * TransportFactory.setProgressConsumer(ProgressConsumer)} method to denote
+ * which <code>ProgressConsumer</code> should be used.
+ *
+ * @version $Id$
+ * @author Ben Alex (<a href="mailto:ben.alex@acegi.com.au">ben.alex@acegi.com.au</a>)
+ */
+public interface ProgressConsumer {
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * Notifies the <code>ProgressConsumer</code> implementation that another
+     * stage of request procesing has occurred. The details of the progress
+     * are contained in the {@link ProgressNotification ProgressNotification}.
+     */
+    public void notify(ProgressNotification progressNotification);
+}

src/java/com/opensymphony/webwork/dispatcher/client/ProgressConsumerNull.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.dispatcher.client;
+
+
+/**
+ * Consumes process notifications, but does nothing with them.
+ * A {@link TransportFactory TransportFactory} should
+ * use this class as its default {@link ProgressConsumer ProgressConsumer},
+ * in case the user has not set one via the
+ * {@link TransportFactory#setProgressConsumer(ProgressConsumer)
+ * TransportFactory.setProgressConsumer(ProgressConsumer)} method.
+ *
+ * @version $Id$
+ * @author Ben Alex (<a href="mailto:ben.alex@acegi.com.au">ben.alex@acegi.com.au</a>)
+ */
+public class ProgressConsumerNull implements ProgressConsumer {
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public void notify(ProgressNotification progressNotification) {
+    }
+}

src/java/com/opensymphony/webwork/dispatcher/client/ProgressConsumerString.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.dispatcher.client;
+
+
+/**
+ * A {@link ProgressConsumer ProgressConsumer} that writes notifications to the
+ * standard output stream.
+ *
+ * @version $Id$
+ * @author Ben Alex (<a href="mailto:ben.alex@acegi.com.au">ben.alex@acegi.com.au</a>)
+ */
+public class ProgressConsumerString implements ProgressConsumer {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private String lastStartedId = "none";
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    public synchronized void notify(ProgressNotification progressNotification) {
+        String status = "uknown";
+        boolean showProgress = false;
+        int completed = 0;
+        int ofTotal = -1;
+
+        if (progressNotification.getStatus() == ProgressNotification.STATUS_NONE) {
+            status = "Not Executing";
+        } else if (progressNotification.getStatus() == ProgressNotification.STATUS_CONNECTING) {
+            status = "Connecting to Remote";
+        } else if (progressNotification.getStatus() == ProgressNotification.STATUS_PROCESSING) {
+            status = "Remote is Processing";
+        } else if (progressNotification.getStatus() == ProgressNotification.STATUS_RECEIVING) {
+            status = "Receiving from Remote";
+            showProgress = true;
+            completed = progressNotification.getInputReceived();
+            ofTotal = progressNotification.getInputSize();
+        } else if (progressNotification.getStatus() == ProgressNotification.STATUS_RECEIVED) {
+            status = "Received";
+        } else if (progressNotification.getStatus() == ProgressNotification.STATUS_RETRY_DELAY) {
+            status = "Retrying";
+        } else if (progressNotification.getStatus() == ProgressNotification.STATUS_SENDING) {
+            status = "Sending to Remote";
+            showProgress = true;
+            completed = progressNotification.getOutputTransmitted();
+            ofTotal = progressNotification.getOutputSize();
+        } else {
+            status = "Unsupported Status";
+        }
+
+        if (progressNotification.getStatus() == ProgressNotification.STATUS_SENDING) {
+            if (!lastStartedId.equals(progressNotification.getId())) {
+                System.out.println("ID: " + progressNotification.getId() + "; Factory: " + progressNotification.getFactoryInformation() + "; Security: " + progressNotification.getSecurityInformation());
+                lastStartedId = progressNotification.getId();
+            }
+        }
+
+        if (showProgress) {
+            System.out.println("ID: " + progressNotification.getId() + "; " + status + "; " + completed + " of " + ofTotal);
+        } else {
+            System.out.println("ID: " + progressNotification.getId() + "; " + status);
+        }
+    }
+}

src/java/com/opensymphony/webwork/dispatcher/client/ProgressInputStream.java

+/*
+ * Copyright (c) 2002-2003 by OpenSymphony
+ * All rights reserved.
+ */
+package com.opensymphony.webwork.dispatcher.client;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import java.util.Date;
+import java.util.Properties;
+
+
+/**
+ * Monitors the progress of reading from an InputStream. Links in with a
+ * {@link ProgressConsumer ProgressConsumer} and <code>Properties</code> from
+ * a {@link TransportFactory TransportFactory}.
+ *
+ * @version $Id$
+ * @author Ben Alex (<a href="mailto:ben.alex@acegi.com.au">ben.alex@acegi.com.au</a>)
+ */
+public class ProgressInputStream extends FilterInputStream {
+    //~ Instance fields ////////////////////////////////////////////////////////
+
+    private ProgressConsumer progressConsumer;
+    private ProgressNotification notification;
+    private int lastNotificationStatus;
+    private int nread = 0;
+    private int size = 0;
+    private long lastUpdate;
+    private long updateFrequency;
+
+    //~ Constructors ///////////////////////////////////////////////////////////
+
+    /**
+     * Monitors the progress of an input stream.<BR><BR>
+     *
+     * The <code>Properties</code> may contain an optional value for
+     * <code>updateFrequency</code>. The <code>updateFrequency</code> specifies
+     * the number of milliseconds that must pass between notifications to
+     * the {@link ProgressConsumer ProgressConsumer} concerning standard
+     * stream processing. Certain major processing events such as stream
+     * establishment and closure will always result in a notification,
+     * irrespective of the <code>updateFrequency</code>. The default value
+     * for <code>updateFrequency</code> is 250.
+     */
+    public ProgressInputStream(ProgressNotification notification, ProgressConsumer progressConsumer, Properties props, InputStream in, int contentLength) {
+        super(in);
+        size = contentLength;
+        this.notification = notification;
+        this.progressConsumer = progressConsumer;
+        this.lastUpdate = new Date().getTime();
+        this.lastNotificationStatus = notification.getStatus();
+        notification.setInputSize(size);
+
+        try {
+            this.updateFrequency = new Long(props.getProperty("updateFrequency", "250")).longValue();
+        } catch (NumberFormatException nfe) {
+            updateFrequency = 500;
+        }
+
+        try {
+            notification.setStatus(ProgressNotification.STATUS_RECEIVING);
+        } catch (ClientException ignored) {
+        }
+
+        updateBytes(nread);
+    }
+
+    //~ Methods ////////////////////////////////////////////////////////////////
+
+    /**
+     * Overrides <code>FilterInputStream.close</code>
+     * to update the progress monitor and close the stream.
+     */
+    public void close() throws IOException {
+        updateBytes(nread);
+        in.close();
+    }
+
+    /**
+     * Overrides <code>FilterInputStream.read</code>
+     * to update the progress monitor after the read.
+     */
+    public int read() throws IOException {
+        int c = in.read();
+
+        if (c >= 0) {
+            ++nread;
+            updateBytes(nread);
+        }
+
+        return c;
+    }
+
+    /**
+     * Overrides <code>FilterInputStream.read</code>
+     * to update the progress monitor after the read.
+     */
+    public int read(byte[] b) throws IOException {
+        int nr = in.read(b);
+
+        if (nr > 0) {
+            nread += nr;
+            updateBytes(nread);
+        }
+
+        return nr;
+    }
+
+    /**
+     * Overrides <code>FilterInputStream.read</code>
+     * to update the progress monitor after the read.
+     */
+    public int read(byte[] b, int off, int len) throws IOException {
+        int nr = in.read(b, off, len);
+
+        if (nr > 0) {
+            nread += nr;
+            updateBytes(nread);
+        }
+
+        return nr;
+    }
+
+    /**
+     * Overrides <code>FilterInputStream.reset</code>
+     * to reset the progress monitor as well as the stream.
+     */
+    public synchronized void reset() throws IOException {
+        in.reset();
+        nread = size - in.available();
+        updateBytes(nread);
+    }
+
+    /**
+     * Overrides <code>FilterInputStream.skip</code>
+     * to update the progress monitor after the skip.
+     */
+    public long skip(long n) throws IOException {
+        long nr = in.skip(n);
+
+        if (nr > 0) {
+            nread += nr;
+            updateBytes(nread);
+        }
+
+        return nr;
+    }
+
+    /**
+     * Causes the progress monitor to be notified if appropriate.
+     */
+    private void updateBytes(int nowRead) {
+        boolean update = false;
+
+        if (nowRead >= size) {
+            if (notification.getStatus() == ProgressNotification.STATUS_RECEIVING) {
+                try {
+                    notification.setStatus(ProgressNotification.STATUS_RECEIVED);
+                } catch (ClientException ignored) {
+                }
+            }
+        }
+