You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@netbeans.apache.org by jt...@apache.org on 2017/09/09 05:30:41 UTC

[3/3] incubator-netbeans-html4j git commit: #270481, #270553: Bugfix for multiple observers and better tests for GC behavior in the TCK

#270481, #270553: Bugfix for multiple observers and better tests for GC behavior in the TCK


Project: http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/commit/7ddcc5f1
Tree: http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/tree/7ddcc5f1
Diff: http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/diff/7ddcc5f1

Branch: refs/heads/master
Commit: 7ddcc5f1ab52f0f674369743202b43e3e1fdb6b4
Parents: e58ccc7
Author: Jaroslav Tulach <ja...@oracle.com>
Authored: Sat Sep 9 07:28:34 2017 +0200
Committer: Jaroslav Tulach <ja...@oracle.com>
Committed: Sat Sep 9 07:28:34 2017 +0200

----------------------------------------------------------------------
 .../html/boot/fx/AbstractFXPresenter.java       |   2 +-
 .../java/net/java/html/js/tests/GCBodyTest.java |  20 ++
 .../main/java/net/java/html/js/tests/Sum.java   |  18 ++
 .../java/html/json/tests/GCKnockoutTest.java    |  15 +-
 .../org/netbeans/html/json/impl/JSONList.java   |   2 +-
 .../org/netbeans/html/json/spi/Observers.java   |  44 +++-
 .../java/org/netbeans/html/json/spi/Proto.java  |   3 +-
 .../netbeans/html/json/impl/DeepChangeTest.java |  77 +++++-
 .../html/json/impl/DependsChangeTest.java       | 238 +++++++++++++++++++
 .../java/org/netbeans/html/ko4j/KOTech.java     |   3 +-
 .../java/org/netbeans/html/ko4j/Knockout.java   |  51 ++--
 src/main/javadoc/overview.html                  |   7 +
 12 files changed, 435 insertions(+), 45 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/boot-fx/src/main/java/org/netbeans/html/boot/fx/AbstractFXPresenter.java
----------------------------------------------------------------------
diff --git a/boot-fx/src/main/java/org/netbeans/html/boot/fx/AbstractFXPresenter.java b/boot-fx/src/main/java/org/netbeans/html/boot/fx/AbstractFXPresenter.java
index ed40fe5..08590c1 100644
--- a/boot-fx/src/main/java/org/netbeans/html/boot/fx/AbstractFXPresenter.java
+++ b/boot-fx/src/main/java/org/netbeans/html/boot/fx/AbstractFXPresenter.java
@@ -406,7 +406,7 @@ Fn.KeepAlive, Fn.ToJavaScript, Fn.FromJavaScript, Executor, Cloneable {
                     LOG.log(Level.FINER, "  params: {0}", Arrays.asList(args));
                 }
                 List<Object> all = new ArrayList<Object>(args.length + 1);
