Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,8 @@ dependencies {
// Content validation
implementation("org.apache.tika:tika-core:3.3.1")

// Import: DateTime Parsing
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.22.1")

// Serialization
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.22.1")
// Serialization (Jackson 3; JSR-310 support is built into jackson-databind 3.x)
implementation("tools.jackson.module:jackson-module-kotlin:3.2.1")

// Testing
testImplementation("io.kotest:kotest-runner-junit5-jvm:6.2.2")
Expand Down
118 changes: 88 additions & 30 deletions src/main/java/ext/Json.kt
Original file line number Diff line number Diff line change
@@ -1,44 +1,95 @@
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 tools.jackson.core.JsonParser
import tools.jackson.core.JsonToken
import tools.jackson.databind.DeserializationContext
import tools.jackson.databind.DeserializationFeature
import tools.jackson.databind.ObjectMapper
import tools.jackson.databind.PropertyNamingStrategies
import tools.jackson.databind.ValueDeserializer
import tools.jackson.databind.ext.javatime.ser.LocalDateTimeSerializer
import tools.jackson.databind.json.JsonMapper
import tools.jackson.databind.module.SimpleModule
import tools.jackson.module.kotlin.KotlinModule
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>() {
val JSON_FUZZY_BOOLEAN = SimpleModule().addDeserializer(Boolean::class.java, object : ValueDeserializer<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 {
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;
val JSON_DATETIME = SimpleModule()
.addSerializer(LocalDateTime::class.java, LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
.addDeserializer(LocalDateTime::class.java, object : ValueDeserializer<LocalDateTime>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): LocalDateTime? {
parser.readLocalDateTimeArray(context)?.let { return it }

// First try standard formats via asDateTime() method
return 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}"
} }
}
})

fun JsonParser.readLocalDateTimeArray(context: DeserializationContext): LocalDateTime? {
if (!isExpectedStartArrayToken) return null

val parts = mutableListOf<Int>()
while (nextToken() != JsonToken.END_ARRAY) {
if (currentToken() != JsonToken.VALUE_NUMBER_INT) {
return context.reportInputMismatch(
LocalDateTime::class.java,
"Invalid legacy date-time array",
)
}
parts += intValue
}

if (parts.size !in 5..7) {
return context.reportInputMismatch(
LocalDateTime::class.java,
"Invalid legacy date-time array; expected 5 to 7 integer components",
)
}

return try {
LocalDateTime.of(
parts[0], parts[1], parts[2], parts[3], parts[4],
parts.getOrElse(5) { 0 }, parts.getOrElse(6) { 0 },
)
} catch (e: Exception) {
context.reportInputMismatch(
LocalDateTime::class.java,
"Invalid legacy date-time array: ${e.message}",
)
}
}

val JACKSON: JsonMapper = JsonMapper.builder()
.addModule(KotlinModule.Builder().build())
.findAndAddModules()
.addModule(JSON_FUZZY_BOOLEAN)
.addModule(JSON_DATETIME)
.changeDefaultPropertyInclusion {
JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL)
}
.propertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE)
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build()

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
Expand Down Expand Up @@ -73,11 +124,18 @@ val JSON = Json {
coerceInputValues = true
}

// Bean for default jackson object mapper
//@Configuration
//class JacksonConfig {
// @Bean
// fun objectMapper(): ObjectMapper {
// return JACKSON
// }
//}
/**
* Expose the shared Jackson 3 mapper as the application JsonMapper.
*
* Spring Boot 4 auto-configures a tools.jackson JsonMapper WITHOUT the Kotlin module
* (Jackson2ObjectMapperBuilder's well-known-module registration no longer exists),
* which silently renamed all `is*` boolean fields in @RestController responses
* (e.g. isClear -> clear) and ignored Jackson 2 databind annotations.
* Providing our own JsonMapper bean makes Boot's JacksonAutoConfiguration back off
* (@ConditionalOnMissingBean), so the HTTP message converters use this mapper.
*/
@org.springframework.context.annotation.Configuration
class JacksonConfig {
@org.springframework.context.annotation.Bean
fun jsonMapper(): JsonMapper = JACKSON
}
4 changes: 2 additions & 2 deletions src/main/java/icu/samnyan/aqua/net/games/ImportController.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package icu.samnyan.aqua.net.games

