Preventing re-entrant access to the same model object, but otherwise one is free to however anywhere.
1.1 --- a/json/src/main/java/org/apidesign/html/json/spi/Proto.java Fri Aug 01 14:16:19 2014 +0200
1.2 +++ b/json/src/main/java/org/apidesign/html/json/spi/Proto.java Fri Aug 01 15:24:57 2014 +0200
1.3 @@ -71,7 +71,6 @@
1.4 private org.netbeans.html.json.impl.Bindings ko;
1.5 private Watcher mine;
1.6 private Watcher.Ref observers;
1.7 - private static Watcher locked;
1.8
1.9 Proto(Object obj, Type type, BrwsrCtx context) {
1.10 this.obj = obj;
1.11 @@ -93,36 +92,28 @@
1.12 * @throws IllegalStateException if already locked
1.13 */
1.14 public void acquireLock() throws IllegalStateException {
1.15 - if (locked != null) throw new IllegalStateException();
1.16 - locked = Watcher.DUMMY;
1.17 + acquireLock(null);
1.18 }
1.19 public void acquireLock(String propName) throws IllegalStateException {
1.20 - if (locked != null) throw new IllegalStateException();
1.21 - locked = Watcher.computing(this, propName);
1.22 + Watcher.beginComputing(this, propName);
1.23 }
1.24
1.25 public void accessValue(String propName) {
1.26 - if (locked != null) {
1.27 - if (locked.forbiddenValue(this)) {
1.28 - throw new IllegalStateException();
1.29 - }
1.30 - observers = locked.observe(observers, propName);
1.31 - }
1.32 + observers = Watcher.accessingValue(this, observers, propName);
1.33 }
1.34
1.35 /** Verifies the model is not locked otherwise throws an exception.
1.36 * @throws IllegalStateException if the model is locked
1.37 */
1.38 public void verifyUnlocked() throws IllegalStateException {
1.39 - if (locked != null) throw new IllegalStateException();
1.40 + Watcher.verifyUnlocked(this);
1.41 }
1.42
1.43 /** When modifications are over, the model is switched into
1.44 * unlocked state by calling this method.
1.45 */
1.46 public void releaseLock() {
1.47 - mine = Watcher.register(mine, locked);
1.48 - locked = null;
1.49 + mine = Watcher.finishComputing(this, mine);
1.50 }
1.51
1.52 /** Whenever model changes a property. It should notify the
1.53 @@ -462,7 +453,7 @@
1.54 }
1.55
1.56 final Watcher watcher(String prop) {
1.57 - return Watcher.find(mine, prop);
1.58 + return Watcher.find(mine, null, prop);
1.59 }
1.60
1.61 /** Functionality used by the code generated by annotation
2.1 --- a/json/src/main/java/org/apidesign/html/json/spi/Watcher.java Fri Aug 01 14:16:19 2014 +0200
2.2 +++ b/json/src/main/java/org/apidesign/html/json/spi/Watcher.java Fri Aug 01 15:24:57 2014 +0200
2.3 @@ -49,7 +49,8 @@
2.4 * @author Jaroslav Tulach
2.5 */
2.6 final class Watcher {
2.7 - static final Watcher DUMMY = new Watcher(null, null);
2.8 + private static final Object LOCK = new Object();
2.9 + private static Watcher global;
2.10
2.11 private final Proto proto;
2.12 private final String prop;
2.13 @@ -60,17 +61,75 @@
2.14 this.prop = prop;
2.15 }
2.16
2.17 - static Watcher find(Watcher first, String prop) {
2.18 + static void beginComputing(Proto p, String name) {
2.19 + synchronized (LOCK) {
2.20 + Watcher alreadyThere = find(global, p, null);
2.21 + if (alreadyThere != null) {
2.22 + throw new IllegalStateException("Re-entrant attempt to access " + p);
2.23 + }
2.24 + final Watcher nw = new Watcher(p, name);
2.25 + nw.next = global;
2.26 + global = nw;
2.27 + }
2.28 + }
2.29 +
2.30 + static void verifyUnlocked(Proto p) {
2.31 + synchronized (LOCK) {
2.32 + Watcher alreadyThere = find(global, p, null);
2.33 + if (alreadyThere != null) {
2.34 + throw new IllegalStateException("Re-entrant attempt to access " + p);
2.35 + }
2.36 + }
2.37 + }
2.38 +
2.39 + static Ref accessingValue(Proto p, Ref observers, String propName) {
2.40 + synchronized (LOCK) {
2.41 + Watcher alreadyThere = find(global, p, null);
2.42 + if (alreadyThere != null) {
2.43 + throw new IllegalStateException("Re-entrant attempt to access " + p);
2.44 + }
2.45 + Watcher w = global;
2.46 + for (;;) {
2.47 + if (w == null) {
2.48 + return observers;
2.49 + }
2.50 + observers = w.observe(observers, propName);
2.51 + w = w.next;
2.52 + }
2.53 + }
2.54 + }
2.55 +
2.56 + static Watcher finishComputing(Proto p, Watcher mine) {
2.57 + synchronized (LOCK) {
2.58 + Watcher w = global;
2.59 + global = w.next;
2.60 + w.next = null;
2.61 + if (w.proto != p) {
2.62 + throw new IllegalStateException("Inconsistency: " + w.proto + " != " + p);
2.63 + }
2.64 + return register(mine, w);
2.65 + }
2.66 + }
2.67 +
2.68 + static Watcher find(Watcher first, Proto proto, String prop) {
2.69 + // assert Thread.holdsLock(LOCK);
2.70 for (;;) {
2.71 - if (prop.equals(first.prop)) {
2.72 + if (first == null) {
2.73 + return null;
2.74 + }
2.75 + if (prop != null && prop.equals(first.prop)) {
2.76 + return first;
2.77 + }
2.78 + if (proto != null && proto == first.proto) {
2.79 return first;
2.80 }
2.81 first = first.next;
2.82 }
2.83 }
2.84
2.85 - static Watcher register(Watcher mine, Watcher locked) {
2.86 - if (locked == DUMMY) {
2.87 + private static Watcher register(Watcher mine, Watcher locked) {
2.88 + assert Thread.holdsLock(LOCK);
2.89 + if (locked.prop == null) {
2.90 return mine;
2.91 }
2.92 Watcher current = mine;
2.93 @@ -99,16 +158,12 @@
2.94 }
2.95
2.96 Ref observe(Ref prev, String prop) {
2.97 - if (this == DUMMY) {
2.98 + if (prop == null) {
2.99 return prev;
2.100 }
2.101 return new Ref(this, prop).chain(prev);
2.102 }
2.103
2.104 - final boolean forbiddenValue(Proto aThis) {
2.105 - return this == DUMMY || proto == aThis;
2.106 - }
2.107 -
2.108 static final class Ref extends WeakReference<Watcher> {
2.109 private final String prop;
2.110 private Ref next;
3.1 --- a/json/src/test/java/net/java/html/json/DeepChangeTest.java Fri Aug 01 14:16:19 2014 +0200
3.2 +++ b/json/src/test/java/net/java/html/json/DeepChangeTest.java Fri Aug 01 15:24:57 2014 +0200
3.3 @@ -113,6 +113,15 @@
3.4 })
3.5 static class Y {
3.6 }
3.7 + @Model(className = "MyOverall", properties = {
3.8 + @Property(name = "x", type = MyX.class)
3.9 + })
3.10 + static class Overall {
3.11 + @ComputedProperty(deep = true)
3.12 + static String valueAccross(MyX x) {
3.13 + return x.getFirstFromNames();
3.14 + }
3.15 + }
3.16
3.17 @Test public void isTransitiveChangeNotifiedProperly() throws Exception {
3.18 MyX p = Models.bind(
3.19 @@ -173,6 +182,27 @@
3.20 assertEquals(o.get(), "Nazdar");
3.21 assertEquals(o.changes, 1, "One change so far");
3.22 }
3.23 +
3.24 + @Test public void firstChangeInArrayNotifiedTransitively() throws Exception {
3.25 + MyOverall p = Models.bind(
3.26 + new MyOverall(new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999))
3.27 + ), c).applyBindings();
3.28 +
3.29 + Map m = (Map)Models.toRaw(p);
3.30 + Object v = m.get("valueAccross");
3.31 + assertNotNull(v, "Value should be in the map");
3.32 + assertEquals(v.getClass(), One.class, "It is instance of One");
3.33 + One o = (One)v;
3.34 + assertEquals(o.changes, 0, "No changes so far");
3.35 + assertTrue(o.pb.isReadOnly(), "Derived property");
3.36 + assertEquals(o.get(), "Hi");
3.37 +
3.38 + p.getX().getAll().get(0).setValue("Nazdar");
3.39 +
3.40 + assertEquals(o.get(), "Nazdar");
3.41 + assertEquals(o.changes, 1, "One change so far");
3.42 + }
3.43 +
3.44 @Test public void secondChangeInArrayIgnored() throws Exception {
3.45 MyX p = Models.bind(
3.46 new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999)
3.47 @@ -281,14 +311,7 @@
3.48 assertEquals(o2.changes, 0, "No changes so far");
3.49 assertTrue(o.pb.isReadOnly(), "Derived property");
3.50 assertEquals(o.get(), "Ahoj");
3.51 - try {
3.52 - assertEquals(o2.get(), "AHOJ");
3.53 - } catch (IllegalStateException ex) {
3.54 - // is it OK to forbid access to subproperties of
3.55 - // when the deep is not true?
3.56 - // that would be incompatible change...
3.57 - return;
3.58 - }
3.59 + assertEquals(o2.get(), "AHOJ");
3.60
3.61 p.getOne().setValue("Nazdar");
3.62