# HG changeset patch # User Jaroslav Tulach # Date 1364973478 -7200 # Node ID 09fee723d65884236234093cd0cce605a05c164a # Parent c3c30f25c7235f6a3e96cbf237c1b861d4adf2e3# Parent b219134a2782e43ee8ecbb8a4cb00994a6eb581d Merge of recent work on model. @Model classes can have @OnFunction methods and their properties are accessible from the knockout bindings diff -r c3c30f25c723 -r 09fee723d658 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java Tue Apr 02 15:52:25 2013 +0200 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java Wed Apr 03 09:17:58 2013 +0200 @@ -41,6 +41,14 @@ Object ret = getProperty(object, property); return ret instanceof Number ? ((Number)ret).intValue() : Integer.MIN_VALUE; } + + public static T toModel(Class modelClass, Object object, String property) { + Object ret = getProperty(object, property); + if (ret == null || modelClass.isInstance(ret)) { + return modelClass.cast(ret); + } + throw new IllegalStateException("Value " + ret + " is not of type " + modelClass); + } @JavaScriptBody(args = { "object", "property" }, body = "if (property === null) return object;\n" diff -r c3c30f25c723 -r 09fee723d658 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/Knockout.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/Knockout.java Tue Apr 02 15:52:25 2013 +0200 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/Knockout.java Wed Apr 03 09:17:58 2013 +0200 @@ -30,45 +30,40 @@ public class Knockout { /** used by tests */ static Knockout next; + private final Object model; - Knockout() { + Knockout(Object model) { + this.model = model == null ? this : model; } public static Knockout applyBindings( + Object model, String[] propsGettersAndSetters, + String[] methodsAndSignatures + ) { + applyImpl(propsGettersAndSetters, model.getClass(), model, model, methodsAndSignatures); + return new Knockout(model); + } + public static Knockout applyBindings( Class modelClass, M model, String[] propsGettersAndSetters, String[] methodsAndSignatures ) { Knockout bindings = next; next = null; if (bindings == null) { - bindings = new Knockout(); + bindings = new Knockout(null); } - for (int i = 0; i < propsGettersAndSetters.length; i += 4) { - try { - Method getter = modelClass.getMethod(propsGettersAndSetters[i + 3]); - bind(bindings, model, propsGettersAndSetters[i], - propsGettersAndSetters[i + 1], - propsGettersAndSetters[i + 2], - getter.getReturnType().isPrimitive(), - List.class.isAssignableFrom(getter.getReturnType()) - ); - } catch (NoSuchMethodException ex) { - throw new IllegalStateException(ex.getMessage()); - } - } - for (int i = 0; i < methodsAndSignatures.length; i += 2) { - expose( - bindings, model, methodsAndSignatures[i], methodsAndSignatures[i + 1] - ); - } + applyImpl(propsGettersAndSetters, modelClass, bindings, model, methodsAndSignatures); applyBindings(bindings); return bindings; } - @JavaScriptBody(args = { "prop" }, body = - "this[prop].valueHasMutated();" + public void valueHasMutated(String prop) { + valueHasMutated(model, prop); + } + @JavaScriptBody(args = { "self", "prop" }, body = + "self[prop].valueHasMutated();" ) - public void valueHasMutated(String prop) { + public void valueHasMutated(Object self, String prop) { } @@ -107,4 +102,29 @@ @JavaScriptBody(args = { "bindings" }, body = "ko.applyBindings(bindings);") private static void applyBindings(Object bindings) {} + + private static void applyImpl( + String[] propsGettersAndSetters, + Class modelClass, + Object bindings, + Object model, + String[] methodsAndSignatures + ) throws IllegalStateException, SecurityException { + for (int i = 0; i < propsGettersAndSetters.length; i += 4) { + try { + Method getter = modelClass.getMethod(propsGettersAndSetters[i + 3]); + bind(bindings, model, propsGettersAndSetters[i], + propsGettersAndSetters[i + 1], + propsGettersAndSetters[i + 2], + getter.getReturnType().isPrimitive(), + List.class.isAssignableFrom(getter.getReturnType())); + } catch (NoSuchMethodException ex) { + throw new IllegalStateException(ex.getMessage()); + } + } + for (int i = 0; i < methodsAndSignatures.length; i += 2) { + expose( + bindings, model, methodsAndSignatures[i], methodsAndSignatures[i + 1]); + } + } } diff -r c3c30f25c723 -r 09fee723d658 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Tue Apr 02 15:52:25 2013 +0200 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Wed Apr 03 09:17:58 2013 +0200 @@ -28,9 +28,9 @@ import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.WeakHashMap; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Completion; import javax.annotation.processing.Completions; @@ -73,6 +73,7 @@ "org.apidesign.bck2brwsr.htmlpage.api.On" }) public final class PageProcessor extends AbstractProcessor { + private final Map models = new WeakHashMap<>(); @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { boolean ok = true; @@ -86,6 +87,9 @@ ok = false; } } + if (roundEnv.processingOver()) { + models.clear(); + } return ok; } @@ -111,6 +115,7 @@ try { StringWriter body = new StringWriter(); List propsGetSet = new ArrayList<>(); + List functions = new ArrayList<>(); Map> propsDeps = new HashMap<>(); if (!generateComputedProperties(body, m.properties(), e.getEnclosedElements(), propsGetSet, propsDeps)) { ok = false; @@ -118,17 +123,29 @@ if (!generateProperties(e, body, m.properties(), propsGetSet, propsDeps)) { ok = false; } + if (!generateFunctions(e, body, className, e.getEnclosedElements(), functions)) { + ok = false; + } FileObject java = processingEnv.getFiler().createSourceFile(pkg + '.' + className, e); w = new OutputStreamWriter(java.openOutputStream()); try { w.append("package " + pkg + ";\n"); 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(" 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(" public ").append(className).append("() {\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("}\n"); } finally { w.close(); @@ -203,25 +220,10 @@ w.write("public " + className + " applyBindings() {\n"); w.write(" ko = org.apidesign.bck2brwsr.htmlpage.Knockout.applyBindings("); w.write(className + ".class, this, "); - w.write("new String[] {\n"); - String sep = ""; - for (String n : propsGetSet) { - w.write(sep); - if (n == null) { - w.write(" null"); - } else { - w.write(" \"" + n + "\""); - } - sep = ",\n"; - } - w.write("\n }, new String[] {\n"); - sep = ""; - for (String n : functions) { - w.write(sep); - w.write(n); - sep = ",\n"; - } - w.write("\n });\n return this;\n}\n"); + writeStringArray(propsGetSet, w); + w.append(", "); + writeStringArray(functions, w); + w.write(");\n return this;\n}\n"); w.write("public void triggerEvent(Element e, OnEvent ev) {\n"); w.write(" org.apidesign.bck2brwsr.htmlpage.Knockout.triggerEvent(e.getId(), ev.getElementPropertyName());\n"); @@ -537,6 +539,7 @@ private String typeName(Element where, Property p) { String ret; boolean isModel = false; + boolean isEnum = false; try { ret = p.type().getName(); } catch (MirroredTypeException ex) { @@ -546,9 +549,13 @@ 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); } if (p.array()) { String bt = findBoxedType(ret); @@ -556,7 +563,7 @@ return bt; } } - if (!isModel && !"java.lang.String".equals(ret)) { + if (!isModel && !"java.lang.String".equals(ret) && !isEnum) { String bt = findBoxedType(ret); if (bt == null) { processingEnv.getMessager().printMessage( @@ -664,8 +671,8 @@ body.append(");\n"); body.append("}\n"); - functions.add('\"' + n + '\"'); - functions.add('\"' + n + "__VLjava_lang_Object_2Ljava_lang_Object_2" + '\"'); + functions.add(n); + functions.add(n + "__VLjava_lang_Object_2Ljava_lang_Object_2"); } return true; } @@ -687,18 +694,21 @@ params.append('"').append(id).append('"'); continue; } - toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toString"; + toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toString("; } if (ve.asType().getKind() == TypeKind.DOUBLE) { - toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toDouble"; + toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toDouble("; } if (ve.asType().getKind() == TypeKind.INT) { - toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toInt"; + toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toInt("; + } + if (dataName != null && ve.getSimpleName().contentEquals(dataName) && isModel(ve.asType())) { + toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toModel(" + ve.asType() + ".class, "; } if (toCall != null) { - params.append(toCall).append('('); - if (dataName != null && ve.getSimpleName().contentEquals("data")) { + params.append(toCall); + if (dataName != null && ve.getSimpleName().contentEquals(dataName)) { params.append(dataName); params.append(", null"); } else { @@ -726,4 +736,35 @@ } return params; } + + private boolean isModel(TypeMirror tm) { + final Element e = processingEnv.getTypeUtils().asElement(tm); + if (e == null) { + return false; + } + for (Element ch : e.getEnclosedElements()) { + if (ch.getKind() == ElementKind.METHOD) { + ExecutableElement ee = (ExecutableElement)ch; + if (ee.getParameters().isEmpty() && ee.getSimpleName().contentEquals("modelFor")) { + return true; + } + } + } + return models.values().contains(e.getSimpleName().toString()); + } + + private void writeStringArray(List strings, Writer w) throws IOException { + w.write("new String[] {\n"); + String sep = ""; + for (String n : strings) { + w.write(sep); + if (n == null) { + w.write(" null"); + } else { + w.write(" \"" + n + "\""); + } + sep = ",\n"; + } + w.write("\n }"); + } } diff -r c3c30f25c723 -r 09fee723d658 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/KnockoutTest.java --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/KnockoutTest.java Tue Apr 02 15:52:25 2013 +0200 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/KnockoutTest.java Wed Apr 03 09:17:58 2013 +0200 @@ -36,7 +36,8 @@ @Page(xhtml="Knockout.xhtml", className="KnockoutModel", properties={ @Property(name="name", type=String.class), @Property(name="results", type=String.class, array = true), - @Property(name="callbackCount", type=int.class) + @Property(name="callbackCount", type=int.class), + @Property(name="people", type=PersonImpl.class, array = true) }) public class KnockoutTest { @@ -98,11 +99,83 @@ assert cnt == 2 : "Two children now, but was " + cnt; } + @HtmlFragment( + "
    \n" + + "
  • \n" + + "
