#257039: @Model.instance() to allow storage of private (and non-JSON like) state in a model
authorJaroslav Tulach <jtulach@netbeans.org>
Wed, 09 Dec 2015 21:39:13 +0100
changeset 10234f906bde3a2e
parent 1022 2f6f1d20fa7a
child 1024 558934b8b835
#257039: @Model.instance() to allow storage of private (and non-JSON like) state in a model
json/src/main/java/net/java/html/json/Function.java
json/src/main/java/net/java/html/json/Model.java
json/src/main/java/net/java/html/json/ModelOperation.java
json/src/main/java/net/java/html/json/OnPropertyChange.java
json/src/main/java/net/java/html/json/OnReceive.java
json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java
json/src/test/java/net/java/html/json/MapModelTest.java
json/src/test/java/net/java/html/json/ModelProcessorTest.java
json/src/test/java/net/java/html/json/PersonImpl.java
src/main/javadoc/overview.html
     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.