OnPropertyChange allows us to update the tweets as soon as active tweets are changed
1.1 --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java Mon Apr 08 09:24:59 2013 +0200
1.2 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java Mon Apr 08 12:12:42 2013 +0200
1.3 @@ -89,9 +89,10 @@
1.4 + "};"
1.5 + "request.send();"
1.6 )
1.7 - public static native void loadJSON(
1.8 + public static void loadJSON(
1.9 String url, Object[] jsonResult, Runnable whenDone
1.10 - );
1.11 + ) {
1.12 + }
1.13
1.14 public static void extractJSON(Object jsonObject, String[] props, Object[] values) {
1.15 for (int i = 0; i < props.length; i++) {
2.1 --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Mon Apr 08 09:24:59 2013 +0200
2.2 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Mon Apr 08 12:12:42 2013 +0200
2.3 @@ -26,6 +26,7 @@
2.4 import java.util.Collection;
2.5 import java.util.Collections;
2.6 import java.util.HashMap;
2.7 +import java.util.HashSet;
2.8 import java.util.LinkedHashSet;
2.9 import java.util.List;
2.10 import java.util.Map;
2.11 @@ -60,6 +61,7 @@
2.12 import org.apidesign.bck2brwsr.htmlpage.api.Model;
2.13 import org.apidesign.bck2brwsr.htmlpage.api.On;
2.14 import org.apidesign.bck2brwsr.htmlpage.api.OnFunction;
2.15 +import org.apidesign.bck2brwsr.htmlpage.api.OnPropertyChange;
2.16 import org.apidesign.bck2brwsr.htmlpage.api.OnReceive;
2.17 import org.apidesign.bck2brwsr.htmlpage.api.Page;
2.18 import org.apidesign.bck2brwsr.htmlpage.api.Property;
2.19 @@ -76,6 +78,7 @@
2.20 "org.apidesign.bck2brwsr.htmlpage.api.Page",
2.21 "org.apidesign.bck2brwsr.htmlpage.api.OnFunction",
2.22 "org.apidesign.bck2brwsr.htmlpage.api.OnReceive",
2.23 + "org.apidesign.bck2brwsr.htmlpage.api.OnPropertyChange",
2.24 "org.apidesign.bck2brwsr.htmlpage.api.On"
2.25 })
2.26 public final class PageProcessor extends AbstractProcessor {
2.27 @@ -128,10 +131,14 @@
2.28 List<String> propsGetSet = new ArrayList<>();
2.29 List<String> functions = new ArrayList<>();
2.30 Map<String, Collection<String>> propsDeps = new HashMap<>();
2.31 + Map<String, Collection<String>> functionDeps = new HashMap<>();
2.32 if (!generateComputedProperties(body, m.properties(), e.getEnclosedElements(), propsGetSet, propsDeps)) {
2.33 ok = false;
2.34 }
2.35 - if (!generateProperties(e, body, m.properties(), propsGetSet, propsDeps)) {
2.36 + if (!generateOnChange(e, propsDeps, m.properties(), className, functionDeps)) {
2.37 + ok = false;
2.38 + }
2.39 + if (!generateProperties(e, body, m.properties(), propsGetSet, propsDeps, functionDeps)) {
2.40 ok = false;
2.41 }
2.42 if (!generateFunctions(e, body, className, e.getEnclosedElements(), functions)) {
2.43 @@ -265,10 +272,14 @@
2.44 List<String> propsGetSet = new ArrayList<>();
2.45 List<String> functions = new ArrayList<>();
2.46 Map<String, Collection<String>> propsDeps = new HashMap<>();
2.47 + Map<String, Collection<String>> functionDeps = new HashMap<>();
2.48 if (!generateComputedProperties(body, p.properties(), e.getEnclosedElements(), propsGetSet, propsDeps)) {
2.49 ok = false;
2.50 }
2.51 - if (!generateProperties(e, body, p.properties(), propsGetSet, propsDeps)) {
2.52 + if (!generateOnChange(e, propsDeps, p.properties(), className, functionDeps)) {
2.53 + ok = false;
2.54 + }
2.55 + if (!generateProperties(e, body, p.properties(), propsGetSet, propsDeps, functionDeps)) {
2.56 ok = false;
2.57 }
2.58 if (!generateFunctions(e, body, className, e.getEnclosedElements(), functions)) {
2.59 @@ -461,7 +472,9 @@
2.60 private boolean generateProperties(
2.61 Element where,
2.62 Writer w, Property[] properties,
2.63 - Collection<String> props, Map<String,Collection<String>> deps
2.64 + Collection<String> props,
2.65 + Map<String,Collection<String>> deps,
2.66 + Map<String,Collection<String>> functionDeps
2.67 ) throws IOException {
2.68 boolean ok = true;
2.69 for (Property p : properties) {
2.70 @@ -498,13 +511,19 @@
2.71 w.write(" prop_" + p.name() + " = v;\n");
2.72 w.write(" if (ko != null) {\n");
2.73 w.write(" ko.valueHasMutated(\"" + p.name() + "\");\n");
2.74 - final Collection<String> dependants = deps.get(p.name());
2.75 + Collection<String> dependants = deps.get(p.name());
2.76 if (dependants != null) {
2.77 for (String depProp : dependants) {
2.78 w.write(" ko.valueHasMutated(\"" + depProp + "\");\n");
2.79 }
2.80 }
2.81 w.write(" }\n");
2.82 + dependants = functionDeps.get(p.name());
2.83 + if (dependants != null) {
2.84 + for (String call : dependants) {
2.85 + w.append(call);
2.86 + }
2.87 + }
2.88 w.write("}\n");
2.89 }
2.90
2.91 @@ -744,6 +763,68 @@
2.92 return true;
2.93 }
2.94
2.95 + private boolean generateOnChange(Element clazz, Map<String,Collection<String>> propDeps,
2.96 + Property[] properties, String className,
2.97 + Map<String, Collection<String>> functionDeps
2.98 + ) {
2.99 + for (Element m : clazz.getEnclosedElements()) {
2.100 + if (m.getKind() != ElementKind.METHOD) {
2.101 + continue;
2.102 + }
2.103 + ExecutableElement e = (ExecutableElement) m;
2.104 + OnPropertyChange onPC = e.getAnnotation(OnPropertyChange.class);
2.105 + if (onPC == null) {
2.106 + continue;
2.107 + }
2.108 + for (String pn : onPC.value()) {
2.109 + if (findProperty(properties, pn) == null && findDerivedFrom(propDeps, pn).isEmpty()) {
2.110 + err().printMessage(Diagnostic.Kind.ERROR, "No property named '" + pn + "' in the model");
2.111 + return false;
2.112 + }
2.113 + }
2.114 + if (!e.getModifiers().contains(Modifier.STATIC)) {
2.115 + err().printMessage(
2.116 + Diagnostic.Kind.ERROR, "@OnPropertyChange method needs to be static", e);
2.117 + return false;
2.118 + }
2.119 + if (e.getModifiers().contains(Modifier.PRIVATE)) {
2.120 + err().printMessage(
2.121 + Diagnostic.Kind.ERROR, "@OnPropertyChange method cannot be private", e);
2.122 + return false;
2.123 + }
2.124 + if (e.getReturnType().getKind() != TypeKind.VOID) {
2.125 + err().printMessage(
2.126 + Diagnostic.Kind.ERROR, "@OnPropertyChange method should return void", e);
2.127 + return false;
2.128 + }
2.129 + String n = e.getSimpleName().toString();
2.130 +
2.131 +
2.132 + for (String pn : onPC.value()) {
2.133 + StringBuilder call = new StringBuilder();
2.134 + call.append(" ").append(clazz.getSimpleName()).append(".").append(n).append("(");
2.135 + call.append(wrapPropName(e, className, "name", pn));
2.136 + call.append(");\n");
2.137 +
2.138 + Collection<String> change = functionDeps.get(pn);
2.139 + if (change == null) {
2.140 + change = new ArrayList<>();
2.141 + functionDeps.put(pn, change);
2.142 + }
2.143 + change.add(call.toString());
2.144 + for (String dpn : findDerivedFrom(propDeps, pn)) {
2.145 + change = functionDeps.get(dpn);
2.146 + if (change == null) {
2.147 + change = new ArrayList<>();
2.148 + functionDeps.put(dpn, change);
2.149 + }
2.150 + change.add(call.toString());
2.151 + }
2.152 + }
2.153 + }
2.154 + return true;
2.155 + }
2.156 +
2.157 private boolean generateReceive(
2.158 Element clazz, StringWriter body, String className,
2.159 List<? extends Element> enclosedElements, List<String> functions
2.160 @@ -898,6 +979,14 @@
2.161 params.append(dataName);
2.162 params.append(", null");
2.163 } else {
2.164 + if (evName == null) {
2.165 + final StringBuilder sb = new StringBuilder();
2.166 + sb.append("Unexpected string parameter name.");
2.167 + if (dataName != null) {
2.168 + sb.append(" Try \"").append(dataName).append("\"");
2.169 + }
2.170 + err().printMessage(Diagnostic.Kind.ERROR, sb.toString(), ee);
2.171 + }
2.172 params.append(evName);
2.173 params.append(", \"");
2.174 params.append(ve.getSimpleName().toString());
2.175 @@ -923,6 +1012,42 @@
2.176 return params;
2.177 }
2.178
2.179 +
2.180 + private CharSequence wrapPropName(
2.181 + ExecutableElement ee, String className, String propName, String propValue
2.182 + ) {
2.183 + TypeMirror stringType = processingEnv.getElementUtils().getTypeElement("java.lang.String").asType();
2.184 + StringBuilder params = new StringBuilder();
2.185 + boolean first = true;
2.186 + for (VariableElement ve : ee.getParameters()) {
2.187 + if (!first) {
2.188 + params.append(", ");
2.189 + }
2.190 + first = false;
2.191 + if (ve.asType() == stringType) {
2.192 + if (propName != null && ve.getSimpleName().contentEquals(propName)) {
2.193 + params.append('"').append(propValue).append('"');
2.194 + } else {
2.195 + err().printMessage(Diagnostic.Kind.ERROR, "Unexpected string parameter name. Try \"" + propName + "\".");
2.196 + }
2.197 + continue;
2.198 + }
2.199 + String rn = fqn(ve.asType(), ee);
2.200 + int last = rn.lastIndexOf('.');
2.201 + if (last >= 0) {
2.202 + rn = rn.substring(last + 1);
2.203 + }
2.204 + if (rn.equals(className)) {
2.205 + params.append(className).append(".this");
2.206 + continue;
2.207 + }
2.208 + err().printMessage(Diagnostic.Kind.ERROR,
2.209 + "@OnPropertyChange method can only accept String or " + className + " arguments",
2.210 + ee);
2.211 + }
2.212 + return params;
2.213 + }
2.214 +
2.215 private boolean isModel(TypeMirror tm) {
2.216 final Element e = processingEnv.getTypeUtils().asElement(tm);
2.217 if (e == null) {
2.218 @@ -1078,4 +1203,14 @@
2.219 "byte".equals(type) ||
2.220 "float".equals(type);
2.221 }
2.222 +
2.223 + private static Collection<String> findDerivedFrom(Map<String, Collection<String>> propsDeps, String derivedProp) {
2.224 + Set<String> names = new HashSet<>();
2.225 + for (Map.Entry<String, Collection<String>> e : propsDeps.entrySet()) {
2.226 + if (e.getValue().contains(derivedProp)) {
2.227 + names.add(e.getKey());
2.228 + }
2.229 + }
2.230 + return names;
2.231 + }
2.232 }
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/OnPropertyChange.java Mon Apr 08 12:12:42 2013 +0200
3.3 @@ -0,0 +1,38 @@
3.4 +/**
3.5 + * Back 2 Browser Bytecode Translator
3.6 + * Copyright (C) 2012 Jaroslav Tulach <jaroslav.tulach@apidesign.org>
3.7 + *
3.8 + * This program is free software: you can redistribute it and/or modify
3.9 + * it under the terms of the GNU General Public License as published by
3.10 + * the Free Software Foundation, version 2 of the License.
3.11 + *
3.12 + * This program is distributed in the hope that it will be useful,
3.13 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
3.14 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3.15 + * GNU General Public License for more details.
3.16 + *
3.17 + * You should have received a copy of the GNU General Public License
3.18 + * along with this program. Look for COPYING file in the top folder.
3.19 + * If not, see http://opensource.org/licenses/GPL-2.0.
3.20 + */
3.21 +package org.apidesign.bck2brwsr.htmlpage.api;
3.22 +
3.23 +import java.lang.annotation.ElementType;
3.24 +import java.lang.annotation.Retention;
3.25 +import java.lang.annotation.RetentionPolicy;
3.26 +import java.lang.annotation.Target;
3.27 +
3.28 +/** Represents a property. Either in a generated model of an HTML
3.29 + * {@link Page} or in a class defined by {@link Model}.
3.30 + *
3.31 + * @author Jaroslav Tulach <jtulach@netbeans.org>
3.32 + */
3.33 +@Retention(RetentionPolicy.SOURCE)
3.34 +@Target(ElementType.METHOD)
3.35 +public @interface OnPropertyChange {
3.36 + /** Name(s) of the properties. One wishes to observe.
3.37 + *
3.38 + * @return valid java identifier
3.39 + */
3.40 + String[] value();
3.41 +}
4.1 --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java Mon Apr 08 09:24:59 2013 +0200
4.2 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java Mon Apr 08 12:12:42 2013 +0200
4.3 @@ -24,6 +24,7 @@
4.4 import java.util.ListIterator;
4.5 import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty;
4.6 import org.apidesign.bck2brwsr.htmlpage.api.OnFunction;
4.7 +import org.apidesign.bck2brwsr.htmlpage.api.OnPropertyChange;
4.8 import org.apidesign.bck2brwsr.htmlpage.api.Page;
4.9 import org.apidesign.bck2brwsr.htmlpage.api.Property;
4.10 import static org.testng.Assert.*;
4.11 @@ -40,7 +41,8 @@
4.12 @Property(name = "unrelated", type = long.class),
4.13 @Property(name = "names", type = String.class, array = true),
4.14 @Property(name = "values", type = int.class, array = true),
4.15 - @Property(name = "people", type = PersonImpl.class, array = true)
4.16 + @Property(name = "people", type = PersonImpl.class, array = true),
4.17 + @Property(name = "changedProperty", type=String.class)
4.18 })
4.19 public class ModelTest {
4.20 private Modelik model;
4.21 @@ -138,6 +140,9 @@
4.22
4.23 model.setValue(33);
4.24
4.25 + // not interested in change of this property
4.26 + my.mutated.remove("changedProperty");
4.27 +
4.28 assertEquals(my.mutated.size(), 2, "Two properties changed: " + my.mutated);
4.29 assertTrue(my.mutated.contains("powerValue"), "Power value is in there: " + my.mutated);
4.30 assertTrue(my.mutated.contains("value"), "Simple value is in there: " + my.mutated);
4.31 @@ -145,7 +150,11 @@
4.32 my.mutated.clear();
4.33
4.34 model.setUnrelated(44);
4.35 - assertEquals(my.mutated.size(), 1, "One property changed");
4.36 +
4.37 +
4.38 + // not interested in change of this property
4.39 + my.mutated.remove("changedProperty");
4.40 + assertEquals(my.mutated.size(), 1, "One property changed: " + my.mutated);
4.41 assertTrue(my.mutated.contains("unrelated"), "Its name is unrelated");
4.42 }
4.43
4.44 @@ -178,6 +187,24 @@
4.45 return value * value;
4.46 }
4.47
4.48 + @OnPropertyChange({ "powerValue", "unrelated" })
4.49 + static void aPropertyChanged(Modelik m, String name) {
4.50 + m.setChangedProperty(name);
4.51 + }
4.52 +
4.53 + @Test public void changeAnything() {
4.54 + model.setCount(44);
4.55 + assertNull(model.getChangedProperty(), "No observed value change");
4.56 + }
4.57 + @Test public void changeValue() {
4.58 + model.setValue(33);
4.59 + assertEquals(model.getChangedProperty(), "powerValue", "power property changed");
4.60 + }
4.61 + @Test public void changeUnrelated() {
4.62 + model.setUnrelated(333);
4.63 + assertEquals(model.getChangedProperty(), "unrelated", "unrelated changed");
4.64 + }
4.65 +
4.66 @ComputedProperty
4.67 static String notAllowedRead() {
4.68 return "Not allowed callback: " + leakedModel.getUnrelated();
5.1 --- a/javaquery/demo-twitter/src/main/java/org/apidesign/bck2brwsr/demo/twitter/TwitterClient.java Mon Apr 08 09:24:59 2013 +0200
5.2 +++ b/javaquery/demo-twitter/src/main/java/org/apidesign/bck2brwsr/demo/twitter/TwitterClient.java Mon Apr 08 12:12:42 2013 +0200
5.3 @@ -63,11 +63,11 @@
5.4 page.getCurrentTweets().addAll(q.getResults());
5.5 }
5.6
5.7 - @OnFunction
5.8 + @OnPropertyChange("activeTweeters")
5.9 static void refreshTweets(TwitterModel model) {
5.10 Tweeters people = model.getActiveTweeters();
5.11 StringBuilder sb = new StringBuilder();
5.12 - sb.append("http://search.twitter.com/search.json?callback=?&rpp=25&q=");
5.13 + sb.append("http://search.twitter.com/search.json?rpp=25&q=");
5.14 String sep = "";
5.15 for (String p : people.getUserNames()) {
5.16 sb.append(sep);
6.1 --- a/javaquery/demo-twitter/src/main/resources/org/apidesign/bck2brwsr/demo/twitter/index.html Mon Apr 08 09:24:59 2013 +0200
6.2 +++ b/javaquery/demo-twitter/src/main/resources/org/apidesign/bck2brwsr/demo/twitter/index.html Mon Apr 08 12:12:42 2013 +0200
6.3 @@ -52,7 +52,6 @@
6.4 <div class='liveExample'>
6.5 <div class='configuration'>
6.6 <div class='listChooser'>
6.7 - <button data-bind='click: $root.refreshTweets'>Refresh</button>
6.8 <button data-bind='click: deleteList, enable: activeTweeters.name'>Delete</button>
6.9 <button data-bind='click: saveChanges, enable: hasUnsavedChanges'>Save</button>
6.10 <select data-bind='options: savedLists, optionsValue: "name", value: activeTweetersName'> </select>