Add simple android client.
[odfkit:webodf.git] / android / assets / lib / runtime.js
1 /*jslint nomen: false, evil: true*/
2 /*global window XMLHttpRequest require console process __dirname setTimeout Packages print readFile quit Buffer*/
3
4 /**
5  * Three implementations of a runtime for browser, node.js and rhino.
6  */
7
8 /**
9  * Abstraction of the runtime environment.
10  * @interface
11  */
12 function Runtime() {}
13 /**
14  * @param {!string} path
15  * @param {!number} offset
16  * @param {!number} length
17  * @param {!function(string=,string=):undefined} callback
18  * @return {undefined}
19  */
20 Runtime.prototype.read = function (path, offset, length, callback) {};
21 /**
22  * @param {!string} path
23  * @param {!string} encoding text encoding or 'binary'
24  * @param {!function(string=,string=):undefined} callback
25  * @return {undefined}
26  */
27 Runtime.prototype.readFile = function (path, encoding, callback) {};
28 /**
29  * @param {!string} path
30  * @param {!string} encoding text encoding or 'binary'
31  * @return {!string}
32  */
33 Runtime.prototype.readFileSync = function (path, encoding) {};
34 /**
35  * @param {!string} path
36  * @param {!function((string|Document)):undefined} callback
37  * @return {undefined}
38  */
39 Runtime.prototype.loadXML = function (path, callback) {};
40 /**
41  * @param {!string} path
42  * @param {!string} data
43  * @param {!string} encoding text encoding or "binary"
44  * @param {!function(?string):undefined} callback
45  * @return {undefined}
46  */
47 Runtime.prototype.writeFile = function (path, data, encoding, callback) {};
48 /**
49  * @param {!string} path
50  * @param {!function(boolean):undefined} callback
51  * @return {undefined}
52  */
53 Runtime.prototype.isFile = function (path, callback) {};
54 /**
55  * @param {!string} path
56  * @param {!function(number):undefined} callback
57  * @return {undefined}
58  */
59 Runtime.prototype.getFileSize = function (path, callback) {};
60 /**
61  * @param {!string} path
62  * @param {!function(?string):undefined} callback
63  * @return {undefined}
64  */
65 Runtime.prototype.deleteFile = function (path, callback) {};
66 /**
67  * @param {!string} msgOrCategory
68  * @param {!string=} msg
69  * @return {undefined}
70  */
71 Runtime.prototype.log = function (msgOrCategory, msg) {};
72 /**
73  * @param {!function():undefined} callback
74  * @param {!number} milliseconds
75  * @return {undefined}
76  */
77 Runtime.prototype.setTimeout = function (callback, milliseconds) {};
78 /**
79  * @return {!Array.<string>}
80  */
81 Runtime.prototype.libraryPaths = function () {};
82 /**
83  * @return {string}
84  */
85 Runtime.prototype.type = function () {};
86 /**
87  * @return {?DOMImplementation}
88  */
89 Runtime.prototype.getDOMImplementation = function () {};
90 /**
91  * @return {?Window}
92  */
93 Runtime.prototype.getWindow = function () {};
94
95 /** @define {boolean} */
96 var IS_COMPILED_CODE = false;
97
98 /**
99  * @constructor
100  * @implements {Runtime}
101  * @param {Element} logoutput
102  */
103 function BrowserRuntime(logoutput) {
104     var cache = {},
105         nativeio = window.nativeio || {};
106     function log(msgOrCategory, msg) {
107         var node, doc, category;
108         if (msg) {
109             category = msgOrCategory;
110         } else {
111             msg = msgOrCategory;
112         }
113         if (logoutput) {
114             doc = logoutput.ownerDocument;
115             if (category) {
116                 node = doc.createElement("span");
117                 node.className = category;
118                 node.appendChild(doc.createTextNode(category));
119                 logoutput.appendChild(node);
120                 logoutput.appendChild(doc.createTextNode(" "));
121             }
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) {
127             console.log(msg);
128         }
129     }
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);
135         }
136         return str;
137     }
138     function readFile(path, encoding, callback) {
139         var xhr = new XMLHttpRequest();
140         function handleResult() {
141             var data;
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) {
148                     // report file
149                     if (encoding === "binary") {
150                         data = cleanDataString(xhr.responseText);
151                         cache[path] = data;
152                     } else {
153                         data = xhr.responseText;
154                     }
155                     callback(null, data);
156                 } else {
157                     // report error
158                     callback(xhr.responseText || xhr.statusText);
159                 }
160             }
161         }
162         xhr.open('GET', path, true);
163         xhr.onreadystatechange = handleResult;
164         if (encoding !== "binary") {
165             xhr.overrideMimeType("text/plain; charset=" + encoding);
166         } else {
167             xhr.overrideMimeType("text/plain; charset=x-user-defined");
168         }
169         try {
170             xhr.send(null);
171         } catch (e) {
172             callback(e.message);
173         }
174     }
175     function read(path, offset, length, callback) {
176         if (path in cache) {
177             callback(null, cache[path].substring(offset, length + offset));
178             return;
179         }
180         this.readFile(path, "binary", function (err, data) {
181             if (err) {
182                 callback(err);
183             } else {
184                 callback(null, data.substring(offset, length + offset));
185             }
186         });
187         //xhr.setRequestHeader('Range', 'bytes=' + offset + '-' +
188         //       (offset + length - 1));
189     }
190     function readFileSync(path, encoding) {
191         var xhr = new XMLHttpRequest(),
192             result;
193         xhr.open('GET', path, false);
194         if (encoding !== "binary") {
195             xhr.overrideMimeType("text/plain; charset=" + encoding);
196         } else {
197             xhr.overrideMimeType("text/plain; charset=x-user-defined");
198         }
199         try {
200             xhr.send(null);
201             if (xhr.status === 200 || xhr.status === 0) {
202                 result = xhr.responseText;
203             }
204         } catch (e) {
205         }
206         return result;
207     }
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) ||
217                            xhr.status === 0) {
218                     // report success
219                     callback(null);
220                 } else {
221                     // report error
222                     callback("Status " + xhr.status + ": " +
223                             xhr.responseText || xhr.statusText);
224                 }
225             }
226         }
227         xhr.open('PUT', path, true);
228         xhr.onreadystatechange = handleResult;
229         if (encoding !== "binary") {
230             xhr.overrideMimeType("text/plain; charset=" + encoding);
231         }
232         try {
233             if (encoding === "binary" && xhr.sendAsBinary) {
234                 data = cleanDataString(data);
235                 xhr.sendAsBinary(data);
236             } else {
237                 xhr.send(data);
238             }
239         } catch (e) {
240             callback(e.message);
241         }
242     }
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);
250                 } else {
251                     callback(null);
252                 }
253             }
254         };
255         xhr.send(null);
256     }
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) {
264                     // report file
265                     callback(xhr.responseXML);
266                 } else {
267                     // report error
268                     callback(xhr.responseText);
269                 }
270             }
271         }
272         xhr.open("GET", path, true);
273         xhr.overrideMimeType("text/xml");
274         xhr.onreadystatechange = handleResult;
275         try {
276             xhr.send(null);
277         } catch (e) {
278             callback(e.message);
279         }
280     }
281     function isFile(path, callback) {
282         this.getFileSize(path, function (size) {
283             callback(size !== -1);
284         });
285     }
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) {
291                 return;
292             }
293             var cl = xhr.getResponseHeader("Content-Length");
294             if (cl) {
295                 callback(parseInt(cl, 10));
296             } else { 
297                 callback(-1);
298             }
299         };
300         xhr.send(null);
301     }
302     function wrap(nativeFunction, nargs) {
303         if (!nativeFunction) {
304             return null;
305         }
306         return function () {
307             // clear cache
308             cache = {};
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);
316             };
317             args.push(callbackname);
318             nativeFunction.apply(this, args);
319         };
320     }
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;
329     this.log = log;
330     this.setTimeout = function (f, msec) {
331         setTimeout(f, msec);
332     };
333     this.libraryPaths = function () {
334         return ["../lib", ".", "lib"]; // TODO: find a good solution
335                                        // probably let html app specify it
336     };
337     this.type = function () {
338         return "BrowserRuntime";
339     };
340     this.getDOMImplementation = function () {
341         return window.document.implementation;
342     };
343     this.exit = function (exitCode) {
344         if (nativeio.exit) {
345             nativeio.exit(exitCode);
346         }
347     };
348     this.getWindow = function () {
349         return window;
350     };
351 }
352
353 /**
354  * @constructor
355  * @implements {Runtime}
356  */
357 function NodeJSRuntime() {
358     var fs = require('fs'),
359         currentDirectory = "";
360
361     function isFile(path, callback) {
362         if (currentDirectory) {
363             path = currentDirectory + "/" + path;
364         }
365         fs.stat(path, function (err, stats) {
366             callback(!err && stats.isFile());
367         });
368     }
369     function loadXML(path, callback) {
370         throw "Not implemented.";
371     }
372     this.readFile = function (path, encoding, callback) {
373         if (encoding !== "binary") {
374             fs.readFile(path, encoding, callback);
375         } else {
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) {
379                 if (err) {
380                     callback(err);
381                     return;
382                 }
383                 callback(null, data.toString("binary"));
384             });
385         }
386     };
387     this.writeFile = function (path, data, encoding, callback) {
388         fs.writeFile(path, data, encoding, function (err) {
389             callback(err || null);
390         });
391     };
392     this.deleteFile = fs.unlink;
393     this.read = function (path, offset, length, callback) {
394         if (currentDirectory) {
395             path = currentDirectory + "/" + path;
396         }
397         fs.open(path, "r+", 666, function (err, fd) {
398             if (err) {
399                 callback(err);
400                 return;
401             }
402             var buffer = new Buffer(length);
403             fs.read(fd, buffer, 0, length, offset, function (err, bytesRead) {
404                 fs.close(fd);
405                 callback(err, buffer.toString("binary", 0, bytesRead));
406             });
407         });
408     };
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;
415         }
416         fs.stat(path, function (err, stats) {
417             if (err) {
418                 callback(-1);
419             } else {
420                 callback(stats.size);
421             }
422         });
423     };
424     this.log = console.log;
425     this.setTimeout = setTimeout;
426     this.libraryPaths = function () {
427         return [__dirname];
428     };
429     this.setCurrentDirectory = function (dir) {
430         currentDirectory = dir;
431     };
432     this.currentDirectory = function () {
433         return currentDirectory;
434     };
435     this.type = function () {
436         return "NodeJSRuntime";
437     };
438     this.getDOMImplementation = function () {
439         return;
440     };
441     this.exit = process.exit;
442     this.getWindow = function () {
443         return null;
444     };
445 }
446
447 /**
448  * @constructor
449  * @implements {Runtime}
450  */
451 function RhinoRuntime() {
452     var dom = Packages.javax.xml.parsers.DocumentBuilderFactory.newInstance(),
453         builder,
454         entityresolver,
455         currentDirectory = "";
456     dom.setValidating(false);
457     dom.setNamespaceAware(true);
458     dom.setExpandEntityReferences(false);
459     dom.setSchema(null);
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);
465                 return source;
466             };
467             file = /[^\/]*$/.exec(systemId);
468             return open(file);
469         }
470     });
471     //dom.setEntityResolver(entityresolver);
472     builder = dom.newDocumentBuilder();
473     builder.setEntityResolver(entityresolver);
474
475     function loadXML(path, callback) {
476         var file = new Packages.java.io.File(path),
477             document;
478         try {
479             document = builder.parse(file);
480         } catch (err) {
481             print(err);
482         }
483         callback(document);
484     }
485     function runtimeReadFile(path, encoding, callback) {
486         var file = new Packages.java.io.File(path),
487             data;
488         if (!file.isFile()) {
489             callback(path + " is not a file.");
490         } else {
491             if (encoding === "binary") {
492                 encoding = "latin1"; // read binary, seems hacky but works
493             }
494             data = readFile(path, encoding);
495             callback(null, data);
496         }
497     }
498     /**
499      * @param {!string} path
500      * @param {!string} encoding
501      * @return {?string}
502      */
503     function runtimeReadFileSync(path, encoding) {
504         var file = new Packages.java.io.File(path), data, i;
505         if (!file.isFile()) {
506             return null;
507         }
508         if (encoding === "binary") {
509             encoding = "latin1"; // read binary, seems hacky but works
510         }
511         return readFile(path, encoding);
512     }
513     function isFile(path, callback) {
514         if (currentDirectory) {
515             path = currentDirectory + "/" + path;
516         }
517         var file = new Packages.java.io.File(path);
518         callback(file.isFile());
519     }
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.";
525         }
526         var out = new Packages.java.io.FileOutputStream(path),
527             i, l = data.length;
528         for (i = 0; i < l; i += 1) {
529             out.write(data.charCodeAt(i));
530         }
531         out.close();
532         callback(null);
533     };
534     this.deleteFile = function (path, callback) {
535         var file = new Packages.java.io.File(path);
536         if (file['delete']()) {
537             callback(null);
538         } else {
539             callback("Could not delete " + path);
540         }
541     };
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;
546         }
547         var data = runtimeReadFileSync(path, "binary");
548         if (data) {
549             callback(null, data.substring(offset, offset + length));
550         } else {
551             callback("Cannot read " + path);
552         }
553     };
554     this.readFileSync = readFile;
555     this.isFile = isFile; 
556     this.getFileSize = function (path, callback) {
557         if (currentDirectory) {
558             path = currentDirectory + "/" + path;
559         }
560         var file = new Packages.java.io.File(path);
561         callback(file.length());
562     };
563     this.log = print;
564     this.setTimeout = function (f, msec) {
565         f();
566     };
567     this.libraryPaths = function () {
568         return ["lib"];
569     };
570     this.setCurrentDirectory = function (dir) {
571         currentDirectory = dir;
572     };
573     this.currentDirectory = function () {
574         return currentDirectory;
575     };
576     this.type = function () {
577         return "RhinoRuntime";
578     };
579     this.getDOMImplementation = function () {
580         return builder.getDOMImplementation();
581     };
582     this.exit = quit;
583     this.getWindow = function () {
584         return null;
585     };
586 }
587
588 /**
589  * @const
590  * @type {Runtime}
591  */
592 var runtime = (function () {
593     if (typeof(window) !== "undefined") {
594         return new BrowserRuntime(window.document.getElementById("logoutput"));
595     } else {
596         if (typeof(require) !== "undefined") {
597             return new NodeJSRuntime();
598         } else {
599             return new RhinoRuntime();
600         }
601     }
602 }());
603
604 (function () {
605     var cache = {};
606     function definePackage(packageNameComponents) {
607         var topname = packageNameComponents[0],
608             i, pkg;
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]] = {};
615             }
616         }
617         return pkg;
618     }
619     /**
620      * @param {string} classpath
621      * @returns {undefined}
622      */
623     runtime.loadClass = function (classpath) {
624         if (IS_COMPILED_CODE) {
625             return;
626         }
627         if (classpath in cache) {
628             return;
629         }
630         var names = classpath.split("."),
631             impl;
632         try {
633             impl = eval(classpath);
634             if (impl) {
635                 cache[classpath] = true;
636                 return;
637             }
638         } catch (e) {
639         }
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());
646             }
647             for (i = 0; i < dirs.length; i += 1) {
648                 try {
649                     code = runtime.readFileSync(dirs[i] + "/" + path, "utf8");
650                     if (code && code.length) {
651                         break;
652                     }
653                 } catch (ex) {
654                 }
655             }
656             if (code === undefined) {
657                 throw "Cannot load class " + classpath;
658             }
659             definePackage(names);
660             try {
661                 code = eval(classpath + " = eval(code);");
662             } catch (e) {
663                 runtime.log("Error loading " + classpath + " " + e);
664                 throw e;
665             }
666             return code;
667         }
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];
673         }
674         cache[classpath] = true;
675     };
676 }());
677 (function (args) {
678     args = Array.prototype.slice.call(args);
679     function run(argv) {
680         if (!argv.length) {
681             return;
682         }
683         var script = argv[0];
684         runtime.readFile(script, "utf8", function (err, code) {
685             var path = "",
686                 paths = runtime.libraryPaths();
687             if (script.indexOf("/") !== -1) {
688                 path = script.substring(0, script.indexOf("/"));
689             }
690             runtime.setCurrentDirectory(path);
691             function run() {
692                 var script, path, paths, args, argv, result; // hide variables
693                 // execute script and make arguments available via argv
694                 result = eval(code);
695                 if (result) {
696                     runtime.exit(result);
697                 }
698                 return;
699             }
700             if (err) {
701                 runtime.log(err);
702             } else {
703                 // run the script with arguments bound to arguments parameter
704                 run.apply(null, argv);
705             }
706         });
707     }
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") {
712         run(args);
713     }
714 }(typeof arguments !== "undefined" && arguments));