blob: ed3f3c2ab784bdfd8d168db33cddab8accb140d6 [file] [log] [blame]
/**
* 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.List;
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 DeepChangeTest {
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 = "MyX", targetId = "anythingX", properties = {
@Property(name = "one", type = MyY.class),
@Property(name = "all", type = MyY.class, array = true)
})
static class X {
@ComputedProperty @Transitive(deep = true)
static MyY oneCopy(MyY one) {
return Models.bind(one, BrwsrCtx.findDefault(X.class));
}
@ComputedProperty @Transitive(deep = true)
static String oneName(MyY one) {
return one.getValue();
}
@ComputedProperty
static String sndName(MyY one) {
if (one == null || one.getValue() == null) {
return null;
} else {
return one.getValue().toUpperCase();
}
}
@ComputedProperty @Transitive(deep = false)
static String noName(MyY one) {
if (one == null || one.getValue() == null) {
return null;
} else {
return one.getValue().toUpperCase();
}
}
@ComputedProperty @Transitive(deep = true)
static String thrdName(MyY one) {
return "X" + one.getCount();
}
@ComputedProperty
static String allNames(List<MyY> all) {
StringBuilder sb = new StringBuilder();
for (MyY y : all) {
sb.append(y.getValue());
}
return sb.toString();
}
@ComputedProperty @Transitive(deep = true)
static String firstFromNames(List<MyY> all) {
for (MyY y : all) {
if (y != null && y.getValue() != null) {
return y.getValue();
}
}
return null;
}
}
@Model(className = "MyY", properties = {
@Property(name = "value", type = String.class),
@Property(name = "count", type = int.class)
})
static class Y {
}
@Model(className = "MyOverall", properties = {
@Property(name = "x", type = MyX.class)
})
static class Overall {
@ComputedProperty @Transitive(deep = true)
static String valueAccross(MyX x) {
return x.getFirstFromNames();
}
}
@Test public void isTransitiveChangeNotifiedProperly() throws Exception {
MyX p = Models.bind(
new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999)
), c).applyBindings();
Map m = (Map)Models.toRaw(p);
Object v = m.get("oneName");
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(), "Ahoj");
p.getOne().setValue("Nazdar");
assertEquals(o.get(), "Nazdar");
assertEquals(o.changes, 1, "One change so far");
}
@Test public void isTransitiveChangeInArrayNotifiedProperly() throws Exception {
MyX p = Models.bind(
new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999)
), c).applyBindings();
Map m = (Map)Models.toRaw(p);
Object v = m.get("allNames");
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(), "HiHello");
p.getAll().get(0).setValue("Nazdar");
assertEquals(o.get(), "NazdarHello");
assertEquals(o.changes, 1, "One change so far");
}
@Test public void changingModelClass() throws Exception {
final MyY myY = new MyY("Ahoj", 0);
MyX p = Models.bind(
new MyX(myY, new MyY("Hi", 333), new MyY("Hello", 999)),
c)
.applyBindings();
MyY realY = p.getOne();
Map m = (Map)Models.toRaw(p);
Object v = m.get("one");
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");
assertFalse(o.pb.isReadOnly(), "Normal property");
final MyY newY = new MyY("Hi", 1);
p.setOne(newY);
assertSame(p.getOne(), newY);
assertEquals(o.changes, 1, "One change");
}
@Test public void addingIntoArray() throws Exception {
MyX p = Models.bind(
new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999)
), c).applyBindings();
Map m = (Map)Models.toRaw(p);
Object v = m.get("allNames");
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(), "HiHello");
MyY y = new MyY("Cus", 1);
p.getAll().add(y);
assertEquals(o.changes, 1, "One change so far");
assertEquals(o.get(), "HiHelloCus");
y.setValue("Nazdar");
assertEquals(o.changes, 2, "2nd change so far");
assertEquals(o.get(), "HiHelloNazdar");
y.setValue("Zdravim");
assertEquals(o.changes, 3, "3rd change so far");
assertEquals(o.get(), "HiHelloZdravim");
}
@Test public void firstChangeInArrayNotifiedProperly() throws Exception {
MyX p = Models.bind(
new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999)
), c).applyBindings();
Map m = (Map)Models.toRaw(p);
Object v = m.get("firstFromNames");
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");
p.getAll().get(0).setValue("Nazdar");
assertEquals(o.get(), "Nazdar");
assertEquals(o.changes, 1, "One change so far");
}
@Test public void firstChangeInArrayToNull() throws Exception {
MyX p = Models.bind(
new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999)
), c).applyBindings();
Map m = (Map)Models.toRaw(p);
Object v = m.get("firstFromNames");
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");
p.getAll().get(0).setValue(null);
assertEquals(o.get(), "Hello");
assertEquals(o.changes, 1, "One change so far");
p.getAll().get(0).setValue("Nazdar");
assertEquals(o.get(), "Nazdar");
assertEquals(o.changes, 2, "2nd change so far");
}
@Test public void firstChangeInArrayNotifiedTransitively() throws Exception {
MyOverall p = Models.bind(
new MyOverall(new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999))
), c);
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);
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 {
MyX p = Models.bind(
new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999)
), c).applyBindings();
Map m = (Map)Models.toRaw(p);
Object v = m.get("firstFromNames");
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");
p.getAll().get(1).setValue("Nazdar");
assertEquals(o.get(), "Hi");
assertEquals(o.changes, 0, "No change so far");
}
@Test public void changeInArraySizeNeedsToBeRecomputed() throws Exception {
MyX p = Models.bind(
new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999)
), c).applyBindings();
Map m = (Map)Models.toRaw(p);
Object v = m.get("firstFromNames");
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");
p.getAll().remove(1);
assertEquals(o.get(), "Hi");
assertEquals(o.changes, 1, "This required a change");
}
@Test public void doublePropertyChangeNotified() throws Exception {
MyX p = Models.bind(
new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999)
), c).applyBindings();
Map m = (Map)Models.toRaw(p);
Object v = m.get("oneName");
assertNotNull(v, "Value should be in the map");
Object v2 = m.get("sndName");
assertNotNull(v2, "Value2 should be in the map");
One o = (One)v;
One o2 = (One)v2;
assertEquals(o.changes, 0, "No changes so far");
assertEquals(o2.changes, 0, "No changes so far");
assertTrue(o.pb.isReadOnly(), "Derived property");
assertEquals(o.get(), "Ahoj");
assertEquals(o2.get(), "AHOJ");
p.getOne().setValue("Nazdar");
assertEquals(o.get(), "Nazdar");
assertEquals(o.changes, 1, "One change so far");
assertEquals(o2.changes, 1, "One change so far");
assertEquals(o2.get(), "NAZDAR");
}
@Test public void onlyAffectedPropertyChangeNotified() throws Exception {
MyX p = Models.bind(
new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999)
), c).applyBindings();
Map m = (Map)Models.toRaw(p);
Object v = m.get("oneName");
assertNotNull(v, "Value should be in the map");
Object v2 = m.get("thrdName");
assertNotNull(v2, "Value2 should be in the map");
One o = (One)v;
One o2 = (One)v2;
assertEquals(o.changes, 0, "No changes so far");
assertEquals(o2.changes, 0, "No changes so far");
assertTrue(o.pb.isReadOnly(), "Derived property");
assertEquals(o.get(), "Ahoj");
assertEquals(o2.get(), "X0");
p.getOne().setCount(10);
assertEquals(o.get(), "Ahoj");
assertEquals(o.changes, 0, "Still no change");
assertEquals(o2.changes, 1, "One change so far");
assertEquals(o2.get(), "X10");
}
@Test public void onlyDeepPropsAreNotified() throws Exception {
MyX p = Models.bind(
new MyX(new MyY("Ahoj", 0), new MyY("Hi", 333), new MyY("Hello", 999)
), c).applyBindings();
Map m = (Map)Models.toRaw(p);
Object v = m.get("oneName");
assertNotNull(v, "Value should be in the map");
Object v2 = m.get("noName");
assertNotNull(v2, "Value2 should be in the map");
One o = (One)v;
One o2 = (One)v2;
assertEquals(o.changes, 0, "No changes so far");
assertEquals(o2.changes, 0, "No changes so far");
assertTrue(o.pb.isReadOnly(), "Derived property");
assertEquals(o.get(), "Ahoj");
assertEquals(o2.get(), "AHOJ");
p.getOne().setValue("Nazdar");
assertEquals(o.get(), "Nazdar");
assertEquals(o.changes, 1, "One change so far");
assertEquals(o2.changes, 0, "This change is not noticed");
assertEquals(o2.get(), "NAZDAR", "but property value changes when computed");
}
@Test
public void mixingContextsIsOK() throws Exception {
BrwsrCtx ctx = Contexts.newBuilder().build();
final MyY one = Models.bind(new MyY("Ahoj", 0), ctx);
MyX p = Models.bind(
new MyX(one, new MyY("Hi", 333), new MyY("Hello", 999)), c
).applyBindings();
Map m = (Map) Models.toRaw(p);
Object v = m.get("oneName");
assertNotNull(v, "Value should be in the map");
One o = (One) v;
assertEquals(o.get(), "Ahoj");
p.getOne().setValue("Nazdar");
assertEquals(o.get(), "Nazdar");
assertEquals(o.changes, 1, "One change so far");
}
@Test
public void rebindReplacesTheInstance() throws Exception {
BrwsrCtx ctx = Contexts.newBuilder().build();
MyX x = new MyX();
MyY y = Models.bind(new MyY(), ctx);
x.setOne(y);
assertSame(x.getOne(), y);
}
@Test
public void rebindReplacesTheInstanceAndNotifies() throws Exception {
BrwsrCtx ctx = Contexts.newBuilder().build();
final MyY one = Models.bind(new MyY(), ctx);
MyX p = Models.bind(
new MyX(one, new MyY("Hi", 333), new MyY("Hello", 999)), c
).applyBindings();
Map m = (Map) Models.toRaw(p);
Object v = m.get("one");
assertNotNull(v, "Value should be in the map");
One o = (One) v;
assertEquals(o.changes, 0, "No changes yet");
MyY y = Models.bind(new MyY(), ctx);
p.setOne(y);
assertSame(p.getOne(), y);
assertSame(o.changes, 1, "One change now");
}
@Test
public void mixingWithCloneIsOK() throws Exception {
BrwsrCtx ctx = Contexts.newBuilder().build();
final MyY one = Models.bind(new MyY("Ahoj", 0), ctx);
MyX p = Models.bind(new MyX(one, new MyY("Hi", 333), new MyY("Hello", 999)
), c).applyBindings();
Map m = (Map) Models.toRaw(p);
Object v = m.get("oneCopy");
assertNotNull(v, "Value should be in the map");
One o = (One) v;
assertEquals(((MyY)o.get()).getValue(), "Ahoj");
p.getOne().setValue("Nazdar");
assertEquals(((MyY)o.get()).getValue(), "Nazdar");
assertEquals(o.changes, 1, "One change so far");
}
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;
}
}