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

229 lines
6.8 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.json.JsonException
import dorkbox.json.OutputType
import dorkbox.storage.AccessFunc
import dorkbox.storage.Storage
import org.slf4j.Logger
import java.io.File
import java.io.IOException
import java.util.concurrent.*
/**
* JSON property file storage system
*/
class JsonStore(
val dbFile: File,
val autoLoad: Boolean,
val readOnly: Boolean,
val readOnlyViolent: Boolean,
logger: Logger,
onLoad: AccessFunc,
onSave: AccessFunc,
) : Storage(logger) {
companion object {
private val comparator = Comparator<Any> { o1, o2 -> o1.toString().compareTo(o2.toString()) }
}
private var version: Long = 0
private val thread = Thread { close() }
private val json = dorkbox.json.Json()
@Volatile
private var lastModifiedTime = 0L
private val loadedProps = ConcurrentHashMap<Any, Any?>()
init {
json.outputType = OutputType.json
load()
val versionInfo = loadedProps[versionTag]
if (versionInfo !is Number) {
loadedProps[versionTag] = version
} else {
setVersion(versionInfo as Long)
}
// 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)
init("Property file storage initialized at: '$dbFile'")
}
private 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) {
@Suppress("UNCHECKED_CAST")
val map = json.fromJson(HashMap::class.java, dbFile) as Map<Any, Any?>?
map?.forEach { (k,v) ->
loadedProps[k] = v
}
lastModifiedTime = dbFile.lastModified()
}
} catch (e: JsonException) {
logger.error("Cannot load JSON file!", e)
} catch (e: IOException) {
logger.error("Cannot load JSON file!", e)
}
}
private fun save() {
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) {
val data = json.toJson(loadedProps)
val pretty = json.prettyPrint(data)
dbFile.writeText(pretty)
lastModifiedTime = dbFile.lastModified()
}
} catch (e: JsonException) {
logger.error("JSON cannot save to: $dbFile", e)
} catch (e: IOException) {
logger.error("JSON cannot save to: $dbFile", e)
}
}
override fun setVersion(version: Long) {
this.version = version
}
override fun file(): File {
return dbFile
}
override fun size(): Int {
return loadedProps.size
}
override fun contains(key: Any): Boolean {
require(key is String) {
"Keys for a json file must be a String"
}
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? {
require(key is String) {
"Keys for a json file must be a String"
}
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?) {
require(key is String) {
"Keys for a json file must be a String"
}
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) {
save()
}
}
/**
* 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 it's content to disk)
*/
override fun close() {
if (Thread.currentThread() != thread) {
try {
Runtime.getRuntime().removeShutdownHook(thread)
}
catch (ignored: Exception) { }
catch (ignored: RuntimeException) { }
}
save()
}
}