Rearrange implementations

This commit is contained in:
Him188 2020-08-22 14:24:36 +08:00
parent 50958868cf
commit 408589b615
14 changed files with 294 additions and 253 deletions

View File

@ -16,6 +16,7 @@ import net.mamoe.mirai.console.data.PluginData.ValueNode
import net.mamoe.mirai.console.internal.data.PluginDataImpl
import net.mamoe.mirai.console.internal.data.serialName
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
import kotlin.annotation.AnnotationTarget.*
import kotlin.reflect.KProperty
/**
@ -54,4 +55,29 @@ public abstract class AbstractPluginData : PluginData, PluginDataImpl() {
* 当所属于这个 [PluginData] [Value] [][Value.value] 被修改时被调用.
*/
public abstract override fun onValueChanged(value: Value<*>)
}
}
/**
* [PluginConfig] 的默认实现.
*
* 支持所有 [PluginData] 支持的功能, 支持通过 UI
*
* @see PluginConfig
*/
@ExperimentalPluginConfig
public abstract class AbstractPluginConfig : AbstractPluginData(), PluginConfig
/**
* 标记实验性的 [PluginConfig] API.
*
* @see ConsoleExperimentalAPI
*/
@ConsoleExperimentalAPI
@Retention(AnnotationRetention.BINARY)
@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
@Target(CLASS, TYPEALIAS, FUNCTION, PROPERTY, FIELD, CONSTRUCTOR)
@MustBeDocumented
public annotation class ExperimentalPluginConfig(
val message: String = ""
)

View File

@ -14,4 +14,5 @@ package net.mamoe.mirai.console.data
*
* 用户可通过 mirai-console 前端修改这些配置, 修改会自动写入这个对象中.
*/
@ExperimentalPluginConfig
public interface PluginConfig : PluginData

View File

@ -0,0 +1,66 @@
package net.mamoe.mirai.console.internal.data
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import net.mamoe.mirai.console.data.*
import net.mamoe.mirai.console.internal.plugin.updateWhen
import net.mamoe.mirai.console.util.ConsoleInternalAPI
import net.mamoe.mirai.utils.currentTimeMillis
import kotlin.reflect.KClass
/**
* 链接自动保存的 [PluginData].
* 当任一相关 [Value] 的值被修改时, 将在一段时间无其他修改时保存
*
* [AutoSavePluginDataHolder.coroutineContext] 含有 [Job], [AutoSavePluginData] 会通过 [Job.invokeOnCompletion] Job 完结时触发自动保存.
*
* @see loadPluginData
*/
internal open class AutoSavePluginData(
private val owner: AutoSavePluginDataHolder,
internal val originPluginDataClass: KClass<out PluginData>
) :
AbstractPluginData(), PluginConfig {
private lateinit var storage: PluginDataStorage
override fun setStorage(storage: PluginDataStorage) {
check(!this::storage.isInitialized) { "storage is already initialized" }
this.storage = storage
}
@JvmField
@Volatile
internal var lastAutoSaveJob: Job? = null
@JvmField
@Volatile
internal var currentFirstStartTime = atomic(0L)
init {
@OptIn(InternalCoroutinesApi::class)
owner.coroutineContext[Job]?.invokeOnCompletion(true) { doSave() }
}
private val updaterBlock: suspend CoroutineScope.() -> Unit = {
currentFirstStartTime.updateWhen({ it == 0L }, { currentTimeMillis })
delay(owner.autoSaveIntervalMillis.first.coerceAtLeast(1000)) // for safety
if (lastAutoSaveJob == this.coroutineContext[Job]) {
doSave()
} else {
if (currentFirstStartTime.updateWhen(
{ currentTimeMillis - it >= owner.autoSaveIntervalMillis.last },
{ 0 })
) doSave()
}
}
@Suppress("RedundantVisibilityModifier")
@ConsoleInternalAPI
public final override fun onValueChanged(value: Value<*>) {
lastAutoSaveJob = owner.launch(block = updaterBlock)
}
private fun doSave() = storage.store(owner, this)
}

