diff -r 6db378e9aa7c json-tck/src/main/java/net/java/html/json/tests/KnockoutTest.java --- a/json-tck/src/main/java/net/java/html/json/tests/KnockoutTest.java Thu Jul 02 08:27:47 2015 +0200 +++ b/json-tck/src/main/java/net/java/html/json/tests/KnockoutTest.java Fri Jul 03 08:34:23 2015 +0200 @@ -136,6 +136,36 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } + + @KOTest public void modifyComputedProperty() throws Throwable { + Object exp = Utils.exposeHTML(KnockoutTest.class, + "Full name:
\n" + + "\n" + + "
\n" + ); + try { + KnockoutModel m = new KnockoutModel(); + m.getPeople().add(new Person()); + + m = Models.bind(m, newContext()); + m.getFirstPerson().setFirstName("Jarda"); + m.getFirstPerson().setLastName("Tulach"); + m.applyBindings(); + + String v = getSetInput(null); + assertEquals("Jarda Tulach", v, "Value: " + v); + + getSetInput("Mickey Mouse"); + triggerEvent("input", "change"); + + assertEquals("Mickey", m.getFirstPerson().getFirstName(), "First name updated"); + assertEquals("Mouse", m.getFirstPerson().getLastName(), "Last name updated"); + } catch (Throwable t) { + throw t; + } finally { + Utils.exposeHTML(KnockoutTest.class, ""); + } + } @KOTest public void modifyValueAssertChangeInModelOnBoolean() throws Throwable { Object exp = Utils.exposeHTML(KnockoutTest.class, diff -r 6db378e9aa7c json-tck/src/main/java/net/java/html/json/tests/PersonImpl.java --- a/json-tck/src/main/java/net/java/html/json/tests/PersonImpl.java Thu Jul 02 08:27:47 2015 +0200 +++ b/json-tck/src/main/java/net/java/html/json/tests/PersonImpl.java Fri Jul 03 08:34:23 2015 +0200 @@ -58,10 +58,16 @@ @Property(name = "address", type = Address.class) }) final class PersonImpl { - @ComputedProperty + @ComputedProperty(write = "parseNames") public static String fullName(String firstName, String lastName) { return firstName + " " + lastName; } + + static void parseNames(Person p, String fullName) { + String[] arr = fullName.split(" "); + p.setFirstName(arr[0]); + p.setLastName(arr[1]); + } @ComputedProperty public static String sexType(Sex sex) { diff -r 6db378e9aa7c json/src/main/java/net/java/html/json/ComputedProperty.java --- a/json/src/main/java/net/java/html/json/ComputedProperty.java Thu Jul 02 08:27:47 2015 +0200 +++ b/json/src/main/java/net/java/html/json/ComputedProperty.java Fri Jul 03 08:34:23 2015 +0200 @@ -75,4 +75,31 @@ @Retention(RetentionPolicy.SOURCE) @Target(ElementType.METHOD) public @interface ComputedProperty { + /** Name of a method to handle changes to the computed property. + * By default the computed properties are read-only, however one can + * make them mutable by defining a static method that takes + * two parameters: + *
    + *
  1. the model class
  2. + *
  3. the value - either exactly the return the method annotated + * by this property or a superclass (like {@link Object})
  4. + *
+ * Sample code snippet using the write feature of {@link ComputedProperty} + * could look like this (assuming the {@link Model model class} named + * DataModel has int property value): + *
+     * {@link ComputedProperty @ComputedProperty}(write="setPowerValue")
+     * static int powerValue(int value) {
+     *   return value * value;
+     * }
+     * static void setPowerValue(DataModel m, int value) {
+     *   m.setValue((int){@link Math}.sqrt(value));
+     * }
+     * 
+ * + * @return the name of a method to handle changes to the computed + * property + * @since 1.2 + */ + public String write() default ""; } diff -r 6db378e9aa7c json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java --- a/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java Thu Jul 02 08:27:47 2015 +0200 +++ b/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java Fri Jul 03 08:34:23 2015 +0200 @@ -190,7 +190,7 @@ Map> functionDeps = new HashMap>(); Prprt[] props = createProps(e, m.properties()); - if (!generateComputedProperties(body, props, e.getEnclosedElements(), propsGetSet, propsDeps)) { + if (!generateComputedProperties(className, body, props, e.getEnclosedElements(), propsGetSet, propsDeps)) { ok = false; } if (!generateOnChange(e, propsDeps, props, className, functionDeps)) { @@ -626,6 +626,7 @@ } private boolean generateComputedProperties( + String className, Writer w, Prprt[] fixedProps, Collection arr, Collection props, Map> deps @@ -646,6 +647,11 @@ continue; } ExecutableElement ee = (ExecutableElement)e; + ExecutableElement write = null; + if (!cp.write().isEmpty()) { + write = findWrite(ee, (TypeElement)e.getEnclosingElement(), cp.write(), className); + ok = write != null; + } final TypeMirror rt = ee.getReturnType(); final Types tu = processingEnv.getTypeUtils(); TypeMirror ert = tu.erasure(rt); @@ -743,13 +749,28 @@ w.write(" }\n"); w.write(" }\n"); - props.add(new GetSet( - e.getSimpleName().toString(), - gs[0], - null, - tn, - true - )); + if (write == null) { + props.add(new GetSet( + e.getSimpleName().toString(), + gs[0], + null, + tn, + true + )); + } else { + w.write(" public void " + gs[4] + "(" + write.getParameters().get(1).asType()); + w.write(" value) {\n"); + w.write(" " + fqn(ee.getEnclosingElement().asType(), ee) + '.' + write.getSimpleName() + "(this, value);\n"); + w.write(" }\n"); + + props.add(new GetSet( + e.getSimpleName().toString(), + gs[0], + gs[4], + tn, + false + )); + } } return ok; @@ -767,14 +788,16 @@ pref + n, null, "a" + n, - null + null, + "set" + n }; } return new String[]{ pref + n, "set" + n, "a" + n, - "" + "", + "set" + n }; } @@ -2009,4 +2032,55 @@ return false; } + private ExecutableElement findWrite(ExecutableElement computedPropElem, TypeElement te, String name, String className) { + String err = null; + METHODS: + for (Element e : te.getEnclosedElements()) { + if (e.getKind() != ElementKind.METHOD) { + continue; + } + if (!e.getSimpleName().contentEquals(name)) { + continue; + } + if (e.equals(computedPropElem)) { + continue; + } + if (!e.getModifiers().contains(Modifier.STATIC)) { + computedPropElem = (ExecutableElement) e; + err = "Would have to be static"; + continue; + } + ExecutableElement ee = (ExecutableElement) e; + TypeMirror retType = computedPropElem.getReturnType(); + final List params = ee.getParameters(); + boolean error = false; + if (params.size() != 2) { + error = true; + } else { + String firstType = params.get(0).asType().toString(); + int lastDot = firstType.lastIndexOf('.'); + if (lastDot != -1) { + firstType = firstType.substring(lastDot + 1); + } + if (!firstType.equals(className)) { + error = true; + } + if (!processingEnv.getTypeUtils().isAssignable(retType, params.get(1).asType())) { + error = true; + } + } + if (error) { + computedPropElem = (ExecutableElement) e; + err = "Write method first argument needs to be " + className + " and second " + retType + " or Object"; + continue; + } + return ee; + } + if (err == null) { + err = "Cannot find " + name + "(" + className + ", value) method in this class"; + } + error(err, computedPropElem); + return null; + } + } diff -r 6db378e9aa7c json/src/test/java/net/java/html/json/MapModelTest.java --- a/json/src/test/java/net/java/html/json/MapModelTest.java Thu Jul 02 08:27:47 2015 +0200 +++ b/json/src/test/java/net/java/html/json/MapModelTest.java Fri Jul 03 08:34:23 2015 +0200 @@ -198,6 +198,24 @@ assertEquals(p.getSex(), Sex.FEMALE, "Changed"); } + + @Test public void changeComputedProperty() { + Modelik p = Models.bind(new Modelik(), c); + p.setValue(5); + + Map m = (Map)Models.toRaw(p); + Object o = m.get("powerValue"); + assertNotNull(o, "Value is there"); + assertEquals(o.getClass(), One.class); + + One one = (One)o; + assertNotNull(one.pb, "Prop binding specified"); + + assertEquals(one.pb.getValue(), 25, "Power of 5"); + + one.pb.setValue(16); + assertEquals(p.getValue(), 4, "Square root of 16"); + } @Test public void removeViaIterator() { People p = Models.bind(new People(), c); diff -r 6db378e9aa7c json/src/test/java/net/java/html/json/ModelProcessorTest.java --- a/json/src/test/java/net/java/html/json/ModelProcessorTest.java Thu Jul 02 08:27:47 2015 +0200 +++ b/json/src/test/java/net/java/html/json/ModelProcessorTest.java Fri Jul 03 08:34:23 2015 +0200 @@ -144,6 +144,72 @@ } } + @Test public void writeableComputedPropertyMissingWrite() throws IOException { + String html = "" + + ""; + String code = "package x.y.z;\n" + + "import net.java.html.json.Model;\n" + + "import net.java.html.json.Property;\n" + + "import net.java.html.json.ComputedProperty;\n" + + "@Model(className=\"XModel\", properties={\n" + + " @Property(name=\"prop\", type=int.class)\n" + + "})\n" + + "class X {\n" + + " static @ComputedProperty(write=\"setY\") int y(int prop) {\n" + + " return prop;\n" + + " }\n" + + "}\n"; + + Compile c = Compile.create(html, code); + assertFalse(c.getErrors().isEmpty(), "One error: " + c.getErrors()); + boolean ok = false; + StringBuilder msgs = new StringBuilder(); + for (Diagnostic e : c.getErrors()) { + String msg = e.getMessage(Locale.ENGLISH); + if (msg.contains("Cannot find setY")) { + ok = true; + } + msgs.append("\n").append(msg); + } + if (!ok) { + fail("Should contain warning about non-static method:" + msgs); + } + } + + @Test public void writeableComputedPropertyWrongWriteType() throws IOException { + String html = "" + + ""; + String code = "package x.y.z;\n" + + "import net.java.html.json.Model;\n" + + "import net.java.html.json.Property;\n" + + "import net.java.html.json.ComputedProperty;\n" + + "@Model(className=\"XModel\", properties={\n" + + " @Property(name=\"prop\", type=int.class)\n" + + "})\n" + + "class X {\n" + + " static @ComputedProperty(write=\"setY\") int y(int prop) {\n" + + " return prop;\n" + + " }\n" + + " static void setY(String prop) {\n" + + " }\n" + + "}\n"; + + Compile c = Compile.create(html, code); + assertFalse(c.getErrors().isEmpty(), "One error: " + c.getErrors()); + boolean ok = false; + StringBuilder msgs = new StringBuilder(); + for (Diagnostic e : c.getErrors()) { + String msg = e.getMessage(Locale.ENGLISH); + if (msg.contains("Write method first argument needs to be XModel and second int or Object")) { + ok = true; + } + msgs.append("\n").append(msg); + } + if (!ok) { + fail("Should contain warning about non-static method:" + msgs); + } + } + @Test public void computedCantReturnVoid() throws IOException { String html = "" + ""; diff -r 6db378e9aa7c json/src/test/java/net/java/html/json/ModelTest.java --- a/json/src/test/java/net/java/html/json/ModelTest.java Thu Jul 02 08:27:47 2015 +0200 +++ b/json/src/test/java/net/java/html/json/ModelTest.java Fri Jul 03 08:34:23 2015 +0200 @@ -173,6 +173,17 @@ assertTrue(my.mutated.contains("count"), "Count is in there: " + my.mutated); } + @Test public void derivedArrayPropChange() { + model.applyBindings(); + model.setCount(5); + + List arr = model.getRepeat(); + assertEquals(arr.size(), 5, "Five items: " + arr); + + model.setRepeat(10); + assertEquals(model.getCount(), 10, "Changing repeat changes count"); + } + @Test public void derivedPropertiesAreNotified() { model.applyBindings(); @@ -255,11 +266,15 @@ static void doSomething() { } - @ComputedProperty + @ComputedProperty(write = "setPowerValue") static int powerValue(int value) { return value * value; } + static void setPowerValue(Modelik m, int value) { + m.setValue((int)Math.sqrt(value)); + } + @OnPropertyChange({ "powerValue", "unrelated" }) static void aPropertyChanged(Modelik m, String name) { m.setChangedProperty(name); @@ -278,6 +293,13 @@ model.setValue(33); assertEquals(model.getChangedProperty(), "powerValue", "power property changed"); } + @Test public void changePowerValue() { + model.setValue(3); + assertEquals(model.getPowerValue(), 9, "Square"); + model.setPowerValue(16); + assertEquals(model.getValue(), 4, "Square root"); + assertEquals(model.getPowerValue(), 16, "Square changed"); + } @Test public void changeUnrelated() { model.setUnrelated(333); assertEquals(model.getChangedProperty(), "unrelated", "unrelated changed"); @@ -302,10 +324,13 @@ return "Not allowed callback!"; } - @ComputedProperty + @ComputedProperty(write="parseRepeat") static List repeat(int count) { return Collections.nCopies(count, "Hello"); } + static void parseRepeat(Modelik m, Object v) { + m.setCount((Integer)v); + } public @Test void hasPersonPropertyAndComputedFullName() { List arr = model.getPeople(); diff -r 6db378e9aa7c src/main/javadoc/overview.html --- a/src/main/javadoc/overview.html Thu Jul 02 08:27:47 2015 +0200 +++ b/src/main/javadoc/overview.html Fri Jul 03 08:34:23 2015 +0200 @@ -79,7 +79,8 @@ One can control {@link net.java.html.json.OnReceive#headers() HTTP request headers} when connecting to server using the {@link net.java.html.json.OnReceive} - annotation. + annotation. It is possible to have + {@link net.java.html.json.ComputedProperty#write() writable computed properties}. Bugfix of issues 250503, 252987.