# HG changeset patch # User Jaroslav Tulach # Date 1456720411 -3600 # Node ID 3554078c32ce605dab542772af94c18a0067b68a # Parent ed4b25eb66f32b3aa943dfc43d5a4b1dfb71d2b7# Parent a753f36c67fcc0fd776553b9ef8d1e32a94c1a79 #258088: Merging @Property(mutable=false) into main development line diff -r ed4b25eb66f3 -r 3554078c32ce json-tck/src/main/java/net/java/html/json/tests/KnockoutTest.java --- a/json-tck/src/main/java/net/java/html/json/tests/KnockoutTest.java Mon Feb 15 05:27:28 2016 +0100 +++ b/json-tck/src/main/java/net/java/html/json/tests/KnockoutTest.java Mon Feb 29 05:33:31 2016 +0100 @@ -73,14 +73,14 @@ @Property(name="choice", type=KnockoutTest.Choice.class), @Property(name="archetype", type=ArchetypeData.class), @Property(name="archetypes", type=ArchetypeData.class, array = true), -}) +}) public final class KnockoutTest { private KnockoutModel js; - + enum Choice { A, B; } - + @ComputedProperty static List resultLengths(List results) { Integer[] arr = new Integer[results.size()]; for (int i = 0; i < arr.length; i++) { @@ -88,9 +88,9 @@ } return Arrays.asList(arr); } - + @KOTest public void modifyValueAssertChangeInModelOnEnum() throws Throwable { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "Latitude: \n" ); try { @@ -143,9 +143,9 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + @KOTest public void modifyValueAssertChangeInModelOnDouble() throws Throwable { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "Latitude: \n" ); try { @@ -167,7 +167,7 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + @KOTest public void rawObject() throws Exception { if (js == null) { final BrwsrCtx ctx = newContext(); @@ -200,7 +200,7 @@ p1.setFirstName("Ondra"); assertEquals(p1.getFirstName(), "Ondra", "1st name updated in original object"); - + js.getPeople().add(p1); } @@ -239,9 +239,9 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + @KOTest public void modifyValueAssertChangeInModelOnBoolean() throws Throwable { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "Latitude: \n" ); try { @@ -263,9 +263,9 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + @KOTest public void modifyValueAssertChangeInModel() throws Exception { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "

Loading Bck2Brwsr's Hello World...

\n" + "Your name: \n" + "\n" @@ -287,7 +287,7 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + private static String getSetSelected(int index, Object value) throws Exception { String s = "var index = arguments[0];\n" + "var value = arguments[1];\n" @@ -305,7 +305,7 @@ ); return ret == null ? null : ret.toString(); } - + @Model(className = "ArchetypeData", properties = { @Property(name = "artifactId", type = String.class), @Property(name = "groupId", type = String.class), @@ -316,35 +316,35 @@ }) static class ArchModel { } - + @KOTest public void selectWorksOnModels() throws Exception { if (js == null) { - Utils.exposeHTML(KnockoutTest.class, + Utils.exposeHTML(KnockoutTest.class, "\n" + "" ); - + js = Models.bind(new KnockoutModel(), newContext()); js.getArchetypes().add(new ArchetypeData("ko4j", "org.netbeans.html", "0.8.3", "ko4j", "ko4j", null)); js.getArchetypes().add(new ArchetypeData("crud", "org.netbeans.html", "0.8.3", "crud", "crud", null)); js.getArchetypes().add(new ArchetypeData("3rd", "org.netbeans.html", "0.8.3", "3rd", "3rd", null)); js.setArchetype(js.getArchetypes().get(1)); js.applyBindings(); - + String v = getSetSelected(0, null); assertEquals("crud", v, "Second index (e.g. crud) is selected: " + v); - + String sel = getSetSelected(2, Models.toRaw(js.getArchetypes().get(2))); assertEquals("3rd", sel, "3rd is selected now: " + sel); } - + if (js.getArchetype() != js.getArchetypes().get(2)) { throw new InterruptedException(); } - + Utils.exposeHTML(KnockoutTest.class, ""); } @@ -374,22 +374,22 @@ assertEquals("org.netbeans.html", v, "groupId has been changed"); Utils.exposeHTML(KnockoutTest.class, ""); } - + @KOTest public void modifyValueAssertAsyncChangeInModel() throws Exception { if (js == null) { - Utils.exposeHTML(KnockoutTest.class, + Utils.exposeHTML(KnockoutTest.class, "

