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