View File

@ -0,0 +1,44 @@
package net.mamoe.mirai.console.internal.data
import net.mamoe.mirai.console.data.*
import net.mamoe.mirai.console.util.ConsoleInternalAPI
internal class MemoryPluginDataStorageImpl(
private val onChanged: MemoryPluginDataStorage.OnChangedCallback
) : PluginDataStorage, MemoryPluginDataStorage,
MutableMap<Class<out PluginData>, PluginData> by mutableMapOf() {
internal inner class MemoryPluginDataImpl : AbstractPluginData() {
@ConsoleInternalAPI
override fun onValueChanged(value: Value<*>) {
onChanged.onChanged(this@MemoryPluginDataStorageImpl, value)
}
override fun setStorage(storage: PluginDataStorage) {
check(storage is MemoryPluginDataStorageImpl) { "storage is not MemoryPluginDataStorageImpl" }
}
}
@Suppress("UNCHECKED_CAST")
override fun <T : PluginData> load(holder: PluginDataHolder, dataClass: Class<T>): T = (synchronized(this) {
this.getOrPut(dataClass) {
dataClass.kotlin.run {
objectInstance ?: createInstanceOrNull() ?: kotlin.run {
if (dataClass != PluginData::class.java) {
throw IllegalArgumentException(
"Cannot create PluginData instance. Make sure dataClass is PluginData::class.java or a Kotlin's object, " +
"or has a constructor which either has no parameters or all parameters of which are optional"
)
}
MemoryPluginDataImpl()
}
}
}
} as T).also { it.setStorage(this) }
override fun store(holder: PluginDataHolder, pluginData: PluginData) {
synchronized(this) {
this[pluginData::class.java] = pluginData
}
}
}

View File

@ -0,0 +1,72 @@
package net.mamoe.mirai.console.internal.data
import net.mamoe.mirai.console.data.*
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
import net.mamoe.yamlkt.Yaml
import java.io.File
import kotlin.reflect.KClass
@Suppress("RedundantVisibilityModifier") // might be public in the future
internal open class MultiFilePluginDataStorageImpl(
public final override val directory: File
) : PluginDataStorage, MultiFilePluginDataStorage {
init {
directory.mkdir()
}
public override fun <T : PluginData> load(holder: PluginDataHolder, dataClass: Class<T>): T =
with(dataClass.kotlin) {
@Suppress("UNCHECKED_CAST")
val instance = objectInstance ?: this.createInstanceOrNull() ?: kotlin.run {
require(dataClass == PluginData::class.java) {
"Cannot create PluginData instance. Make sure dataClass is PluginData::class.java or a Kotlin's object, " +
"or has a constructor which either has no parameters or all parameters of which are optional"
}
if (holder is AutoSavePluginDataHolder) {
AutoSavePluginData(holder, this) as T?
} else null
} ?: throw IllegalArgumentException(
"Cannot create PluginData instance. Make sure 'holder' is a AutoSavePluginDataHolder, " +
"or 'data' is an object or has a constructor which either has no parameters or all parameters of which are optional"
)
val file = getPluginDataFile(holder, this)
file.createNewFile()
check(file.exists() && file.isFile && file.canRead()) { "${file.absolutePath} cannot be read" }
val text = file.readText()
if (text.isNotBlank()) {
Yaml.default.decodeFromString(instance.updaterSerializer, file.readText())
}
instance
}.also { it.setStorage(this) }
protected open fun getPluginDataFile(holder: PluginDataHolder, clazz: KClass<*>): File = with(clazz) {
val name = findASerialName()
val dir = File(directory, holder.name)
if (dir.isFile) {
error("Target directory ${dir.path} for holder $holder is occupied by a file therefore data $qualifiedNameOrTip can't be saved.")
}
dir.mkdir()
val file = File(directory, name)
if (file.isDirectory) {
error("Target file $file is occupied by a directory therefore data $qualifiedNameOrTip can't be saved.")
}
return file
}
@ConsoleExperimentalAPI
public override fun store(holder: PluginDataHolder, pluginData: PluginData) {
val file =
getPluginDataFile(
holder,
if (pluginData is AutoSavePluginData) pluginData.originPluginDataClass else pluginData::class
)
if (file.exists() && file.isFile && file.canRead()) {
file.writeText(Yaml.default.encodeToString(pluginData.updaterSerializer, Unit))
}
}
}

