#257039: @Model.instance() to allow storage of private (and non-JSON like) state in a model
1.1 --- a/json/src/main/java/net/java/html/json/Function.java Wed Dec 02 08:44:31 2015 +0100
1.2 +++ b/json/src/main/java/net/java/html/json/Function.java Wed Dec 09 21:39:13 2015 +0100
1.3 @@ -52,7 +52,8 @@
1.4 /** Methods in class annotated by {@link Model} can be
1.5 * annotated by this annotation to signal that they should be available
1.6 * as functions to users of the model classes. The method
1.7 - * should be non-private, static and return <code>void</code>.
1.8 + * should be non-private, static (unless {@link Model#instance() instance mode} is on)
1.9 + * and return <code>void</code>.
1.10 * It may take few arguments. The first argument can be the type of
1.11 * the associated model class, the other argument can be of any type,
1.12 * but has to be named <code>data</code> - this one represents the
2.1 --- a/json/src/main/java/net/java/html/json/Model.java Wed Dec 02 08:44:31 2015 +0100
2.2 +++ b/json/src/main/java/net/java/html/json/Model.java Wed Dec 09 21:39:13 2015 +0100
2.3 @@ -247,4 +247,46 @@
2.4 * @since 1.3
2.5 */
2.6 String builder() default "";
2.7 +
2.8 + /** Controls keeping of per-instance private state. Sometimes
2.9 + * the class generated by the {@link Model @Model annotation} needs to
2.10 + * keep non-public, or non-JSON like state. One can achieve that by
2.11 + * specifying <code>instance=true</code> when using the annotation. Then
2.12 + * the generated class gets a pointer to the instance of the annotated
2.13 + * class (there needs to be default constructor) and all the {@link ModelOperation @ModelOperation},
2.14 + * {@link Function @Function}, {@link OnPropertyChange @OnPropertyChange}
2.15 + * and {@link OnReceive @OnReceive} methods may be non-static. The
2.16 + * instance of the implementation class isn't accessible directly, just
2.17 + * through calls to above defined (non-static) methods. Example:
2.18 + * <pre>
2.19 + * {@link Model @Model}(className="Data", instance=<b>true</b>, properties={
2.20 + * {@link Property @Property}(name="message", type=String.<b>class</b>)
2.21 + * })
2.22 + * <b>final class</b> DataPrivate {
2.23 + * <b>private int</b> count;
2.24 + *
2.25 + * {@link ModelOperation @ModelOperation} <b>void</b> increment(Data model) {
2.26 + * count++;
2.27 + * }
2.28 + *
2.29 + * {@link ModelOperation @ModelOperation} <b>void</b> hello(Data model) {
2.30 + * model.setMessage("Hello " + count + " times!");
2.31 + * }
2.32 + * }
2.33 + * Data data = <b>new</b> Data();
2.34 + * data.increment();
2.35 + * data.increment();
2.36 + * data.increment();
2.37 + * data.hello();
2.38 + * <b>assert</b> data.getMessage().equals("Hello 3 times!");
2.39 + * </pre>
2.40 + * The methods annotated by {@link ComputedProperty} need to remain static, as
2.41 + * they are supposed to be <em>pure</em> functions (e.g. depend only on their parameters)
2.42 + * and shouldn't use any internal state.
2.43 + *
2.44 + * @return <code>true</code> if the model class should keep pointer to
2.45 + * instance of the implementation class
2.46 + * @since 1.3
2.47 + */
2.48 + boolean instance() default false;
2.49 }
3.1 --- a/json/src/main/java/net/java/html/json/ModelOperation.java Wed Dec 02 08:44:31 2015 +0100
3.2 +++ b/json/src/main/java/net/java/html/json/ModelOperation.java Wed Dec 09 21:39:13 2015 +0100
3.3 @@ -53,7 +53,8 @@
3.4 * <p>
3.5 * A method in a class annotated by {@link Model @Model} annotation may be
3.6 * annotated by {@link ModelOperation @ModelOperation}. The method has
3.7 - * to be static, non-private and return <code>void</code>. The first parameter
3.8 + * to be static (unless {@link Model#instance() instance mode} is on),
3.9 + * non-private and return <code>void</code>. The first parameter
3.10 * of the method must be the {@link Model#className() model class} followed
3.11 * by any number of additional arguments.
3.12 * <p>
4.1 --- a/json/src/main/java/net/java/html/json/OnPropertyChange.java Wed Dec 02 08:44:31 2015 +0100
4.2 +++ b/json/src/main/java/net/java/html/json/OnPropertyChange.java Wed Dec 09 21:39:13 2015 +0100
4.3 @@ -74,7 +74,8 @@
4.4 * }
4.5 * </pre>
4.6 * The method's first argument should be the instance of the
4.7 - * associated {@link Model model class}.
4.8 + * associated {@link Model model class}. The method shall be non-private
4.9 + * and unless {@link Model#instance() instance mode} is on also static.
4.10 * There can be an optional second {@link String} argument which will be set
4.11 * to the name of the changed property. The second argument is only useful when
4.12 * a single method reacts to changes in multiple properties.
5.1 --- a/json/src/main/java/net/java/html/json/OnReceive.java Wed Dec 02 08:44:31 2015 +0100
5.2 +++ b/json/src/main/java/net/java/html/json/OnReceive.java Wed Dec 09 21:39:13 2015 +0100
5.3 @@ -102,6 +102,8 @@
5.4 * One can use this method to communicate with the server
5.5 * via <a href="doc-files/websockets.html">WebSocket</a> protocol since version 0.6.
5.6 * Read the <a href="doc-files/websockets.html">tutorial</a> to see how.
5.7 + * The method shall be non-private
5.8 + * and unless {@link Model#instance() instance mode} is on also static.
5.9 * <p>
5.10 * Visit an <a target="_blank" href="http://dew.apidesign.org/dew/#7138581">on-line demo</a>
5.11 * to see REST access via {@link OnReceive} annotation.
6.1 --- a/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java Wed Dec 02 08:44:31 2015 +0100
6.2 +++ b/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java Wed Dec 09 21:39:13 2015 +0100
6.3 @@ -223,11 +223,36 @@
6.4 try {
6.5 w.append("package " + pkg + ";\n");
6.6 w.append("import net.java.html.json.*;\n");
6.7 - final String inPckName = inPckName(e);
6.8 + final String inPckName = inPckName(e, false);
6.9 w.append("/** Generated for {@link ").append(inPckName).append("}*/\n");
6.10 w.append("public final class ").append(className).append(" implements Cloneable {\n");
6.11 w.append(" private static Class<").append(inPckName).append("> modelFor() { return ").append(inPckName).append(".class; }\n");
6.12 w.append(" private static final Html4JavaType TYPE = new Html4JavaType();\n");
6.13 + if (m.instance()) {
6.14 + int cCnt = 0;
6.15 + for (Element c : e.getEnclosedElements()) {
6.16 + if (c.getKind() != ElementKind.CONSTRUCTOR) {
6.17 + continue;
6.18 + }
6.19 + cCnt++;
6.20 + ExecutableElement ec = (ExecutableElement) c;
6.21 + if (ec.getParameters().size() > 0) {
6.22 + continue;
6.23 + }
6.24 + if (ec.getModifiers().contains(Modifier.PRIVATE)) {
6.25 + continue;
6.26 + }
6.27 + cCnt = 0;
6.28 + break;
6.29 + }
6.30 + if (cCnt > 0) {
6.31 + ok = false;
6.32 + error("Needs non-private default constructor when instance=true", e);
6.33 + w.append(" private final ").append(inPckName).append(" instance = null;\n");
6.34 + } else {
6.35 + w.append(" private final ").append(inPckName).append(" instance = new ").append(inPckName).append("();\n");
6.36 + }
6.37 + }
6.38 w.append(" private final org.netbeans.html.json.spi.Proto proto;\n");
6.39 w.append(body.toString());
6.40 w.append(" private ").append(className).append("(net.java.html.BrwsrCtx context) {\n");
6.41 @@ -382,7 +407,13 @@
6.42 if (param instanceof ExecutableElement) {
6.43 ExecutableElement ee = (ExecutableElement)param;
6.44 w.append(" case " + (i / 2) + ":\n");
6.45 - w.append(" ").append(((TypeElement)e).getQualifiedName()).append(".").append(name).append("(");
6.46 + w.append(" ");
6.47 + if (m.instance()) {
6.48 + w.append("model.instance");
6.49 + } else {
6.50 + w.append(((TypeElement)e).getQualifiedName());
6.51 + }
6.52 + w.append(".").append(name).append("(");
6.53 w.append(wrapParams(ee, null, className, "model", "ev", "data"));
6.54 w.append(");\n");
6.55 w.append(" return;\n");
6.56 @@ -939,6 +970,7 @@
6.57 Element clazz, StringWriter body, String className,
6.58 List<? extends Element> enclosedElements, List<Object> functions
6.59 ) {
6.60 + boolean instance = clazz.getAnnotation(Model.class).instance();
6.61 for (Element m : enclosedElements) {
6.62 if (m.getKind() != ElementKind.METHOD) {
6.63 continue;
6.64 @@ -948,7 +980,7 @@
6.65 if (onF == null) {
6.66 continue;
6.67 }
6.68 - if (!e.getModifiers().contains(Modifier.STATIC)) {
6.69 + if (!instance && !e.getModifiers().contains(Modifier.STATIC)) {
6.70 error("@OnFunction method needs to be static", e);
6.71 return false;
6.72 }
6.73 @@ -971,6 +1003,7 @@
6.74 Prprt[] properties, String className,
6.75 Map<String, Collection<String>> functionDeps
6.76 ) {
6.77 + boolean instance = clazz.getAnnotation(Model.class).instance();
6.78 for (Element m : clazz.getEnclosedElements()) {
6.79 if (m.getKind() != ElementKind.METHOD) {
6.80 continue;
6.81 @@ -986,7 +1019,7 @@
6.82 return false;
6.83 }
6.84 }
6.85 - if (!e.getModifiers().contains(Modifier.STATIC)) {
6.86 + if (!instance && !e.getModifiers().contains(Modifier.STATIC)) {
6.87 error("@OnPrprtChange method needs to be static", e);
6.88 return false;
6.89 }
6.90 @@ -1003,7 +1036,7 @@
6.91
6.92 for (String pn : onPC.value()) {
6.93 StringBuilder call = new StringBuilder();
6.94 - call.append(" ").append(clazz.getSimpleName()).append(".").append(n).append("(");
6.95 + call.append(" ").append(inPckName(clazz, instance)).append(".").append(n).append("(");
6.96 call.append(wrapPropName(e, className, "name", pn));
6.97 call.append(");\n");
6.98
6.99 @@ -1031,6 +1064,7 @@
6.100 List<? extends Element> enclosedElements,
6.101 List<Object> functions
6.102 ) {
6.103 + boolean instance = clazz.getAnnotation(Model.class).instance();
6.104 for (Element m : enclosedElements) {
6.105 if (m.getKind() != ElementKind.METHOD) {
6.106 continue;
6.107 @@ -1040,7 +1074,7 @@
6.108 if (mO == null) {
6.109 continue;
6.110 }
6.111 - if (!e.getModifiers().contains(Modifier.STATIC)) {
6.112 + if (!instance && !e.getModifiers().contains(Modifier.STATIC)) {
6.113 error("@ModelOperation method needs to be static", e);
6.114 return false;
6.115 }
6.116 @@ -1091,7 +1125,7 @@
6.117
6.118 StringBuilder call = new StringBuilder();
6.119 call.append("{ Object[] arr = (Object[])data; ");
6.120 - call.append(inPckName(clazz)).append(".").append(m.getSimpleName()).append("(");
6.121 + call.append(inPckName(clazz, true)).append(".").append(m.getSimpleName()).append("(");
6.122 int i = 0;
6.123 for (VariableElement ve : e.getParameters()) {
6.124 if (i++ == 0) {
6.125 @@ -1132,6 +1166,7 @@
6.126 inType.append(" switch (index) {\n");
6.127 int index = 0;
6.128 boolean ok = true;
6.129 + boolean instance = clazz.getAnnotation(Model.class).instance();
6.130 for (Element m : enclosedElements) {
6.131 if (m.getKind() != ElementKind.METHOD) {
6.132 continue;
6.133 @@ -1141,7 +1176,7 @@
6.134 if (onR == null) {
6.135 continue;
6.136 }
6.137 - if (!e.getModifiers().contains(Modifier.STATIC)) {
6.138 + if (!instance && !e.getModifiers().contains(Modifier.STATIC)) {
6.139 error("@OnReceive method needs to be static", e);
6.140 return false;
6.141 }
6.142 @@ -1391,7 +1426,7 @@
6.143 body.append(
6.144 " case " + index + ": {\n" +
6.145 " if (type == 0) { /* on open */\n" +
6.146 - " ").append(inPckName(clazz)).append(".").append(n).append("(");
6.147 + " ").append(inPckName(clazz, true)).append(".").append(n).append("(");
6.148 {
6.149 String sep = "";
6.150 for (String arg : args) {
6.151 @@ -1418,7 +1453,7 @@
6.152 if (!findOnError(e, ((TypeElement)clazz), onR.onError(), className)) {
6.153 return true;
6.154 }
6.155 - body.append(" ").append(inPckName(clazz)).append(".").append(onR.onError()).append("(");
6.156 + body.append(" ").append(inPckName(clazz, true)).append(".").append(onR.onError()).append("(");
6.157 body.append("model, value);\n");
6.158 }
6.159 body.append(
6.160 @@ -1439,7 +1474,7 @@
6.161 " TYPE.copyJSON(model.proto.getContext(), ev, " + modelClass + ".class, arr);\n"
6.162 );
6.163 {
6.164 - body.append(" ").append(inPckName(clazz)).append(".").append(n).append("(");
6.165 + body.append(" ").append(inPckName(clazz, true)).append(".").append(n).append("(");
6.166 String sep = "";
6.167 for (String arg : args) {
6.168 body.append(sep);
6.169 @@ -1454,7 +1489,7 @@
6.170 );
6.171 if (!onR.onError().isEmpty()) {
6.172 body.append(" else if (type == 3) { /* on close */\n");
6.173 - body.append(" ").append(inPckName(clazz)).append(".").append(onR.onError()).append("(");
6.174 + body.append(" ").append(inPckName(clazz, true)).append(".").append(onR.onError()).append("(");
6.175 body.append("model, null);\n");
6.176 body.append(
6.177 " return;" +
6.178 @@ -1686,7 +1721,10 @@
6.179 w.write(" }\n");
6.180 }
6.181
6.182 - private String inPckName(Element e) {
6.183 + private String inPckName(Element e, boolean preferInstance) {
6.184 + if (preferInstance && e.getAnnotation(Model.class).instance()) {
6.185 + return "model.instance";
6.186 + }
6.187 StringBuilder sb = new StringBuilder();
6.188 while (e.getKind() != ElementKind.PACKAGE) {
6.189 if (sb.length() == 0) {
7.1 --- a/json/src/test/java/net/java/html/json/MapModelTest.java Wed Dec 02 08:44:31 2015 +0100
7.2 +++ b/json/src/test/java/net/java/html/json/MapModelTest.java Wed Dec 09 21:39:13 2015 +0100
7.3 @@ -304,6 +304,38 @@
7.4 assertEquals(p.getAge().get(1).intValue(), 7);
7.5 }
7.6
7.7 + @Test
7.8 + public void addAge42ThreeTimes() {
7.9 + People p = Models.bind(new People(), c);
7.10 + Map m = (Map)Models.toRaw(p);
7.11 + assertNotNull(m);
7.12 +
7.13 + class Inc implements Runnable {
7.14 + int cnt;
7.15 +
7.16 + @Override
7.17 + public void run() {
7.18 + cnt++;
7.19 + }
7.20 + }
7.21 + Inc incThreeTimes = new Inc();
7.22 + p.onInfoChange(incThreeTimes);
7.23 +
7.24 + p.addAge42();
7.25 + p.addAge42();
7.26 + p.addAge42();
7.27 + final int[] cnt = { 0, 0 };
7.28 + p.readAddAgeCount(cnt, new Runnable() {
7.29 + @Override
7.30 + public void run() {
7.31 + cnt[1] = 1;
7.32 + }
7.33 + });
7.34 + assertEquals(cnt[1], 1, "Callback called");
7.35 + assertEquals(cnt[0], 3, "Internal state kept");
7.36 + assertEquals(incThreeTimes.cnt, 3, "Property change delivered three times");
7.37 + }
7.38 +
7.39 static final class One {
7.40 int changes;
7.41 final PropertyBinding pb;
8.1 --- a/json/src/test/java/net/java/html/json/ModelProcessorTest.java Wed Dec 02 08:44:31 2015 +0100
8.2 +++ b/json/src/test/java/net/java/html/json/ModelProcessorTest.java Wed Dec 09 21:39:13 2015 +0100
8.3 @@ -386,6 +386,59 @@
8.4 Compile c = Compile.create(html, code, "1.5");
8.5 assertTrue(c.getErrors().isEmpty(), "No errors: " + c.getErrors());
8.6 }
8.7 +
8.8 + @Test public void instanceNeedsDefaultConstructor() throws IOException {
8.9 + String html = "<html><body>"
8.10 + + "</body></html>";
8.11 + String code = "package x.y.z;\n"
8.12 + + "import net.java.html.json.Model;\n"
8.13 + + "import net.java.html.json.Property;\n"
8.14 + + "import net.java.html.json.ComputedProperty;\n"
8.15 + + "@Model(className=\"XModel\", instance=true, properties={\n"
8.16 + + " @Property(name=\"prop\", type=long.class)\n"
8.17 + + "})\n"
8.18 + + "class X {\n"
8.19 + + " X(int x) {}\n"
8.20 + + "}\n";
8.21 +
8.22 + Compile c = Compile.create(html, code);
8.23 + c.assertError("Needs non-private default constructor when instance=true");
8.24 + }
8.25 +
8.26 + @Test public void instanceNeedsNonPrivateConstructor() throws IOException {
8.27 + String html = "<html><body>"
8.28 + + "</body></html>";
8.29 + String code = "package x.y.z;\n"
8.30 + + "import net.java.html.json.Model;\n"
8.31 + + "import net.java.html.json.Property;\n"
8.32 + + "import net.java.html.json.ComputedProperty;\n"
8.33 + + "@Model(className=\"XModel\", instance=true, properties={\n"
8.34 + + " @Property(name=\"prop\", type=long.class)\n"
8.35 + + "})\n"
8.36 + + "class X {\n"
8.37 + + " private X() {}\n"
8.38 + + "}\n";
8.39 +
8.40 + Compile c = Compile.create(html, code);
8.41 + c.assertError("Needs non-private default constructor when instance=true");
8.42 + }
8.43 +
8.44 + @Test public void instanceNoConstructorIsOK() throws IOException {
8.45 + String html = "<html><body>"
8.46 + + "</body></html>";
8.47 + String code = "package x.y.z;\n"
8.48 + + "import net.java.html.json.Model;\n"
8.49 + + "import net.java.html.json.Property;\n"
8.50 + + "import net.java.html.json.ComputedProperty;\n"
8.51 + + "@Model(className=\"XModel\", instance=true, properties={\n"
8.52 + + " @Property(name=\"prop\", type=long.class)\n"
8.53 + + "})\n"
8.54 + + "class X {\n"
8.55 + + "}\n";
8.56 +
8.57 + Compile c = Compile.create(html, code);
8.58 + c.assertNoErrors();
8.59 + }
8.60
8.61 @Test public void putNeedsDataArgument() throws Exception {
8.62 needsAnArg("PUT");
9.1 --- a/json/src/test/java/net/java/html/json/PersonImpl.java Wed Dec 02 08:44:31 2015 +0100
9.2 +++ b/json/src/test/java/net/java/html/json/PersonImpl.java Wed Dec 09 21:39:13 2015 +0100
9.3 @@ -91,26 +91,45 @@
9.4 }
9.5 }
9.6
9.7 - @Model(className = "People", targetId="myPeople", properties = {
9.8 + @Model(className = "People", instance = true, targetId="myPeople", properties = {
9.9 @Property(array = true, name = "info", type = Person.class),
9.10 @Property(array = true, name = "nicknames", type = String.class),
9.11 @Property(array = true, name = "age", type = int.class),
9.12 @Property(array = true, name = "sex", type = Sex.class)
9.13 })
9.14 public static class PeopleImpl {
9.15 - @ModelOperation static void addAge42(People p) {
9.16 + private int addAgeCount;
9.17 + private Runnable onInfoChange;
9.18 +
9.19 + @ModelOperation void onInfoChange(People self, Runnable r) {
9.20 + onInfoChange = r;
9.21 + }
9.22 +
9.23 + @ModelOperation void addAge42(People p) {
9.24 p.getAge().add(42);
9.25 + addAgeCount++;
9.26 }
9.27
9.28 @OnReceive(url = "url", method = "WebSocket", data = String.class)
9.29 - static void innerClass(People p, String d) {
9.30 + void innerClass(People p, String d) {
9.31 }
9.32
9.33 - @Function static void inInnerClass(People p, Person data, int x, double y, String nick) throws IOException {
9.34 + @Function void inInnerClass(People p, Person data, int x, double y, String nick) throws IOException {
9.35 p.getInfo().add(data);
9.36 p.getAge().add(x);
9.37 p.getAge().add((int)y);
9.38 p.getNicknames().add(nick);
9.39 }
9.40 +
9.41 + @ModelOperation void readAddAgeCount(People p, int[] holder, Runnable whenDone) {
9.42 + holder[0] = addAgeCount;
9.43 + whenDone.run();
9.44 + }
9.45 +
9.46 + @OnPropertyChange("age") void infoChange(People p) {
9.47 + if (onInfoChange != null) {
9.48 + onInfoChange.run();
9.49 + }
9.50 + }
9.51 }
9.52 }
10.1 --- a/src/main/javadoc/overview.html Wed Dec 02 08:44:31 2015 +0100
10.2 +++ b/src/main/javadoc/overview.html Wed Dec 09 21:39:13 2015 +0100
10.3 @@ -96,6 +96,8 @@
10.4
10.5 <h3>Improvements in version 1.3</h3>
10.6
10.7 + {@link net.java.html.json.Model Model classes} can have
10.8 + {@link net.java.html.json.Model#instance() per-instance private data}.
10.9 {@link net.java.html.json.Model Model classes} can generate
10.10 builder-like construction methods if builder
10.11 {@link net.java.html.json.Model#builder() prefix} is specified.