THRIFT-1087 Nonblocking asynchronous JS services
Patch: Henrique Mendonca


git-svn-id: https://svn.apache.org/repos/asf/thrift/trunk@1089637 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/compiler/cpp/src/generate/t_js_generator.cc b/compiler/cpp/src/generate/t_js_generator.cc
index 9def5da..fa06b05 100644
--- a/compiler/cpp/src/generate/t_js_generator.cc
+++ b/compiler/cpp/src/generate/t_js_generator.cc
@@ -47,6 +47,9 @@
      
      iter = parsed_options.find("node");
      gen_node_ = (iter != parsed_options.end());
+     
+     iter = parsed_options.find("jquery");
+     gen_jquery_ = (iter != parsed_options.end());
 
      if (gen_node_) {
        out_dir_base_ = "gen-nodejs";
@@ -160,7 +163,7 @@
   std::string render_includes();
   std::string declare_field(t_field* tfield, bool init=false, bool obj=false);
   std::string function_signature(t_function* tfunction, std::string prefix="", bool include_callback=false);
-  std::string argument_list(t_struct* tstruct);
+  std::string argument_list(t_struct* tstruct, bool include_callback=false);
   std::string type_to_enum(t_type* ttype);
 
   std::string autogen_comment() {
@@ -227,6 +230,11 @@
   bool gen_node_;
 
   /**
+   * True if we should generate services that use jQuery ajax (async/sync).
+   */
+  bool gen_jquery_;
+
+  /**
    * File streams
    */
   std::ofstream f_types_;
@@ -341,11 +349,11 @@
   vector<t_enum_value*>::iterator c_iter;
   for (c_iter = constants.begin(); c_iter != constants.end(); ++c_iter) {
     int value = (*c_iter)->get_value();
-	f_types_ << "'" << (*c_iter)->get_name() << "' : " << value;
-    if (c_iter != constants.end()-1)
+    f_types_ << "'" << (*c_iter)->get_name() << "' : " << value;
+    if (c_iter != constants.end()-1) {
         f_types_ << ",";
-
-	f_types_ << endl;
+    }
+    f_types_ << endl;
   }
 
   f_types_ << "};"<<endl;
@@ -990,9 +998,11 @@
     const vector<t_field*>& fields = arg_struct->get_members();
     vector<t_field*>::const_iterator fld_iter;
     string funname = (*f_iter)->get_name();
+    string arglist = argument_list(arg_struct);
 
     // Open function
-    f_service_ <<  js_namespace(tservice->get_program())<<service_name_<<"Client.prototype." << function_signature(*f_iter, "", gen_node_) << " {" << endl;
+    f_service_ <<  js_namespace(tservice->get_program())<<service_name_<<"Client.prototype." <<
+      function_signature(*f_iter, "", gen_node_ || gen_jquery_) << " {" << endl;
 
     indent_up();
 
@@ -1000,21 +1010,14 @@
       f_service_ <<
         indent() << "this.seqid += 1;" << endl <<
         indent() << "this._reqs[this.seqid] = callback;" << endl;
+    } else if (gen_jquery_) {
+      f_service_ <<
+        indent() << "if (callback === undefined) {" << endl;
+        indent_up();
     }
 
-    indent(f_service_) << indent() <<
-      "this.send_" << funname << "(";
-
-    bool first = true;
-    for (fld_iter = fields.begin(); fld_iter != fields.end(); ++fld_iter) {
-      if (first) {
-        first = false;
-      } else {
-        f_service_ << ", ";
-      }
-      f_service_ << (*fld_iter)->get_name();
-    }
-    f_service_ << ");" << endl;
+    f_service_ << indent() <<
+      "this.send_" << funname << "(" << arglist << ");" << endl;
 
     if (!gen_node_ && !(*f_iter)->is_oneway()) {
       f_service_ << indent();
@@ -1025,12 +1028,28 @@
         "this.recv_" << funname << "();" << endl;
     }
 
+    if (gen_jquery_) {
+      indent_down();
+      f_service_ << indent() << "} else {" << endl;
+      indent_up();
+        f_service_ << indent() << "var postData = this.send_" << funname <<
+           "(" << arglist << (arglist.empty() ? "" : ", ") << "true);" << endl;
+        f_service_ << indent() << "return this.output.getTransport()" << endl;
+        indent_up();
+          f_service_ << indent() << ".jqRequest(this, postData, arguments, this.recv_" << funname << ");" << endl;
+        indent_down();
+      indent_down();
+      f_service_ << indent() << "}" << endl;
+    }
+
     indent_down();
 
     f_service_ << "};" << endl << endl;
 
+
+    // Send function
     f_service_ <<  js_namespace(tservice->get_program())<<service_name_ <<
-        "Client.prototype.send_" << function_signature(*f_iter) << " {" <<endl;
+        "Client.prototype.send_" << function_signature(*f_iter, "", gen_jquery_) << " {" <<endl;
 
     indent_up();
 
@@ -1065,7 +1084,11 @@
     if (gen_node_) {
       f_service_ << indent() << "return this.output.flush();" << endl;
     } else {
-      f_service_ << indent() << "return this.output.getTransport().flush();" << endl;
+      if (gen_jquery_) {
+        f_service_ << indent() << "return this.output.getTransport().flush(callback);" << endl;
+      } else {
+        f_service_ << indent() << "return this.output.getTransport().flush();" << endl;
+      }
     }
 
 
@@ -1675,25 +1698,7 @@
 
   str  = prefix + tfunction->get_name() + " = function(";
 
-
-  //Need to create js function arg inputs
-  const vector<t_field*> &fields = tfunction->get_arglist()->get_members();
-  vector<t_field*>::const_iterator f_iter;
-
-  for (f_iter = fields.begin(); f_iter != fields.end(); ++f_iter) {
-
-      if(f_iter != fields.begin())
-          str += ", ";
-
-      str += (*f_iter)->get_name();
-  }
-
-  if (include_callback) {
-    if (!fields.empty()) {
-      str += ", ";
-    }
-    str += "callback";
-  }
+  str += argument_list(tfunction->get_arglist(), include_callback);
 
   str += ")";
   return str;
@@ -1702,7 +1707,8 @@
 /**
  * Renders a field list
  */
-string t_js_generator::argument_list(t_struct* tstruct) {
+string t_js_generator::argument_list(t_struct* tstruct,
+                                       bool include_callback) {
   string result = "";
 
   const vector<t_field*>& fields = tstruct->get_members();
@@ -1716,6 +1722,14 @@
     }
     result += (*f_iter)->get_name();
   }
+
+  if (include_callback) {
+    if (!fields.empty()) {
+      result += ", ";
+    }
+    result += "callback";
+  }
+
   return result;
 }
 
