THRIFT-1893 HTTP/JSON server/client for node.js
Client: nodejs
Patch: Phillip Campbell
diff --git a/lib/nodejs/examples/server_http.js b/lib/nodejs/examples/server_http.js
new file mode 100644
index 0000000..ef2dc83
--- /dev/null
+++ b/lib/nodejs/examples/server_http.js
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+var connect = require('connect');
+var thrift = require('thrift');
+
+var UserStorage = require('./gen-nodejs/UserStorage'),
+ ttypes = require('./gen-nodejs/user_types');
+
+var users = {};
+
+var store = function(user, result) {
+ console.log("stored:", user.uid);
+ users[user.uid] = user;
+ result(null);
+};
+var retrieve = function(uid, result) {
+ console.log("retrieved:", uid);
+ result(null, users[uid]);
+};
+
+var server_http = thrift.createHttpServer(UserStorage, {
+ store: store,
+ retrieve: retrieve
+});
+server_http.listen(9090);
+
+var server_connect = connect(thrift.httpMiddleware(UserStorage, {
+ store: store,
+ retrieve: retrieve
+}));
+server_http.listen(9091);
+
+var server_connect_json = connect(thrift.httpMiddleware(UserStorage, {
+ store: store,
+ retrieve: retrieve
+}, {protocol: thrift.TJSONProtocol}));
+server_connect_json.listen(9092);
diff --git a/lib/nodejs/lib/thrift/index.js b/lib/nodejs/lib/thrift/index.js
index 2554a2f..bf34085 100644
--- a/lib/nodejs/lib/thrift/index.js
+++ b/lib/nodejs/lib/thrift/index.js
@@ -25,7 +25,10 @@
exports.createStdIOClient = connection.createStdIOClient;
exports.createStdIOConnection = connection.createStdIOConnection;
-exports.createServer = require('./server').createServer;
+server = require('./server')
+exports.createServer = server.createServer;
+exports.createHttpServer = server.createHttpServer;
+exports.httpMiddleware = server.httpMiddleware
exports.Int64 = require('node-int64')
@@ -36,3 +39,4 @@
exports.TFramedTransport = require('./transport').TFramedTransport;
exports.TBufferedTransport = require('./transport').TBufferedTransport;
exports.TBinaryProtocol = require('./protocol').TBinaryProtocol;
+exports.TJSONProtocol = require('./protocol').TJSONProtocol;
diff --git a/lib/nodejs/lib/thrift/protocol.js b/lib/nodejs/lib/thrift/protocol.js
index cbf6c9a..f3cd938 100644
--- a/lib/nodejs/lib/thrift/protocol.js
+++ b/lib/nodejs/lib/thrift/protocol.js
@@ -36,6 +36,13 @@
}
util.inherits(TProtocolException, Error);
+// NastyHaxx. JavaScript forces hex constants to be
+// positive, converting this into a long. If we hardcode the int value
+// instead it'll stay in 32 bit-land.
+var VERSION_MASK = -65536, // 0xffff0000
+ VERSION_1 = -2147418112, // 0x80010000
+ TYPE_MASK = 0x000000ff;
+
var TBinaryProtocol = exports.TBinaryProtocol = function(trans, strictRead, strictWrite) {
this.trans = trans;
this.strictRead = (strictRead !== undefined ? strictRead : false);
@@ -46,14 +53,6 @@
return this.trans.flush();
}
-// NastyHaxx. JavaScript forces hex constants to be
-// positive, converting this into a long. If we hardcode the int value
-// instead it'll stay in 32 bit-land.
-
-var VERSION_MASK = -65536, // 0xffff0000
- VERSION_1 = -2147418112, // 0x80010000
- TYPE_MASK = 0x000000ff;
-
TBinaryProtocol.prototype.writeMessageBegin = function(name, type, seqid) {
if (this.strictWrite) {
this.writeI32(VERSION_1 | type);
@@ -105,7 +104,6 @@
}
TBinaryProtocol.prototype.writeSetBegin = function(etype, size) {
- console.log('write set', etype, size);
this.writeByte(etype);
this.writeI32(size);
}
@@ -287,7 +285,6 @@
}
TBinaryProtocol.prototype.skip = function(type) {
- // console.log("skip: " + type);
switch (type) {
case Type.STOP:
return;
@@ -350,3 +347,465 @@
throw Error("Invalid type: " + type);
}
}
+
+var TJSONProtocol = exports.TJSONProtocol = function(trans) {
+ this.trans = trans;
+}
+
+TJSONProtocol.Type = {};
+TJSONProtocol.Type[Thrift.Type.BOOL] = '"tf"';
+TJSONProtocol.Type[Thrift.Type.BYTE] = '"i8"';
+TJSONProtocol.Type[Thrift.Type.I16] = '"i16"';
+TJSONProtocol.Type[Thrift.Type.I32] = '"i32"';
+TJSONProtocol.Type[Thrift.Type.I64] = '"i64"';
+TJSONProtocol.Type[Thrift.Type.DOUBLE] = '"dbl"';
+TJSONProtocol.Type[Thrift.Type.STRUCT] = '"rec"';
+TJSONProtocol.Type[Thrift.Type.STRING] = '"str"';
+TJSONProtocol.Type[Thrift.Type.MAP] = '"map"';
+TJSONProtocol.Type[Thrift.Type.LIST] = '"lst"';
+TJSONProtocol.Type[Thrift.Type.SET] = '"set"';
+
+
+TJSONProtocol.RType = {};
+TJSONProtocol.RType.tf = Thrift.Type.BOOL;
+TJSONProtocol.RType.i8 = Thrift.Type.BYTE;
+TJSONProtocol.RType.i16 = Thrift.Type.I16;
+TJSONProtocol.RType.i32 = Thrift.Type.I32;
+TJSONProtocol.RType.i64 = Thrift.Type.I64;
+TJSONProtocol.RType.dbl = Thrift.Type.DOUBLE;
+TJSONProtocol.RType.rec = Thrift.Type.STRUCT;
+TJSONProtocol.RType.str = Thrift.Type.STRING;
+TJSONProtocol.RType.map = Thrift.Type.MAP;
+TJSONProtocol.RType.lst = Thrift.Type.LIST;
+TJSONProtocol.RType.set = Thrift.Type.SET;
+
+TJSONProtocol.Version = 1;
+
+TJSONProtocol.prototype.flush = function() {
+ return this.trans.flush();
+}
+
+TJSONProtocol.prototype.writeMessageBegin = function(name, messageType, seqid) {
+ this.tstack = [];
+ this.tpos = [];
+
+ this.tstack.push([TJSONProtocol.Version, '"' + name + '"', messageType, seqid]);
+}
+
+TJSONProtocol.prototype.writeMessageEnd = function() {
+ var obj = this.tstack.pop();
+
+ this.wobj = this.tstack.pop();
+ this.wobj.push(obj);
+
+ this.wbuf = '[' + this.wobj.join(',') + ']';
+
+ this.trans.write(this.wbuf);
+}
+
+TJSONProtocol.prototype.writeStructBegin = function(name) {
+ this.tpos.push(this.tstack.length);
+ this.tstack.push({});
+}
+
+TJSONProtocol.prototype.writeStructEnd = function() {
+ var p = this.tpos.pop();
+ var struct = this.tstack[p];
+ var str = '{';
+ var first = true;
+ for (var key in struct) {
+ if (first) {
+ first = false;
+ } else {
+ str += ',';
+ }
+
+ str += key + ':' + struct[key];
+ }
+
+ str += '}';
+ this.tstack[p] = str;
+}
+
+TJSONProtocol.prototype.writeFieldBegin = function(name, fieldType, fieldId) {
+ this.tpos.push(this.tstack.length);
+ this.tstack.push({ 'fieldId': '"' +
+ fieldId + '"', 'fieldType': TJSONProtocol.Type[fieldType]
+ });
+}
+
+TJSONProtocol.prototype.writeFieldEnd = function() {
+ var value = this.tstack.pop();
+ var fieldInfo = this.tstack.pop();
+
+ this.tstack[this.tstack.length - 1][fieldInfo.fieldId] = '{' +
+ fieldInfo.fieldType + ':' + value + '}';
+ this.tpos.pop();
+}
+
+TJSONProtocol.prototype.writeFieldStop = function() {
+}
+
+TJSONProtocol.prototype.writeMapBegin = function(ktype, vtype, size) {
+ //size is invalid, we'll set it on end.
+ this.tpos.push(this.tstack.length);
+ this.tstack.push([TJSONProtocol.Type[ktype], TJSONProtocol.Type[vtype], 0]);
+}
+
+TJSONProtocol.prototype.writeMapEnd = function() {
+ var p = this.tpos.pop();
+
+ if (p == this.tstack.length) {
+ return;
+ }
+
+ if ((this.tstack.length - p - 1) % 2 !== 0) {
+ this.tstack.push('');
+ }
+
+ var size = (this.tstack.length - p - 1) / 2;
+
+ this.tstack[p][this.tstack[p].length - 1] = size;
+
+ var map = '}';
+ var first = true;
+ while (this.tstack.length > p + 1) {
+ var v = this.tstack.pop();
+ var k = this.tstack.pop();
+ if (first) {
+ first = false;
+ } else {
+ map = ',' + map;
+ }
+
+ if (! isNaN(k)) { k = '"' + k + '"'; } //json "keys" need to be strings
+ map = k + ':' + v + map;
+ }
+ map = '{' + map;
+
+ this.tstack[p].push(map);
+ this.tstack[p] = '[' + this.tstack[p].join(',') + ']';
+}
+
+TJSONProtocol.prototype.writeListBegin = function(etype, size) {
+ this.tpos.push(this.tstack.length);
+ this.tstack.push([TJSONProtocol.Type[etype], size]);
+}
+
+TJSONProtocol.prototype.writeListEnd = function() {
+ var p = this.tpos.pop();
+
+ while (this.tstack.length > p + 1) {
+ var tmpVal = this.tstack[p + 1];
+ this.tstack.splice(p + 1, 1);
+ this.tstack[p].push(tmpVal);
+ }
+
+ this.tstack[p] = '[' + this.tstack[p].join(',') + ']';
+}
+
+TJSONProtocol.prototype.writeSetBegin = function(etype, size) {
+ this.tpos.push(this.tstack.length);
+ this.tstack.push([TJSONProtocol.Type[etype], size]);
+}
+
+TJSONProtocol.prototype.writeSetEnd = function() {
+ var p = this.tpos.pop();
+
+ while (this.tstack.length > p + 1) {
+ var tmpVal = this.tstack[p + 1];
+ this.tstack.splice(p + 1, 1);
+ this.tstack[p].push(tmpVal);
+ }
+
+ this.tstack[p] = '[' + this.tstack[p].join(',') + ']';
+}
+
+TJSONProtocol.prototype.writeBool = function(bool) {
+ this.tstack.push(bool ? 1 : 0);
+}
+
+TJSONProtocol.prototype.writeByte = function(byte) {
+ this.tstack.push(byte);
+}
+
+TJSONProtocol.prototype.writeI16 = function(i16) {
+ this.tstack.push(i16);
+}
+
+TJSONProtocol.prototype.writeI32 = function(i32) {
+ this.tstack.push(i32);
+}
+
+TJSONProtocol.prototype.writeI64 = function(i64) {
+ this.tstack.push(i64);
+}
+
+TJSONProtocol.prototype.writeDouble = function(dub) {
+ this.tstack.push(dub);
+}
+
+TJSONProtocol.prototype.writeString = function(str) {
+ // We do not encode uri components for wire transfer:
+ if (str === null) {
+ this.tstack.push(null);
+ } else {
+ // concat may be slower than building a byte buffer
+ var escapedString = '';
+ for (var i = 0; i < str.length; i++) {
+ var ch = str.charAt(i); // a single double quote: "
+ if (ch === '\\\\"') {
+ escapedString += '\\\\\\\\\\\\"'; // write out as: \\\\"
+ } else if (ch === '\\\\\\\\') { // a single backslash: \\\\
+ escapedString += '\\\\\\\\\\\\\\\\'; // write out as: \\\\\\\\
+ /* Currently escaped forward slashes break TJSONProtocol.
+ * As it stands, we can simply pass forward slashes into
+ * our strings across the wire without being escaped.
+ * I think this is the protocol's bug, not thrift.js
+ * } else if(ch === '/') { // a single forward slash: /
+ * escapedString += '\\\\\\\\/'; // write out as \\\\/
+ * }
+ */
+ } else if (ch === '\\\\b') { // a single backspace: invisible
+ escapedString += '\\\\\\\\b'; // write out as: \\\\b"
+ } else if (ch === '\\\\f') { // a single formfeed: invisible
+ escapedString += '\\\\\\\\f'; // write out as: \\\\f"
+ } else if (ch === '\\\\n') { // a single newline: invisible
+ escapedString += '\\\\\\\\n'; // write out as: \\\\n"
+ } else if (ch === '\\\\r') { // a single return: invisible
+ escapedString += '\\\\\\\\r'; // write out as: \\\\r"
+ } else if (ch === '\\\\t') { // a single tab: invisible
+ escapedString += '\\\\\\\\t'; // write out as: \\\\t"
+ } else {
+ escapedString += ch; // Else it need not be escaped
+ }
+ }
+ this.tstack.push('"' + escapedString + '"');
+ }
+}
+
+TJSONProtocol.prototype.writeBinary = function(arg) {
+ this.writeString(arg);
+}
+
+TJSONProtocol.prototype.readMessageBegin = function() {
+ this.rstack = [];
+ this.rpos = [];
+
+ this.robj = JSON.parse(this.trans.readAll());
+
+ var r = {};
+ var version = this.robj.shift();
+
+ if (version != TJSONProtocol.Version) {
+ throw 'Wrong thrift protocol version: ' + version;
+ }
+
+ r.fname = this.robj.shift();
+ r.mtype = this.robj.shift();
+ r.rseqid = this.robj.shift();
+
+
+ //get to the main obj
+ this.rstack.push(this.robj.shift());
+
+ return r;
+}
+
+TJSONProtocol.prototype.readMessageEnd = function() {
+}
+
+TJSONProtocol.prototype.readStructBegin = function() {
+ var r = {};
+ r.fname = '';
+
+ //incase this is an array of structs
+ if (this.rstack[this.rstack.length - 1] instanceof Array) {
+ this.rstack.push(this.rstack[this.rstack.length - 1].shift());
+ }
+
+ return r;
+}
+
+TJSONProtocol.prototype.readStructEnd = function() {
+ if (this.rstack[this.rstack.length - 2] instanceof Array) {
+ this.rstack.pop();
+ }
+}
+
+TJSONProtocol.prototype.readFieldBegin = function() {
+ var r = {};
+
+ var fid = -1;
+ var ftype = Thrift.Type.STOP;
+
+ //get a fieldId
+ for (var f in (this.rstack[this.rstack.length - 1])) {
+ if (f === null) {
+ continue;
+ }
+
+ fid = parseInt(f, 10);
+ this.rpos.push(this.rstack.length);
+
+ var field = this.rstack[this.rstack.length - 1][fid];
+
+ //remove so we don't see it again
+ delete this.rstack[this.rstack.length - 1][fid];
+
+ this.rstack.push(field);
+
+ break;
+ }
+
+ if (fid != -1) {
+ //should only be 1 of these but this is the only
+ //way to match a key
+ for (var i in (this.rstack[this.rstack.length - 1])) {
+ if (TJSONProtocol.RType[i] === null) {
+ continue;
+ }
+
+ ftype = TJSONProtocol.RType[i];
+ this.rstack[this.rstack.length - 1] = this.rstack[this.rstack.length - 1][i];
+ }
+ }
+
+ r.fname = '';
+ r.ftype = ftype;
+ r.fid = fid;
+
+ return r;
+}
+
+TJSONProtocol.prototype.readFieldEnd = function() {
+ var pos = this.rpos.pop();
+
+ //get back to the right place in the stack
+ while (this.rstack.length > pos) {
+ this.rstack.pop();
+ }
+}
+
+TJSONProtocol.prototype.readMapBegin = function() {
+ var map = this.rstack.pop();
+
+ var r = {};
+ r.ktype = TJSONProtocol.RType[map.shift()];
+ r.vtype = TJSONProtocol.RType[map.shift()];
+ r.size = map.shift();
+
+
+ this.rpos.push(this.rstack.length);
+ this.rstack.push(map.shift());
+
+ return r;
+}
+
+TJSONProtocol.prototype.readMapEnd = function() {
+ this.readFieldEnd();
+}
+
+TJSONProtocol.prototype.readListBegin = function() {
+ var list = this.rstack[this.rstack.length - 1];
+
+ var r = {};
+ r.etype = TJSONProtocol.RType[list.shift()];
+ r.size = list.shift();
+
+ this.rpos.push(this.rstack.length);
+ this.rstack.push(list);
+
+ return r;
+}
+
+TJSONProtocol.prototype.readListEnd = function() {
+ this.readFieldEnd();
+}
+
+TJSONProtocol.prototype.readSetBegin = function() {
+ return this.readListBegin();
+}
+
+TJSONProtocol.prototype.readSetEnd = function() {
+ return this.readListEnd();
+}
+
+TJSONProtocol.prototype.readBool = function() {
+ var r = this.readI32();
+
+ if (r !== null && r.value == '1') {
+ r.value = true;
+ } else {
+ r.value = false;
+ }
+
+ return r;
+}
+
+TJSONProtocol.prototype.readByte = function() {
+ return this.readI32();
+}
+
+TJSONProtocol.prototype.readI16 = function() {
+ return this.readI32();
+}
+
+TJSONProtocol.prototype.readI32 = function(f) {
+ if (f === undefined) {
+ f = this.rstack[this.rstack.length - 1];
+ }
+
+ var r = {};
+
+ if (f instanceof Array) {
+ if (f.length === 0) {
+ r.value = undefined;
+ } else {
+ r.value = f.shift();
+ }
+ } else if (f instanceof Object) {
+ for (var i in f) {
+ if (i === null) {
+ continue;
+ }
+ this.rstack.push(f[i]);
+ delete f[i];
+
+ r.value = i;
+ break;
+ }
+ } else {
+ r.value = f;
+ this.rstack.pop();
+ }
+
+ return r.value;
+}
+
+TJSONProtocol.prototype.readI64 = function() {
+ return new Int64(this.readI32());
+}
+
+TJSONProtocol.prototype.readDouble = function() {
+ return this.readI32();
+}
+
+TJSONProtocol.prototype.readBinary = function() {
+ return this.readString();
+}
+
+TJSONProtocol.prototype.readString = function() {
+ var r = this.readI32();
+ return r;
+}
+
+TJSONProtocol.prototype.getTransport = function() {
+ return this.trans;
+}
+
+//Method to arbitrarily skip over data.
+TJSONProtocol.prototype.skip = function(type) {
+ throw 'skip not supported yet';
+}
+
diff --git a/lib/nodejs/lib/thrift/server.js b/lib/nodejs/lib/thrift/server.js
index f219048..228bb03 100644
--- a/lib/nodejs/lib/thrift/server.js
+++ b/lib/nodejs/lib/thrift/server.js
@@ -17,6 +17,7 @@
* under the License.
*/
var net = require('net');
+var http = require('http');
var ttransport = require('./transport')
, TBinaryProtocol = require('./protocol').TBinaryProtocol;
@@ -62,3 +63,48 @@
});
});
};
+
+function httpRequestHandler(cls, handler, options) {
+ if (cls.Processor) {
+ cls = cls.Processor;
+ }
+ var processor = new cls(handler);
+ var transport = (options && options.transport) ? options.transport : ttransport.TBufferedTransport;
+ var protocol = (options && options.protocol) ? options.protocol : TBinaryProtocol;
+
+ return function(request, response) {
+ var self = this;
+
+ request.on('data', transport.receiver(function(transportWithData) {
+ var input = new protocol(transportWithData);
+ var output = new protocol(new transport(undefined, function(buf) {
+ try {
+ response.write(buf);
+ } catch (err) {
+ response.writeHead(500);
+ }
+ response.end();
+ }));
+
+ try {
+ processor.process(input, output);
+ transportWithData.commitPosition();
+ }
+ catch (err) {
+ if (err instanceof ttransport.InputBufferUnderrunError) {
+ transportWithData.rollbackPosition();
+ } else {
+ response.writeHead(500);
+ response.end();
+ throw err;
+ }
+ }
+ }));
+ };
+};
+
+exports.httpMiddleware = httpRequestHandler;
+
+exports.createHttpServer = function(cls, handler, options) {
+ return http.createServer(httpRequestHandler(cls, handler, options));
+};
diff --git a/lib/nodejs/package.json b/lib/nodejs/package.json
index c57a07b..668c58d 100755
--- a/lib/nodejs/package.json
+++ b/lib/nodejs/package.json
@@ -27,5 +27,8 @@
"dependencies": {
"node-int64": "~0.3.0",
"nodeunit": "~0.8.0"
+ },
+ "devDependencies": {
+ "connect": "2.7.x"
}
}