# HG changeset patch # User Jaroslav Tulach # Date 1365442388 -7200 # Node ID c75bd6823179051b0ff9ebc4c9f974fd0c20ec58 # Parent 2fb3e929962fdada477ad3413ec3385f7f0d3857# Parent 022f62873be6ea16040f5c9d692ac32eaeba87f6 Is model branch in good shape for merging? I think so. diff -r 2fb3e929962f -r c75bd6823179 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java Wed Apr 03 13:43:22 2013 +0200 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java Mon Apr 08 19:33:08 2013 +0200 @@ -50,6 +50,22 @@ throw new IllegalStateException("Value " + ret + " is not of type " + modelClass); } + public static String toJSON(Object value) { + if (value == null) { + return "null"; + } + if (value instanceof String) { + return '"' + + ((String)value). + replace("\"", "\\\""). + replace("\n", "\\n"). + replace("\r", "\\r"). + replace("\t", "\\t") + + '"'; + } + return value.toString(); + } + @JavaScriptBody(args = { "object", "property" }, body = "if (property === null) return object;\n" + "var p = object[property]; return p ? p : null;" @@ -57,4 +73,78 @@ private static Object getProperty(Object object, String property) { return null; } + + public static String createJSONP(Object[] jsonResult, Runnable whenDone) { + int h = whenDone.hashCode(); + String name; + for (;;) { + name = "jsonp" + Integer.toHexString(h); + if (defineIfUnused(name, jsonResult, whenDone)) { + return name; + } + h++; + } + } + + @JavaScriptBody(args = { "name", "arr", "run" }, body = + "if (window[name]) return false;\n " + + "window[name] = function(data) {\n " + + " arr[0] = data;\n" + + " run.run__V();\n" + + " delete window[name];\n" + + "};" + + "return true;\n" + ) + private static boolean defineIfUnused(String name, Object[] arr, Runnable run) { + return true; + } + + @JavaScriptBody(args = { "url", "arr", "callback" }, body = "" + + "var request = new XMLHttpRequest();\n" + + "request.open('GET', url, true);\n" + + "request.setRequestHeader('Content-Type', 'application/json; charset=utf-8');\n" + + "request.onreadystatechange = function() {\n" + + " if (this.readyState!==4) return;\n" + + " try {\n" + + " arr[0] = eval('(' + this.response + ')');\n" + + " } catch (error) {;\n" + + " throw 'Cannot parse' + error + ':' + this.response;\n" + + " };\n" + + " callback.run__V();\n" + + "};" + + "request.send();" + ) + private static void loadJSON( + String url, Object[] jsonResult, Runnable whenDone + ) { + } + + public static void loadJSON( + String url, Object[] jsonResult, Runnable whenDone, String jsonp + ) { + if (jsonp == null) { + loadJSON(url, jsonResult, whenDone); + } else { + loadJSONP(url, jsonp); + } + } + + @JavaScriptBody(args = { "url", "jsonp" }, body = + "var scrpt = window.document.createElement('script');\n " + + "scrpt.setAttribute('src', url);\n " + + "scrpt.setAttribute('id', jsonp);\n " + + "scrpt.setAttribute('type', 'text/javascript');\n " + + "var body = document.getElementsByTagName('body')[0];\n " + + "body.appendChild(scrpt);\n" + ) + private static void loadJSONP(String url, String jsonp) { + + } + + public static void extractJSON(Object jsonObject, String[] props, Object[] values) { + for (int i = 0; i < props.length; i++) { + values[i] = getProperty(jsonObject, props[i]); + } + } + } diff -r 2fb3e929962f -r c75bd6823179 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/KOList.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/KOList.java Wed Apr 03 13:43:22 2013 +0200 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/KOList.java Mon Apr 08 19:33:08 2013 +0200 @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import org.apidesign.bck2brwsr.core.JavaScriptOnly; /** @@ -29,6 +30,7 @@ private final String name; private final String[] deps; private Knockout model; + private Runnable onchange; public KOList(String name, String... deps) { this.name = name; @@ -36,7 +38,18 @@ } public void assign(Knockout model) { - this.model = model; + if (this.model != model) { + this.model = model; + notifyChange(); + } + } + + public KOList onChange(Runnable r) { + if (this.onchange != null) { + throw new IllegalStateException(); + } + this.onchange = r; + return this; } @Override @@ -47,6 +60,20 @@ } @Override + public boolean addAll(Collection c) { + boolean ret = super.addAll(c); + notifyChange(); + return ret; + } + + @Override + public boolean addAll(int index, Collection c) { + boolean ret = super.addAll(index, c); + notifyChange(); + return ret; + } + + @Override public boolean remove(Object o) { boolean ret = super.remove(o); notifyChange(); @@ -92,7 +119,25 @@ notifyChange(); return ret; } - + + @Override + public String toString() { + Iterator it = iterator(); + if (!it.hasNext()) { + return "[]"; + } + String sep = ""; + StringBuilder sb = new StringBuilder(); + sb.append('['); + while (it.hasNext()) { + T t = it.next(); + sb.append(sep); + sb.append(ConvertTypes.toJSON(t)); + sep = ","; + } + sb.append(']'); + return sb.toString(); + } @JavaScriptOnly(name = "koArray", value = "function() { return this.toArray___3Ljava_lang_Object_2(); }") @@ -100,14 +145,23 @@ private void notifyChange() { Knockout m = model; - if (m == null) { - return; + if (m != null) { + m.valueHasMutated(name); + for (String dependant : deps) { + m.valueHasMutated(dependant); + } } - m.valueHasMutated(name); - for (String dependant : deps) { - m.valueHasMutated(dependant); + Runnable r = onchange; + if (r != null) { + r.run(); } } - + + @Override + public KOList clone() { + KOList ko = (KOList)super.clone(); + ko.model = null; + return ko; + } } diff -r 2fb3e929962f -r c75bd6823179 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Wed Apr 03 13:43:22 2013 +0200 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Mon Apr 08 19:33:08 2013 +0200 @@ -26,6 +26,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -34,6 +35,7 @@ import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Completion; import javax.annotation.processing.Completions; +import javax.annotation.processing.Messager; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; @@ -45,9 +47,11 @@ import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.ArrayType; import javax.lang.model.type.MirroredTypeException; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; import javax.tools.FileObject; @@ -56,6 +60,8 @@ import org.apidesign.bck2brwsr.htmlpage.api.Model; import org.apidesign.bck2brwsr.htmlpage.api.On; import org.apidesign.bck2brwsr.htmlpage.api.OnFunction; +import org.apidesign.bck2brwsr.htmlpage.api.OnPropertyChange; +import org.apidesign.bck2brwsr.htmlpage.api.OnReceive; import org.apidesign.bck2brwsr.htmlpage.api.Page; import org.apidesign.bck2brwsr.htmlpage.api.Property; import org.openide.util.lookup.ServiceProvider; @@ -70,6 +76,8 @@ "org.apidesign.bck2brwsr.htmlpage.api.Model", "org.apidesign.bck2brwsr.htmlpage.api.Page", "org.apidesign.bck2brwsr.htmlpage.api.OnFunction", + "org.apidesign.bck2brwsr.htmlpage.api.OnReceive", + "org.apidesign.bck2brwsr.htmlpage.api.OnPropertyChange", "org.apidesign.bck2brwsr.htmlpage.api.On" }) public final class PageProcessor extends AbstractProcessor { @@ -102,6 +110,10 @@ return processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, pkg, name).openInputStream(); } } + + private Messager err() { + return processingEnv.getMessager(); + } private boolean processModel(Element e) { boolean ok = true; @@ -112,15 +124,20 @@ String pkg = findPkgName(e); Writer w; String className = m.className(); + models.put(e, className); try { StringWriter body = new StringWriter(); List propsGetSet = new ArrayList<>(); List functions = new ArrayList<>(); Map> propsDeps = new HashMap<>(); + Map> functionDeps = new HashMap<>(); if (!generateComputedProperties(body, m.properties(), e.getEnclosedElements(), propsGetSet, propsDeps)) { ok = false; } - if (!generateProperties(e, body, m.properties(), propsGetSet, propsDeps)) { + if (!generateOnChange(e, propsDeps, m.properties(), className, functionDeps)) { + ok = false; + } + if (!generateProperties(e, body, m.properties(), propsGetSet, propsDeps, functionDeps)) { ok = false; } if (!generateFunctions(e, body, className, e.getEnclosedElements(), functions)) { @@ -133,25 +150,94 @@ w.append("import org.apidesign.bck2brwsr.htmlpage.api.*;\n"); w.append("import org.apidesign.bck2brwsr.htmlpage.KOList;\n"); w.append("import org.apidesign.bck2brwsr.core.JavaScriptOnly;\n"); - w.append("final class ").append(className).append(" {\n"); - w.append(" private Object json;\n"); + w.append("final class ").append(className).append(" implements Cloneable {\n"); w.append(" private boolean locked;\n"); w.append(" private org.apidesign.bck2brwsr.htmlpage.Knockout ko;\n"); w.append(body.toString()); - w.append(" private static Class<" + e.getSimpleName() + "> modelFor() { return null; }\n"); + w.append(" private static Class<" + inPckName(e) + "> modelFor() { return null; }\n"); w.append(" public ").append(className).append("() {\n"); + w.append(" intKnckt();\n"); + w.append(" };\n"); + w.append(" private void intKnckt() {\n"); w.append(" ko = org.apidesign.bck2brwsr.htmlpage.Knockout.applyBindings(this, "); writeStringArray(propsGetSet, w); w.append(", "); writeStringArray(functions, w); w.append(" );\n"); w.append(" };\n"); + w.append(" ").append(className).append("(Object json) {\n"); + int values = 0; + for (int i = 0; i < propsGetSet.size(); i += 4) { + Property p = findProperty(m.properties(), propsGetSet.get(i)); + if (p == null) { + continue; + } + values++; + } + w.append(" Object[] ret = new Object[" + values + "];\n"); + w.append(" org.apidesign.bck2brwsr.htmlpage.ConvertTypes.extractJSON(json, new String[] {\n"); + for (int i = 0; i < propsGetSet.size(); i += 4) { + Property p = findProperty(m.properties(), propsGetSet.get(i)); + if (p == null) { + continue; + } + w.append(" \"").append(propsGetSet.get(i)).append("\",\n"); + } + w.append(" }, ret);\n"); + for (int i = 0, cnt = 0, prop = 0; i < propsGetSet.size(); i += 4) { + final String pn = propsGetSet.get(i); + Property p = findProperty(m.properties(), pn); + if (p == null) { + continue; + } + boolean[] isModel = { false }; + boolean[] isEnum = { false }; + String type = checkType(m.properties()[prop++], isModel, isEnum); + if (isEnum[0]) { +// w.append(type).append(".valueOf((String)"); +// close = true; + w.append(" this.prop_").append(pn); + w.append(" = null;\n"); + } else if (p.array()) { + w.append("if (ret[" + cnt + "] instanceof Object[]) {\n"); + w.append(" for (Object e : ((Object[])ret[" + cnt + "])) {\n"); + if (isModel[0]) { + w.append(" this.prop_").append(pn).append(".add(new "); + w.append(type).append("(e));\n"); + } else { + if (isPrimitive(type)) { + w.append(" this.prop_").append(pn).append(".add(((Number)e)."); + w.append(type).append("Value());\n"); + } else { + w.append(" this.prop_").append(pn).append(".add(("); + w.append(type).append(")e);\n"); + } + } + w.append(" }\n"); + w.append("}\n"); + } else { + if (isPrimitive(type)) { + w.append(" this.prop_").append(pn); + w.append(" = ((Number)").append("ret[" + cnt + "])."); + w.append(type).append("Value();\n"); + } else { + w.append(" this.prop_").append(pn); + w.append(" = (").append(type).append(')'); + w.append("ret[" + cnt + "];\n"); + } + } + cnt++; + } + w.append(" intKnckt();\n"); + w.append(" };\n"); + writeToString(m.properties(), w); + writeClone(className, m.properties(), w); w.append("}\n"); } finally { w.close(); } } catch (IOException ex) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e); + err().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e); return false; } return ok; @@ -170,7 +256,7 @@ pp = ProcessPage.readPage(is); is.close(); } catch (IOException iOException) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Can't read " + p.xhtml(), e); + err().printMessage(Diagnostic.Kind.ERROR, "Can't read " + p.xhtml() + " as " + iOException.getMessage(), e); ok = false; pp = null; } @@ -185,15 +271,22 @@ List propsGetSet = new ArrayList<>(); List functions = new ArrayList<>(); Map> propsDeps = new HashMap<>(); + Map> functionDeps = new HashMap<>(); if (!generateComputedProperties(body, p.properties(), e.getEnclosedElements(), propsGetSet, propsDeps)) { ok = false; } - if (!generateProperties(e, body, p.properties(), propsGetSet, propsDeps)) { + if (!generateOnChange(e, propsDeps, p.properties(), className, functionDeps)) { + ok = false; + } + if (!generateProperties(e, body, p.properties(), propsGetSet, propsDeps, functionDeps)) { ok = false; } if (!generateFunctions(e, body, className, e.getEnclosedElements(), functions)) { ok = false; } + if (!generateReceive(e, body, className, e.getEnclosedElements(), functions)) { + ok = false; + } FileObject java = processingEnv.getFiler().createSourceFile(pkg + '.' + className, e); w = new OutputStreamWriter(java.openOutputStream()); @@ -206,7 +299,7 @@ if (!initializeOnClick(className, (TypeElement) e, w, pp)) { ok = false; } else { - for (String id : pp.ids()) { + if (pp != null) for (String id : pp.ids()) { String tag = pp.tagNameForId(id); String type = type(tag); w.append(" ").append("public final "). @@ -234,7 +327,7 @@ w.close(); } } catch (IOException ex) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e); + err().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e); return false; } return ok; @@ -280,24 +373,24 @@ if (oc != null) { for (String id : oc.id()) { if (pp == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "id = " + id + " not found in HTML page."); + err().printMessage(Diagnostic.Kind.ERROR, "id = " + id + " not found in HTML page."); ok = false; continue; } if (pp.tagNameForId(id) == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "id = " + id + " does not exist in the HTML page. Found only " + pp.ids(), method); + err().printMessage(Diagnostic.Kind.ERROR, "id = " + id + " does not exist in the HTML page. Found only " + pp.ids(), method); ok = false; continue; } ExecutableElement ee = (ExecutableElement)method; CharSequence params = wrapParams(ee, id, className, "ev", null); if (!ee.getModifiers().contains(Modifier.STATIC)) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@On method has to be static", ee); + err().printMessage(Diagnostic.Kind.ERROR, "@On method has to be static", ee); ok = false; continue; } if (ee.getModifiers().contains(Modifier.PRIVATE)) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@On method can't be private", ee); + err().printMessage(Diagnostic.Kind.ERROR, "@On method can't be private", ee); ok = false; continue; } @@ -378,7 +471,9 @@ private boolean generateProperties( Element where, Writer w, Property[] properties, - Collection props, Map> deps + Collection props, + Map> deps, + Map> functionDeps ) throws IOException { boolean ok = true; for (Property p : properties) { @@ -389,7 +484,7 @@ if (p.array()) { w.write("private KOList<" + tn + "> prop_" + p.name() + " = new KOList<" + tn + ">(\"" + p.name() + "\""); - final Collection dependants = deps.get(p.name()); + Collection dependants = deps.get(p.name()); if (dependants != null) { for (String depProp : dependants) { w.write(", "); @@ -398,7 +493,18 @@ w.write('\"'); } } - w.write(");\n"); + w.write(")"); + + dependants = functionDeps.get(p.name()); + if (dependants != null) { + w.write(".onChange(new Runnable() { public void run() {\n"); + for (String call : dependants) { + w.append(call); + } + w.write("}})"); + } + w.write(";\n"); + w.write("public java.util.List<" + tn + "> " + gs[0] + "() {\n"); w.write(" if (locked) throw new IllegalStateException();\n"); w.write(" prop_" + p.name() + ".assign(ko);\n"); @@ -415,13 +521,19 @@ w.write(" prop_" + p.name() + " = v;\n"); w.write(" if (ko != null) {\n"); w.write(" ko.valueHasMutated(\"" + p.name() + "\");\n"); - final Collection dependants = deps.get(p.name()); + Collection dependants = deps.get(p.name()); if (dependants != null) { for (String depProp : dependants) { w.write(" ko.valueHasMutated(\"" + depProp + "\");\n"); } } w.write(" }\n"); + dependants = functionDeps.get(p.name()); + if (dependants != null) { + for (String call : dependants) { + w.append(call); + } + } w.write("}\n"); } @@ -450,7 +562,7 @@ final TypeMirror rt = ee.getReturnType(); final Types tu = processingEnv.getTypeUtils(); TypeMirror ert = tu.erasure(rt); - String tn = ert.toString(); + String tn = fqn(ert, ee); boolean array = false; if (tn.equals("java.util.List")) { array = true; @@ -469,7 +581,7 @@ ok = false; } - final String dt = pe.asType().toString(); + final String dt = fqn(pe.asType(), ee); String[] call = toGetSet(dn, dt, false); w.write(" " + dt + " arg" + (++arg) + " = "); w.write(call[0] + "();\n"); @@ -483,7 +595,7 @@ } w.write(" try {\n"); w.write(" locked = true;\n"); - w.write(" return " + e.getEnclosingElement().getSimpleName() + '.' + e.getSimpleName() + "("); + w.write(" return " + fqn(ee.getEnclosingElement().asType(), ee) + '.' + e.getSimpleName() + "("); String sep = ""; for (int i = 1; i <= arg; i++) { w.write(sep); @@ -538,35 +650,19 @@ private String typeName(Element where, Property p) { String ret; - boolean isModel = false; - boolean isEnum = false; - try { - ret = p.type().getName(); - } catch (MirroredTypeException ex) { - TypeMirror tm = processingEnv.getTypeUtils().erasure(ex.getTypeMirror()); - final Element e = processingEnv.getTypeUtils().asElement(tm); - final Model m = e == null ? null : e.getAnnotation(Model.class); - if (m != null) { - ret = findPkgName(e) + '.' + m.className(); - isModel = true; - models.put(e, m.className()); - } else { - ret = tm.toString(); - } - TypeMirror enm = processingEnv.getElementUtils().getTypeElement("java.lang.Enum").asType(); - enm = processingEnv.getTypeUtils().erasure(enm); - isEnum = processingEnv.getTypeUtils().isSubtype(tm, enm); - } + boolean[] isModel = { false }; + boolean[] isEnum = { false }; + ret = checkType(p, isModel, isEnum); if (p.array()) { String bt = findBoxedType(ret); if (bt != null) { return bt; } } - if (!isModel && !"java.lang.String".equals(ret) && !isEnum) { + if (!isModel[0] && !"java.lang.String".equals(ret) && !isEnum[0]) { String bt = findBoxedType(ret); if (bt == null) { - processingEnv.getMessager().printMessage( + err().printMessage( Diagnostic.Kind.ERROR, "Only primitive types supported in the mapping. Not " + ret, where @@ -617,7 +713,7 @@ sb.append('"'); sep = ", "; } - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, + err().printMessage(Diagnostic.Kind.ERROR, propName + " is not one of known properties: " + sb , e ); @@ -647,25 +743,25 @@ continue; } if (!e.getModifiers().contains(Modifier.STATIC)) { - processingEnv.getMessager().printMessage( + err().printMessage( Diagnostic.Kind.ERROR, "@OnFunction method needs to be static", e ); return false; } if (e.getModifiers().contains(Modifier.PRIVATE)) { - processingEnv.getMessager().printMessage( + err().printMessage( Diagnostic.Kind.ERROR, "@OnFunction method cannot be private", e ); return false; } if (e.getReturnType().getKind() != TypeKind.VOID) { - processingEnv.getMessager().printMessage( + err().printMessage( Diagnostic.Kind.ERROR, "@OnFunction method should return void", e ); return false; } String n = e.getSimpleName().toString(); - body.append("void ").append(n).append("(Object data, Object ev) {\n"); + body.append("private void ").append(n).append("(Object data, Object ev) {\n"); body.append(" ").append(clazz.getSimpleName()).append(".").append(n).append("("); body.append(wrapParams(e, null, className, "ev", "data")); body.append(");\n"); @@ -677,6 +773,205 @@ return true; } + private boolean generateOnChange(Element clazz, Map> propDeps, + Property[] properties, String className, + Map> functionDeps + ) { + for (Element m : clazz.getEnclosedElements()) { + if (m.getKind() != ElementKind.METHOD) { + continue; + } + ExecutableElement e = (ExecutableElement) m; + OnPropertyChange onPC = e.getAnnotation(OnPropertyChange.class); + if (onPC == null) { + continue; + } + for (String pn : onPC.value()) { + if (findProperty(properties, pn) == null && findDerivedFrom(propDeps, pn).isEmpty()) { + err().printMessage(Diagnostic.Kind.ERROR, "No property named '" + pn + "' in the model"); + return false; + } + } + if (!e.getModifiers().contains(Modifier.STATIC)) { + err().printMessage( + Diagnostic.Kind.ERROR, "@OnPropertyChange method needs to be static", e); + return false; + } + if (e.getModifiers().contains(Modifier.PRIVATE)) { + err().printMessage( + Diagnostic.Kind.ERROR, "@OnPropertyChange method cannot be private", e); + return false; + } + if (e.getReturnType().getKind() != TypeKind.VOID) { + err().printMessage( + Diagnostic.Kind.ERROR, "@OnPropertyChange method should return void", e); + return false; + } + String n = e.getSimpleName().toString(); + + + for (String pn : onPC.value()) { + StringBuilder call = new StringBuilder(); + call.append(" ").append(clazz.getSimpleName()).append(".").append(n).append("("); + call.append(wrapPropName(e, className, "name", pn)); + call.append(");\n"); + + Collection change = functionDeps.get(pn); + if (change == null) { + change = new ArrayList<>(); + functionDeps.put(pn, change); + } + change.add(call.toString()); + for (String dpn : findDerivedFrom(propDeps, pn)) { + change = functionDeps.get(dpn); + if (change == null) { + change = new ArrayList<>(); + functionDeps.put(dpn, change); + } + change.add(call.toString()); + } + } + } + return true; + } + + private boolean generateReceive( + Element clazz, StringWriter body, String className, + List enclosedElements, List functions + ) { + for (Element m : enclosedElements) { + if (m.getKind() != ElementKind.METHOD) { + continue; + } + ExecutableElement e = (ExecutableElement)m; + OnReceive onR = e.getAnnotation(OnReceive.class); + if (onR == null) { + continue; + } + if (!e.getModifiers().contains(Modifier.STATIC)) { + err().printMessage( + Diagnostic.Kind.ERROR, "@OnReceive method needs to be static", e + ); + return false; + } + if (e.getModifiers().contains(Modifier.PRIVATE)) { + err().printMessage( + Diagnostic.Kind.ERROR, "@OnReceive method cannot be private", e + ); + return false; + } + if (e.getReturnType().getKind() != TypeKind.VOID) { + err().printMessage( + Diagnostic.Kind.ERROR, "@OnReceive method should return void", e + ); + return false; + } + String modelClass = null; + boolean expectsList = false; + List args = new ArrayList<>(); + { + for (VariableElement ve : e.getParameters()) { + TypeMirror modelType = null; + if (ve.asType().toString().equals(className)) { + args.add(className + ".this"); + } else if (isModel(ve.asType())) { + modelType = ve.asType(); + } else if (ve.asType().getKind() == TypeKind.ARRAY) { + modelType = ((ArrayType)ve.asType()).getComponentType(); + expectsList = true; + } + if (modelType != null) { + if (modelClass != null) { + err().printMessage(Diagnostic.Kind.ERROR, "There can be only one model class among arguments", e); + } else { + modelClass = modelType.toString(); + if (expectsList) { + args.add("arr"); + } else { + args.add("arr[0]"); + } + } + } + } + } + if (modelClass == null) { + err().printMessage(Diagnostic.Kind.ERROR, "The method needs to have one @Model class as parameter", e); + } + String n = e.getSimpleName().toString(); + body.append("public void ").append(n).append("("); + StringBuilder assembleURL = new StringBuilder(); + String jsonpVarName = null; + { + String sep = ""; + boolean skipJSONP = onR.jsonp().isEmpty(); + for (String p : findParamNames(e, onR.url(), assembleURL)) { + if (!skipJSONP && p.equals(onR.jsonp())) { + skipJSONP = true; + jsonpVarName = p; + continue; + } + body.append(sep); + body.append("String ").append(p); + sep = ", "; + } + if (!skipJSONP) { + err().printMessage(Diagnostic.Kind.ERROR, + "Name of jsonp attribute ('" + onR.jsonp() + + "') is not used in url attribute '" + onR.url() + "'" + ); + } + } + body.append(") {\n"); + body.append(" final Object[] result = { null };\n"); + body.append( + " class ProcessResult implements Runnable {\n" + + " @Override\n" + + " public void run() {\n" + + " Object value = result[0];\n"); + body.append( + " " + modelClass + "[] arr;\n"); + body.append( + " if (value instanceof Object[]) {\n" + + " Object[] data = ((Object[])value);\n" + + " arr = new " + modelClass + "[data.length];\n" + + " for (int i = 0; i < data.length; i++) {\n" + + " arr[i] = new " + modelClass + "(data[i]);\n" + + " }\n" + + " } else {\n" + + " arr = new " + modelClass + "[1];\n" + + " arr[0] = new " + modelClass + "(value);\n" + + " }\n" + ); + { + body.append(clazz.getSimpleName()).append(".").append(n).append("("); + String sep = ""; + for (String arg : args) { + body.append(sep); + body.append(arg); + sep = ", "; + } + body.append(");\n"); + } + body.append( + " }\n" + + " }\n" + ); + body.append(" ProcessResult pr = new ProcessResult();\n"); + if (jsonpVarName != null) { + body.append(" String ").append(jsonpVarName). + append(" = org.apidesign.bck2brwsr.htmlpage.ConvertTypes.createJSONP(result, pr);\n"); + } + body.append(" org.apidesign.bck2brwsr.htmlpage.ConvertTypes.loadJSON(\n "); + body.append(assembleURL); + body.append(", result, pr, ").append(jsonpVarName).append("\n );\n"); +// body.append(" ").append(clazz.getSimpleName()).append(".").append(n).append("("); +// body.append(wrapParams(e, null, className, "ev", "data")); +// body.append(");\n"); + body.append("}\n"); + } + return true; + } + private CharSequence wrapParams( ExecutableElement ee, String id, String className, String evName, String dataName ) { @@ -712,6 +1007,14 @@ params.append(dataName); params.append(", null"); } else { + if (evName == null) { + final StringBuilder sb = new StringBuilder(); + sb.append("Unexpected string parameter name."); + if (dataName != null) { + sb.append(" Try \"").append(dataName).append("\""); + } + err().printMessage(Diagnostic.Kind.ERROR, sb.toString(), ee); + } params.append(evName); params.append(", \""); params.append(ve.getSimpleName().toString()); @@ -720,7 +1023,7 @@ params.append(")"); continue; } - String rn = ve.asType().toString(); + String rn = fqn(ve.asType(), ee); int last = rn.lastIndexOf('.'); if (last >= 0) { rn = rn.substring(last + 1); @@ -729,7 +1032,7 @@ params.append(className).append(".this"); continue; } - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, + err().printMessage(Diagnostic.Kind.ERROR, "@On method can only accept String named 'id' or " + className + " arguments", ee ); @@ -737,6 +1040,42 @@ return params; } + + private CharSequence wrapPropName( + ExecutableElement ee, String className, String propName, String propValue + ) { + TypeMirror stringType = processingEnv.getElementUtils().getTypeElement("java.lang.String").asType(); + StringBuilder params = new StringBuilder(); + boolean first = true; + for (VariableElement ve : ee.getParameters()) { + if (!first) { + params.append(", "); + } + first = false; + if (ve.asType() == stringType) { + if (propName != null && ve.getSimpleName().contentEquals(propName)) { + params.append('"').append(propValue).append('"'); + } else { + err().printMessage(Diagnostic.Kind.ERROR, "Unexpected string parameter name. Try \"" + propName + "\"."); + } + continue; + } + String rn = fqn(ve.asType(), ee); + int last = rn.lastIndexOf('.'); + if (last >= 0) { + rn = rn.substring(last + 1); + } + if (rn.equals(className)) { + params.append(className).append(".this"); + continue; + } + err().printMessage(Diagnostic.Kind.ERROR, + "@OnPropertyChange method can only accept String or " + className + " arguments", + ee); + } + return params; + } + private boolean isModel(TypeMirror tm) { final Element e = processingEnv.getTypeUtils().asElement(tm); if (e == null) { @@ -767,4 +1106,141 @@ } w.write("\n }"); } + + private void writeToString(Property[] props, Writer w) throws IOException { + w.write(" public String toString() {\n"); + w.write(" StringBuilder sb = new StringBuilder();\n"); + w.write(" sb.append('{');\n"); + String sep = ""; + for (Property p : props) { + w.write(sep); + w.append(" sb.append(\"" + p.name() + ": \");\n"); + w.append(" sb.append(org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toJSON(prop_"); + w.append(p.name()).append("));\n"); + sep = " sb.append(',');\n"; + } + w.write(" sb.append('}');\n"); + w.write(" return sb.toString();\n"); + w.write(" }\n"); + } + private void writeClone(String className, Property[] props, Writer w) throws IOException { + w.write(" public " + className + " clone() {\n"); + w.write(" " + className + " ret = new " + className + "();\n"); + for (Property p : props) { + if (!p.array()) { + boolean isModel[] = { false }; + boolean isEnum[] = { false }; + checkType(p, isModel, isEnum); + if (!isModel[0]) { + w.write(" ret.prop_" + p.name() + " = prop_" + p.name() + ";\n"); + continue; + } + w.write(" ret.prop_" + p.name() + " = prop_" + p.name() + ".clone();\n"); + } else { + w.write(" ret.prop_" + p.name() + " = prop_" + p.name() + ".clone();\n"); + } + } + + w.write(" return ret;\n"); + w.write(" }\n"); + } + + private String inPckName(Element e) { + StringBuilder sb = new StringBuilder(); + while (e.getKind() != ElementKind.PACKAGE) { + if (sb.length() == 0) { + sb.append(e.getSimpleName()); + } else { + sb.insert(0, '.'); + sb.insert(0, e.getSimpleName()); + } + e = e.getEnclosingElement(); + } + return sb.toString(); + } + + private String fqn(TypeMirror pt, Element relative) { + if (pt.getKind() == TypeKind.ERROR) { + final Elements eu = processingEnv.getElementUtils(); + PackageElement pckg = eu.getPackageOf(relative); + return pckg.getQualifiedName() + "." + pt.toString(); + } + return pt.toString(); + } + + private String checkType(Property p, boolean[] isModel, boolean[] isEnum) { + String ret; + try { + ret = p.type().getName(); + } catch (MirroredTypeException ex) { + TypeMirror tm = processingEnv.getTypeUtils().erasure(ex.getTypeMirror()); + final Element e = processingEnv.getTypeUtils().asElement(tm); + final Model m = e == null ? null : e.getAnnotation(Model.class); + if (m != null) { + ret = findPkgName(e) + '.' + m.className(); + isModel[0] = true; + models.put(e, m.className()); + } else { + ret = tm.toString(); + } + TypeMirror enm = processingEnv.getElementUtils().getTypeElement("java.lang.Enum").asType(); + enm = processingEnv.getTypeUtils().erasure(enm); + isEnum[0] = processingEnv.getTypeUtils().isSubtype(tm, enm); + } + return ret; + } + + private Iterable findParamNames(Element e, String url, StringBuilder assembleURL) { + List params = new ArrayList<>(); + + for (int pos = 0; ;) { + int next = url.indexOf('{', pos); + if (next == -1) { + assembleURL.append('"') + .append(url.substring(pos)) + .append('"'); + return params; + } + int close = url.indexOf('}', next); + if (close == -1) { + err().printMessage(Diagnostic.Kind.ERROR, "Unbalanced '{' and '}' in " + url, e); + return params; + } + final String paramName = url.substring(next + 1, close); + params.add(paramName); + assembleURL.append('"') + .append(url.substring(pos, next)) + .append("\" + ").append(paramName).append(" + "); + pos = close + 1; + } + } + + private static Property findProperty(Property[] properties, String propName) { + for (Property p : properties) { + if (propName.equals(p.name())) { + return p; + } + } + return null; + } + + private boolean isPrimitive(String type) { + return + "int".equals(type) || + "double".equals(type) || + "long".equals(type) || + "short".equals(type) || + "byte".equals(type) || + "float".equals(type); + } + + private static Collection findDerivedFrom(Map> propsDeps, String derivedProp) { + Set names = new HashSet<>(); + for (Map.Entry> e : propsDeps.entrySet()) { + if (e.getValue().contains(derivedProp)) { + names.add(e.getKey()); + } + } + return names; + } } diff -r 2fb3e929962f -r c75bd6823179 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/OnPropertyChange.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/OnPropertyChange.java Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,38 @@ +/** + * Back 2 Browser Bytecode Translator + * Copyright (C) 2012 Jaroslav Tulach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. Look for COPYING file in the top folder. + * If not, see http://opensource.org/licenses/GPL-2.0. + */ +package org.apidesign.bck2brwsr.htmlpage.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Represents a property. Either in a generated model of an HTML + * {@link Page} or in a class defined by {@link Model}. + * + * @author Jaroslav Tulach + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface OnPropertyChange { + /** Name(s) of the properties. One wishes to observe. + * + * @return valid java identifier + */ + String[] value(); +} diff -r 2fb3e929962f -r c75bd6823179 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/OnReceive.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/OnReceive.java Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,54 @@ +/** + * Back 2 Browser Bytecode Translator + * Copyright (C) 2012 Jaroslav Tulach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. Look for COPYING file in the top folder. + * If not, see http://opensource.org/licenses/GPL-2.0. + */ +package org.apidesign.bck2brwsr.htmlpage.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Static methods in classes annotated by {@link Model} or {@link Page} + * can be marked by this annotation establish a JSON communication point. + * The associated model page then gets new method to invoke a network + * connection + * + * @author Jaroslav Tulach + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface OnReceive { + /** The URL to connect to. Can contain variable names surrounded by '{' and '}'. + * Those parameters will then become variables of the associated method. + * + * @return the (possibly parametrized) url to connect to + */ + String url(); + + /** Support for JSONP requires + * a callback from the server generated page to a function defined in the + * system. The name of such function is usually specified as a property + * (of possibly different names). By defining the jsonp attribute + * one turns on the JSONP + * transmission and specifies the name of the property. The property should + * also be used in the {@link #url()} attribute on appropriate place. + * + * @return name of a property to carry the name of JSONP + * callback function. + */ + String jsonp() default ""; +} diff -r 2fb3e929962f -r c75bd6823179 javaquery/api/src/main/resources/org/apidesign/bck2brwsr/htmlpage/knockout-2.2.1.js --- a/javaquery/api/src/main/resources/org/apidesign/bck2brwsr/htmlpage/knockout-2.2.1.js Wed Apr 03 13:43:22 2013 +0200 +++ b/javaquery/api/src/main/resources/org/apidesign/bck2brwsr/htmlpage/knockout-2.2.1.js Mon Apr 08 19:33:08 2013 +0200 @@ -2193,7 +2193,14 @@ else element[attrName] = attrValue; } else if (!toRemove) { - element.setAttribute(attrName, attrValue.toString()); + try { + element.setAttribute(attrName, attrValue.toString()); + } catch (err) { + // ignore for now + if (console) { + console.log("Can't set attribute " + attrName + " to " + attrValue + " error: " + err); + } + } } // Treat "name" specially - although you can think of it as an attribute, it also needs diff -r 2fb3e929962f -r c75bd6823179 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypesTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypesTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,52 @@ +/** + * Back 2 Browser Bytecode Translator + * Copyright (C) 2012 Jaroslav Tulach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. Look for COPYING file in the top folder. + * If not, see http://opensource.org/licenses/GPL-2.0. + */ +package org.apidesign.bck2brwsr.htmlpage; + +import org.apidesign.bck2brwsr.core.JavaScriptBody; +import org.apidesign.bck2brwsr.vmtest.BrwsrTest; +import org.apidesign.bck2brwsr.vmtest.VMTest; +import org.testng.annotations.Factory; + +/** + * + * @author Jaroslav Tulach + */ +public class ConvertTypesTest { + @JavaScriptBody(args = { }, body = "var json = new Object();" + + "json.firstName = 'son';\n" + + "json.lastName = 'dj';\n" + + "json.sex = 'MALE';\n" + + "return json;" + ) + private static native Object createJSON(); + + @BrwsrTest + public void testConvertToPeople() { + final Object o = createJSON(); + + Person p = new Person(o); + + assert "son".equals(p.getFirstName()) : "First name: " + p.getFirstName(); + assert "dj".equals(p.getLastName()) : "Last name: " + p.getLastName(); +// assert Sex.MALE.equals(p.getSex()) : "Sex: " + p.getSex(); + } + + @Factory public static Object[] create() { + return VMTest.create(ConvertTypesTest.class); + } +} \ No newline at end of file diff -r 2fb3e929962f -r c75bd6823179 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/JSONTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/JSONTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,325 @@ +/** + * Back 2 Browser Bytecode Translator + * Copyright (C) 2012 Jaroslav Tulach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. Look for COPYING file in the top folder. + * If not, see http://opensource.org/licenses/GPL-2.0. + */ +package org.apidesign.bck2brwsr.htmlpage; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import org.apidesign.bck2brwsr.htmlpage.api.OnReceive; +import org.apidesign.bck2brwsr.htmlpage.api.Page; +import org.apidesign.bck2brwsr.htmlpage.api.Property; +import org.apidesign.bck2brwsr.vmtest.BrwsrTest; +import org.apidesign.bck2brwsr.vmtest.Http; +import org.apidesign.bck2brwsr.vmtest.VMTest; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; +import org.testng.annotations.Test; +import static org.testng.Assert.*; +import org.testng.annotations.Factory; + +/** Need to verify that models produce reasonable JSON objects. + * + * @author Jaroslav Tulach + */ +@Page(xhtml = "Empty.html", className = "JSONik", properties = { + @Property(name = "fetched", type = PersonImpl.class), + @Property(name = "fetchedCount", type = int.class) +}) +public class JSONTest { + private JSONik js; + + @Test public void personToString() throws JSONException { + Person p = new Person(); + p.setSex(Sex.MALE); + p.setFirstName("Jarda"); + p.setLastName("Tulach"); + + JSONTokener t = new JSONTokener(p.toString()); + JSONObject o; + try { + o = new JSONObject(t); + } catch (JSONException ex) { + throw new AssertionError("Can't parse " + p.toString(), ex); + } + + Iterator it = o.sortedKeys(); + assertEquals(it.next(), "firstName"); + assertEquals(it.next(), "lastName"); + assertEquals(it.next(), "sex"); + + assertEquals(o.getString("firstName"), "Jarda"); + assertEquals(o.getString("lastName"), "Tulach"); + assertEquals(o.getString("sex"), "MALE"); + } + + @Test public void personWithWildCharactersAndNulls() throws JSONException { + Person p = new Person(); + p.setFirstName("'\"\n"); + p.setLastName("\t\r\u0002"); + + JSONTokener t = new JSONTokener(p.toString()); + JSONObject o; + try { + o = new JSONObject(t); + } catch (JSONException ex) { + throw new AssertionError("Can't parse " + p.toString(), ex); + } + + Iterator it = o.sortedKeys(); + assertEquals(it.next(), "firstName"); + assertEquals(it.next(), "lastName"); + assertEquals(it.next(), "sex"); + + assertEquals(o.getString("firstName"), p.getFirstName()); + assertEquals(o.getString("lastName"), p.getLastName()); + assertEquals(o.get("sex"), JSONObject.NULL); + } + + @Test public void personsInArray() throws JSONException { + Person p1 = new Person(); + p1.setFirstName("One"); + + Person p2 = new Person(); + p2.setFirstName("Two"); + + People arr = new People(); + arr.getInfo().add(p1); + arr.getInfo().add(p2); + arr.getNicknames().add("Prvn\u00ed k\u016f\u0148"); + final String n2 = "Druh\u00fd hlem\u00fd\u017e\u010f, star\u0161\u00ed"; + arr.getNicknames().add(n2); + arr.getAge().add(33); + arr.getAge().add(73); + + + final String json = arr.toString(); + + JSONTokener t = new JSONTokener(json); + JSONObject o; + try { + o = new JSONObject(t); + } catch (JSONException ex) { + throw new AssertionError("Can't parse " + json, ex); + } + + assertEquals(o.getJSONArray("info").getJSONObject(0).getString("firstName"), "One"); + assertEquals(o.getJSONArray("nicknames").getString(1), n2); + assertEquals(o.getJSONArray("age").getInt(1), 73); + } + + + @OnReceive(url="/{url}") + static void fetch(Person p, JSONik model) { + model.setFetched(p); + } + + @OnReceive(url="/{url}") + static void fetchArray(Person[] p, JSONik model) { + model.setFetchedCount(p.length); + model.setFetched(p[0]); + } + + @OnReceive(url="/{url}") + static void fetchPeople(People p, JSONik model) { + model.setFetchedCount(p.getInfo().size()); + model.setFetched(p.getInfo().get(0)); + } + + @OnReceive(url="/{url}") + static void fetchPeopleAge(People p, JSONik model) { + int sum = 0; + for (int a : p.getAge()) { + sum += a; + } + model.setFetchedCount(sum); + } + + @Http(@Http.Resource( + content = "{'firstName': 'Sitar', 'sex': 'MALE'}", + path="/person.json", + mimeType = "application/json" + )) + @BrwsrTest public void loadAndParseJSON() throws InterruptedException { + if (js == null) { + js = new JSONik(); + js.applyBindings(); + + js.fetch("person.json"); + } + + Person p = js.getFetched(); + if (p == null) { + throw new InterruptedException(); + } + + assert "Sitar".equals(p.getFirstName()) : "Expecting Sitar: " + p.getFirstName(); + // assert Sex.MALE.equals(p.getSex()) : "Expecting MALE: " + p.getSex(); + } + + @OnReceive(url="/{url}?callme={me}", jsonp = "me") + static void fetchViaJSONP(Person p, JSONik model) { + model.setFetched(p); + } + + @Http(@Http.Resource( + content = "$0({'firstName': 'Mitar', 'sex': 'MALE'})", + path="/person.json", + mimeType = "application/javascript", + parameters = { "callme" } + )) + @BrwsrTest public void loadAndParseJSONP() throws InterruptedException { + if (js == null) { + js = new JSONik(); + js.applyBindings(); + + js.fetchViaJSONP("person.json"); + } + + Person p = js.getFetched(); + if (p == null) { + throw new InterruptedException(); + } + + assert "Mitar".equals(p.getFirstName()) : "Unexpected: " + p.getFirstName(); + // assert Sex.MALE.equals(p.getSex()) : "Expecting MALE: " + p.getSex(); + } + + @Http(@Http.Resource( + content = "{'firstName': 'Sitar', 'sex': 'MALE'}", + path="/person.json", + mimeType = "application/json" + )) + @BrwsrTest public void loadAndParseJSONSentToArray() throws InterruptedException { + if (js == null) { + js = new JSONik(); + js.applyBindings(); + + js.fetchArray("person.json"); + } + + Person p = js.getFetched(); + if (p == null) { + throw new InterruptedException(); + } + + assert p != null : "We should get our person back: " + p; + assert "Sitar".equals(p.getFirstName()) : "Expecting Sitar: " + p.getFirstName(); +// assert Sex.MALE.equals(p.getSex()) : "Expecting MALE: " + p.getSex(); + } + + @Http(@Http.Resource( + content = "[{'firstName': 'Gitar', 'sex': 'FEMALE'}]", + path="/person.json", + mimeType = "application/json" + )) + @BrwsrTest public void loadAndParseJSONArraySingle() throws InterruptedException { + if (js == null) { + js = new JSONik(); + js.applyBindings(); + + js.fetch("person.json"); + } + + Person p = js.getFetched(); + if (p == null) { + throw new InterruptedException(); + } + + assert p != null : "We should get our person back: " + p; + assert "Gitar".equals(p.getFirstName()) : "Expecting Gitar: " + p.getFirstName(); +// assert Sex.MALE.equals(p.getSex()) : "Expecting MALE: " + p.getSex(); + } + + @Http(@Http.Resource( + content = "{'info':[{'firstName': 'Gitar', 'sex': 'FEMALE'}]}", + path="/people.json", + mimeType = "application/json" + )) + @BrwsrTest public void loadAndParseArrayInPeople() throws InterruptedException { + if (js == null) { + js = new JSONik(); + js.applyBindings(); + + js.fetchPeople("people.json"); + } + + if (0 == js.getFetchedCount()) { + throw new InterruptedException(); + } + + assert js.getFetchedCount() == 1 : "One person loaded: " + js.getFetchedCount(); + + Person p = js.getFetched(); + + assert p != null : "We should get our person back: " + p; + assert "Gitar".equals(p.getFirstName()) : "Expecting Gitar: " + p.getFirstName(); +// assert Sex.MALE.equals(p.getSex()) : "Expecting MALE: " + p.getSex(); + } + + @Http(@Http.Resource( + content = "{'age':[1, 2, 3]}", + path="/people.json", + mimeType = "application/json" + )) + @BrwsrTest public void loadAndParseArrayOfIntegers() throws InterruptedException { + if (js == null) { + js = new JSONik(); + js.applyBindings(); + + js.fetchPeopleAge("people.json"); + } + + if (0 == js.getFetchedCount()) { + throw new InterruptedException(); + } + + assert js.getFetchedCount() == 6 : "1 + 2 + 3 is " + js.getFetchedCount(); + } + + @Http(@Http.Resource( + content = "[{'firstName': 'Gitar', 'sex': 'FEMALE'}," + + "{'firstName': 'Peter', 'sex': 'MALE'}" + + "]", + path="/person.json", + mimeType = "application/json" + )) + @BrwsrTest public void loadAndParseJSONArray() throws InterruptedException { + if (js == null) { + js = new JSONik(); + js.applyBindings(); + js.fetchArray("person.json"); + } + + + Person p = js.getFetched(); + if (p == null) { + throw new InterruptedException(); + } + + assert js.getFetchedCount() == 2 : "We got two values: " + js.getFetchedCount(); + assert p != null : "We should get our person back: " + p; + assert "Gitar".equals(p.getFirstName()) : "Expecting Gitar: " + p.getFirstName(); +// assert Sex.MALE.equals(p.getSex()) : "Expecting MALE: " + p.getSex(); + } + + @Factory public static Object[] create() { + return VMTest.create(JSONTest.class); + } + +} diff -r 2fb3e929962f -r c75bd6823179 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/KnockoutTest.java --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/KnockoutTest.java Wed Apr 03 13:43:22 2013 +0200 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/KnockoutTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -27,7 +27,9 @@ import org.apidesign.bck2brwsr.vmtest.BrwsrTest; import org.apidesign.bck2brwsr.vmtest.HtmlFragment; import org.apidesign.bck2brwsr.vmtest.VMTest; +import static org.testng.Assert.assertEquals; import org.testng.annotations.Factory; +import org.testng.annotations.Test; /** * @@ -139,30 +141,70 @@ assert "changed".equals(txt) : "Expecting 'changed': " + txt; } + @ComputedProperty + static Person firstPerson(List people) { + return people.isEmpty() ? null : people.get(0); + } + + @HtmlFragment( + "

