-
Notifications
You must be signed in to change notification settings - Fork 0
Collection Adjustment
The number of times I've had 2 related lists that I needed to reconcile is so many that I can scarecely believe there's not a standard Java API for addressing this problem.
Imagine an API that exposes a list of values. Suppose you want to represent those values to the user in a UI list or table. Then you receive an event, from either the service or the UI, that some change has occurred. You now need to either update the UI for the changes from the service, or communicate to the service that the user has requested a change to the data.
The easy way this is handled for service->UI changes is to clear the entire table and re-add all the data, but this causes many undesirable effects. For one, it is extremely inefficient, especially for large data sets. It also will usually clear the user's selection and scroll location among other undesirable UI effects. In the other direction (UI->service), clearing and re-adding data is not usually possible.
The solution I creates is called adjustment. Adjustment goes through 2 lists to find elements that both lists have in common. Then it performs actions around these elements for each difference (and common element).
The CollectionUtils class exposes the synchronize() methods, which accept 2 lists. One of the methods accepts a BiPredicate which determines the equivalence of 2 elements. The other may only be called where the value-type of the second list extends that of the first. These methods return a CollectionAdjustment class which may be used in 1 of 2 ways.
For the Custom method of adjustment, CollectionAdjustment.adjust() is called with a custom CollectionSynchronizer implementation and an order. CollectionSynchronizer operates on ElementSyncInput arguments. These usually represent a single element which may be either common to both lists or present in only one or the other. ElementSyncInput exposes:
- getLeftValue(), the value in the element in the left list that is represented. This will throw a NoSuchElementExcetion for right-only elements not yet added to the left list.
- getRightValue(), the value in the element in the right list that is represented. This will throw a NoSuchElementExcetion for left-only elements.
- getOriginalLeftIndex(), the index of the element in the left list that is represented before any updates that may have occurred with this adjustment. This will return -1 for right-only elements.
- getRightIndex(), the index of the element in the right list that is represented. This will return -1 for left-only elements.
- getUpdatedLeftIndex(). The index of the element in the left list that is represented, adjusted for any add/remove/move operations that may have occurred so far in the adjustment operation.
- getTargetIndex(). The index into which the element would be preserved, set, inserted, or moved. For each element in the lists, ElementSyncInput exposes 3 operations: preserve(), remove(), or useValue(L). These have slightly different meanings for each of the 3 different types of elements, so they will be documented there.
CollectionSynchronizer contains 4 methods:
- getOrder(ElementSyncInput<L, R>). When an element is present in the left collection and another, non-equivalent element is present in the right collection, this method determines which element will be dealt with first. Unlike the parameter to the other methods, this ElementSyncInput represents 2 non-equivalent elements from the 2 collections. If this method returns true, the element in the left collection will be dealt with first. If that element is kept in the collection and the right element is added (as determined by calls to other methods in this class), returning true here will mean that the new element is added after the existing one.
- leftOnly(ElementSyncInput<L, R>). This is called when an element in the left list has no equivalent in the right list. The ElementSyncAction returned by this method should be a result of calling:
- ElementSyncInput.preserve() if the element should remain in the collection without changing its value (and without calling a set method on that element).
- ElementSyncInput.remove() if the element should be removed from the collection
- ElementSyncInput.useValue(L) if the element should be changed or updated (i.e. a set method should be called on that element with the given value).
- rightOnly(ElementSyncInput<L, R>). This is called when an element in the right list has no equivalent in the left list. The ElementSyncAction returned should be a result of calling:
- ElementSyncInput.preserve() or ElementSyncInput.remove() if the element should NOT be added to the left list--these 2 calls are equivalent in this case.
- ElementSyncInput.useValue(L) if an element should be added to the left list.
- common(ElementSyncInput<L, R>). This is called when an element in the left list has an equivalent in the right list. The result should be a result of:
- ElementSyncInput.preserve() if the element should remain in the collection without changing its value (and without calling a set method on that element).
- ElementSyncInput.remove() if the element should be removed from the collection
- ElementSyncInput.useValue(L) if the element should be changed or updated (i.e. a set method should be called on that element with the given value).
Much of the time, the purpose of an adjustment operation is simple: to cause every element in the right list to have an equivalent element in the left list. To accommodate this typical case and some variations of it, the simple adjustment API is provided. CollectionAdjustment.simple(Function) accepts a function that converts values from the right list to values in the left (vice versa is not required, as the right list cannot be modified by this feature). It returns a SimpleAdjustment. Calling adjust() on this class without any additional configuration will remove all left-only elements, add the mapped value for all right-only elements, and adjust common elements with the mapped value for the element in the right list.
This class has many methods to configure its behavior:
- withAdd(boolean). If called with false, values from the right list with no equivalent in the left list will be ignored.
- withRemove(boolean). If called with false, values from the left list with no equivalent in the right list will be preserved untouched.
- commonUses(boolean left, boolean update). For each element that is common between the lists, if the first boolean is false the value mapped from the right element will be used (default behavior). The update boolean is hether to update the left value when the map of the right value is identical to the left value (if left==false) or always (if left==true)
- commonUses(boolean, BiPredicate<L, R>). Same as common(boolean, boolean), but if the first boolean is true, the predicate will be used to determine whether a set method is called.
- removeCommon(). Will remove common elements from the left list.
- leftFirst(boolean). Will cause left-only elements to be handled before similarly-positioned right-only elements (true) or vice-versa (false).
- withElementCompare(Comparator<? super L>). Affects whether left-only elements are handled before or after similarly-positioned right-only elements. If the comparator returns a value <=0, the left value will be handled first.
- commonUsesLeft(), commonUsesRight(), commonUsesLeft(BiPredicate<L,R>), commonUsesRight(BiPredicate<L, R>): Variations of the commonUses() methods described above.
- updateCommon(BiFunction<L, R, L>). Changes the function used to generate values to pass to ElementSyncInput.useValue(L) for common elements. This allows the previous left value to be used, unlike the value passed to CollectionAdjustment.simple(Function).
- adjustCommon(BiConsumer<L, R>). Similar to onCommon(Consumer<ElementSyncInput<L, R>>), but only accepts the values.
- onLeft(Consumer<ElementSyncInput<L, R>>). Sets an action to be called for each left-only element encountered.
- onRight(Consumer<ElementSyncInput<L, R>>). Sets an action to be called for each right-only element encountered. Unlike the method previously invoked in CollectionSynchronizer, if the result of the operation was an addition, the added value will be available in this element.
- onCommon(Consumer<ElementSyncInput<L, R>>). Sets an action to be called for each common element encountered. If the value was changed via ElementSyncInput.useValue(L) in the synchronizer, the left value will be the new value.
- handleLeft(Function<ElementSyncInput<L, R>>, ElementSyncAction>) replaces the default functionality of the CollectionSynchronizer.leftOnly() method call.
- handleRight(Function<ElementSyncInput<L, R>>, ElementSyncAction>) replaces the default functionality of the CollectionSynchronizer.rightOnly() method call.
- handleCommon(Function<ElementSyncInput<L, R>>, ElementSyncAction>) replaces the default functionality of the CollectionSynchronizer.common() method call.
This type of adjustment satisfies most uses and is more intuitive and requires less boilerplate to configure.