\n" + ) + @BrwsrTest public void displayContentOfArrayOfPeople() { + KnockoutModel m = new KnockoutModel(); + + final Person first = new Person(); + first.setFirstName("first"); + m.getPeople().add(first); + + m.applyBindings(); + + int cnt = countChildren("ul"); + assert cnt == 1 : "One child, but was " + cnt; + + final Person second = new Person(); + second.setFirstName("second"); + m.getPeople().add(second); + + cnt = countChildren("ul"); + assert cnt == 2 : "Two children now, but was " + cnt; + + triggerChildClick("ul", 1); + + assert 1 == m.getCallbackCount() : "One callback " + m.getCallbackCount(); + + cnt = countChildren("ul"); + assert cnt == 1 : "Again one child, but was " + cnt; + + String txt = childText("ul", 0); + assert "first".equals(txt) : "Expecting 'first': " + txt; + + first.setFirstName("changed"); + + txt = childText("ul", 0); + assert "changed".equals(txt) : "Expecting 'changed': " + txt; + } + + @HtmlFragment( + "
    \n" + + "
  • \n" + + "
\n" + ) + @BrwsrTest public void onPersonFunction() { + 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(); + } + @OnFunction static void call(KnockoutModel m, String data) { m.setName(data); m.setCallbackCount(m.getCallbackCount() + 1); } + + @OnFunction + static void removePerson(KnockoutModel model, Person data) { + model.setCallbackCount(model.getCallbackCount() + 1); + model.getPeople().remove(data); + } + @ComputedProperty static String helloMessage(String name) { @@ -133,4 +206,11 @@ + "e.children[pos].dispatchEvent(ev);\n " ) private static native void triggerChildClick(String id, int pos); + + @JavaScriptBody(args = { "id", "pos" }, body = + "var e = window.document.getElementById(id);\n " + + "var t = e.children[pos].innerHTML;\n " + + "return t ? t : null;" + ) + private static native String childText(String id, int pos); } diff -r c3c30f25c723 -r 09fee723d658 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java Tue Apr 02 15:52:25 2013 +0200 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java Wed Apr 03 09:17:58 2013 +0200 @@ -197,6 +197,10 @@ static class MockKnockout extends Knockout { List mutated = new ArrayList<>(); + MockKnockout() { + super(null); + } + @Override public void valueHasMutated(String prop) { mutated.add(prop); diff -r c3c30f25c723 -r 09fee723d658 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/PersonImpl.java --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/PersonImpl.java Tue Apr 02 15:52:25 2013 +0200 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/PersonImpl.java Wed Apr 03 09:17:58 2013 +0200 @@ -19,6 +19,7 @@ import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty; import org.apidesign.bck2brwsr.htmlpage.api.Model; +import org.apidesign.bck2brwsr.htmlpage.api.OnFunction; import org.apidesign.bck2brwsr.htmlpage.api.Property; /** @@ -28,7 +29,7 @@ @Model(className = "Person", properties = { @Property(name = "firstName", type = String.class), @Property(name = "lastName", type = String.class), - @Property(name = "male", type = boolean.class) + @Property(name = "sex", type = Sex.class) }) final class PersonImpl { @ComputedProperty @@ -37,7 +38,16 @@ } @ComputedProperty - public static String sex(boolean male) { - return male ? "Male" : "Female"; + public static String sexType(Sex sex) { + return sex == null ? "unknown" : sex.toString(); + } + + @OnFunction + static void changeSex(Person p) { + if (p.getSex() == Sex.MALE) { + p.setSex(Sex.FEMALE); + } else { + p.setSex(Sex.MALE); + } } } diff -r c3c30f25c723 -r 09fee723d658 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/Sex.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/Sex.java Wed Apr 03 09:17:58 2013 +0200 @@ -0,0 +1,26 @@ +/** + * 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; + +/** + * + * @author Jaroslav Tulach + */ +public enum Sex { + MALE, FEMALE; +} diff -r c3c30f25c723 -r 09fee723d658 javaquery/demo-calculator-dynamic/nbactions.xml --- a/javaquery/demo-calculator-dynamic/nbactions.xml Tue Apr 02 15:52:25 2013 +0200 +++ b/javaquery/demo-calculator-dynamic/nbactions.xml Wed Apr 03 09:17:58 2013 +0200 @@ -23,7 +23,7 @@ run process-classes - org.apidesign.bck2brwsr:mojo:0.5-SNAPSHOT:brwsr + org.apidesign.bck2brwsr:mojo:0.6-SNAPSHOT:brwsr diff -r c3c30f25c723 -r 09fee723d658 javaquery/demo-calculator-dynamic/src/main/java/org/apidesign/bck2brwsr/demo/calc/Calc.java --- a/javaquery/demo-calculator-dynamic/src/main/java/org/apidesign/bck2brwsr/demo/calc/Calc.java Tue Apr 02 15:52:25 2013 +0200 +++ b/javaquery/demo-calculator-dynamic/src/main/java/org/apidesign/bck2brwsr/demo/calc/Calc.java Wed Apr 03 09:17:58 2013 +0200 @@ -36,7 +36,7 @@ @Property(name = "display", type = double.class), @Property(name = "operation", type = String.class), @Property(name = "hover", type = boolean.class), - @Property(name = "history", type = double.class, array = true) + @Property(name = "history", type = HistoryImpl.class, array = true) }) public class Calc { static { @@ -74,19 +74,22 @@ c.getDisplay() ); c.setDisplay(newValue); - if (!c.getHistory().contains(newValue)) { - c.getHistory().add(newValue); + if (!containsValue(c.getHistory(), newValue)) { + History h = new History(); + h.setValue(newValue); + h.setOperation(c.getOperation()); + c.getHistory().add(h); } c.setMemory(0); } @OnFunction - static void recoverMemory(Calculator c, double data) { - c.setDisplay(data); + static void recoverMemory(Calculator c, History data) { + c.setDisplay(data.getValue()); } @OnFunction - static void removeMemory(Calculator c, double data) { + static void removeMemory(Calculator c, History data) { c.getHistory().remove(data); } @@ -131,4 +134,13 @@ static boolean emptyHistory(List history) { return history.isEmpty(); } + + private static boolean containsValue(List arr, final double newValue) { + for (History history : arr) { + if (history.getValue() == newValue) { + return true; + } + } + return false; + } } diff -r c3c30f25c723 -r 09fee723d658 javaquery/demo-calculator-dynamic/src/main/java/org/apidesign/bck2brwsr/demo/calc/HistoryImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/demo-calculator-dynamic/src/main/java/org/apidesign/bck2brwsr/demo/calc/HistoryImpl.java Wed Apr 03 09:17:58 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.demo.calc; + +import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty; +import org.apidesign.bck2brwsr.htmlpage.api.Model; +import org.apidesign.bck2brwsr.htmlpage.api.OnFunction; +import org.apidesign.bck2brwsr.htmlpage.api.Property; + +/** + * + * @author Jaroslav Tulach + */ +@Model(className = "History", properties = { + @Property(name = "value", type = double.class), + @Property(name = "operation", type = String.class) +}) +public class HistoryImpl { + @ComputedProperty + static String resultOf(String operation) { + return "result of " + operation; + } + + @OnFunction + static void twice(History data) { + data.setValue(2.0 * data.getValue()); + } +} diff -r c3c30f25c723 -r 09fee723d658 javaquery/demo-calculator-dynamic/src/main/resources/org/apidesign/bck2brwsr/demo/calc/Calculator.xhtml --- a/javaquery/demo-calculator-dynamic/src/main/resources/org/apidesign/bck2brwsr/demo/calc/Calculator.xhtml Tue Apr 02 15:52:25 2013 +0200 +++ b/javaquery/demo-calculator-dynamic/src/main/resources/org/apidesign/bck2brwsr/demo/calc/Calculator.xhtml Wed Apr 03 09:17:58 2013 +0200 @@ -83,9 +83,11 @@
No results yet.