@@ -1761,6 +1775,7 @@
 }
 
 
-THRIFT_REGISTER_GENERATOR(js, "Javascript", 
+THRIFT_REGISTER_GENERATOR(js, "Javascript",
+"    jquery:          Generate jQuery compatible code.\n"
 "    node:            Generate node.js compatible code.\n")
 
diff --git a/lib/js/test/build.xml b/lib/js/test/build.xml
index c1a17e8..843ec6b 100644
--- a/lib/js/test/build.xml
+++ b/lib/js/test/build.xml
@@ -66,7 +66,7 @@
         </not>
       </condition>
       You need libthrift*.jar and libthrift*test.jar located at
-      ${thrift.java.dir}
+      ${thrift.java.dir}/build
       Did you compile Thrift Java library and its test suite by "ant compile-test"?
     </fail>
     <fail>
@@ -101,7 +101,7 @@
     <javac srcdir="${src}" destdir="${build}" classpathref="libs.classpath" />
   </target>
 
-  <target name="jstest" description="" depends="compile">
+  <target name="jstest" description="" depends="compile, lint">
     <jar jarfile="${jar.file}" basedir="${build}"/>
   </target>
 
@@ -117,14 +117,14 @@
       <arg line="--gen java ${thrift.dir}/test/ThriftTest.thrift" />
     </exec>
     <exec executable="${thrift.compiler}" failonerror="true">
-      <arg line="--gen js ${thrift.dir}/test/ThriftTest.thrift" />
+      <arg line="--gen js:jquery ${thrift.dir}/test/ThriftTest.thrift" />
     </exec>
   </target>
 
   <!-- @TODO QUnit tests as part of the testsuite-->
   <target name="test" description="run test suite" depends="init, generate, resolve, lint"/>
 
-  <target name="lint" description="code quality checks" depends="gjslint, jslint, generate"/>
+  <target name="lint" description="code quality checks" depends="generate, gjslint, jslint"/>
 
   <target name="jslint">
     <taskdef uri="antlib:com.googlecode.jslint4java" resource="com/googlecode/jslint4java/antlib.xml" classpathref="libs.classpath" />
diff --git a/lib/js/test/test.html b/lib/js/test/test.html
index 4615f88..f99da01 100644
--- a/lib/js/test/test.html
+++ b/lib/js/test/test.html
@@ -27,7 +27,7 @@
   <script src="gen-js/ThriftTest.js"       type="text/javascript" charset="utf-8"></script>
 
   <!-- jQuery -->
-  <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" charset="utf-8"></script>
+  <script type="text/javascript" src="http://code.jquery.com/jquery-1.5.2.js" charset="utf-8"></script>
   
   <!-- json2  -->
   <script type="text/javascript" src="json2.js" charset="utf-8"></script>
@@ -39,9 +39,9 @@
   <script type="text/javascript" charset="utf-8">
   //<![CDATA[
   $(document).ready(function(){
-    var transport = new Thrift.Transport("/service")
-    var protocol  = new Thrift.Protocol(transport)
-    var client    = new ThriftTest.ThriftTestClient(protocol)
+    var transport = new Thrift.Transport("/service");
+    var protocol  = new Thrift.Protocol(transport);
+    var client    = new ThriftTest.ThriftTestClient(protocol);
 
 
     module("Base Types");
@@ -69,11 +69,6 @@
     });
     test("I32", function() {
       equals(client.testI32(Math.pow(2,30)), Math.pow(2,30));
-      
-      /*
-        how to test things like that?
-        equals(client.testI32(Math.pow(2,60)), Math.pow(2,60));
-      */
     });
     test("I64", function() {
       equals(client.testI64(Math.pow(2,60)), Math.pow(2,60));
@@ -174,9 +169,6 @@
       equals(JSON.stringify(mapMapTestOutput), JSON.stringify(mapMapTestExpectedResult))
     });
 
-    test("testMulti", function() {
-    });
-
 
     module("Exception");
 
@@ -204,7 +196,7 @@
         client.testException("ApplicationException");
       } catch(e) {
         ok(true); //@HACK: ignore faulty java server response for exceptions
-	//equals(e.message, "ApplicationException");
+        //equals(e.message, "ApplicationException");
       }
     });
 
