http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/226089a5/json/src/test/java/net/java/html/json/MapModelTest.java ---------------------------------------------------------------------- diff --git a/json/src/test/java/net/java/html/json/MapModelTest.java b/json/src/test/java/net/java/html/json/MapModelTest.java new file mode 100644 index 0000000..3a9143d --- /dev/null +++ b/json/src/test/java/net/java/html/json/MapModelTest.java @@ -0,0 +1,521 @@ +/** + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Oracle. Portions Copyright 2013-2016 Oracle. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ +package net.java.html.json; + +import net.java.html.BrwsrCtx; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.Map; +import org.netbeans.html.context.spi.Contexts; +import org.netbeans.html.json.spi.FunctionBinding; +import org.netbeans.html.json.spi.JSONCall; +import org.netbeans.html.json.spi.PropertyBinding; +import org.netbeans.html.json.spi.Technology; +import org.netbeans.html.json.spi.Transfer; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +/** + * + * @author Jaroslav Tulach + */ +public class MapModelTest { + private MapTechnology t; + private BrwsrCtx c; + + @BeforeMethod public void initTechnology() { + t = new MapTechnology(); + c = Contexts.newBuilder().register(Technology.class, t, 1). + register(Transfer.class, t, 1).build(); + } + + @Test public void isThereNoApplyBinding() throws Exception { + try { + Person.class.getMethod("applyBindings"); + } catch (NoSuchMethodException ex) { + // OK + return; + } + fail("There should be no applyBindings() method"); + } + + @Test public void isThereABinding() throws Exception { + Person p = Models.bind(new Person(), c); + Models.applyBindings(p); + assertNull(t.appliedId, "Applied globally"); + p.setFirstName("Jarda"); + + Map m = (Map)Models.toRaw(p); + Object v = m.get("firstName"); + assertNotNull(v, "Value should be in the map"); + assertEquals(v.getClass(), One.class, "It is instance of One"); + One o = (One)v; + assertEquals(o.changes, 1, "One change so far"); + assertFalse(o.pb.isReadOnly(), "Mutable property"); + + assertEquals(o.get(), "Jarda", "Value should be in the map"); + + o.set("Karle"); + + assertEquals(p.getFirstName(), "Karle", "Model value updated"); + assertEquals(o.changes, 2, "Snd change"); + } + + @Test public void applyLocally() throws Exception { + Person p = Models.bind(new Person(), c); + Models.applyBindings(p, "local"); + assertEquals(t.appliedId, "local", "Applied locally"); + } + + @Test public void dontNotifySameProperty() throws Exception { + Person p = Models.bind(new Person(), c); + p.setFirstName("Jirka"); + + Map m = (Map)Models.toRaw(p); + Object v = m.get("firstName"); + assertNotNull(v, "Value should be in the map"); + assertEquals(v.getClass(), One.class, "It is instance of One"); + One o = (One)v; + assertEquals(o.changes, 0, "No change so far the only one change happened before we connected"); + + p.setFirstName(new String("Jirka")); + assertEquals(o.changes, 0, "No change so far, the value is the same"); + + p.setFirstName("Jarda"); + assertFalse(o.pb.isReadOnly(), "Mutable property"); + + assertEquals(o.get(), "Jarda", "Value should be in the map"); + + o.set("Karle"); + + assertEquals(p.getFirstName(), "Karle", "Model value updated"); + assertEquals(o.changes, 2, "Snd change"); + } + + @Test public void canSetEnumAsString() throws Exception { + Person p = Models.bind(new Person(), c); + p.setFirstName("Jirka"); + p.setSex(Sex.MALE); + + Map m = (Map)Models.toRaw(p); + Object v = m.get("sex"); + assertNotNull(v, "Value should be in the map"); + assertEquals(v.getClass(), One.class, "It is instance of One"); + One o = (One)v; + + o.set("FEMALE"); + + assertEquals(p.getSex(), Sex.FEMALE, "Changed to female"); + } + + @Test public void derivedProperty() throws Exception { + Person p = Models.bind(new Person(), c); + + Map m = (Map)Models.toRaw(p); + Object v = m.get("fullName"); + assertNotNull(v, "Value should be in the map"); + assertEquals(v.getClass(), One.class, "It is instance of One"); + One o = (One)v; + assertTrue(o.pb.isReadOnly(), "Mutable property"); + } + + @Test public void changeSex() { + Person p = Models.bind(new Person(), c); + p.setFirstName("Trans"); + p.setSex(Sex.MALE); + + Map m = (Map)Models.toRaw(p); + Object o = m.get("changeSex"); + assertNotNull(o, "Function registered in the model"); + assertEquals(o.getClass(), One.class); + + One one = (One)o; + assertNotNull(one.fb, "Function binding specified"); + + one.fb.call(null, null); + + assertEquals(p.getSex(), Sex.FEMALE, "Changed"); + } + + @Test public void setSex() { + Person p = Models.bind(new Person(), c); + p.setFirstName("Trans"); + + Map m = (Map)Models.toRaw(p); + Object o = m.get("changeSex"); + assertNotNull(o, "Function registered in the model"); + assertEquals(o.getClass(), One.class); + + One one = (One)o; + assertNotNull(one.fb, "Function binding specified"); + + one.fb.call("FEMALE", new Object()); + + 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); + p.getNicknames().add("One"); + p.getNicknames().add("Two"); + p.getNicknames().add("Three"); + + Map m = (Map)Models.toRaw(p); + Object o = m.get("nicknames"); + assertNotNull(o, "List registered in the model"); + assertEquals(o.getClass(), One.class); + One one = (One)o; + + + assertEquals(one.changes, 0, "No change"); + + Iterator<String> it = p.getNicknames().iterator(); + assertEquals(it.next(), "One"); + assertEquals(it.next(), "Two"); + it.remove(); + assertEquals(it.next(), "Three"); + assertFalse(it.hasNext()); + + + assertEquals(one.changes, 1, "One change"); + } + + @Test public void removeViaListIterator() { + People p = Models.bind(new People(), c); + p.getNicknames().add("One"); + p.getNicknames().add("Two"); + p.getNicknames().add("Three"); + + Map m = (Map)Models.toRaw(p); + Object o = m.get("nicknames"); + assertNotNull(o, "List registered in the model"); + assertEquals(o.getClass(), One.class); + One one = (One)o; + + + assertEquals(one.changes, 0, "No change"); + + ListIterator<String> it = p.getNicknames().listIterator(1); + assertEquals(it.next(), "Two"); + it.remove(); + assertEquals(it.next(), "Three"); + assertFalse(it.hasNext()); + + + assertEquals(one.changes, 1, "One change"); + + it.set("3"); + assertEquals(p.getNicknames().get(1), "3"); + + assertEquals(one.changes, 2, "Snd change"); + } + + @Test public void subListChange() { + People p = Models.bind(new People(), c); + p.getNicknames().add("One"); + p.getNicknames().add("Two"); + p.getNicknames().add("Three"); + + Map m = (Map)Models.toRaw(p); + Object o = m.get("nicknames"); + assertNotNull(o, "List registered in the model"); + assertEquals(o.getClass(), One.class); + One one = (One)o; + + + assertEquals(one.changes, 0, "No change"); + + p.getNicknames().subList(1, 2).clear(); + + assertEquals(p.getNicknames().size(), 2, "Two elements"); + + ListIterator<String> it = p.getNicknames().listIterator(0); + assertEquals(it.next(), "One"); + assertEquals(it.next(), "Three"); + assertFalse(it.hasNext()); + + + assertEquals(one.changes, 1, "One change"); + } + + @Test public void sort() { + People p = Models.bind(new People(), c); + p.getNicknames().add("One"); + p.getNicknames().add("Two"); + p.getNicknames().add("Three"); + p.getNicknames().add("Four"); + + Map m = (Map)Models.toRaw(p); + Object o = m.get("nicknames"); + assertNotNull(o, "List registered in the model"); + assertEquals(o.getClass(), One.class); + One one = (One)o; + + + assertEquals(one.changes, 0, "No change"); + + Collections.sort(p.getNicknames()); + + Iterator<String> it = p.getNicknames().iterator(); + assertEquals(it.next(), "Four"); + assertEquals(it.next(), "One"); + assertEquals(it.next(), "Three"); + assertEquals(it.next(), "Two"); + assertFalse(it.hasNext()); + + + assertNotEquals(one.changes, 0, "At least one change"); + + if (isJDK8()) { + assertEquals(one.changes, 1, "Exactly one echange"); + } + } + + @Test public void functionWithParameters() { + People p = Models.bind(new People(), c); + p.getNicknames().add("One"); + p.getNicknames().add("Two"); + p.getNicknames().add("Three"); + + Map m = (Map)Models.toRaw(p); + Object o = m.get("inInnerClass"); + assertNotNull(o, "functiton is available"); + assertEquals(o.getClass(), One.class); + One one = (One)o; + + Map<String,Object> obj = new HashMap<String, Object>(); + obj.put("nick", "newNick"); + obj.put("x", 42); + obj.put("y", 7.7f); + final Person data = new Person("a", "b", Sex.MALE); + + one.fb.call(data, obj); + + assertEquals(p.getInfo().size(), 1, "a+b is there: " + p.getInfo()); + assertEquals(p.getInfo().get(0), data, "Expecting data: " + p.getInfo()); + + assertEquals(p.getNicknames().size(), 4, "One more nickname: " + p.getNicknames()); + assertEquals(p.getNicknames().get(3), "newNick"); + + assertEquals(p.getAge().size(), 2, "Two new values: " + p.getAge()); + assertEquals(p.getAge().get(0).intValue(), 42); + assertEquals(p.getAge().get(1).intValue(), 7); + } + + @Test + public void addAge42ThreeTimes() { + People p = Models.bind(new People(), c); + Map m = (Map)Models.toRaw(p); + assertNotNull(m); + + class Inc implements Runnable { + int cnt; + + @Override + public void run() { + cnt++; + } + } + Inc incThreeTimes = new Inc(); + p.onInfoChange(incThreeTimes); + + p.addAge42(); + p.addAge42(); + p.addAge42(); + final int[] cnt = { 0, 0 }; + p.readAddAgeCount(cnt, new Runnable() { + @Override + public void run() { + cnt[1] = 1; + } + }); + assertEquals(cnt[1], 1, "Callback called"); + assertEquals(cnt[0], 3, "Internal state kept"); + assertEquals(incThreeTimes.cnt, 3, "Property change delivered three times"); + } + + private static boolean isJDK8() { + try { + Class.forName("java.lang.FunctionalInterface"); + return true; + } catch (ClassNotFoundException ex) { + return false; + } + } + + static final class One { + int changes; + final PropertyBinding pb; + final FunctionBinding fb; + + One(Object m, PropertyBinding pb) throws NoSuchMethodException { + this.pb = pb; + this.fb = null; + } + One(Object m, FunctionBinding fb) throws NoSuchMethodException { + this.pb = null; + this.fb = fb; + } + + Object get() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + return pb.getValue(); + } + + void set(Object v) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + pb.setValue(v); + } + } + + static final class MapTechnology + implements Technology.ApplyId<Map<String,One>>, Transfer { + private Map<String, One> appliedData; + private String appliedId; + + @Override + public Map<String, One> wrapModel(Object model) { + return new HashMap<String, One>(); + } + + @Override + public void valueHasMutated(Map<String, One> data, String propertyName) { + One p = data.get(propertyName); + if (p != null) { + p.changes++; + } + } + + @Override + public void bind(PropertyBinding b, Object model, Map<String, One> data) { + try { + One o = new One(model, b); + data.put(b.getPropertyName(), o); + } catch (NoSuchMethodException ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public void expose(FunctionBinding fb, Object model, Map<String, One> data) { + try { + data.put(fb.getFunctionName(), new One(model, fb)); + } catch (NoSuchMethodException ex) { + throw new IllegalStateException(ex); + } + } + + @Override + public void applyBindings(Map<String, One> data) { + throw new UnsupportedOperationException("Never called!"); + } + + @Override + public Object wrapArray(Object[] arr) { + return arr; + } + + @Override + public void extract(Object obj, String[] props, Object[] values) { + Map<?,?> map = obj instanceof Map ? (Map<?,?>)obj : null; + for (int i = 0; i < Math.min(props.length, values.length); i++) { + if (map == null) { + values[i] = null; + } else { + values[i] = map.get(props[i]); + if (values[i] instanceof One) { + values[i] = ((One)values[i]).pb.getValue(); + } + } + } + } + + @Override + public void loadJSON(JSONCall call) { + call.notifyError(new UnsupportedOperationException()); + } + + @Override + public <M> M toModel(Class<M> modelClass, Object data) { + return modelClass.cast(data); + } + + @Override + public Object toJSON(InputStream is) throws IOException { + throw new IOException(); + } + + @Override + public void runSafe(Runnable r) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + + @Override + public void applyBindings(String id, Map<String, One> data) { + this.appliedId = id; + this.appliedData = data; + } + } +}
http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/226089a5/json/src/test/java/net/java/html/json/ModelProcessorTest.java ---------------------------------------------------------------------- diff --git a/json/src/test/java/net/java/html/json/ModelProcessorTest.java b/json/src/test/java/net/java/html/json/ModelProcessorTest.java new file mode 100644 index 0000000..9c0d336 --- /dev/null +++ b/json/src/test/java/net/java/html/json/ModelProcessorTest.java @@ -0,0 +1,823 @@ +/** + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Oracle. Portions Copyright 2013-2016 Oracle. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ +package net.java.html.json; + +import java.io.IOException; +import java.util.Locale; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import static org.testng.Assert.*; +import org.testng.annotations.Test; + +/** Verify errors emitted by the processor. + * + * @author Jaroslav Tulach + */ +public class ModelProcessorTest { + @Test public void verifyWrongType() throws IOException { + String html = "<html><body>" + + "</body></html>"; + String code = "package x.y.z;\n" + + "import net.java.html.json.Model;\n" + + "import net.java.html.json.Property;\n" + + "@Model(className=\"XModel\", properties={\n" + + " @Property(name=\"prop\", type=Runnable.class)\n" + + "})\n" + + "class X {\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<? extends JavaFileObject> e : c.getErrors()) { + String msg = e.getMessage(Locale.ENGLISH); + if (msg.contains("Runnable")) { + ok = true; + } + msgs.append("\n").append(msg); + } + if (!ok) { + fail("Should contain warning about Runnable:" + msgs); + } + } + + @Test public void verifyWrongTypeInInnerClass() throws IOException { + String html = "<html><body>" + + "</body></html>"; + String code = "package x.y.z;\n" + + "import net.java.html.json.Model;\n" + + "import net.java.html.json.Property;\n" + + "class X {\n" + + " @Model(className=\"XModel\", properties={\n" + + " @Property(name=\"prop\", type=Runnable.class)\n" + + " })\n" + + " static class Inner {\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<? extends JavaFileObject> e : c.getErrors()) { + String msg = e.getMessage(Locale.ENGLISH); + if (msg.contains("Runnable")) { + ok = true; + } + msgs.append("\n").append(msg); + } + if (!ok) { + fail("Should contain warning about Runnable:" + msgs); + } + } + + @Test public void warnOnNonStatic() throws IOException { + String html = "<html><body>" + + "</body></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" + + " @ComputedProperty 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<? extends JavaFileObject> e : c.getErrors()) { + String msg = e.getMessage(Locale.ENGLISH); + if (msg.contains("y has to be static")) { + ok = true; + } + msgs.append("\n").append(msg); + } + if (!ok) { + fail("Should contain warning about non-static method:" + msgs); + } + } + + @Test public void warnOnDuplicated() throws IOException { + String html = "<html><body>" + + "</body></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=\"prop1\", type=int.class),\n" + + " @Property(name=\"prop2\", type=int.class)\n" + + "})\n" + + "class X {\n" + + " @ComputedProperty static int y(int prop1) {\n" + + " return prop1;\n" + + " }\n" + + " @ComputedProperty static int y(int prop1, int prop2) {\n" + + " return prop2;\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<? extends JavaFileObject> e : c.getErrors()) { + String msg = e.getMessage(Locale.ENGLISH); + if (msg.contains("Cannot have the property y defined twice")) { + ok = true; + } + msgs.append("\n").append(msg); + } + if (!ok) { + fail("Should contain warning duplicated property:" + msgs); + } + } + + @Test public void warnOnDuplicatedWithNormalProp() throws IOException { + String html = "<html><body>" + + "</body></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=\"prop1\", type=int.class),\n" + + " @Property(name=\"prop2\", type=int.class)\n" + + "})\n" + + "class X {\n" + + " @ComputedProperty static int prop2(int prop1) {\n" + + " return prop1;\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<? extends JavaFileObject> e : c.getErrors()) { + String msg = e.getMessage(Locale.ENGLISH); + if (msg.contains("Cannot have the property prop2 defined twice")) { + ok = true; + } + msgs.append("\n").append(msg); + } + if (!ok) { + fail("Should contain warning duplicated property:" + msgs); + } + } + + @Test public void tooManyProperties() throws IOException { + manyProperties(255, false, 0); + } + + @Test public void tooManyArrayPropertiesIsOK() throws IOException { + manyProperties(0, true, 300); + } + + @Test public void justEnoughProperties() throws IOException { + manyProperties(254, true, 0); + } + + @Test public void justEnoughPropertiesWithArrayOne() throws IOException { + manyProperties(253, true, 300); + } + + @Test public void justEnoughPropertiesButOneArrayOne() throws IOException { + manyProperties(254, false, 300); + } + + private void manyProperties( + int cnt, boolean constructorWithParams, int arrayCnt + ) throws IOException { + String html = "<html><body>" + + "</body></html>"; + StringBuilder code = new StringBuilder(); + code.append("package x.y.z;\n" + + "import net.java.html.json.Model;\n" + + "import net.java.html.json.Property;\n" + + "@Model(className=\"XModel\", properties={\n" + ); + for (int i = 1; i <= cnt; i++) { + code.append(" @Property(name=\"prop").append(i).append("\", "); + code.append("type=int.class),\n"); + } + for (int i = 1; i <= arrayCnt; i++) { + code.append(" @Property(name=\"array").append(i).append("\", "); + code.append("array=true, "); + code.append("type=int.class),\n"); + } + code.append("" + + "})\n" + + "class X {\n" + + " static {\n" + + " new XModel();\n" + + " new XModel(" + ); + if (constructorWithParams) { + code.append("0"); + for (int i = 1; i < cnt; i++) { + code.append(",\n").append(i); + } + } + code.append(");\n" + + " }\n" + + "}\n" + ); + + Compile c = Compile.create(html, code.toString()); + assertTrue(c.getErrors().isEmpty(), "Compiles OK: " + c.getErrors()); + } + + @Test public void writeableComputedPropertyMissingWrite() throws IOException { + String html = "<html><body>" + + "</body></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<? extends JavaFileObject> 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 = "<html><body>" + + "</body></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(XModel model, 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<? extends JavaFileObject> 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 writeableComputedPropertyReturnsVoid() throws IOException { + String html = "<html><body>" + + "</body></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 Number setY(XModel model, int 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<? extends JavaFileObject> e : c.getErrors()) { + String msg = e.getMessage(Locale.ENGLISH); + if (msg.contains("Write method has to return void")) { + 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 = "<html><body>" + + "</body></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" + + " @ComputedProperty static void y(int 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<? extends JavaFileObject> e : c.getErrors()) { + String msg = e.getMessage(Locale.ENGLISH); + if (msg.contains("y cannot return void")) { + ok = true; + } + msgs.append("\n").append(msg); + } + if (!ok) { + fail("Should contain warning about non-static method:" + msgs); + } + } + + @Test public void computedCantReturnRunnable() throws IOException { + String html = "<html><body>" + + "</body></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" + + " @ComputedProperty static Runnable y(int prop) {\n" + + " return null;\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<? extends JavaFileObject> e : c.getErrors()) { + String msg = e.getMessage(Locale.ENGLISH); + if (msg.contains("y cannot return java.lang.Runnable")) { + ok = true; + } + msgs.append("\n").append(msg); + } + if (!ok) { + fail("Should contain warning about non-static method:" + msgs); + } + } + + @Test public void canWeCompileWithJDK1_5SourceLevel() throws IOException { + String html = "<html><body>" + + "</body></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=long.class)\n" + + "})\n" + + "class X {\n" + + " @ComputedProperty static double derived(long prop) { return prop; }" + + "}\n"; + + Compile c = Compile.create(html, code, "1.5"); + assertTrue(c.getErrors().isEmpty(), "No errors: " + c.getErrors()); + } + + @Test public void instanceNeedsDefaultConstructor() throws IOException { + String html = "<html><body>" + + "</body></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\", instance=true, properties={\n" + + " @Property(name=\"prop\", type=long.class)\n" + + "})\n" + + "class X {\n" + + " X(int x) {}\n" + + "}\n"; + + Compile c = Compile.create(html, code); + c.assertError("Needs non-private default constructor when instance=true"); + } + + @Test public void instanceNeedsNonPrivateConstructor() throws IOException { + String html = "<html><body>" + + "</body></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\", instance=true, properties={\n" + + " @Property(name=\"prop\", type=long.class)\n" + + "})\n" + + "class X {\n" + + " private X() {}\n" + + "}\n"; + + Compile c = Compile.create(html, code); + c.assertError("Needs non-private default constructor when instance=true"); + } + + @Test public void instanceNoConstructorIsOK() throws IOException { + String html = "<html><body>" + + "</body></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\", instance=true, properties={\n" + + " @Property(name=\"prop\", type=long.class)\n" + + "})\n" + + "class X {\n" + + "}\n"; + + Compile c = Compile.create(html, code); + c.assertNoErrors(); + } + + @Test public void putNeedsDataArgument() throws Exception { + needsAnArg("PUT"); + } + + @Test public void postNeedsDataArgument() throws Exception { + needsAnArg("POST"); + } + + private void needsAnArg(String method) throws Exception { + String html = "<html><body>" + + "</body></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.OnReceive;\n" + + "@Model(className=\"XModel\", properties={\n" + + " @Property(name=\"prop\", type=long.class)\n" + + "})\n" + + "class X {\n" + + " @Model(className=\"PQ\", properties={})\n" + + " class PImpl {\n" + + " }\n" + + " @OnReceive(method=\"" + method + "\", url=\"whereever\")\n" + + " static void obtained(XModel m, PQ p) { }\n" + + "}\n"; + + Compile c = Compile.create(html, code); + assertFalse(c.getErrors().isEmpty(), "One error: " + c.getErrors()); + for (Diagnostic<? extends JavaFileObject> diagnostic : c.getErrors()) { + String msg = diagnostic.getMessage(Locale.ENGLISH); + if (msg.contains("specify a data()")) { + return; + } + } + fail("Needs an error message about missing data():\n" + c.getErrors()); + + } + + + @Test public void jsonNeedsToUseGet () throws Exception { + String html = "<html><body>" + + "</body></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.OnReceive;\n" + + "@Model(className=\"XModel\", properties={\n" + + " @Property(name=\"prop\", type=long.class)\n" + + "})\n" + + "class X {\n" + + " @Model(className=\"PQ\", properties={})\n" + + " class PImpl {\n" + + " }\n" + + " @OnReceive(method=\"POST\", jsonp=\"callback\", url=\"whereever\")\n" + + " static void obtained(XModel m, PQ p) { }\n" + + "}\n"; + + Compile c = Compile.create(html, code); + assertFalse(c.getErrors().isEmpty(), "One error: " + c.getErrors()); + for (Diagnostic<? extends JavaFileObject> diagnostic : c.getErrors()) { + String msg = diagnostic.getMessage(Locale.ENGLISH); + if (msg.contains("JSONP works only with GET")) { + return; + } + } + fail("Needs an error message about wrong method:\n" + c.getErrors()); + + } + + @Test public void noHeadersForWebSockets() throws Exception { + String html = "<html><body>" + + "</body></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.OnReceive;\n" + + "@Model(className=\"XModel\", properties={\n" + + " @Property(name=\"prop\", type=long.class)\n" + + "})\n" + + "class X {\n" + + " @Model(className=\"PQ\", properties={})\n" + + " class PImpl {\n" + + " }\n" + + " @OnReceive(method=\"WebSocket\", data = PQ.class, headers=\"SomeHeader: {some}\", url=\"whereever\")\n" + + " static void obtained(XModel m, PQ p) { }\n" + + "}\n"; + + Compile c = Compile.create(html, code); + assertFalse(c.getErrors().isEmpty(), "One error: " + c.getErrors()); + for (Diagnostic<? extends JavaFileObject> diagnostic : c.getErrors()) { + String msg = diagnostic.getMessage(Locale.ENGLISH); + if (msg.contains("WebSocket spec does not support headers")) { + return; + } + } + fail("Needs an error message about headers:\n" + c.getErrors()); + + } + + @Test public void webSocketsWithoutDataIsError() throws Exception { + String html = "<html><body>" + + "</body></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.OnReceive;\n" + + "@Model(className=\"XModel\", properties={\n" + + " @Property(name=\"prop\", type=long.class)\n" + + "})\n" + + "class X {\n" + + " @Model(className=\"PQ\", properties={})\n" + + " class PImpl {\n" + + " }\n" + + " @OnReceive(method=\"WebSocket\", url=\"whereever\")\n" + + " static void obtained(XModel m, PQ p) { }\n" + + "}\n"; + + Compile c = Compile.create(html, code); + assertFalse(c.getErrors().isEmpty(), "One error: " + c.getErrors()); + for (Diagnostic<? extends JavaFileObject> diagnostic : c.getErrors()) { + String msg = diagnostic.getMessage(Locale.ENGLISH); + if (msg.contains("eeds to specify a data()")) { + return; + } + } + fail("Needs data attribute :\n" + c.getErrors()); + } + + @Test public void noNewLinesInHeaderLines() throws Exception { + String html = "<html><body>" + + "</body></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.OnReceive;\n" + + "@Model(className=\"XModel\", properties={\n" + + " @Property(name=\"prop\", type=long.class)\n" + + "})\n" + + "class X {\n" + + " @Model(className=\"PQ\", properties={})\n" + + " class PImpl {\n" + + " }\n" + + " @OnReceive(headers=\"SomeHeader\\n: {some}\", url=\"whereever\")\n" + + " static void obtained(XModel m, PQ p) { }\n" + + "}\n"; + + Compile c = Compile.create(html, code); + assertFalse(c.getErrors().isEmpty(), "One error: " + c.getErrors()); + for (Diagnostic<? extends JavaFileObject> diagnostic : c.getErrors()) { + String msg = diagnostic.getMessage(Locale.ENGLISH); + if (msg.contains("Header line cannot contain line separator")) { + return; + } + } + fail("Needs an error message about headers:\n" + c.getErrors()); + + } + + @Test public void noReturnInHeaderLines() throws Exception { + String html = "<html><body>" + + "</body></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.OnReceive;\n" + + "@Model(className=\"XModel\", properties={\n" + + " @Property(name=\"prop\", type=long.class)\n" + + "})\n" + + "class X {\n" + + " @Model(className=\"PQ\", properties={})\n" + + " class PImpl {\n" + + " }\n" + + " @OnReceive(headers=\"Some\\rHeader: {some}\", url=\"whereever\")\n" + + " static void obtained(XModel m, PQ p) { }\n" + + "}\n"; + + Compile c = Compile.create(html, code); + assertFalse(c.getErrors().isEmpty(), "One error: " + c.getErrors()); + for (Diagnostic<? extends JavaFileObject> diagnostic : c.getErrors()) { + String msg = diagnostic.getMessage(Locale.ENGLISH); + if (msg.contains("Header line cannot contain line separator")) { + return; + } + } + fail("Needs an error message about headers:\n" + c.getErrors()); + + } + + @Test public void onErrorHasToExist() throws IOException { + Compile res = Compile.create("", "package x;\n" + + "@net.java.html.json.Model(className=\"MyModel\", properties= {\n" + + " @net.java.html.json.Property(name=\"x\", type=String.class)\n" + + "})\n" + + "class UseOnReceive {\n" + + " @net.java.html.json.OnReceive(url=\"http://nowhere.com\", onError=\"doesNotExist\")\n" + + " static void onMessage(MyModel model, String value) {\n" + + " }\n" + + "}\n" + ); + res.assertErrors(); + res.assertError("not find doesNotExist"); + } + + @Test public void usingListIsOK() throws IOException { + Compile res = Compile.create("", "package x;\n" + + "@net.java.html.json.Model(className=\"MyModel\", properties= {\n" + + " @net.java.html.json.Property(name=\"x\", type=String.class)\n" + + "})\n" + + "class UseOnReceive {\n" + + " @net.java.html.json.OnReceive(url=\"http://nowhere.com\")\n" + + " static void onMessage(MyModel model, java.util.List<MyData> value) {\n" + + " }\n" + + "\n" + + " @net.java.html.json.Model(className=\"MyData\", properties={\n" + + " })\n" + + " static class MyDataModel {\n" + + " }\n" + + "}\n" + ); + res.assertNoErrors(); + } + + @Test public void functionAndPropertyCollide() throws IOException { + Compile res = Compile.create("", "package x;\n" + + "@net.java.html.json.Model(className=\"MyModel\", properties= {\n" + + " @net.java.html.json.Property(name=\"x\", type=String.class)\n" + + "})\n" + + "class Collision {\n" + + " @net.java.html.json.Function\n" + + " static void x(MyModel model, String value) {\n" + + " }\n" + + "}\n" + ); + res.assertErrors(); + res.assertError("cannot have the name"); + } + + @Test public void twoPropertiesCollide() throws IOException { + Compile res = Compile.create("", "package x;\n" + + "@net.java.html.json.Model(className=\"MyModel\", properties= {\n" + + " @net.java.html.json.Property(name=\"x\", type=String.class),\n" + + " @net.java.html.json.Property(name=\"x\", type=int.class)\n" + + "})\n" + + "class Collision {\n" + + "}\n" + ); + res.assertErrors(); + res.assertError("Cannot have the property"); + } + + @Test public void propertyAndComputedOneCollide() throws IOException { + Compile res = Compile.create("", "package x;\n" + + "@net.java.html.json.Model(className=\"MyModel\", properties= {\n" + + " @net.java.html.json.Property(name=\"x\", type=String.class),\n" + + "})\n" + + "class Collision {\n" + + " @net.java.html.json.ComputedProperty static int x(String x) {\n" + + " return x.length();\n" + + " }\n" + + "}\n" + ); + res.assertErrors(); + res.assertError("Cannot have the property"); + } + + @Test public void onWebSocketJustTwoArgs() throws IOException { + Compile res = Compile.create("", "package x;\n" + + "@net.java.html.json.Model(className=\"MyModel\", properties= {\n" + + " @net.java.html.json.Property(name=\"x\", type=String.class)\n" + + "})\n" + + "class UseOnReceive {\n" + + " @net.java.html.json.OnReceive(url=\"http://nowhere.com\", method=\"WebSocket\", data=String.class)\n" + + " static void onMessage(MyModel model, String value, int arg) {\n" + + " }\n" + + "}\n" + ); + res.assertErrors(); + res.assertError("only have two arg"); + } + + @Test public void onErrorWouldHaveToBeStatic() throws IOException { + Compile res = Compile.create("", "package x;\n" + + "@net.java.html.json.Model(className=\"MyModel\", properties= {\n" + + " @net.java.html.json.Property(name=\"x\", type=String.class)\n" + + "})\n" + + "class UseOnReceive {\n" + + " @net.java.html.json.OnReceive(url=\"http://nowhere.com\", onError=\"notStatic\")\n" + + " static void onMessage(MyModel model, String value) {\n" + + " }\n" + + " void notStatic(Exception e) {}\n" + + "}\n" + ); + res.assertErrors(); + res.assertError("have to be static"); + } + + @Test public void onErrorMustAcceptExceptionArgument() throws IOException { + Compile res = Compile.create("", "package x;\n" + + "@net.java.html.json.Model(className=\"MyModel\", properties= {\n" + + " @net.java.html.json.Property(name=\"x\", type=String.class)\n" + + "})\n" + + "class UseOnReceive {\n" + + " @net.java.html.json.OnReceive(url=\"http://nowhere.com\", onError=\"subclass\")\n" + + " static void onMessage(MyModel model, String value) {\n" + + " }\n" + + " static void subclass(java.io.IOException e) {}\n" + + "}\n" + ); + res.assertErrors(); + res.assertError("Error method first argument needs to be MyModel and second Exception"); + } +} http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/226089a5/json/src/test/java/net/java/html/json/ModelTest.java ---------------------------------------------------------------------- diff --git a/json/src/test/java/net/java/html/json/ModelTest.java b/json/src/test/java/net/java/html/json/ModelTest.java new file mode 100644 index 0000000..bb4a7be --- /dev/null +++ b/json/src/test/java/net/java/html/json/ModelTest.java @@ -0,0 +1,506 @@ +/** + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Oracle. Portions Copyright 2013-2016 Oracle. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ +package net.java.html.json; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.concurrent.Executor; +import net.java.html.BrwsrCtx; +import org.netbeans.html.context.spi.Contexts; +import org.netbeans.html.json.spi.FunctionBinding; +import org.netbeans.html.json.spi.PropertyBinding; +import org.netbeans.html.json.spi.Technology; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +/** + * + * @author Jaroslav Tulach + */ +@Model(className = "Modelik", builder = "change", targetId = "", properties = { + @Property(name = "value", type = int.class), + @Property(name = "count", type = int.class), + @Property(name = "unrelated", type = long.class), + @Property(name = "names", type = String.class, array = true), + @Property(name = "values", type = int.class, array = true), + @Property(name = "people", type = Person.class, array = true), + @Property(name = "changedProperty", type=String.class) +}) +public class ModelTest { + private MockTechnology my; + private Modelik model; + private static Modelik leakedModel; + + @BeforeMethod + public void createModel() { + my = new MockTechnology(); + final BrwsrCtx c = Contexts.newBuilder().register(Technology.class, my, 1).build(); + model = Models.bind(new Modelik(), c); + } + + @Test public void classGeneratedWithSetterGetter() { + model.setValue(10); + assertEquals(10, model.getValue(), "Value changed"); + } + + @Test public void computedMethod() { + model.setValue(4); + assertEquals(16, model.getPowerValue()); + } + + @Test public void equalsAndHashCode() { + Modelik m1 = new Modelik(); + m1.setValue(10); + m1.setCount(20); + m1.setUnrelated(30); + m1.setChangedProperty("changed"); + m1.getNames().add("firstName"); + Modelik m2 = new Modelik(). + changeValue(10). + changeCount(20). + changeUnrelated(30). + changeChangedProperty("changed"). + changeNames("firstName"); + + assertTrue(m1.equals(m2), "They are the same"); + assertEquals(m1.hashCode(), m2.hashCode(), "Hashcode is the same"); + + m1.setCount(33); + + assertFalse(m1.equals(m2), "No longer the same"); + assertFalse(m1.hashCode() == m2.hashCode(), "No longe is hashcode is the same"); + } + + @Test public void arrayIsMutable() { + assertEquals(model.getNames().size(), 0, "Is empty"); + model.getNames().add("Jarda"); + assertEquals(model.getNames().size(), 1, "One element"); + } + + @Test public void arrayChangesNotNotifiedUntilInitied() { + model.getNames().add("Hello"); + assertTrue(my.mutated.isEmpty(), "No change now " + my.mutated); + model.getNames().remove("Hello"); + assertTrue(my.mutated.isEmpty(), "No change still " + my.mutated); + assertTrue(model.getNames().isEmpty(), "No empty"); + } + + @Test public void arrayChangesNotified() { + Models.applyBindings(model); + model.getNames().add("Hello"); + + assertFalse(my.mutated.isEmpty(), "There was a change" + my.mutated); + assertTrue(my.mutated.contains("names"), "Change in names property: " + my.mutated); + + my.mutated.clear(); + + Iterator<String> it = model.getNames().iterator(); + assertEquals(it.next(), "Hello"); + it.remove(); + + assertFalse(my.mutated.isEmpty(), "There was a change" + my.mutated); + assertTrue(my.mutated.contains("names"), "Change in names property: " + my.mutated); + + my.mutated.clear(); + + ListIterator<String> lit = model.getNames().listIterator(); + lit.add("Jarda"); + + assertFalse(my.mutated.isEmpty(), "There was a change" + my.mutated); + assertTrue(my.mutated.contains("names"), "Change in names property: " + my.mutated); + } + + @Test public void autoboxedArray() { + model.getValues().add(10); + + assertEquals(model.getValues().get(0), Integer.valueOf(10), "Really ten"); + } + + @Test public void derivedArrayProp() { + model.applyBindings(); + model.setCount(10); + + List<String> arr = model.getRepeat(); + assertEquals(arr.size(), 10, "Ten items: " + arr); + + my.mutated.clear(); + + model.setCount(5); + + arr = model.getRepeat(); + assertEquals(arr.size(), 5, "Five items: " + arr); + + assertEquals(my.mutated.size(), 2, "Two properties changed: " + my.mutated); + assertTrue(my.mutated.contains("repeat"), "Array is in there: " + my.mutated); + assertTrue(my.mutated.contains("count"), "Count is in there: " + my.mutated); + } + + @Test public void derivedArrayPropChange() { + model.applyBindings(); + model.setCount(5); + + List<String> 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(); + + model.setValue(33); + + // not interested in change of this property + my.mutated.remove("changedProperty"); + + assertEquals(my.mutated.size(), 2, "Two properties changed: " + my.mutated); + assertTrue(my.mutated.contains("powerValue"), "Power value is in there: " + my.mutated); + assertTrue(my.mutated.contains("value"), "Simple value is in there: " + my.mutated); + + my.mutated.clear(); + + model.setUnrelated(44); + + + // not interested in change of this property + my.mutated.remove("changedProperty"); + assertEquals(my.mutated.size(), 1, "One property changed: " + my.mutated); + assertTrue(my.mutated.contains("unrelated"), "Its name is unrelated"); + } + + @Test public void computedPropertyCannotWriteToModel() { + leakedModel = model; + try { + String res = model.getNotAllowedWrite(); + fail("We should not be allowed to write to the model: " + res); + } catch (IllegalStateException ex) { + // OK, we can't read + } + } + + @Test public void computedPropertyCannotReadToModel() { + leakedModel = model; + try { + String res = model.getNotAllowedRead(); + fail("We should not be allowed to read from the model: " + res); + } catch (IllegalStateException ex) { + // OK, we can't read + } + } + + @OnReceive(url = "{protocol}://{host}?query={query}", data = Person.class, onError = "errorState") + static void loadPeople(Modelik thiz, People p) { + Modelik m = null; + m.applyBindings(); + m.loadPeople("http", "apidesign.org", "query", new Person()); + } + + static void errorState(Modelik thiz, Exception ex) { + + } + + @OnReceive(url="{url}", headers={ + "Easy: {easy}", + "H-a+r!d?e.r: {harder}", + "H-a+r!d?e's\"t: {harder}", + "Repeat-ed: {rep}", + "Repeat+ed: {rep}", + "Same-URL: {url}" + }) + static void fetchPeopleWithHeaders(Modelik model, People p) { + model.fetchPeopleWithHeaders("url", "easy", "harder", "rep"); + } + + @OnReceive(url = "{protocol}://{host}?callback={back}&query={query}", jsonp = "back") + static void loadPeopleViaJSONP(Modelik thiz, People p) { + Modelik m = null; + m.applyBindings(); + m.loadPeopleViaJSONP("http", "apidesign.org", "query"); + } + + @OnReceive(url = "{rep}://{rep}") + static void repeatedTest(Modelik thiz, People p) { + thiz.repeatedTest("justOneParameterRep"); + } + + @Function + static void doSomething() { + } + + @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); + } + + @OnPropertyChange({ "values" }) + static void anArrayPropertyChanged(String name, Modelik m) { + m.setChangedProperty(name); + } + + @Test public void changeAnything() { + model.setCount(44); + assertNull(model.getChangedProperty(), "No observed value change"); + } + @Test public void changeValue() { + 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"); + } + + @Test public void changeInArray() { + model.getValues().add(10); + assertNull(model.getChangedProperty(), "No change before applyBindings"); + model.applyBindings(); + model.getValues().add(10); + assertEquals(model.getChangedProperty(), "values", "Something added into the array"); + } + + @ComputedProperty + static String notAllowedRead() { + return "Not allowed callback: " + leakedModel.getUnrelated(); + } + + @ComputedProperty + static String notAllowedWrite() { + leakedModel.setUnrelated(11); + return "Not allowed callback!"; + } + + @ComputedProperty(write="parseRepeat") + static List<String> repeat(int count) { + return Collections.nCopies(count, "Hello"); + } + static void parseRepeat(Modelik m, Object v) { + m.setCount((Integer)v); + } + + public @Test void hasPersonPropertyAndComputedFullName() { + List<Person> arr = model.getPeople(); + assertEquals(arr.size(), 0, "By default empty"); + Person p = null; + if (p != null) { + String fullNameGenerated = p.getFullName(); + assertNotNull(fullNameGenerated); + } + } + + public @Test void computedListIsOfTypeString() { + Person p = new Person("1st", "2nd", Sex.MALE); + String first = p.getBothNames().get(0); + String last = p.getBothNames().get(1); + assertEquals(first, "1st"); + assertEquals(last, "2nd"); + } + + @Model(className = "Inner", instance = true, properties = { + @Property(name = "x", type = int.class), + @Property(name = "y", type = int.class) + }) + static final class InnerCntrl { + private BrwsrCtx ctx; + + @ModelOperation + void init(Inner model, BrwsrCtx ctx) { + this.ctx = ctx; + } + + @Function + void setYToTen(Inner model) { + assertCtx(); + model.setY(10); + } + + @ModelOperation + void modelYToTen(Inner model) { + assertCtx(); + model.setY(10); + } + + @OnPropertyChange("y") + void increment(Inner model) { + model.setX(model.getX() + 1); + assertCtx(); + } + + private void assertCtx() { + BrwsrCtx realCtx = BrwsrCtx.findDefault(InnerCntrl.class); + assertSame(realCtx, ctx, "Proper Ctx is provided"); + } + } + +// directly using setters doesn't set the context currently +// @Test +// public void incrementXOnChangeOfY() { +// doIncreementXOnChangeOfY(0); +// } + + @Test + public void incrementXOnChangeOfYViaFunction() { + doIncreementXOnChangeOfY(1); + } + + @Test + public void incrementXOnChangeOfYViaModel() { + doIncreementXOnChangeOfY(2); + } + + private void doIncreementXOnChangeOfY(int modificationType) { + class Exec implements Executor { + int cnt; + @Override + public void execute(Runnable command) { + cnt++; + command.run(); + } + + final void assertCount(int expected, String msg) { + assertEquals(cnt, expected, msg); + cnt = 0; + } + } + MapModelTest.MapTechnology tech = new MapModelTest.MapTechnology(); + Exec exec = new Exec(); + final BrwsrCtx c = Contexts.newBuilder(). + register(Technology.class, tech, 1). + register(Executor.class, exec, 5). + build(); + + Inner model = Models.bind(new Inner(), c); + exec.assertCount(0, "Executor not used for anything yet"); + model.init(c); + exec.assertCount(1, "Executor used for initialization"); + + Models.applyBindings(model); + + assertEquals(model.getX(), 0, "Zero"); + assertEquals(model.getY(), 0, "Zero too"); + int execUse; + if (modificationType != 1) { + model.modelYToTen(); + execUse = 3; + } else { + Object raw = Models.toRaw(model); + assertTrue(raw instanceof Map); + Map<?,?> map = (Map<?,?>) raw; + MapModelTest.One one = (MapModelTest.One) map.get("setYToTen"); + assertNotNull(one); + one.fb.call(model, null); + execUse = 3; + } + assertEquals(model.getX(), 1, "One"); + assertEquals(model.getY(), 10, "Ten"); + exec.assertCount(execUse, "Executor used"); + } + + private static class MockTechnology implements Technology<Object> { + private final List<String> mutated = new ArrayList<String>(); + + @Override + public Object wrapModel(Object model) { + return this; + } + + @Override + public void valueHasMutated(Object data, String propertyName) { + mutated.add(propertyName); + } + + @Override + public void bind(PropertyBinding b, Object model, Object data) { + } + + @Override + public void expose(FunctionBinding fb, Object model, Object d) { + } + + @Override + public void applyBindings(Object data) { + } + + @Override + public Object wrapArray(Object[] arr) { + return arr; + } + + @Override + public <M> M toModel(Class<M> modelClass, Object data) { + return modelClass.cast(data); + } + + @Override + public void runSafe(Runnable r) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/226089a5/json/src/test/java/net/java/html/json/ModelsTest.java ---------------------------------------------------------------------- diff --git a/json/src/test/java/net/java/html/json/ModelsTest.java b/json/src/test/java/net/java/html/json/ModelsTest.java new file mode 100644 index 0000000..7e7c5b0 --- /dev/null +++ b/json/src/test/java/net/java/html/json/ModelsTest.java @@ -0,0 +1,72 @@ +/** + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Oracle. Portions Copyright 2013-2016 Oracle. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ +package net.java.html.json; + +import static org.testng.Assert.*; +import org.testng.annotations.Test; + +/** + * + * @author Jaroslav Tulach + */ +public class ModelsTest { + + public ModelsTest() { + } + + @Test public void peopleAreModel() { + assertTrue(Models.isModel(People.class), "People are generated class"); + } + + @Test public void personIsModel() { + assertTrue(Models.isModel(Person.class), "Person is generated class"); + } + + @Test public void implClassIsNotModel() { + assertFalse(Models.isModel(PersonImpl.class), "Impl is not model"); + } + + @Test public void randomClassIsNotModel() { + assertFalse(Models.isModel(StringBuilder.class), "JDK classes are not model"); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/226089a5/json/src/test/java/net/java/html/json/OperationTest.java ---------------------------------------------------------------------- diff --git a/json/src/test/java/net/java/html/json/OperationTest.java b/json/src/test/java/net/java/html/json/OperationTest.java new file mode 100644 index 0000000..db30837 --- /dev/null +++ b/json/src/test/java/net/java/html/json/OperationTest.java @@ -0,0 +1,125 @@ +/** + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Oracle. Portions Copyright 2013-2016 Oracle. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ +package net.java.html.json; + +import java.io.IOException; +import java.util.Arrays; +import net.java.html.BrwsrCtx; +import org.netbeans.html.context.spi.Contexts; +import static org.testng.Assert.*; +import org.testng.annotations.Test; + +/** + * + * @author Jaroslav Tulach + */ +@Model(className = "OpModel", properties = { + @Property(name = "names", type = String.class, array = true) +}) +public class OperationTest { + @ModelOperation static void add(OpModel m, String name, BrwsrCtx exp) { + assertSame(BrwsrCtx.findDefault(OpModel.class), exp, "Context is passed in"); + m.getNames().add(name); + } + + @ModelOperation static void add(OpModel m, int times, String name) throws IOException { + while (times-- > 0) { + m.getNames().add(name.toUpperCase()); + } + } + + @ModelOperation static void copy(OpModel m, OpModel orig) { + m.getNames().clear(); + m.getNames().addAll(orig.getNames()); + } + + @Test public void addOneToTheModel() { + BrwsrCtx ctx = Contexts.newBuilder().build(); + OpModel m = Models.bind(new OpModel("One"), ctx); + m.add("Second", ctx); + assertEquals(m.getNames().size(), 2, "Both are there: " + m.getNames()); + } + + @Test public void addTwoUpperCasesToTheModel() { + BrwsrCtx ctx = Contexts.newBuilder().build(); + OpModel m = Models.bind(new OpModel("One"), ctx); + m.add(2, "Second"); + assertEquals(m.getNames().size(), 3, "Both are there: " + m.getNames()); + assertEquals(m.getNames().get(1), "SECOND", "Converted to upper case"); + assertEquals(m.getNames().get(2), "SECOND", "Also converted to upper case"); + } + + @Test public void noAnnonymousInnerClass() { + int cnt = 0; + for (Class<?> c : OpModel.class.getDeclaredClasses()) { + cnt++; + int dolar = c.getName().lastIndexOf('$'); + assertNotEquals(dolar, -1, "There is dolar in : " + c.getName()); + String res = c.getName().substring(dolar + 1); + try { + int number = Integer.parseInt(res); + if (number == 1) { + // one is OK, #2 was a problem + continue; + } + fail("There seems to annonymous innerclass! " + c.getName() + "\nImplements: " + + Arrays.toString(c.getInterfaces()) + " extends: " + c.getSuperclass() + ); + } catch (NumberFormatException ex) { + // OK, go on + } + } + if (cnt == 0) { + fail("There should be at least one inner class: " + cnt); + } + } + + @Test public void copyOperation() { + OpModel orig = new OpModel("Ahoj", "Jardo"); + OpModel n = new OpModel(); + n.copy(orig); + assertEquals(n.getNames().size(), 2, "Two elems"); + assertEquals(n.getNames().get(0), "Ahoj", "1st"); + assertEquals(n.getNames().get(1), "Jardo", "2nd"); + } +} http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/226089a5/json/src/test/java/net/java/html/json/PersonImpl.java ---------------------------------------------------------------------- diff --git a/json/src/test/java/net/java/html/json/PersonImpl.java b/json/src/test/java/net/java/html/json/PersonImpl.java new file mode 100644 index 0000000..581bb6a --- /dev/null +++ b/json/src/test/java/net/java/html/json/PersonImpl.java @@ -0,0 +1,135 @@ +/** + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Oracle. Portions Copyright 2013-2016 Oracle. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ +package net.java.html.json; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +/** + * + * @author Jaroslav Tulach + */ +@Model(className = "Person", properties = { + @Property(name = "firstName", type = String.class), + @Property(name = "lastName", type = String.class), + @Property(name = "sex", type = Sex.class) +}) +final class PersonImpl { + @ComputedProperty + public static String fullName(String firstName, String lastName) { + return firstName + " " + lastName; + } + + @ComputedProperty + public static List<String> bothNames(String firstName, String lastName) { + return Arrays.asList(firstName, lastName); + } + + @ComputedProperty + public static String sexType(Sex sex) { + return sex == null ? "unknown" : sex.toString(); + } + + @ComputedProperty static Sex attractedBy(Sex sex) { + if (sex == null) { + return null; + } + return sex == Sex.MALE ? Sex.FEMALE : Sex.MALE; + } + + @Function + static void changeSex(Person p, String data) { + if (data != null) { + p.setSex(Sex.valueOf(data)); + return; + } + if (p.getSex() == Sex.MALE) { + p.setSex(Sex.FEMALE); + } else { + p.setSex(Sex.MALE); + } + } + + @Model(className = "People", instance = true, targetId="myPeople", properties = { + @Property(array = true, name = "info", type = Person.class), + @Property(array = true, name = "nicknames", type = String.class), + @Property(array = true, name = "age", type = int.class), + @Property(array = true, name = "sex", type = Sex.class) + }) + public static class PeopleImpl { + private int addAgeCount; + private Runnable onInfoChange; + + @ModelOperation void onInfoChange(People self, Runnable r) { + onInfoChange = r; + } + + @ModelOperation void addAge42(People p) { + p.getAge().add(42); + addAgeCount++; + } + + @OnReceive(url = "url", method = "WebSocket", data = String.class) + void innerClass(People p, String d) { + } + + @Function void inInnerClass(People p, Person data, int x, double y, String nick) throws IOException { + p.getInfo().add(data); + p.getAge().add(x); + p.getAge().add((int)y); + p.getNicknames().add(nick); + } + + @ModelOperation void readAddAgeCount(People p, int[] holder, Runnable whenDone) { + holder[0] = addAgeCount; + whenDone.run(); + } + + @OnPropertyChange("age") void infoChange(People p) { + if (onInfoChange != null) { + onInfoChange.run(); + } + } + } +}
