# HG changeset patch # User Jaroslav Tulach # Date 1456117773 -3600 # Node ID 4c40ceb185e5d6b7b447279c4ad6fe760a596f22 # Parent ed4b25eb66f32b3aa943dfc43d5a4b1dfb71d2b7 #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 diff -r ed4b25eb66f3 -r 4c40ceb185e5 json/src/main/java/net/java/html/json/Property.java --- a/json/src/main/java/net/java/html/json/Property.java Mon Feb 15 05:27:28 2016 +0100 +++ b/json/src/main/java/net/java/html/json/Property.java Mon Feb 22 06:09:33 2016 +0100 @@ -46,6 +46,8 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.List; +import org.netbeans.html.context.spi.Contexts; +import org.netbeans.html.json.spi.Technology; /** Represents a property in a class defined with {@link Model} annotation. * @@ -76,4 +78,21 @@ * @return true, if this property is supposed to represent an array of values */ boolean array() default false; + + /** Can the value of the property be mutated without restriction or not. + * If a property is defined as not mutable, it defines + * semi-immutable value that can only be changed in construction time + * before the object is passed to underlying {@link Technology}. + * Attempts to modify the object later yield {@link IllegalStateException}. + * + * Technologies may decide to represent such non-mutable + * property in more effective way - for + * example Knockout Java Bindings technology (with {@link Contexts.Id id} "ko4j") + * uses plain JavaScript value (number, string, array, boolean) rather + * than classical observable. + * + * @return false if the value cannot change after its first use + * @since 1.3 + */ + boolean mutable() default true; } diff -r ed4b25eb66f3 -r 4c40ceb185e5 json/src/main/java/org/netbeans/html/json/impl/JSONList.java --- a/json/src/main/java/org/netbeans/html/json/impl/JSONList.java Mon Feb 15 05:27:28 2016 +0100 +++ b/json/src/main/java/org/netbeans/html/json/impl/JSONList.java Mon Feb 22 06:09:33 2016 +0100 @@ -88,6 +88,7 @@ @Override public boolean add(T e) { + prepareChange(); boolean ret = super.add(e); notifyChange(); return ret; @@ -95,6 +96,7 @@ @Override public boolean addAll(Collection c) { + prepareChange(); boolean ret = super.addAll(c); notifyChange(); return ret; @@ -102,12 +104,14 @@ @Override public boolean addAll(int index, Collection c) { + prepareChange(); boolean ret = super.addAll(index, c); notifyChange(); return ret; } public void fastReplace(Collection c) { + prepareChange(); super.clear(); super.addAll(c); notifyChange(); @@ -115,6 +119,7 @@ @Override public boolean remove(Object o) { + prepareChange(); boolean ret = super.remove(o); notifyChange(); return ret; @@ -122,12 +127,14 @@ @Override public void clear() { + prepareChange(); super.clear(); notifyChange(); } @Override public boolean removeAll(Collection c) { + prepareChange(); boolean ret = super.removeAll(c); notifyChange(); return ret; @@ -135,6 +142,7 @@ @Override public boolean retainAll(Collection c) { + prepareChange(); boolean ret = super.retainAll(c); notifyChange(); return ret; @@ -142,6 +150,7 @@ @Override public T set(int index, T element) { + prepareChange(); T ret = super.set(index, element); notifyChange(); return ret; @@ -149,12 +158,14 @@ @Override public void add(int index, T element) { + prepareChange(); super.add(index, element); notifyChange(); } @Override public T remove(int index) { + prepareChange(); T ret = super.remove(index); notifyChange(); return ret; @@ -179,6 +190,16 @@ return sb.toString(); } + private void prepareChange() { + if (index == Integer.MIN_VALUE) { + try { + proto.initTo(null, null); + } catch (IllegalStateException ex) { + throw new UnsupportedOperationException(); + } + } + } + private void notifyChange() { proto.getContext().execute(new Runnable() { @Override diff -r ed4b25eb66f3 -r 4c40ceb185e5 json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java --- a/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java Mon Feb 15 05:27:28 2016 +0100 +++ b/json/src/main/java/org/netbeans/html/json/impl/ModelProcessor.java Mon Feb 22 06:09:33 2016 +0100 @@ -263,11 +263,15 @@ String[] gs = toGetSet(p.name(), tn, p.array()); w.write(" this.prop_" + p.name() + " = proto.createList(\"" + p.name() + "\""); - if (functionDeps.containsKey(p.name())) { - int index = Arrays.asList(functionDeps.keySet().toArray()).indexOf(p.name()); - w.write(", " + index); + if (p.mutable()) { + if (functionDeps.containsKey(p.name())) { + int index = Arrays.asList(functionDeps.keySet().toArray()).indexOf(p.name()); + w.write(", " + index); + } else { + w.write(", -1"); + } } else { - w.write(", -1"); + w.write(", java.lang.Integer.MIN_VALUE"); } Collection dependants = propsDeps.get(p.name()); if (dependants != null) { @@ -672,6 +676,9 @@ w.write(" return (" + tn + ")prop_" + p.name() + ";\n"); w.write(" }\n"); w.write(" public void " + gs[1] + "(" + tn + " v) {\n"); + if (!p.mutable()) { + w.write(" proto.initTo(null, null);\n"); + } w.write(" proto.verifyUnlocked();\n"); w.write(" Object o = prop_" + p.name() + ";\n"); if (isModel[0]) { @@ -2001,6 +2008,10 @@ return p.array(); } + boolean mutable() { + return p.mutable(); + } + String typeName(ProcessingEnvironment env) { RuntimeException ex; try { diff -r ed4b25eb66f3 -r 4c40ceb185e5 json/src/main/java/org/netbeans/html/json/spi/Proto.java --- a/json/src/main/java/org/netbeans/html/json/spi/Proto.java Mon Feb 15 05:27:28 2016 +0100 +++ b/json/src/main/java/org/netbeans/html/json/spi/Proto.java Mon Feb 22 06:09:33 2016 +0100 @@ -48,6 +48,7 @@ import net.java.html.BrwsrCtx; import net.java.html.json.ComputedProperty; import net.java.html.json.Model; +import net.java.html.json.Property; import org.netbeans.html.json.impl.Bindings; import org.netbeans.html.json.impl.JSON; import org.netbeans.html.json.impl.JSON.WS; @@ -463,7 +464,10 @@ * @param the type of the list elements * @param propName name of a property this list is associated with * @param onChange index of the property to use when the list is modified - * during callback to {@link Type#onChange(java.lang.Object, int)} + * during callback to {@link Type#onChange(java.lang.Object, int)}. + * If the value is {@link Integer#MIN_VALUE}, then the list is + * not fully {@link Property#mutable()} and throws {@link UnsupportedOperationException} + * on such attempts. * @param dependingProps the array of {@link ComputedProperty derived properties} * that depend on the value of the list * @return new, empty list associated with this proto-object and its model diff -r ed4b25eb66f3 -r 4c40ceb185e5 json/src/test/java/net/java/html/json/MapModelNotMutableTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/json/src/test/java/net/java/html/json/MapModelNotMutableTest.java Mon Feb 22 06:09:33 2016 +0100 @@ -0,0 +1,227 @@ +/** + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Oracle. Portions Copyright 2013-2014 Oracle. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ +package net.java.html.json; + +import java.util.Map; +import net.java.html.BrwsrCtx; +import org.netbeans.html.context.spi.Contexts; +import org.netbeans.html.json.spi.Technology; +import org.netbeans.html.json.spi.Transfer; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.fail; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +@Model(className = "ConstantValues", properties = { + @Property(name = "byteNumber", type = byte.class, mutable = false), + @Property(name = "shortNumber", type = short.class, mutable = false), + @Property(name = "intNumber", type = int.class, mutable = false), + @Property(name = "longNumber", type = long.class, mutable = false), + @Property(name = "floatNumber", type = float.class, mutable = false), + @Property(name = "doubleNumber", type = double.class, mutable = false), + @Property(name = "stringValue", type = String.class, mutable = false), + @Property(name = "byteArray", type = byte.class, mutable = false, array = true), + @Property(name = "shortArray", type = short.class, mutable = false, array = true), + @Property(name = "intArray", type = int.class, mutable = false, array = true), + @Property(name = "longArray", type = long.class, mutable = false, array = true), + @Property(name = "floatArray", type = float.class, mutable = false, array = true), + @Property(name = "doubleArray", type = double.class, mutable = false, array = true), + @Property(name = "stringArray", type = String.class, mutable = false, array = true), +}) +public class MapModelNotMutableTest { + private BrwsrCtx c; + + @BeforeMethod + public void initTechnology() { + MapModelTest.MapTechnology t = new MapModelTest.MapTechnology(); + c = Contexts.newBuilder().register(Technology.class, t, 1). + register(Transfer.class, t, 1).build(); + } + + @Test + public void byteConstant() throws Exception { + ConstantValues value = Models.bind(new ConstantValues(), c); + value.setByteNumber((byte)13); + + Map m = (Map) Models.toRaw(value); + Object v = m.get("byteNumber"); + assertNotNull(v, "Value should be in the map"); + assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One"); + MapModelTest.One o = (MapModelTest.One) v; + assertEquals(o.changes, 0, "No change so far the only one change happened before we connected"); + assertEquals((byte)13, o.get()); + + try { + value.setByteNumber((byte)15); + fail("Changing value shouldn't succeed!"); + } catch (IllegalStateException ex) { + // OK + } + assertEquals(o.get(), (byte)13, "Old value should still be in the map"); + assertEquals(o.changes, 0, "No change"); + assertFalse(o.pb.isReadOnly(), "Mutable property"); + } + + @Test + public void shortConstant() throws Exception { + ConstantValues value = Models.bind(new ConstantValues(), c); + value.setShortNumber((short)13); + + Map m = (Map) Models.toRaw(value); + Object v = m.get("shortNumber"); + assertNotNull(v, "Value should be in the map"); + assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One"); + MapModelTest.One o = (MapModelTest.One) v; + assertEquals(o.changes, 0, "No change so far the only one change happened before we connected"); + assertEquals((short)13, o.get()); + + try { + value.setShortNumber((short)15); + fail("Changing value shouldn't succeed!"); + } catch (IllegalStateException ex) { + // OK + } + assertEquals(o.get(), (short)13, "Old value should still be in the map"); + assertEquals(o.changes, 0, "No change"); + assertFalse(o.pb.isReadOnly(), "Mutable property"); + } + + @Test + public void intConstant() throws Exception { + ConstantValues value = Models.bind(new ConstantValues(), c); + value.setIntNumber(13); + + Map m = (Map) Models.toRaw(value); + Object v = m.get("intNumber"); + assertNotNull(v, "Value should be in the map"); + assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One"); + MapModelTest.One o = (MapModelTest.One) v; + assertEquals(o.changes, 0, "No change so far the only one change happened before we connected"); + assertEquals(13, o.get()); + + try { + value.setIntNumber(15); + fail("Changing value shouldn't succeed!"); + } catch (IllegalStateException ex) { + // OK + } + assertEquals(o.get(), 13, "Old value should still be in the map"); + assertEquals(o.changes, 0, "No change"); + assertFalse(o.pb.isReadOnly(), "Mutable property"); + } + + @Test + public void doubleConstant() throws Exception { + ConstantValues value = Models.bind(new ConstantValues(), c); + value.setDoubleNumber(13); + + Map m = (Map) Models.toRaw(value); + Object v = m.get("doubleNumber"); + assertNotNull(v, "Value should be in the map"); + assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One"); + MapModelTest.One o = (MapModelTest.One) v; + assertEquals(o.changes, 0, "No change so far the only one change happened before we connected"); + assertEquals(13.0, o.get()); + + try { + value.setDoubleNumber(15); + fail("Changing value shouldn't succeed!"); + } catch (IllegalStateException ex) { + // OK + } + assertEquals(o.get(), 13.0, "Old value should still be in the map"); + assertEquals(o.changes, 0, "No change"); + assertFalse(o.pb.isReadOnly(), "Mutable property"); + } + + @Test + public void stringConstant() throws Exception { + ConstantValues value = Models.bind(new ConstantValues(), c); + value.setStringValue("Hi"); + + Map m = (Map) Models.toRaw(value); + Object v = m.get("stringValue"); + assertNotNull(v, "Value should be in the map"); + assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One"); + MapModelTest.One o = (MapModelTest.One) v; + assertEquals(o.changes, 0, "No change so far the only one change happened before we connected"); + assertEquals("Hi", o.get()); + + try { + value.setStringValue("Hello"); + fail("Changing value shouldn't succeed!"); + } catch (IllegalStateException ex) { + // OK + } + assertEquals(o.get(), "Hi", "Old value should still be in the map"); + assertEquals(o.changes, 0, "No change"); + assertFalse(o.pb.isReadOnly(), "Mutable property"); + } + + @Test + public void stringArray() throws Exception { + ConstantValues value = Models.bind(new ConstantValues(), c); + value.getStringArray().add("Hi"); + + Map m = (Map) Models.toRaw(value); + Object v = m.get("stringArray"); + assertNotNull(v, "Value should be in the map"); + assertEquals(v.getClass(), MapModelTest.One.class, "It is instance of One"); + MapModelTest.One o = (MapModelTest.One) v; + assertEquals(o.changes, 0, "No change so far the only one change happened before we connected"); + assertEquals(o.get(), new String[] { "Hi" }, "One element"); + + try { + value.getStringArray().add("Hello"); + fail("Changing value shouldn't succeed!"); + } catch (UnsupportedOperationException ex) { + // OK + } + assertEquals(o.get(), new String[] { "Hi" }, "Old value should still be in the map"); + assertEquals(o.changes, 0, "No change"); + assertFalse(o.pb.isReadOnly(), "Mutable property"); + } + +}