OnPropertyChange allows us to update the tweets as soon as active tweets are changed model
authorJaroslav Tulach <jaroslav.tulach@apidesign.org>
Mon, 08 Apr 2013 12:12:42 +0200
branchmodel
changeset 9493bd43aa6f08d
parent 948 35f4f8af296c
child 950 445d5f1d4177
OnPropertyChange allows us to update the tweets as soon as active tweets are changed
javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java
javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java
javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/OnPropertyChange.java
javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java
javaquery/demo-twitter/src/main/java/org/apidesign/bck2brwsr/demo/twitter/TwitterClient.java
javaquery/demo-twitter/src/main/resources/org/apidesign/bck2brwsr/demo/twitter/index.html
     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>