-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
doh, didn't commit files, bump ver again
- Loading branch information
Showing
17 changed files
with
1,296 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
group 'io.kweb' | ||
version '0.2.15' | ||
version '0.2.16' | ||
|
||
buildscript { | ||
ext.kotlin_version = '1.2.50' | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package io.kweb.shoebox | ||
|
||
data class KeyValue<V>(val key: String, val value: V) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package io.kweb.shoebox | ||
|
||
import io.kweb.shoebox.BinarySearchResult.* | ||
import java.util.concurrent.ConcurrentHashMap | ||
|
||
/** | ||
* Created by ian on 3/14/17. | ||
*/ | ||
class OrderedViewSet<T : Any>(val view : View<T>, val viewKey : String, val comparator: Comparator<T>) { | ||
|
||
private val orderedList : MutableList<KeyValue<T>> | ||
private val modificationHandlers = ConcurrentHashMap<String, Long>() | ||
private val additionHandle: Long | ||
private val removalHandle: Long | ||
|
||
init { | ||
val ol = ArrayList<KeyValue<T>>() | ||
val kvComparator : Comparator<KeyValue<T>> = Comparator<KeyValue<T>> { o1, o2 -> comparator.compare(o1.value, o2.value) }.thenBy(KeyValue<T>::key) | ||
ol.addAll(view.getKeyValues(viewKey)) | ||
ol.sortWith(kvComparator) | ||
orderedList = ol | ||
additionHandle = view.onAdd(viewKey) { keyValue -> | ||
val binarySearchResult = orderedList.betterBinarySearch(keyValue, kvComparator) | ||
val insertionPoint: Int = when (binarySearchResult) { | ||
is Exact -> { | ||
throw RuntimeException("Listener called for key/value already in list keyValue: $keyValue orderedList[${binarySearchResult.index}] = ${orderedList[binarySearchResult.index]}") | ||
} | ||
is Between -> binarySearchResult.highIndex | ||
} | ||
ol.add(insertionPoint, keyValue) | ||
insertListeners.values.forEach { it(insertionPoint, keyValue) } | ||
} | ||
|
||
removalHandle = view.onRemove(viewKey) { keyValue -> | ||
if (keyValue.value != null) { | ||
val binarySearchResult = orderedList.betterBinarySearch(keyValue as KeyValue<T>, kvComparator) | ||
when (binarySearchResult) { | ||
is Exact -> { | ||
removeListeners.values.forEach { it(binarySearchResult.index, keyValue) } | ||
orderedList.removeAt(binarySearchResult.index) | ||
} | ||
is Between -> throw RuntimeException("remove listener called for unknown value") | ||
} | ||
} else { | ||
// On very rare occasions the View callback doesn't supply the value that was removed, in this case | ||
// there isn't much we can do, so just ignore it | ||
} | ||
} | ||
|
||
ol.forEach { kv -> | ||
modificationHandlers.put(kv.key, view.viewOf.onChange(kv.key) {oldValue, newValue, _ -> | ||
if (comparator.compare(oldValue, newValue) != 0) { | ||
val newKeyValue = KeyValue(kv.key, newValue) | ||
val insertPoint = orderedList.betterBinarySearch(newKeyValue, kvComparator) | ||
val insertionIndex: Int = when (insertPoint) { | ||
is Exact -> throw RuntimeException("Object modified to same value as an existing object ($newValue)") | ||
is Between -> insertPoint.highIndex | ||
} | ||
insertListeners.values.forEach { it(insertionIndex, newKeyValue) } | ||
|
||
val oldKeyValue = KeyValue(kv.key, oldValue) | ||
val removePoint = orderedList.betterBinarySearch(oldKeyValue, kvComparator) | ||
val removalIndex = when (removePoint) { | ||
is Exact -> removePoint.index | ||
is Between -> throw RuntimeException("Object modified from an unknown value ($oldValue)") | ||
} | ||
removeListeners.values.forEach { it(removalIndex, oldKeyValue) } | ||
} | ||
}) | ||
} | ||
} | ||
|
||
private val insertListeners = ConcurrentHashMap<Long, (Int, KeyValue<T>) -> Unit>() | ||
private val removeListeners = ConcurrentHashMap<Long, (Int, KeyValue<T>) -> Unit>() | ||
|
||
val entries : List<T> get() = keyValueEntries.map(KeyValue<T>::value) | ||
|
||
val keyValueEntries : List<KeyValue<T>> = orderedList | ||
|
||
fun onInsert(listener : (Int, KeyValue<T>) -> Unit) : Long { | ||
val handle = listenerHandleSource.incrementAndGet() | ||
insertListeners.put(handle, listener) | ||
return handle | ||
} | ||
|
||
fun deleteInsertListener(handle : Long) { | ||
insertListeners.remove(handle) | ||
} | ||
|
||
fun onRemove(listener : (Int, KeyValue<T>) -> Unit) : Long { | ||
val handle = listenerHandleSource.incrementAndGet() | ||
removeListeners.put(handle, listener) | ||
return handle | ||
} | ||
|
||
fun deleteRemoveListener(handle : Long) { | ||
removeListeners.remove(handle) | ||
} | ||
|
||
protected fun finalize() { | ||
view.deleteAddListener(viewKey, additionHandle) | ||
view.deleteRemoveListener(viewKey, removalHandle) | ||
modificationHandlers.forEach { key, handler -> view.viewOf.deleteChangeListener(key, handler) } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
package io.kweb.shoebox | ||
|
||
import io.kweb.shoebox.Source.LOCAL | ||
import io.kweb.shoebox.View.* | ||
import io.kweb.shoebox.View.VerifyBehavior.BLOCKING_VERIFY | ||
import io.kweb.shoebox.stores.* | ||
import java.nio.file.Path | ||
import java.util.concurrent.ConcurrentHashMap | ||
import kotlin.reflect.KClass | ||
|
||
|
||
/* | ||
* TODO: 1) Add a lockfile mechanism to prevent multiple JVMs or threads from | ||
* TODO: using the same directory | ||
* TODO: 2) Handle changes that occur to the filesystem which aren't initiated here | ||
* TODO: (then remove the previous lockfile mechanism) | ||
*/ | ||
|
||
/** | ||
* Create a [Shoebox], use this in preference to the Shoebox constructor to avoid having to provide a `KClass` | ||
* | ||
* @param T The type of the objects to store, these must be serializable with [Gson](https://github.com/google/gson), | ||
* | ||
* @param directory The path to a directory in which data will be stored, will be created if it doesn't already exist | ||
* | ||
* @sample com.github.sanity.shoebox.samples.basic usage sample | ||
**/ | ||
inline fun <reified T : Any> Shoebox(store : Store<T>) = Shoebox(store, T::class) | ||
inline fun <reified T : Any> Shoebox(dir : Path) = Shoebox(DirectoryStore(dir), T::class) | ||
inline fun <reified T : Any> Shoebox() = Shoebox(MemoryStore(), T::class) | ||
|
||
|
||
/** | ||
* Can persistently store and retrieve objects, and notify listeners of changes to those objects | ||
* | ||
* @constructor You probably want to use `Shoebox<T>(directory)` instead | ||
* @param T The type of the objects to store, these must be serializable with [Gson](https://github.com/google/gson), | ||
* @param directory The path to a directory in which data will be stored, will be created if it doesn't already exist | ||
* @param kc The KClass associated with T. To avoid having to provide this use `Shoebox<T>(directory)` | ||
*/ | ||
class Shoebox<T : Any>(val store: Store<T>, private val kc: KClass<T>) { | ||
|
||
private val keySpecificChangeListeners = ConcurrentHashMap<String, ConcurrentHashMap<Long, (T, T, Source) -> Unit>>() | ||
private val newListeners = ConcurrentHashMap<Long, (KeyValue<T>, Source) -> Unit>() | ||
private val removeListeners = ConcurrentHashMap<Long, (KeyValue<T>, Source) -> Unit>() | ||
private val changeListeners = ConcurrentHashMap<Long, (T, KeyValue<T>, Source) -> Unit>() | ||
|
||
/** | ||
* Retrieve a value, similar to [Map.get] | ||
* | ||
* @param key The key associated with the desired value | ||
* @return The value associated with the key, or null if no value is associated | ||
*/ | ||
operator fun get(key: String): T? { | ||
return store.get(key) | ||
} | ||
|
||
/** | ||
* Remove a key-value pair | ||
* | ||
* @param key The key associated with the value to be removed, similar to [MutableMap.remove] | ||
*/ | ||
fun remove(key: String) : T? { | ||
val removed = store.remove(key) | ||
if (removed != null) { | ||
removeListeners.values.forEach { it.invoke(KeyValue(key, removed), LOCAL) } | ||
} | ||
return removed | ||
} | ||
|
||
/** | ||
* Set or change a value, simliar to [MutableMap.set] | ||
* | ||
* @param key The key associated with the value to be set or changed | ||
* @param value The new value | ||
*/ | ||
operator fun set(key: String, value: T) { | ||
val previousValue = store.set(key, value) | ||
if (previousValue == null) { | ||
newListeners.values.forEach { l -> l(KeyValue(key, value), LOCAL) } | ||
} else if (value != previousValue) { | ||
changeListeners.values.forEach { cl -> cl(previousValue, KeyValue(key, value), LOCAL) } | ||
keySpecificChangeListeners[key]?.values?.forEach { l -> l(previousValue, value, LOCAL) } | ||
} | ||
} | ||
|
||
/** | ||
* A utility method to make it easier to modify an existing item | ||
*/ | ||
fun modify(key : String, modifier : (T) -> T) : Boolean { | ||
val oldValue = this[key] | ||
return if (oldValue == null) { | ||
false | ||
} else { | ||
this[key] = modifier(oldValue) | ||
true | ||
} | ||
} | ||
|
||
val entries get() = store.entries | ||
|
||
/** | ||
* Add a listener for when a new key-value pair are added to the Shoebox | ||
* | ||
* @param listener The listener to be called | ||
*/ | ||
fun onNew(listener: (KeyValue<T>, Source) -> Unit) : Long { | ||
val handle = listenerHandleSource.incrementAndGet() | ||
newListeners.put(handle, listener) | ||
return handle | ||
} | ||
|
||
fun deleteNewListener(handle : Long) { | ||
newListeners.remove(handle) | ||
} | ||
|
||
fun onRemove(listener: (KeyValue<T>, Source) -> Unit) : Long { | ||
val handle = listenerHandleSource.incrementAndGet() | ||
removeListeners.put(handle, listener) | ||
return handle | ||
} | ||
|
||
fun deleteRemoveListener(handle : Long) { | ||
removeListeners.remove(handle) | ||
} | ||
|
||
fun onChange(listener: (T, KeyValue<T>, Source) -> Unit) : Long { | ||
val handle = listenerHandleSource.incrementAndGet() | ||
changeListeners.put(handle, listener) | ||
return handle | ||
} | ||
|
||
fun onChange(key: String, listener: (T, T, Source) -> Unit) : Long { | ||
val handle = listenerHandleSource.incrementAndGet() | ||
keySpecificChangeListeners.computeIfAbsent(key, { ConcurrentHashMap() }).put(handle, listener) | ||
return handle | ||
} | ||
|
||
fun deleteChangeListener(handle : Long) { | ||
changeListeners.remove(handle) | ||
} | ||
|
||
fun deleteChangeListener(key: String, handle : Long) { | ||
keySpecificChangeListeners[key]?.let { | ||
it.remove(handle) | ||
if (it.isEmpty()) { | ||
keySpecificChangeListeners.remove(key) | ||
} | ||
} | ||
} | ||
|
||
fun view(name : String, by : (T) -> String, verify : VerifyBehavior = BLOCKING_VERIFY) : View<T> { | ||
val store = when (store) { | ||
is MemoryStore<T> -> MemoryStore<Reference>() | ||
is DirectoryStore<T> -> | ||
DirectoryStore<Reference>(store.directory.parent.resolve("${store.directory.fileName}-$name-view")) | ||
else -> throw RuntimeException("Shoebox doesn't currently support creating a view for store type ${store::class.simpleName}") | ||
} | ||
return View<T>(Shoebox(store), this, verify, by) | ||
} | ||
} | ||
|
||
/** | ||
* The source of the event that generated this change | ||
*/ | ||
enum class Source { | ||
/** | ||
* The event was due to a modification initiated by a call to this instance's [Shoebox.set] | ||
*/ | ||
LOCAL, | ||
/** | ||
* The event was due to a filesystem change external to this instance | ||
*/ | ||
REMOTE | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package io.kweb.shoebox | ||
|
||
/** | ||
* Created by ian on 3/22/17. | ||
*/ | ||
interface Store<T> { | ||
val entries: Iterable<KeyValue<T>> | ||
fun remove(key: String): T? | ||
operator fun get(key: String): T? | ||
operator fun set(key: String, value: T) : T? | ||
} |
Oops, something went wrong.