Loading Bck2Brwsr's Hello World...

\n" + "Your name: \n" + "\n" ); - + js = Models.bind(new KnockoutModel(), newContext()); js.setName("Kukuc"); js.applyBindings(); - + String v = getSetInput("input", null); assertEquals("Kukuc", v, "Value is really kukuc: " + v); - + Timer t = new Timer("Set to Jardo"); t.schedule(new TimerTask() { @Override @@ -398,15 +398,100 @@ } }, 1); } - + String v = getSetInput("input", null); if (!"Jardo".equals(v)) { throw new InterruptedException(); } - + Utils.exposeHTML(KnockoutTest.class, ""); } - + + @Model(className = "ConstantModel", targetId = "", builder = "assign", properties = { + @Property(name = "doubleValue", mutable = false, type = double.class), + @Property(name = "longValue", mutable = false, type = long.class), + @Property(name = "stringValue", mutable = false, type = String.class), + @Property(name = "boolValue", mutable = false, type = boolean.class), + @Property(name = "intArray", mutable = false, type = int.class, array = true), + }) + static class ConstantCntrl { + } + + @KOTest public void nonMutableDouble() throws Exception { + Utils.exposeHTML(KnockoutTest.class, + "Type: \n" + ); + + ConstantModel model = Models.bind(new ConstantModel(), newContext()); + model.assignStringValue("Hello").assignDoubleValue(10.0); + model.applyBindings(); + + String v = getSetInput("input", null); + assertEquals(v, "number", "Right type found: " + v); + + Utils.exposeHTML(KnockoutTest.class, ""); + } + + @KOTest public void nonMutableString() throws Exception { + Utils.exposeHTML(KnockoutTest.class, + "Type: \n" + ); + + ConstantModel model = Models.bind(new ConstantModel(), newContext()); + model.assignStringValue("Hello").assignDoubleValue(10.0); + model.applyBindings(); + + String v = getSetInput("input", null); + assertEquals(v, "string", "Right type found: " + v); + + Utils.exposeHTML(KnockoutTest.class, ""); + } + + @KOTest public void nonMutableBoolean() throws Exception { + Utils.exposeHTML(KnockoutTest.class, + "Type: \n" + ); + + ConstantModel model = Models.bind(new ConstantModel(), newContext()); + model.assignStringValue("Hello").assignBoolValue(true); + model.applyBindings(); + + String v = getSetInput("input", null); + assertEquals(v, "boolean", "Right type found: " + v); + + Utils.exposeHTML(KnockoutTest.class, ""); + } + + @KOTest public void nonMutableLong() throws Exception { + Utils.exposeHTML(KnockoutTest.class, + "Type: \n" + ); + + ConstantModel model = Models.bind(new ConstantModel(), newContext()); + model.assignStringValue("Hello").assignLongValue(Long.MAX_VALUE); + model.applyBindings(); + + String v = getSetInput("input", null); + assertEquals(v, "number", "Right type found: " + v); + + Utils.exposeHTML(KnockoutTest.class, ""); + } + + @KOTest public void nonMutableIntArray() throws Exception { + Utils.exposeHTML(KnockoutTest.class, + "Type: \n" + ); + + ConstantModel model = Models.bind(new ConstantModel(), newContext()); + model.assignStringValue("Hello").assignLongValue(Long.MAX_VALUE).assignIntArray(1, 2, 3, 4); + model.applyBindings(); + + String v = getSetInput("input", null); + assertEquals(v, "object", "Right type found: " + v); + + Utils.exposeHTML(KnockoutTest.class, ""); + } + private static String getSetInput(String id, String value) throws Exception { String s = "var value = arguments[0];\n" + "var n = window.document.getElementById(arguments[1]); \n " @@ -429,7 +514,7 @@ ); return Boolean.TRUE.equals(ret); } - + public static void triggerEvent(String id, String ev) throws Exception { Utils.executeScript( KnockoutTest.class, @@ -437,9 +522,9 @@ id, ev ); } - + @KOTest public void displayContentOfArray() throws Exception { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "
    \n" + "
  • \n" + "
