# HG changeset patch # User Jaroslav Tulach # Date 1365415962 -7200 # Node ID 3bd43aa6f08d216218cb886301b59ceb877d4111 # Parent 35f4f8af296c1d9a989740bb37ef27716e113c1d OnPropertyChange allows us to update the tweets as soon as active tweets are changed diff -r 35f4f8af296c -r 3bd43aa6f08d javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java Mon Apr 08 09:24:59 2013 +0200 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java Mon Apr 08 12:12:42 2013 +0200 @@ -89,9 +89,10 @@ + "};" + "request.send();" ) - public static native void loadJSON( + public static void loadJSON( String url, Object[] jsonResult, Runnable whenDone - ); + ) { + } public static void extractJSON(Object jsonObject, String[] props, Object[] values) { for (int i = 0; i < props.length; i++) { diff -r 35f4f8af296c -r 3bd43aa6f08d javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Mon Apr 08 09:24:59 2013 +0200 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Mon Apr 08 12:12:42 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; @@ -60,6 +61,7 @@ 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; @@ -76,6 +78,7 @@ "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 { @@ -128,10 +131,14 @@ 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)) { @@ -265,10 +272,14 @@ 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)) { @@ -461,7 +472,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) { @@ -498,13 +511,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"); } @@ -744,6 +763,68 @@ 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 @@ -898,6 +979,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()); @@ -923,6 +1012,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) { @@ -1078,4 +1203,14 @@ "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 35f4f8af296c -r 3bd43aa6f08d 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 12:12:42 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 35f4f8af296c -r 3bd43aa6f08d javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java Mon Apr 08 09:24:59 2013 +0200 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java Mon Apr 08 12:12:42 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,24 @@ return value * value; } + @OnPropertyChange({ "powerValue", "unrelated" }) + static void aPropertyChanged(Modelik m, String name) { + 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"); + } + @ComputedProperty static String notAllowedRead() { return "Not allowed callback: " + leakedModel.getUnrelated(); diff -r 35f4f8af296c -r 3bd43aa6f08d javaquery/demo-twitter/src/main/java/org/apidesign/bck2brwsr/demo/twitter/TwitterClient.java --- a/javaquery/demo-twitter/src/main/java/org/apidesign/bck2brwsr/demo/twitter/TwitterClient.java Mon Apr 08 09:24:59 2013 +0200 +++ b/javaquery/demo-twitter/src/main/java/org/apidesign/bck2brwsr/demo/twitter/TwitterClient.java Mon Apr 08 12:12:42 2013 +0200 @@ -63,11 +63,11 @@ page.getCurrentTweets().addAll(q.getResults()); } - @OnFunction + @OnPropertyChange("activeTweeters") static void refreshTweets(TwitterModel model) { Tweeters people = model.getActiveTweeters(); StringBuilder sb = new StringBuilder(); - sb.append("http://search.twitter.com/search.json?callback=?&rpp=25&q="); + sb.append("http://search.twitter.com/search.json?rpp=25&q="); String sep = ""; for (String p : people.getUserNames()) { sb.append(sep); diff -r 35f4f8af296c -r 3bd43aa6f08d javaquery/demo-twitter/src/main/resources/org/apidesign/bck2brwsr/demo/twitter/index.html --- a/javaquery/demo-twitter/src/main/resources/org/apidesign/bck2brwsr/demo/twitter/index.html Mon Apr 08 09:24:59 2013 +0200 +++ b/javaquery/demo-twitter/src/main/resources/org/apidesign/bck2brwsr/demo/twitter/index.html Mon Apr 08 12:12:42 2013 +0200 @@ -52,7 +52,6 @@
-