-                all.add(thiz == null ? presenter.undefined() : thiz);
+                all.add(thiz == null ? presenter.undefined() : presenter.toJavaScript(thiz, true));
                 for (int i = 0; i < args.length; i++) {
                     Object conv = args[i];
                     if (arrayChecks) {

http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/json-tck/src/main/java/net/java/html/js/tests/GCBodyTest.java
----------------------------------------------------------------------
diff --git a/json-tck/src/main/java/net/java/html/js/tests/GCBodyTest.java b/json-tck/src/main/java/net/java/html/js/tests/GCBodyTest.java
index 1d28cf6..c092a4a 100644
--- a/json-tck/src/main/java/net/java/html/js/tests/GCBodyTest.java
+++ b/json-tck/src/main/java/net/java/html/js/tests/GCBodyTest.java
@@ -99,6 +99,26 @@ public class GCBodyTest {
         assertEquals(r.value, null, "Setter called with null value");
     }
 
+    @KOTest public void thisIsHeldStrongly() throws Exception {
+        Sum s = new Sum();
+        Object res = s.jsSum(12, 30);
+        int intRes = Bodies.readIntX(res);
+        assertEquals(42, intRes);
+        WeakReference<Sum> ref = new WeakReference<Sum>(s);
+        s = null;
+        assertNotGC(ref, true, "s cannot disappear: we have reference to s via res.y field");
+    }
+
+    @KOTest public void argsArentHeldStrongly() throws Exception {
+        Sum s = new Sum();
+        Object res = Sum.jsStaticSum(s, 12, 30);
+        int intRes = Bodies.readIntX(res);
+        assertEquals(42, intRes);
+        WeakReference<Sum> ref = new WeakReference<Sum>(s);
+        s = null;
+        assertGC(ref, "Reference to s via res.y field is weak");
+    }
+
     private static Reference<?> sendRunnable(final int[] arr) {
         Runnable r = new Runnable() {
             @Override

http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/json-tck/src/main/java/net/java/html/js/tests/Sum.java
----------------------------------------------------------------------
diff --git a/json-tck/src/main/java/net/java/html/js/tests/Sum.java b/json-tck/src/main/java/net/java/html/js/tests/Sum.java
index bf03645..215df29 100644
--- a/json-tck/src/main/java/net/java/html/js/tests/Sum.java
+++ b/json-tck/src/main/java/net/java/html/js/tests/Sum.java
@@ -18,6 +18,8 @@
  */
 package net.java.html.js.tests;
 
+import net.java.html.js.JavaScriptBody;
+
 /**
  *
  * @author Jaroslav Tulach
@@ -26,7 +28,23 @@ public final class Sum {
     public int sum(int a, int b) {
         return a + b;
     }
+
+    @JavaScriptBody(args = { "a", "b" }, javacall = true, keepAlive = false, body =
+        "return {\n"
+      + "  'x' : this.@net.java.html.js.tests.Sum::sum(II)(a, b),\n"
+      + "  'y' : this\n"
+      + "}\n"
+    )
+    public native Object jsSum(int a, int b);
     
+    @JavaScriptBody(args = { "thiz", "a", "b" }, javacall = true, keepAlive = false, body =
+        "return {\n"
+      + "  'x' : thiz.@net.java.html.js.tests.Sum::sum(II)(a, b),\n"
+      + "  'y' : thiz\n"
+      + "}\n"
+    )
+    public static native Object jsStaticSum(Sum thiz, int a, int b);
+
     public int sum(Object[] arr) {
         int s = 0;
         for (int i = 0; i < arr.length; i++) {

http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/json-tck/src/main/java/net/java/html/json/tests/GCKnockoutTest.java
----------------------------------------------------------------------
diff --git a/json-tck/src/main/java/net/java/html/json/tests/GCKnockoutTest.java b/json-tck/src/main/java/net/java/html/json/tests/GCKnockoutTest.java
index 967a033..946f2bc 100644
--- a/json-tck/src/main/java/net/java/html/json/tests/GCKnockoutTest.java
+++ b/json-tck/src/main/java/net/java/html/json/tests/GCKnockoutTest.java
@@ -37,7 +37,7 @@ public class GCKnockoutTest {
     })
     static class FullnameCntrl {
     }
-    
+
     @KOTest public void noLongerNeededArrayElementsCanDisappear() throws Exception {
         BrwsrCtx ctx = Utils.newContext(GCKnockoutTest.class);
         Object exp = Utils.exposeHTML(GCKnockoutTest.class,
@@ -58,8 +58,7 @@ public class GCKnockoutTest {
             cnt = Utils.countChildren(GCKnockoutTest.class, "ul");
             assertEquals(cnt, 2, "Now two " + cnt);
 
-            Fullname removed = m.getAll().get(0);
-            m.getAll().remove(0);
+            Fullname removed = m.getAll().remove(0);
 
             cnt = Utils.countChildren(GCKnockoutTest.class, "ul");
             assertEquals(cnt, 1, "Again One " + cnt);
@@ -67,16 +66,16 @@ public class GCKnockoutTest {
             Reference<?> ref = new WeakReference<Object>(removed);
             removed = null;
             assertGC(ref, "Can removed object disappear?");
-            
+
             ref = new WeakReference<Object>(m);
             m = null;
             assertNotGC(ref, "Root model cannot GC");
         } finally {
             Utils.exposeHTML(GCKnockoutTest.class, "");
         }
-        
+
     }
-    
+
     private void assertGC(Reference<?> ref, String msg) throws Exception {
         for (int i = 0; i < 100; i++) {
             if (ref.get() == null) {
@@ -94,7 +93,7 @@ public class GCKnockoutTest {
         }
         throw new OutOfMemoryError(msg);
     }
-    
+
     private void assertNotGC(Reference<?> ref, String msg) throws Exception {
         for (int i = 0; i < 10; i++) {
             if (ref.get() == null) {
@@ -111,5 +110,5 @@ public class GCKnockoutTest {
             System.runFinalization();
         }
     }
-    
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/json/src/main/java/org/netbeans/html/json/impl/JSONList.java
----------------------------------------------------------------------
diff --git a/json/src/main/java/org/netbeans/html/json/impl/JSONList.java b/json/src/main/java/org/netbeans/html/json/impl/JSONList.java
index bafe1cd..6988c81 100644
--- a/json/src/main/java/org/netbeans/html/json/impl/JSONList.java
+++ b/json/src/main/java/org/netbeans/html/json/impl/JSONList.java
@@ -197,9 +197,9 @@ public final class JSONList<T> extends ArrayList<T> {
         proto.getContext().execute(new Runnable() {
             @Override
             public void run() {
+                proto.valueHasMutated(name);
                 Bindings m = PropertyBindingAccessor.getBindings(proto, false, null);
                 if (m != null) {
-                    m.valueHasMutated(name, null, JSONList.this);
                     for (String dependant : deps) {
                         m.valueHasMutated(dependant, null, null);
                     }

http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/json/src/main/java/org/netbeans/html/json/spi/Observers.java
----------------------------------------------------------------------
diff --git a/json/src/main/java/org/netbeans/html/json/spi/Observers.java b/json/src/main/java/org/netbeans/html/json/spi/Observers.java
index 163c793..59c225e 100644
--- a/json/src/main/java/org/netbeans/html/json/spi/Observers.java
+++ b/json/src/main/java/org/netbeans/html/json/spi/Observers.java
@@ -18,11 +18,12 @@
  */
 package org.netbeans.html.json.spi;
 
-import java.lang.ref.WeakReference;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 
 /**
  *
@@ -37,11 +38,12 @@ final class Observers {
         assert Thread.holdsLock(GLOBAL);
     }
     
-    static void beginComputing(Proto p, String name) {
+    static Usages beginComputing(Proto p, String name, Usages usages) {
         synchronized (GLOBAL) {
             verifyUnlocked(p);
             final Watcher nw = new Watcher(p, name);
             GLOBAL.push(nw);
+            return Usages.register(name, nw, usages);
         }
     }
     
@@ -88,11 +90,12 @@ final class Observers {
         }
     }
     
-    private static final class Ref extends WeakReference<Watcher> {
+    private static final class Ref {
+        private final Watcher ref;
         private final String prop;
         
         public Ref(Watcher ref, String prop) {
-            super(ref);
+            this.ref = ref;
             this.prop = prop;
         }
         
@@ -110,6 +113,10 @@ final class Observers {
             }
             return null;
         }
+
+        Watcher get() {
+            return ref.proto == null ? null : ref;
+        }
     }
     
     private Watcher find(String prop) {
@@ -183,7 +190,7 @@ final class Observers {
                 it.remove();
                 continue;
             }
-            if (rw == w && r.prop.equals(r.prop)) {
+            if (rw == w && ref.prop.equals(r.prop)) {
                 return;
             }
         }
@@ -191,8 +198,8 @@ final class Observers {
     }
     
     private static final class Watcher {
+        Proto proto;
         final Thread owner;
-        final Proto proto;
         final String prop;
 
         Watcher(Proto proto, String prop) {
@@ -205,5 +212,30 @@ final class Observers {
         public String toString() {
             return "Watcher: " + proto + ", " + prop;
         }
+
+        void destroy() {
+            proto = null;
+        }
+    }
+
+    static final class Usages {
+        private final Map<String,Watcher> watchers = new HashMap<String, Watcher>();
+
+        private Usages() {
+        }
+
+        static Usages register(String propName, Watcher w, Usages usages) {
+            if (propName != null) {
+                if (usages == null) {
+                    usages = new Usages();
+                }
+                Observers.Watcher prev = usages.watchers.put(propName, w);
+                if (prev != null) {
+                    prev.destroy();
+                }
+            }
+            return usages;
+        }
+
     }
 }

http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/json/src/main/java/org/netbeans/html/json/spi/Proto.java
----------------------------------------------------------------------
diff --git a/json/src/main/java/org/netbeans/html/json/spi/Proto.java b/json/src/main/java/org/netbeans/html/json/spi/Proto.java
index db51a8b..626a6cb 100644
--- a/json/src/main/java/org/netbeans/html/json/spi/Proto.java
+++ b/json/src/main/java/org/netbeans/html/json/spi/Proto.java
@@ -50,6 +50,7 @@ public final class Proto {
     private final net.java.html.BrwsrCtx context;
     private org.netbeans.html.json.impl.Bindings ko;
     private Observers observers;
+    private Observers.Usages usages;
 
     Proto(Object obj, Type type, BrwsrCtx context) {
         this.obj = obj;
@@ -88,7 +89,7 @@ public final class Proto {
      * @since 0.9
      */
     public void acquireLock(String propName) throws IllegalStateException {
-        Observers.beginComputing(this, propName);
+        usages = Observers.beginComputing(this, propName, usages);
     }
 
     /** A property on this proto object is about to be accessed. Verifies

http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/json/src/test/java/org/netbeans/html/json/impl/DeepChangeTest.java
----------------------------------------------------------------------
diff --git a/json/src/test/java/org/netbeans/html/json/impl/DeepChangeTest.java b/json/src/test/java/org/netbeans/html/json/impl/DeepChangeTest.java
index d3774ab..4e3427a 100644
--- a/json/src/test/java/org/netbeans/html/json/impl/DeepChangeTest.java
+++ b/json/src/test/java/org/netbeans/html/json/impl/DeepChangeTest.java
@@ -20,6 +20,8 @@ package org.netbeans.html.json.impl;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
 import java.lang.reflect.InvocationTargetException;
 import java.util.HashMap;
 import java.util.List;
@@ -278,10 +280,62 @@ public class DeepChangeTest {
         assertTrue(o.pb.isReadOnly(), "Derived property");
         assertEquals(o.get(), "Hi");
 
-        p.getX().getAll().get(0).setValue("Nazdar");
+        final List<MyY> all = p.getX().getAll();
+        MyY refStrong = all.get(0);
+        Reference<MyY> ref = new WeakReference<MyY>(refStrong);
+        refStrong.setValue("Nazdar");
         
         assertEquals(o.get(), "Nazdar");
         assertEquals(o.changes, 1, "One change so far");
+
+        final MyY hi = Models.bind(new MyY("Ciao", 33), c);
+        all.set(0, hi);
+
+        assertEquals(o.changes, 2, "Second change");
+        assertEquals(o.get(), "Ciao");
+
+        refStrong.setValue("Ignore");
+        assertEquals(o.changes, 2, "Still two changes");
+
+        refStrong = null;
+        assertGC(ref, "Original MyY can now disappear");
+    }
+
+    @Test
+    public void disappearModel() throws Exception {
+        MyOverall p = Models.bind(
+            new MyOverall(new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999))
+        ), c);
+
+        MyY refStrong = disappearModelOperations(p);
+
+        Reference<MyOverall> ref = new WeakReference<MyOverall>(p);
+        p = null;
+        assertGC(ref, "MyOverall can now disappear");
+        assertNotNull(refStrong, "Submodel still used");
+    }
+
+    private MyY disappearModelOperations(MyOverall p) throws InvocationTargetException, IllegalAccessException, IllegalArgumentException {
+        Models.applyBindings(p);
+        Map m = (Map)Models.toRaw(p);
+        Object v = m.get("valueAccross");
+        assertNotNull(v, "Value should be in the map");
+        assertEquals(v.getClass(), One.class, "It is instance of One");
+        One o = (One)v;
+        assertEquals(o.changes, 0, "No changes so far");
+        assertTrue(o.pb.isReadOnly(), "Derived property");
+        assertEquals(o.get(), "Hi");
+        final List<MyY> all = p.getX().getAll();
+        MyY refStrong = all.get(0);
+        refStrong.setValue("Nazdar");
+        assertEquals(o.get(), "Nazdar");
+        assertEquals(o.changes, 1, "One change so far");
+        all.clear();
+        assertEquals(o.changes, 2, "Second change");
+        assertNull(o.get(), "MyY array is empty now");
+        refStrong.setValue("Ignore");
+        assertEquals(o.changes, 2, "Still two changes");
+        return refStrong;
     }
     
     @Test public void secondChangeInArrayIgnored() throws Exception {
@@ -589,5 +643,24 @@ public class DeepChangeTest {
             throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
         }
     }
-    
+
+    private static void assertGC(Reference<?> ref, String msg) throws InterruptedException {
+        for (int i = 0; i < 100; i++) {
+            if (isGone(ref)) {
+                return;
+            }
+            try {
+                System.gc();
+                System.runFinalization();
+            } catch (Error err) {
+                err.printStackTrace();
+            }
+        }
+        throw new InterruptedException(msg);
+    }
+
+    private static boolean isGone(Reference<?> ref) {
+        return ref.get() == null;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/json/src/test/java/org/netbeans/html/json/impl/DependsChangeTest.java
----------------------------------------------------------------------
diff --git a/json/src/test/java/org/netbeans/html/json/impl/DependsChangeTest.java b/json/src/test/java/org/netbeans/html/json/impl/DependsChangeTest.java
new file mode 100644
index 0000000..3102e08
--- /dev/null
+++ b/json/src/test/java/org/netbeans/html/json/impl/DependsChangeTest.java
@@ -0,0 +1,238 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.html.json.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.Map;
+import net.java.html.BrwsrCtx;
+import net.java.html.json.ComputedProperty;
+import net.java.html.json.Model;
+import net.java.html.json.Models;
+import net.java.html.json.Property;
+import org.netbeans.html.context.spi.Contexts;
+import org.netbeans.html.json.spi.FunctionBinding;
+import org.netbeans.html.json.spi.JSONCall;
+import org.netbeans.html.json.spi.PropertyBinding;
+import org.netbeans.html.json.spi.Technology;
+import org.netbeans.html.json.spi.Transfer;
+import static org.testng.Assert.*;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+/**
+ *
+ * @author Jaroslav Tulach
+ */
+public class DependsChangeTest {
+    private MapTechnology t;
+    private BrwsrCtx c;
+
+    @BeforeMethod public void initTechnology() {
+        t = new MapTechnology();
+        c = Contexts.newBuilder().register(Technology.class, t, 1).
+            register(Transfer.class, t, 1).build();
+    }
+
+    @Model(className = "Depends", instance = true, properties = {
+        @Property(name = "value", type = int.class),
+        @Property(name = "next", type = Depends.class),
+    })
+    static class DependsCntrl {
+        @ComputedProperty @Transitive(deep = true)
+        static int sumPositive(Depends next, int value) {
+            while (next != null && next.getValue() > 0) {
+                value += next.getValue();
+                next = next.getNext();
+            }
+            return value;
+        }
+    }
+
+    @Test
+    public void disappearModel() throws Exception {
+        Depends p = Models.bind(
+            new Depends(10, new Depends(20, new Depends(30, null))
+        ), c);
+
+        Depends refStrong = disappearModelOperations(p);
+
+        Reference<Object> ref = new WeakReference<Object>(p);
+        p = null;
+        assertGC(ref, "MyOverall can now disappear");
+        assertNotNull(refStrong, "Submodel still used");
+    }
+
+    private Depends disappearModelOperations(Depends p) throws InvocationTargetException, IllegalAccessException, IllegalArgumentException {
+        Models.applyBindings(p);
+        Map m = (Map)Models.toRaw(p);
+        Object v = m.get("sumPositive");
+        assertNotNull(v, "Value should be in the map");
+        assertEquals(v.getClass(), One.class, "It is instance of One");
+        One o = (One)v;
+        assertEquals(o.changes, 0, "No changes so far");
+        assertTrue(o.pb.isReadOnly(), "Derived property");
+        assertEquals(o.get(), 60);
+        Depends refStrong = p.getNext().getNext();
+        p.getNext().setNext(null);
+        assertEquals(o.changes, 1, "Change in sum");
+        assertEquals(o.get(), 30);
+        return refStrong;
+    }
+
+    static final class One {
+
+        int changes;
+        final PropertyBinding pb;
+        final FunctionBinding fb;
+
+        One(Object m, PropertyBinding pb) throws NoSuchMethodException {
+            this.pb = pb;
+            this.fb = null;
+        }
+
+        One(Object m, FunctionBinding fb) throws NoSuchMethodException {
+            this.pb = null;
+            this.fb = fb;
+        }
+
+        Object get() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+            return pb.getValue();
+        }
+
+        void set(Object v) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
+            pb.setValue(v);
+        }
+
+        void assertNoChange(String msg) {
+            assertEquals(changes, 0, msg);
+        }
+
+        void assertChange(String msg) {
+            if (changes == 0) {
+                fail(msg);
+            }
+            changes = 0;
+        }
+    }
+
+    static final class MapTechnology
+            implements Technology<Map<String, One>>, Transfer {
+
+        @Override
+        public Map<String, One> wrapModel(Object model) {
+            return new HashMap<String, One>();
+        }
+
+        @Override
+        public void valueHasMutated(Map<String, One> data, String propertyName) {
+            One p = data.get(propertyName);
+            if (p != null) {
+                p.changes++;
+            }
+        }
+
+        @Override
+        public void bind(PropertyBinding b, Object model, Map<String, One> data) {
+            try {
+                One o = new One(model, b);
+                data.put(b.getPropertyName(), o);
+            } catch (NoSuchMethodException ex) {
+                throw new IllegalStateException(ex);
+            }
+        }
+
+        @Override
+        public void expose(FunctionBinding fb, Object model, Map<String, One> data) {
+            try {
+                data.put(fb.getFunctionName(), new One(model, fb));
+            } catch (NoSuchMethodException ex) {
+                throw new IllegalStateException(ex);
+            }
+        }
+
+        @Override
+        public void applyBindings(Map<String, One> data) {
+        }
+
+        @Override
+        public Object wrapArray(Object[] arr) {
+            return arr;
+        }
+
+        @Override
+        public void extract(Object obj, String[] props, Object[] values) {
+            Map<?, ?> map = obj instanceof Map ? (Map<?, ?>) obj : null;
+            for (int i = 0; i < Math.min(props.length, values.length); i++) {
+                if (map == null) {
+                    values[i] = null;
+                } else {
+                    values[i] = map.get(props[i]);
+                    if (values[i] instanceof One) {
+                        values[i] = ((One) values[i]).pb.getValue();
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void loadJSON(JSONCall call) {
+            call.notifyError(new UnsupportedOperationException());
+        }
+
+        @Override
+        public <M> M toModel(Class<M> modelClass, Object data) {
+            return modelClass.cast(data);
+        }
+
+        @Override
+        public Object toJSON(InputStream is) throws IOException {
+            throw new IOException();
+        }
+
+        @Override
+        public void runSafe(Runnable r) {
+            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+        }
+    }
+
+    private static void assertGC(Reference<?> ref, String msg) throws InterruptedException {
+        for (int i = 0; i < 100; i++) {
+            if (isGone(ref)) {
+                return;
+            }
+            try {
+                System.gc();
+                System.runFinalization();
+            } catch (Error err) {
+                err.printStackTrace();
+            }
+        }
+        throw new InterruptedException(msg);
+    }
+
+    private static boolean isGone(Reference<?> ref) {
+        return ref.get() == null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/ko4j/src/main/java/org/netbeans/html/ko4j/KOTech.java
----------------------------------------------------------------------
diff --git a/ko4j/src/main/java/org/netbeans/html/ko4j/KOTech.java b/ko4j/src/main/java/org/netbeans/html/ko4j/KOTech.java
index 426399a..dd1ba6d 100644
--- a/ko4j/src/main/java/org/netbeans/html/ko4j/KOTech.java
+++ b/ko4j/src/main/java/org/netbeans/html/ko4j/KOTech.java
@@ -70,7 +70,8 @@ implements Technology.BatchCopy<Object>, Technology.ValueMutated<Object>, Techno
         if (ko != null) {
             ko[0] = newKO;
         }
-        newKO.wrapModel(
+        Knockout.wrapModel(
+            newKO,
             ret, copyFrom,
             propNames, propInfo, propValues, funcNames
         );

http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java
----------------------------------------------------------------------
diff --git a/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java b/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java
index 0eefdfd..f175ee0 100644
--- a/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java
+++ b/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java
@@ -34,7 +34,7 @@ import org.netbeans.html.json.spi.PropertyBinding;
  * to access the functionality.
  * <p>
  * Provides binding between {@link Model models} and knockout.js running
- * inside a JavaFX WebView. 
+ * inside a JavaFX WebView.
  *
  * @author Jaroslav Tulach
  */
@@ -43,17 +43,17 @@ final class Knockout extends WeakReference<Object> {
     private static final ReferenceQueue<Object> QUEUE = new ReferenceQueue();
     private static final Set<Knockout> active = Collections.synchronizedSet(new HashSet<Knockout>());
 
-    @JavaScriptBody(args = {"object", "property"}, body = 
-        "var ret;\n" + 
-        "if (property === null) ret = object;\n" + 
-        "else if (object === null) ret = null;\n" + 
-        "else ret = object[property];\n" + 
+    @JavaScriptBody(args = {"object", "property"}, body =
+        "var ret;\n" +
+        "if (property === null) ret = object;\n" +
+        "else if (object === null) ret = null;\n" +
+        "else ret = object[property];\n" +
         "return ret ? ko['utils']['unwrapObservable'](ret) : null;"
     )
     static Object getProperty(Object object, String property) {
         return null;
     }
-    
+
     private PropertyBinding[] props;
     private FunctionBinding[] funcs;
     private Object js;
@@ -72,7 +72,7 @@ final class Knockout extends WeakReference<Object> {
         }
         active.add(this);
     }
-    
+
     static void cleanUp() {
         for (;;) {
             Knockout ko = (Knockout)QUEUE.poll();
@@ -86,27 +86,27 @@ final class Knockout extends WeakReference<Object> {
             ko.funcs = null;
         }
     }
-    
+
     final void hold() {
         strong = get();
     }
-    
+
     final Object getValue(int index) {
         return props[index].getValue();
     }
-    
+
     final void setValue(int index, Object v) {
         if (v instanceof Knockout) {
             v = ((Knockout)v).get();
         }
         props[index].setValue(v);
     }
-    
+
     final void call(int index, Object data, Object ev) {
         funcs[index].call(data, ev);
     }
-    
-    @JavaScriptBody(args = { "model", "prop", "oldValue", "newValue" }, 
+
+    @JavaScriptBody(args = { "model", "prop", "oldValue", "newValue" },
         wait4js = false,
         body =
           "if (model) {\n"
@@ -127,7 +127,7 @@ final class Knockout extends WeakReference<Object> {
         Object model, String prop, Object oldValue, Object newValue
     );
 
-    @JavaScriptBody(args = { "id", "bindings" }, body = 
+    @JavaScriptBody(args = { "id", "bindings" }, body =
         "var d = window['document'];\n" +
         "var e = id ? d['getElementById'](id) : d['body'];\n" +
         "ko['cleanNode'](e);\n" +
@@ -135,21 +135,21 @@ final class Knockout extends WeakReference<Object> {
         "return bindings['ko4j'];\n"
     )
     native static Object applyBindings(String id, Object bindings);
-    
-    @JavaScriptBody(args = { "cnt" }, body = 
+
+    @JavaScriptBody(args = { "cnt" }, body =
         "var arr = new Array(cnt);\n" +
         "for (var i = 0; i < cnt; i++) arr[i] = new Object();\n" +
         "return arr;\n"
     )
     native static Object[] allocJS(int cnt);
-    
+
     @JavaScriptBody(
         javacall = true,
         keepAlive = false,
         wait4js = false,
-        args = { "ret", "copyFrom", "propNames", "propInfo", "propValues", "funcNames" },
-        body = 
-          "Object.defineProperty(ret, 'ko4j', { value : this });\n"
+        args = { "thiz", "ret", "copyFrom", "propNames", "propInfo", "propValues", "funcNames" },
+        body =
+          "Object.defineProperty(ret, 'ko4j', { value : thiz });\n"
         + "function normalValue(r) {\n"
         + "  if (r) try { var br = r.valueOf(); } catch (err) {}\n"
         + "  return br === undefined ? r: br;\n"
@@ -224,14 +224,15 @@ final class Knockout extends WeakReference<Object> {
         + "  koExpose(i, funcNames[i]);\n"
         + "}\n"
         )
-    native void wrapModel(
+    static native void wrapModel(
+        Knockout thiz,
         Object ret, Object copyFrom,
         String[] propNames, Number[] propInfo,
         Object propValues,
         String[] funcNames
     );
-    
-    @JavaScriptBody(args = { "js" }, wait4js = false, body = 
+
+    @JavaScriptBody(args = { "js" }, wait4js = false, body =
         "delete js['ko4j'];\n" +
         "for (var p in js) {\n" +
         "  delete js[p];\n" +
@@ -239,7 +240,7 @@ final class Knockout extends WeakReference<Object> {
         "\n"
     )
     private static native void clean(Object js);
-    
+
     @JavaScriptBody(args = { "o" }, body = "return o['ko4j'] ? o['ko4j'] : o;")
     private static native Object toModelImpl(Object wrapper);
     static Object toModel(Object wrapper) {

http://git-wip-us.apache.org/repos/asf/incubator-netbeans-html4j/blob/7ddcc5f1/src/main/javadoc/overview.html
----------------------------------------------------------------------
diff --git a/src/main/javadoc/overview.html b/src/main/javadoc/overview.html
index 58c7cf3..408e0cb 100644
--- a/src/main/javadoc/overview.html
+++ b/src/main/javadoc/overview.html
@@ -51,6 +51,13 @@
          yet the application code can be written in Java.
         </p>
 
+        <h3>New in version 1.4+</h3>
+
+        Bug fix for <a target="_blank" href="https://netbeans.org/bugzilla/show_bug.cgi?id=270481">
+        multiple observers</a> on a single model object.
+        Better <a target="_blank" href="https://netbeans.org/bugzilla/show_bug.cgi?id=270553">
+        GC behavior</a> specified in TCK and used in Knockout for Java implementation.
+
         <h3>New features in version 1.4</h3>
 
         Both values <code>null</code> and <code>undefined</code> are