View File

@ -12,7 +12,6 @@
package net.mamoe.mirai.console.internal.data
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.builtins.MapSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
@ -23,10 +22,6 @@ import net.mamoe.mirai.console.data.PluginData
import net.mamoe.mirai.console.data.PluginData.ValueNode
import net.mamoe.mirai.console.data.Value
import net.mamoe.yamlkt.YamlNullableDynamicSerializer
import kotlin.reflect.KProperty
import kotlin.reflect.full.findAnnotation
internal val KProperty<*>.serialName: String get() = this.findAnnotation<SerialName>()?.value ?: this.name
/**
* Internal implementation for [PluginData] including:

View File

@ -9,193 +9,3 @@
package net.mamoe.mirai.console.internal.data
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import net.mamoe.mirai.console.data.*
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import net.mamoe.mirai.console.internal.plugin.updateWhen
import net.mamoe.mirai.console.plugin.jvm.loadPluginData
import net.mamoe.mirai.console.util.ConsoleExperimentalAPI
import net.mamoe.mirai.console.util.ConsoleInternalAPI
import net.mamoe.mirai.utils.currentTimeMillis
import net.mamoe.yamlkt.Yaml
import java.io.File
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.full.findAnnotation
/**
* 链接自动保存的 [PluginData].
* 当任一相关 [Value] 的值被修改时, 将在一段时间无其他修改时保存
*
* [AutoSavePluginDataHolder.coroutineContext] 含有 [Job], [AutoSavePluginData] 会通过 [Job.invokeOnCompletion] Job 完结时触发自动保存.
*
* @see loadPluginData
*/
internal open class AutoSavePluginData(
private val owner: AutoSavePluginDataHolder,
internal val originPluginDataClass: KClass<out PluginData>
) :
AbstractPluginData() {
private lateinit var storage: PluginDataStorage
override fun setStorage(storage: PluginDataStorage) {
check(!this::storage.isInitialized) { "storage is already initialized" }
this.storage = storage
}
@JvmField
@Volatile
internal var lastAutoSaveJob: Job? = null
@JvmField
@Volatile
internal var currentFirstStartTime = atomic(0L)
init {
@OptIn(InternalCoroutinesApi::class)
owner.coroutineContext[Job]?.invokeOnCompletion(true) { doSave() }
}
private val updaterBlock: suspend CoroutineScope.() -> Unit = {
currentFirstStartTime.updateWhen({ it == 0L }, { currentTimeMillis })
delay(owner.autoSaveIntervalMillis.first.coerceAtLeast(1000)) // for safety
if (lastAutoSaveJob == this.coroutineContext[Job]) {
doSave()
} else {
if (currentFirstStartTime.updateWhen(
{ currentTimeMillis - it >= owner.autoSaveIntervalMillis.last },
{ 0 })
) doSave()
}
}
@Suppress("RedundantVisibilityModifier")
@ConsoleInternalAPI
public final override fun onValueChanged(value: Value<*>) {
lastAutoSaveJob = owner.launch(block = updaterBlock)
}
private fun doSave() = storage.store(owner, this)
}
internal class MemoryPluginDataStorageImpl(
private val onChanged: MemoryPluginDataStorage.OnChangedCallback
) : PluginDataStorage, MemoryPluginDataStorage,
MutableMap<Class<out PluginData>, PluginData> by mutableMapOf() {
internal inner class MemoryPluginDataImpl : AbstractPluginData() {
@ConsoleInternalAPI
override fun onValueChanged(value: Value<*>) {
onChanged.onChanged(this@MemoryPluginDataStorageImpl, value)
}
override fun setStorage(storage: PluginDataStorage) {
check(storage is MemoryPluginDataStorageImpl) { "storage is not MemoryPluginDataStorageImpl" }
}
}
@Suppress("UNCHECKED_CAST")
override fun <T : PluginData> load(holder: PluginDataHolder, dataClass: Class<T>): T = (synchronized(this) {
this.getOrPut(dataClass) {
dataClass.kotlin.run {
objectInstance ?: createInstanceOrNull() ?: kotlin.run {
if (dataClass != PluginData::class.java) {
throw IllegalArgumentException(
"Cannot create PluginData instance. Make sure dataClass is PluginData::class.java or a Kotlin's object, " +
"or has a constructor which either has no parameters or all parameters of which are optional"
)
}
MemoryPluginDataImpl()
}
}
}
} as T).also { it.setStorage(this) }
override fun store(holder: PluginDataHolder, pluginData: PluginData) {
synchronized(this) {
this[pluginData::class.java] = pluginData
}
}
}
@Suppress("RedundantVisibilityModifier") // might be public in the future
internal open class MultiFilePluginDataStorageImpl(
public final override val directory: File
) : PluginDataStorage, MultiFilePluginDataStorage {
init {
directory.mkdir()
}
public override fun <T : PluginData> load(holder: PluginDataHolder, dataClass: Class<T>): T =
with(dataClass.kotlin) {
@Suppress("UNCHECKED_CAST")
val instance = objectInstance ?: this.createInstanceOrNull() ?: kotlin.run {
require(dataClass == PluginData::class.java) {
"Cannot create PluginData instance. Make sure dataClass is PluginData::class.java or a Kotlin's object, " +
"or has a constructor which either has no parameters or all parameters of which are optional"
}
if (holder is AutoSavePluginDataHolder) {
AutoSavePluginData(holder, this) as T?
} else null
} ?: throw IllegalArgumentException(
"Cannot create PluginData instance. Make sure 'holder' is a AutoSavePluginDataHolder, " +
"or 'data' is an object or has a constructor which either has no parameters or all parameters of which are optional"
)
val file = getPluginDataFile(holder, this)
file.createNewFile()
check(file.exists() && file.isFile && file.canRead()) { "${file.absolutePath} cannot be read" }
val text = file.readText()
if (text.isNotBlank()) {
Yaml.default.decodeFromString(instance.updaterSerializer, file.readText())
}
instance
}.also { it.setStorage(this) }
protected open fun getPluginDataFile(holder: PluginDataHolder, clazz: KClass<*>): File = with(clazz) {
val name = findASerialName()
val dir = File(directory, holder.name)
if (dir.isFile) {
error("Target directory ${dir.path} for holder $holder is occupied by a file therefore data $qualifiedNameOrTip can't be saved.")
}
dir.mkdir()
val file = File(directory, name)
if (file.isDirectory) {
error("Target file $file is occupied by a directory therefore data $qualifiedNameOrTip can't be saved.")
}
return file
}
@ConsoleExperimentalAPI
public override fun store(holder: PluginDataHolder, pluginData: PluginData) {
val file =
getPluginDataFile(
holder,
if (pluginData is AutoSavePluginData) pluginData.originPluginDataClass else pluginData::class
)
if (file.exists() && file.isFile && file.canRead()) {
file.writeText(Yaml.default.encodeToString(pluginData.updaterSerializer, Unit))
}
}
}
@JvmSynthetic
internal fun <T : Any> KClass<T>.createInstanceOrNull(): T? {
val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) }
?: return null
return noArgsConstructor.callBy(emptyMap())
}
@JvmSynthetic
internal fun KClass<*>.findASerialName(): String =
findAnnotation<SerialName>()?.value
?: qualifiedName
?: throw IllegalArgumentException("Cannot find a serial name for $this")