\n" @@ -465,10 +550,10 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + @KOTest public void displayContentOfAsyncArray() throws Exception { if (js == null) { - Utils.exposeHTML(KnockoutTest.class, + Utils.exposeHTML(KnockoutTest.class, "
    \n" + "
  • \n" + "
\n" @@ -479,7 +564,7 @@ int cnt = Utils.countChildren(KnockoutTest.class, "ul"); assertEquals(cnt, 1, "One child, but was " + cnt); - + Timer t = new Timer("add to array"); t.schedule(new TimerTask() { @Override @@ -504,9 +589,9 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + @KOTest public void displayContentOfComputedArray() throws Exception { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "
    \n" + "
  • \n" + "
\n" @@ -521,23 +606,23 @@ triggerChildClick("ul", 1); assertEquals("Last", m.getFirstName(), "We got callback from 2nd child " + m.getFirstName()); - + m.setLastName("Verylast"); cnt = Utils.countChildren(KnockoutTest.class, "ul"); assertEquals(cnt, 2, "Two children now, but was " + cnt); - + triggerChildClick("ul", 1); assertEquals("Verylast", m.getFirstName(), "We got callback from 2nd child " + m.getFirstName()); - + } finally { Utils.exposeHTML(KnockoutTest.class, ""); } } - + @KOTest public void displayContentOfComputedArrayOnASubpair() throws Exception { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "
\n" + "
    \n" + "
  • \n" @@ -553,7 +638,7 @@ assertEquals(cnt, 2, "Two children now, but was " + cnt); triggerChildClick("ul", 1); - + assertEquals(PairModel.ctx, ctx, "Context remains the same"); assertEquals("Last", m.getFirstName(), "We got callback from 2nd child " + m.getFirstName()); @@ -561,9 +646,9 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + @KOTest public void displayContentOfComputedArrayOnComputedASubpair() throws Exception { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "
    \n" + "
      \n" + "
    • \n" @@ -586,7 +671,7 @@ } @KOTest public void checkBoxToBooleanBinding() throws Exception { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "\n" ); try { @@ -602,11 +687,11 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - - - + + + @KOTest public void displayContentOfDerivedArray() throws Exception { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "
        \n" + "
      • \n" + "
      \n" @@ -627,9 +712,9 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + @KOTest public void displayContentOfArrayOfPeople() throws Exception { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "
        \n" + "
      • \n" + "
      \n" @@ -672,14 +757,14 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + @ComputedProperty static Person firstPerson(List people) { return people.isEmpty() ? null : people.get(0); } - + @KOTest public void accessFirstPersonWithOnFunction() throws Exception { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "

      \n" + " \n" + "

      \n" @@ -690,9 +775,9 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + @KOTest public void onPersonFunction() throws Exception { - Object exp = Utils.exposeHTML(KnockoutTest.class, + Object exp = Utils.exposeHTML(KnockoutTest.class, "
        \n" + "
      • \n" + "
      \n" @@ -703,7 +788,7 @@ Utils.exposeHTML(KnockoutTest.class, ""); } } - + private void trasfertToFemale() throws Exception { KnockoutModel m = Models.bind(new KnockoutModel(), newContext()); @@ -723,7 +808,7 @@ assertEquals(first.getSex(), Sex.FEMALE, "Transverted to female: " + first.getSex()); } - + @KOTest public void stringArrayModificationVisible() throws Exception { Object exp = Utils.exposeHTML(KnockoutTest.class, "
      \n" @@ -737,19 +822,19 @@ m.getResults().add("Ahoj"); m.getResults().add("Hello"); m.applyBindings(); - + int cnt = Utils.countChildren(KnockoutTest.class, "ul"); assertEquals(cnt, 2, "Two children " + cnt); - + Object arr = Utils.addChildren(KnockoutTest.class, "ul", "results", "Hi"); assertTrue(arr instanceof Object[], "Got back an array: " + arr); final int len = ((Object[])arr).length; - + assertEquals(len, 3, "Three elements in the array " + len); - + int newCnt = Utils.countChildren(KnockoutTest.class, "ul"); assertEquals(newCnt, 3, "Three children in the DOM: " + newCnt); - + assertEquals(m.getResults().size(), 3, "Three java strings: " + m.getResults()); } finally { Utils.exposeHTML(KnockoutTest.class, ""); @@ -769,19 +854,19 @@ m.getNumbers().add(1); m.getNumbers().add(31); m.applyBindings(); - + int cnt = Utils.countChildren(KnockoutTest.class, "ul"); assertEquals(cnt, 2, "Two children " + cnt); - + Object arr = Utils.addChildren(KnockoutTest.class, "ul", "numbers", 42); assertTrue(arr instanceof Object[], "Got back an array: " + arr); final int len = ((Object[])arr).length; - + assertEquals(len, 3, "Three elements in the array " + len); - + int newCnt = Utils.countChildren(KnockoutTest.class, "ul"); assertEquals(newCnt, 3, "Three children in the DOM: " + newCnt); - + assertEquals(m.getNumbers().size(), 3, "Three java ints: " + m.getNumbers()); assertEquals(m.getNumbers().get(2), 42, "Meaning of world: " + m.getNumbers()); } finally { @@ -802,26 +887,26 @@ m.getResults().add("Ahoj"); m.getResults().add("Hello"); m.applyBindings(); - + int cnt = Utils.countChildren(KnockoutTest.class, "ul"); assertEquals(cnt, 2, "Two children " + cnt); - + Object arr = Utils.addChildren(KnockoutTest.class, "ul", "results", "Hi"); assertTrue(arr instanceof Object[], "Got back an array: " + arr); final int len = ((Object[])arr).length; - + assertEquals(len, 3, "Three elements in the array " + len); - + int newCnt = Utils.countChildren(KnockoutTest.class, "ul"); assertEquals(newCnt, 3, "Three children in the DOM: " + newCnt); - + assertEquals(m.getResultLengths().size(), 3, "Three java ints: " + m.getResultLengths()); assertEquals(m.getResultLengths().get(2), 2, "Size is two: " + m.getResultLengths()); } finally { Utils.exposeHTML(KnockoutTest.class, ""); } } - + @KOTest public void archetypeArrayModificationVisible() throws Exception { Object exp = Utils.exposeHTML(KnockoutTest.class, "
      \n" @@ -833,19 +918,19 @@ try { KnockoutModel m = Models.bind(new KnockoutModel(), newContext()); m.applyBindings(); - + int cnt = Utils.countChildren(KnockoutTest.class, "ul"); assertEquals(cnt, 0, "No children " + cnt); - + Object arr = Utils.addChildren(KnockoutTest.class, "ul", "archetypes", new ArchetypeData("aid", "gid", "v", "n", "d", "u")); assertTrue(arr instanceof Object[], "Got back an array: " + arr); final int len = ((Object[])arr).length; - + assertEquals(len, 1, "One element in the array " + len); - + int newCnt = Utils.countChildren(KnockoutTest.class, "ul"); assertEquals(newCnt, 1, "One child in the DOM: " + newCnt); - + assertEquals(m.getArchetypes().size(), 1, "One archetype: " + m.getArchetypes()); assertNotNull(m.getArchetypes().get(0), "Not null: " + m.getArchetypes()); assertEquals(m.getArchetypes().get(0).getArtifactId(), "aid", "'aid' == " + m.getArchetypes()); @@ -865,18 +950,18 @@ model.setCallbackCount(model.getCallbackCount() + 1); model.getPeople().remove(data); } - - + + @ComputedProperty static String helloMessage(String name) { return "Hello " + name + "!"; } - + @ComputedProperty static List cmpResults(List results) { return results; } - + private static void triggerClick(String id) throws Exception { String s = "var id = arguments[0];" + "var e = window.document.getElementById(id);\n " @@ -893,17 +978,17 @@ s, id); } private static void triggerChildClick(String id, int pos) throws Exception { - String s = + String s = "var id = arguments[0]; var pos = arguments[1];\n" + "var e = window.document.getElementById(id);\n " + "var ev = window.document.createEvent('MouseEvents');\n " + "ev.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);\n " + "var list = e.childNodes;\n" + - "var cnt = -1;\n" + - "for (var i = 0; i < list.length; i++) {\n" + - " if (list[i].nodeType == 1) cnt++;\n" + - " if (cnt == pos) return list[i].dispatchEvent(ev);\n" + - "}\n" + + "var cnt = -1;\n" + + "for (var i = 0; i < list.length; i++) {\n" + + " if (list[i].nodeType == 1) cnt++;\n" + + " if (cnt == pos) return list[i].dispatchEvent(ev);\n" + + "}\n" + "return null;\n"; Utils.executeScript( KnockoutTest.class, @@ -911,15 +996,15 @@ } private static String childText(String id, int pos) throws Exception { - String s = + String s = "var id = arguments[0]; var pos = arguments[1];" + "var e = window.document.getElementById(id);\n" + "var list = e.childNodes;\n" + - "var cnt = -1;\n" + - "for (var i = 0; i < list.length; i++) {\n" + - " if (list[i].nodeType == 1) cnt++;\n" + - " if (cnt == pos) return list[i].innerHTML;\n" + - "}\n" + + "var cnt = -1;\n" + + "for (var i = 0; i < list.length; i++) {\n" + + " if (list[i].nodeType == 1) cnt++;\n" + + " if (cnt == pos) return list[i].innerHTML;\n" + + "}\n" + "return null;\n"; return (String)Utils.executeScript( KnockoutTest.class, diff -r ed4b25eb66f3 -r 3554078c32ce 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 29 05:33:31 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 3554078c32ce json/src/main/java/org/netbeans/html/json/impl/Bindings.java --- a/json/src/main/java/org/netbeans/html/json/impl/Bindings.java Mon Feb 15 05:27:28 2016 +0100 +++ b/json/src/main/java/org/netbeans/html/json/impl/Bindings.java Mon Feb 29 05:33:31 2016 +0100 @@ -64,8 +64,8 @@ this.bp = bp; } - public PropertyBinding registerProperty(String propName, int index, M model, Proto.Type access, boolean readOnly) { - return PropertyBindingAccessor.create(access, this, propName, index, model, readOnly); + public PropertyBinding registerProperty(String propName, int index, M model, Proto.Type access, byte propertyType) { + return PropertyBindingAccessor.create(access, this, propName, index, model, propertyType); } public static Bindings apply(BrwsrCtx c) { diff -r ed4b25eb66f3 -r 3554078c32ce 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 29 05:33:31 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 3554078c32ce 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 29 05:33:31 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) { @@ -355,7 +359,7 @@ { for (int i = 0; i < propsGetSet.size(); i++) { w.append(" registerProperty(\"").append(propsGetSet.get(i).name).append("\", "); - w.append((i) + ", " + propsGetSet.get(i).readOnly + ");\n"); + w.append((i) + ", " + propsGetSet.get(i).readOnly + ", " + propsGetSet.get(i).constant + ");\n"); } } { @@ -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]) { @@ -721,7 +728,8 @@ gs[0], gs[1], tn, - gs[3] == null && !p.array() + gs[3] == null && !p.array(), + !p.mutable() )); } return ok; @@ -857,7 +865,8 @@ gs[0], null, tn, - true + true, + false )); } else { w.write(" public void " + gs[4] + "(" + write.getParameters().get(1).asType()); @@ -870,6 +879,7 @@ gs[0], gs[4], tn, + false, false )); } @@ -2001,6 +2011,10 @@ return p.array(); } + boolean mutable() { + return p.mutable(); + } + String typeName(ProcessingEnvironment env) { RuntimeException ex; try { @@ -2058,12 +2072,15 @@ final String setter; final String type; final boolean readOnly; - GetSet(String name, String getter, String setter, String type, boolean readOnly) { + final boolean constant; + + GetSet(String name, String getter, String setter, String type, boolean readOnly, boolean constant) { this.name = name; this.getter = getter; this.setter = setter; this.type = type; this.readOnly = readOnly; + this.constant = constant; } } diff -r ed4b25eb66f3 -r 3554078c32ce json/src/main/java/org/netbeans/html/json/impl/PropertyBindingAccessor.java --- a/json/src/main/java/org/netbeans/html/json/impl/PropertyBindingAccessor.java Mon Feb 15 05:27:28 2016 +0100 +++ b/json/src/main/java/org/netbeans/html/json/impl/PropertyBindingAccessor.java Mon Feb 29 05:33:31 2016 +0100 @@ -64,8 +64,7 @@ } protected abstract PropertyBinding newBinding( - Proto.Type access, Bindings bindings, String name, int index, M model, boolean readOnly - ); + Proto.Type access, Bindings bindings, String name, int index, M model, byte propertyType); protected abstract JSONCall newCall( BrwsrCtx ctx, RcvrJSON callback, String headers, String urlBefore, String urlAfter, @@ -87,9 +86,9 @@ } static PropertyBinding create( - Proto.Type access, Bindings bindings, String name, int index, M model , boolean readOnly + Proto.Type access, Bindings bindings, String name, int index, M model , byte propertyType ) { - return DEFAULT.newBinding(access, bindings, name, index, model, readOnly); + return DEFAULT.newBinding(access, bindings, name, index, model, propertyType); } public static JSONCall createCall( BrwsrCtx ctx, RcvrJSON callback, diff -r ed4b25eb66f3 -r 3554078c32ce json/src/main/java/org/netbeans/html/json/spi/PropertyBinding.java --- a/json/src/main/java/org/netbeans/html/json/spi/PropertyBinding.java Mon Feb 15 05:27:28 2016 +0100 +++ b/json/src/main/java/org/netbeans/html/json/spi/PropertyBinding.java Mon Feb 29 05:33:31 2016 +0100 @@ -45,6 +45,7 @@ import java.lang.ref.Reference; import java.lang.ref.WeakReference; import net.java.html.BrwsrCtx; +import net.java.html.json.ComputedProperty; import org.netbeans.html.json.impl.Bindings; import org.netbeans.html.json.impl.JSON; import org.netbeans.html.json.impl.PropertyBindingAccessor; @@ -93,10 +94,8 @@ @Override protected PropertyBinding newBinding( - Proto.Type access, Bindings bindings, String name, - int index, M model, boolean readOnly - ) { - return new Impl(model, bindings, name, index, access, readOnly); + Proto.Type access, Bindings bindings, String name, int index, M model, byte propertyType) { + return new Impl(model, bindings, name, index, access, propertyType); } }; } @@ -121,12 +120,22 @@ */ public abstract Object getValue(); - /** Is this property read only? Or can one call {@link #setValue(java.lang.Object)}? + /** Is this property read only?. Or can one call {@link #setValue(java.lang.Object)}? + * The property can still change, but only as a result of other + * properties being changed, just like {@link ComputedProperty} can. * * @return true, if this property is read only */ public abstract boolean isReadOnly(); + /** Is this property constant?. If a property is constant, than its + * value cannot changed after it is read. + * + * @return true, if this property is constant + * @since 1.3 + */ + public abstract boolean isConstant(); + /** Returns identical version of the binding, but one that holds on the * original model object via weak reference. * @@ -137,17 +146,17 @@ private static abstract class AImpl extends PropertyBinding { public final String name; - public final boolean readOnly; + public final byte propertyType; final Proto.Type access; final Bindings bindings; final int index; - public AImpl(Bindings bindings, String name, int index, Proto.Type access, boolean readOnly) { + public AImpl(Bindings bindings, String name, int index, Proto.Type access, byte propertyType) { this.bindings = bindings; this.name = name; this.index = index; this.access = access; - this.readOnly = readOnly; + this.propertyType = propertyType; } protected abstract M model(); @@ -174,7 +183,12 @@ @Override public boolean isReadOnly() { - return readOnly; + return (propertyType & 1) != 0; + } + + @Override + public boolean isConstant() { + return (propertyType & 2) != 0; } @Override @@ -186,8 +200,8 @@ private static final class Impl extends AImpl { private final M model; - public Impl(M model, Bindings bindings, String name, int index, Proto.Type access, boolean readOnly) { - super(bindings, name, index, access, readOnly); + public Impl(M model, Bindings bindings, String name, int index, Proto.Type access, byte propertyType) { + super(bindings, name, index, access, propertyType); this.model = model; } @@ -198,14 +212,14 @@ @Override public PropertyBinding weak() { - return new Weak(model, bindings, name, index, access, readOnly); + return new Weak(model, bindings, name, index, access, propertyType); } } private static final class Weak extends AImpl { private final Reference ref; - public Weak(M model, Bindings bindings, String name, int index, Proto.Type access, boolean readOnly) { - super(bindings, name, index, access, readOnly); + public Weak(M model, Bindings bindings, String name, int index, Proto.Type access, byte propertyType) { + super(bindings, name, index, access, propertyType); this.ref = new WeakReference(model); } diff -r ed4b25eb66f3 -r 3554078c32ce 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 29 05:33:31 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 @@ -508,7 +512,7 @@ PropertyBinding[] pb = new PropertyBinding[type.propertyNames.length]; for (int i = 0; i < pb.length; i++) { pb[i] = b.registerProperty( - type.propertyNames[i], i, obj, type, type.propertyReadOnly[i] + type.propertyNames[i], i, obj, type, type.propertyType[i] ); } FunctionBinding[] fb = new FunctionBinding[type.functions.length]; @@ -547,7 +551,7 @@ public static abstract class Type { private final Class clazz; private final String[] propertyNames; - private final boolean[] propertyReadOnly; + private final byte[] propertyType; private final String[] functions; /** Constructor for subclasses generated by the annotation processor @@ -569,7 +573,7 @@ } this.clazz = clazz; this.propertyNames = new String[properties]; - this.propertyReadOnly = new boolean[properties]; + this.propertyType = new byte[properties]; this.functions = new String[functions]; JSON.register(clazz, this); } @@ -584,7 +588,29 @@ protected final void registerProperty(String name, int index, boolean readOnly) { assert propertyNames[index] == null; propertyNames[index] = name; - propertyReadOnly[index] = readOnly; + propertyType[index] = (byte) (readOnly ? 1 : 0); + } + + /** Registers property for the type. It is expected each index + * is initialized only once. The difference between readOnly + * and constant is: The constant value is + * assigned only at the beginning and never changed then - like the + * {@link Property#mutable() non-mutable} property. On the other + * hand, a readOnly property can change its value, + * but not via a setter - just like {@link ComputedProperty}. + * + * @param name name of the property + * @param index index of the property + * @param readOnly is the property read only? + * @param constant is the property assigned once and never changed again? + * @since 1.3 + */ + protected final void registerProperty( + String name, int index, boolean readOnly, boolean constant + ) { + assert propertyNames[index] == null; + propertyNames[index] = name; + propertyType[index] = (byte) ((readOnly ? 1 : 0) | (constant ? 2 : 0)); } /** Registers function of given name at given index. diff -r ed4b25eb66f3 -r 3554078c32ce 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 29 05:33:31 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"); + } + +} diff -r ed4b25eb66f3 -r 3554078c32ce ko4j/src/main/java/org/netbeans/html/ko4j/KOTech.java --- a/ko4j/src/main/java/org/netbeans/html/ko4j/KOTech.java Mon Feb 15 05:27:28 2016 +0100 +++ b/ko4j/src/main/java/org/netbeans/html/ko4j/KOTech.java Mon Feb 29 05:33:31 2016 +0100 @@ -72,10 +72,12 @@ final Object createKO(Object model, Object copyFrom, PropertyBinding[] propArr, FunctionBinding[] funcArr, Knockout[] ko) { String[] propNames = new String[propArr.length]; Boolean[] propReadOnly = new Boolean[propArr.length]; + Boolean[] propConstant = new Boolean[propArr.length]; Object[] propValues = new Object[propArr.length]; for (int i = 0; i < propNames.length; i++) { propNames[i] = propArr[i].getPropertyName(); propReadOnly[i] = propArr[i].isReadOnly(); + propConstant[i] = propArr[i].isConstant(); Object value = propArr[i].getValue(); if (value instanceof Enum) { value = value.toString(); @@ -93,7 +95,7 @@ } newKO.wrapModel( ret, copyFrom, - propNames, propReadOnly, propValues, + propNames, propReadOnly, propConstant, propValues, funcNames ); return ret; diff -r ed4b25eb66f3 -r 3554078c32ce ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java --- a/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java Mon Feb 15 05:27:28 2016 +0100 +++ b/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java Mon Feb 29 05:33:31 2016 +0100 @@ -168,7 +168,7 @@ javacall = true, keepAlive = false, wait4js = false, - args = { "ret", "copyFrom", "propNames", "propReadOnly", "propValues", "funcNames" }, + args = { "ret", "copyFrom", "propNames", "propReadOnly", "propConstant", "propValues", "funcNames" }, body = "Object.defineProperty(ret, 'ko4j', { value : this });\n" + "function koComputed(index, name, readOnly, value) {\n" @@ -225,7 +225,11 @@ + " ret[name] = cmpt;\n" + "}\n" + "for (var i = 0; i < propNames.length; i++) {\n" - + " koComputed(i, propNames[i], propReadOnly[i], propValues[i]);\n" + + " if (propConstant[i]) {\n" + + " ret[propNames[i]] = propValues[i];\n" + + " } else {\n" + + " koComputed(i, propNames[i], propReadOnly[i], propValues[i]);\n" + + " }\n" + "}\n" + "function koExpose(index, name) {\n" + " ret[name] = function(data, ev) {\n" @@ -240,7 +244,8 @@ ) native void wrapModel( Object ret, Object copyFrom, - String[] propNames, Boolean[] propReadOnly, Object propValues, + String[] propNames, Boolean[] propReadOnly, Boolean[] propConstant, + Object propValues, String[] funcNames ); diff -r ed4b25eb66f3 -r 3554078c32ce src/main/javadoc/overview.html --- a/src/main/javadoc/overview.html Mon Feb 15 05:27:28 2016 +0100 +++ b/src/main/javadoc/overview.html Mon Feb 29 05:33:31 2016 +0100 @@ -101,7 +101,11 @@ {@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. - The JavaFX presenter can be executed in headless mode - + {@link net.java.html.json.Property#mutable} can be false + to define a non-mutable (almost constant) property. That + in case of Knockout bindings means: the property is + represented by a plain value rather than an observable in the JavaScript + object. The JavaFX presenter can be executed in headless mode - just specify -Dfxpresenter.headless=true when launching its virtual machine and no window will be shown. This is particularly useful for testing. Configure your surefire or failsafe