#250450: Support for writable @ComputedProperty
authorJaroslav Tulach <jtulach@netbeans.org>
Wed, 15 Jul 2015 22:06:19 +0200
changeset 9515ce0aab2c03c
parent 950 b171b7ec2f04
child 952 f5d1e573de92
#250450: Support for writable @ComputedProperty
json-tck/src/main/java/net/java/html/json/tests/KnockoutTest.java
json-tck/src/main/java/net/java/html/json/tests/PersonImpl.java
json/src/main/java/net/java/html/json/ComputedProperty.java
json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java
json/src/test/java/net/java/html/json/MapModelTest.java
json/src/test/java/net/java/html/json/ModelProcessorTest.java
json/src/test/java/net/java/html/json/ModelTest.java
src/main/javadoc/overview.html
     1.1 --- a/json-tck/src/main/java/net/java/html/json/tests/KnockoutTest.java	Sat Jul 04 22:41:17 2015 +0200
     1.2 +++ b/json-tck/src/main/java/net/java/html/json/tests/KnockoutTest.java	Wed Jul 15 22:06:19 2015 +0200
     1.3 @@ -136,6 +136,36 @@
     1.4              Utils.exposeHTML(KnockoutTest.class, "");
     1.5          }
     1.6      }
     1.7 +
     1.8 +    @KOTest public void modifyComputedProperty() throws Throwable {
     1.9 +        Object exp = Utils.exposeHTML(KnockoutTest.class,
    1.10 +            "Full name: <div data-bind='with:firstPerson'>\n"
    1.11 +                + "<input id='input' data-bind=\"value: fullName\"></input>\n"
    1.12 +                + "</div>\n"
    1.13 +        );
    1.14 +        try {
    1.15 +            KnockoutModel m = new KnockoutModel();
    1.16 +            m.getPeople().add(new Person());
    1.17 +
    1.18 +            m = Models.bind(m, newContext());
    1.19 +            m.getFirstPerson().setFirstName("Jarda");
    1.20 +            m.getFirstPerson().setLastName("Tulach");
    1.21 +            m.applyBindings();
    1.22 +
    1.23 +            String v = getSetInput(null);
    1.24 +            assertEquals("Jarda Tulach", v, "Value: " + v);
    1.25 +
    1.26 +            getSetInput("Mickey Mouse");
    1.27 +            triggerEvent("input", "change");
    1.28 +
    1.29 +            assertEquals("Mickey", m.getFirstPerson().getFirstName(), "First name updated");
    1.30 +            assertEquals("Mouse", m.getFirstPerson().getLastName(), "Last name updated");
    1.31 +        } catch (Throwable t) {
    1.32 +            throw t;
    1.33 +        } finally {
    1.34 +            Utils.exposeHTML(KnockoutTest.class, "");
    1.35 +        }
    1.36 +    }
    1.37      
    1.38      @KOTest public void modifyValueAssertChangeInModelOnBoolean() throws Throwable {
    1.39          Object exp = Utils.exposeHTML(KnockoutTest.class, 
     2.1 --- a/json-tck/src/main/java/net/java/html/json/tests/PersonImpl.java	Sat Jul 04 22:41:17 2015 +0200
     2.2 +++ b/json-tck/src/main/java/net/java/html/json/tests/PersonImpl.java	Wed Jul 15 22:06:19 2015 +0200
     2.3 @@ -58,10 +58,16 @@
     2.4      @Property(name = "address", type = Address.class)
     2.5  })
     2.6  final class PersonImpl {
     2.7 -    @ComputedProperty 
     2.8 +    @ComputedProperty(write = "parseNames")
     2.9      public static String fullName(String firstName, String lastName) {
    2.10          return firstName + " " + lastName;
    2.11      }
    2.12 +
    2.13 +    static void parseNames(Person p, String fullName) {
    2.14 +        String[] arr = fullName.split(" ");
    2.15 +        p.setFirstName(arr[0]);
    2.16 +        p.setLastName(arr[1]);
    2.17 +    }
    2.18      
    2.19      @ComputedProperty
    2.20      public static String sexType(Sex sex) {
     3.1 --- a/json/src/main/java/net/java/html/json/ComputedProperty.java	Sat Jul 04 22:41:17 2015 +0200
     3.2 +++ b/json/src/main/java/net/java/html/json/ComputedProperty.java	Wed Jul 15 22:06:19 2015 +0200
     3.3 @@ -75,4 +75,31 @@
     3.4  @Retention(RetentionPolicy.SOURCE)
     3.5  @Target(ElementType.METHOD)
     3.6  public @interface ComputedProperty {
     3.7 +    /** Name of a method to handle changes to the computed property.
     3.8 +     * By default the computed properties are read-only, however one can
     3.9 +     * make them mutable by defining a static method that takes
    3.10 +     * two parameters:
    3.11 +     * <ol>
    3.12 +     * <li>the model class</li>
    3.13 +     * <li>the value - either exactly the return the method annotated
    3.14 +     *   by this property or a superclass (like {@link Object})</li>
    3.15 +     * </ol>
    3.16 +     * Sample code snippet using the <b>write</b> feature of {@link ComputedProperty}
    3.17 +     * could look like this (assuming the {@link Model model class} named
    3.18 +     * <em>DataModel</em> has <b>int</b> property <em>value</em>):
    3.19 +     * <pre>
    3.20 +     * {@link ComputedProperty @ComputedProperty}(write="setPowerValue")
    3.21 +     * <b>static int</b> powerValue(<b>int</b> value) {
    3.22 +     *   <b>return</b> value * value;
    3.23 +     * }
    3.24 +     * <b>static void</b> setPowerValue(DataModel m, <b>int</b> value) {
    3.25 +     *   m.setValue((<b>int</b>){@link Math}.sqrt(value));
    3.26 +     * }
    3.27 +     * </pre>
    3.28 +     * 
    3.29 +     * @return the name of a method to handle changes to the computed
    3.30 +     *   property
    3.31 +     * @since 1.2
    3.32 +     */
    3.33 +    public String write() default "";
    3.34  }
     4.1 --- a/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java	Sat Jul 04 22:41:17 2015 +0200
     4.2 +++ b/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java	Wed Jul 15 22:06:19 2015 +0200
     4.3 @@ -190,7 +190,7 @@
     4.4              Map<String, Collection<String>> functionDeps = new HashMap<String, Collection<String>>();
     4.5              Prprt[] props = createProps(e, m.properties());
     4.6  
     4.7 -            if (!generateComputedProperties(body, props, e.getEnclosedElements(), propsGetSet, propsDeps)) {
     4.8 +            if (!generateComputedProperties(className, body, props, e.getEnclosedElements(), propsGetSet, propsDeps)) {
     4.9                  ok = false;
    4.10              }
    4.11              if (!generateOnChange(e, propsDeps, props, className, functionDeps)) {
    4.12 @@ -635,6 +635,7 @@
    4.13      }
    4.14  
    4.15      private boolean generateComputedProperties(
    4.16 +        String className,
    4.17          Writer w, Prprt[] fixedProps,
    4.18          Collection<? extends Element> arr, Collection<GetSet> props,
    4.19          Map<String,Collection<String[]>> deps
    4.20 @@ -655,6 +656,11 @@
    4.21                  continue;
    4.22              }
    4.23              ExecutableElement ee = (ExecutableElement)e;
    4.24 +            ExecutableElement write = null;
    4.25 +            if (!cp.write().isEmpty()) {
    4.26 +                write = findWrite(ee, (TypeElement)e.getEnclosingElement(), cp.write(), className);
    4.27 +                ok = write != null;
    4.28 +            }
    4.29              final TypeMirror rt = ee.getReturnType();
    4.30              final Types tu = processingEnv.getTypeUtils();
    4.31              TypeMirror ert = tu.erasure(rt);
    4.32 @@ -752,13 +758,28 @@
    4.33              w.write("    }\n");
    4.34              w.write("  }\n");
    4.35  
    4.36 -            props.add(new GetSet(
    4.37 -                e.getSimpleName().toString(),
    4.38 -                gs[0],
    4.39 -                null,
    4.40 -                tn,
    4.41 -                true
    4.42 -            ));
    4.43 +            if (write == null) {
    4.44 +                props.add(new GetSet(
    4.45 +                    e.getSimpleName().toString(),
    4.46 +                    gs[0],
    4.47 +                    null,
    4.48 +                    tn,
    4.49 +                    true
    4.50 +                ));
    4.51 +            } else {
    4.52 +                w.write("  public void " + gs[4] + "(" + write.getParameters().get(1).asType());
    4.53 +                w.write(" value) {\n");
    4.54 +                w.write("    " + fqn(ee.getEnclosingElement().asType(), ee) + '.' + write.getSimpleName() + "(this, value);\n");
    4.55 +                w.write("  }\n");
    4.56 +
    4.57 +                props.add(new GetSet(
    4.58 +                    e.getSimpleName().toString(),
    4.59 +                    gs[0],
    4.60 +                    gs[4],
    4.61 +                    tn,
    4.62 +                    false
    4.63 +                ));
    4.64 +            }
    4.65          }
    4.66  
    4.67          return ok;
    4.68 @@ -776,14 +797,16 @@
    4.69                  pref + n,
    4.70                  null,
    4.71                  "a" + n,
    4.72 -                null
    4.73 +                null,
    4.74 +                "set" + n
    4.75              };
    4.76          }
    4.77          return new String[]{
    4.78              pref + n,
    4.79              "set" + n,
    4.80              "a" + n,
    4.81 -            ""
    4.82 +            "",
    4.83 +            "set" + n
    4.84          };
    4.85      }
    4.86  
    4.87 @@ -2020,4 +2043,55 @@
    4.88          return false;
    4.89      }
    4.90  
    4.91 +    private ExecutableElement findWrite(ExecutableElement computedPropElem, TypeElement te, String name, String className) {
    4.92 +        String err = null;
    4.93 +        METHODS:
    4.94 +        for (Element e : te.getEnclosedElements()) {
    4.95 +            if (e.getKind() != ElementKind.METHOD) {
    4.96 +                continue;
    4.97 +            }
    4.98 +            if (!e.getSimpleName().contentEquals(name)) {
    4.99 +                continue;
   4.100 +            }
   4.101 +            if (e.equals(computedPropElem)) {
   4.102 +                continue;
   4.103 +            }
   4.104 +            if (!e.getModifiers().contains(Modifier.STATIC)) {
   4.105 +                computedPropElem = (ExecutableElement) e;
   4.106 +                err = "Would have to be static";
   4.107 +                continue;
   4.108 +            }
   4.109 +            ExecutableElement ee = (ExecutableElement) e;
   4.110 +            TypeMirror retType = computedPropElem.getReturnType();
   4.111 +            final List<? extends VariableElement> params = ee.getParameters();
   4.112 +            boolean error = false;
   4.113 +            if (params.size() != 2) {
   4.114 +                error = true;
   4.115 +            } else {
   4.116 +                String firstType = params.get(0).asType().toString();
   4.117 +                int lastDot = firstType.lastIndexOf('.');
   4.118 +                if (lastDot != -1) {
   4.119 +                    firstType = firstType.substring(lastDot + 1);
   4.120 +                }
   4.121 +                if (!firstType.equals(className)) {
   4.122 +                    error = true;
   4.123 +                }
   4.124 +                if (!processingEnv.getTypeUtils().isAssignable(retType, params.get(1).asType())) {
   4.125 +                    error = true;
   4.126 +                }
   4.127 +            }
   4.128 +            if (error) {
   4.129 +                computedPropElem = (ExecutableElement) e;
   4.130 +                err = "Write method first argument needs to be " + className + " and second " + retType + " or Object";
   4.131 +                continue;
   4.132 +            }
   4.133 +            return ee;
   4.134 +        }
   4.135 +        if (err == null) {
   4.136 +            err = "Cannot find " + name + "(" + className + ", value) method in this class";
   4.137 +        }
   4.138 +        error(err, computedPropElem);
   4.139 +        return null;
   4.140 +    }
   4.141 +
   4.142  }
     5.1 --- a/json/src/test/java/net/java/html/json/MapModelTest.java	Sat Jul 04 22:41:17 2015 +0200
     5.2 +++ b/json/src/test/java/net/java/html/json/MapModelTest.java	Wed Jul 15 22:06:19 2015 +0200
     5.3 @@ -198,6 +198,24 @@
     5.4          
     5.5          assertEquals(p.getSex(), Sex.FEMALE, "Changed");
     5.6      }
     5.7 +
     5.8 +    @Test public void changeComputedProperty() {
     5.9 +        Modelik p = Models.bind(new Modelik(), c);
    5.10 +        p.setValue(5);
    5.11 +
    5.12 +        Map m = (Map)Models.toRaw(p);
    5.13 +        Object o = m.get("powerValue");
    5.14 +        assertNotNull(o, "Value is there");
    5.15 +        assertEquals(o.getClass(), One.class);
    5.16 +
    5.17 +        One one = (One)o;
    5.18 +        assertNotNull(one.pb, "Prop binding specified");
    5.19 +
    5.20 +        assertEquals(one.pb.getValue(), 25, "Power of 5");
    5.21 +
    5.22 +        one.pb.setValue(16);
    5.23 +        assertEquals(p.getValue(), 4, "Square root of 16");
    5.24 +    }
    5.25      
    5.26      @Test public void removeViaIterator() {
    5.27          People p = Models.bind(new People(), c);
     6.1 --- a/json/src/test/java/net/java/html/json/ModelProcessorTest.java	Sat Jul 04 22:41:17 2015 +0200
     6.2 +++ b/json/src/test/java/net/java/html/json/ModelProcessorTest.java	Wed Jul 15 22:06:19 2015 +0200
     6.3 @@ -144,6 +144,72 @@
     6.4          }
     6.5      }
     6.6  
     6.7 +    @Test public void writeableComputedPropertyMissingWrite() throws IOException {
     6.8 +        String html = "<html><body>"
     6.9 +            + "</body></html>";
    6.10 +        String code = "package x.y.z;\n"
    6.11 +            + "import net.java.html.json.Model;\n"
    6.12 +            + "import net.java.html.json.Property;\n"
    6.13 +            + "import net.java.html.json.ComputedProperty;\n"
    6.14 +            + "@Model(className=\"XModel\", properties={\n"
    6.15 +            + "  @Property(name=\"prop\", type=int.class)\n"
    6.16 +            + "})\n"
    6.17 +            + "class X {\n"
    6.18 +            + "    static @ComputedProperty(write=\"setY\") int y(int prop) {\n"
    6.19 +            + "        return prop;\n"
    6.20 +            + "    }\n"
    6.21 +            + "}\n";
    6.22 +
    6.23 +        Compile c = Compile.create(html, code);
    6.24 +        assertFalse(c.getErrors().isEmpty(), "One error: " + c.getErrors());
    6.25 +        boolean ok = false;
    6.26 +        StringBuilder msgs = new StringBuilder();
    6.27 +        for (Diagnostic<? extends JavaFileObject> e : c.getErrors()) {
    6.28 +            String msg = e.getMessage(Locale.ENGLISH);
    6.29 +            if (msg.contains("Cannot find setY")) {
    6.30 +                ok = true;
    6.31 +            }
    6.32 +            msgs.append("\n").append(msg);
    6.33 +        }
    6.34 +        if (!ok) {
    6.35 +            fail("Should contain warning about non-static method:" + msgs);
    6.36 +        }
    6.37 +    }
    6.38 +
    6.39 +    @Test public void writeableComputedPropertyWrongWriteType() throws IOException {
    6.40 +        String html = "<html><body>"
    6.41 +            + "</body></html>";
    6.42 +        String code = "package x.y.z;\n"
    6.43 +            + "import net.java.html.json.Model;\n"
    6.44 +            + "import net.java.html.json.Property;\n"
    6.45 +            + "import net.java.html.json.ComputedProperty;\n"
    6.46 +            + "@Model(className=\"XModel\", properties={\n"
    6.47 +            + "  @Property(name=\"prop\", type=int.class)\n"
    6.48 +            + "})\n"
    6.49 +            + "class X {\n"
    6.50 +            + "    static @ComputedProperty(write=\"setY\") int y(int prop) {\n"
    6.51 +            + "        return prop;\n"
    6.52 +            + "    }\n"
    6.53 +            + "    static void setY(String prop) {\n"
    6.54 +            + "    }\n"
    6.55 +            + "}\n";
    6.56 +
    6.57 +        Compile c = Compile.create(html, code);
    6.58 +        assertFalse(c.getErrors().isEmpty(), "One error: " + c.getErrors());
    6.59 +        boolean ok = false;
    6.60 +        StringBuilder msgs = new StringBuilder();
    6.61 +        for (Diagnostic<? extends JavaFileObject> e : c.getErrors()) {
    6.62 +            String msg = e.getMessage(Locale.ENGLISH);
    6.63 +            if (msg.contains("Write method first argument needs to be XModel and second int or Object")) {
    6.64 +                ok = true;
    6.65 +            }
    6.66 +            msgs.append("\n").append(msg);
    6.67 +        }
    6.68 +        if (!ok) {
    6.69 +            fail("Should contain warning about non-static method:" + msgs);
    6.70 +        }
    6.71 +    }
    6.72 +
    6.73      @Test public void computedCantReturnVoid() throws IOException {
    6.74          String html = "<html><body>"
    6.75              + "</body></html>";
     7.1 --- a/json/src/test/java/net/java/html/json/ModelTest.java	Sat Jul 04 22:41:17 2015 +0200
     7.2 +++ b/json/src/test/java/net/java/html/json/ModelTest.java	Wed Jul 15 22:06:19 2015 +0200
     7.3 @@ -173,6 +173,17 @@
     7.4          assertTrue(my.mutated.contains("count"), "Count is in there: " + my.mutated);
     7.5      }
     7.6  
     7.7 +    @Test public void derivedArrayPropChange() {
     7.8 +        model.applyBindings();
     7.9 +        model.setCount(5);
    7.10 +
    7.11 +        List<String> arr = model.getRepeat();
    7.12 +        assertEquals(arr.size(), 5, "Five items: " + arr);
    7.13 +
    7.14 +        model.setRepeat(10);
    7.15 +        assertEquals(model.getCount(), 10, "Changing repeat changes count");
    7.16 +    }
    7.17 +
    7.18      @Test public void derivedPropertiesAreNotified() {
    7.19          model.applyBindings();
    7.20  
    7.21 @@ -255,11 +266,15 @@
    7.22      static void doSomething() {
    7.23      }
    7.24  
    7.25 -    @ComputedProperty
    7.26 +    @ComputedProperty(write = "setPowerValue")
    7.27      static int powerValue(int value) {
    7.28          return value * value;
    7.29      }
    7.30  
    7.31 +    static void setPowerValue(Modelik m, int value) {
    7.32 +        m.setValue((int)Math.sqrt(value));
    7.33 +    }
    7.34 +
    7.35      @OnPropertyChange({ "powerValue", "unrelated" })
    7.36      static void aPropertyChanged(Modelik m, String name) {
    7.37          m.setChangedProperty(name);
    7.38 @@ -278,6 +293,13 @@
    7.39          model.setValue(33);
    7.40          assertEquals(model.getChangedProperty(), "powerValue", "power property changed");
    7.41      }
    7.42 +    @Test public void changePowerValue() {
    7.43 +        model.setValue(3);
    7.44 +        assertEquals(model.getPowerValue(), 9, "Square");
    7.45 +        model.setPowerValue(16);
    7.46 +        assertEquals(model.getValue(), 4, "Square root");
    7.47 +        assertEquals(model.getPowerValue(), 16, "Square changed");
    7.48 +    }
    7.49      @Test public void changeUnrelated() {
    7.50          model.setUnrelated(333);
    7.51          assertEquals(model.getChangedProperty(), "unrelated", "unrelated changed");
    7.52 @@ -302,10 +324,13 @@
    7.53          return "Not allowed callback!";
    7.54      }
    7.55  
    7.56 -    @ComputedProperty
    7.57 +    @ComputedProperty(write="parseRepeat")
    7.58      static List<String> repeat(int count) {
    7.59          return Collections.nCopies(count, "Hello");
    7.60      }
    7.61 +    static void parseRepeat(Modelik m, Object v) {
    7.62 +        m.setCount((Integer)v);
    7.63 +    }
    7.64  
    7.65      public @Test void hasPersonPropertyAndComputedFullName() {
    7.66          List<Person> arr = model.getPeople();
     8.1 --- a/src/main/javadoc/overview.html	Sat Jul 04 22:41:17 2015 +0200
     8.2 +++ b/src/main/javadoc/overview.html	Wed Jul 15 22:06:19 2015 +0200
     8.3 @@ -79,7 +79,8 @@
     8.4  
     8.5          One can control {@link net.java.html.json.OnReceive#headers() HTTP request headers}
     8.6          when connecting to server using the {@link net.java.html.json.OnReceive}
     8.7 -        annotation.
     8.8 +        annotation. It is possible to have
     8.9 +        {@link net.java.html.json.ComputedProperty#write() writable computed properties}.
    8.10          Bugfix of issues <a target="_blank" href='https://netbeans.org/bugzilla/show_bug.cgi?id=250503'>250503</a>,
    8.11          <a target="_blank" href='https://netbeans.org/bugzilla/show_bug.cgi?id=252987'>252987</a>.
    8.12