View File

@ -17,7 +17,7 @@ import kotlinx.serialization.encoding.Encoder
import net.mamoe.mirai.console.data.*
/**
* The super class to all ValueImpl s
* The super class to all ValueImpl
*/
internal abstract class AbstractValueImpl<T> : Value<T> {
open fun setValueBySerializer(value: T) {

View File

@ -9,10 +9,14 @@
package net.mamoe.mirai.console.internal.data
import kotlinx.serialization.SerialName
import net.mamoe.mirai.console.data.PluginData
import net.mamoe.mirai.console.internal.command.qualifiedNameOrTip
import kotlin.reflect.KClass
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty
import kotlin.reflect.KType
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.isSubclassOf
@Suppress("UNCHECKED_CAST")
@ -42,3 +46,33 @@ internal inline fun <reified T : PluginData> newPluginDataInstanceUsingReflectio
)
}
}
private fun isReferenceArray(rootClass: KClass<Any>): Boolean = rootClass.java.isArray
@Suppress("UNCHECKED_CAST")
private fun KType.kclass() = when (val t = classifier) {
is KClass<*> -> t
else -> error("Only KClass supported as classifier, got $t")
} as KClass<Any>
@JvmSynthetic
internal fun <T : Any> KClass<T>.createInstanceOrNull(): T? {
val noArgsConstructor = constructors.singleOrNull { it.parameters.all(KParameter::isOptional) }
?: return null
return noArgsConstructor.callBy(emptyMap())
}
@JvmSynthetic
internal fun KClass<*>.findASerialName(): String =
findAnnotation<SerialName>()?.value
?: qualifiedName
?: throw IllegalArgumentException("Cannot find a serial name for $this")
internal val KProperty<*>.serialNameOrPropertyName: String get() = this.findAnnotation<SerialName>()?.value ?: this.name
internal fun Int.isOdd() = this and 0b1 != 0
internal val KProperty<*>.serialName: String get() = this.findAnnotation<SerialName>()?.value ?: this.name

