#250611: Builder style properties
authorJaroslav Tulach <jtulach@netbeans.org>
Thu, 05 Nov 2015 23:38:18 +0100
changeset 101710427ce1c0ee
parent 1016 665b10c62f3d
child 1018 82ec9872793a
#250611: Builder style properties
json/src/main/java/net/java/html/json/Model.java
json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java
json/src/main/java/org/netbeans/html/json/spi/Proto.java
json/src/test/java/net/java/html/json/ModelTest.java
json/src/test/java/org/netbeans/html/json/impl/BuilderTest.java
src/main/javadoc/overview.html
     1.1 --- a/json/src/main/java/net/java/html/json/Model.java	Sun Nov 01 16:59:42 2015 +0100
     1.2 +++ b/json/src/main/java/net/java/html/json/Model.java	Thu Nov 05 23:38:18 2015 +0100
     1.3 @@ -48,7 +48,6 @@
     1.4  import java.lang.annotation.Target;
     1.5  import java.net.URL;
     1.6  import java.util.List;
     1.7 -import org.netbeans.html.json.spi.Technology;
     1.8  
     1.9  /** Defines a model class that represents a single 
    1.10   * <a target="_blank" href="http://en.wikipedia.org/wiki/JSON">JSON</a>-like object
    1.11 @@ -222,4 +221,30 @@
    1.12       * @since 1.1
    1.13       */
    1.14      String targetId() default "";
    1.15 +
    1.16 +    /** Controls whether builder-like setters shall be generated. Once this
    1.17 +     * attribute is set, all {@link #properties()} will get a builder like
    1.18 +     * setter (takes value of the property and returns <code>this</code>
    1.19 +     * so invocations can be chained). When this attribute is specified,
    1.20 +     * the non-default constructor isn't generated at all.
    1.21 +     * <p>
    1.22 +     * Specifying <code>builder="assign"</code>
    1.23 +     * and having {@link #properties() properties} <code>name</code> and
    1.24 +     * <code>age</code> will generate method: <pre>
    1.25 +     * <b>public</b> MyModel assignName(String name) { ... }
    1.26 +     * <b>public</b> MyModel assignAge(int age) { ... }
    1.27 +     * </pre>
    1.28 +     * These methods can then be chained as <pre>
    1.29 +     * MyModel m = <b>new</b> MyModel().assignName("name").assignAge(3);
    1.30 +     * </pre>
    1.31 +     * The <code>builder</code> attribute can be set to empty string <code>""</code> -
    1.32 +     * then it is possible that some property names clash with Java keywords.
    1.33 +     * It's responsibility of the user to specify valid builder prefix,
    1.34 +     * so the generated methods are compilable.
    1.35 +     *
    1.36 +     * @return the prefix to put before {@link Property property} names when
    1.37 +     *   generating their builder methods
    1.38 +     * @since 1.3
    1.39 +     */
    1.40 +    String builder() default "";
    1.41  }
     2.1 --- a/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java	Sun Nov 01 16:59:42 2015 +0100
     2.2 +++ b/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java	Thu Nov 05 23:38:18 2015 +0100
     2.3 @@ -189,6 +189,7 @@
     2.4              Map<String, Collection<String[]>> propsDeps = new HashMap<String, Collection<String[]>>();
     2.5              Map<String, Collection<String>> functionDeps = new HashMap<String, Collection<String>>();
     2.6              Prprt[] props = createProps(e, m.properties());
     2.7 +            final String builderPrefix = findBuilderPrefix(e, m);
     2.8  
     2.9              if (!generateComputedProperties(className, body, props, e.getEnclosedElements(), propsGetSet, propsDeps)) {
    2.10                  ok = false;
    2.11 @@ -196,7 +197,7 @@
    2.12              if (!generateOnChange(e, propsDeps, props, className, functionDeps)) {
    2.13                  ok = false;
    2.14              }
    2.15 -            if (!generateProperties(e, body, className, props, propsGetSet, propsDeps, functionDeps)) {
    2.16 +            if (!generateProperties(e, builderPrefix, body, className, props, propsGetSet, propsDeps, functionDeps)) {
    2.17                  ok = false;
    2.18              }
    2.19              if (!generateFunctions(e, body, className, e.getEnclosedElements(), functions)) {
    2.20 @@ -271,7 +272,7 @@
    2.21                      }
    2.22                  }
    2.23                  w.append("  };\n");
    2.24 -                if (props.length > 0) {
    2.25 +                if (props.length > 0 && builderPrefix == null) {
    2.26                      StringBuilder constructorWithArguments = new StringBuilder();
    2.27                      constructorWithArguments.append("  public ").append(className).append("(");
    2.28                      Prprt firstArray = null;
    2.29 @@ -462,8 +463,13 @@
    2.30                              w.append(type).append(".valueOf(TYPE.stringValue(e)));\n");
    2.31                          } else {
    2.32                              if (isPrimitive(type)) {
    2.33 -                                w.append("        this.prop_").append(pn).append(".add(TYPE.numberValue(e).");
    2.34 -                                w.append(type).append("Value());\n");
    2.35 +                                if (type.equals("char")) {
    2.36 +                                    w.append("        this.prop_").append(pn).append(".add((char)TYPE.numberValue(e).");
    2.37 +                                    w.append("intValue());\n");
    2.38 +                                } else {
    2.39 +                                    w.append("        this.prop_").append(pn).append(".add(TYPE.numberValue(e).");
    2.40 +                                    w.append(type).append("Value());\n");
    2.41 +                                }
    2.42                              } else {
    2.43                                  w.append("        this.prop_").append(pn).append(".add((");
    2.44                                  w.append(type).append(")e);\n");
    2.45 @@ -561,8 +567,29 @@
    2.46          return ok;
    2.47      }
    2.48  
    2.49 +    private static String findBuilderPrefix(Element e, Model m) {
    2.50 +        if (!m.builder().isEmpty()) {
    2.51 +            return m.builder();
    2.52 +        }
    2.53 +        for (AnnotationMirror am : e.getAnnotationMirrors()) {
    2.54 +            for (Map.Entry<? extends Object, ? extends Object> entry : am.getElementValues().entrySet()) {
    2.55 +                if ("builder()".equals(entry.getKey().toString())) {
    2.56 +                    return "";
    2.57 +                }
    2.58 +            }
    2.59 +        }
    2.60 +        return null;
    2.61 +    }
    2.62 +
    2.63 +    private static String builderMethod(String builderPrefix, Prprt p) {
    2.64 +        if (builderPrefix.isEmpty()) {
    2.65 +            return p.name();
    2.66 +        }
    2.67 +        return builderPrefix + Character.toUpperCase(p.name().charAt(0)) + p.name().substring(1);
    2.68 +    }
    2.69 +
    2.70      private boolean generateProperties(
    2.71 -        Element where,
    2.72 +        Element where, String builderPrefix,
    2.73          Writer w, String className, Prprt[] properties,
    2.74          List<GetSet> props,
    2.75          Map<String,Collection<String[]>> deps,
    2.76 @@ -583,6 +610,17 @@
    2.77                  w.write("    proto.accessProperty(\"" + p.name() + "\");\n");
    2.78                  w.write("    return prop_" + p.name() + ";\n");
    2.79                  w.write("  }\n");
    2.80 +                if (builderPrefix != null) {
    2.81 +                    boolean[] isModel = {false};
    2.82 +                    boolean[] isEnum = {false};
    2.83 +                    boolean isPrimitive[] = {false};
    2.84 +                    String ret = checkType(p, isModel, isEnum, isPrimitive);
    2.85 +                    w.write("  public " + className + " " + builderMethod(builderPrefix, p) + "(" + ret + "... v) {\n");
    2.86 +                    w.write("    proto.accessProperty(\"" + p.name() + "\");\n");
    2.87 +                    w.append("   TYPE.replaceValue(prop_").append(p.name()).append(", " + tn + ".class, v);\n");
    2.88 +                    w.write("    return this;\n");
    2.89 +                    w.write("  }\n");
    2.90 +                }
    2.91              } else {
    2.92                  castTo = tn;
    2.93                  boolean isModel[] = { false };
    2.94 @@ -623,6 +661,12 @@
    2.95                      }
    2.96                  }
    2.97                  w.write("  }\n");
    2.98 +                if (builderPrefix != null) {
    2.99 +                    w.write("  public " + className + " " + builderMethod(builderPrefix, p) + "(" + tn + " v) {\n");
   2.100 +                    w.write("    " + gs[1] + "(v);\n");
   2.101 +                    w.write("    return this;\n");
   2.102 +                    w.write("  }\n");
   2.103 +                }
   2.104              }
   2.105  
   2.106              for (int i = 0; i < props.size(); i++) {
     3.1 --- a/json/src/main/java/org/netbeans/html/json/spi/Proto.java	Sun Nov 01 16:59:42 2015 +0100
     3.2 +++ b/json/src/main/java/org/netbeans/html/json/spi/Proto.java	Thu Nov 05 23:38:18 2015 +0100
     3.3 @@ -875,15 +875,41 @@
     3.4           * @since 1.0
     3.5           */
     3.6          public final <T> void replaceValue(Collection<? super T> arr, Class<T> type, Object value) {
     3.7 -            Object[] newArr;
     3.8 +            List<T> tmp = new ArrayList<T>();
     3.9              if (value instanceof Object[]) {
    3.10 -                newArr = (Object[]) value;
    3.11 +                for (Object e : (Object[]) value) {
    3.12 +                    tmp.add(extractValue(type, e));
    3.13 +                }
    3.14 +            } else if (value instanceof byte[]) {
    3.15 +                for (Object e : (byte[]) value) {
    3.16 +                    tmp.add(extractValue(type, e));
    3.17 +                }
    3.18 +            } else if (value instanceof short[]) {
    3.19 +                for (Object e : (short[]) value) {
    3.20 +                    tmp.add(extractValue(type, e));
    3.21 +                }
    3.22 +            } else if (value instanceof int[]) {
    3.23 +                for (Object e : (int[]) value) {
    3.24 +                    tmp.add(extractValue(type, e));
    3.25 +                }
    3.26 +            } else if (value instanceof char[]) {
    3.27 +                for (Object e : (char[]) value) {
    3.28 +                    tmp.add(extractValue(type, e));
    3.29 +                }
    3.30 +            } else if (value instanceof long[]) {
    3.31 +                for (Object e : (long[]) value) {
    3.32 +                    tmp.add(extractValue(type, e));
    3.33 +                }
    3.34 +            } else if (value instanceof float[]) {
    3.35 +                for (Object e : (float[]) value) {
    3.36 +                    tmp.add(extractValue(type, e));
    3.37 +                }
    3.38 +            } else if (value instanceof double[]) {
    3.39 +                for (Object e : (double[]) value) {
    3.40 +                    tmp.add(extractValue(type, e));
    3.41 +                }
    3.42              } else {
    3.43 -                newArr = new Object[] { value };
    3.44 -            }
    3.45 -            List<T> tmp = new ArrayList<T>(newArr.length);
    3.46 -            for (Object e : newArr) {
    3.47 -                tmp.add(extractValue(type, e));
    3.48 +                tmp.add(extractValue(type, value));
    3.49              }
    3.50              if (arr instanceof JSONList) {
    3.51                  JSONList jsList = (JSONList) arr;
     4.1 --- a/json/src/test/java/net/java/html/json/ModelTest.java	Sun Nov 01 16:59:42 2015 +0100
     4.2 +++ b/json/src/test/java/net/java/html/json/ModelTest.java	Thu Nov 05 23:38:18 2015 +0100
     4.3 @@ -65,7 +65,7 @@
     4.4   *
     4.5   * @author Jaroslav Tulach
     4.6   */
     4.7 -@Model(className = "Modelik", targetId = "", properties = {
     4.8 +@Model(className = "Modelik", builder = "change", targetId = "", properties = {
     4.9      @Property(name = "value", type = int.class),
    4.10      @Property(name = "count", type = int.class),
    4.11      @Property(name = "unrelated", type = long.class),
    4.12 @@ -97,8 +97,18 @@
    4.13      }
    4.14  
    4.15      @Test public void equalsAndHashCode() {
    4.16 -        Modelik m1 = new Modelik(10, 20, 30, "changed", "firstName");
    4.17 -        Modelik m2 = new Modelik(10, 20, 30, "changed", "firstName");
    4.18 +        Modelik m1 = new Modelik();
    4.19 +        m1.setValue(10);
    4.20 +        m1.setCount(20);
    4.21 +        m1.setUnrelated(30);
    4.22 +        m1.setChangedProperty("changed");
    4.23 +        m1.getNames().add("firstName");
    4.24 +        Modelik m2 = new Modelik().
    4.25 +            changeValue(10).
    4.26 +            changeCount(20).
    4.27 +            changeUnrelated(30).
    4.28 +            changeChangedProperty("changed").
    4.29 +            changeNames("firstName");
    4.30  
    4.31          assertTrue(m1.equals(m2), "They are the same");
    4.32          assertEquals(m1.hashCode(), m2.hashCode(), "Hashcode is the same");
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/json/src/test/java/org/netbeans/html/json/impl/BuilderTest.java	Thu Nov 05 23:38:18 2015 +0100
     5.3 @@ -0,0 +1,117 @@
     5.4 +/**
     5.5 + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     5.6 + *
     5.7 + * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved.
     5.8 + *
     5.9 + * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    5.10 + * Other names may be trademarks of their respective owners.
    5.11 + *
    5.12 + * The contents of this file are subject to the terms of either the GNU
    5.13 + * General Public License Version 2 only ("GPL") or the Common
    5.14 + * Development and Distribution License("CDDL") (collectively, the
    5.15 + * "License"). You may not use this file except in compliance with the
    5.16 + * License. You can obtain a copy of the License at
    5.17 + * http://www.netbeans.org/cddl-gplv2.html
    5.18 + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
    5.19 + * specific language governing permissions and limitations under the
    5.20 + * License.  When distributing the software, include this License Header
    5.21 + * Notice in each file and include the License file at
    5.22 + * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
    5.23 + * particular file as subject to the "Classpath" exception as provided
    5.24 + * by Oracle in the GPL Version 2 section of the License file that
    5.25 + * accompanied this code. If applicable, add the following below the
    5.26 + * License Header, with the fields enclosed by brackets [] replaced by
    5.27 + * your own identifying information:
    5.28 + * "Portions Copyrighted [year] [name of copyright owner]"
    5.29 + *
    5.30 + * Contributor(s):
    5.31 + *
    5.32 + * The Original Software is NetBeans. The Initial Developer of the Original
    5.33 + * Software is Oracle. Portions Copyright 2013-2014 Oracle. All Rights Reserved.
    5.34 + *
    5.35 + * If you wish your version of this file to be governed by only the CDDL
    5.36 + * or only the GPL Version 2, indicate your decision by adding
    5.37 + * "[Contributor] elects to include this software in this distribution
    5.38 + * under the [CDDL or GPL Version 2] license." If you do not indicate a
    5.39 + * single choice of license, a recipient has the option to distribute
    5.40 + * your version of this file under either the CDDL, the GPL Version 2 or
    5.41 + * to extend the choice of license to its licensees as provided above.
    5.42 + * However, if you add GPL Version 2 code and therefore, elected the GPL
    5.43 + * Version 2 license, then the option applies only if the new code is
    5.44 + * made subject to such option by the copyright holder.
    5.45 + */
    5.46 +package org.netbeans.html.json.impl;
    5.47 +
    5.48 +import java.lang.reflect.Constructor;
    5.49 +import net.java.html.json.Model;
    5.50 +import net.java.html.json.Property;
    5.51 +import static org.testng.Assert.assertEquals;
    5.52 +import org.testng.annotations.Test;
    5.53 +
    5.54 +@Model(className="Builder", builder = "", properties = {
    5.55 +    @Property(name="bytes", type = byte.class, array = true),
    5.56 +    @Property(name="chars", type = char.class, array = true),
    5.57 +    @Property(name="shorts", type = short.class, array = true),
    5.58 +    @Property(name="ints", type = int.class, array = true),
    5.59 +    @Property(name="longs", type = long.class, array = true),
    5.60 +    @Property(name="floats", type = float.class, array = true),
    5.61 +    @Property(name="doubles", type = double.class, array = true),
    5.62 +    @Property(name="strings", type = String.class, array = true),
    5.63 +})
    5.64 +public class BuilderTest {
    5.65 +    @Test
    5.66 +    public void onlyDefaultClassLoader() {
    5.67 +        Constructor<?>[] arr = Builder.class.getConstructors();
    5.68 +        assertEquals(arr.length, 1, "One constructor");
    5.69 +        assertEquals(arr[0].getParameterTypes().length, 0, "No parameters");
    5.70 +    }
    5.71 +
    5.72 +    @Test
    5.73 +    public void assignBytes() {
    5.74 +        Builder b = new Builder().bytes((byte)10, (byte)20, (byte)30);
    5.75 +        assertEquals(b.getBytes().size(), 3);
    5.76 +        assertEquals(b.getBytes().get(0).byteValue(), (byte)10);
    5.77 +    }
    5.78 +    @Test
    5.79 +    public void assignChars() {
    5.80 +        Builder b = new Builder().chars((char)10, (char)20, (char)30);
    5.81 +        assertEquals(b.getChars().size(), 3);
    5.82 +        assertEquals(b.getChars().get(0).charValue(), 10);
    5.83 +    }
    5.84 +    @Test
    5.85 +    public void assignShort() {
    5.86 +        Builder b = new Builder().shorts((short)10, (short)20, (short)30);
    5.87 +        assertEquals(b.getShorts().size(), 3);
    5.88 +        assertEquals(b.getShorts().get(0).intValue(), 10);
    5.89 +    }
    5.90 +    @Test
    5.91 +    public void assignInts() {
    5.92 +        Builder b = new Builder().ints(10, 20, 30);
    5.93 +        assertEquals(b.getInts().size(), 3);
    5.94 +        assertEquals(b.getInts().get(0).intValue(), 10);
    5.95 +    }
    5.96 +    @Test
    5.97 +    public void assignLongs() {
    5.98 +        Builder b = new Builder().longs(10, 20, 30);
    5.99 +        assertEquals(b.getLongs().size(), 3);
   5.100 +        assertEquals(b.getLongs().get(1).intValue(), 20);
   5.101 +    }
   5.102 +    @Test
   5.103 +    public void assignDouble() {
   5.104 +        Builder b = new Builder().doubles(10, 20, 30);
   5.105 +        assertEquals(b.getDoubles().size(), 3);
   5.106 +        assertEquals(b.getDoubles().get(0), 10.0);
   5.107 +    }
   5.108 +    @Test
   5.109 +    public void assignFloats() {
   5.110 +        Builder b = new Builder().floats(10, 20, 30);
   5.111 +        assertEquals(b.getFloats().size(), 3);
   5.112 +        assertEquals(b.getFloats().get(0), 10.0f);
   5.113 +    }
   5.114 +    @Test
   5.115 +    public void assignStrings() {
   5.116 +        Builder b = new Builder().strings("A", "AB", "ABC");
   5.117 +        assertEquals(b.getStrings().size(), 3);
   5.118 +        assertEquals(b.getStrings().get(1), "AB");
   5.119 +    }
   5.120 +}
     6.1 --- a/src/main/javadoc/overview.html	Sun Nov 01 16:59:42 2015 +0100
     6.2 +++ b/src/main/javadoc/overview.html	Thu Nov 05 23:38:18 2015 +0100
     6.3 @@ -96,6 +96,9 @@
     6.4  
     6.5          <h3>Improvements in version 1.3</h3>
     6.6  
     6.7 +        {@link net.java.html.json.Model Model classes} can generate
     6.8 +        builder-like construction methods if builder
     6.9 +        {@link net.java.html.json.Model#builder() prefix} is specified.
    6.10          The <em>JavaFX</em> presenter can be executed in headless mode -
    6.11          just specify <code>-Dfxpresenter.headless=true</code> when launching
    6.12          its virtual machine and no window will be shown. This is particularly