From 6ee3348f50cb6acbe07e94568fccf908c96198d1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 14:52:00 -0400 Subject: [PATCH] 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 --- gradle/libs.versions.toml | 12 ++-- .../suwayomi/tachidesk/global/impl/WebView.kt | 2 +- .../server/JavalinGraphQLRequestParser.kt | 10 ++- .../ApolloSubscriptionProtocolHandler.kt | 2 +- .../ApolloSubscriptionSessionState.kt | 2 +- .../suwayomi/tachidesk/server/JavalinSetup.kt | 64 ++++++++++--------- .../tachidesk/server/util/DocumentationDsl.kt | 5 +- .../server/util/WebInterfaceManager.kt | 4 +- 8 files changed, 54 insertions(+), 47 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d1e9dd32..2c98da29 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,9 +4,9 @@ coroutines = "1.11.0" serialization = "1.11.0" jvmTarget = "21" okhttp = "5.3.2" # Major version is locked by Tachiyomi extensions -javalin = "6.7.0" +javalin = "7.2.0" 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" dex2jar = "2.4.36" polyglot = "25.0.3" @@ -51,10 +51,10 @@ okio = "com.squareup.okio:okio:3.17.0" # Javalin api javalin-core = { module = "io.javalin:javalin", version.ref = "javalin" } javalin-openapi = { module = "io.javalin:javalin-openapi", version.ref = "javalin" } -javalin-rendering = { module = "io.javalin:javalin-rendering", version.ref = "javalin" } -jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } -jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } -jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations", version.ref = "jackson" } +javalin-rendering = { module = "io.javalin:javalin-rendering-jte", version.ref = "javalin" } +jackson-databind = { module = "tools.jackson.core:jackson-databind", version.ref = "jackson" } +jackson-kotlin = { module = "tools.jackson.module:jackson-module-kotlin", version.ref = "jackson" } +jackson-annotations = "com.fasterxml.jackson.core:jackson-annotations:2.20" jte = { module = "gg.jte:jte", version.ref = "jte" } kte = { module = "gg.jte:jte-kotlin", version.ref = "jte" } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/WebView.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/WebView.kt index ccade4a0..8ef66345 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/WebView.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/WebView.kt @@ -6,7 +6,7 @@ import io.javalin.websocket.WsMessageContext import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable 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 object WebView : Websocket() { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt index 2c0a1ef8..e8fefcb9 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/JavalinGraphQLRequestParser.kt @@ -13,10 +13,14 @@ import com.expediagroup.graphql.server.types.GraphQLRequest import com.expediagroup.graphql.server.types.GraphQLServerRequest import io.javalin.http.Context import io.javalin.http.UploadedFile +import io.javalin.json.JavalinJackson +import io.javalin.json.fromJsonStream import io.javalin.json.fromJsonString import java.io.IOException class JavalinGraphQLRequestParser : GraphQLRequestParser { + val jsonMapper = JavalinJackson() + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") override suspend fun parseRequest(context: Context): GraphQLServerRequest? { return try { @@ -29,17 +33,17 @@ class JavalinGraphQLRequestParser : GraphQLRequestParser { context.formParam("operations") ?: throw IllegalArgumentException("Cannot find 'operations' body") } else { - return context.bodyAsClass(GraphQLServerRequest::class.java) + return context.bodyInputStream().use { jsonMapper.fromJsonStream(it) } } val request = - context.jsonMapper().fromJsonString(formParam) + jsonMapper.fromJsonString(formParam) val map = context .formParam("map") ?.let { - context.jsonMapper().fromJsonString>>(it) + jsonMapper.fromJsonString>>(it) }.orEmpty() val mapItems = diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt index b3f0d494..aea05df0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionProtocolHandler.kt @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.job 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.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_INIT import suwayomi.tachidesk.graphql.server.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_SUBSCRIBE diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt index 332d3b01..cb2f34c7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/subscriptions/ApolloSubscriptionSessionState.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow 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 java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index 51f164db..fa11e9cd 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -13,12 +13,14 @@ import io.github.oshai.kotlinlogging.KotlinLogging import io.javalin.Javalin import io.javalin.apibuilder.ApiBuilder.after import io.javalin.apibuilder.ApiBuilder.path +import io.javalin.config.RoutesConfig import io.javalin.http.Context import io.javalin.http.HandlerType import io.javalin.http.HttpStatus import io.javalin.http.NotFoundResponse import io.javalin.http.RedirectResponse import io.javalin.http.UnauthorizedResponse +import io.javalin.json.JavalinJackson3 import io.javalin.rendering.template.JavalinJte import io.javalin.websocket.WsContext import kotlinx.coroutines.CoroutineScope @@ -47,6 +49,7 @@ import java.net.URLEncoder import java.util.Locale import java.util.concurrent.CompletableFuture import kotlin.concurrent.thread +import kotlin.text.get import kotlin.time.Duration.Companion.days object JavalinSetup { @@ -58,10 +61,12 @@ object JavalinSetup { fun javalinSetup() { val app = - Javalin.create { config -> + Javalin.start { config -> val templateEngine = TemplateEngine.createPrecompiled(ContentType.Html) config.fileRenderer(JavalinJte(templateEngine)) + config.jsonMapper(JavalinJackson3()) + WebInterfaceManager.setup(config) // config.registerPlugin(OpenApiPlugin(getOpenApiOptions())) @@ -104,7 +109,8 @@ object JavalinSetup { } } - config.router.apiBuilder { + config.routes.defineCore() + config.routes.apiBuilder { path(ServerSubpath.maybeAddAsPrefix("api/")) { path("v1/") { GlobalAPI.defineEndpoints() @@ -117,17 +123,32 @@ object JavalinSetup { after { ctx -> // If not matched, the request was for an invalid endpoint // Return a 404 instead of redirecting to the UI for usability - if (ctx.endpointHandlerPath() == "*") { + if (ctx.endpoints().lastHttpEndpoint()?.path == "*") { 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") - app.get(loginPath) { ctx -> + get(loginPath) { ctx -> val locale: Locale = LocalizationHelper.ctxToLocale(ctx) ctx.header("content-type", "text/html") val httpCacheSeconds = 1.days.inWholeSeconds @@ -141,7 +162,7 @@ object JavalinSetup { ) } - app.post(loginPath) { ctx -> + post(loginPath) { ctx -> val username = ctx.formParam("user") val password = ctx.formParam("pass") val isValid = @@ -174,7 +195,7 @@ object JavalinSetup { ) } - app.beforeMatched { ctx -> + beforeMatched { ctx -> val isWebManifest = listOf("site.webmanifest", "manifest.json", "login.html").any { ctx.path().endsWith(it) } val isPageIcon = @@ -219,60 +240,43 @@ object JavalinSetup { ctx.setAttribute(Attribute.TachideskBasic, credentialsValid()) } - app.events { event -> - event.serverStarted { - if (serverConfig.initialOpenInBrowserEnabled.value) { - Browser.openInBrowser() - } - } - } - - app.wsBefore { + wsBefore { it.onConnect { ctx -> ctx.setAttribute(Attribute.TachideskUser, getUserFromWsContext(ctx)) } } - // when JVM is prompted to shutdown, stop javalin gracefully - Runtime.getRuntime().addShutdownHook( - thread(start = false) { - app.stop() - }, - ) - - app.exception(NullPointerException::class.java) { e, ctx -> + exception(NullPointerException::class.java) { e, ctx -> logger.error(e) { "NullPointerException while handling the request" } ctx.status(404) } - app.exception(NoSuchElementException::class.java) { e, ctx -> + exception(NoSuchElementException::class.java) { e, ctx -> logger.error(e) { "NoSuchElementException while handling the request" } ctx.status(404) } - app.exception(IOException::class.java) { e, ctx -> + exception(IOException::class.java) { e, ctx -> logger.error(e) { "IOException while handling the request" } ctx.status(500) 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" } ctx.status(400) 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" } ctx.status(HttpStatus.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" } ctx.status(HttpStatus.FORBIDDEN) ctx.result(e.message ?: "Forbidden") } - - app.start() } // private fun getOpenApiOptions(): OpenApiOptions { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/DocumentationDsl.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/DocumentationDsl.kt index 3cdbe9a3..011c8ace 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/DocumentationDsl.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/DocumentationDsl.kt @@ -71,15 +71,14 @@ fun getParam( is Param.FormParam -> ctx.formParamAsClass(param.key, clazz) is Param.PathParam -> ctx.pathParamAsClass(param.key, clazz) is Param.QueryParam -> ctx.queryParamAsClass(param.key, clazz) - else -> throw IllegalStateException("Invalid param") }.let { if (param.nullable) { - it.allowNullable().get() ?: param.defaultValue + it.getOrNull() ?: param.defaultValue } else { if (param.defaultValue != null) { it.getOrDefault(param.defaultValue!!) } else { - it.get() + it.required().get() } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt index 5d5f7dd5..ba3ae653 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt @@ -17,6 +17,7 @@ import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import io.github.reactivecircus.cache4k.Cache import io.javalin.config.JavalinConfig +import io.javalin.http.staticfiles.AliasCheck import io.javalin.http.staticfiles.Location import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi @@ -39,7 +40,6 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import net.lingala.zip4j.ZipFile -import org.eclipse.jetty.server.handler.ContextHandler import suwayomi.tachidesk.graphql.types.AboutWebUI import suwayomi.tachidesk.graphql.types.UpdateState import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING @@ -180,7 +180,7 @@ object WebInterfaceManager { // Use canonical path to avoid Jetty alias issues staticFiles.directory = File(applicationDirs.webUIServe).canonicalPath staticFiles.location = Location.EXTERNAL - staticFiles.aliasCheck = ContextHandler.ApproveAliases() + staticFiles.aliasCheck = AliasCheck { _, _ -> true } } serveWebUI = {