View File

@ -13,6 +13,9 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.*
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.serializer
import net.mamoe.yamlkt.YamlDynamicSerializer
import net.mamoe.yamlkt.YamlNullableDynamicSerializer
@ -129,10 +132,27 @@ private fun <T : Any> findObjectSerializer(jClass: Class<T>): KSerializer<T>? {
return result as? KSerializer<T>
}
private fun isReferenceArray(rootClass: KClass<Any>): Boolean = rootClass.java.isArray
internal inline fun <E> KSerializer<E>.bind(
crossinline setter: (E) -> Unit,
crossinline getter: () -> E
): KSerializer<E> {
return object : KSerializer<E> {
override val descriptor: SerialDescriptor get() = this@bind.descriptor
override fun deserialize(decoder: Decoder): E = this@bind.deserialize(decoder).also { setter(it) }
@Suppress("UNCHECKED_CAST")
private fun KType.kclass() = when (val t = classifier) {
is KClass<*> -> t
else -> error("Only KClass supported as classifier, got $t")
} as KClass<Any>
@Suppress("UNCHECKED_CAST")
override fun serialize(encoder: Encoder, value: E) =
this@bind.serialize(encoder, getter())
}
}
internal inline fun <E, R> KSerializer<E>.map(
crossinline serializer: (R) -> E,
crossinline deserializer: (E) -> R
): KSerializer<R> {
return object : KSerializer<R> {
override val descriptor: SerialDescriptor get() = this@map.descriptor
override fun deserialize(decoder: Decoder): R = this@map.deserialize(decoder).let(deserializer)
override fun serialize(encoder: Encoder, value: R) = this@map.serialize(encoder, value.let(serializer))
}
}

