diff --git a/README.md b/README.md index 69b8db1..b822d5e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,45 @@ -# Java to Javascript for QuintOS +# Java to Javascript -I've built on top of the [java-to-javascript](https://github.com/wyattades/java-to-javascript) transpiler by @wyattades and got inspiration from the JDK (Java Development Kit) implementation in [java2javascript](https://github.com/java2script/java2script) by @BobHanson, @zhourenjian, @abego, and others. +I've built on top of the [java-to-javascript](https://github.com/wyattades/java-to-javascript) transpiler by @wyattades and got inspiration from the barebones JDK (Java Development Kit) implementation in [java2javascript](https://github.com/java2script/java2script) by @BobHanson, @zhourenjian, @abego, and others. -My purpose in creating this project is to allow intro level CS students to write Java code but still use my [QuintOS](https://github.com/quinton-ashley/quintos) retro game engine library which is web based. Yes, I went to all this trouble just so some teenagers don't have to run their programs in a boring Java console! I made a barebones JDK implementation in modern Javascript to acheive this. +I created this project so intro level computer science students could write Java code but still use my [QuintOS](https://github.com/quinton-ashley/quintos) retro game engine library, which is web based. Yes, I went to all this trouble just so my students don't have to run their programs in a boring Java console! To achieve this I've been working on a JDK implementation in modern Javascript. + +## Java Classes Included in the JS JDK + +| java.io | | | +| ------- | ----------- | ----------- | +| File | InputStream | PrintStream | + +| java.lang | | | +| --------- | ------------- | --------- | +| Boolean | Byte | Character | +| Double | Exception | Float | +| Integer | Long | Short | +| String | StringBuilder | System | +| Thread | Throwable | | + +| java.security | | | +| ------------- | --- | --- | +| MessageDigest | | | + +| java.time | | | +| --------- | --- | --- | +| Instant | | | + +| java.util | | | +| ------------------ | ---------------------- | ----------- | +| AbstractCollection | AbstractList | AbstractMap | +| AbstractSet | ArrayList | Arrays | +| Collections | Formatter | HashMap | +| HashSet | IllegalFormatException | Itr | +| LinkedList | Random | Scanner | +| Set | Stack | | ## API ### jdk.init(root) -- root is the local path or url to the parent folder of the jdk, by default it links to 'https://unpkg.com/java2js' (this package) so you don't need to change it +- root (optional) can be a local path or url to the parent folder of the JS `jdk` folder, by default it links to 'https://unpkg.com/java2js' (this package) so you don't need to change it This function imports much of the standard Java lang classes into the global scope. You must use it before translating or running files. @@ -26,16 +57,22 @@ Loads the JS class file but doesn't run the main method. ### jdk.run(jvmArgs) -Runs the main method with the given JVM arguments. +Runs the main method with optional JVM arguments. ## Known limitations -- casting to int truncation workaround requires parenthesis around the number being cast +- casting to int truncation workaround requires parenthesis around the number being cast `int x = (int) (Math.random() * 100);` - no support for method overloading, though a workaround might be possible by making a function with the og name route to each of the variations of the overloaded function - no support for private/public methods yet, though this could be done since they are included in modern JavaScript classes +- no three dimensional arrays + +- no third level static classes + +- not much error checking + ## Contribute -I've only done a barebones implementation of Java 17 JDK, a lot is missing, so if you are interested in adding more please go for it and submit a pull request! +I've only done a partial implementation of the Java 17 JDK in JavaScript, so if you're interested in adding more please go for it and submit a pull request! diff --git a/jdk.js b/jdk.js index 4c6a8cc..06927d5 100755 --- a/jdk.js +++ b/jdk.js @@ -80,7 +80,10 @@ lang.push('java.lang.' + name); } await this.import(lang); - } catch (e) {} + } catch (ror) { + console.error(ror); + return; + } // make java.lang classes global for (let name in this.java.lang) { @@ -130,40 +133,32 @@ for (let className of classNames) { // log('importing ' + className); let imp = this.imports[className]; - if (imp) { - // class is ready for use - if (imp.ready) ready++; - // class file loaded but not ready yet - continue; + if (!imp) { + imp = this.imports[className] = {}; + imp.load = null; + imp.classPath = className.split('.'); + + let src = this.root + '/jdk'; + for (let part of imp.classPath) { + src += '/' + part; + } + src += '.js'; + + const loadJS = () => { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + document.body.appendChild(script); + script.onload = resolve; + script.onerror = reject; + script.async = true; + script.src = src; + }); + }; + await loadJS(); } - imp = this.imports[className] = {}; - imp.load = null; - imp.classPath = className.split('.'); - - let src = this.root + '/jdk'; - for (let part of imp.classPath) { - src += '/' + part; - } - src += '.js'; - - const loadJS = () => { - return new Promise((resolve, reject) => { - const script = document.createElement('script'); - document.body.appendChild(script); - script.onload = resolve; - script.onerror = reject; - script.async = true; - script.src = src; - }); - }; - - await loadJS(); - - await imp.load(); - + if (!imp.ready) await imp.load(); classes.push(this.getClass(imp.classPath)); } - if (classNames.length == 1) classes = classes[0]; return classes; } @@ -186,13 +181,17 @@ return '(' + p1.replace(/( |\t){0,3}(.*)(\r*\n|$)/gm, '"$2\\n"+').slice(0, -1) + ')'; }); + let arrToString = ''; + // hacky support for Array literals file = file.replace(/new\s*\w*\s*\[\s*\]\s*\{([^}]*)}/gm, 'Array.of($1)'); + // 2D arrays file = file.replace(/new\s*\w*(\s*\[\s*\])*\s*\{([^}]*)\}/gm, (match, p1, p2) => { return 'Array.of(' + p2.replace(/\{([^\}]*)\}/gm, 'Array.of($1)') + ')'; }); + // Array with defined length file = file.replace(/new\s*\w*\s*\[\s*(\d)+\s*\]\s*/gm, 'new Array($1)'); // workaround hack for converting lambda expressions to Runnables @@ -19210,13 +19209,21 @@ case 'ArrayCreation': return `new Array()`; case 'ArrayAccess': + let arr = expr.array; // TODO support for three dimensional arrays - if (expr.array.array) { - return `${assignParent(expr.array.array.identifier)}[${parseExpr(expr.array.index)}][${parseExpr( - expr.index - )}]`; + if (arr.array) { + return `${assignParent(arr.array.identifier)}[${parseExpr(arr.index)}][${parseExpr(expr.index)}]`; } - return `${assignParent(expr.array.identifier)}[${parseExpr(expr.index)}]`; + if (arr.identifier) { + return `${assignParent(arr.identifier)}[${parseExpr(expr.index)}]`; + } + // TODO support for third level subclasses + if (arr.qualifier.qualifier) { + return `${assignParent(arr.qualifier.qualifier.identifier)}.${arr.qualifier.name.identifier}.${ + arr.name.identifier + }[${parseExpr(expr.index)}]`; + } + return `${assignParent(arr.qualifier.identifier)}.${arr.name.identifier}[${parseExpr(expr.index)}]`; case 'ParenthesizedExpression': return `(${parseExpr(expr.expression)})`; default: @@ -19372,13 +19379,17 @@ return classData; }; - const classToJs = ({ name: className, vars, superclass, methods, abstract }) => { + const classToJs = ({ name: className, vars, superclass, methods, abstract, classes }) => { if (abstract) return ''; const initVars = []; const classProps = []; const staticVars = []; + for (const cl of classes) { + staticVars.push(`${classToJs(cl)}\n${className}.${cl.name}=${cl.name};`); + } + for (const var_ of vars) { if (var_.value === undefined) var_.value = literalInitializers[var_.type] || 'null'; if (var_.static) { @@ -19404,7 +19415,7 @@ ? initVars.join('') + (block ? opts.separator : '') : ''; let method = `${name}(${parameters}){${preblock}${block}}`; - if (typeof QuintOS != 'undefined' && /(System|alert|prompt|eraseRect)/gm.test(block)) + if (typeof QuintOS != 'undefined' && /(alert|prompt|erase|eraseRect|text|textRect|frame)/gm.test(block)) method = 'async ' + method; classProps.push(method); } diff --git a/jdk/java/io/PrintStream.js b/jdk/java/io/PrintStream.js index 5eb4724..9e5803e 100755 --- a/jdk/java/io/PrintStream.js +++ b/jdk/java/io/PrintStream.js @@ -21,7 +21,7 @@ jdk.imports['java.io.PrintStream'].load = async () => { ); let md = MessageDigest.getInstance(); let str = '[Ljava.lang.'; - let hash = md.digest(arr.toString()).slice(0, 8); + let hash = md.digest(arr.join(',')).slice(0, 8); if (arr.length == 0) { return str + 'Array;@' + hash; } @@ -36,6 +36,8 @@ jdk.imports['java.io.PrintStream'].load = async () => { let str; if (Array.isArray(arg)) { str = this._printArray(arg); + } else if (typeof arg == 'undefined' || arg == null) { + str = 'null'; } else { str = arg.toString(); } @@ -57,7 +59,7 @@ jdk.imports['java.io.PrintStream'].load = async () => { } printf(format, ...args) { - let str = String.format(format, args); + let str = String.format(format, ...args); return this._print(str); } } diff --git a/jdk/java/util/AbstractCollection.js b/jdk/java/util/AbstractCollection.js new file mode 100644 index 0000000..1e445be --- /dev/null +++ b/jdk/java/util/AbstractCollection.js @@ -0,0 +1,71 @@ +jdk.imports['java.util.AbstractCollection'].load = async () => { + const Itr = await jdk.import('java.util.Itr'); + + class AbstractCollection { + constructor(co) { + this.arr = co || []; + } + + add(idx, elem) { + if (typeof elem == 'undefined') { + return this.arr.push(idx); + } else { + return this.arr.splice(idx, 0, elem); + } + } + + addAll(idx, vals) { + this.arr.splice(idx, 0, ...vals); + } + + clear() { + this.arr = []; + } + + contains(val) { + return this.arr.indexOf(val) != -1; + } + + // containsAll(elems) {} + + isEmpty() { + return this.arr.length == 0; + } + + iterator() { + return new Itr(this); + } + + // parallelStream() {} + + remove(idxOrElem) { + let index = idxOrElem; + if (typeof idxOrElem != 'number') { + index = this.arr.indexOf(idxOrElem); + } + return this.arr.splice(index, 1); + } + + removeAll() { + this.arr = []; + return true; + } + + // removeIf() {} + + // retainAll(collection) {} + + size() { + return this.arr.length; + } + + toArray() { + return this.arr; + } + + toString() { + return '[' + this.arr.join(', ') + ']'; + } + } + jdk.java.util.AbstractCollection = AbstractCollection; +}; diff --git a/jdk/java/util/AbstractList.js b/jdk/java/util/AbstractList.js index 1ed8a9b..12077ad 100755 --- a/jdk/java/util/AbstractList.js +++ b/jdk/java/util/AbstractList.js @@ -1,87 +1,32 @@ jdk.imports['java.util.AbstractList'].load = async () => { - const Itr = await jdk.import('java.util.Itr'); + let AbstractCollection = await jdk.import('java.util.AbstractCollection'); - class AbstractList { - constructor(arr) { - // TODO - this.arr = arr || []; + class AbstractList extends AbstractCollection { + constructor(...args) { + super(...args); } - addAll(index, vals) { - this.arr.splice(index, 0, ...vals); + // equals(o) {} + + get(idx) { + return this.arr[idx]; } - clear() { - this.arr = []; + indexOf(elem) { + return this.arr.indexOf(elem); + } + + lastIndexOf(elem) { + return this.arr.lastIndexOf(elem); } poll() { return this.arr.shift(); } - remove(indexOrElem) { - let index = indexOrElem; - if (typeof indexOrElem != 'number') { - index = this.arr.indexOf(indexOrElem); - } - return this.arr.splice(index, 1); - } - - removeAll() { - this.arr = []; - return true; - } - - toArray() { - return this.arr; - } - - toString() { - return '[' + this.arr.toString() + ']'; - } - - size() { - return this.arr.length; - } - - add(index, elem) { - if (typeof elem == 'undefined') { - return this.arr.push(index); - } else { - return this.arr.splice(index, 0, elem); - } - } - - get(index) { - return this.arr[index]; - } - - contains(val) { - return this.arr.indexOf(val) != -1; - } - - // containsAll(elems) { - // } - - isEmpty() { - return this.arr.length == 0; - } - - set(index, element) { - this.arr[index] = element; - return element; - } - - indexOf(element) { - return this.arr.indexOf(element); - } - - lastIndexOf(element) { - return this.arr.lastIndexOf(element); - } - - iterator() { - return new Itr(this); + set(idx, elem) { + this.arr[idx] = elem; + return elem; } } jdk.java.util.AbstractList = AbstractList; diff --git a/jdk/java/util/AbstractMap.js b/jdk/java/util/AbstractMap.js new file mode 100644 index 0000000..b225769 --- /dev/null +++ b/jdk/java/util/AbstractMap.js @@ -0,0 +1,80 @@ +jdk.imports['java.util.AbstractMap'].load = async () => { + let AbstractCollection = await jdk.import('java.util.AbstractCollection'); + let Set = await jdk.import('java.util.Set'); + + class AbstractMap { + constructor(co) { + this.map = co || {}; + class SimpleEntry { + constructor(key, val) { + this.key = key; + this.val = val; + } + + getKey() { + return this.key; + } + + getValue() { + return this.val; + } + + toString() { + return this.key + '=' + this.val; + } + } + this.SimpleEntry = SimpleEntry; + } + + clear() { + this.map = {}; + } + + containsKey(key) { + return this.map.hasOwnProperty(key); + } + + containsValue(val) { + return Object.values(this.map).find((x) => x == val) ? true : false; + } + + get(key) { + return this.map[key] || null; + } + + isEmpty() { + return Object.keys(this.map).length == 0; + } + + entrySet() { + let set = new Set(); + for (let key in this.map) { + set.add(new this.SimpleEntry(key, this.map[key])); + } + return set; + } + + put(key, value) { + const previous_val = this.map[key]; + this.map[key] = value; + return previous_val || null; + } + + remove(key) { + if (!this.map[key]) return null; + const val = this.map[key]; + delete this.map[key]; + return val; + } + + values() { + const co = new AbstractCollection(Object.values(this.map)); + return co; + } + + size() { + return Object.keys(this.map).length; + } + } + jdk.java.util.AbstractMap = AbstractMap; +}; diff --git a/jdk/java/util/AbstractSet.js b/jdk/java/util/AbstractSet.js new file mode 100644 index 0000000..b7e618e --- /dev/null +++ b/jdk/java/util/AbstractSet.js @@ -0,0 +1,17 @@ +jdk.imports['java.util.AbstractSet'].load = async () => { + let AbstractCollection = await jdk.import('java.util.AbstractCollection'); + + class AbstractSet extends AbstractCollection { + constructor(...args) { + super(...args); + } + + add(elem) { + if (this.arr.includes(elem)) return; + super.add(elem); + } + + // equals(o) {} + } + jdk.java.util.AbstractSet = AbstractSet; +}; diff --git a/jdk/java/util/ArrayList.js b/jdk/java/util/ArrayList.js index ef39cf8..78fd312 100755 --- a/jdk/java/util/ArrayList.js +++ b/jdk/java/util/ArrayList.js @@ -3,7 +3,7 @@ jdk.imports['java.util.ArrayList'].load = async () => { class ArrayList extends AbstractList { constructor(...args) { - super(args); + super(...args); } } jdk.java.util.ArrayList = ArrayList; diff --git a/jdk/java/util/HashMap.js b/jdk/java/util/HashMap.js index b042b46..cebf003 100755 --- a/jdk/java/util/HashMap.js +++ b/jdk/java/util/HashMap.js @@ -1,59 +1,9 @@ jdk.imports['java.util.HashMap'].load = async () => { - class HashMap { - constructor() { - this.content = {}; - } + let AbstractMap = await jdk.import('java.util.AbstractMap'); - get(key) { - return this.content[key]; - } - - put(key, value) { - const previous_val = this.content[key]; - this.content[key] = value; - return previous_val; - } - - containsKey(key) { - return this.content.hasOwnProperty(key); - } - - remove(key) { - const tmp = this.content[key]; - delete this.content[key]; - return tmp; - } - - keySet() { - const result = new HashSet(); - for (const p in this.content) { - if (this.content.hasOwnProperty(p)) { - result.add(p); - } - } - return result; - } - - isEmpty() { - return Object.keys(this.content).length == 0; - } - - values() { - const result = new HashSet(); - for (const p in this.content) { - if (this.content.hasOwnProperty(p)) { - result.add(this.content[p]); - } - } - return result; - } - - clear() { - this.content = {}; - } - - size() { - return Object.keys(this.content).length; + class HashMap extends AbstractMap { + constructor(...args) { + super(...args); } } jdk.java.util.HashMap = HashMap; diff --git a/jdk/java/util/HashSet.js b/jdk/java/util/HashSet.js index 90870d5..1631dd8 100755 --- a/jdk/java/util/HashSet.js +++ b/jdk/java/util/HashSet.js @@ -1,72 +1,12 @@ jdk.imports['java.util.HashSet'].load = async () => { - class HashSet { - constructor() { - this.content = {}; + let AbstractSet = await jdk.import('java.util.AbstractSet'); + + class HashSet extends AbstractSet { + constructor(...args) { + super(...args); } - add(val) { - this.content[val] = val; - } - - clear() { - this.content = {}; - } - - contains(val) { - return this.content.hasOwnProperty(val); - } - - containsAll(elems) { - return false; - } - - addAll(vals) { - const tempArray = vals.toArray(null); - for (let i = 0; i < tempArray.length; i++) { - this.content[tempArray[i]] = tempArray[i]; - } - return true; - } - - remove(val) { - let b = false; - if (this.content[val]) { - b = true; - } - delete this.content[val]; - return b; - } - - removeAll() { - return false; - } - - size() { - return Object.keys(this.content).length; - } - - isEmpty() { - return this.size() == 0; - } - - toArray(a) { - const _this = this; - return Object.keys(this.content).map((key) => _this.content[key]); - } - - iterator() { - return new java.util.Itr(this); - } - - forEach(f) { - for (const p in this.content) { - f(this.content[p]); - } - } - - get(index) { - return this.content[index]; - } + // equals(o) {} } jdk.java.util.HashSet = HashSet; }; diff --git a/jdk/java/util/LinkedList.js b/jdk/java/util/LinkedList.js index fa414f2..99cbb8c 100755 --- a/jdk/java/util/LinkedList.js +++ b/jdk/java/util/LinkedList.js @@ -3,7 +3,7 @@ jdk.imports['java.util.LinkedList'].load = async () => { class LinkedList extends AbstractList { constructor(...args) { - super(args); + super(...args); } } jdk.java.util.LinkedList = LinkedList; diff --git a/jdk/java/util/Set.js b/jdk/java/util/Set.js new file mode 100644 index 0000000..0cf7ae3 --- /dev/null +++ b/jdk/java/util/Set.js @@ -0,0 +1,12 @@ +jdk.imports['java.util.Set'].load = async () => { + let AbstractSet = await jdk.import('java.util.AbstractSet'); + + class Set extends AbstractSet { + constructor(...args) { + super(...args); + } + + // equals(o) {} + } + jdk.java.util.Set = Set; +}; diff --git a/package.json b/package.json index 8efa787..912a4bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "java2js", - "version": "1.1.3", + "version": "1.1.4", "description": "Converts Java to JavaScript with support for p5.js and QuintOS.", "main": "jdk.js", "scripts": {