Index: src/webapp/tests/fluid-tests/uploader/FlashBridgeTests.html
===================================================================
--- src/webapp/tests/fluid-tests/uploader/FlashBridgeTests.html	(revision 0)
+++ src/webapp/tests/fluid-tests/uploader/FlashBridgeTests.html	(revision 0)
@@ -0,0 +1,36 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr" id="html">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+    <title>Flash Bridge Tests</title>
+    <link rel="Stylesheet" media="screen" href="../../jqUnit/testsuite.css" />
+    
+    <script src="../../../fluid-components/js/jquery/jquery-1.2.6.js" type="text/javascript"></script>
+    <script type="text/javascript" src="../../jqUnit/testrunner.js"></script>
+    <script type="text/javascript" src="../../jqUnit/jqUnit.js"></script>
+    
+    <script src="../../../fluid-components/js/fluid/Fluid.js" type="text/javascript"></script>
+    <script type="text/javascript" src="../../../fluid-components/js/fluid/uploader/FlashBridge.js"></script>
+    
+    <script type="text/javascript" src="js/FlashBridgeTests.js"></script>
+</head>
+
+<body id="body">
+    <h1 id="header">Flash Bridge Tests Tests</h1>
+    <h2 id="banner"></h2>
+    <h2 id="userAgent"></h2>
+
+    <!-- Test HTML -->
+    <div id="main" style="display: none;">
+        <div id="existingMarkup">
+            <object class="flashMovie">Flash is here</object>
+        </div>
+        
+        <div id="selfRender">
+
+        </div>
+        
+    </div>
+    <ol id="tests"></ol>
+</body>
+</html>
Index: src/webapp/tests/fluid-tests/uploader/js/FlashBridgeTests.js
===================================================================
--- src/webapp/tests/fluid-tests/uploader/js/FlashBridgeTests.js	(revision 0)
+++ src/webapp/tests/fluid-tests/uploader/js/FlashBridgeTests.js	(revision 0)
@@ -0,0 +1,139 @@
+/*global jQuery, fluid, jqUnit*/
+
+(function ($) {
+    $(document).ready(function () {  
+        var flashBridgeTests = new jqUnit.TestCase("Flash Bridge Tests");
+
+        flashBridgeTests.test("Don't self-render", function () {
+            var bridge = fluid.flashBridge("#existingMarkup");
+            var container = $("#existingMarkup");
+            jqUnit.assertEquals("The component container should still be in the DOM.", 1, container.length);
+            jqUnit.assertEquals("The object tag should remain in the DOM", 1, container.find("object").length);
+            jqUnit.assertNotUndefined("The object tag should have been given an id.", container.find("object").attr("id"));
+        });
+        
+        flashBridgeTests.test("Self render: don't replace container", function () {
+            var bridge = fluid.flashBridge("#selfRender");
+            var container = $("#selfRender");
+            jqUnit.assertEquals("The component container should still be in the DOM.", 1, container.length);
+            
+            var object = container.find("object");
+            jqUnit.assertEquals("The container should have an object tag inside it.", 1, object.length);
+        });
+        
+        flashBridgeTests.test("Self render: replace container", function () {
+            var bridge = fluid.flashBridge("#selfRender", {
+                replaceContainer: true
+            });
+            
+            var container = $("div#selfRender");
+            jqUnit.assertEquals("The component container should not be in the DOM anymore.", 0, container.length);
+            
+            var object = $("object#selfRender");
+            jqUnit.assertEquals("The container should have been replaced by an object tag and its id should be preserved.", 
+                                1, object.length);
+        });
+        
+        flashBridgeTests.test("Parameter rendering", function () {
+            // Test the defaults.
+            var bridge = fluid.flashBridge("#selfRender", {
+                replaceContainer: true
+            });
+            
+            jqUnit.assertEquals("The quality param should have been set.", 
+                               "high", 
+                               bridge.container.find("param[name=quality]").attr("value"));
+                               
+            jqUnit.assertEquals("The allowScriptAccess param should have been set.", 
+                                "always", 
+                                bridge.container.find("param[name=allowScriptAccess]").attr("value"));
+                                
+            jqUnit.assertEquals("The wMode param should have been set.", 
+                                "transparent", 
+                                bridge.container.find("param[name=wMode]").attr("value"));
+                                
+            jqUnit.assertEquals("The swfLiveConnect param should have been set.", 
+                                "true", 
+                                bridge.container.find("param[name=swLiveConnect]").attr("value"));
+                                
+            jqUnit.assertEquals("The flashVars param should be empty.", 
+                                "", 
+                                bridge.container.find("param[name=flashvars]").attr("value"));  
+                                
+            // Now try overriding a parameter.
+            bridge = fluid.flashBridge("#selfRender", {
+                replaceContainer: true,
+                params: {
+                    wMode: "window",
+                    foo: "bar"
+                }
+            });
+            
+            jqUnit.assertEquals("The allowScriptAccess param should have the default value.", 
+                                "always", 
+                                bridge.container.find("param[name=allowScriptAccess]").attr("value"));
+                                
+            jqUnit.assertEquals("The wMode param should have been set to a custom value.", 
+                                "window", 
+                                bridge.container.find("param[name=wMode]").attr("value"));
+                                
+            jqUnit.assertEquals("Additional parameters should also be rendered", 
+                                "bar", 
+                                bridge.container.find("param[name=foo]").attr("value"));
+        });
+        
+        flashBridgeTests.test("Event exposure", function () {
+            var wasOnLoadCalled = false;
+            var wasOnFooCalled = false;
+            
+            bridge = fluid.flashBridge("#selfRender", {
+                replaceContainer: true,
+                events: {
+                    onLoad: null,
+                    onFoo: null
+                },
+                listeners: {
+                    onLoad: function () {
+                        wasOnLoadCalled = true;
+                    },
+                    onFoo: function () {
+                        wasOnFooCalled = true;
+                    }
+                }
+            });
+            
+            var flashvars = bridge.container.find("param[name=flashvars]").attr("value");
+            var eventSegments = flashvars.split("&");
+            var onLoadCallbackPath = eventSegments[0].split("=")[1];
+            var onFooCallbackPath = eventSegments[1].split("=")[1];
+            
+            jqUnit.assertEquals("The flashVars param should expose the event names to Flash.", 
+                                "onLoad=fluid.flashBridge.callbacks.selfRender.onLoad&onFoo=fluid.flashBridge.callbacks.selfRender.onFoo", 
+                                flashvars);
+                                
+            // Ensure that the event is globally callable.
+            fluid.invokeGlobalFunction(onLoadCallbackPath);
+            jqUnit.assertTrue("The onLoad callback should have been invoked.", wasOnLoadCalled);
+            jqUnit.assertFalse("The onFoo callback should not have been invoked.", wasOnFooCalled);
+            fluid.invokeGlobalFunction(onFooCallbackPath);
+            jqUnit.assertTrue("The onFoo callback should have been invoked.", wasOnFooCalled);
+        });
+        
+        flashBridgeTests.test("Flash URL", function () {
+            var url = "file:///foo/bar.swf";
+            var bridge = fluid.flashBridge("#selfRender", {
+                replaceContainer: true,
+                movieURL: url
+            });
+            
+            if ($.browser.msie) {
+                var movieParam = bridge.container.find("param[name=movie]");
+                jqUnit.assertEquals("In IE, the movie URL should have been added as a param element.", 1, movieParam.length);
+                jqUnit.assertEquals("The movieURL should have been specified correctly.", url, movieParam.attr("value"));
+            } else {
+                jqUnit.assertEquals("In non-IE browsers, the movieURL should have been specified as a 'data' attribute on the object.",
+                                    url, bridge.container.attr("data"));
+            }
+        });
+    });
+})(jQuery);
Index: src/webapp/tests/fluid-tests/js/FluidJSTests.js
===================================================================
--- src/webapp/tests/fluid-tests/js/FluidJSTests.js	(revision 6263)
+++ src/webapp/tests/fluid-tests/js/FluidJSTests.js	(working copy)
@@ -267,5 +267,38 @@
             jqUnit.assertNull("The defaults for a nonexistent component should be null.", 
                               fluid.defaults("timemachine"));
         });
