#258088: Introducing @Property(mutable=false) and making sure the generated Java code yields exceptions when such properties are modified after their use by the technology NonMutable258088
authorJaroslav Tulach <jtulach@netbeans.org>
Mon, 22 Feb 2016 06:09:33 +0100
branchNonMutable258088
changeset 10544c40ceb185e5
parent 1053 ed4b25eb66f3
child 1055 c61d247f087a
#258088: Introducing @Property(mutable=false) and making sure the generated Java code yields exceptions when such properties are modified after their use by the technology
json/src/main/java/net/java/html/json/Property.java
json/src/main/java/org/netbeans/html/json/impl/JSONList.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/MapModelNotMutableTest.java
     1.1 --- a/json/src/main/java/net/java/html/json/Property.java	Mon Feb 15 05:27:28 2016 +0100
     1.2 +++ b/json/src/main/java/net/java/html/json/Property.java	Mon Feb 22 06:09:33 2016 +0100
     1.3 @@ -46,6 +46,8 @@
     1.4  import java.lang.annotation.RetentionPolicy;
     1.5  import java.lang.annotation.Target;
     1.6  import java.util.List;
     1.7 +import org.netbeans.html.context.spi.Contexts;
     1.8 +import org.netbeans.html.json.spi.Technology;
     1.9  
    1.10  /** Represents a property in a class defined with {@link Model} annotation.
    1.11   *
    1.12 @@ -76,4 +78,21 @@
    1.13       * @return true, if this property is supposed to represent an array of values
    1.14       */
    1.15      boolean array() default false;
    1.16 +
    1.17 +    /** Can the value of the property be mutated without restriction or not.
    1.18 +     * If a property is defined as <em>not mutable</em>, it defines
    1.19 +     * semi-immutable value that can only be changed in construction time
    1.20 +     * before the object is passed to underlying {@link Technology}. 
    1.21 +     * Attempts to modify the object later yield {@link IllegalStateException}.
    1.22 +     *
    1.23 +     * Technologies may decide to represent such non-mutable
    1.24 +     * property in more effective way - for
    1.25 +     * example Knockout Java Bindings technology (with {@link Contexts.Id id} "ko4j")
    1.26 +     * uses plain JavaScript value (number, string, array, boolean) rather
    1.27 +     * than classical observable.
    1.28 +     *
    1.29 +     * @return false if the value cannot change after its <em>first use</em>
    1.30 +     * @since 1.3
    1.31 +     */
    1.32 +    boolean mutable() default true;
    1.33  }
     2.1 --- a/json/src/main/java/org/netbeans/html/json/impl/JSONList.java	Mon Feb 15 05:27:28 2016 +0100
     2.2 +++ b/json/src/main/java/org/netbeans/html/json/impl/JSONList.java	Mon Feb 22 06:09:33 2016 +0100
     2.3 @@ -88,6 +88,7 @@
     2.4      
     2.5      @Override
     2.6      public boolean add(T e) {
     2.7 +        prepareChange();
     2.8          boolean ret = super.add(e);
     2.9          notifyChange();
    2.10          return ret;
    2.11 @@ -95,6 +96,7 @@
    2.12  
    2.13      @Override
    2.14      public boolean addAll(Collection<? extends T> c) {
    2.15 +        prepareChange();
    2.16          boolean ret = super.addAll(c);
    2.17          notifyChange();
    2.18          return ret;
    2.19 @@ -102,12 +104,14 @@
    2.20  
    2.21      @Override
    2.22      public boolean addAll(int index, Collection<? extends T> c) {
    2.23 +        prepareChange();
    2.24          boolean ret = super.addAll(index, c);
    2.25          notifyChange();
    2.26          return ret;
    2.27      }
    2.28  
    2.29      public void fastReplace(Collection<? extends T> c) {
    2.30 +        prepareChange();
    2.31          super.clear();
    2.32          super.addAll(c);
    2.33          notifyChange();
    2.34 @@ -115,6 +119,7 @@
    2.35  
    2.36      @Override
    2.37      public boolean remove(Object o) {
    2.38 +        prepareChange();
    2.39          boolean ret = super.remove(o);
    2.40          notifyChange();
    2.41          return ret;
    2.42 @@ -122,12 +127,14 @@
    2.43  
    2.44      @Override
    2.45      public void clear() {
    2.46 +        prepareChange();
    2.47          super.clear();
    2.48          notifyChange();
    2.49      }
    2.50  
    2.51      @Override
    2.52      public boolean removeAll(Collection<?> c) {
    2.53 +        prepareChange();
    2.54          boolean ret = super.removeAll(c);
    2.55          notifyChange();
    2.56          return ret;
    2.57 @@ -135,6 +142,7 @@
    2.58  
    2.59      @Override
    2.60      public boolean retainAll(Collection<?> c) {
    2.61 +        prepareChange();
    2.62          boolean ret = super.retainAll(c);
    2.63          notifyChange();
    2.64          return ret;
    2.65 @@ -142,6 +150,7 @@
    2.66  
    2.67      @Override
    2.68      public T set(int index, T element) {
    2.69 +        prepareChange();
    2.70          T ret = super.set(index, element);
    2.71          notifyChange();
    2.72          return ret;
    2.73 @@ -149,12 +158,14 @@
    2.74  
    2.75      @Override
    2.76      public void add(int index, T element) {
    2.77 +        prepareChange();
    2.78          super.add(index, element);
    2.79          notifyChange();
    2.80      }
    2.81  
    2.82      @Override
    2.83      public T remove(int index) {
    2.84 +        prepareChange();
    2.85          T ret = super.remove(index);
    2.86          notifyChange();
    2.87          return ret;
    2.88 @@ -179,6 +190,16 @@
    2.89          return sb.toString();
    2.90      }
    2.91  
    2.92 +    private void prepareChange() {
    2.93 +        if (index == Integer.MIN_VALUE) {
    2.94 +            try {
    2.95 +                proto.initTo(null, null);
    2.96 +            } catch (IllegalStateException ex) {
    2.97 +                throw new UnsupportedOperationException();
    2.98 +            }
    2.99 +        }
   2.100 +    }
   2.101 +
   2.102      private void notifyChange() {
   2.103          proto.getContext().execute(new Runnable() {
   2.104              @Override
     3.1 --- a/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java	Mon Feb 15 05:27:28 2016 +0100
     3.2 +++ b/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java	Mon Feb 22 06:09:33 2016 +0100
     3.3 @@ -263,11 +263,15 @@
     3.4                          String[] gs = toGetSet(p.name(), tn, p.array());
     3.5                          w.write("    this.prop_" + p.name() + " = proto.createList(\""
     3.6                              + p.name() + "\"");
     3.7 -                        if (functionDeps.containsKey(p.name())) {
     3.8 -                            int index = Arrays.asList(functionDeps.keySet().toArray()).indexOf(p.name());
     3.9 -                            w.write(", " + index);
    3.10 +                        if (p.mutable()) {
    3.11 +                            if (functionDeps.containsKey(p.name())) {
    3.12 +                                int index = Arrays.asList(functionDeps.keySet().toArray()).indexOf(p.name());
    3.13 +                                w.write(", " + index);
    3.14 +                            } else {
    3.15 +                                w.write(", -1");
    3.16 +                            }
    3.17                          } else {
    3.18 -                            w.write(", -1");
    3.19 +                            w.write(", java.lang.Integer.MIN_VALUE");
    3.20                          }
    3.21                          Collection<String[]> dependants = propsDeps.get(p.name());
    3.22                          if (dependants != null) {
    3.23 @@ -672,6 +676,9 @@
    3.24                  w.write("    return (" + tn + ")prop_" + p.name() + ";\n");
    3.25                  w.write("  }\n");
    3.26                  w.write("  public void " + gs[1] + "(" + tn + " v) {\n");
    3.27 +                if (!p.mutable()) {
    3.28 +                    w.write("    proto.initTo(null, null);\n");
    3.29 +                }
    3.30                  w.write("    proto.verifyUnlocked();\n");
    3.31                  w.write("    Object o = prop_" + p.name() + ";\n");
    3.32                  if (isModel[0]) {
    3.33 @@ -2001,6 +2008,10 @@
    3.34              return p.array();
    3.35          }
    3.36  
    3.37 +        boolean mutable() {
    3.38 +            return p.mutable();
    3.39 +        }
    3.40 +
    3.41          String typeName(ProcessingEnvironment env) {
    3.42              RuntimeException ex;
    3.43              try {
     4.1 --- a/json/src/main/java/org/netbeans/html/json/spi/Proto.java	Mon Feb 15 05:27:28 2016 +0100
     4.2 +++ b/json/src/main/java/org/netbeans/html/json/spi/Proto.java	Mon Feb 22 06:09:33 2016 +0100
     4.3 @@ -48,6 +48,7 @@
     4.4  import net.java.html.BrwsrCtx;
     4.5  import net.java.html.json.ComputedProperty;
     4.6  import net.java.html.json.Model;
     4.7 +import net.java.html.json.Property;
     4.8  import org.netbeans.html.json.impl.Bindings;
     4.9  import org.netbeans.html.json.impl.JSON;
    4.10  import org.netbeans.html.json.impl.JSON.WS;
    4.11 @@ -463,7 +464,10 @@
    4.12       * @param <T> the type of the list elements
    4.13       * @param propName name of a property this list is associated with
    4.14       * @param onChange index of the property to use when the list is modified
    4.15 -     *   during callback to {@link Type#onChange(java.lang.Object, int)}
    4.16 +     *   during callback to {@link Type#onChange(java.lang.Object, int)}.
    4.17 +     *   If the value is {@link Integer#MIN_VALUE}, then the list is
    4.18 +     *   not fully {@link Property#mutable()} and throws {@link UnsupportedOperationException}
    4.19 +     *   on such attempts.
    4.20       * @param dependingProps the array of {@link ComputedProperty derived properties}
    4.21       *   that depend on the value of the list
    4.22       * @return new, empty list associated with this proto-object and its model
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/json/src/test/java/net/java/html/json/MapModelNotMutableTest.java	Mon Feb 22 06:09:33 2016 +0100
     5.3 @@ -0,0 +1,227 @@
     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 net.java.html.json;
    5.47 +
    5.48 +import java.util.Map;
    5.49 +import net.java.html.BrwsrCtx;
    5.50 +import org.netbeans.html.context.spi.Contexts;
    5.51 +import org.netbeans.html.json.spi.Technology;
    5.52 +import org.netbeans.html.json.spi.Transfer;
    5.53 +import static org.testng.Assert.assertEquals;
    5.54 +import static org.testng.Assert.assertFalse;
    5.55 +import static org.testng.Assert.assertNotNull;
    5.56 +import static org.testng.Assert.fail;
    5.57 +import org.testng.annotations.BeforeMethod;
    5.58 +import org.testng.annotations.Test;
    5.59 +
    5.60 +@Model(className = "ConstantValues", properties = {
    5.61 +    @Property(name = "byteNumber", type = byte.class, mutable = false),
    5.62 +    @Property(name = "shortNumber", type = short.class, mutable = false),
    5.63 +    @Property(name = "intNumber", type = int.class, mutable = false),
    5.64 +    @Property(name = "longNumber", type = long.class, mutable = false),
    5.65 +    @Property(name = "floatNumber", type = float.class, mutable = false),
    5.66 +    @Property(name = "doubleNumber", type = double.class, mutable = false),
    5.67 +    @Property(name = "stringValue", type = String.class, mutable = false),
    5.68 +    @Property(name = "byteArray", type = byte.class, mutable = false, array = true),
    5.69 +    @Property(name = "shortArray", type = short.class, mutable = false, array = true),
    5.70 +    @Property(name = "intArray", type = int.class, mutable = false, array = true),
    5.71 +    @Property(name = "longArray", type = long.class, mutable = false, array = true),
    5.72 +    @Property(name = "floatArray", type = float.class, mutable = false, array = true),
    5.73 +    @Property(name = "doubleArray", type = double.class, mutable = false, array = true),
    5.74 +    @Property(name = "stringArray", type = String.class, mutable = false, array = true),
    5.75 +})
    5.76 +public class MapModelNotMutableTest {
    5.77 +    private BrwsrCtx c;
    5.78 +
    5.79 +    @BeforeMethod
    5.80 +    public void initTechnology() {
    5.81 +        MapModelTest.MapTechnology t = new MapModelTest.MapTechnology();
    5.82 +        c = Contexts.newBuilder().register(Technology.class, t, 1).
    5.83 +            register(Transfer.class, t, 1).build();
    5.84 +    }
    5.85 +
    5.86 +    @Test
    5.87 +    public void byteConstant() throws Exception {
    5.88 +        ConstantValues value = Models.bind(new ConstantValues(), c);
    5.89 +        value.setByteNumber((byte)13);
    5.90 +
    5.91 +        Map m = (Map) Models.toRaw(value);
    5.92 +        Object v = m.get("byteNumber");
    5.93 +        assertNotNull(v, "Value should be in the map");
    5.94 +        assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One");
    5.95 +        MapModelTest.One o = (MapModelTest.One) v;
    5.96 +        assertEquals(o.changes, 0, "No change so far the only one change happened before we connected");
    5.97 +        assertEquals((byte)13, o.get());
    5.98 +
    5.99 +        try {
   5.100 +            value.setByteNumber((byte)15);
   5.101 +            fail("Changing value shouldn't succeed!");
   5.102 +        } catch (IllegalStateException ex) {
   5.103 +            // OK
   5.104 +        }
   5.105 +        assertEquals(o.get(), (byte)13, "Old value should still be in the map");
   5.106 +        assertEquals(o.changes, 0, "No change");
   5.107 +        assertFalse(o.pb.isReadOnly(), "Mutable property");
   5.108 +    }
   5.109 +
   5.110 +    @Test
   5.111 +    public void shortConstant() throws Exception {
   5.112 +        ConstantValues value = Models.bind(new ConstantValues(), c);
   5.113 +        value.setShortNumber((short)13);
   5.114 +
   5.115 +        Map m = (Map) Models.toRaw(value);
   5.116 +        Object v = m.get("shortNumber");
   5.117 +        assertNotNull(v, "Value should be in the map");
   5.118 +        assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One");
   5.119 +        MapModelTest.One o = (MapModelTest.One) v;
   5.120 +        assertEquals(o.changes, 0, "No change so far the only one change happened before we connected");
   5.121 +        assertEquals((short)13, o.get());
   5.122 +
   5.123 +        try {
   5.124 +            value.setShortNumber((short)15);
   5.125 +            fail("Changing value shouldn't succeed!");
   5.126 +        } catch (IllegalStateException ex) {
   5.127 +            // OK
   5.128 +        }
   5.129 +        assertEquals(o.get(), (short)13, "Old value should still be in the map");
   5.130 +        assertEquals(o.changes, 0, "No change");
   5.131 +        assertFalse(o.pb.isReadOnly(), "Mutable property");
   5.132 +    }
   5.133 +
   5.134 +    @Test
   5.135 +    public void intConstant() throws Exception {
   5.136 +        ConstantValues value = Models.bind(new ConstantValues(), c);
   5.137 +        value.setIntNumber(13);
   5.138 +
   5.139 +        Map m = (Map) Models.toRaw(value);
   5.140 +        Object v = m.get("intNumber");
   5.141 +        assertNotNull(v, "Value should be in the map");
   5.142 +        assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One");
   5.143 +        MapModelTest.One o = (MapModelTest.One) v;
   5.144 +        assertEquals(o.changes, 0, "No change so far the only one change happened before we connected");
   5.145 +        assertEquals(13, o.get());
   5.146 +
   5.147 +        try {
   5.148 +            value.setIntNumber(15);
   5.149 +            fail("Changing value shouldn't succeed!");
   5.150 +        } catch (IllegalStateException ex) {
   5.151 +            // OK
   5.152 +        }
   5.153 +        assertEquals(o.get(), 13, "Old value should still be in the map");
   5.154 +        assertEquals(o.changes, 0, "No change");
   5.155 +        assertFalse(o.pb.isReadOnly(), "Mutable property");
   5.156 +    }
   5.157 +
   5.158 +    @Test
   5.159 +    public void doubleConstant() throws Exception {
   5.160 +        ConstantValues value = Models.bind(new ConstantValues(), c);
   5.161 +        value.setDoubleNumber(13);
   5.162 +
   5.163 +        Map m = (Map) Models.toRaw(value);
   5.164 +        Object v = m.get("doubleNumber");
   5.165 +        assertNotNull(v, "Value should be in the map");
   5.166 +        assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One");
   5.167 +        MapModelTest.One o = (MapModelTest.One) v;
   5.168 +        assertEquals(o.changes, 0, "No change so far the only one change happened before we connected");
   5.169 +        assertEquals(13.0, o.get());
   5.170 +
   5.171 +        try {
   5.172 +            value.setDoubleNumber(15);
   5.173 +            fail("Changing value shouldn't succeed!");
   5.174 +        } catch (IllegalStateException ex) {
   5.175 +            // OK
   5.176 +        }
   5.177 +        assertEquals(o.get(), 13.0, "Old value should still be in the map");
   5.178 +        assertEquals(o.changes, 0, "No change");
   5.179 +        assertFalse(o.pb.isReadOnly(), "Mutable property");
   5.180 +    }
   5.181 +
   5.182 +    @Test
   5.183 +    public void stringConstant() throws Exception {
   5.184 +        ConstantValues value = Models.bind(new ConstantValues(), c);
   5.185 +        value.setStringValue("Hi");
   5.186 +
   5.187 +        Map m = (Map) Models.toRaw(value);
   5.188 +        Object v = m.get("stringValue");
   5.189 +        assertNotNull(v, "Value should be in the map");
   5.190 +        assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One");
   5.191 +        MapModelTest.One o = (MapModelTest.One) v;
   5.192 +        assertEquals(o.changes, 0, "No change so far the only one change happened before we connected");
   5.193 +        assertEquals("Hi", o.get());
   5.194 +
   5.195 +        try {
   5.196 +            value.setStringValue("Hello");
   5.197 +            fail("Changing value shouldn't succeed!");
   5.198 +        } catch (IllegalStateException ex) {
   5.199 +            // OK
   5.200 +        }
   5.201 +        assertEquals(o.get(), "Hi", "Old value should still be in the map");
   5.202 +        assertEquals(o.changes, 0, "No change");
   5.203 +        assertFalse(o.pb.isReadOnly(), "Mutable property");
   5.204 +    }
   5.205 +
   5.206 +    @Test
   5.207 +    public void stringArray() throws Exception {
   5.208 +        ConstantValues value = Models.bind(new ConstantValues(), c);
   5.209 +        value.getStringArray().add("Hi");
   5.210 +
   5.211 +        Map m = (Map) Models.toRaw(value);
   5.212 +        Object v = m.get("stringArray");
   5.213 +        assertNotNull(v, "Value should be in the map");
   5.214 +        assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One");
   5.215 +        MapModelTest.One o = (MapModelTest.One) v;
   5.216 +        assertEquals(o.changes, 0, "No change so far the only one change happened before we connected");
   5.217 +        assertEquals(o.get(), new String[] { "Hi" }, "One element");
   5.218 +
   5.219 +        try {
   5.220 +            value.getStringArray().add("Hello");
   5.221 +            fail("Changing value shouldn't succeed!");
   5.222 +        } catch (UnsupportedOperationException ex) {
   5.223 +            // OK
   5.224 +        }
   5.225 +        assertEquals(o.get(), new String[] { "Hi" }, "Old value should still be in the map");
   5.226 +        assertEquals(o.changes, 0, "No change");
   5.227 +        assertFalse(o.pb.isReadOnly(), "Mutable property");
   5.228 +    }
   5.229 +
   5.230 +}