\n" + + " \n" + + "

\n" + ) + @BrwsrTest public void accessFirstPersonWithOnFunction() { + trasfertToFemale(); + } + @HtmlFragment( "
    \n" + "
  • \n" + "
\n" ) @BrwsrTest public void onPersonFunction() { + trasfertToFemale(); + } + + private void trasfertToFemale() { KnockoutModel m = new KnockoutModel(); - + final Person first = new Person(); first.setFirstName("first"); first.setSex(Sex.MALE); m.getPeople().add(first); - - + + m.applyBindings(); - + int cnt = countChildren("ul"); assert cnt == 1 : "One child, but was " + cnt; - - + + triggerChildClick("ul", 0); - + assert first.getSex() == Sex.FEMALE : "Transverted to female: " + first.getSex(); } + + @Test public void cloneModel() { + Person model = new Person(); + + model.setFirstName("first"); + Person snd = model.clone(); + snd.setFirstName("clone"); + assertEquals("first", model.getFirstName(), "Value has not changed"); + assertEquals("clone", snd.getFirstName(), "Value has changed in clone"); + } + + + @Test public void deepCopyOnClone() { + People model = new People(); + model.getNicknames().add("Jarda"); + assertEquals(model.getNicknames().size(), 1, "One element"); + People snd = model.clone(); + snd.getNicknames().clear(); + assertEquals(snd.getNicknames().size(), 0, "Clone is empty"); + assertEquals(model.getNicknames().size(), 1, "Still one element"); + } + @OnFunction static void call(KnockoutModel m, String data) { diff -r 2fb3e929962f -r c75bd6823179 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java Wed Apr 03 13:43:22 2013 +0200 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -24,6 +24,7 @@ import java.util.ListIterator; import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty; import org.apidesign.bck2brwsr.htmlpage.api.OnFunction; +import org.apidesign.bck2brwsr.htmlpage.api.OnPropertyChange; import org.apidesign.bck2brwsr.htmlpage.api.Page; import org.apidesign.bck2brwsr.htmlpage.api.Property; import static org.testng.Assert.*; @@ -40,7 +41,8 @@ @Property(name = "unrelated", type = long.class), @Property(name = "names", type = String.class, array = true), @Property(name = "values", type = int.class, array = true), - @Property(name = "people", type = PersonImpl.class, array = true) + @Property(name = "people", type = PersonImpl.class, array = true), + @Property(name = "changedProperty", type=String.class) }) public class ModelTest { private Modelik model; @@ -138,6 +140,9 @@ model.setValue(33); + // not interested in change of this property + my.mutated.remove("changedProperty"); + assertEquals(my.mutated.size(), 2, "Two properties changed: " + my.mutated); assertTrue(my.mutated.contains("powerValue"), "Power value is in there: " + my.mutated); assertTrue(my.mutated.contains("value"), "Simple value is in there: " + my.mutated); @@ -145,7 +150,11 @@ my.mutated.clear(); model.setUnrelated(44); - assertEquals(my.mutated.size(), 1, "One property changed"); + + + // not interested in change of this property + my.mutated.remove("changedProperty"); + assertEquals(my.mutated.size(), 1, "One property changed: " + my.mutated); assertTrue(my.mutated.contains("unrelated"), "Its name is unrelated"); } @@ -178,6 +187,34 @@ return value * value; } + @OnPropertyChange({ "powerValue", "unrelated" }) + static void aPropertyChanged(Modelik m, String name) { + m.setChangedProperty(name); + } + + @OnPropertyChange({ "values" }) + static void anArrayPropertyChanged(String name, Modelik m) { + m.setChangedProperty(name); + } + + @Test public void changeAnything() { + model.setCount(44); + assertNull(model.getChangedProperty(), "No observed value change"); + } + @Test public void changeValue() { + model.setValue(33); + assertEquals(model.getChangedProperty(), "powerValue", "power property changed"); + } + @Test public void changeUnrelated() { + model.setUnrelated(333); + assertEquals(model.getChangedProperty(), "unrelated", "unrelated changed"); + } + + @Test public void changeInArray() { + model.getValues().add(10); + assertEquals(model.getChangedProperty(), "values", "Something added into the array"); + } + @ComputedProperty static String notAllowedRead() { return "Not allowed callback: " + leakedModel.getUnrelated(); diff -r 2fb3e929962f -r c75bd6823179 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/PersonImpl.java --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/PersonImpl.java Wed Apr 03 13:43:22 2013 +0200 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/PersonImpl.java Mon Apr 08 19:33:08 2013 +0200 @@ -50,4 +50,11 @@ p.setSex(Sex.MALE); } } + + @Model(className = "People", properties = { + @Property(array = true, name = "info", type = PersonImpl.class), + @Property(array = true, name = "nicknames", type = String.class), + @Property(array = true, name = "age", type = int.class),}) + public class PeopleImpl { + } } diff -r 2fb3e929962f -r c75bd6823179 javaquery/demo-twitter/bck2brwsr-assembly.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/demo-twitter/bck2brwsr-assembly.xml Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,62 @@ + + + + + bck2brwsr + + zip + + public_html + + + false + runtime + lib + + *:jar + *:rt + + + + + + ${project.build.directory}/classes/org/apidesign/bck2brwsr/demo/twitter/ + + **/* + + + **/*.class + + / + + + + + ${project.build.directory}/${project.build.finalName}.jar + / + + + ${project.build.directory}/bck2brwsr.js + / + + + \ No newline at end of file diff -r 2fb3e929962f -r c75bd6823179 javaquery/demo-twitter/nb-configuration.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/demo-twitter/nb-configuration.xml Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,37 @@ + + + + + + + none + + diff -r 2fb3e929962f -r c75bd6823179 javaquery/demo-twitter/nbactions.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/demo-twitter/nbactions.xml Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,29 @@ + + + + + run + + process-classes + org.apidesign.bck2brwsr:mojo:0.6-SNAPSHOT:brwsr + + + diff -r 2fb3e929962f -r c75bd6823179 javaquery/demo-twitter/pom.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/demo-twitter/pom.xml Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,137 @@ + + + 4.0.0 + + javaquery + org.apidesign.bck2brwsr + 0.6-SNAPSHOT + + + org.apidesign.bck2brwsr + demo-twitter + 0.6-SNAPSHOT + jar + + Bck2Brwsr's Twttr + + Rewrite of knockoutjs example to use model written in Java and + execute using Bck2Brwsr virtual machine. + + + + + java.net + Java.net + https://maven.java.net/content/repositories/releases/ + + + + + netbeans + NetBeans + http://bits.netbeans.org/maven2/ + + + + + java.net + Java.net + https://maven.java.net/content/repositories/releases/ + + + + + + + UTF-8 + FULL + + + + + org.apidesign.bck2brwsr + mojo + 0.6-SNAPSHOT + + + + brwsr + j2js + + + + + org/apidesign/bck2brwsr/demo/twitter/index.html + ${project.build.directory}/bck2brwsr.js + ${bck2brwsr.obfuscationlevel} + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + 1.7 + 1.7 + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + true + lib/ + + + + + + maven-assembly-plugin + 2.4 + + + distro-assembly + package + + single + + + + bck2brwsr-assembly.xml + + + + + + + + + + + org.apidesign.bck2brwsr + emul + 0.6-SNAPSHOT + rt + + + org.apidesign.bck2brwsr + javaquery.api + 0.6-SNAPSHOT + + + org.testng + testng + 6.5.2 + test + + + org.apidesign.bck2brwsr + vmtest + 0.6-SNAPSHOT + test + + + diff -r 2fb3e929962f -r c75bd6823179 javaquery/demo-twitter/src/main/java/org/apidesign/bck2brwsr/demo/twitter/TwitterClient.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/demo-twitter/src/main/java/org/apidesign/bck2brwsr/demo/twitter/TwitterClient.java Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,194 @@ +/** + * Back 2 Browser Bytecode Translator + * Copyright (C) 2012 Jaroslav Tulach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. Look for COPYING file in the top folder. + * If not, see http://opensource.org/licenses/GPL-2.0. + */ +package org.apidesign.bck2brwsr.demo.twitter; + +import java.util.Arrays; +import java.util.List; +import org.apidesign.bck2brwsr.htmlpage.api.*; +import org.apidesign.bck2brwsr.htmlpage.api.Page; +import org.apidesign.bck2brwsr.htmlpage.api.Property; +import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty; + +/** Controller class for access to Twitter. + * + * @author Jaroslav Tulach + */ +@Page(xhtml="index.html", className="TwitterModel", properties={ + @Property(name="savedLists", type=TwitterClient.Twttrs.class, array = true), + @Property(name="activeTweetersName", type=String.class), + @Property(name="activeTweeters", type=String.class, array = true), + @Property(name="userNameToAdd", type=String.class), + @Property(name="currentTweets", type=TwitterClient.Twt.class, array = true) +}) +public class TwitterClient { + @Model(className = "Tweeters", properties = { + @Property(name="name", type = String.class), + @Property(name="userNames", type = String.class, array = true) + }) + static class Twttrs { + } + @Model(className = "Tweet", properties = { + @Property(name = "from_user", type = String.class), + @Property(name = "from_user_id", type = int.class), + @Property(name = "profile_image_url", type = String.class), + @Property(name = "text", type = String.class), + @Property(name = "created_at", type = String.class), + }) + static final class Twt { + @ComputedProperty static String html(String text) { + StringBuilder sb = new StringBuilder(320); + for (int pos = 0;;) { + int http = text.indexOf("http", pos); + if (http == -1) { + sb.append(text.substring(pos)); + return sb.toString(); + } + int spc = text.indexOf(' ', http); + if (spc == -1) { + spc = text.length(); + } + sb.append(text.substring(pos, http)); + String url = text.substring(http, spc); + sb.append("").append(url).append(""); + pos = spc; + } + } + + @ComputedProperty static String userUrl(String from_user) { + return "http://twitter.com/" + from_user; + } + } + @Model(className = "TwitterQuery", properties = { + @Property(array = true, name = "results", type = Twt.class) + }) + public static final class TwttrQr { + } + + @OnReceive(url="{root}/search.json?{query}&callback={me}", jsonp="me") + static void queryTweets(TwitterModel page, TwitterQuery q) { + page.getCurrentTweets().clear(); + page.getCurrentTweets().addAll(q.getResults()); + } + + @OnPropertyChange("activeTweetersName") + static void changeTweetersList(TwitterModel model) { + Tweeters people = findByName(model.getSavedLists(), model.getActiveTweetersName()); + model.getActiveTweeters().clear(); + model.getActiveTweeters().addAll(people.getUserNames()); + } + + @OnPropertyChange({ "activeTweeters", "activeTweetersCount" }) + static void refreshTweets(TwitterModel model) { + StringBuilder sb = new StringBuilder(); + sb.append("rpp=25&q="); + String sep = ""; + for (String p : model.getActiveTweeters()) { + sb.append(sep); + sb.append("from:"); + sb.append(p); + sep = " OR "; + } + model.queryTweets("http://search.twitter.com", sb.toString()); + } + + static { + final TwitterModel model = new TwitterModel(); + final List svdLst = model.getSavedLists(); + svdLst.add(newTweeters("API Design", "JaroslavTulach")); + svdLst.add(newTweeters("Celebrities", "JohnCleese", "MCHammer", "StephenFry", "algore", "StevenSanderson")); + svdLst.add(newTweeters("Microsoft people", "BillGates", "shanselman", "ScottGu")); + svdLst.add(newTweeters("NetBeans", "GeertjanW","monacotoni", "NetBeans", "petrjiricka")); + svdLst.add(newTweeters("Tech pundits", "Scobleizer", "LeoLaporte", "techcrunch", "BoingBoing", "timoreilly", "codinghorror")); + + model.setActiveTweetersName("NetBeans"); + + model.applyBindings(); + } + + @ComputedProperty + static boolean hasUnsavedChanges(List activeTweeters, List savedLists, String activeTweetersName) { + Tweeters tw = findByName(savedLists, activeTweetersName); + if (activeTweeters == null) { + return false; + } + return !tw.getUserNames().equals(activeTweeters); + } + + @ComputedProperty + static int activeTweetersCount(List activeTweeters) { + return activeTweeters.size(); + } + + @ComputedProperty + static boolean userNameToAddIsValid( + String userNameToAdd, String activeTweetersName, List savedLists, List activeTweeters + ) { + return userNameToAdd != null && + userNameToAdd.matches("[a-zA-Z0-9_]{1,15}") && + !activeTweeters.contains(userNameToAdd); + } + + @OnFunction + static void deleteList(TwitterModel model) { + final List sl = model.getSavedLists(); + sl.remove(findByName(sl, model.getActiveTweetersName())); + if (sl.isEmpty()) { + final Tweeters t = new Tweeters(); + t.setName("New"); + sl.add(t); + } + model.setActiveTweetersName(sl.get(0).getName()); + } + + @OnFunction + static void saveChanges(TwitterModel model) { + Tweeters t = findByName(model.getSavedLists(), model.getActiveTweetersName()); + int indx = model.getSavedLists().indexOf(t); + if (indx != -1) { + t.setName(model.getActiveTweetersName()); + t.getUserNames().clear(); + t.getUserNames().addAll(model.getActiveTweeters()); + } + } + + @OnFunction + static void addUser(TwitterModel model) { + String n = model.getUserNameToAdd(); + model.getActiveTweeters().add(n); + } + @OnFunction + static void removeUser(String data, TwitterModel model) { + model.getActiveTweeters().remove(data); + } + + private static Tweeters findByName(List list, String name) { + for (Tweeters l : list) { + if (l.getName() != null && l.getName().equals(name)) { + return l; + } + } + return list.isEmpty() ? new Tweeters() : list.get(0); + } + + private static Tweeters newTweeters(String listName, String... userNames) { + Tweeters t = new Tweeters(); + t.setName(listName); + t.getUserNames().addAll(Arrays.asList(userNames)); + return t; + } +} diff -r 2fb3e929962f -r c75bd6823179 javaquery/demo-twitter/src/main/resources/org/apidesign/bck2brwsr/demo/twitter/index.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/demo-twitter/src/main/resources/org/apidesign/bck2brwsr/demo/twitter/index.html Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,99 @@ + + + + + + + + + Bck2Brwsr's Twitter + + + + + + + +

