diff --git a/client/src/main/kotlin/com/r3corda/client/fxutils/FlattenedList.kt b/client/src/main/kotlin/com/r3corda/client/fxutils/FlattenedList.kt index b048c173dc..a417fa8e8f 100644 --- a/client/src/main/kotlin/com/r3corda/client/fxutils/FlattenedList.kt +++ b/client/src/main/kotlin/com/r3corda/client/fxutils/FlattenedList.kt @@ -1,10 +1,12 @@ package com.r3corda.client.fxutils import javafx.beans.InvalidationListener +import javafx.beans.value.ChangeListener import javafx.beans.value.ObservableValue import javafx.collections.ListChangeListener import javafx.collections.ObservableList import javafx.collections.transformation.TransformationList +import org.eclipse.jetty.server.Authentication import java.util.* /** @@ -20,21 +22,30 @@ class FlattenedList(val sourceList: ObservableList * * Note that because of the bookkeeping required for this map, any remove operation and any add operation that * inserts to the middle of the list will be O(N) as we need to scan the map and shift indices accordingly. + * + * Note also that we're wrapping each ObservableValue, this is required because we want to support reusing of + * ObservableValues and we need each to have a different hash. */ - val indexMap = HashMap, Pair>() + class WrappedObservableValue( + val observableValue: ObservableValue + ) + val indexMap = HashMap, Pair>>() init { sourceList.forEachIndexed { index, observableValue -> - indexMap[observableValue] = Pair(index, createListener(observableValue)) + val wrappedObservableValue = WrappedObservableValue(observableValue) + indexMap[wrappedObservableValue] = Pair(index, createListener(wrappedObservableValue)) } } - private fun createListener(observableValue: ObservableValue): InvalidationListener { - return InvalidationListener { - val currentIndex = indexMap[observableValue]!!.first + private fun createListener(wrapped: WrappedObservableValue): ChangeListener { + val listener = ChangeListener { _observableValue, oldValue, newValue -> + val currentIndex = indexMap[wrapped]!!.first beginChange() - nextAdd(currentIndex, currentIndex + 1) + nextReplace(currentIndex, currentIndex + 1, listOf(oldValue)) endChange() } + wrapped.observableValue.addListener(listener) + return listener } override fun sourceChanged(c: ListChangeListener.Change>) { @@ -51,17 +62,17 @@ class FlattenedList(val sourceList: ObservableList } else { val removed = c.removed if (removed.size != 0) { - val removeStart = indexMap[removed.first()]!!.first - val removeEnd = indexMap[removed.last()]!!.first + 1 - require(removeStart < removeEnd) - val removeRange = removeEnd - removeStart + // TODO this assumes that if wasAdded() == true then we are adding elements to the getFrom() position + val removeStart = c.from + val removeRange = c.removed.size + val removeEnd = c.from + removeRange val iterator = indexMap.iterator() for (entry in iterator) { - val (observableValue, pair) = entry + val (wrapped, pair) = entry val (index, listener) = pair if (index >= removeStart) { if (index < removeEnd) { - observableValue.removeListener(listener) + wrapped.observableValue.removeListener(listener) iterator.remove() } else { // Shift indices @@ -87,7 +98,8 @@ class FlattenedList(val sourceList: ObservableList } } c.addedSubList.forEachIndexed { sublistIndex, observableValue -> - indexMap[observableValue] = Pair(addStart + sublistIndex, createListener(observableValue)) + val wrapped = WrappedObservableValue(observableValue) + indexMap[wrapped] = Pair(addStart + sublistIndex, createListener(wrapped)) } nextAdd(addStart, addEnd) } diff --git a/client/src/test/kotlin/com/r3corda/client/fxutils/FlattenedListTest.kt b/client/src/test/kotlin/com/r3corda/client/fxutils/FlattenedListTest.kt index 8c5cd1b714..646418b3ae 100644 --- a/client/src/test/kotlin/com/r3corda/client/fxutils/FlattenedListTest.kt +++ b/client/src/test/kotlin/com/r3corda/client/fxutils/FlattenedListTest.kt @@ -9,81 +9,105 @@ class FlattenedListTest { var sourceList = FXCollections.observableArrayList(SimpleObjectProperty(1234)) var flattenedList = FlattenedList(sourceList) + var replayedList = ReplayedList(flattenedList) @Before fun setup() { sourceList = FXCollections.observableArrayList(SimpleObjectProperty(1234)) flattenedList = FlattenedList(sourceList) + replayedList = ReplayedList(flattenedList) } @Test fun addWorks() { - require(flattenedList.size == 1) - require(flattenedList[0] == 1234) + require(replayedList.size == 1) + require(replayedList[0] == 1234) sourceList.add(SimpleObjectProperty(12)) - require(flattenedList.size == 2) - require(flattenedList[0] == 1234) - require(flattenedList[1] == 12) + require(replayedList.size == 2) + require(replayedList[0] == 1234) + require(replayedList[1] == 12) sourceList.add(SimpleObjectProperty(34)) - require(flattenedList.size == 3) - require(flattenedList[0] == 1234) - require(flattenedList[1] == 12) - require(flattenedList[2] == 34) + require(replayedList.size == 3) + require(replayedList[0] == 1234) + require(replayedList[1] == 12) + require(replayedList[2] == 34) sourceList.add(0, SimpleObjectProperty(56)) - require(flattenedList.size == 4) - require(flattenedList[0] == 56) - require(flattenedList[1] == 1234) - require(flattenedList[2] == 12) - require(flattenedList[3] == 34) + require(replayedList.size == 4) + require(replayedList[0] == 56) + require(replayedList[1] == 1234) + require(replayedList[2] == 12) + require(replayedList[3] == 34) sourceList.addAll(2, listOf(SimpleObjectProperty(78), SimpleObjectProperty(910))) - require(flattenedList.size == 6) - require(flattenedList[0] == 56) - require(flattenedList[1] == 1234) - require(flattenedList[2] == 78) - require(flattenedList[3] == 910) - require(flattenedList[4] == 12) - require(flattenedList[5] == 34) + require(replayedList.size == 6) + require(replayedList[0] == 56) + require(replayedList[1] == 1234) + require(replayedList[2] == 78) + require(replayedList[3] == 910) + require(replayedList[4] == 12) + require(replayedList[5] == 34) } @Test fun removeWorks() { val firstRemoved = sourceList.removeAt(0) require(firstRemoved.get() == 1234) - require(flattenedList.size == 0) + require(replayedList.size == 0) firstRemoved.set(123) sourceList.add(SimpleObjectProperty(12)) sourceList.add(SimpleObjectProperty(34)) sourceList.add(SimpleObjectProperty(56)) - require(flattenedList.size == 3) + require(replayedList.size == 3) val secondRemoved = sourceList.removeAt(1) require(secondRemoved.get() == 34) - require(flattenedList.size == 2) - require(flattenedList[0] == 12) - require(flattenedList[1] == 56) + require(replayedList.size == 2) + require(replayedList[0] == 12) + require(replayedList[1] == 56) secondRemoved.set(123) sourceList.clear() - require(flattenedList.size == 0) + require(replayedList.size == 0) } @Test fun updatingObservableWorks() { - require(flattenedList[0] == 1234) + require(replayedList[0] == 1234) sourceList[0].set(4321) - require(flattenedList[0] == 4321) + require(replayedList[0] == 4321) sourceList.add(0, SimpleObjectProperty(12)) sourceList[1].set(8765) - require(flattenedList[0] == 12) - require(flattenedList[1] == 8765) + require(replayedList[0] == 12) + require(replayedList[1] == 8765) sourceList[0].set(34) - require(flattenedList[0] == 34) - require(flattenedList[1] == 8765) + require(replayedList[0] == 34) + require(replayedList[1] == 8765) + } + + @Test + fun reusingObservableWorks() { + val observable = SimpleObjectProperty(12) + sourceList.add(observable) + sourceList.add(observable) + require(replayedList.size == 3) + require(replayedList[0] == 1234) + require(replayedList[1] == 12) + require(replayedList[2] == 12) + + observable.set(34) + require(replayedList.size == 3) + require(replayedList[0] == 1234) + require(replayedList[1] == 34) + require(replayedList[2] == 34) + + sourceList.removeAt(1) + require(replayedList.size == 2) + require(replayedList[0] == 1234) + require(replayedList[1] == 34) } } diff --git a/client/src/test/kotlin/com/r3corda/client/fxutils/ReplayedList.kt b/client/src/test/kotlin/com/r3corda/client/fxutils/ReplayedList.kt new file mode 100644 index 0000000000..c9b828e9ab --- /dev/null +++ b/client/src/test/kotlin/com/r3corda/client/fxutils/ReplayedList.kt @@ -0,0 +1,63 @@ +package com.r3corda.client.fxutils + +import javafx.collections.ListChangeListener +import javafx.collections.ObservableList +import javafx.collections.transformation.TransformationList +import java.util.* + +/** + * This list type just replays changes propagated from the underlying source list. Used for testing changes. + */ +class ReplayedList(sourceList: ObservableList) : TransformationList(sourceList) { + + val replayedList = ArrayList(sourceList) + + override val size: Int get() = replayedList.size + + override fun sourceChanged(c: ListChangeListener.Change) { + + beginChange() + while (c.next()) { + if (c.wasPermutated()) { + val from = c.from + val to = c.to + val permutation = IntArray(to, { c.getPermutation(it) }) + val permutedSubList = ArrayList(to - from) + for (i in 0 .. (to - from - 1)) { + permutedSubList.add(replayedList[permutation[from + i]]) + } + permutedSubList.forEachIndexed { i, element -> + replayedList[from + i] = element + } + nextPermutation(from, to, permutation) + } else if (c.wasUpdated()) { + for (i in c.from .. c.to - 1) { + replayedList[i] = c.list[i] + nextUpdate(i) + } + } else { + if (c.wasRemoved()) { + // TODO this assumes that if wasAdded() == true then we are adding elements to the getFrom() position + val removePosition = c.from + for (i in 0 .. c.removedSize - 1) { + replayedList.removeAt(removePosition) + } + nextRemove(c.from, c.removed) + } + if (c.wasAdded()) { + val addStart = c.from + val addEnd = c.to + for (i in addStart .. addEnd - 1) { + replayedList.add(i, c.list[i]) + } + nextAdd(addStart, addEnd) + } + } + } + endChange() + } + + override fun getSourceIndex(index: Int) = index + + override fun get(index: Int) = replayedList[index] +} diff --git a/client/src/test/kotlin/com/r3corda/client/fxutils/ReplayedListTest.kt b/client/src/test/kotlin/com/r3corda/client/fxutils/ReplayedListTest.kt new file mode 100644 index 0000000000..e0653eeb2e --- /dev/null +++ b/client/src/test/kotlin/com/r3corda/client/fxutils/ReplayedListTest.kt @@ -0,0 +1,86 @@ +package com.r3corda.client.fxutils + +import javafx.collections.FXCollections +import org.junit.Before +import org.junit.Test + +class ReplayedListTest { + + var sourceList = FXCollections.observableArrayList(1234) + var replayedList = ReplayedList(sourceList) + + @Before + fun setup() { + sourceList = FXCollections.observableArrayList(1234) + replayedList = ReplayedList(sourceList) + } + + @Test + fun addWorks() { + require(replayedList.size == 1) + require(replayedList[0] == 1234) + + sourceList.add(12) + require(replayedList.size == 2) + require(replayedList[0] == 1234) + require(replayedList[1] == 12) + + sourceList.add(34) + require(replayedList.size == 3) + require(replayedList[0] == 1234) + require(replayedList[1] == 12) + require(replayedList[2] == 34) + + sourceList.add(0, 56) + require(replayedList.size == 4) + require(replayedList[0] == 56) + require(replayedList[1] == 1234) + require(replayedList[2] == 12) + require(replayedList[3] == 34) + + sourceList.addAll(2, listOf(78, 910)) + require(replayedList.size == 6) + require(replayedList[0] == 56) + require(replayedList[1] == 1234) + require(replayedList[2] == 78) + require(replayedList[3] == 910) + require(replayedList[4] == 12) + require(replayedList[5] == 34) + } + + @Test + fun removeWorks() { + val firstRemoved = sourceList.removeAt(0) + require(firstRemoved == 1234) + require(replayedList.size == 0) + + sourceList.add(12) + sourceList.add(34) + sourceList.add(56) + require(replayedList.size == 3) + val secondRemoved = sourceList.removeAt(1) + require(secondRemoved == 34) + require(replayedList.size == 2) + require(replayedList[0] == 12) + require(replayedList[1] == 56) + + sourceList.clear() + require(replayedList.size == 0) + } + + @Test + fun updateWorks() { + require(replayedList[0] == 1234) + sourceList[0] = 4321 + require(replayedList[0] == 4321) + + sourceList.add(0, 12) + sourceList[1] = 8765 + require(replayedList[0] == 12) + require(replayedList[1] == 8765) + + sourceList[0] = 34 + require(replayedList[0] == 34) + require(replayedList[1] == 8765) + } +}