@@ -217,63 +209,157 @@
       ok(res);
     });
 
+
+    //////////////////////////////////
+    //Run same tests asynchronously
+    jQuery.ajaxSetup({ timeout: 0 });
+    $(document).ajaxError( function() { QUnit.start(); } );
+
+    module("Async Manual");
+
+    test("testI32", function() {
+      expect( 2 );
+      QUnit.stop();
+
+      var transport = new Thrift.Transport();
+      var protocol  = new Thrift.Protocol(transport);
+      var client    = new ThriftTest.ThriftTestClient(protocol);
+
+      var jqxhr = jQuery.ajax({
+        url: "/service",
+        data: client.send_testI32(Math.pow(-2,31)),
+        type: "POST",
+        cache: false,
+        dataType: "text",
+        success: function(res){
+          transport.setRecvBuffer( res );
+          equals(client.recv_testI32(), Math.pow(-2,31));
+        },
+        error: function() { ok(false); },
+        complete: function() {
+          ok(true);
+          QUnit.start();
+        }
+      });
+    });
+
+
+    test("testI64", function() {
+      expect( 2 );
+      QUnit.stop();
+
+      var transport = new Thrift.Transport();
+      var protocol  = new Thrift.Protocol(transport);
+      var client    = new ThriftTest.ThriftTestClient(protocol);
+
+      jQuery.ajax({
+        url: "/service",
+        data: client.send_testI64(Math.pow(-2,61)),
+        type: "POST",
+        cache: false,
+        dataType: "text",
+        success: function(res){
+          transport.setRecvBuffer( res );
+          equals(client.recv_testI64(), Math.pow(-2,61));
+        },
+        error: function() { ok(false); },
+        complete: function() {
+          ok(true);
+          QUnit.start();
+        }
+      });
+    });
+
+
+
+    module("Async");
+
+    test("Double", function() {
+      expect( 1 );
+
+      QUnit.stop();
+      client.testDouble(3.14159265, function(result) {
+        equals(result, 3.14159265);
+        QUnit.start();
+      });
+    });
+
+    test("Byte", function() {
+      expect( 1 );
+
+      QUnit.stop();
+      client.testByte(0x01, function(result) {
+        equals(result, 0x01);
+        QUnit.start();
+      });
+    });
+
+    test("I32", function() {
+      expect( 3 );
+
+      QUnit.stop();
+      client.testI32(Math.pow(2,30), function(result) {
+        equals(result, Math.pow(2,30));
+        QUnit.start();
+      });
+
+      QUnit.stop();
+      var jqxhr = client.testI32(Math.pow(-2,31), function(result) {
+        equals(result, Math.pow(-2,31));
+      });
+
+      jqxhr.success(function(result) {
+        equals(result, Math.pow(-2,31));
+        QUnit.start();
+      });
+    });
+
+    test("I64", function() {
+      expect( 4 );
+
+      QUnit.stop();
+      client.testI64(Math.pow(2,60), function(result) {
+        equals(result, Math.pow(2,60));
+        QUnit.start();
+      });
+
+      QUnit.stop();
+      client.testI64(Math.pow(-2,61), function(result) {
+        equals(result, Math.pow(-2,61));
+      })
+      .error( function(e) {  ok(false); } )
+      .success(function(result) {
+        equals(result, Math.pow(-2,61));
+      })
+      .complete(function() {
+        ok(true);
+        QUnit.start();
+      });
+    });
+
+    test("Xception", function() {
+      expect( 2 );
+
+      QUnit.stop();
+
+      var dfd = client.testException("Xception", function(result) {
+        ok(false);
+        QUnit.start();
+      })
+      .error(function(e){
+        equals(e.errorCode, 1001);
+        equals(e.message, "Xception");
+        QUnit.start();
+      });
+    });
+
+
   });
   //]]>
   </script>
 
 </head>
 <body>
