mirror of
https://github.com/hykilpikonna/AquaDX.git
synced 2026-05-09 04:31:43 -05:00
[O] Split extensions
This commit is contained in:
parent
fb5ea51d92
commit
adaa255dd3
|
|
@ -1,278 +1,156 @@
|
|||
@file:OptIn(ExperimentalStdlibApi::class)
|
||||
|
||||
package ext
|
||||
|
||||
import icu.samnyan.aqua.net.utils.ApiException
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import jakarta.persistence.Query
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.tika.Tika
|
||||
import org.apache.tika.mime.MimeTypes
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity.BodyBuilder
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.io.File
|
||||
import java.lang.reflect.Field
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Path
|
||||
import java.security.MessageDigest
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset.UTC
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.Lock
|
||||
import kotlin.reflect.KCallable
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
import kotlin.reflect.full.isSubclassOf
|
||||
import kotlin.reflect.full.memberProperties
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
typealias RP = RequestParam
|
||||
typealias RB = RequestBody
|
||||
typealias RT = RequestPart
|
||||
typealias RH = RequestHeader
|
||||
typealias PV = PathVariable
|
||||
typealias API = RequestMapping
|
||||
typealias Var<T, V> = KMutableProperty1<T, V>
|
||||
typealias Str = String
|
||||
typealias Bool = Boolean
|
||||
typealias JavaSerializable = java.io.Serializable
|
||||
|
||||
typealias JDict = Map<String, Any?>
|
||||
typealias MutJDict = MutableMap<String, Any?>
|
||||
|
||||
fun HttpServletRequest.details() = mapOf(
|
||||
"method" to method,
|
||||
"uri" to requestURI,
|
||||
"query" to queryString,
|
||||
"remote" to remoteAddr,
|
||||
"headers" to headerNames.asSequence().associateWith { getHeader(it) }
|
||||
)
|
||||
|
||||
fun HttpServletResponse.details() = mapOf(
|
||||
"status" to status,
|
||||
"headers" to headerNames.asSequence().associateWith { getHeader(it) },
|
||||
)
|
||||
|
||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.PROPERTY_GETTER)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Doc(
|
||||
val desc: String,
|
||||
val ret: String = ""
|
||||
)
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class SettingField(
|
||||
val game: String
|
||||
)
|
||||
|
||||
// Reflection
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> KClass<T>.ownVars() = declaredMemberProperties.sortedBy { it.javaField?.declaringClass?.declaredFields?.indexOf(it.javaField) ?: Int.MAX_VALUE }.mapNotNull { it as? Var<T, Any> }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> KClass<T>.vars(): List<Var<T, Any>> = supertypes.mapNotNull { it.classifier as? KClass<*> }.filter { !it.java.isInterface }.flatMap{ it.vars() as List<Var<T, Any>> } + ownVars()
|
||||
fun <T : Any> KClass<T>.varsMap() = vars().associateBy { it.name }
|
||||
fun <T : Any> KClass<T>.getters() = java.methods.filter { it.name.startsWith("get") }
|
||||
fun <T : Any> KClass<T>.gettersMap() = getters().associateBy { it.name.removePrefix("get").firstCharLower() }
|
||||
infix fun KCallable<*>.returns(type: KClass<*>) = returnType.jvmErasure.isSubclassOf(type)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <C, T: Any> Var<C, T>.setCast(obj: C, value: String) = set(obj, when (returnType.classifier) {
|
||||
String::class -> value
|
||||
Int::class -> value.toInt()
|
||||
Boolean::class -> value.toBoolean()
|
||||
else -> 400 - "Invalid field type $returnType"
|
||||
} as T)
|
||||
inline fun <reified T: Any> Field.gets(obj: Any): T? = get(obj)?.let { it as T }
|
||||
|
||||
// HTTP
|
||||
operator fun HttpStatus.invoke(message: String? = null): Nothing = throw ApiException(value(), message ?: this.reasonPhrase)
|
||||
operator fun Int.minus(message: String): Nothing {
|
||||
ApiException.log.info("> Error $this: $message")
|
||||
throw ApiException(this, message)
|
||||
}
|
||||
fun <R> parsing(block: () -> R) = try { block() }
|
||||
catch (e: ApiException) { throw e }
|
||||
catch (e: Exception) { 400 - e.message.toString() }
|
||||
fun BodyBuilder.headers(vararg pairs: Pair<String, String>) = headers(HttpHeaders().apply { pairs.forEach { (k, v) -> set(k, v) } })
|
||||
|
||||
// Email validation
|
||||
// https://www.baeldung.com/java-email-validation-regex
|
||||
val emailRegex = "^(?=.{1,64}@)[\\p{L}0-9_-]+(\\.[\\p{L}0-9_-]+)*@[^-][\\p{L}0-9-]+(\\.[\\p{L}0-9-]+)*(\\.[\\p{L}]{2,})$".toRegex()
|
||||
fun Str.isValidEmail(): Bool = emailRegex.matches(this)
|
||||
|
||||
// Global Tools
|
||||
val HTTP = HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json(JSON)
|
||||
}
|
||||
}
|
||||
val TIKA = Tika()
|
||||
val MIMES = MimeTypes.getDefaultMimeTypes()
|
||||
|
||||
// Class resource
|
||||
object Ext { val log = logger() }
|
||||
fun res(name: Str) = Ext::class.java.getResourceAsStream(name)
|
||||
fun resStr(name: Str) = res(name)?.reader()?.readText()
|
||||
inline fun <reified T> resJson(name: Str, warn: Boolean = true) = resStr(name)?.let {
|
||||
JSON.decodeFromString<T>(it)
|
||||
} ?: run { if (warn) Ext.log.warn("Resource $name is not found"); null }
|
||||
|
||||
// Date and time
|
||||
val JST_ZONE = ZoneId.of("Asia/Tokyo")
|
||||
fun jstNow() = LocalDateTime.now(JST_ZONE)
|
||||
fun millis() = System.currentTimeMillis()
|
||||
fun utcNow() = LocalDateTime.now(UTC)
|
||||
val DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
fun LocalDate.isoDate() = format(DATE_FORMAT)
|
||||
fun String.isoDate() = DATE_FORMAT.parse(this, LocalDate::from)
|
||||
fun Date.utc() = toInstant().atZone(UTC).toLocalDate()
|
||||
fun LocalDate.toDate() = Date(atStartOfDay().toInstant(UTC).toEpochMilli())
|
||||
fun LocalDateTime.isoDateTime() = format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||
fun String.isoDateTime() = LocalDateTime.parse(this, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||
val URL_SAFE_DT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")
|
||||
fun LocalDateTime.urlSafeStr() = format(URL_SAFE_DT)
|
||||
val DATE_2018 = LocalDateTime.parse("2018-01-01T00:00:00")
|
||||
|
||||
val ALT_DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||
fun Str.asDateTime() = try { LocalDateTime.parse(this, DateTimeFormatter.ISO_LOCAL_DATE_TIME) }
|
||||
catch (e: Exception) { try { LocalDateTime.parse(this, ALT_DATETIME_FORMAT) }
|
||||
catch (e: Exception) { null } }
|
||||
|
||||
val Calendar.year get() = get(Calendar.YEAR)
|
||||
val Calendar.month get() = get(Calendar.MONTH) + 1
|
||||
val Calendar.day get() = get(Calendar.DAY_OF_MONTH)
|
||||
fun cal() = Calendar.getInstance()
|
||||
fun Date.cal() = Calendar.getInstance().apply { time = this@cal }
|
||||
operator fun Calendar.invoke(field: Int) = get(field)
|
||||
val Date.sec get() = time / 1000
|
||||
|
||||
// Encodings
|
||||
fun Long.toHex(len: Int = 16): Str = "0x${this.toString(len).padStart(len, '0').uppercase()}"
|
||||
fun Map<String, Any>.toUrl() = entries.joinToString("&") { (k, v) -> "$k=$v" }
|
||||
fun String.firstCharLower() = replaceFirstChar { it.lowercase() }
|
||||
|
||||
fun Any.long() = when (this) {
|
||||
is Long -> this
|
||||
is Boolean -> if (this) 1L else 0
|
||||
is Number -> toLong()
|
||||
is String -> toLong()
|
||||
else -> 400 - "Invalid number: $this"
|
||||
}
|
||||
fun Any.uint32() = long() and 0xFFFFFFFF
|
||||
fun Any.int() = long().toInt()
|
||||
val Any.long get() = long()
|
||||
val Any.int get() = int()
|
||||
val Any.double get() = when (this) {
|
||||
is Boolean -> if (this) 1.0 else 0.0
|
||||
is Number -> toDouble()
|
||||
is String -> toDouble()
|
||||
else -> 400 - "Invalid number: $this"
|
||||
}
|
||||
operator fun Bool.unaryPlus() = if (this) 1 else 0
|
||||
val Any?.truthy get() = when (this) {
|
||||
null -> false
|
||||
is Bool -> this
|
||||
is Float -> this != 0f && !isNaN()
|
||||
is Double -> this != 0.0 && !isNaN()
|
||||
is Number -> this != 0
|
||||
is String -> this.isNotBlank()
|
||||
is Collection<*> -> isNotEmpty()
|
||||
is Map<*, *> -> isNotEmpty()
|
||||
else -> true
|
||||
}
|
||||
val Any?.str get() = toString()
|
||||
|
||||
// Collections
|
||||
fun <T> ls(vararg args: T) = args.toList()
|
||||
inline fun <reified T> arr(vararg args: T) = arrayOf(*args)
|
||||
operator fun <K, V> Map<K, V>.plus(map: Map<K, V>) = mut.apply { putAll(map) }
|
||||
operator fun <K, V> MutableMap<K, V>.plusAssign(map: Map<K, V>) { putAll(map) }
|
||||
fun <K, V: Any> Map<K, V?>.vNotNull(): Map<K, V> = filterValues { it != null }.mapValues { it.value!! }
|
||||
fun <T> MutableList<T>.popAll(list: List<T>) = list.also { removeAll(it) }
|
||||
fun <T> MutableList<T>.popAll(vararg items: T) = popAll(items.toList())
|
||||
inline fun <T> Iterable<T>.mapApply(block: T.() -> Unit) = map { it.apply(block) }
|
||||
inline fun <T> Iterable<T>.mapApplyI(block: T.(Int) -> Unit) = mapIndexed { i, e -> e.apply { block(i) } }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <K, V: Any> Map<K, V?>.recursiveNotNull(): Map<K, V> = mapNotNull { (k, v) ->
|
||||
k to if (v is Map<*, *>) (v as Map<Any?, Any?>).recursiveNotNull() else v
|
||||
}.toMap() as Map<K, V>
|
||||
|
||||
val <T> List<T>.mut get() = toMutableList()
|
||||
val <K, V> Map<K, V>.mut get() = toMutableMap()
|
||||
val <T> Set<T>.mut get() = toMutableSet()
|
||||
|
||||
fun <T> List<T>.unique(fn: (T) -> Any) = distinctBy(fn).ifEmpty { null }
|
||||
val <T> Collection<T>.csv get() = joinToString(",")
|
||||
val IntArray.csv get() = joinToString(",")
|
||||
|
||||
// Optionals
|
||||
operator fun <T> Optional<T>.invoke(): T? = orElse(null)
|
||||
fun <T> Optional<T>.expect(message: Str = "Value is not present") = orElseGet { (400 - message) }
|
||||
|
||||
// Strings
|
||||
operator fun Str.get(range: IntRange) = substring(range.first, (range.last + 1).coerceAtMost(length))
|
||||
operator fun Str.get(start: Int, end: Int) = substring(start, end.coerceAtMost(length))
|
||||
fun Str.center(width: Int, padChar: Char = ' ') = padStart((length + width) / 2, padChar).padEnd(width, padChar)
|
||||
fun Str.splitLines() = replace("\r\n", "\n").split('\n')
|
||||
fun Str.hash(algo: Str) = MessageDigest.getInstance(algo).digest(toByteArray(StandardCharsets.UTF_8))
|
||||
fun Str.md5() = hash("MD5")
|
||||
fun Str.fromChusanUsername() = String(this.toByteArray(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)
|
||||
fun Str.truncate(len: Int) = if (this.length > len) this.take(len) + "..." else this
|
||||
val Str.some get() = ifBlank { null }
|
||||
val ByteArray.hexStr get() = toHexString()
|
||||
operator fun StringBuilder.plusAssign(other: String) { this.append(other) }
|
||||
|
||||
// Coroutine
|
||||
suspend fun <T> async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() }
|
||||
fun <T> thread(block: () -> T) = Thread { block() }.apply { start() }
|
||||
fun <T> Lock.maybeLock(block: () -> T) = if (tryLock()) try { block() } finally { unlock() } else null
|
||||
|
||||
// Paths
|
||||
fun path(part1: Str, vararg parts: Str) = Path.of(part1, *parts)
|
||||
fun Str.path() = Path.of(this)
|
||||
operator fun Path.div(part: Str) = resolve(part)
|
||||
operator fun File.div(fileName: Str) = File(this, fileName)
|
||||
fun Str.ensureEndingSlash() = if (endsWith('/')) this else "$this/"
|
||||
fun Str.ensureNoEndingSlash() = if (endsWith('/')) dropLast(1) else this
|
||||
|
||||
fun <T: Any> T.logger() = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
// I hate this ;-; (list destructuring)
|
||||
operator fun <E> List<E>.component6(): E = get(5)
|
||||
operator fun <E> List<E>.component7(): E = get(6)
|
||||
operator fun <E> List<E>.component8(): E = get(7)
|
||||
operator fun <E> List<E>.component9(): E = get(8)
|
||||
operator fun <E> List<E>.component10(): E = get(9)
|
||||
operator fun <E> List<E>.component11(): E = get(10)
|
||||
operator fun <E> List<E>.component12(): E = get(11)
|
||||
operator fun <E> List<E>.component13(): E = get(12)
|
||||
|
||||
inline operator fun <reified E> List<Any?>.invoke(i: Int) = get(i) as E
|
||||
val empty = emptyList<Any>()
|
||||
val emptyMap = emptyMap<Any, Any>()
|
||||
|
||||
val <F> Pair<F, *>.l get() = component1()
|
||||
val <S> Pair<*, S>.r get() = component2()
|
||||
|
||||
// Database
|
||||
val Query.exec get() = resultList.map { (it as Array<*>).toList() }
|
||||
fun List<List<Any?>>.numCsv(vararg head: Str) = head.joinToString(",") + "\n" +
|
||||
joinToString("\n") { it.joinToString(",") }
|
||||
|
||||
// DI
|
||||
inline fun <reified T> ApplicationContext.lazy() = lazy { getBean(T::class.java) }
|
||||
@file:OptIn(ExperimentalStdlibApi::class)
|
||||
|
||||
package ext
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.File
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Path
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.Lock
|
||||
|
||||
typealias Str = String
|
||||
typealias Bool = Boolean
|
||||
typealias JavaSerializable = java.io.Serializable
|
||||
|
||||
typealias JDict = Map<String, Any?>
|
||||
typealias MutJDict = MutableMap<String, Any?>
|
||||
|
||||
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, AnnotationTarget.PROPERTY_GETTER)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class Doc(
|
||||
val desc: String,
|
||||
val ret: String = ""
|
||||
)
|
||||
|
||||
@Target(AnnotationTarget.PROPERTY)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class SettingField(
|
||||
val game: String
|
||||
)
|
||||
|
||||
// Email validation
|
||||
val emailRegex = "^(?=.{1,64}@)[\\p{L}0-9_-]+(\\.[\\p{L}0-9_-]+)*@[^-][\\p{L}0-9-]+(\\.[\\p{L}0-9-]+)*(\\.[\\p{L}]{2,})$".toRegex()
|
||||
fun Str.isValidEmail(): Bool = emailRegex.matches(this)
|
||||
|
||||
// Class resource
|
||||
object Ext { val log = logger() }
|
||||
fun res(name: Str) = Ext::class.java.getResourceAsStream(name)
|
||||
fun resStr(name: Str) = res(name)?.reader()?.readText()
|
||||
inline fun <reified T> resJson(name: Str, warn: Boolean = true) = resStr(name)?.let {
|
||||
JSON.decodeFromString<T>(it)
|
||||
} ?: run { if (warn) Ext.log.warn("Resource $name is not found"); null }
|
||||
|
||||
// Encodings
|
||||
fun Long.toHex(len: Int = 16): Str = "0x${this.toString(len).padStart(len, '0').uppercase()}"
|
||||
fun Map<String, Any>.toUrl() = entries.joinToString("&") { (k, v) -> "$k=$v" }
|
||||
fun String.firstCharLower() = replaceFirstChar { it.lowercase() }
|
||||
|
||||
fun Any.long() = when (this) {
|
||||
is Long -> this
|
||||
is Boolean -> if (this) 1L else 0
|
||||
is Number -> toLong()
|
||||
is String -> toLong()
|
||||
else -> 400 - "Invalid number: $this"
|
||||
}
|
||||
fun Any.uint32() = long() and 0xFFFFFFFF
|
||||
fun Any.int() = long().toInt()
|
||||
val Any.long get() = long()
|
||||
val Any.int get() = int()
|
||||
val Any.double get() = when (this) {
|
||||
is Boolean -> if (this) 1.0 else 0.0
|
||||
is Number -> toDouble()
|
||||
is String -> toDouble()
|
||||
else -> 400 - "Invalid number: $this"
|
||||
}
|
||||
operator fun Bool.unaryPlus() = if (this) 1 else 0
|
||||
val Any?.truthy get() = when (this) {
|
||||
null -> false
|
||||
is Bool -> this
|
||||
is Float -> this != 0f && !isNaN()
|
||||
is Double -> this != 0.0 && !isNaN()
|
||||
is Number -> this != 0
|
||||
is String -> this.isNotBlank()
|
||||
is Collection<*> -> isNotEmpty()
|
||||
is Map<*, *> -> isNotEmpty()
|
||||
else -> true
|
||||
}
|
||||
val Any?.str get() = toString()
|
||||
|
||||
// Collections
|
||||
fun <T> ls(vararg args: T) = args.toList()
|
||||
inline fun <reified T> arr(vararg args: T) = arrayOf(*args)
|
||||
operator fun <K, V> Map<K, V>.plus(map: Map<K, V>) = mut.apply { putAll(map) }
|
||||
operator fun <K, V> MutableMap<K, V>.plusAssign(map: Map<K, V>) { putAll(map) }
|
||||
fun <K, V: Any> Map<K, V?>.vNotNull(): Map<K, V> = filterValues { it != null }.mapValues { it.value!! }
|
||||
fun <T> MutableList<T>.popAll(list: List<T>) = list.also { removeAll(it) }
|
||||
fun <T> MutableList<T>.popAll(vararg items: T) = popAll(items.toList())
|
||||
inline fun <T> Iterable<T>.mapApply(block: T.() -> Unit) = map { it.apply(block) }
|
||||
inline fun <T> Iterable<T>.mapApplyI(block: T.(Int) -> Unit) = mapIndexed { i, e -> e.apply { block(i) } }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <K, V: Any> Map<K, V?>.recursiveNotNull(): Map<K, V> = mapNotNull { (k, v) ->
|
||||
k to if (v is Map<*, *>) (v as Map<Any?, Any?>).recursiveNotNull() else v
|
||||
}.toMap() as Map<K, V>
|
||||
|
||||
val <T> List<T>.mut get() = toMutableList()
|
||||
val <K, V> Map<K, V>.mut get() = toMutableMap()
|
||||
val <T> Set<T>.mut get() = toMutableSet()
|
||||
|
||||
fun <T> List<T>.unique(fn: (T) -> Any) = distinctBy(fn).ifEmpty { null }
|
||||
val <T> Collection<T>.csv get() = joinToString(",")
|
||||
val IntArray.csv get() = joinToString(",")
|
||||
|
||||
// Optionals
|
||||
operator fun <T> Optional<T>.invoke(): T? = orElse(null)
|
||||
fun <T> Optional<T>.expect(message: Str = "Value is not present") = orElseGet { (400 - message) }
|
||||
|
||||
// Strings
|
||||
operator fun Str.get(range: IntRange) = substring(range.first, (range.last + 1).coerceAtMost(length))
|
||||
operator fun Str.get(start: Int, end: Int) = substring(start, end.coerceAtMost(length))
|
||||
fun Str.center(width: Int, padChar: Char = ' ') = padStart((length + width) / 2, padChar).padEnd(width, padChar)
|
||||
fun Str.splitLines() = replace("\r\n", "\n").split('\n')
|
||||
fun Str.hash(algo: Str) = MessageDigest.getInstance(algo).digest(toByteArray(StandardCharsets.UTF_8))
|
||||
fun Str.md5() = hash("MD5")
|
||||
fun Str.fromChusanUsername() = String(this.toByteArray(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)
|
||||
fun Str.truncate(len: Int) = if (this.length > len) this.take(len) + "..." else this
|
||||
val Str.some get() = ifBlank { null }
|
||||
val ByteArray.hexStr get() = toHexString()
|
||||
operator fun StringBuilder.plusAssign(other: String) { this.append(other) }
|
||||
|
||||
// Coroutine
|
||||
suspend fun <T> async(block: suspend kotlinx.coroutines.CoroutineScope.() -> T): T = withContext(Dispatchers.IO) { block() }
|
||||
fun <T> thread(block: () -> T) = Thread { block() }.apply { start() }
|
||||
fun <T> Lock.maybeLock(block: () -> T) = if (tryLock()) try { block() } finally { unlock() } else null
|
||||
|
||||
// Paths
|
||||
fun path(part1: Str, vararg parts: Str) = Path.of(part1, *parts)
|
||||
fun Str.path() = Path.of(this)
|
||||
operator fun Path.div(part: Str) = resolve(part)
|
||||
operator fun File.div(fileName: Str) = File(this, fileName)
|
||||
fun Str.ensureEndingSlash() = if (endsWith('/')) this else "$this/"
|
||||
fun Str.ensureNoEndingSlash() = if (endsWith('/')) dropLast(1) else this
|
||||
|
||||
fun <T: Any> T.logger() = LoggerFactory.getLogger(this::class.java)
|
||||
|
||||
// I hate this ;-; (list destructuring)
|
||||
operator fun <E> List<E>.component6(): E = get(5)
|
||||
operator fun <E> List<E>.component7(): E = get(6)
|
||||
operator fun <E> List<E>.component8(): E = get(7)
|
||||
operator fun <E> List<E>.component9(): E = get(8)
|
||||
operator fun <E> List<E>.component10(): E = get(9)
|
||||
operator fun <E> List<E>.component11(): E = get(10)
|
||||
operator fun <E> List<E>.component12(): E = get(11)
|
||||
operator fun <E> List<E>.component13(): E = get(12)
|
||||
|
||||
inline operator fun <reified E> List<Any?>.invoke(i: Int) = get(i) as E
|
||||
val empty = emptyList<Any>()
|
||||
val emptyMap = emptyMap<Any, Any>()
|
||||
|
||||
val <F> Pair<F, *>.l get() = component1()
|
||||
val <S> Pair<*, S>.r get() = component2()
|
||||
|
||||
fun List<List<Any?>>.numCsv(vararg head: Str) = head.joinToString(",") + "\n" +
|
||||
joinToString("\n") { it.joinToString(",") }
|
||||
|
|
|
|||
62
planet/src/main/kotlin/ext/Jackson.kt
Normal file
62
planet/src/main/kotlin/ext/Jackson.kt
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package ext
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
// Jackson
|
||||
val ACCEPTABLE_FALSE = setOf("0", "false", "no", "off", "False", "None", "null")
|
||||
val ACCEPTABLE_TRUE = setOf("1", "true", "yes", "on", "True")
|
||||
val JSON_FUZZY_BOOLEAN = SimpleModule().addDeserializer(Boolean::class.java, object : JsonDeserializer<Boolean>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext) = when(parser.text) {
|
||||
in ACCEPTABLE_FALSE -> false
|
||||
in ACCEPTABLE_TRUE -> true
|
||||
else -> 400 - "Invalid boolean value ${parser.text}"
|
||||
}
|
||||
})
|
||||
val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<LocalDateTime>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext) =
|
||||
// First try standard formats via asDateTime() method
|
||||
parser.text.takeIf { it.isNotEmpty() }?.run { asDateTime() ?: try {
|
||||
// Try maimai2 format (yyyy-MM-dd HH:mm:ss.0)
|
||||
LocalDateTime.parse(parser.text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0"))
|
||||
} catch (e: Exception) {
|
||||
400 - "Invalid date time value ${parser.text}"
|
||||
} }
|
||||
})
|
||||
val JACKSON = jacksonObjectMapper().apply {
|
||||
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
|
||||
findAndRegisterModules()
|
||||
registerModule(JSON_FUZZY_BOOLEAN)
|
||||
registerModule(JSON_DATETIME)
|
||||
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
propertyNamingStrategy = PropertyNamingStrategies.LOWER_CAMEL_CASE;
|
||||
}
|
||||
inline fun <reified T> ObjectMapper.parse(str: Str) = readValue(str, T::class.java)
|
||||
inline fun <reified T> ObjectMapper.parse(map: Map<*, *>) = convertValue(map, T::class.java)
|
||||
// TODO: https://stackoverflow.com/q/78197784/7346633
|
||||
fun <T> Str.parseJackson(cls: Class<T>) = if (contains("null")) {
|
||||
val map = JACKSON.parse<MutableMap<String, Any>>(this)
|
||||
JACKSON.convertValue(map.recursiveNotNull(), cls)
|
||||
}
|
||||
else JACKSON.readValue(this, cls)
|
||||
fun <T> T.toJson() = JACKSON.writeValueAsString(this)
|
||||
|
||||
inline fun <reified T> String.json() = try {
|
||||
if (isEmpty() || this == "null") null
|
||||
else JACKSON.readValue(this, T::class.java)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
println("Failed to parse JSON: $this")
|
||||
throw e
|
||||
}
|
||||
|
||||
fun String.jsonMap(): Map<String, Any?> = json() ?: emptyMap()
|
||||
fun String.jsonArray(): List<Map<String, Any?>> = json() ?: emptyList()
|
||||
fun String.jsonMaybeMap(): Map<String, Any?>? = json()
|
||||
fun String.jsonMaybeArray(): List<Map<String, Any?>>? = json()
|
||||
|
|
@ -1,68 +1,8 @@
|
|||
package ext
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.databind.*
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
// Jackson
|
||||
val ACCEPTABLE_FALSE = setOf("0", "false", "no", "off", "False", "None", "null")
|
||||
val ACCEPTABLE_TRUE = setOf("1", "true", "yes", "on", "True")
|
||||
val JSON_FUZZY_BOOLEAN = SimpleModule().addDeserializer(Boolean::class.java, object : JsonDeserializer<Boolean>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext) = when(parser.text) {
|
||||
in ACCEPTABLE_FALSE -> false
|
||||
in ACCEPTABLE_TRUE -> true
|
||||
else -> 400 - "Invalid boolean value ${parser.text}"
|
||||
}
|
||||
})
|
||||
val JSON_DATETIME = SimpleModule().addDeserializer(java.time.LocalDateTime::class.java, object : JsonDeserializer<LocalDateTime>() {
|
||||
override fun deserialize(parser: JsonParser, context: DeserializationContext) =
|
||||
// First try standard formats via asDateTime() method
|
||||
parser.text.takeIf { it.isNotEmpty() }?.run { asDateTime() ?: try {
|
||||
// Try maimai2 format (yyyy-MM-dd HH:mm:ss.0)
|
||||
LocalDateTime.parse(parser.text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0"))
|
||||
} catch (e: Exception) {
|
||||
400 - "Invalid date time value ${parser.text}"
|
||||
} }
|
||||
})
|
||||
val JACKSON = jacksonObjectMapper().apply {
|
||||
setSerializationInclusion(JsonInclude.Include.NON_NULL)
|
||||
setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
|
||||
findAndRegisterModules()
|
||||
registerModule(JSON_FUZZY_BOOLEAN)
|
||||
registerModule(JSON_DATETIME)
|
||||
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
propertyNamingStrategy = PropertyNamingStrategies.LOWER_CAMEL_CASE;
|
||||
}
|
||||
inline fun <reified T> ObjectMapper.parse(str: Str) = readValue(str, T::class.java)
|
||||
inline fun <reified T> ObjectMapper.parse(map: Map<*, *>) = convertValue(map, T::class.java)
|
||||
// TODO: https://stackoverflow.com/q/78197784/7346633
|
||||
fun <T> Str.parseJackson(cls: Class<T>) = if (contains("null")) {
|
||||
val map = JACKSON.parse<MutableMap<String, Any>>(this)
|
||||
JACKSON.convertValue(map.recursiveNotNull(), cls)
|
||||
}
|
||||
else JACKSON.readValue(this, cls)
|
||||
fun <T> T.toJson() = JACKSON.writeValueAsString(this)
|
||||
|
||||
inline fun <reified T> String.json() = try {
|
||||
if (isEmpty() || this == "null") null
|
||||
else JACKSON.readValue(this, T::class.java)
|
||||
}
|
||||
catch (e: Exception) {
|
||||
println("Failed to parse JSON: $this")
|
||||
throw e
|
||||
}
|
||||
|
||||
fun String.jsonMap(): Map<String, Any?> = json() ?: emptyMap()
|
||||
fun String.jsonArray(): List<Map<String, Any?>> = json() ?: emptyList()
|
||||
fun String.jsonMaybeMap(): Map<String, Any?>? = json()
|
||||
fun String.jsonMaybeArray(): List<Map<String, Any?>>? = json()
|
||||
|
||||
// KotlinX Serialization
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
|
|
@ -73,12 +13,3 @@ val JSON = Json {
|
|||
explicitNulls = false
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
// Bean for default jackson object mapper
|
||||
//@Configuration
|
||||
//class JacksonConfig {
|
||||
// @Bean
|
||||
// fun objectMapper(): ObjectMapper {
|
||||
// return JACKSON
|
||||
// }
|
||||
//}
|
||||
|
|
|
|||
12
planet/src/main/kotlin/ext/Ktor.kt
Normal file
12
planet/src/main/kotlin/ext/Ktor.kt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package ext
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
|
||||
val HTTP = HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json(JSON)
|
||||
}
|
||||
}
|
||||
30
planet/src/main/kotlin/ext/Reflect.kt
Normal file
30
planet/src/main/kotlin/ext/Reflect.kt
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package ext
|
||||
|
||||
import java.lang.reflect.Field
|
||||
import kotlin.reflect.KCallable
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KMutableProperty1
|
||||
import kotlin.reflect.full.declaredMemberProperties
|
||||
import kotlin.reflect.full.isSubclassOf
|
||||
import kotlin.reflect.jvm.javaField
|
||||
import kotlin.reflect.jvm.jvmErasure
|
||||
|
||||
typealias Var<T, V> = KMutableProperty1<T, V>
|
||||
|
||||
// Reflection
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> KClass<T>.ownVars() = declaredMemberProperties.sortedBy { it.javaField?.declaringClass?.declaredFields?.indexOf(it.javaField) ?: Int.MAX_VALUE }.mapNotNull { it as? Var<T, Any> }
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : Any> KClass<T>.vars(): List<Var<T, Any>> = supertypes.mapNotNull { it.classifier as? KClass<*> }.filter { !it.java.isInterface }.flatMap{ it.vars() as List<Var<T, Any>> } + ownVars()
|
||||
fun <T : Any> KClass<T>.varsMap() = vars().associateBy { it.name }
|
||||
fun <T : Any> KClass<T>.getters() = java.methods.filter { it.name.startsWith("get") }
|
||||
fun <T : Any> KClass<T>.gettersMap() = getters().associateBy { it.name.removePrefix("get").firstCharLower() }
|
||||
infix fun KCallable<*>.returns(type: KClass<*>) = returnType.jvmErasure.isSubclassOf(type)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <C, T: Any> Var<C, T>.setCast(obj: C, value: String) = set(obj, when (returnType.classifier) {
|
||||
String::class -> value
|
||||
Int::class -> value.toInt()
|
||||
Boolean::class -> value.toBoolean()
|
||||
else -> 400 - "Invalid field type $returnType"
|
||||
} as T)
|
||||
inline fun <reified T: Any> Field.gets(obj: Any): T? = get(obj)?.let { it as T }
|
||||
48
planet/src/main/kotlin/ext/Spring.kt
Normal file
48
planet/src/main/kotlin/ext/Spring.kt
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
package ext
|
||||
|
||||
import icu.samnyan.aqua.net.utils.ApiException
|
||||
import jakarta.persistence.Query
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.context.ApplicationContext
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.ResponseEntity.BodyBuilder
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
typealias RP = RequestParam
|
||||
typealias RB = RequestBody
|
||||
typealias RT = RequestPart
|
||||
typealias RH = RequestHeader
|
||||
typealias PV = PathVariable
|
||||
typealias API = RequestMapping
|
||||
|
||||
fun HttpServletRequest.details() = mapOf(
|
||||
"method" to method,
|
||||
"uri" to requestURI,
|
||||
"query" to queryString,
|
||||
"remote" to remoteAddr,
|
||||
"headers" to headerNames.asSequence().associateWith { getHeader(it) }
|
||||
)
|
||||
|
||||
fun HttpServletResponse.details() = mapOf(
|
||||
"status" to status,
|
||||
"headers" to headerNames.asSequence().associateWith { getHeader(it) },
|
||||
)
|
||||
|
||||
// HTTP
|
||||
operator fun HttpStatus.invoke(message: String? = null): Nothing = throw ApiException(value(), message ?: this.reasonPhrase)
|
||||
operator fun Int.minus(message: String): Nothing {
|
||||
ApiException.log.info("> Error $this: $message")
|
||||
throw ApiException(this, message)
|
||||
}
|
||||
fun <R> parsing(block: () -> R) = try { block() }
|
||||
catch (e: ApiException) { throw e }
|
||||
catch (e: Exception) { 400 - e.message.toString() }
|
||||
fun BodyBuilder.headers(vararg pairs: Pair<String, String>) = headers(HttpHeaders().apply { pairs.forEach { (k, v) -> set(k, v) } })
|
||||
|
||||
// Database
|
||||
val Query.exec get() = resultList.map { (it as Array<*>).toList() }
|
||||
|
||||
// DI
|
||||
inline fun <reified T> ApplicationContext.lazy() = lazy { getBean(T::class.java) }
|
||||
7
planet/src/main/kotlin/ext/Tika.kt
Normal file
7
planet/src/main/kotlin/ext/Tika.kt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
package ext
|
||||
|
||||
import org.apache.tika.Tika
|
||||
import org.apache.tika.mime.MimeTypes
|
||||
|
||||
val TIKA = Tika()
|
||||
val MIMES = MimeTypes.getDefaultMimeTypes()
|
||||
37
planet/src/main/kotlin/ext/Time.kt
Normal file
37
planet/src/main/kotlin/ext/Time.kt
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package ext
|
||||
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset.UTC
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
|
||||
// Date and time
|
||||
val JST_ZONE = ZoneId.of("Asia/Tokyo")
|
||||
fun jstNow() = LocalDateTime.now(JST_ZONE)
|
||||
fun millis() = System.currentTimeMillis()
|
||||
fun utcNow() = LocalDateTime.now(UTC)
|
||||
val DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
||||
fun LocalDate.isoDate() = format(DATE_FORMAT)
|
||||
fun String.isoDate() = DATE_FORMAT.parse(this, LocalDate::from)
|
||||
fun Date.utc() = toInstant().atZone(UTC).toLocalDate()
|
||||
fun LocalDate.toDate() = Date(atStartOfDay().toInstant(UTC).toEpochMilli())
|
||||
fun LocalDateTime.isoDateTime() = format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||
fun String.isoDateTime() = LocalDateTime.parse(this, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
|
||||
val URL_SAFE_DT = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss")
|
||||
fun LocalDateTime.urlSafeStr() = format(URL_SAFE_DT)
|
||||
val DATE_2018 = LocalDateTime.parse("2018-01-01T00:00:00")
|
||||
|
||||
val ALT_DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
||||
fun Str.asDateTime() = try { LocalDateTime.parse(this, DateTimeFormatter.ISO_LOCAL_DATE_TIME) }
|
||||
catch (e: Exception) { try { LocalDateTime.parse(this, ALT_DATETIME_FORMAT) }
|
||||
catch (e: Exception) { null } }
|
||||
|
||||
val Calendar.year get() = get(Calendar.YEAR)
|
||||
val Calendar.month get() = get(Calendar.MONTH) + 1
|
||||
val Calendar.day get() = get(Calendar.DAY_OF_MONTH)
|
||||
fun cal() = Calendar.getInstance()
|
||||
fun Date.cal() = Calendar.getInstance().apply { time = this@cal }
|
||||
operator fun Calendar.invoke(field: Int) = get(field)
|
||||
val Date.sec get() = time / 1000
|
||||
Loading…
Reference in New Issue
Block a user