Skip to content

Commit

Permalink
doh, didn't commit files, bump ver again
Browse files Browse the repository at this point in the history
  • Loading branch information
sanity committed Jun 25, 2018
1 parent b5ee53a commit 09e09ca
Show file tree
Hide file tree
Showing 17 changed files with 1,296 additions and 1 deletion.
2 changes: 1 addition & 1 deletion build.gradle
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'
Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/io/kweb/shoebox/KeyValue.kt
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)
105 changes: 105 additions & 0 deletions src/main/kotlin/io/kweb/shoebox/OrderedViewSet.kt
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) }
}
}
175 changes: 175 additions & 0 deletions src/main/kotlin/io/kweb/shoebox/Shoebox.kt
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
}
11 changes: 11 additions & 0 deletions src/main/kotlin/io/kweb/shoebox/Store.kt
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?
}
Loading

0 comments on commit 09e09ca

Please sign in to comment.