diff -r 2f6f1d20fa7a json/src/main/java/net/java/html/json/Function.java --- a/json/src/main/java/net/java/html/json/Function.java Wed Dec 02 08:44:31 2015 +0100 +++ b/json/src/main/java/net/java/html/json/Function.java Mon Dec 07 23:26:27 2015 +0100 @@ -52,7 +52,8 @@ /** Methods in class annotated by {@link Model} can be * annotated by this annotation to signal that they should be available * as functions to users of the model classes. The method - * should be non-private, static and return void. + * should be non-private, static (unless {@link Model#instance() instance mode} is on) + * and return void. * It may take few arguments. The first argument can be the type of * the associated model class, the other argument can be of any type, * but has to be named data - this one represents the diff -r 2f6f1d20fa7a json/src/main/java/net/java/html/json/Model.java --- a/json/src/main/java/net/java/html/json/Model.java Wed Dec 02 08:44:31 2015 +0100 +++ b/json/src/main/java/net/java/html/json/Model.java Mon Dec 07 23:26:27 2015 +0100 @@ -247,4 +247,46 @@ * @since 1.3 */ String builder() default ""; + + /** Controls keeping of per-instance private state. Sometimes + * the class generated by the {@link Model @Model annotation} needs to + * keep non-public, or non-JSON like state. One can achieve that by + * specifying instance=true when using the annotation. Then + * the generated class gets a pointer to the instance of the annotated + * class (there needs to be default constructor) and all the {@link ModelOperation @ModelOperation}, + * {@link Function @Function}, {@link OnPropertyChange @OnPropertyChange} + * and {@link OnReceive @OnReceive} methods may be non-static. The + * instance of the implementation class isn't accessible directly, just + * through calls to above defined (non-static) methods. Example: + *
+     * {@link Model @Model}(className="Data", instance=true, properties={
+     *   {@link Property @Property}(name="message", type=String.class)
+     * })
+     * final class DataPrivate {
+     *   private int count;
+     * 
+     *   {@link ModelOperation @ModelOperation} void increment(Data model) {
+     *     count++;
+     *   }
+     * 
+     *   {@link ModelOperation @ModelOperation} void hello(Data model) {
+     *     model.setMessage("Hello " + count + " times!");
+     *   }
+     * }
+     * Data data = new Data();
+     * data.increment();
+     * data.increment();
+     * data.increment();
+     * data.hello();
+     * assert data.getMessage().equals("Hello 3 times!");
+     * 
+ * The methods annotated by {@link ComputedProperty} need to remain static, as + * they are supposed to be pure functions (e.g. depend only on their parameters) + * and shouldn't use any internal state. + * + * @return true if the model class should keep pointer to + * instance of the implementation class + * @since 1.3 + */ + boolean instance() default false; } diff -r 2f6f1d20fa7a json/src/main/java/net/java/html/json/ModelOperation.java --- a/json/src/main/java/net/java/html/json/ModelOperation.java Wed Dec 02 08:44:31 2015 +0100 +++ b/json/src/main/java/net/java/html/json/ModelOperation.java Mon Dec 07 23:26:27 2015 +0100 @@ -53,7 +53,8 @@ *

