Update javalin to v7 (major) (#1920)

* Update javalin to v7

* Update Javalin usage to v7 and Jackson 3

* Import fix

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Syer10 <syer10@users.noreply.github.com>
This commit is contained in:
renovate[bot]
2026-05-09 14:52:00 -04:00
committed by GitHub
parent c98899d501
commit 6ee3348f50
8 changed files with 54 additions and 47 deletions
+6 -6
View File
@@ -4,9 +4,9 @@ coroutines = "1.11.0"
serialization = "1.11.0" serialization = "1.11.0"
jvmTarget = "21" jvmTarget = "21"
okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions
javalin = "6.7.0" javalin = "7.2.0"
jte = "3.2.4" jte = "3.2.4"
jackson = "2.18.3" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency` jackson = "3.1.2" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
exposed = "0.61.0" exposed = "0.61.0"
dex2jar = "2.4.36" dex2jar = "2.4.36"
polyglot = "25.0.3" polyglot = "25.0.3"
@@ -51,10 +51,10 @@ okio = "com.squareup.okio:okio:3.17.0"
# Javalin api # Javalin api
javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" } javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" }
javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javalin" } javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javalin" }
javalin-rendering = { module = "io.javalin:javalin-rendering", version.ref = "javalin" } javalin-rendering = { module = "io.javalin:javalin-rendering-jte", version.ref = "javalin" }
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } jackson-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" } jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.20"
jte = { module = "gg.jte:jte", version.ref = "jte" } jte = { module = "gg.jte:jte", version.ref = "jte" }
kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" } kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" }
@@ -6,7 +6,7 @@ import io.javalin.websocket.WsMessageContext
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.eclipse.jetty.websocket.api.CloseStatus import org.eclipse.jetty.websocket.core.CloseStatus
import suwayomi.tachidesk.manga.impl.update.Websocket import suwayomi.tachidesk.manga.impl.update.Websocket
object WebView : Websocket<String>() { object WebView : Websocket<String>() {
@@ -13,10 +13,14 @@ import com.expediagroup.graphql.server.types.GraphQLRequest
import com.expediagroup.graphql.server.types.GraphQLServerRequest import com.expediagroup.graphql.server.types.GraphQLServerRequest
import io.javalin.http.Context import io.javalin.http.Context
import io.javalin.http.UploadedFile import io.javalin.http.UploadedFile
import io.javalin.json.JavalinJackson
import io.javalin.json.fromJsonStream
import io.javalin.json.fromJsonString import io.javalin.json.fromJsonString
import java.io.IOException import java.io.IOException
class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> { class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
val jsonMapper = JavalinJackson()
@Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE")
override suspend fun parseRequest(context: Context): GraphQLServerRequest? { override suspend fun parseRequest(context: Context): GraphQLServerRequest? {
return try { return try {
@@ -29,17 +33,17 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser<Context> {
context.formParam("operations") context.formParam("operations")
?: throw IllegalArgumentException("Cannot find 'operations' body") ?: throw IllegalArgumentException("Cannot find 'operations' body")
} else { } else {
return context.bodyAsClass(GraphQLServerRequest::class.java) return context.bodyInputStream().use { jsonMapper.fromJsonStream<GraphQLServerRequest>(it) }
} }
val request = val request =
context.jsonMapper().fromJsonString<GraphQLServerRequest>(formParam) jsonMapper.fromJsonString<GraphQLServerRequest>(formParam)
val map = val map =
context context
.formParam("map") .formParam("map")
?.let { ?.let {
context.jsonMapper().fromJsonString<Map<String, List<String>>>(it) jsonMapper.fromJsonString<Map<String, List<String>>>(it)
}.orEmpty() }.orEmpty()
val mapItems = val mapItems =
@@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.job import kotlinx.coroutines.job
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.eclipse.jetty.websocket.api.CloseStatus import org.eclipse.jetty.websocket.core.CloseStatus
import suwayomi.tachidesk.graphql.server.TachideskGraphQLContextFactory import suwayomi.tachidesk.graphql.server.TachideskGraphQLContextFactory
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_INIT import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_INIT
import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_SUBSCRIBE import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_SUBSCRIBE
@@ -13,7 +13,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import org.eclipse.jetty.websocket.api.CloseStatus import org.eclipse.jetty.websocket.core.CloseStatus
import suwayomi.tachidesk.graphql.server.toGraphQLContext import suwayomi.tachidesk.graphql.server.toGraphQLContext
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
@@ -13,12 +13,14 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.Javalin import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.after import io.javalin.apibuilder.ApiBuilder.after
import io.javalin.apibuilder.ApiBuilder.path import io.javalin.apibuilder.ApiBuilder.path
import io.javalin.config.RoutesConfig
import io.javalin.http.Context import io.javalin.http.Context
import io.javalin.http.HandlerType import io.javalin.http.HandlerType
import io.javalin.http.HttpStatus import io.javalin.http.HttpStatus
import io.javalin.http.NotFoundResponse import io.javalin.http.NotFoundResponse
import io.javalin.http.RedirectResponse import io.javalin.http.RedirectResponse
import io.javalin.http.UnauthorizedResponse import io.javalin.http.UnauthorizedResponse
import io.javalin.json.JavalinJackson3
import io.javalin.rendering.template.JavalinJte import io.javalin.rendering.template.JavalinJte
import io.javalin.websocket.WsContext import io.javalin.websocket.WsContext
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -47,6 +49,7 @@ import java.net.URLEncoder
import java.util.Locale import java.util.Locale
import java.util.concurrent.CompletableFuture import java.util.concurrent.CompletableFuture
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.text.get
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
object JavalinSetup { object JavalinSetup {
@@ -58,10 +61,12 @@ object JavalinSetup {
fun javalinSetup() { fun javalinSetup() {
val app = val app =
Javalin.create { config -> Javalin.start { config ->
val templateEngine = TemplateEngine.createPrecompiled(ContentType.Html) val templateEngine = TemplateEngine.createPrecompiled(ContentType.Html)
config.fileRenderer(JavalinJte(templateEngine)) config.fileRenderer(JavalinJte(templateEngine))
config.jsonMapper(JavalinJackson3())
WebInterfaceManager.setup(config) WebInterfaceManager.setup(config)
// config.registerPlugin(OpenApiPlugin(getOpenApiOptions())) // config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
@@ -104,7 +109,8 @@ object JavalinSetup {
} }
} }
config.router.apiBuilder { config.routes.defineCore()
config.routes.apiBuilder {
path(ServerSubpath.maybeAddAsPrefix("api/")) { path(ServerSubpath.maybeAddAsPrefix("api/")) {
path("v1/") { path("v1/") {
GlobalAPI.defineEndpoints() GlobalAPI.defineEndpoints()
@@ -117,17 +123,32 @@ object JavalinSetup {
after { ctx -> after { ctx ->
// If not matched, the request was for an invalid endpoint // If not matched, the request was for an invalid endpoint
// Return a 404 instead of redirecting to the UI for usability // Return a 404 instead of redirecting to the UI for usability
if (ctx.endpointHandlerPath() == "*") { if (ctx.endpoints().lastHttpEndpoint()?.path == "*") {
throw NotFoundResponse() throw NotFoundResponse()
} }
} }
} }
} }
config.events.serverStarted {
if (serverConfig.initialOpenInBrowserEnabled.value) {
Browser.openInBrowser()
}
}
} }
// when JVM is prompted to shutdown, stop javalin gracefully
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
app.stop()
},
)
}
fun RoutesConfig.defineCore() {
val loginPath = ServerSubpath.maybeAddAsPrefix("/login.html") val loginPath = ServerSubpath.maybeAddAsPrefix("/login.html")
app.get(loginPath) { ctx -> get(loginPath) { ctx ->
val locale: Locale = LocalizationHelper.ctxToLocale(ctx) val locale: Locale = LocalizationHelper.ctxToLocale(ctx)
ctx.header("content-type", "text/html") ctx.header("content-type", "text/html")
val httpCacheSeconds = 1.days.inWholeSeconds val httpCacheSeconds = 1.days.inWholeSeconds
@@ -141,7 +162,7 @@ object JavalinSetup {
) )
} }
app.post(loginPath) { ctx -> post(loginPath) { ctx ->
val username = ctx.formParam("user") val username = ctx.formParam("user")
val password = ctx.formParam("pass") val password = ctx.formParam("pass")
val isValid = val isValid =
@@ -174,7 +195,7 @@ object JavalinSetup {
) )
} }
app.beforeMatched { ctx -> beforeMatched { ctx ->
val isWebManifest = val isWebManifest =
listOf("site.webmanifest", "manifest.json", "login.html").any { ctx.path().endsWith(it) } listOf("site.webmanifest", "manifest.json", "login.html").any { ctx.path().endsWith(it) }
val isPageIcon = val isPageIcon =
@@ -219,60 +240,43 @@ object JavalinSetup {
ctx.setAttribute(Attribute.TachideskBasic, credentialsValid()) ctx.setAttribute(Attribute.TachideskBasic, credentialsValid())
} }
app.events { event -> wsBefore {
event.serverStarted {
if (serverConfig.initialOpenInBrowserEnabled.value) {
Browser.openInBrowser()
}
}
}
app.wsBefore {
it.onConnect { ctx -> it.onConnect { ctx ->
ctx.setAttribute(Attribute.TachideskUser, getUserFromWsContext(ctx)) ctx.setAttribute(Attribute.TachideskUser, getUserFromWsContext(ctx))
} }
} }
// when JVM is prompted to shutdown, stop javalin gracefully exception(NullPointerException::class.java) { e, ctx ->
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
app.stop()
},
)
app.exception(NullPointerException::class.java) { e, ctx ->
logger.error(e) { "NullPointerException while handling the request" } logger.error(e) { "NullPointerException while handling the request" }
ctx.status(404) ctx.status(404)
} }
app.exception(NoSuchElementException::class.java) { e, ctx -> exception(NoSuchElementException::class.java) { e, ctx ->
logger.error(e) { "NoSuchElementException while handling the request" } logger.error(e) { "NoSuchElementException while handling the request" }
ctx.status(404) ctx.status(404)
} }
app.exception(IOException::class.java) { e, ctx -> exception(IOException::class.java) { e, ctx ->
logger.error(e) { "IOException while handling the request" } logger.error(e) { "IOException while handling the request" }
ctx.status(500) ctx.status(500)
ctx.result(e.message ?: "Internal Server Error") ctx.result(e.message ?: "Internal Server Error")
} }
app.exception(IllegalArgumentException::class.java) { e, ctx -> exception(IllegalArgumentException::class.java) { e, ctx ->
logger.error(e) { "IllegalArgumentException while handling the request" } logger.error(e) { "IllegalArgumentException while handling the request" }
ctx.status(400) ctx.status(400)
ctx.result(e.message ?: "Bad Request") ctx.result(e.message ?: "Bad Request")
} }
app.exception(UnauthorizedException::class.java) { e, ctx -> exception(UnauthorizedException::class.java) { e, ctx ->
logger.error(e) { "UnauthorizedException while handling the request" } logger.error(e) { "UnauthorizedException while handling the request" }
ctx.status(HttpStatus.UNAUTHORIZED) ctx.status(HttpStatus.UNAUTHORIZED)
ctx.result(e.message ?: "Unauthorized") ctx.result(e.message ?: "Unauthorized")
} }
app.exception(ForbiddenException::class.java) { e, ctx -> exception(ForbiddenException::class.java) { e, ctx ->
logger.error(e) { "ForbiddenException while handling the request" } logger.error(e) { "ForbiddenException while handling the request" }
ctx.status(HttpStatus.FORBIDDEN) ctx.status(HttpStatus.FORBIDDEN)
ctx.result(e.message ?: "Forbidden") ctx.result(e.message ?: "Forbidden")
} }
app.start()
} }
// private fun getOpenApiOptions(): OpenApiOptions { // private fun getOpenApiOptions(): OpenApiOptions {
@@ -71,15 +71,14 @@ fun <T> getParam(
is Param.FormParam -> ctx.formParamAsClass(param.key, clazz) is Param.FormParam -> ctx.formParamAsClass(param.key, clazz)
is Param.PathParam -> ctx.pathParamAsClass(param.key, clazz) is Param.PathParam -> ctx.pathParamAsClass(param.key, clazz)
is Param.QueryParam -> ctx.queryParamAsClass(param.key, clazz) is Param.QueryParam -> ctx.queryParamAsClass(param.key, clazz)
else -> throw IllegalStateException("Invalid param")
}.let { }.let {
if (param.nullable) { if (param.nullable) {
it.allowNullable().get() ?: param.defaultValue it.getOrNull() ?: param.defaultValue
} else { } else {
if (param.defaultValue != null) { if (param.defaultValue != null) {
it.getOrDefault(param.defaultValue!!) it.getOrDefault(param.defaultValue!!)
} else { } else {
it.get() it.required().get()
} }
} }
} }
@@ -17,6 +17,7 @@ import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.github.reactivecircus.cache4k.Cache import io.github.reactivecircus.cache4k.Cache
import io.javalin.config.JavalinConfig import io.javalin.config.JavalinConfig
import io.javalin.http.staticfiles.AliasCheck
import io.javalin.http.staticfiles.Location import io.javalin.http.staticfiles.Location
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
@@ -39,7 +40,6 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import net.lingala.zip4j.ZipFile import net.lingala.zip4j.ZipFile
import org.eclipse.jetty.server.handler.ContextHandler
import suwayomi.tachidesk.graphql.types.AboutWebUI import suwayomi.tachidesk.graphql.types.AboutWebUI
import suwayomi.tachidesk.graphql.types.UpdateState import suwayomi.tachidesk.graphql.types.UpdateState
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING
@@ -180,7 +180,7 @@ object WebInterfaceManager {
// Use canonical path to avoid Jetty alias issues // Use canonical path to avoid Jetty alias issues
staticFiles.directory = File(applicationDirs.webUIServe).canonicalPath staticFiles.directory = File(applicationDirs.webUIServe).canonicalPath
staticFiles.location = Location.EXTERNAL staticFiles.location = Location.EXTERNAL
staticFiles.aliasCheck = ContextHandler.ApproveAliases() staticFiles.aliasCheck = AliasCheck { _, _ -> true }
} }
serveWebUI = { serveWebUI = {