diff --git a/libraries/stdlib/src/kotlin/properties/Properties.kt b/libraries/stdlib/src/kotlin/properties/Properties.kt index 6c5aa09312f..e045c5993c6 100644 --- a/libraries/stdlib/src/kotlin/properties/Properties.kt +++ b/libraries/stdlib/src/kotlin/properties/Properties.kt @@ -1,9 +1,8 @@ package kotlin.properties -import kotlin.* -import kotlin.util.* import java.util.HashMap import java.util.ArrayList +import kotlin.properties.delegation.ObservableProperty public class ChangeEvent(val source: Any, val name: String, val oldValue: Any?, val newValue: Any?) { var propogationId: Any? = null @@ -66,6 +65,10 @@ public abstract class ChangeSupport { } } + protected fun property(init: T): ObservableProperty { + return ObservableProperty(init) { name, oldValue, newValue -> changeProperty(name, oldValue, newValue) } + } + public fun onPropertyChange(fn: (ChangeEvent) -> Unit) { // TODO //addChangeListener(DelegateChangeListener(fn)) diff --git a/libraries/stdlib/src/kotlin/properties/delegation/Delegation.kt b/libraries/stdlib/src/kotlin/properties/delegation/Delegation.kt new file mode 100644 index 00000000000..fe4dbb05a6d --- /dev/null +++ b/libraries/stdlib/src/kotlin/properties/delegation/Delegation.kt @@ -0,0 +1,118 @@ +package kotlin.properties.delegation + +import kotlin.properties.ChangeSupport + +public class NotNullVar { + private var value: T? = null + + public fun get(thisRef: Any?, desc: PropertyMetadata): T { + return value ?: throw IllegalStateException("Property ${desc.name} should be initialized before get") + } + + public fun set(thisRef: Any?, desc: PropertyMetadata, value: T) { + this.value = value + } +} + +public class SynchronizedVar(private val initValue: T, private val lock: Any) { + private var value: T = initValue + + public fun get(thisRef: Any?, desc: PropertyMetadata): T { + return synchronized(lock) { value } + } + + public fun set(thisRef: Any?, desc: PropertyMetadata, newValue: T) { + synchronized(lock) { + value = newValue + } + } +} + +class ObservableProperty(initialValue: T, val changeSupport: (name: String, oldValue: T, newValue: T) -> Unit) { + private var value = initialValue + + public fun get(thisRef: Any?, desc: PropertyMetadata): T { + return value + } + + public fun set(thisRef: Any?, desc: PropertyMetadata, newValue: T) { + changeSupport(desc.name, value, newValue) + value = newValue + } +} + +public class KeyMissingException(message: String): RuntimeException(message) + +public abstract class MapVal { + public abstract fun getMap(thisRef: T): Map<*, *> + public abstract fun getKey(desc: PropertyMetadata): Any? + + public open fun getDefaultValue(desc: PropertyMetadata, key: Any?): V { + throw KeyMissingException("Key $key is missing") + } + + public fun get(thisRef: T, desc: PropertyMetadata): V { + val key = getKey(desc) + val map = getMap(thisRef) + if (!map.containsKey(key)) { + return getDefaultValue(desc, key) + } + return map[key] as V + } +} + +public abstract class MapVar: MapVal() { + + public fun set(thisRef: T, desc: PropertyMetadata, newValue: V) { + val map = getMap(thisRef) as MutableMap + map[getKey(desc)] = newValue + } +} + +public fun Map.readOnlyProperty(default: (() -> V)? = null): MapVal { + return object: MapVal() { + override fun getMap(thisRef: Any?) = this@readOnlyProperty + override fun getKey(desc: PropertyMetadata) = desc.name + override fun getDefaultValue(desc: PropertyMetadata, key: Any?) = if (default == null) super.getDefaultValue(desc, key) else default() + } +} + +public fun Map<*, *>.readOnlyProperty(default: (() -> V)? = null, key: Any?): MapVal { + return object: MapVal() { + override fun getMap(thisRef: Any?) = this@readOnlyProperty + override fun getKey(desc: PropertyMetadata) = key + override fun getDefaultValue(desc: PropertyMetadata, key: Any?) = if (default == null) super.getDefaultValue(desc, key) else default() + } +} + +public fun Map<*, *>.readOnlyProperty(default: (() -> V)? = null, key: (desc: PropertyMetadata) -> Any?): MapVal { + return object: MapVal() { + override fun getMap(thisRef: Any?) = this@readOnlyProperty + override fun getKey(desc: PropertyMetadata) = key(desc) + override fun getDefaultValue(desc: PropertyMetadata, key: Any?) = if (default == null) super.getDefaultValue(desc, key) else default() + } +} + +public fun MutableMap.property(default: (() -> V)? = null): MapVar { + return object: MapVar() { + override fun getMap(thisRef: Any?) = this@property + override fun getKey(desc: PropertyMetadata) = desc.name + override fun getDefaultValue(desc: PropertyMetadata, key: Any?) = if (default == null) super.getDefaultValue(desc, key) else default() + } +} + +public fun MutableMap<*, *>.property(default: (() -> V)? = null, key: Any?): MapVar { + return object: MapVar() { + override fun getMap(thisRef: Any?) = this@property + override fun getKey(desc: PropertyMetadata) = key + override fun getDefaultValue(desc: PropertyMetadata, key: Any?) = if (default == null) super.getDefaultValue(desc, key) else default() + } +} + +public fun MutableMap<*, *>.property(default: (() -> V)? = null, key: (desc: PropertyMetadata) -> Any?): MapVar { + return object: MapVar() { + override fun getMap(thisRef: Any?) = this@property + override fun getKey(desc: PropertyMetadata) = key(desc) + override fun getDefaultValue(desc: PropertyMetadata, key: Any?) = if (default == null) super.getDefaultValue(desc, key) else default() + } +} \ No newline at end of file diff --git a/libraries/stdlib/src/kotlin/properties/delegation/lazy/LazyValues.kt b/libraries/stdlib/src/kotlin/properties/delegation/lazy/LazyValues.kt new file mode 100644 index 00000000000..2d0767446d3 --- /dev/null +++ b/libraries/stdlib/src/kotlin/properties/delegation/lazy/LazyValues.kt @@ -0,0 +1,75 @@ +package kotlin.properties.delegation.lazy + +private val NULL_VALUE: Any = Any() + +private fun escape(value: T): Any { + return if (value == null) NULL_VALUE else value +} + +private fun unescape(value: Any): T? { + return if (value == NULL_VALUE) null else value as T +} + +public open class LazyVal(initializer: () -> T): NullableLazyVal(initializer) { + override fun get(thisRef: Any?, desc: PropertyMetadata): T { + return super.get(thisRef, desc)!! + } +} + +public open class NullableLazyVal(private val initializer: () -> T?) { + private var value: Any? = null + + public open fun get(thisRef: Any?, desc: PropertyMetadata): T? { + if (value == null) { + value = escape(initializer()) + } + return unescape(value!!) + } +} + +public open class VolatileLazyVal(initializer: () -> T): VolatileNullableLazyVal(initializer) { + override fun get(thisRef: Any?, desc: PropertyMetadata): T { + return super.get(thisRef, desc)!! + } +} + +public open class VolatileNullableLazyVal(private val initializer: () -> T?) { + private volatile var value: Any? = null + + public open fun get(thisRef: Any?, desc: PropertyMetadata): T? { + if (value == null) { + value = escape(initializer()) + } + return unescape(value!!) + } +} + +public open class AtomicLazyVal(lock: Any? = null, initializer: () -> T): AtomicNullableLazyVal(lock, initializer) { + override fun get(thisRef: Any?, desc: PropertyMetadata): T { + return super.get(thisRef, desc)!! + } +} + +public open class AtomicNullableLazyVal(lock: Any? = null, private val initializer: () -> T?) { + private val lock = lock ?: this + private volatile var value: Any? = null + + public open fun get(thisRef: Any?, desc: PropertyMetadata): T? { + val _v1 = value + if (_v1 != null) { + return unescape(_v1) + } + + return synchronized(lock) { + val _v2 = value + if (_v2 != null) { + unescape(_v2) + } + else { + val typedValue = initializer() + value = escape(typedValue) + typedValue + } + } + } +} \ No newline at end of file diff --git a/libraries/stdlib/test/properties/delegation/DelegationTest.kt b/libraries/stdlib/test/properties/delegation/DelegationTest.kt new file mode 100644 index 00000000000..cceeeaf6af1 --- /dev/null +++ b/libraries/stdlib/test/properties/delegation/DelegationTest.kt @@ -0,0 +1,64 @@ +package test.properties.delegation + +import junit.framework.TestCase +import kotlin.test.* +import kotlin.properties.* +import kotlin.properties.delegation.* +import kotlin.properties.delegation.lazy.* + +trait WithBox { + fun box(): String +} + +abstract class DelegationTestBase: TestCase() { + fun doTest(klass: WithBox) { + assertEquals("OK", klass.box()) + } +} + +class DelegationTest(): DelegationTestBase() { + fun testNotNullVar() { + doTest(TestNotNullVar("a", "b")) + } + + fun testObservableProperty() { + doTest(TestObservableProperty()) + } +} + +public class TestNotNullVar(val a1: String, val b1: T): WithBox { + var a: String by NotNullVar() + var b by NotNullVar() + + override fun box(): String { + a = a1 + b = b1 + if (a != "a") return "fail: a shouuld be a, but was $a" + if (b != "b") return "fail: b should be b, but was $b" + return "OK" + } +} + +class TestObservableProperty: WithBox, ChangeSupport() { + + var b by property(init = 2) + var c by property(3) + + override fun box(): String { + var result = false + addChangeListener("b", object: ChangeListener { + public override fun onPropertyChange(event: ChangeEvent) { + result = true + } + }) + addChangeListener("c", object: ChangeListener { + public override fun onPropertyChange(event: ChangeEvent) { + result = false + } + }) + b = 4 + if (b != 4) return "fail: b != 4" + if (!result) return "fail: result should be true" + return "OK" + } +} \ No newline at end of file diff --git a/libraries/stdlib/test/properties/delegation/MapDelegationTest.kt b/libraries/stdlib/test/properties/delegation/MapDelegationTest.kt new file mode 100644 index 00000000000..9bc21c81c20 --- /dev/null +++ b/libraries/stdlib/test/properties/delegation/MapDelegationTest.kt @@ -0,0 +1,224 @@ +package test.properties.delegation + +import java.util.HashMap +import kotlin.properties.delegation.* + +class MapDelegationTest(): DelegationTestBase() { + + fun testMapPropertyString() { + doTest(TestMapPropertyString()) + } + + fun testMapValWithDifferentTypes() { + doTest(TestMapValWithDifferentTypes()) + } + + fun testMapVarWithDifferentTypes() { + doTest(TestMapVarWithDifferentTypes()) + } + + fun testNullableKey() { + doTest(TestNullableKey()) + } + + fun testMapPropertyKey() { + doTest(TestMapPropertyKey()) + } + + fun testMapPropertyFunction() { + doTest(TestMapPropertyFunction()) + } + + fun testMapPropertyCustom() { + doTest(TestMapPropertyCustom()) + } + + fun testMapValWithDefault() { + doTest(TestMapValWithDefault()) + } + + fun testMapVarWithDefault() { + doTest(TestMapVarWithDefault()) + } + + fun testMapPropertyCustomWithDefault() { + doTest(TestMapPropertyCustomWithDefault()) + } +} + +class TestMapValWithDifferentTypes(): WithBox { + val map = hashMapOf("a" to "a", "b" to 1, "c" to A(1), "d" to null) + val a by map.readOnlyProperty() + val b by map.readOnlyProperty() + val c by map.readOnlyProperty() + val d by map.readOnlyProperty() + + override fun box(): String { + if (a != "a") return "fail at 'a'" + if (b != 1) return "fail at 'b'" + if (c != A(1)) return "fail at 'c'" + if (d != null) return "fail at 'd'" + return "OK" + } + + data class A(val a: Int) +} + +class TestMapVarWithDifferentTypes(): WithBox { + val map: HashMap = hashMapOf("a" to "a", "b" to 1, "c" to A(1), "d" to "d") + var a by map.property() + var b by map.property() + var c by map.property() + var d by map.property() + + override fun box(): String { + a = "aa" + b = 11 + c = A(11) + d = null + if (a != "aa") return "fail at 'a'" + if (b != 11) return "fail at 'b'" + if (c != A(11)) return "fail at 'c'" + if (d != null) return "fail at 'd'" + return "OK" + } + + data class A(val a: Int) +} + +class TestNullableKey: WithBox { + val map = hashMapOf(null to "null") + var a by map.property { desc -> null } + + override fun box(): String { + if (a != "null") return "fail at 'a'" + a = "foo" + if (a != "foo") return "fail at 'a' after set" + return "OK" + } +} + +class TestMapPropertyString(): WithBox { + val map = hashMapOf("a" to "a", "b" to "b", "c" to "c") + val a by map.readOnlyProperty() + var b by map.property() + val c by map.property() + + override fun box(): String { + b = "newB" + if (a != "a") return "fail at 'a'" + if (b != "newB") return "fail at 'b'" + if (c != "c") return "fail at 'c'" + return "OK" + } +} + +class TestMapValWithDefault(): WithBox { + val map = hashMapOf() + val a by map.readOnlyProperty(default = { "aDefault" }) + val b by map.readOnlyProperty(default = { "bDefault" }, key = "b") + val c by map.readOnlyProperty(default = { "cDefault" }, key = { desc -> desc.name }) + + override fun box(): String { + if (a != "aDefault") return "fail at 'a'" + if (b != "bDefault") return "fail at 'b'" + if (c != "cDefault") return "fail at 'c'" + return "OK" + } +} + +class TestMapVarWithDefault(): WithBox { + val map = hashMapOf() + var a by map.property(default = { "aDefault" }) + var b by map.property(default = { "bDefault" }, key = "b") + var c by map.property(default = { "cDefault" }, key = { desc -> desc.name }) + + override fun box(): String { + if (a != "aDefault") return "fail at 'a'" + if (b != "bDefault") return "fail at 'b'" + if (c != "cDefault") return "fail at 'c'" + a = "a" + b = "b" + c = "c" + if (a != "a") return "fail at 'a' after set" + if (b != "b") return "fail at 'b' after set" + if (c != "c") return "fail at 'c' after set" + return "OK" + } +} + +class TestMapPropertyKey(): WithBox { + val map = hashMapOf("a" to "a", "b" to "b") + val a by map.readOnlyProperty(key = "a") + var b by map.property(key = "b") + + override fun box(): String { + b = "c" + if (a != "a") return "fail at 'a'" + if (b != "c") return "fail at 'b'" + return "OK" + } +} + +class TestMapPropertyFunction(): WithBox { + val map = hashMapOf("aDesc" to "a", "bDesc" to "b") + val a by map.readOnlyProperty { desc -> "${desc.name}Desc" } + var b by map.property { desc -> "${desc.name}Desc" } + + override fun box(): String { + b = "c" + if (a != "a") return "fail at 'a'" + if (b != "c") return "fail at 'b' after set" + return "OK" + } +} + +val mapVal = object : MapVal() { + override fun getMap(thisRef: TestMapPropertyCustom) = thisRef.map + override fun getKey(desc: PropertyMetadata) = "${desc.name}Desc" +} +val mapVar = object : MapVar() { + override fun getMap(thisRef: TestMapPropertyCustom) = thisRef.map + override fun getKey(desc: PropertyMetadata) = "${desc.name}Desc" +} + +class TestMapPropertyCustom(): WithBox { + val map = hashMapOf("aDesc" to "a", "bDesc" to "b") + val a by mapVal + var b by mapVar + + override fun box(): String { + b = "newB" + if (a != "a") return "fail at 'a'" + if (b != "newB") return "fail at 'b' after set" + return "OK" + } +} + +val mapValWithDefault = object : MapVal() { + override fun getMap(thisRef: TestMapPropertyCustomWithDefault) = thisRef.map + override fun getKey(desc: PropertyMetadata) = desc.name + + override fun getDefaultValue(desc: PropertyMetadata, key: Any?) = "default" +} + +val mapVarWithDefault = object : MapVar() { + override fun getMap(thisRef: TestMapPropertyCustomWithDefault) = thisRef.map + override fun getKey(desc: PropertyMetadata) = desc.name + + override fun getDefaultValue(desc: PropertyMetadata, key: Any?) = "default" +} + +class TestMapPropertyCustomWithDefault(): WithBox { + val map = hashMapOf() + val a by mapValWithDefault + var b by mapVarWithDefault + + override fun box(): String { + if (a != "default") return "fail at 'a'" + if (b != "default") return "fail at 'b'" + b = "c" + if (b != "c") return "fail at 'b' after set" + return "OK" + } +} \ No newline at end of file diff --git a/libraries/stdlib/test/properties/delegation/lazy/LazyValuesTest.kt b/libraries/stdlib/test/properties/delegation/lazy/LazyValuesTest.kt new file mode 100644 index 00000000000..6a3a4cac847 --- /dev/null +++ b/libraries/stdlib/test/properties/delegation/lazy/LazyValuesTest.kt @@ -0,0 +1,143 @@ +package test.properties.delegation.lazy + +import kotlin.properties.delegation.lazy.* +import test.properties.delegation.WithBox +import test.properties.delegation.DelegationTestBase + +class LazyValuesTest(): DelegationTestBase() { + + fun testLazyVal() { + doTest(TestLazyVal()) + } + + fun testNullableLazyVal() { + doTest(TestNullableLazyVal()) + } + + fun testAtomicNullableLazyVal() { + doTest(TestAtomicNullableLazyVal()) + } + + fun testAtomicLazyVal() { + doTest(TestAtomicLazyVal()) + } + + fun testVolatileNullableLazyVal() { + doTest(TestVolatileNullableLazyVal()) + } + + fun testVolatileLazyVal() { + doTest(TestVolatileLazyVal()) + } +} + +class TestLazyVal: WithBox { + var result = 0 + val a by LazyVal { + ++result + } + + override fun box(): String { + a + if (a != 1) return "fail: initializer should be invoked only once" + return "OK" + } +} + +class TestNullableLazyVal: WithBox { + var resultA = 0 + var resultB = 0 + + val a: Int? by NullableLazyVal { resultA++; null} + val b by NullableLazyVal { foo() } + + override fun box(): String { + a + b + + if (a != null) return "fail: a should be null" + if (b != null) return "fail: a should be null" + if (resultA != 1) return "fail: initializer for a should be invoked only once" + if (resultB != 1) return "fail: initializer for b should be invoked only once" + return "OK" + } + + fun foo(): String? { + resultB++ + return null + } +} + +class TestAtomicLazyVal: WithBox { + var result = 0 + val a by AtomicLazyVal { + ++result + } + + override fun box(): String { + a + if (a != 1) return "fail: initializer should be invoked only once" + return "OK" + } +} + +class TestVolatileNullableLazyVal: WithBox { + var resultA = 0 + var resultB = 0 + + val a: Int? by VolatileNullableLazyVal { resultA++; null} + val b by VolatileNullableLazyVal { foo() } + + override fun box(): String { + a + b + + if (a != null) return "fail: a should be null" + if (b != null) return "fail: a should be null" + if (resultA != 1) return "fail: initializer for a should be invoked only once" + if (resultB != 1) return "fail: initializer for b should be invoked only once" + return "OK" + } + + fun foo(): String? { + resultB++ + return null + } +} + +class TestVolatileLazyVal: WithBox { + var result = 0 + val a by VolatileLazyVal { + ++result + } + + override fun box(): String { + a + if (a != 1) return "fail: initializer should be invoked only once" + return "OK" + } +} + +class TestAtomicNullableLazyVal: WithBox { + var resultA = 0 + var resultB = 0 + + val a: Int? by AtomicNullableLazyVal { resultA++; null} + val b by AtomicNullableLazyVal { foo() } + + override fun box(): String { + a + b + + if (a != null) return "fail: a should be null" + if (b != null) return "fail: a should be null" + if (resultA != 1) return "fail: initializer for a should be invoked only once" + if (resultB != 1) return "fail: initializer for b should be invoked only once" + return "OK" + } + + fun foo(): String? { + resultB++ + return null + } +}