* A method in a class annotated by {@link Model @Model} annotation may be * annotated by {@link ModelOperation @ModelOperation}. The method has - * to be static, non-private and return void. The first parameter + * to be static (unless {@link Model#instance() instance mode} is on), + * non-private and return void. The first parameter * of the method must be the {@link Model#className() model class} followed * by any number of additional arguments. *

diff -r 2f6f1d20fa7a json/src/main/java/net/java/html/json/OnPropertyChange.java --- a/json/src/main/java/net/java/html/json/OnPropertyChange.java Wed Dec 02 08:44:31 2015 +0100 +++ b/json/src/main/java/net/java/html/json/OnPropertyChange.java Mon Dec 07 23:26:27 2015 +0100 @@ -74,7 +74,8 @@ * } * * The method's first argument should be the instance of the - * associated {@link Model model class}. + * associated {@link Model model class}. The method shall be non-private + * and unless {@link Model#instance() instance mode} is on also static. * There can be an optional second {@link String} argument which will be set * to the name of the changed property. The second argument is only useful when * a single method reacts to changes in multiple properties. diff -r 2f6f1d20fa7a json/src/main/java/net/java/html/json/OnReceive.java --- a/json/src/main/java/net/java/html/json/OnReceive.java Wed Dec 02 08:44:31 2015 +0100 +++ b/json/src/main/java/net/java/html/json/OnReceive.java Mon Dec 07 23:26:27 2015 +0100 @@ -102,6 +102,8 @@ * One can use this method to communicate with the server * via WebSocket protocol since version 0.6. * Read the tutorial to see how. + * The method shall be non-private + * and unless {@link Model#instance() instance mode} is on also static. *

* Visit an on-line demo * to see REST access via {@link OnReceive} annotation. diff -r 2f6f1d20fa7a json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java --- a/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java Wed Dec 02 08:44:31 2015 +0100 +++ b/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java Mon Dec 07 23:26:27 2015 +0100 @@ -223,11 +223,36 @@ try { w.append("package " + pkg + ";\n"); w.append("import net.java.html.json.*;\n"); - final String inPckName = inPckName(e); + final String inPckName = inPckName(e, false); w.append("/** Generated for {@link ").append(inPckName).append("}*/\n"); w.append("public final class ").append(className).append(" implements Cloneable {\n"); w.append(" private static Class<").append(inPckName).append("> modelFor() { return ").append(inPckName).append(".class; }\n"); w.append(" private static final Html4JavaType TYPE = new Html4JavaType();\n"); + if (m.instance()) { + int cCnt = 0; + for (Element c : e.getEnclosedElements()) { + if (c.getKind() != ElementKind.CONSTRUCTOR) { + continue; + } + cCnt++; + ExecutableElement ec = (ExecutableElement) c; + if (ec.getParameters().size() > 0) { + continue; + } + if (ec.getModifiers().contains(Modifier.PRIVATE)) { + continue; + } + cCnt = 0; + break; + } + if (cCnt > 0) { + ok = false; + error("Needs non-private default constructor when instance=true", e); + w.append(" private final ").append(inPckName).append(" instance = null;\n"); + } else { + w.append(" private final ").append(inPckName).append(" instance = new ").append(inPckName).append("();\n"); + } + } w.append(" private final org.netbeans.html.json.spi.Proto proto;\n"); w.append(body.toString()); w.append(" private ").append(className).append("(net.java.html.BrwsrCtx context) {\n"); @@ -382,7 +407,13 @@ if (param instanceof ExecutableElement) { ExecutableElement ee = (ExecutableElement)param; w.append(" case " + (i / 2) + ":\n"); - w.append(" ").append(((TypeElement)e).getQualifiedName()).append(".").append(name).append("("); + w.append(" "); + if (m.instance()) { + w.append("model.instance"); + } else { + w.append(((TypeElement)e).getQualifiedName()); + } + w.append(".").append(name).append("("); w.append(wrapParams(ee, null, className, "model", "ev", "data")); w.append(");\n"); w.append(" return;\n"); @@ -939,6 +970,7 @@ Element clazz, StringWriter body, String className, List enclosedElements, List functions ) { + boolean instance = clazz.getAnnotation(Model.class).instance(); for (Element m : enclosedElements) { if (m.getKind() != ElementKind.METHOD) { continue; @@ -948,7 +980,7 @@ if (onF == null) { continue; } - if (!e.getModifiers().contains(Modifier.STATIC)) { + if (!instance && !e.getModifiers().contains(Modifier.STATIC)) { error("@OnFunction method needs to be static", e); return false; } @@ -971,6 +1003,7 @@ Prprt[] properties, String className, Map> functionDeps ) { + boolean instance = clazz.getAnnotation(Model.class).instance(); for (Element m : clazz.getEnclosedElements()) { if (m.getKind() != ElementKind.METHOD) { continue; @@ -986,7 +1019,7 @@ return false; } } - if (!e.getModifiers().contains(Modifier.STATIC)) { + if (!instance && !e.getModifiers().contains(Modifier.STATIC)) { error("@OnPrprtChange method needs to be static", e); return false; } @@ -1003,7 +1036,7 @@ for (String pn : onPC.value()) { StringBuilder call = new StringBuilder(); - call.append(" ").append(clazz.getSimpleName()).append(".").append(n).append("("); + call.append(" ").append(inPckName(clazz, instance)).append(".").append(n).append("("); call.append(wrapPropName(e, className, "name", pn)); call.append(");\n"); @@ -1031,6 +1064,7 @@ List enclosedElements, List functions ) { + boolean instance = clazz.getAnnotation(Model.class).instance(); for (Element m : enclosedElements) { if (m.getKind() != ElementKind.METHOD) { continue; @@ -1040,7 +1074,7 @@ if (mO == null) { continue; } - if (!e.getModifiers().contains(Modifier.STATIC)) { + if (!instance && !e.getModifiers().contains(Modifier.STATIC)) { error("@ModelOperation method needs to be static", e); return false; } @@ -1091,7 +1125,7 @@ StringBuilder call = new StringBuilder(); call.append("{ Object[] arr = (Object[])data; "); - call.append(inPckName(clazz)).append(".").append(m.getSimpleName()).append("("); + call.append(inPckName(clazz, true)).append(".").append(m.getSimpleName()).append("("); int i = 0; for (VariableElement ve : e.getParameters()) { if (i++ == 0) { @@ -1132,6 +1166,7 @@ inType.append(" switch (index) {\n"); int index = 0; boolean ok = true; + boolean instance = clazz.getAnnotation(Model.class).instance(); for (Element m : enclosedElements) { if (m.getKind() != ElementKind.METHOD) { continue; @@ -1141,7 +1176,7 @@ if (onR == null) { continue; } - if (!e.getModifiers().contains(Modifier.STATIC)) { + if (!instance && !e.getModifiers().contains(Modifier.STATIC)) { error("@OnReceive method needs to be static", e); return false; } @@ -1391,7 +1426,7 @@ body.append( " case " + index + ": {\n" + " if (type == 0) { /* on open */\n" + - " ").append(inPckName(clazz)).append(".").append(n).append("("); + " ").append(inPckName(clazz, true)).append(".").append(n).append("("); { String sep = ""; for (String arg : args) { @@ -1418,7 +1453,7 @@ if (!findOnError(e, ((TypeElement)clazz), onR.onError(), className)) { return true; } - body.append(" ").append(inPckName(clazz)).append(".").append(onR.onError()).append("("); + body.append(" ").append(inPckName(clazz, true)).append(".").append(onR.onError()).append("("); body.append("model, value);\n"); } body.append( @@ -1439,7 +1474,7 @@ " TYPE.copyJSON(model.proto.getContext(), ev, " + modelClass + ".class, arr);\n" ); { - body.append(" ").append(inPckName(clazz)).append(".").append(n).append("("); + body.append(" ").append(inPckName(clazz, true)).append(".").append(n).append("("); String sep = ""; for (String arg : args) { body.append(sep); @@ -1454,7 +1489,7 @@ ); if (!onR.onError().isEmpty()) { body.append(" else if (type == 3) { /* on close */\n"); - body.append(" ").append(inPckName(clazz)).append(".").append(onR.onError()).append("("); + body.append(" ").append(inPckName(clazz, true)).append(".").append(onR.onError()).append("("); body.append("model, null);\n"); body.append( " return;" + @@ -1686,7 +1721,10 @@ w.write(" }\n"); } - private String inPckName(Element e) { + private String inPckName(Element e, boolean preferInstance) { + if (preferInstance && e.getAnnotation(Model.class).instance()) { + return "model.instance"; + } StringBuilder sb = new StringBuilder(); while (e.getKind() != ElementKind.PACKAGE) { if (sb.length() == 0) { diff -r 2f6f1d20fa7a json/src/test/java/net/java/html/json/MapModelTest.java --- a/json/src/test/java/net/java/html/json/MapModelTest.java Wed Dec 02 08:44:31 2015 +0100 +++ b/json/src/test/java/net/java/html/json/MapModelTest.java Mon Dec 07 23:26:27 2015 +0100 @@ -304,6 +304,38 @@ 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"); + } + static final class One { int changes; final PropertyBinding pb; diff -r 2f6f1d20fa7a json/src/test/java/net/java/html/json/ModelProcessorTest.java --- a/json/src/test/java/net/java/html/json/ModelProcessorTest.java Wed Dec 02 08:44:31 2015 +0100 +++ b/json/src/test/java/net/java/html/json/ModelProcessorTest.java Mon Dec 07 23:26:27 2015 +0100 @@ -386,6 +386,59 @@ Compile c = Compile.create(html, code, "1.5"); assertTrue(c.getErrors().isEmpty(), "No errors: " + c.getErrors()); } + + @Test public void instanceNeedsDefaultConstructor() throws IOException { + String html = "" + + ""; + String code = "package x.y.z;\n" + + "import net.java.html.json.Model;\n" + + "import net.java.html.json.Property;\n" + + "import net.java.html.json.ComputedProperty;\n" + + "@Model(className=\"XModel\", 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 = "" + + ""; + 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 = "" + + ""; + 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"); diff -r 2f6f1d20fa7a json/src/test/java/net/java/html/json/PersonImpl.java --- a/json/src/test/java/net/java/html/json/PersonImpl.java Wed Dec 02 08:44:31 2015 +0100 +++ b/json/src/test/java/net/java/html/json/PersonImpl.java Mon Dec 07 23:26:27 2015 +0100 @@ -91,26 +91,45 @@ } } - @Model(className = "People", targetId="myPeople", properties = { + @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 { - @ModelOperation static void addAge42(People p) { + 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) - static void innerClass(People p, String d) { + void innerClass(People p, String d) { } - @Function static void inInnerClass(People p, Person data, int x, double y, String nick) throws IOException { + @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(); + } + } } } diff -r 2f6f1d20fa7a src/main/javadoc/overview.html --- a/src/main/javadoc/overview.html Wed Dec 02 08:44:31 2015 +0100 +++ b/src/main/javadoc/overview.html Mon Dec 07 23:26:27 2015 +0100 @@ -96,6 +96,8 @@

Improvements in version 1.3

+ {@link net.java.html.json.Model Model classes} can have + {@link net.java.html.json.Model#instance() per-instance private data}. {@link net.java.html.json.Model Model classes} can generate builder-like construction methods if builder {@link net.java.html.json.Model#builder() prefix} is specified.