View File

@ -1,47 +0,0 @@
/*
* Copyright 2019-2020 Mamoe Technologies and contributors.
*
* 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 with Mamoe Exceptions 许可证的约束, 可以在以下链接找到该许可证.
* Use of this source code is governed by the GNU AFFERO GENERAL PUBLIC LICENSE version 3 with Mamoe Exceptions license that can be found via the following link.
*
* https://github.com/mamoe/mirai/blob/master/LICENSE
*/
package net.mamoe.mirai.console.internal.data
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlin.reflect.KProperty
import kotlin.reflect.full.findAnnotation
internal val KProperty<*>.serialNameOrPropertyName: String get() = this.findAnnotation<SerialName>()?.value ?: this.name
internal fun Int.isOdd() = this and 0b1 != 0
internal inline fun <E> KSerializer<E>.bind(
crossinline setter: (E) -> Unit,
crossinline getter: () -> E
): KSerializer<E> {
return object : KSerializer<E> {
override val descriptor: SerialDescriptor get() = this@bind.descriptor
override fun deserialize(decoder: Decoder): E = this@bind.deserialize(decoder).also { setter(it) }
@Suppress("UNCHECKED_CAST")
override fun serialize(encoder: Encoder, value: E) =
this@bind.serialize(encoder, getter())
}
}
internal inline fun <E, R> KSerializer<E>.map(
crossinline serializer: (R) -> E,
crossinline deserializer: (E) -> R
): KSerializer<R> {
return object : KSerializer<R> {
override val descriptor: SerialDescriptor get() = this@map.descriptor
override fun deserialize(decoder: Decoder): R = this@map.deserialize(decoder).let(deserializer)
override fun serialize(encoder: Encoder, value: R) = this@map.serialize(encoder, value.let(serializer))
}
}

View File

@ -13,6 +13,7 @@ package net.mamoe.mirai.console.plugin.jvm
import kotlinx.coroutines.CoroutineScope
import net.mamoe.mirai.console.data.AutoSavePluginDataHolder
import net.mamoe.mirai.console.data.PluginConfig
import net.mamoe.mirai.console.data.PluginData
import net.mamoe.mirai.console.plugin.Plugin
import net.mamoe.mirai.console.plugin.PluginFileExtensions
@ -54,6 +55,12 @@ public interface JvmPlugin : Plugin, CoroutineScope,
@JvmDefault
public fun <T : PluginData> loadPluginData(clazz: Class<T>): T = loader.dataStorage.load(this, clazz)
/**
* [JarPluginLoader.dataStorage] 获取一个 [PluginData] 实例
*/
@JvmDefault
public fun <T : PluginConfig> loadPluginConfig(clazz: Class<T>): T = loader.dataStorage.load(this, clazz)
/**
* 在插件被加载时调用. 只会被调用一次.
*/
@ -76,13 +83,26 @@ public interface JvmPlugin : Plugin, CoroutineScope,
}
}
/**
* 读取一个插件数据.
*/
@JvmSynthetic
public inline fun <T : PluginData> JvmPlugin.loadPluginData(clazz: KClass<T>): T = this.loadPluginData(clazz.java)
/**
* 读取一个插件数据.
*
* 插件数据
*/
@JvmSynthetic
public inline fun <reified T : PluginData> JvmPlugin.loadPluginData(): T = this.loadPluginData(T::class)
public inline fun <reified T : PluginData> JvmPlugin.loadPluginData(): T = this.loadPluginData(T::class)
/**
* 读取一个插件配置.
*/
@JvmSynthetic
public inline fun <T : PluginConfig> JvmPlugin.loadPluginConfig(clazz: KClass<T>): T = this.loadPluginConfig(clazz.java)
/**
* 读取一个插件配置.
*/
@JvmSynthetic
public inline fun <reified T : PluginConfig> JvmPlugin.loadPluginConfig(): T = this.loadPluginConfig(T::class)