-
-  <script type="text/javascript" charset="utf-8">
-  //<![CDATA[
-
-  //////////////////////////////////
-  //Run same tests asynchronously
-/*
-  var transport = new Thrift.Transport()
-  var protocol  = new Thrift.Protocol(transport)
-  var client    = new ThriftTest.ThriftTestClient(protocol)
-
-  document.write("<h2>Asynchronous Example<\/h2>")
-  jQuery.ajax({
-     url: "/service",
-     data: client.send_testI32(Math.pow(2,30)),
-     type: "POST",
-     cache: false,
-     success: function(res){
-            var _transport = new Thrift.Transport()
-            var _protocol  = new Thrift.Protocol(_transport)
-            var _client    = new ThriftTest.ThriftTestClient(_protocol)
-
-            _transport.setRecvBuffer( res )
-
-            var v =  _client.recv_testI32()
-            $("#body").append("client.testI32()    => "+(v == Math.pow(2,30))+"<br/>")
-
-     }
-  })
-
-  jQuery.ajax({
-     url: "/service",
-     data: client.send_testI64(Math.pow(2,60)),
-     type: "POST",
-     cache: false,
-     success: function(res){
-            var _transport = new Thrift.Transport()
-            var _protocol  = new Thrift.Protocol(_transport)
-            var _client    = new ThriftTest.ThriftTestClient(_protocol)
-
-            _transport.setRecvBuffer( res )
-
-            var v =  _client.recv_testI64()
-            $("#body").append("client.testI64()    => "+(v == Math.pow(2,60))+"<br/>")
-
-     }
-  })
-*/
-
-  //]]>
-  </script>
   <h1 id="qunit-header">Thrift Javascript Bindings: Unit Test (<a href="https://svn.apache.org/repos/asf/thrift/trunk/test/ThriftTest.thrift">ThriftTest.thrift</a>)</h1>
   <h2 id="qunit-banner"></h2>
   <div id="qunit-testrunner-toolbar"></div> 
diff --git a/lib/js/thrift.js b/lib/js/thrift.js
index b8ca2c0..c7a46fd 100644
--- a/lib/js/thrift.js
+++ b/lib/js/thrift.js
@@ -56,7 +56,7 @@
         var length = 0;
         for (var k in obj) {
             if (obj.hasOwnProperty(k)) {
-              length++;
+                length++;
             }
         }
 
@@ -75,8 +75,8 @@
 Thrift.TException = {};
 Thrift.TException.prototype = {
     initialize: function(message, code) {
-            this.message = message;
-            this.code = (code === null) ? 0 : code;
+        this.message = message;
+        this.code = (code === null) ? 0 : code;
     }
 };
 