Bck2Brwsr's Twitter

+ +

+ This code based on original knockout.js Twitter example and + uses almost unmodified HTML code. It just changes the model. It + is written in Java language and it is executed using Bck2Brwsr + virtual machine. The Java source code has about 190 lines and is available + here + - in fact it may even be more dense than the original JavaScript model. +

+ +
+
+
+ + + +
+ +

Currently viewing user(s):

+
+
    +
  • + +
    +
  • +
+
+ +
+ + + +
+
+
+
Loading...
+ + + + + +
+ + +
+
+
+
+ + + + + + + diff -r 2fb3e929962f -r c75bd6823179 javaquery/demo-twitter/src/main/resources/org/apidesign/bck2brwsr/demo/twitter/twitterExample.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/demo-twitter/src/main/resources/org/apidesign/bck2brwsr/demo/twitter/twitterExample.css Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,50 @@ +/** + * Back 2 Browser Bytecode Translator + * Copyright (C) 2012 Jaroslav Tulach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. Look for COPYING file in the top folder. + * If not, see http://opensource.org/licenses/GPL-2.0. + */ + +/* + Copied from knockout.js Twitter example: + http://knockoutjs.com/examples/twitter.html +*/ + +.configuration, .tweets, .tweets td { font-family: Verdana; font-size: 13px; } +.configuration { background-color: #DEDEDE; border: 2px solid gray; float:left; height: 40em; width: 40%; padding: 0.5em; border-right-width:0; } +.tweets { width: 55%; border: 2px solid gray; height: 40em; overflow: scroll; overflow-x: hidden; background-color: Black; color: White; padding: 0.5em; position: relative; } +.tweets table { border-width: 0;} +.tweets tr { vertical-align: top; } +.tweets td { padding: 0.4em 0.3em 1em 0.4em; border-width: 0; } +.tweets img { width: 4em; } +.tweetInfo { color: Gray; font-size: 0.9em; } +.twitterUser { color: #77AAFF; text-decoration: none; font-size: 1.1em; font-weight: bold; } +input.invalid { border: 1px solid red !important; background-color: #FFAAAA !important; } + +.listChooser select, .listChooser button { vertical-align:top; } +.listChooser select { width: 60%; font-size:1.2em; height:1.4em; } +.listChooser button { width: 19%; height:1.68em; float:right; } + +.currentUsers { height: 28em; overflow-y: auto; overflow-x: hidden; } +.currentUsers button { float: right; height: 2.5em; margin: 0.1em; padding-left: 1em; padding-right: 1em; } +.currentUsers ul, .configuration li { list-style: none; margin: 0; padding: 0 } +.currentUsers li { height: 2.4em; font-size: 1.2em; background-color: #A7D0E3; border: 1px solid gray; margin-bottom: 0.3em; -webkit-border-radius: 5px; -moz-border-radius: 5px; -webkit-box-shadow: 0 0.2em 0.5em gray; -moz-box-shadow: 0 0.2em 0.5em gray; } +.currentUsers li div { padding: 0.6em; } +.currentUsers li:hover { background-color: #EEC; } + +.configuration form label { width: 25%; display: inline-block; text-align:right; overflow: hidden; } +.configuration form input { width:40%; font-size: 1.3em; border:1px solid silver; background-color: White; padding: 0.1em; } +.configuration form button { width: 20%; margin-left: 0.3em; height: 2em; } + +.loadingIndicator { position: absolute; top: 0.1em; left: 0.1em; font: 0.8em Arial; background-color: #229; color: White; padding: 0.2em 0.5em 0.2em 0.5em; display: none; } diff -r 2fb3e929962f -r c75bd6823179 javaquery/demo-twitter/src/test/java/org/apidesign/bck2brwsr/demo/twitter/TwitterClientTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/demo-twitter/src/test/java/org/apidesign/bck2brwsr/demo/twitter/TwitterClientTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,67 @@ +/** + * Back 2 Browser Bytecode Translator + * Copyright (C) 2012 Jaroslav Tulach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. Look for COPYING file in the top folder. + * If not, see http://opensource.org/licenses/GPL-2.0. + */ +package org.apidesign.bck2brwsr.demo.twitter; + +import java.util.List; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** We can unit test the TwitterModel smoothly. + * + * @author Jaroslav Tulach + */ +public class TwitterClientTest { + private TwitterModel model; + + + @BeforeMethod + public void initModel() { + model = new TwitterModel().applyBindings(); + } + + @Test public void testIsValidToAdd() { + model.setUserNameToAdd("Joe"); + Tweeters t = new Tweeters(); + t.setName("test"); + model.getSavedLists().add(t); + model.setActiveTweetersName("test"); + + assertTrue(model.isUserNameToAddIsValid(), "Joe is OK"); + TwitterClient.addUser(model); + assertFalse(model.isUserNameToAddIsValid(), "Can't add Joe for the 2nd time"); + assertEquals(t.getUserNames().size(), 0, "Original tweeters list remains empty"); + + List mod = model.getActiveTweeters(); + assertTrue(model.isHasUnsavedChanges(), "We have modifications"); + assertEquals(mod.size(), 1, "One element in the list"); + assertEquals(mod.get(0), "Joe", "Its name is Joe"); + + assertSame(model.getActiveTweeters(), mod, "Editing list is the modified one"); + + TwitterClient.saveChanges(model); + assertFalse(model.isHasUnsavedChanges(), "Does not have anything to save"); + + assertSame(model.getActiveTweeters(), mod, "Still editing the old modified one"); + } + + @Test public void httpAtTheEnd() { + String res = TwitterClient.Twt.html("Ahoj http://kuk"); + assertEquals(res, "Ahoj http://kuk"); + } +} diff -r 2fb3e929962f -r c75bd6823179 javaquery/demo-twitter/src/test/java/org/apidesign/bck2brwsr/demo/twitter/TwitterProtocolTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/demo-twitter/src/test/java/org/apidesign/bck2brwsr/demo/twitter/TwitterProtocolTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,94 @@ +/** + * Back 2 Browser Bytecode Translator + * Copyright (C) 2012 Jaroslav Tulach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. Look for COPYING file in the top folder. + * If not, see http://opensource.org/licenses/GPL-2.0. + */ +package org.apidesign.bck2brwsr.demo.twitter; + +import org.apidesign.bck2brwsr.vmtest.BrwsrTest; +import org.apidesign.bck2brwsr.vmtest.Http; +import org.apidesign.bck2brwsr.vmtest.VMTest; +import org.testng.annotations.Factory; + +/** + * + * @author Jaroslav Tulach + */ +public class TwitterProtocolTest { + private TwitterModel page; + @Http(@Http.Resource( + path = "/search.json", + mimeType = "application/json", + parameters = {"callback"}, + content = "$0({\"completed_in\":0.04,\"max_id\":320055706885689344,\"max_id_str\"" + + ":\"320055706885689344\",\"page\":1,\"query\":\"from%3AJaroslavTulach\",\"refresh_url\":" + + "\"?since_id=320055706885689344&q=from%3AJaroslavTulach\"," + + "\"results\":[{\"created_at\":\"Fri, 05 Apr 2013 06:10:01 +0000\"," + + "\"from_user\":\"JaroslavTulach\",\"from_user_id\":420944648,\"from_user_id_str\":" + + "\"420944648\",\"from_user_name\":\"Jaroslav Tulach\",\"geo\":null,\"id\":320055706885689344," + + "\"id_str\":\"320055706885689344\",\"iso_language_code\":\"en\",\"metadata\":{\"result_type\":" + + "\"recent\"},\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/1656828312\\/jst_normal.gif\"," + + "\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/1656828312\\/jst_normal.gif\"," + + "\"source\":\"<a href="http:\\/\\/twitter.com\\/">web<\\/a>\",\"text\":" + + "\"@tom_enebo Amzng! Not that I would like #ruby, but I am really glad you guys stabilized the plugin + " + + "made it work in #netbeans 7.3! Gd wrk.\",\"to_user\":\"tom_enebo\",\"to_user_id\":14498747," + + "\"to_user_id_str\":\"14498747\",\"to_user_name\":\"tom_enebo\",\"in_reply_to_status_id\":319832359509839872," + + "\"in_reply_to_status_id_str\":\"319832359509839872\"},{\"created_at\":\"Thu, 04 Apr 2013 07:33:06 +0000\"," + + "\"from_user\":\"JaroslavTulach\",\"from_user_id\":420944648,\"from_user_id_str\":" + + "\"420944648\",\"from_user_name\":\"Jaroslav Tulach\",\"geo\":null,\"id\":319714227088678913," + + "\"id_str\":\"319714227088678913\",\"iso_language_code\":\"en\",\"metadata\":{\"result_type\":" + + "\"recent\"},\"profile_image_url\":\"http:\\/\\/a0.twimg.com\\/profile_images\\/1656828312\\/jst_normal.gif\"," + + "\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/1656828312\\/jst_normal.gif\"," + + "\"source\":\"<a href="http:\\/\\/twitter.com\\/">web<\\/a>\",\"text\":" + + "\"RT @drkrab: At #erlangfactory @joerl: Frameworks grow in complexity until nobody can use them.\"}," + + "{\"created_at\":\"Tue, 02 Apr 2013 07:44:34 +0000\",\"from_user\":\"JaroslavTulach\"," + + "\"from_user_id\":420944648,\"from_user_id_str\":\"420944648\",\"from_user_name\":\"Jaroslav Tulach\"," + + "\"geo\":null,\"id\":318992336145248256,\"id_str\":\"318992336145248256\",\"iso_language_code\":\"en\"," + + "\"metadata\":{\"result_type\":\"recent\"},\"profile_image_url\":" + + "\"http:\\/\\/a0.twimg.com\\/profile_images\\/1656828312\\/jst_normal.gif\"," + + "\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/1656828312\\/jst_normal.gif\"," + + "\"source\":\"<a href="http:\\/\\/twitter.com\\/">web<\\/a>\",\"text\":" + + "\"Twitter renamed to twttr http:\\/\\/t.co\\/tqaN4T1xlZ - good, I don't have to rename #bck2brwsr!\"}," + + "{\"created_at\":\"Sun, 31 Mar 2013 03:52:04 +0000\",\"from_user\":\"JaroslavTulach\",\"from_user_id\":420944648," + + "\"from_user_id_str\":\"420944648\",\"from_user_name\":\"Jaroslav Tulach\",\"geo\":null," + + "\"id\":318209051223789568,\"id_str\":\"318209051223789568\",\"iso_language_code\":\"en\",\"metadata\":" + + "{\"result_type\":\"recent\"},\"profile_image_url\":" + + "\"http:\\/\\/a0.twimg.com\\/profile_images\\/1656828312\\/jst_normal.gif\"," + + "\"profile_image_url_https\":\"https:\\/\\/si0.twimg.com\\/profile_images\\/1656828312\\/jst_normal.gif\"," + + "\"source\":\"<a href="http:\\/\\/twitter.com\\/">web<\\/a>\",\"text\":" + + "\"Math proofs without words. Ingenious: http:\\/\\/t.co\\/sz7yVbfpGw\"}],\"results_per_page\":100," + + "\"since_id\":0,\"since_id_str\":\"0\"})" + )) + @BrwsrTest public void readFromTwttr() throws InterruptedException { + if (page == null) { + page = new TwitterModel(); + page.applyBindings(); + page.queryTweets("", "q=xyz"); + } + + if (page.getCurrentTweets().isEmpty()) { + throw new InterruptedException(); + } + + assert 4 == page.getCurrentTweets().size() : "Four tweets: " + page.getCurrentTweets(); + + String firstDate = page.getCurrentTweets().get(0).getCreated_at(); + assert "Fri, 05 Apr 2013 06:10:01 +0000".equals(firstDate) : "Date is OK: " + firstDate; + } + + @Factory public static Object[] create() { + return VMTest.create(TwitterProtocolTest.class); + } +} diff -r 2fb3e929962f -r c75bd6823179 javaquery/pom.xml --- a/javaquery/pom.xml Wed Apr 03 13:43:22 2013 +0200 +++ b/javaquery/pom.xml Mon Apr 08 19:33:08 2013 +0200 @@ -15,5 +15,6 @@ api demo-calculator demo-calculator-dynamic - - + demo-twitter + + \ No newline at end of file diff -r 2fb3e929962f -r c75bd6823179 rt/emul/mini/src/main/java/java/lang/Class.java --- a/rt/emul/mini/src/main/java/java/lang/Class.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/emul/mini/src/main/java/java/lang/Class.java Mon Apr 08 19:33:08 2013 +0200 @@ -1252,6 +1252,7 @@ } @JavaScriptBody(args = { "sig" }, body = + "if (!sig) sig = '[Ljava/lang/Object;';\n" + "var c = Array[sig];\n" + "if (c) return c;\n" + "c = vm.java_lang_Class(true);\n" + diff -r 2fb3e929962f -r c75bd6823179 rt/emul/mini/src/main/java/java/lang/String.java --- a/rt/emul/mini/src/main/java/java/lang/String.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/emul/mini/src/main/java/java/lang/String.java Mon Apr 08 19:33:08 2013 +0200 @@ -2220,9 +2220,19 @@ * replacement is null. * @since 1.5 */ - public String replace(CharSequence target, CharSequence replacement) { - throw new UnsupportedOperationException("This one should be supported, but without dep on rest of regexp"); - } + @JavaScriptBody(args = { "target", "replacement" }, body = + "var s = this.toString();\n" + + "target = target.toString();\n" + + "replacement = replacement.toString();\n" + + "for (;;) {\n" + + " var ret = s.replace(target, replacement);\n" + + " if (ret === s) {\n" + + " return ret;\n" + + " }\n" + + " s = ret;\n" + + "}" + ) + public native String replace(CharSequence target, CharSequence replacement); /** * Splits this string around matches of the given diff -r 2fb3e929962f -r c75bd6823179 rt/emul/mini/src/main/java/java/lang/reflect/Method.java --- a/rt/emul/mini/src/main/java/java/lang/reflect/Method.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/emul/mini/src/main/java/java/lang/reflect/Method.java Mon Apr 08 19:33:08 2013 +0200 @@ -501,8 +501,8 @@ throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { - final boolean isStatic = (getModifiers() & Modifier.STATIC) == 0; - if (isStatic && obj == null) { + final boolean nonStatic = (getModifiers() & Modifier.STATIC) == 0; + if (nonStatic && obj == null) { throw new NullPointerException(); } Class[] types = getParameterTypes(); @@ -517,7 +517,7 @@ } } } - Object res = invoke0(isStatic, this, obj, args); + Object res = invokeTry(nonStatic, this, obj, args); if (getReturnType().isPrimitive()) { res = fromPrimitive(getReturnType(), res); } @@ -536,6 +536,15 @@ + "return method._data().apply(self, p);\n" ) private static native Object invoke0(boolean isStatic, Method m, Object self, Object[] args); + + private static Object invokeTry(boolean isStatic, Method m, Object self, Object[] args) + throws InvocationTargetException { + try { + return invoke0(isStatic, m, self, args); + } catch (Throwable ex) { + throw new InvocationTargetException(ex, ex.getMessage()); + } + } static Object fromPrimitive(Class type, Object o) { if (type == Integer.TYPE) { diff -r 2fb3e929962f -r c75bd6823179 rt/launcher/src/main/java/org/apidesign/bck2brwsr/launcher/Bck2BrwsrLauncher.java --- a/rt/launcher/src/main/java/org/apidesign/bck2brwsr/launcher/Bck2BrwsrLauncher.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/launcher/src/main/java/org/apidesign/bck2brwsr/launcher/Bck2BrwsrLauncher.java Mon Apr 08 19:33:08 2013 +0200 @@ -98,9 +98,9 @@ } HttpServer s = initServer(".", true); int last = startpage.lastIndexOf('/'); + String prefix = startpage.substring(0, last); String simpleName = startpage.substring(last); - s.getServerConfiguration().addHttpHandler(new Page(resources, startpage), simpleName); - s.getServerConfiguration().addHttpHandler(new Page(resources, null), "/"); + s.getServerConfiguration().addHttpHandler(new SubTree(resources, prefix), "/"); try { launchServerAndBrwsr(s, simpleName); } catch (URISyntaxException | InterruptedException ex) { @@ -177,7 +177,16 @@ if (r.httpPath.equals(request.getRequestURI())) { LOG.log(Level.INFO, "Serving HttpResource for {0}", request.getRequestURI()); response.setContentType(r.httpType); - copyStream(r.httpContent, response.getOutputStream(), null); + r.httpContent.reset(); + String[] params = null; + if (r.parameters.length != 0) { + params = new String[r.parameters.length]; + for (int i = 0; i < r.parameters.length; i++) { + params[i] = request.getParameter(r.parameters[i]); + } + } + + copyStream(r.httpContent, response.getOutputStream(), null, params); } } } @@ -315,7 +324,7 @@ } if (ch == '$' && params.length > 0) { int cnt = is.read() - '0'; - if (cnt == 'U' - '0') { + if (baseURL != null && cnt == 'U' - '0') { os.write(baseURL.getBytes("UTF-8")); } else { if (cnt >= 0 && cnt < params.length) { @@ -454,7 +463,7 @@ } private static class Page extends HttpHandler { - private final String resource; + final String resource; private final String[] args; private final Res res; @@ -466,10 +475,7 @@ @Override public void service(Request request, Response response) throws Exception { - String r = resource; - if (r == null) { - r = request.getHttpHandlerPath(); - } + String r = computePage(request); if (r.startsWith("/")) { r = r.substring(1); } @@ -493,6 +499,28 @@ response.setStatus(404); } } + + protected String computePage(Request request) { + String r = resource; + if (r == null) { + r = request.getHttpHandlerPath(); + } + return r; + } + } + + private static class SubTree extends Page { + + public SubTree(Res res, String resource, String... args) { + super(res, resource, args); + } + + @Override + protected String computePage(Request request) { + return resource + request.getHttpHandlerPath(); + } + + } private static class VM extends HttpHandler { diff -r 2fb3e929962f -r c75bd6823179 rt/launcher/src/main/java/org/apidesign/bck2brwsr/launcher/InvocationContext.java --- a/rt/launcher/src/main/java/org/apidesign/bck2brwsr/launcher/InvocationContext.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/launcher/src/main/java/org/apidesign/bck2brwsr/launcher/InvocationContext.java Mon Apr 08 19:33:08 2013 +0200 @@ -55,11 +55,11 @@ /** HTTP resource to be available during execution. An invocation may * perform an HTTP query and obtain a resource relative to the page. */ - public void addHttpResource(String relativePath, String mimeType, InputStream content) { - if (relativePath == null || mimeType == null || content == null) { + public void addHttpResource(String relativePath, String mimeType, String[] parameters, InputStream content) { + if (relativePath == null || mimeType == null || content == null || parameters == null) { throw new NullPointerException(); } - resources.add(new Resource(content, mimeType, relativePath)); + resources.add(new Resource(content, mimeType, relativePath, parameters)); } /** Invokes the associated method. @@ -100,11 +100,16 @@ final InputStream httpContent; final String httpType; final String httpPath; + final String[] parameters; - Resource(InputStream httpContent, String httpType, String httpPath) { + Resource(InputStream httpContent, String httpType, String httpPath, + String[] parameters + ) { + httpContent.mark(Integer.MAX_VALUE); this.httpContent = httpContent; this.httpType = httpType; this.httpPath = httpPath; + this.parameters = parameters; } } } diff -r 2fb3e929962f -r c75bd6823179 rt/launcher/src/main/java/org/apidesign/bck2brwsr/launcher/impl/Console.java --- a/rt/launcher/src/main/java/org/apidesign/bck2brwsr/launcher/impl/Console.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/launcher/src/main/java/org/apidesign/bck2brwsr/launcher/impl/Console.java Mon Apr 08 19:33:08 2013 +0200 @@ -113,13 +113,6 @@ ) private static native void beginTest(String test, Case c, Object[] arr); - public static void execute() throws Exception { - String clazz = (String) getAttr("clazz", "value"); - String method = (String) getAttr("method", "value"); - Object res = invokeMethod(clazz, method); - setAttr("bck2brwsr.result", "value", res); - } - @JavaScriptBody(args = { "url", "callback", "arr" }, body = "" + "var request = new XMLHttpRequest();\n" + "request.open('GET', url, true);\n" @@ -141,39 +134,53 @@ private static class Request implements Runnable { private final String[] arr = { null }; private final String url; + private Case c; + private int retries; private Request(String url) throws IOException { this.url = url; loadText(url, this, arr); } + private Request(String url, String u) throws IOException { + this.url = url; + loadText(u, this, arr); + } @Override public void run() { try { - String data = arr[0]; - log("\nGot \"" + data + "\""); + if (c == null) { + String data = arr[0]; + + if (data == null) { + log("Some error exiting"); + closeWindow(); + return; + } + + if (data.isEmpty()) { + log("No data, exiting"); + closeWindow(); + return; + } + + c = Case.parseData(data); + beginTest(c); + log("Got \"" + data + "\""); + } else { + log("Processing \"" + arr[0] + "\" for " + retries + " time"); + } + Object result = retries++ >= 10 ? "java.lang.InterruptedException:timeout" : c.runTest(); + finishTest(c, result); - if (data == null) { - log("Some error exiting"); - closeWindow(); + String u = url + "?request=" + c.getRequestId() + "&result=" + result; + new Request(url, u); + } catch (Exception ex) { + if (ex instanceof InterruptedException) { + log("Re-scheduling in 100ms"); + schedule(this, 100); return; } - - if (data.isEmpty()) { - log("No data, exiting"); - closeWindow(); - return; - } - - Case c = Case.parseData(data); - beginTest(c); - Object result = c.runTest(); - finishTest(c, result); - String u = url + "?request=" + c.getRequestId() + "&result=" + result; - - loadText(u, this, arr); - - } catch (Exception ex) { log(ex.getClass().getName() + ":" + ex.getMessage()); } } @@ -199,8 +206,10 @@ return sb.toString(); } - static String invoke(String clazz, String method) throws ClassNotFoundException, InvocationTargetException, IllegalAccessException, InstantiationException { - final Object r = invokeMethod(clazz, method); + static String invoke(String clazz, String method) throws + ClassNotFoundException, InvocationTargetException, IllegalAccessException, + InstantiationException, InterruptedException { + final Object r = new Case(null).invokeMethod(clazz, method); return r == null ? "null" : r.toString().toString(); } @@ -235,40 +244,17 @@ } } - private static Object invokeMethod(String clazz, String method) - throws ClassNotFoundException, InvocationTargetException, - SecurityException, IllegalAccessException, IllegalArgumentException, - InstantiationException { - Method found = null; - Class c = Class.forName(clazz); - for (Method m : c.getMethods()) { - if (m.getName().equals(method)) { - found = m; - } - } - Object res; - if (found != null) { - try { - if ((found.getModifiers() & Modifier.STATIC) != 0) { - res = found.invoke(null); - } else { - res = found.invoke(c.newInstance()); - } - } catch (Throwable ex) { - res = ex.getClass().getName() + ":" + ex.getMessage(); - } - } else { - res = "Can't find method " + method + " in " + clazz; - } - return res; - } - @JavaScriptBody(args = {}, body = "vm.desiredAssertionStatus = true;") private static void turnAssetionStatusOn() { } + + @JavaScriptBody(args = {"r", "time"}, body = + "return window.setTimeout(function() { r.run__V(); }, time);") + private static native Object schedule(Runnable r, int time); private static final class Case { private final Object data; + private Object inst; private Case(Object data) { this.data = data; @@ -305,7 +291,9 @@ } } - private Object runTest() throws IllegalAccessException, IllegalArgumentException, ClassNotFoundException, UnsupportedEncodingException, InvocationTargetException, InstantiationException, SecurityException { + private Object runTest() throws IllegalAccessException, + IllegalArgumentException, ClassNotFoundException, UnsupportedEncodingException, + InvocationTargetException, InstantiationException, InterruptedException { if (this.getHtmlFragment() != null) { setAttr("bck2brwsr.fragment", "innerHTML", this.getHtmlFragment()); } @@ -317,6 +305,43 @@ log("Sending back: ...?request=" + this.getRequestId() + "&result=" + result); return result; } + + private Object invokeMethod(String clazz, String method) + throws ClassNotFoundException, InvocationTargetException, + InterruptedException, IllegalAccessException, IllegalArgumentException, + InstantiationException { + Method found = null; + Class c = Class.forName(clazz); + for (Method m : c.getMethods()) { + if (m.getName().equals(method)) { + found = m; + } + } + Object res; + if (found != null) { + try { + if ((found.getModifiers() & Modifier.STATIC) != 0) { + res = found.invoke(null); + } else { + if (inst == null) { + inst = c.newInstance(); + } + res = found.invoke(inst); + } + } catch (Throwable ex) { + if (ex instanceof InvocationTargetException) { + ex = ((InvocationTargetException) ex).getTargetException(); + } + if (ex instanceof InterruptedException) { + throw (InterruptedException)ex; + } + res = ex.getClass().getName() + ":" + ex.getMessage(); + } + } else { + res = "Can't find method " + method + " in " + clazz; + } + return res; + } @JavaScriptBody(args = "s", body = "return eval('(' + s + ')');") private static native Object toJSON(String s); diff -r 2fb3e929962f -r c75bd6823179 rt/vm/src/test/java/org/apidesign/vm4brwsr/StringSample.java --- a/rt/vm/src/test/java/org/apidesign/vm4brwsr/StringSample.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/vm/src/test/java/org/apidesign/vm4brwsr/StringSample.java Mon Apr 08 19:33:08 2013 +0200 @@ -18,6 +18,7 @@ package org.apidesign.vm4brwsr; import java.io.UnsupportedEncodingException; +import org.apidesign.bck2brwsr.core.JavaScriptBody; /** * @@ -129,4 +130,20 @@ public String toString() { return HELLO + cnt; } + + @JavaScriptBody(args = {}, body = "return [1, 2];") + private static native Object crtarr(); + @JavaScriptBody(args = { "o" }, body = "return o.toString();") + private static native String toStrng(Object o); + + public static String toStringArray(boolean fakeArr, boolean toString) { + final Object arr = fakeArr ? crtarr() : new Object[2]; + final String whole = toString ? arr.toString() : toStrng(arr); + int zav = whole.indexOf('@'); + if (zav <= 0) { + zav = whole.length(); + } + return whole.substring(0, zav).toString().toString(); + } + } diff -r 2fb3e929962f -r c75bd6823179 rt/vm/src/test/java/org/apidesign/vm4brwsr/StringTest.java --- a/rt/vm/src/test/java/org/apidesign/vm4brwsr/StringTest.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/vm/src/test/java/org/apidesign/vm4brwsr/StringTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -194,6 +194,34 @@ } + @Test public void toStringOnJSArray() throws Exception { + String exp = StringSample.toStringArray(false, true); + + assertExec( + "Treated as Java Object array", + StringSample.class, "toStringArray__Ljava_lang_String_2ZZ", + exp, true, true + ); + } + + @Test public void toStringOnRealArray() throws Exception { + String exp = StringSample.toStringArray(false, true); + + assertExec( + "Is Java Object array", + StringSample.class, "toStringArray__Ljava_lang_String_2ZZ", + exp, false, true + ); + } + + @Test public void valueOfOnJSArray() throws Exception { + assertExec( + "Treated as classical JavaScript array", + StringSample.class, "toStringArray__Ljava_lang_String_2ZZ", + "1,2", true, false + ); + } + private static TestVM code; @BeforeClass diff -r 2fb3e929962f -r c75bd6823179 rt/vmtest/src/main/java/org/apidesign/bck2brwsr/vmtest/BrwsrTest.java --- a/rt/vmtest/src/main/java/org/apidesign/bck2brwsr/vmtest/BrwsrTest.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/vmtest/src/main/java/org/apidesign/bck2brwsr/vmtest/BrwsrTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -29,6 +29,10 @@ * The browser to is by default executed via {@link java.awt.Desktop#browse(java.net.URI)}, * but one can change that by specifying -Dvmtest.brwsrs=firefox,google-chrome * property. + *

+ * If the annotated method throws {@link InterruptedException}, it will return + * the processing to the browser and after 100ms, called again. This is useful + * for testing asynchronous communication, etc. * * @author Jaroslav Tulach */ diff -r 2fb3e929962f -r c75bd6823179 rt/vmtest/src/main/java/org/apidesign/bck2brwsr/vmtest/Http.java --- a/rt/vmtest/src/main/java/org/apidesign/bck2brwsr/vmtest/Http.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/vmtest/src/main/java/org/apidesign/bck2brwsr/vmtest/Http.java Mon Apr 08 19:33:08 2013 +0200 @@ -53,5 +53,10 @@ String resource() default ""; /** mime type of the resource */ String mimeType(); + /** query parameters. Can be referenced from the {@link #content} as + * $0, $1, etc. The values will be extracted + * from URL parameters of the request. + */ + String[] parameters() default {}; } } diff -r 2fb3e929962f -r c75bd6823179 rt/vmtest/src/main/java/org/apidesign/bck2brwsr/vmtest/impl/Bck2BrwsrCase.java --- a/rt/vmtest/src/main/java/org/apidesign/bck2brwsr/vmtest/impl/Bck2BrwsrCase.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/vmtest/src/main/java/org/apidesign/bck2brwsr/vmtest/impl/Bck2BrwsrCase.java Mon Apr 08 19:33:08 2013 +0200 @@ -65,17 +65,17 @@ for (Http.Resource r : http) { if (!r.content().isEmpty()) { InputStream is = new ByteArrayInputStream(r.content().getBytes("UTF-8")); - c.addHttpResource(r.path(), r.mimeType(), is); + c.addHttpResource(r.path(), r.mimeType(), r.parameters(), is); } else { InputStream is = m.getDeclaringClass().getResourceAsStream(r.resource()); - c.addHttpResource(r.path(), r.mimeType(), is); + c.addHttpResource(r.path(), r.mimeType(), r.parameters(), is); } } } String res = c.invoke(); value = res; if (fail) { - int idx = res.indexOf(':'); + int idx = res == null ? -1 : res.indexOf(':'); if (idx >= 0) { Class thrwbl = null; try { diff -r 2fb3e929962f -r c75bd6823179 rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/CompareStringsTest.java --- a/rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/CompareStringsTest.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/CompareStringsTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -149,6 +149,18 @@ public int stringToBytesLenght() throws UnsupportedEncodingException { return "\u017dlu\u0165ou\u010dk\u00fd k\u016f\u0148".getBytes("utf8").length; } + + @Compare public String replaceSeq() { + return "Hello World.".replace(".", "!"); + } + @Compare public String replaceSeqAll() { + return "Hello World! Hello World.".replace("World", "Jarda"); + } + @Compare public String replaceSeqAA() { + String res = "aaa".replace("aa", "b"); + assert res.equals("ba") : "Expecting ba: " + res; + return res; + } @Factory public static Object[] create() { diff -r 2fb3e929962f -r c75bd6823179 rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/ReflectionArrayTest.java --- a/rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/ReflectionArrayTest.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/ReflectionArrayTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -18,6 +18,8 @@ package org.apidesign.bck2brwsr.tck; import java.lang.reflect.Array; +import org.apidesign.bck2brwsr.core.JavaScriptBody; +import org.apidesign.bck2brwsr.vmtest.BrwsrTest; import org.apidesign.bck2brwsr.vmtest.Compare; import org.apidesign.bck2brwsr.vmtest.VMTest; import org.testng.annotations.Factory; @@ -127,6 +129,30 @@ return Array.newInstance(int.class, 3, 3, 3).getClass().getName(); } + @JavaScriptBody(args = {}, body = "return [1, 2];") + private static native Object crtarr(); + + @JavaScriptBody(args = {}, body = "return new Object();") + private static native Object newobj(); + + @BrwsrTest + public static void toStringArray() { + final Object arr = crtarr(); + final Object real = new Object[2]; + assert arr instanceof Object[] : "Any array is Java array: " + arr; + assert arr.getClass() == real.getClass() : "Same classes " + arr + " and " + real.getClass(); + final String str = arr.toString(); + assert str != null; + assert str.startsWith("[Ljava.lang.Object;@") : str; + } + + @BrwsrTest + public static void objectToString() { + String s = newobj().toString(); + assert s != null : "Some string computed"; + assert s.startsWith("java.lang.Object@") : "Regular object toString(): " + s; + } + @Factory public static Object[] create() { diff -r 2fb3e929962f -r c75bd6823179 rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/ReflectionTest.java --- a/rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/ReflectionTest.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/ReflectionTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -104,6 +104,11 @@ return "should not happen"; } + @Compare public String methodThatThrowsException() throws Exception { + StaticUse.class.getMethod("instanceMethod").invoke(new StaticUse()); + return "should not happen"; + } + @Compare public Object voidReturnType() throws Exception { return StaticUse.class.getMethod("instanceMethod").getReturnType(); } diff -r 2fb3e929962f -r c75bd6823179 rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/StaticUse.java --- a/rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/StaticUse.java Wed Apr 03 13:43:22 2013 +0200 +++ b/rt/vmtest/src/test/java/org/apidesign/bck2brwsr/tck/StaticUse.java Mon Apr 08 19:33:08 2013 +0200 @@ -30,6 +30,7 @@ } public void instanceMethod() { + throw new IllegalStateException(); } public static int plus(int a, int b) { diff -r 2fb3e929962f -r c75bd6823179 rt/vmtest/src/test/java/org/apidesign/bck2brwsr/vmtest/impl/CallMeTwiceTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rt/vmtest/src/test/java/org/apidesign/bck2brwsr/vmtest/impl/CallMeTwiceTest.java Mon Apr 08 19:33:08 2013 +0200 @@ -0,0 +1,43 @@ +/** + * Back 2 Browser Bytecode Translator + * Copyright (C) 2012 Jaroslav Tulach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. Look for COPYING file in the top folder. + * If not, see http://opensource.org/licenses/GPL-2.0. + */ +package org.apidesign.bck2brwsr.vmtest.impl; + +import org.apidesign.bck2brwsr.vmtest.BrwsrTest; +import org.apidesign.bck2brwsr.vmtest.VMTest; +import org.testng.annotations.Factory; + +/** + * + * @author Jaroslav Tulach + */ +public class CallMeTwiceTest { + int cnt; + + @BrwsrTest public void callMeTwice() throws InterruptedException { + if (cnt++ == 0) { + throw new InterruptedException(); + } + int prevCnt = cnt; + cnt = 0; + assert prevCnt == 2 : "We need to receive two calls " + prevCnt; + } + + @Factory public static Object[] create() { + return VMTest.create(CallMeTwiceTest.class); + } +}