jaroslav@323: /** jaroslav@323: * Back 2 Browser Bytecode Translator jaroslav@323: * Copyright (C) 2012 Jaroslav Tulach jaroslav@323: * jaroslav@323: * This program is free software: you can redistribute it and/or modify jaroslav@323: * it under the terms of the GNU General Public License as published by jaroslav@323: * the Free Software Foundation, version 2 of the License. jaroslav@323: * jaroslav@323: * This program is distributed in the hope that it will be useful, jaroslav@323: * but WITHOUT ANY WARRANTY; without even the implied warranty of jaroslav@323: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the jaroslav@323: * GNU General Public License for more details. jaroslav@323: * jaroslav@323: * You should have received a copy of the GNU General Public License jaroslav@323: * along with this program. Look for COPYING file in the top folder. jaroslav@323: * If not, see http://opensource.org/licenses/GPL-2.0. jaroslav@323: */ jaroslav@1041: package org.apidesign.bck2brwsr.launcher.fximpl; jaroslav@323: jaroslav@360: import java.io.IOException; jaroslav@360: import java.io.InputStream; jaroslav@802: import java.io.UnsupportedEncodingException; jaroslav@332: import java.lang.reflect.InvocationTargetException; jaroslav@323: import java.lang.reflect.Method; jaroslav@412: import java.lang.reflect.Modifier; jaroslav@332: import java.net.URL; jaroslav@360: import java.util.Enumeration; jaroslav@1041: import javafx.scene.web.WebEngine; jaroslav@1041: import netscape.javascript.JSObject; jaroslav@323: jaroslav@323: /** jaroslav@323: * jaroslav@323: * @author Jaroslav Tulach jaroslav@323: */ jaroslav@1041: public final class Console { jaroslav@1041: public Console() { jaroslav@517: } jaroslav@323: jaroslav@1041: private static Object getAttr(Object elem, String attr) { jaroslav@1041: return InvokeJS.CObject.call("getAttr", elem, attr); jaroslav@1041: } jaroslav@323: jaroslav@1041: private static void setAttr(String id, String attr, Object value) { jaroslav@1041: InvokeJS.CObject.call("setAttrId", id, attr, value); jaroslav@1041: } jaroslav@1041: private static void setAttr(Object id, String attr, Object value) { jaroslav@1041: InvokeJS.CObject.call("setAttr", id, attr, value); jaroslav@1041: } jaroslav@366: jaroslav@1041: private static void closeWindow() {} jaroslav@342: jaroslav@916: private static Object textArea; jaroslav@916: private static Object statusArea; jaroslav@916: jaroslav@343: private static void log(String newText) { jaroslav@916: if (textArea == null) { jaroslav@916: return; jaroslav@916: } jaroslav@343: String attr = "value"; jaroslav@916: setAttr(textArea, attr, getAttr(textArea, attr) + "\n" + newText); jaroslav@916: setAttr(textArea, "scrollTop", getAttr(textArea, "scrollHeight")); jaroslav@342: } jaroslav@323: jaroslav@916: private static void beginTest(Case c) { jaroslav@916: Object[] arr = new Object[2]; jaroslav@916: beginTest(c.getClassName() + "." + c.getMethodName(), c, arr); jaroslav@916: textArea = arr[0]; jaroslav@916: statusArea = arr[1]; jaroslav@916: } jaroslav@916: jaroslav@916: private static void finishTest(Case c, Object res) { jaroslav@916: if ("null".equals(res)) { jaroslav@922: setAttr(statusArea, "innerHTML", "Success"); jaroslav@916: } else { jaroslav@922: setAttr(statusArea, "innerHTML", "Result " + res); jaroslav@916: } jaroslav@916: statusArea = null; jaroslav@916: textArea = null; jaroslav@916: } jaroslav@916: jaroslav@1041: private static final String BEGIN_TEST = jaroslav@1041: "var ul = window.document.getElementById('bck2brwsr.result');\n" jaroslav@916: + "var li = window.document.createElement('li');\n" jaroslav@922: + "var span = window.document.createElement('span');" jaroslav@916: + "span.innerHTML = test + ' - ';\n" jaroslav@922: + "var details = window.document.createElement('a');\n" jaroslav@922: + "details.innerHTML = 'Details';\n" jaroslav@922: + "details.href = '#';\n" jaroslav@916: + "var p = window.document.createElement('p');\n" jaroslav@916: + "var status = window.document.createElement('a');\n" jaroslav@916: + "status.innerHTML = 'running';" jaroslav@922: + "details.onclick = function() { li.appendChild(p); li.removeChild(details); status.innerHTML = 'Run Again'; status.href = '#'; };\n" jaroslav@1041: + "status.onclick = function() { c.again(arr); }\n" jaroslav@916: + "var pre = window.document.createElement('textarea');\n" jaroslav@922: + "pre.cols = 100;" jaroslav@922: + "pre.rows = 10;" jaroslav@916: + "li.appendChild(span);\n" jaroslav@916: + "li.appendChild(status);\n" jaroslav@922: + "var span = window.document.createElement('span');" jaroslav@922: + "span.innerHTML = ' ';\n" jaroslav@922: + "li.appendChild(span);\n" jaroslav@922: + "li.appendChild(details);\n" jaroslav@916: + "p.appendChild(pre);\n" jaroslav@916: + "ul.appendChild(li);\n" jaroslav@916: + "arr[0] = pre;\n" jaroslav@1041: + "arr[1] = status;\n"; jaroslav@1041: jaroslav@1041: private static void beginTest(String test, Case c, Object[] arr) { jaroslav@1041: InvokeJS.CObject.call("beginTest", test, c, arr); jaroslav@1041: } jaroslav@916: jaroslav@1041: private static final String LOAD_TEXT = jaroslav@1041: "var request = new XMLHttpRequest();\n" jaroslav@519: + "request.open('GET', url, true);\n" jaroslav@800: + "request.setRequestHeader('Content-Type', 'text/plain; charset=utf-8');\n" jaroslav@519: + "request.onreadystatechange = function() {\n" jaroslav@519: + " if (this.readyState!==4) return;\n" jaroslav@1041: + " try {" jaroslav@519: + " arr[0] = this.responseText;\n" jaroslav@519: + " callback.run__V();\n" jaroslav@1041: + " } catch (e) { alert(e); }" jaroslav@519: + "};" jaroslav@1041: + "request.send();"; jaroslav@1041: private static void loadText(String url, Runnable callback, String[] arr) throws IOException { jaroslav@1041: InvokeJS.CObject.call("loadText", url, new Run(callback), arr); jaroslav@1041: } jaroslav@332: jaroslav@1041: public static void runHarness(String url) throws IOException { jaroslav@1041: new Console().harness(url); jaroslav@1041: } jaroslav@1041: jaroslav@1041: public void harness(String url) throws IOException { jaroslav@343: log("Connecting to " + url); jaroslav@519: Request r = new Request(url); jaroslav@519: } jaroslav@519: jaroslav@519: private static class Request implements Runnable { jaroslav@519: private final String[] arr = { null }; jaroslav@519: private final String url; jaroslav@939: private Case c; jaroslav@942: private int retries; jaroslav@519: jaroslav@519: private Request(String url) throws IOException { jaroslav@519: this.url = url; jaroslav@519: loadText(url, this, arr); jaroslav@519: } jaroslav@939: private Request(String url, String u) throws IOException { jaroslav@939: this.url = url; jaroslav@939: loadText(u, this, arr); jaroslav@939: } jaroslav@519: jaroslav@519: @Override jaroslav@519: public void run() { jaroslav@519: try { jaroslav@939: if (c == null) { jaroslav@939: String data = arr[0]; jaroslav@939: jaroslav@939: if (data == null) { jaroslav@939: log("Some error exiting"); jaroslav@939: closeWindow(); jaroslav@939: return; jaroslav@939: } jaroslav@939: jaroslav@939: if (data.isEmpty()) { jaroslav@939: log("No data, exiting"); jaroslav@939: closeWindow(); jaroslav@939: return; jaroslav@939: } jaroslav@939: jaroslav@939: c = Case.parseData(data); jaroslav@939: beginTest(c); jaroslav@940: log("Got \"" + data + "\""); jaroslav@940: } else { jaroslav@942: log("Processing \"" + arr[0] + "\" for " + retries + " time"); jaroslav@939: } jaroslav@942: Object result = retries++ >= 10 ? "java.lang.InterruptedException:timeout" : c.runTest(); jaroslav@939: finishTest(c, result); jaroslav@519: jaroslav@939: String u = url + "?request=" + c.getRequestId() + "&result=" + result; jaroslav@939: new Request(url, u); jaroslav@939: } catch (Exception ex) { jaroslav@939: if (ex instanceof InterruptedException) { jaroslav@940: log("Re-scheduling in 100ms"); jaroslav@940: schedule(this, 100); jaroslav@519: return; jaroslav@519: } jaroslav@707: log(ex.getClass().getName() + ":" + ex.getMessage()); jaroslav@342: } jaroslav@332: } jaroslav@332: } jaroslav@356: jaroslav@802: private static String encodeURL(String r) throws UnsupportedEncodingException { jaroslav@802: final String SPECIAL = "%$&+,/:;=?@"; jaroslav@381: StringBuilder sb = new StringBuilder(); jaroslav@802: byte[] utf8 = r.getBytes("UTF-8"); jaroslav@802: for (int i = 0; i < utf8.length; i++) { jaroslav@802: int ch = utf8[i] & 0xff; jaroslav@802: if (ch < 32 || ch > 127 || SPECIAL.indexOf(ch) >= 0) { jaroslav@802: final String numbers = "0" + Integer.toHexString(ch); jaroslav@802: sb.append("%").append(numbers.substring(numbers.length() - 2)); jaroslav@381: } else { jaroslav@381: if (ch == 32) { jaroslav@381: sb.append("+"); jaroslav@381: } else { jaroslav@381: sb.append((char)ch); jaroslav@381: } jaroslav@381: } jaroslav@381: } jaroslav@381: return sb.toString(); jaroslav@381: } jaroslav@381: jaroslav@939: static String invoke(String clazz, String method) throws jaroslav@939: ClassNotFoundException, InvocationTargetException, IllegalAccessException, jaroslav@939: InstantiationException, InterruptedException { jaroslav@939: final Object r = new Case(null).invokeMethod(clazz, method); jaroslav@356: return r == null ? "null" : r.toString().toString(); jaroslav@356: } jaroslav@323: jaroslav@360: /** Helper method that inspects the classpath and loads given resource jaroslav@360: * (usually a class file). Used while running tests in Rhino. jaroslav@360: * jaroslav@360: * @param name resource name to find jaroslav@360: * @return the array of bytes in the given resource jaroslav@360: * @throws IOException I/O in case something goes wrong jaroslav@360: */ jaroslav@360: public static byte[] read(String name) throws IOException { jaroslav@360: URL u = null; jaroslav@360: Enumeration en = Console.class.getClassLoader().getResources(name); jaroslav@360: while (en.hasMoreElements()) { jaroslav@360: u = en.nextElement(); jaroslav@360: } jaroslav@360: if (u == null) { jaroslav@360: throw new IOException("Can't find " + name); jaroslav@360: } jaroslav@360: try (InputStream is = u.openStream()) { jaroslav@360: byte[] arr; jaroslav@360: arr = new byte[is.available()]; jaroslav@360: int offset = 0; jaroslav@360: while (offset < arr.length) { jaroslav@360: int len = is.read(arr, offset, arr.length - offset); jaroslav@360: if (len == -1) { jaroslav@360: throw new IOException("Can't read " + name); jaroslav@360: } jaroslav@360: offset += len; jaroslav@360: } jaroslav@360: return arr; jaroslav@360: } jaroslav@360: } jaroslav@360: jaroslav@517: private static void turnAssetionStatusOn() { jaroslav@517: } jaroslav@939: jaroslav@1041: private static Object schedule(Runnable r, int time) { jaroslav@1041: return InvokeJS.CObject.call("schedule", new Run(r), time); jaroslav@1041: } jaroslav@342: jaroslav@342: private static final class Case { jaroslav@342: private final Object data; jaroslav@939: private Object inst; jaroslav@342: jaroslav@342: private Case(Object data) { jaroslav@342: this.data = data; jaroslav@342: } jaroslav@342: jaroslav@342: public static Case parseData(String s) { jaroslav@342: return new Case(toJSON(s)); jaroslav@342: } jaroslav@342: jaroslav@342: public String getMethodName() { jaroslav@1041: return (String) value("methodName", data); jaroslav@342: } jaroslav@342: jaroslav@342: public String getClassName() { jaroslav@1041: return (String) value("className", data); jaroslav@342: } jaroslav@342: jaroslav@1041: public int getRequestId() { jaroslav@1041: Object v = value("request", data); jaroslav@1041: if (v instanceof Number) { jaroslav@1041: return ((Number)v).intValue(); jaroslav@1041: } jaroslav@1041: return Integer.parseInt(v.toString()); jaroslav@342: } jaroslav@526: jaroslav@526: public String getHtmlFragment() { jaroslav@1041: return (String) value("html", data); jaroslav@526: } jaroslav@342: jaroslav@916: void again(Object[] arr) { jaroslav@916: try { jaroslav@916: textArea = arr[0]; jaroslav@916: statusArea = arr[1]; jaroslav@916: setAttr(textArea, "value", ""); jaroslav@916: runTest(); jaroslav@916: } catch (Exception ex) { jaroslav@916: log(ex.getClass().getName() + ":" + ex.getMessage()); jaroslav@916: } jaroslav@916: } jaroslav@916: jaroslav@939: private Object runTest() throws IllegalAccessException, jaroslav@939: IllegalArgumentException, ClassNotFoundException, UnsupportedEncodingException, jaroslav@939: InvocationTargetException, InstantiationException, InterruptedException { jaroslav@916: if (this.getHtmlFragment() != null) { jaroslav@916: setAttr("bck2brwsr.fragment", "innerHTML", this.getHtmlFragment()); jaroslav@916: } jaroslav@916: log("Invoking " + this.getClassName() + '.' + this.getMethodName() + " as request: " + this.getRequestId()); jaroslav@916: Object result = invokeMethod(this.getClassName(), this.getMethodName()); jaroslav@916: setAttr("bck2brwsr.fragment", "innerHTML", ""); jaroslav@916: log("Result: " + result); jaroslav@916: result = encodeURL("" + result); jaroslav@916: log("Sending back: ...?request=" + this.getRequestId() + "&result=" + result); jaroslav@916: return result; jaroslav@916: } jaroslav@939: jaroslav@939: private Object invokeMethod(String clazz, String method) jaroslav@939: throws ClassNotFoundException, InvocationTargetException, jaroslav@939: InterruptedException, IllegalAccessException, IllegalArgumentException, jaroslav@939: InstantiationException { jaroslav@939: Method found = null; jaroslav@939: Class c = Class.forName(clazz); jaroslav@939: for (Method m : c.getMethods()) { jaroslav@939: if (m.getName().equals(method)) { jaroslav@939: found = m; jaroslav@939: } jaroslav@939: } jaroslav@939: Object res; jaroslav@939: if (found != null) { jaroslav@939: try { jaroslav@939: if ((found.getModifiers() & Modifier.STATIC) != 0) { jaroslav@939: res = found.invoke(null); jaroslav@939: } else { jaroslav@939: if (inst == null) { jaroslav@939: inst = c.newInstance(); jaroslav@939: } jaroslav@939: res = found.invoke(inst); jaroslav@939: } jaroslav@939: } catch (Throwable ex) { jaroslav@939: if (ex instanceof InvocationTargetException) { jaroslav@939: ex = ((InvocationTargetException) ex).getTargetException(); jaroslav@939: } jaroslav@939: if (ex instanceof InterruptedException) { jaroslav@939: throw (InterruptedException)ex; jaroslav@939: } jaroslav@939: res = ex.getClass().getName() + ":" + ex.getMessage(); jaroslav@939: } jaroslav@939: } else { jaroslav@939: res = "Can't find method " + method + " in " + clazz; jaroslav@939: } jaroslav@939: return res; jaroslav@939: } jaroslav@916: jaroslav@1041: private static Object toJSON(String s) { jaroslav@1041: return InvokeJS.CObject.call("toJSON", s); jaroslav@1041: } jaroslav@342: jaroslav@1041: private static Object value(String p, Object d) { jaroslav@1041: return ((JSObject)d).getMember(p); jaroslav@1041: } jaroslav@342: } jaroslav@1041: jaroslav@1041: private static String safe(String txt) { jaroslav@1041: return "try {" + txt + "} catch (err) { alert(err); }"; jaroslav@1041: } jaroslav@1041: jaroslav@1041: static { jaroslav@1041: turnAssetionStatusOn(); jaroslav@1041: } jaroslav@1041: jaroslav@1041: private static final class InvokeJS { jaroslav@1041: static final JSObject CObject = initJS(); jaroslav@1041: jaroslav@1041: private static JSObject initJS() { jaroslav@1041: WebEngine web = (WebEngine) System.getProperties().get("webEngine"); jaroslav@1041: return (JSObject) web.executeScript("(function() {" jaroslav@1041: + "var CObject = {};" jaroslav@1041: jaroslav@1041: + "CObject.getAttr = function(elem, attr) { return elem[attr].toString(); };" jaroslav@1041: jaroslav@1041: + "CObject.setAttrId = function(id, attr, value) { window.document.getElementById(id)[attr] = value; };" jaroslav@1041: + "CObject.setAttr = function(elem, attr, value) { elem[attr] = value; };" jaroslav@1041: jaroslav@1041: + "CObject.beginTest = function(test, c, arr) {" + safe(BEGIN_TEST) + "};" jaroslav@1041: jaroslav@1041: + "CObject.loadText = function(url, callback, arr) {" + safe(LOAD_TEXT.replace("run__V", "run")) + "};" jaroslav@1041: jaroslav@1041: + "CObject.schedule = function(r, time) { return window.setTimeout(function() { r.run(); }, time); };" jaroslav@1041: jaroslav@1041: + "CObject.toJSON = function(s) { return eval('(' + s + ')'); };" jaroslav@1041: jaroslav@1041: + "return CObject;" jaroslav@1041: + "})(this)"); jaroslav@1041: } jaroslav@1041: } jaroslav@1041: jaroslav@323: }