@@ -186,19 +186,16 @@
 
     //Gets the browser specific XmlHttpRequest Object
     getXmlHttpRequestObject: function() {
-
         try { return new XMLHttpRequest(); } catch (e1) { }
         try { return new ActiveXObject('Msxml2.XMLHTTP'); } catch (e2) { }
         try { return new ActiveXObject('Microsoft.XMLHTTP'); } catch (e3) { }
 
         throw "Your browser doesn't support the XmlHttpRequest object.";
-
     },
 
-    flush: function() {
-
+    flush: function(async) {
         //async mode
-        if (this.url === undefined || this.url === '') {
+        if (async || this.url === undefined || this.url === '') {
             return this.send_buf;
         }
 
@@ -225,6 +222,54 @@
         this.rpos = 0;
     },
 
+    jqRequest: function(client, postData, args, recv_method) {
+        if (typeof jQuery === 'undefined' ||
+            typeof jQuery.Deferred === 'undefined') {
+            throw 'Thrift.js requires jQuery 1.5+ to use asynchronous requests';
+        }
+
+        // Deferreds
+        var deferred = jQuery.Deferred();
+        var completeDfd = jQuery._Deferred();
+        var dfd = deferred.promise();
+        dfd.success = dfd.done;
+        dfd.error = dfd.fail;
+        dfd.complete = completeDfd.done;
+
+        var jqXHR = jQuery.ajax({
+            url: this.url,
+            data: postData,
+            type: 'POST',
+            cache: false,
+            dataType: 'text',
+            context: this,
+            success: this.jqResponse,
+            error: function(xhr, status, e) {
+                deferred.rejectWith(client, jQuery.merge([e], xhr.tArgs));
+            },
+            complete: function(xhr, status) {
+                completeDfd.resolveWith(client, [xhr, status]);
+            }
+        });
+
+        deferred.done(jQuery.makeArray(args).pop()); //pop callback from args
+        jqXHR.tArgs = args;
+        jqXHR.tClient = client;
+        jqXHR.tRecvFn = recv_method;
+        jqXHR.tDfd = deferred;
+        return dfd;
+    },
+
+    jqResponse: function(responseData, textStatus, jqXHR) {
+      this.setRecvBuffer(responseData);
+      try {
+          var value = jqXHR.tRecvFn.call(jqXHR.tClient);
+          jqXHR.tDfd.resolveWith(jqXHR, jQuery.merge([value], jqXHR.tArgs));
+      } catch (ex) {
+          jqXHR.tDfd.rejectWith(jqXHR, jQuery.merge([ex], jqXHR.tArgs));
+      }
+    },
+
     setRecvBuffer: function(buf) {
         this.recv_buf = buf;
         this.recv_buf_sz = this.recv_buf.length;
@@ -531,7 +576,11 @@
         this.rstack = [];
         this.rpos = [];
 
-        this.robj = eval(this.transport.readAll());
+        if (typeof jQuery !== 'undefined') {
+            this.robj = jQuery.parseJSON(this.transport.readAll());
+        } else {
+            this.robj = eval(this.transport.readAll());
+        }
 
         var r = {};
         var version = this.robj.shift();
@@ -551,7 +600,6 @@
         return r;
     },
 
-
     readMessageEnd: function() {
     },
 
@@ -617,7 +665,6 @@
         r.ftype = ftype;
         r.fid = fid;
 
-
         return r;
     },
 
@@ -632,7 +679,6 @@
     },
 
     readMapBegin: function(keyType, valType, size) {
-
         var map = this.rstack.pop();
 
         var r = {};
@@ -652,14 +698,12 @@
     },
 
     readListBegin: function(elemType, size) {
-
         var list = this.rstack[this.rstack.length - 1];
 
         var r = {};
         r.etype = Thrift.Protocol.RType[list.shift()];
         r.size = list.shift();
 
-
         this.rpos.push(this.rstack.length);
         this.rstack.push(list);
 
@@ -698,7 +742,6 @@
         return this.readI32();
     },
 
-
     readI32: function(f) {
         if (f === undefined) {
             f = this.rstack[this.rstack.length - 1];
@@ -753,5 +796,4 @@
     skip: function(type) {
         throw 'skip not supported yet';
     }
-
 };