+        
+        fluidJSTests.test("toQueryString()", function () {
+            // Start with a single key and value.
+            var obj = {
+                key: "value"
+            };
+            
+            var queryString = fluid.toQueryString(obj);
+            jqUnit.assertEquals("The query string should contain the key and value with no trailing &.",
+                                "key=value", queryString);
+                                
+            // Several simple keys in the object.
+            obj = {
+                key1: 1,
+                key2: "2",
+                key3: "three"
+            };
+            
+            queryString = fluid.toQueryString(obj);
+            jqUnit.assertEquals("The query string should contain each key and value separated by &.",
+                                "key1=1&key2=2&key3=three", queryString);
+
+            // Object containing an array
+            obj = {
+                string: "foo",
+                number: 42,
+                list: ["foo", 42, {}]
+            };
+            
+            queryString = fluid.toQueryString(obj);
+            jqUnit.assertEquals("The query string should be a serialized representation of the object's key/value pairs.",
+                                "string=foo&number=42&list=foo&list=42&list=[object Object]", queryString);
+        });
     });
 })(jQuery);
Index: src/webapp/fluid-components/js/fluid/uploader/FlashBridge.js
===================================================================
--- src/webapp/fluid-components/js/fluid/uploader/FlashBridge.js	(revision 0)
+++ src/webapp/fluid-components/js/fluid/uploader/FlashBridge.js	(revision 0)
@@ -0,0 +1,124 @@
+/*global jQuery*/
+/*global fluid_0_7*/
+/*global __flash__argumentsToXML*/
+
+fluid_0_7 = fluid_0_7 || {};
+
+(function ($, fluid) {
+    
+    var invokeFlash = function (that, fnName) {
+        var xmlArgs = __flash__argumentsToXML(arguments, 2);
+        var invocation = that.movie.CallFunction("<invoke name='" + 
+                                                 fnName + 
+                                                 "' returntype='javascript'>" + 
+                                                 xmlArgs + 
+                                                 "</invoke>");
+        return eval(invocation);
+    };
+    
+    var exposeEventsToFlash = function (that) {
+        var callbacks = {};
+        var id = that.movie.attr("id");
+        
+        $.each(that.events, function (eventName, firer) {
+            callbacks[eventName] = function () {
+                firer.fire(arguments);
+            };
+            that.options.params.flashvars[eventName] = "fluid.flashBridge.callbacks." + id + "." + eventName;
+        });
+        
+        fluid.flashBridge.callbacks[id] = callbacks;
+    };
+    
+    var setupParamsAndProps = function (that) {
+        exposeEventsToFlash(that);
+        that.options.params.flashvars = fluid.toQueryString(that.options.params.flashvars);
+        if ($.browser.msie) {
+            that.options.params.movie = that.options.movieURL;
+        } else {
+            that.options.properties.classid = null; // classid is only needed on IE browsers.
+            that.options.properties.data = that.options.movieURL;
+        }
+    };
+    
+    var renderParamsAndProps = function (that) {
+        // We should really only be rendering these if they're not already found in the markup!
+        $.each(that.options.properties, function (propName, value) {
+            that.movie.attr(propName, value);
+        });
+        
+        $.each(that.options.params, function (paramName, value) {
+            that.movie.append("<param name='" + paramName + "' value='" + value + "' />");
+        });
+    };
+    
+    var flashObjectRenderer = function (that) {
+        var object = $("<object></object>");
+        if (that.options.replaceContainer) {
+            var containerId = that.container.attr("id");
+            that.container.replaceWith(object);
+            object.attr("id", containerId);
+            that.container = object;
+        } else {
+            that.container.append(object);
+        }
+  
+        return object;
+    };
+    
+    var setupFlashBridge = function (that) {
+        that.movie = that.locate("flashMovie");
+        
+        // Self-render if necessary.
+        if (!that.movie || that.movie.length < 1) {
+            that.movie = flashObjectRenderer(that);
+        }
+        
+        fluid.allocateSimpleId(that.movie);
+        
+        // Add parameters and properties to the markup.
+        setupParamsAndProps(that);
+        renderParamsAndProps(that);
+    };
+    
+    fluid.flashBridge = function (container, options) {
+        var that = fluid.initView("fluid.flashBridge", container, options);
+        
+        that.invokeFlash = function (fnName) {
+            invokeFlash(that, fnName);
+        };
+        
+        setupFlashBridge(that);
+        return that;
+    };
+    
+    fluid.flashBridge.callbacks = {};
+    
+    fluid.defaults("fluid.flashBridge", {
+        movieURL: null,
+        replaceContainer: false,
+        objectRenderer: flashObjectRenderer,
+        
+        properties: {
+            height: 1,
+            width: 1,
+            type: "application/x-shockwave-flash",
+            classid: "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
+        },
+        
+        params: {
+            quality: "high",
+            allowScriptAccess: "always",
+            wMode: "transparent",
+            swLiveConnect: true,
+            flashvars: {}
+        },
+
+        events: {},
+        
+        selectors: {
+            flashMovie: ".flashMovie"
+        }
+    });
+    
+})(jQuery, fluid_0_7);
Index: src/webapp/fluid-components/js/fluid/Fluid.js
===================================================================
--- src/webapp/fluid-components/js/fluid/Fluid.js	(revision 6263)
+++ src/webapp/fluid-components/js/fluid/Fluid.js	(working copy)
@@ -889,5 +889,31 @@
         }
         return newString;
     };
+        
+    var serializeKeyValuePair = function (key, value) {
+        return key + "=" + value.toString();
+    };
     
+    /**
+     * Converts an object's key/value pairs into a URL-friendly query string.
+     * 
+     * @param {Object} obj the object to convert into a query string
+     */
+    fluid.toQueryString = function (obj) {
+        var queries = [];
+        
+        $.each(obj, function (name, value) {
+            var valueType = typeof value;
+            
+            if (value && typeof value !== "string" && typeof value.length === "number") {
+                $.each(value, function (idx, arrayVal) {
+                    queries.push(serializeKeyValuePair(name, arrayVal)); 
+                });
+            } else {
+                queries.push(serializeKeyValuePair(name, value));
+            }
+        });
+        
+        return queries.join("&");
+    };
 })(jQuery, fluid_0_7);