import com.fasterxml.jackson.core.JsonProcessingException
import tools.jackson.core.JacksonException
import ext.*
import icu.samnyan.aqua.net.Fedy
import icu.samnyan.aqua.net.db.AquaNetUser
Expand Down Expand Up @@ -124,7 +124,7 @@ abstract class ImportController<ExportModel: IExportClass<UserModel>, UserModel:
json.parseJackson(exportClass.java)
} catch (e: Exception) {
val jsonError = generateSequence<Throwable>(e) { it.cause }
.filterIsInstance<JsonProcessingException>()
.filterIsInstance<JacksonException>()
.firstOrNull()
?: throw e

Expand Down
14 changes: 6 additions & 8 deletions src/main/java/icu/samnyan/aqua/net/games/ImportHelper.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package icu.samnyan.aqua.net.games

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import tools.jackson.core.JsonParser
import tools.jackson.databind.DeserializationContext
import tools.jackson.databind.ValueDeserializer
import tools.jackson.databind.module.SimpleModule
import ext.JACKSON
import ext.minus

Expand Down Expand Up @@ -31,7 +31,7 @@ fun String.asSqlInsert(): SqlInsert {
}

@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "UNCHECKED_CAST")
val JSON_INT_LIST_STR = SimpleModule().addDeserializer(List::class.java, object : JsonDeserializer<List<Integer>>() {
val JSON_INT_LIST_STR = SimpleModule().addDeserializer(List::class.java, object : ValueDeserializer<List<Integer>>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext) =
try {
val text = parser.text.trim('[', ']')
Expand All @@ -41,6 +41,4 @@ val JSON_INT_LIST_STR = SimpleModule().addDeserializer(List::class.java, object
400 - "Invalid list value ${parser.text}: $e" }
})

val JACKSON_ARTEMIS = JACKSON.copy().apply {
registerModule(JSON_INT_LIST_STR)
}
val JACKSON_ARTEMIS = JACKSON.rebuild().addModule(JSON_INT_LIST_STR).build()
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package icu.samnyan.aqua.sega.cardmaker

import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import tools.jackson.core.type.TypeReference
import tools.jackson.databind.json.JsonMapper
import ext.logger
import jakarta.servlet.http.HttpServletRequest
import org.springframework.web.bind.annotation.ModelAttribute
Expand All @@ -14,7 +14,7 @@ import java.nio.charset.StandardCharsets
@RestControllerAdvice(basePackages = ["icu.samnyan.aqua.sega.cardmaker"])
class CardMakerControllerAdvice {
val logger = logger()
val mapper = ObjectMapper()
val mapper = JsonMapper()

/**
* Get the map object from json string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package icu.samnyan.aqua.sega.chusan.model.userdata
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import tools.jackson.core.JsonParser
import tools.jackson.databind.DeserializationContext
import tools.jackson.databind.ValueDeserializer
import tools.jackson.databind.annotation.JsonDeserialize
import tools.jackson.databind.annotation.JsonSerialize
import ext.readLocalDateTimeArray
import icu.samnyan.aqua.net.games.BaseEntity
import icu.samnyan.aqua.net.games.IUserData
import icu.samnyan.aqua.sega.chusan.model.request.UserEmoney
Expand All @@ -21,16 +22,18 @@ import java.time.format.DateTimeFormatterBuilder
import java.time.format.ResolverStyle
import java.time.temporal.ChronoField

class FlexibleDateTimeDeserializer : JsonDeserializer<LocalDateTime?>() {
class FlexibleDateTimeDeserializer : ValueDeserializer<LocalDateTime?>() {
@Throws(IOException::class)
public override fun deserialize(p: JsonParser, ctxt: DeserializationContext): LocalDateTime {
p.readLocalDateTimeArray(ctxt)?.let { return it }

val value = p.text
return FORMATTERS.firstNotNullOfOrNull { formatter ->
runCatching { LocalDateTime.parse(value, formatter) }.getOrNull()
} ?: throw ctxt.weirdStringException(
value,
} ?: ctxt.reportInputMismatch<LocalDateTime>(
LocalDateTime::class.java,
"Invalid date-time; expected yyyy-MM-dd'T'HH:mm:ss or yyyy-MM-dd HH:mm:ss with an optional fractional second",
"Invalid date-time '%s'; expected yyyy-MM-dd'T'HH:mm:ss or yyyy-MM-dd HH:mm:ss with an optional fractional second",
value,
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package icu.samnyan.aqua.sega.chusan.model.userdata

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import tools.jackson.databind.annotation.JsonDeserialize
import icu.samnyan.aqua.net.games.IGenericUserMusic
import icu.samnyan.aqua.sega.util.BooleanToIntegerDeserializer
import jakarta.persistence.Entity
Expand Down
38 changes: 19 additions & 19 deletions src/main/java/icu/samnyan/aqua/sega/diva/util/DivaMapper.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package icu.samnyan.aqua.sega.diva.util

import com.fasterxml.jackson.core.json.JsonWriteFeature
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.json.JsonMapper
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer
import tools.jackson.core.json.JsonWriteFeature
import tools.jackson.core.type.TypeReference
import tools.jackson.databind.DeserializationFeature
import tools.jackson.databind.ObjectMapper
import tools.jackson.databind.ext.javatime.deser.LocalDateTimeDeserializer
import tools.jackson.databind.json.JsonMapper
import tools.jackson.databind.module.SimpleModule
import ext.JDict
import icu.samnyan.aqua.sega.util.BooleanNumberDeserializer
import icu.samnyan.aqua.sega.util.BooleanNumberSerializer
Expand All @@ -18,19 +18,19 @@ import java.time.format.DateTimeFormatter

@Component
class DivaMapper {
private val mapper: ObjectMapper = JsonMapper.builder().enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS)
private val mapper: ObjectMapper = JsonMapper.builder()
.addModule(SimpleModule().apply {
addSerializer(LocalDateTime::class.java, DivaDateTimeSerializer())
addDeserializer(LocalDateTime::class.java, LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")))
addDeserializer(ZonedDateTime::class.java, ZonedDateTimeDeserializer())
addSerializer(Boolean::class.java, BooleanNumberSerializer())
addSerializer(Boolean::class.javaPrimitiveType, BooleanNumberSerializer())
addDeserializer(Boolean::class.java, BooleanNumberDeserializer())
addDeserializer(Boolean::class.javaPrimitiveType, BooleanNumberDeserializer())
})
.enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.build().apply {
registerModule(SimpleModule().apply {
addSerializer(LocalDateTime::class.java, DivaDateTimeSerializer())
addDeserializer(LocalDateTime::class.java, LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.0")))
addDeserializer(ZonedDateTime::class.java, ZonedDateTimeDeserializer())
addSerializer(Boolean::class.java, BooleanNumberSerializer())
addSerializer(Boolean::class.javaPrimitiveType, BooleanNumberSerializer())
addDeserializer(Boolean::class.java, BooleanNumberDeserializer())
addDeserializer(Boolean::class.javaPrimitiveType, BooleanNumberDeserializer())
})
}
.build()

fun write(o: Any) = mapper.writeValueAsString(o)
fun <T> convert(map: JDict, toClass: Class<T>) = mapper.convertValue<T>(map, toClass)
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/icu/samnyan/aqua/sega/diva/util/DivaUtils.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package icu.samnyan.aqua.sega.diva.util

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.ser.std.StdSerializer
import tools.jackson.core.JsonGenerator
import tools.jackson.databind.SerializationContext
import tools.jackson.databind.ser.std.StdSerializer
import icu.samnyan.aqua.sega.diva.PlayerPvRecordRepository
import icu.samnyan.aqua.sega.diva.model.common.Edition
import icu.samnyan.aqua.sega.diva.model.common.LevelInfo
Expand Down Expand Up @@ -30,7 +30,7 @@ object DivaTime {
}

class DivaDateTimeSerializer(t: Class<LocalDateTime>? = null) : StdSerializer<LocalDateTime>(t) {
override fun serialize(value: LocalDateTime, gen: JsonGenerator, provider: SerializerProvider) {
override fun serialize(value: LocalDateTime, gen: JsonGenerator, provider: SerializationContext) {
gen.writeString(DivaTime.getString(value))
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package icu.samnyan.aqua.sega.general

import com.fasterxml.jackson.core.JsonProcessingException
import tools.jackson.core.JacksonException
import ext.*
import jakarta.servlet.http.HttpServletRequest
import org.springframework.scheduling.annotation.Scheduled


fun interface BaseHandler {
@Throws(JsonProcessingException::class)
@Throws(JacksonException::class)
fun handle(request: Map<String, Any>): Any?
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package icu.samnyan.aqua.sega.maimai2.handler

import com.fasterxml.jackson.core.JsonProcessingException
import tools.jackson.core.JacksonException
import ext.mapApply
import ext.minus
import ext.unique
Expand Down Expand Up @@ -28,7 +28,7 @@ class UpsertUserAllHandler(
) : BaseHandler {
fun String.isValidUsername() = isNotBlank() && length <= 8

@Throws(JsonProcessingException::class)
@Throws(JacksonException::class)
override fun handle(request: Map<String, Any>): Any? {
val upsertUserAll = mapper.convert(request, Mai2UpsertUserAll::class.java)
val userId = upsertUserAll.userId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package icu.samnyan.aqua.sega.maimai2.model.userdata
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import tools.jackson.databind.annotation.JsonSerialize
import icu.samnyan.aqua.net.games.BaseEntity
import icu.samnyan.aqua.net.games.IUserData
import icu.samnyan.aqua.sega.general.IntegerListConverter
Expand Down
Loading