diff --git a/engines/default/lib/http-engine.js b/engines/default/lib/http-engine.js deleted file mode 100644 index 9338e1a9..00000000 --- a/engines/default/lib/http-engine.js +++ /dev/null @@ -1,2 +0,0 @@ -require("narwhal").deprecated("use http-client-engine instead of http-engine"); -require("narwhal/util").update(exports. require("http-client-engine")); diff --git a/engines/rhino/lib/http-client-engine.js b/engines/rhino/lib/http-client-engine.js index 46ab52ab..e4a0f3b4 100644 --- a/engines/rhino/lib/http-client-engine.js +++ b/engines/rhino/lib/http-client-engine.js @@ -1,99 +1,80 @@ -// -- isaacs Isaac Schlueter // -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License +// -- tlrobinson Tom Robinson -var IO = require("io").IO, - UTIL = require("narwhal/util"); +var IO = require("io").IO; -exports.IO = function (url) { - return new IO( - new java.net.URL(url).openStream(), - null - ); -}; +exports.open = function(url, mode, options) { + var connection, output, input; -exports.connect = function HttpClient_engine_connect (tx) { - if (tx._isConnected) return; - - var con = java.net.HttpURLConnection(new java.net.URL(tx.url).openConnection()); - con.setRequestMethod(tx.method.toUpperCase()); - - UTIL.forEach(tx.headers, function (h, v) { - con.setRequestProperty(h, v); - }); - if (!tx.headers) tx.headers = {}; - var cl = UTIL.get(tx.headers, "Content-Length") || 0; - if (cl > 0) { - con.setDoOutput(true); - var os = null; - try { - os = con.getOutputStream(); - } catch (ex) {} - if (os) { - var writer = new IO(null, con.getOutputStream()); - tx.body.forEach(function (piece) { - writer.write(piece); - }); - writer.close(); - } - } - - try { - con.connect(); - } catch (ex) { - // It would be nice to do something clever and special here, - // but I'm not feeling it at the moment. - ex.message = [ - "Could not connect to "+tx.url+". Probably a bad hostname.", - ex.message - ].join("\n"); - throw ex; - } - - tx._isConnected = true; - var resp = tx._response = {status:200, headers:{}, body:[]}; - - // Call this now to trigger the fetch asynchronously. - // This way, if you set up multiple HttpClients, and then call connect() - // on all of them, you'll only wait as long as the slowest one, since - // the streams will start filling up right away. - - - // now pull everything out. - var fields = con.getHeaderFields(); - var fieldKeys = fields.keySet().toArray(); - for (var i = 0, l = fieldKeys.length; i < l; i ++ ) { - var fieldValue = fields.get(fieldKeys[i]).toArray().join(''); - var fieldName = fieldKeys[i]; - if (fieldName === null) { - // Something like: HTTP/1.1 200 OK - UTIL.set(resp, "status", +(/HTTP\/1\.[01] ([0-9]{3})/.exec(fieldValue)[1])); - // fieldName = "Status"; - resp.statusText = fieldValue; - continue; + function startRequest() { + connection = new java.net.URL(url).openConnection(); + connection.setDoInput(true); + connection.setDoOutput(true); + connection.setRequestMethod(options.method); + connection.setInstanceFollowRedirects(!!options.followRedirects); + + for (var name in options.headers) { + if (options.headers.hasOwnProperty(name)) { + connection.addRequestProperty(String(name), String(options.headers[name])); + } } - UTIL.set(resp.headers, fieldName, fieldValue); + + connection.connect(); + + output = new IO(null, connection.getOutputStream()); + input = null; } - // TODO: Restructure using non-blocking IO to support asynchronous interactions. - // In that case, you could just have a callback that gets each bytestring. - var is = null; - try { - var is = con.getInputStream(); - } catch (ex) { - return resp; + startRequest(); + + var request = { + status : null, + headers : {}, + read : function() { + if (!input) { + output.close(); + input = new IO(connection.getInputStream(), null); + this.status = Number(connection.getResponseCode()); + this.statusText = String(connection.getResponseMessage() || ""); + for (var i = 0; ; i++) { + var key = connection.getHeaderFieldKey(i), value = connection.getHeaderField(i); + if (!key && !value) { + break; + } + if (key) { + key = String(key); + value = String(value); + this.headers[key] = value; + if (key.toUpperCase() === "LOCATION") + url = value; + } + } + + // Manually follow cross-protocol redirects because Java doesn't: + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4620571 + if (options.followRedirects && this.status >= 300 && this.status < 400) { + // TODO: should we change the method to GET if it was not a GET like curl does? + startRequest(); + return this.read.apply(this, arguments); + } + } + return input.read.apply(input, arguments); + }, + write : function() { + output.write.apply(output, arguments); + return this; + }, + flush : function() { + output.flush.apply(output, arguments); + return this; + }, + close : function() { + output && output.close(); + input && input.close(); + return this; + }, + copy : IO.prototype.copy } - - // TODO: Should the input stream be rewindable? - var reader = new IO(con.getInputStream(), null); - resp.body = {forEach : function (block) { - var buflen = 1024; - for ( - var bytes = reader.read(buflen); - bytes.length > 0; - bytes = reader.read(buflen) - ) block(bytes); - }}; - - return resp; + return request; }; diff --git a/engines/rhino/lib/http-engine.js b/engines/rhino/lib/http-engine.js deleted file mode 100644 index ad499f25..00000000 --- a/engines/rhino/lib/http-engine.js +++ /dev/null @@ -1,12 +0,0 @@ - -// -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License - -var IO = require('./io').IO; - -exports.IO = function (url) { - return new IO( - new java.net.URL(url).openStream(), - null - ); -}; - diff --git a/engines/rhino/lib/http.js b/engines/rhino/lib/http.js deleted file mode 100644 index 230e8454..00000000 --- a/engines/rhino/lib/http.js +++ /dev/null @@ -1,79 +0,0 @@ - -// -- tlrobinson Tom Robinson - -var FILE = require("file"); -var IO = require("io").IO; - -exports.open = function(url, mode, options) { - mode = mode || "b"; - options = options || {}; - - options.method = options.method || "GET"; - options.headers = options.headers || {}; - - var connection = new java.net.URL(url).openConnection(); - connection.setDoInput(true); - connection.setDoOutput(true); - connection.setRequestMethod(options.method); - - for (var name in options.headers){ - connection.addRequestProperty(String(name), String(options.headers[name])); - } - - connection.connect(); - - var output = new IO(null, connection.getOutputStream()); - var input = null; - - var request = { - status : null, - headers : {}, - read : function() { - if (!input) { - output.close(); - input = new IO(connection.getInputStream(), null); - this.status = Number(connection.getResponseCode()); - this.statusText = String(connection.getResponseMessage() || ""); - for (var i = 0; ; i++) { - var key = connection.getHeaderFieldKey(i), value = connection.getHeaderField(i); - if (! key && ! value) - break; - if (key) - this.headers[String(key)] = String(value); - } - } - return input.read.apply(input, arguments); - }, - write : function() { - output.write.apply(output, arguments); - return this; - }, - flush : function() { - output.flush.apply(output, arguments); - return this; - }, - close : function() { - if (output) - output.close(); - if (input) - input.close(); - return this; - }, - copy : IO.prototype.copy - } - return request; -}; - -exports.read = function(url) { - var stream = exports.open(url); - try { - return stream.read(); - } finally { - stream.close(); - } -}; - -exports.copy = function(source, target, mode) { - mode = mode || "b"; - return FILE.path(target).write(exports.read(source, mode), mode); -}; diff --git a/lib/http-client.js b/lib/http-client.js index ca20a7be..d1e31763 100644 --- a/lib/http-client.js +++ b/lib/http-client.js @@ -4,15 +4,22 @@ // -- kriskowal Kris Kowal Copyright (C) 2009-2010 MIT License // -- cadorn Christoph Dorn TODO // -- veged Sergey Berezhnoy TODO +// -- tlrobinson Tom Robinson -var IO = require("io").IO, - UTIL = require("narwhal/util"), - ENGINE = require("http-client-engine"), - FS = require("file"); +var ENGINE = require("http-client-engine"); +var FILE = require("file"); exports.open = function (url, mode, options) { // todo mode negotiation, particularly for binary vs text buffering - return new ENGINE.IO(url); + mode = mode || "b"; + options = options || {}; + + options.method = options.method || "GET"; + options.headers = options.headers || {}; + if (options.followRedirects === undefined) + options.followRedirects = true; + + return ENGINE.open(url, mode, options); }; exports.read = function (url) { @@ -27,147 +34,5 @@ exports.read = function (url) { // TODO resolve the source as a file URL exports.copy = function (source, target, mode) { mode = mode || 'b'; - return FS.path(target).write(exports.read(source, mode), mode); -}; - -exports.HttpClient = HttpClient; - -var newTransactionId = (function () { - var id = 1; - return function () { - return id ++; - }; -})(); - -var transaction = (function () { - var hidden = {}; - return function (id, del) { - if (del && hidden.hasOwnProperty(id)) { - delete hidden[id]; - return; - } - if (!hidden.hasOwnProperty(id)) hidden[id] = {}; - return hidden[id]; - }; -})(); - -// {method, url, headers, body} -// return value is {status:status, headers:{..}, body:[..]} -function HttpClient (settings) { - if (!(this instanceof HttpClient)) return new HttpClient(settings); - if (!this.txId) this.create(); - if (settings) this.setOptions(settings); -}; - -HttpClient.prototype = { - - create : function HttpClient_create () { - // clean up, set up defaults. - transaction(this.txId, true); - this.txId = newTransactionId(); - this.setOptions({ - "method" : "GET", - "headers" : {}, - "body" : [] - }); - return this; - }, - - setOptions : function HttpClient_setOption (settings) { - for (var key in settings) if (settings.hasOwnProperty(key)) { - this.setOption(key, settings[key]); - } - return this; - }, - - setOption : function HttpClient_setOption (key, val) { - var guts = transaction(this.txId); - switch (key) { - case "headers": - if (typeof val !== 'object') throw new Error( - "HttpClient: headers must be a simple object." - ); - return this.setHeaders(val); - case "body": - if (typeof val.forEach !== 'function') throw new Error( - "HttpClient: body must be iterable." - ); - // fallthrough - default: - guts[key] = val; - } - return this; - }, - - setHeaders : function HttpClient_setHeaders (headers) { - for (var h in headers) if (headers.hasOwnProperty(h)) { - this.setHeader(h, headers[h]); - } - return this; - }, - - setHeader : function HttpClient_setHeader (key, val) { - var guts = transaction(this.txId); - if (!guts.hasOwnProperty("headers")) guts.headers = {}; - UTIL.set(guts.headers, key, val); - return this; - }, - - write : function HttpClient_write (data) { - var guts = transaction(this.txId); - var len = UTIL.get(guts.headers, "Content-Length") || 0; - len += data.length; - UTIL.set(guts.headers, "Content-Length", len); - guts.body.push(data); - return this; - }, - - connect : function HttpClient_connect (decode) { - var guts = transaction(this.txId); - var resp = ENGINE.connect(guts); - if (decode) HttpClient.decode(resp); - transaction(this.txId, true); - return resp; - } - + return FILE.path(target).write(exports.read(source, mode), mode); }; - -HttpClient.decode = function HttpClient_decode (resp, encoding) { - encoding = encoding || UTIL.get(resp.headers, "Content-Encoding"); - if (!encoding) { - var contentType = UTIL.get(resp.headers, "Content-Type"); - if (contentType) { - encoding = /charset=([^;\s]+)/.exec(contentType); - if (encoding) encoding = encoding[1]; - } - } - // fall back on UTF-8. It's almost always a good choice. - if (!encoding) encoding = 'UTF-8'; - var raw = resp.body; - resp._rawBody = raw; - resp.body = {forEach : function (block) { - raw.forEach(function (i) { - block(i.decodeToString(encoding)); - }); - }}; - return resp; -}; - -HttpClient.undecode = function HttpClient_undecode (resp) { - if ("_rawBody" in resp) resp.body = resp._rawBody; - delete resp._rawBody; - return resp; -}; - -HttpClient.print = function HttpClient_print (resp) { - var out = []; - out.push(resp.statusText); - UTIL.forEach(resp.headers, function (h, v) { - out.push(h+": "+v); - }); - out.push(""); - out=[out.join("\n")]; - resp.body.forEach(function (p) { out.push(p) }); - print(out.join("")); -}; -