Storage/src/dorkbox/storage/types/StringStore.kt

292 lines
9.0 KiB
Kotlin

/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.storage.types
import dorkbox.storage.AccessFunc
import dorkbox.storage.Storage
import dorkbox.storage.serializer.SerializerBytes
import org.slf4j.Logger
import java.io.File
import java.io.IOException
import java.util.concurrent.*
/**
* Java STRING file storage system
*/
abstract class StringStore(
val dbFile: File,
val autoLoad: Boolean,
val readOnly: Boolean,
val readOnlyViolent: Boolean,
logger: Logger,
val serializer: SerializerBytes,
val onLoad: AccessFunc,
val onSave: AccessFunc,
) : Storage(logger) {
companion object {
internal val comparator = Comparator<Any> { o1, o2 -> o1.toString().compareTo(o2.toString()) }
}
private val thread = Thread { close() }
@Volatile
private var lastModifiedTime = 0L
@Volatile
private var isDirty = false
protected val loadFunc: (key: Any, value: Any?) -> Unit
protected val saveFunc: (key: Any, value: Any?) -> Unit
private val loadedProps = ConcurrentHashMap<Any, Any?>()
init {
loadFunc = { key, value ->
loadedProps[key] = value
}
saveFunc = { key, value ->
onSave(key, value)
}
// Make sure that the timer is run on shutdown. A HARD shutdown will just POW! kill it, a "nice" shutdown will run the hook
Runtime.getRuntime().addShutdownHook(thread)
}
abstract fun onSave(key: Any, value: Any?)
abstract fun doLoad()
abstract fun doSave()
protected fun load() {
// if we cannot load, then we create a properties file.
if (!dbFile.canRead() && !dbFile.parentFile.mkdirs() && !dbFile.createNewFile()) {
throw IOException("Cannot create file")
}
try {
synchronized(dbFile) {
lastModifiedTime = dbFile.lastModified()
doLoad()
}
} catch (e: IOException) {
logger.error("Cannot load properties!", e)
}
}
private fun save(key: Any, value: Any?) {
if (readOnly) {
// don't accidentally save this!
return
}
// if we cannot save, then we create a NEW properties file. It could have been DELETED out from under us (while in use!)
if (!dbFile.canRead() && !dbFile.parentFile.mkdirs() && !dbFile.createNewFile()) {
throw IOException("Cannot create file")
}
try {
synchronized(dbFile) {
if (value != null) {
try {
onSave(serializer, key, value, saveFunc)
} catch (e: Exception) {
logger.error("Unable to parse property ($dbFile) [$key] : $value", e)
}
} else {
// value was deleted
saveFunc(key, null)
}
doSave()
lastModifiedTime = dbFile.lastModified()
}
isDirty = false
} catch (e: IOException) {
logger.error("Properties cannot save to: $dbFile", e)
}
}
private fun save() {
if (readOnly || !isDirty) {
// don't accidentally save this!
return
}
// if we cannot save, then we create a NEW properties file. It could have been DELETED out from under us (while in use!)
if (!dbFile.canRead() && !dbFile.parentFile.mkdirs() && !dbFile.createNewFile()) {
throw IOException("Cannot create file")
}
try {
synchronized(dbFile) {
loadedProps.forEach { (k, v) ->
if (k == versionTag) {
save(k, v.toString())
} else {
try {
onSave(serializer, k, v, saveFunc)
} catch (e: Exception) {
logger.error("Unable to parse property ($dbFile) [$k] : $v", e)
}
}
}
doSave()
lastModifiedTime = dbFile.lastModified()
}
} catch (e: IOException) {
logger.error("Properties cannot save to: $dbFile", e)
}
}
override fun setVersion(version: Long) {
if (!readOnly) {
// this is because `setVersion` is called internally, and we don't want to encounter errors during initialization
set(versionTag, version)
}
}
override fun file(): File {
return dbFile
}
override fun size(): Int {
return loadedProps.size
}
override fun contains(key: Any): Boolean {
if (autoLoad) {
// we want to check the last modified time when getting, because if we edit the on-disk file, we want to load those changes
val lastModifiedTime = dbFile.lastModified()
if (this.lastModifiedTime != lastModifiedTime) {
// we want to reload the info
load()
}
}
return loadedProps[key] != null
}
override operator fun <V> get(key: Any): V? {
if (autoLoad) {
// we want to check the last modified time when getting, because if we edit the on-disk file, we want to load those changes
val lastModifiedTime = dbFile.lastModified()
if (this.lastModifiedTime != lastModifiedTime) {
// we want to reload the info
load()
}
}
val any = loadedProps[key]
if (any != null) {
@Suppress("UNCHECKED_CAST")
return any as V
}
return null
}
override operator fun set(key: Any, data: Any?) {
if (readOnly) {
if (readOnlyViolent) {
throw IOException("Unable to save data in $dbFile for $key : $data")
} else {
return
}
}
val hasChanged = if (data == null) {
loadedProps.remove(key) != null
} else {
val prev = loadedProps.put(key, data)
prev !== data
}
// every time we set info, we want to save it to disk (so the file on disk will ALWAYS be current, and so we can modify it as we choose)
if (hasChanged) {
isDirty = true
save(key, data)
}
}
/**
* Deletes all contents of this storage, and if applicable, it's location on disk.
*/
override fun deleteAll() {
if (readOnly) {
if (readOnlyViolent) {
throw IOException("Unable to delete all data in $dbFile")
} else {
return
}
}
loadedProps.clear()
dbFile.delete()
}
/**
* Closes this storage (and if applicable, flushes its content to disk)
*/
override fun close() {
if (Thread.currentThread() != thread) {
try {
Runtime.getRuntime().removeShutdownHook(thread)
}
catch (ignored: Exception) { }
catch (ignored: RuntimeException) { }
}
save()
loadedProps.clear()
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is StringStore) return false
if (dbFile != other.dbFile) return false
if (autoLoad != other.autoLoad) return false
if (readOnly != other.readOnly) return false
if (readOnlyViolent != other.readOnlyViolent) return false
if (lastModifiedTime != other.lastModifiedTime) return false
if (isDirty != other.isDirty) return false
if (serializer != other.serializer) return false // class with lambda
if (onLoad != other.onLoad) return false //lambda
if (onSave != other.onSave) return false //lambda
if (loadedProps != other.loadedProps) return false
return true
}
override fun hashCode(): Int {
var result = dbFile.hashCode()
result = 31 * result + autoLoad.hashCode()
result = 31 * result + readOnly.hashCode()
result = 31 * result + readOnlyViolent.hashCode()
result = 31 * result + lastModifiedTime.hashCode()
result = 31 * result + isDirty.hashCode()
result = 31 * result + serializer.hashCode() // class with lambda
result = 31 * result + onLoad.hashCode() //lambda
result = 31 * result + onSave.hashCode() //lambda
result = 31 * result + loadedProps.hashCode()
return result
}
}