1 /*jslint nomen: false, evil: true*/
2 /*global window XMLHttpRequest require console process __dirname setTimeout Packages print readFile quit Buffer*/
5 * Three implementations of a runtime for browser, node.js and rhino.
9 * Abstraction of the runtime environment.
14 * @param {!string} path
15 * @param {!number} offset
16 * @param {!number} length
17 * @param {!function(string=,string=):undefined} callback
20 Runtime.prototype.read = function (path, offset, length, callback) {};
22 * @param {!string} path
23 * @param {!string} encoding text encoding or 'binary'
24 * @param {!function(string=,string=):undefined} callback
27 Runtime.prototype.readFile = function (path, encoding, callback) {};
29 * @param {!string} path
30 * @param {!string} encoding text encoding or 'binary'
33 Runtime.prototype.readFileSync = function (path, encoding) {};
35 * @param {!string} path
36 * @param {!function((string|Document)):undefined} callback
39 Runtime.prototype.loadXML = function (path, callback) {};
41 * @param {!string} path
42 * @param {!string} data
43 * @param {!string} encoding text encoding or "binary"
44 * @param {!function(?string):undefined} callback
47 Runtime.prototype.writeFile = function (path, data, encoding, callback) {};
49 * @param {!string} path
50 * @param {!function(boolean):undefined} callback
53 Runtime.prototype.isFile = function (path, callback) {};
55 * @param {!string} path
56 * @param {!function(number):undefined} callback
59 Runtime.prototype.getFileSize = function (path, callback) {};
61 * @param {!string} path
62 * @param {!function(?string):undefined} callback
65 Runtime.prototype.deleteFile = function (path, callback) {};
67 * @param {!string} msgOrCategory
68 * @param {!string=} msg
71 Runtime.prototype.log = function (msgOrCategory, msg) {};
73 * @param {!function():undefined} callback
74 * @param {!number} milliseconds
77 Runtime.prototype.setTimeout = function (callback, milliseconds) {};
79 * @return {!Array.<string>}
81 Runtime.prototype.libraryPaths = function () {};
85 Runtime.prototype.type = function () {};
87 * @return {?DOMImplementation}
89 Runtime.prototype.getDOMImplementation = function () {};
93 Runtime.prototype.getWindow = function () {};
95 /** @define {boolean} */
96 var IS_COMPILED_CODE = false;
100 * @implements {Runtime}
101 * @param {Element} logoutput
103 function BrowserRuntime(logoutput) {
105 nativeio = window.nativeio || {};
106 function log(msgOrCategory, msg) {
107 var node, doc, category;
109 category = msgOrCategory;
114 doc = logoutput.ownerDocument;
116 node = doc.createElement("span");
117 node.className = category;
118 node.appendChild(doc.createTextNode(category));
119 logoutput.appendChild(node);
120 logoutput.appendChild(doc.createTextNode(" "));
122 node = doc.createElement("span");
123 node.appendChild(doc.createTextNode(msg));
124 logoutput.appendChild(node);
125 logoutput.appendChild(doc.createElement("br"));
126 } else if (console) {
130 // tentative function to fix problems with sending binary data
131 function cleanDataString(s) {
132 var str = "", i, l = s.length;
133 for (i = 0; i < l; i += 1) {
134 str += String.fromCharCode(s.charCodeAt(i) & 0xff);
138 function readFile(path, encoding, callback) {
139 var xhr = new XMLHttpRequest();
140 function handleResult() {
142 if (xhr.readyState === 4) {
143 if (xhr.status === 0 && !xhr.responseText) {
144 // for local files there is no difference between missing
145 // and empty files, so empty files are considered as errors
146 callback("File is empty.");
147 } else if (xhr.status === 200 || xhr.status === 0) {
149 if (encoding === "binary") {
150 data = cleanDataString(xhr.responseText);
153 data = xhr.responseText;
155 callback(null, data);
158 callback(xhr.responseText || xhr.statusText);
162 xhr.open('GET', path, true);
163 xhr.onreadystatechange = handleResult;
164 if (encoding !== "binary") {
165 xhr.overrideMimeType("text/plain; charset=" + encoding);
167 xhr.overrideMimeType("text/plain; charset=x-user-defined");
175 function read(path, offset, length, callback) {
177 callback(null, cache[path].substring(offset, length + offset));
180 this.readFile(path, "binary", function (err, data) {
184 callback(null, data.substring(offset, length + offset));
187 //xhr.setRequestHeader('Range', 'bytes=' + offset + '-' +
188 // (offset + length - 1));
190 function readFileSync(path, encoding) {
191 var xhr = new XMLHttpRequest(),
193 xhr.open('GET', path, false);
194 if (encoding !== "binary") {
195 xhr.overrideMimeType("text/plain; charset=" + encoding);
197 xhr.overrideMimeType("text/plain; charset=x-user-defined");
201 if (xhr.status === 200 || xhr.status === 0) {
202 result = xhr.responseText;
208 function writeFile(path, data, encoding, callback) {
209 var xhr = new XMLHttpRequest();
210 function handleResult() {
211 if (xhr.readyState === 4) {
212 if (xhr.status === 0 && !xhr.responseText) {
213 // for local files there is no difference between missing
214 // and empty files, so empty files are considered as errors
215 callback("File is empty.");
216 } else if ((xhr.status >= 200 && xhr.status < 300) ||
222 callback("Status " + xhr.status + ": " +
223 xhr.responseText || xhr.statusText);
227 xhr.open('PUT', path, true);
228 xhr.onreadystatechange = handleResult;
229 if (encoding !== "binary") {
230 xhr.overrideMimeType("text/plain; charset=" + encoding);
233 if (encoding === "binary" && xhr.sendAsBinary) {
234 data = cleanDataString(data);
235 xhr.sendAsBinary(data);
243 function deleteFile(path, callback) {
244 var xhr = new XMLHttpRequest();
245 xhr.open('DELETE', path, true);
246 xhr.onreadystatechange = function () {
247 if (xhr.readyState === 4) {
248 if (xhr.status < 200 && xhr.status >= 300) {
249 callback(xhr.responseText);
257 function loadXML(path, callback) {
258 var xhr = new XMLHttpRequest();
259 function handleResult() {
260 if (xhr.readyState === 4) {
261 if (xhr.status === 0 && !xhr.responseText) {
262 callback("File is empty.");
263 } else if (xhr.status === 200 || xhr.status === 0) {
265 callback(xhr.responseXML);
268 callback(xhr.responseText);
272 xhr.open("GET", path, true);
273 xhr.overrideMimeType("text/xml");
274 xhr.onreadystatechange = handleResult;
281 function isFile(path, callback) {
282 this.getFileSize(path, function (size) {
283 callback(size !== -1);
286 function getFileSize(path, callback) {
287 var xhr = new XMLHttpRequest();
288 xhr.open("HEAD", path, true);
289 xhr.onreadystatechange = function () {
290 if (xhr.readyState !== 4) {
293 var cl = xhr.getResponseHeader("Content-Length");
295 callback(parseInt(cl, 10));
302 function wrap(nativeFunction, nargs) {
303 if (!nativeFunction) {
309 // assume the last argument is a callback function
310 var callback = arguments[nargs],
311 args = Array.prototype.slice.call(arguments, 0, nargs),
312 callbackname = "callback" + String(Math.random()).substring(2);
313 window[callbackname] = function () {
314 delete window[callbackname];
315 callback.apply(this, arguments);
317 args.push(callbackname);
318 nativeFunction.apply(this, args);
321 this.readFile = readFile;
322 this.read = read;//wrap(nativeio.read, 3) || read;
323 this.readFileSync = readFileSync;
324 this.writeFile = wrap(nativeio.writeFile, 3) || writeFile;
325 this.deleteFile = wrap(nativeio.deleteFile, 1) || deleteFile;
326 this.loadXML = loadXML;
327 this.isFile = isFile;
328 this.getFileSize = wrap(nativeio.getFileSize, 1) || getFileSize;
330 this.setTimeout = function (f, msec) {
333 this.libraryPaths = function () {
334 return ["../lib", ".", "lib"]; // TODO: find a good solution
335 // probably let html app specify it
337 this.type = function () {
338 return "BrowserRuntime";
340 this.getDOMImplementation = function () {
341 return window.document.implementation;
343 this.exit = function (exitCode) {
345 nativeio.exit(exitCode);
348 this.getWindow = function () {
355 * @implements {Runtime}
357 function NodeJSRuntime() {
358 var fs = require('fs'),
359 currentDirectory = "";
361 function isFile(path, callback) {
362 if (currentDirectory) {
363 path = currentDirectory + "/" + path;
365 fs.stat(path, function (err, stats) {
366 callback(!err && stats.isFile());
369 function loadXML(path, callback) {
370 throw "Not implemented.";
372 this.readFile = function (path, encoding, callback) {
373 if (encoding !== "binary") {
374 fs.readFile(path, encoding, callback);
376 // we have to encode the returned buffer to a string
377 // it would be nice if we would have a blob or buffer object
378 fs.readFile(path, null, function (err, data) {
383 callback(null, data.toString("binary"));
387 this.writeFile = function (path, data, encoding, callback) {
388 fs.writeFile(path, data, encoding, function (err) {
389 callback(err || null);
392 this.deleteFile = fs.unlink;
393 this.read = function (path, offset, length, callback) {
394 if (currentDirectory) {
395 path = currentDirectory + "/" + path;
397 fs.open(path, "r+", 666, function (err, fd) {
402 var buffer = new Buffer(length);
403 fs.read(fd, buffer, 0, length, offset, function (err, bytesRead) {
405 callback(err, buffer.toString("binary", 0, bytesRead));
409 this.readFileSync = fs.readFileSync;
410 this.loadXML = loadXML;
411 this.isFile = isFile;
412 this.getFileSize = function (path, callback) {
413 if (currentDirectory) {
414 path = currentDirectory + "/" + path;
416 fs.stat(path, function (err, stats) {
420 callback(stats.size);
424 this.log = console.log;
425 this.setTimeout = setTimeout;
426 this.libraryPaths = function () {
429 this.setCurrentDirectory = function (dir) {
430 currentDirectory = dir;
432 this.currentDirectory = function () {
433 return currentDirectory;
435 this.type = function () {
436 return "NodeJSRuntime";
438 this.getDOMImplementation = function () {
441 this.exit = process.exit;
442 this.getWindow = function () {
449 * @implements {Runtime}
451 function RhinoRuntime() {
452 var dom = Packages.javax.xml.parsers.DocumentBuilderFactory.newInstance(),
455 currentDirectory = "";
456 dom.setValidating(false);
457 dom.setNamespaceAware(true);
458 dom.setExpandEntityReferences(false);
460 entityresolver = Packages.org.xml.sax.EntityResolver({
461 resolveEntity: function (publicId, systemId) {
462 var file, open = function (path) {
463 var reader = new Packages.java.io.FileReader(path),
464 source = new Packages.org.xml.sax.InputSource(reader);
467 file = /[^\/]*$/.exec(systemId);
471 //dom.setEntityResolver(entityresolver);
472 builder = dom.newDocumentBuilder();
473 builder.setEntityResolver(entityresolver);
475 function loadXML(path, callback) {
476 var file = new Packages.java.io.File(path),
479 document = builder.parse(file);
485 function runtimeReadFile(path, encoding, callback) {
486 var file = new Packages.java.io.File(path),
488 if (!file.isFile()) {
489 callback(path + " is not a file.");
491 if (encoding === "binary") {
492 encoding = "latin1"; // read binary, seems hacky but works
494 data = readFile(path, encoding);
495 callback(null, data);
499 * @param {!string} path
500 * @param {!string} encoding
503 function runtimeReadFileSync(path, encoding) {
504 var file = new Packages.java.io.File(path), data, i;
505 if (!file.isFile()) {
508 if (encoding === "binary") {
509 encoding = "latin1"; // read binary, seems hacky but works
511 return readFile(path, encoding);
513 function isFile(path, callback) {
514 if (currentDirectory) {
515 path = currentDirectory + "/" + path;
517 var file = new Packages.java.io.File(path);
518 callback(file.isFile());
520 this.loadXML = loadXML;
521 this.readFile = runtimeReadFile;
522 this.writeFile = function (path, data, encoding, callback) {
523 if (encoding !== "binary") {
524 throw "Non-binary encoding not implemented.";
526 var out = new Packages.java.io.FileOutputStream(path),
528 for (i = 0; i < l; i += 1) {
529 out.write(data.charCodeAt(i));
534 this.deleteFile = function (path, callback) {
535 var file = new Packages.java.io.File(path);
536 if (file['delete']()) {
539 callback("Could not delete " + path);
542 this.read = function (path, offset, length, callback) {
543 // TODO: adapt to read only a part instead of the whole file
544 if (currentDirectory) {
545 path = currentDirectory + "/" + path;
547 var data = runtimeReadFileSync(path, "binary");
549 callback(null, data.substring(offset, offset + length));
551 callback("Cannot read " + path);
554 this.readFileSync = readFile;
555 this.isFile = isFile;
556 this.getFileSize = function (path, callback) {
557 if (currentDirectory) {
558 path = currentDirectory + "/" + path;
560 var file = new Packages.java.io.File(path);
561 callback(file.length());
564 this.setTimeout = function (f, msec) {
567 this.libraryPaths = function () {
570 this.setCurrentDirectory = function (dir) {
571 currentDirectory = dir;
573 this.currentDirectory = function () {
574 return currentDirectory;
576 this.type = function () {
577 return "RhinoRuntime";
579 this.getDOMImplementation = function () {
580 return builder.getDOMImplementation();
583 this.getWindow = function () {
592 var runtime = (function () {
593 if (typeof(window) !== "undefined") {
594 return new BrowserRuntime(window.document.getElementById("logoutput"));
596 if (typeof(require) !== "undefined") {
597 return new NodeJSRuntime();
599 return new RhinoRuntime();
606 function definePackage(packageNameComponents) {
607 var topname = packageNameComponents[0],
609 // ensure top level package exists
610 pkg = eval("if (typeof " + topname + " === 'undefined') {" +
611 "eval('" + topname + " = {};');}" + topname);
612 for (i = 1; i < packageNameComponents.length - 1; i += 1) {
613 if (!(packageNameComponents[i] in pkg)) {
614 pkg = pkg[packageNameComponents[i]] = {};
620 * @param {string} classpath
621 * @returns {undefined}
623 runtime.loadClass = function (classpath) {
624 if (IS_COMPILED_CODE) {
627 if (classpath in cache) {
630 var names = classpath.split("."),
633 impl = eval(classpath);
635 cache[classpath] = true;
640 function load(classpath) {
641 var code, path, dirs, i;
642 path = classpath.replace(".", "/") + ".js";
643 dirs = runtime.libraryPaths();
644 if (runtime.currentDirectory) {
645 dirs.push(runtime.currentDirectory());
647 for (i = 0; i < dirs.length; i += 1) {
649 code = runtime.readFileSync(dirs[i] + "/" + path, "utf8");
650 if (code && code.length) {
656 if (code === undefined) {
657 throw "Cannot load class " + classpath;
659 definePackage(names);
661 code = eval(classpath + " = eval(code);");
663 runtime.log("Error loading " + classpath + " " + e);
668 // check if the class in context already
669 impl = load(classpath);
670 if (!impl || impl.name !== names[names.length - 1]) {
671 runtime.log("Loaded code is not for " + names[names.length - 1]);
672 throw "Loaded code is not for " + names[names.length - 1];
674 cache[classpath] = true;
678 args = Array.prototype.slice.call(args);
683 var script = argv[0];
684 runtime.readFile(script, "utf8", function (err, code) {
686 paths = runtime.libraryPaths();
687 if (script.indexOf("/") !== -1) {
688 path = script.substring(0, script.indexOf("/"));
690 runtime.setCurrentDirectory(path);
692 var script, path, paths, args, argv, result; // hide variables
693 // execute script and make arguments available via argv
696 runtime.exit(result);
703 // run the script with arguments bound to arguments parameter
704 run.apply(null, argv);
708 // if rhino or node.js, run the scripts provided as arguments
709 if (runtime.type() === "NodeJSRuntime") {
710 run(process.argv.slice(2));
711 } else if (runtime.type() === "RhinoRuntime") {
714 }(typeof arguments !== "undefined" && arguments));