diff --git a/build.gradle.kts b/build.gradle.kts index 001247a5..18278ed0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,9 +32,8 @@ dependencies { implementation("org.jetbrains.kotlinx:kotlinx-html-jvm:0.9.1") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0") - implementation("org.eclipse.jetty:jetty-server:11.0.17") - implementation("org.eclipse.jetty:jetty-servlet:11.0.17") - implementation("org.eclipse.jetty.websocket:websocket-jetty-server:11.0.17") + implementation("org.eclipse.jetty:jetty-server:12.0.2") + implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-server:12.0.2") runtimeOnly("org.slf4j:slf4j-simple:2.0.9") runtimeOnly("org.jetbrains.kotlin:kotlin-scripting-jsr223:1.9.10") @@ -43,7 +42,7 @@ dependencies { testImplementation(kotlin("test")) testImplementation("org.junit.jupiter:junit-jupiter-params") - testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.26.1") + testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.27.0") } application { diff --git a/src/main/kotlin/nl/avisi/structurizr/site/generatr/ServeCommand.kt b/src/main/kotlin/nl/avisi/structurizr/site/generatr/ServeCommand.kt index f760e2ec..59a77912 100644 --- a/src/main/kotlin/nl/avisi/structurizr/site/generatr/ServeCommand.kt +++ b/src/main/kotlin/nl/avisi/structurizr/site/generatr/ServeCommand.kt @@ -3,20 +3,23 @@ package nl.avisi.structurizr.site.generatr import com.sun.nio.file.SensitivityWatchEventModifier -import jakarta.servlet.ServletContext import kotlinx.cli.* import nl.avisi.structurizr.site.generatr.site.copySiteWideAssets import nl.avisi.structurizr.site.generatr.site.generateDiagrams import nl.avisi.structurizr.site.generatr.site.generateRedirectingIndexPage import nl.avisi.structurizr.site.generatr.site.generateSite import org.eclipse.jetty.server.Server -import org.eclipse.jetty.servlet.DefaultServlet -import org.eclipse.jetty.servlet.ServletContextHandler -import org.eclipse.jetty.servlet.ServletHolder +import org.eclipse.jetty.server.handler.ContextHandler +import org.eclipse.jetty.server.handler.ContextHandlerCollection +import org.eclipse.jetty.server.handler.ResourceHandler +import org.eclipse.jetty.util.resource.ResourceFactory +import org.eclipse.jetty.websocket.api.Callback import org.eclipse.jetty.websocket.api.Session -import org.eclipse.jetty.websocket.api.WebSocketAdapter -import org.eclipse.jetty.websocket.server.JettyWebSocketServerContainer -import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError +import org.eclipse.jetty.websocket.api.annotations.OnWebSocketOpen +import org.eclipse.jetty.websocket.api.annotations.WebSocket +import org.eclipse.jetty.websocket.server.WebSocketUpgradeHandler import java.io.File import java.nio.file.* import java.time.Duration @@ -104,33 +107,34 @@ class ServeCommand : Subcommand("serve", "Start a development server") { Server(port).also { server -> println("Starting server...") - server.handler = createServletContextHandler() + server.handler = createRootContextHandler(server) server.start() println("Server started") println("Open http://localhost:$port in your browser to view the site") } - private fun createServletContextHandler() = - ServletContextHandler().apply { - contextPath = "/" - addServlet(createStaticResourceServlet(), "/*") - addWebSocketServlet(this, "/_events") + private fun createRootContextHandler(server: Server) = ContextHandlerCollection( + ContextHandler(createStaticResourceHandler(), "/"), + ContextHandler("/").apply { + handler = createWebSocketHandler(server, this, "/_events") } + ) - private fun createStaticResourceServlet() = - ServletHolder("default", DefaultServlet()).apply { - setInitParameter("resourceBase", siteDir) + private fun createStaticResourceHandler() = + ResourceHandler().apply { + baseResource = ResourceFactory.of(this).newResource(siteDir) } - private fun addWebSocketServlet( - context: ServletContextHandler, + private fun createWebSocketHandler( + server: Server, + context: ContextHandler, @Suppress("SameParameterValue") pathSpec: String ) = - JettyWebSocketServletContainerInitializer - .configure(context) { _: ServletContext?, container: JettyWebSocketServerContainer -> + WebSocketUpgradeHandler.from(server, context) + .configure { container -> container.idleTimeout = Duration.ZERO - container.addMapping(pathSpec) { _, _ -> EventSocket() } + container.addMapping(pathSpec) { _, _, _ -> EventSocket() } } private fun startWatchService(): WatchService { @@ -199,24 +203,34 @@ class ServeCommand : Subcommand("serve", "Start a development server") { eventSockets.forEach { it.send(message) } } - private inner class EventSocket : WebSocketAdapter() { - override fun onWebSocketConnect(sess: Session?) { - super.onWebSocketConnect(sess) + @WebSocket + inner class EventSocket { + private var session: Session? = null + + @OnWebSocketOpen + fun onWebSocketConnect(session: Session?) { synchronized(eventSocketsLock) { eventSockets.add(this) } + this.session = session updateSiteError?.let { send(it) } } - override fun onWebSocketClose(statusCode: Int, reason: String?) { + @OnWebSocketClose + fun onWebSocketClose(statusCode: Int, reason: String?) { + session = null synchronized(eventSocketsLock) { eventSockets.remove(this) } } - override fun onWebSocketError(cause: Throwable?) { + @OnWebSocketError + fun onWebSocketError(cause: Throwable?) { + session = null synchronized(eventSocketsLock) { eventSockets.remove(this) } } fun send(message: String) { - if (session != null && session.isOpen) - this.session.remote?.sendString(message) + session?.let { + if (it.isOpen) + it.sendText(message, Callback.NOOP) + } } } } diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/FaviconViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/FaviconViewModelTest.kt index ab1fe611..1e648844 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/FaviconViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/FaviconViewModelTest.kt @@ -1,5 +1,6 @@ package nl.avisi.structurizr.site.generatr.site.model +import assertk.assertFailure import assertk.assertThat import assertk.assertions.* import kotlin.test.Test @@ -27,7 +28,7 @@ class FaviconViewModelTest : ViewModelTest() { "favicon" ) - assertThat { faviconViewModel() }.isFailure().hasMessage("Favicon must be a valid *.ico, *.png of *.gif file") + assertFailure { faviconViewModel() }.hasMessage("Favicon must be a valid *.ico, *.png of *.gif file") } @Test diff --git a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt index 680d0294..b2a6a128 100644 --- a/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt +++ b/src/test/kotlin/nl/avisi/structurizr/site/generatr/site/model/SoftwareSystemDependenciesPageViewModelTest.kt @@ -80,8 +80,13 @@ class SoftwareSystemDependenciesPageViewModelTest : ViewModelTest() { backend2.uses(softwareSystem1, "Uses from container 2 to system 1", "REST") softwareSystem1.uses(backend2, "Uses from system 1 to container 2", "REST") - assertThat { SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) } - .isSuccess() + val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) + // Inbound Table + assertThat(viewModel.dependenciesInboundTable.bodyRows.extractTitle()) + .containsExactly("Software system 2") + // Outbound Table + assertThat(viewModel.dependenciesOutboundTable.bodyRows.extractTitle()) + .containsExactly("Software system 2") } @Test @@ -107,31 +112,11 @@ class SoftwareSystemDependenciesPageViewModelTest : ViewModelTest() { val viewModel = SoftwareSystemDependenciesPageViewModel(generatorContext, softwareSystem1) // Inbound Table - assertThat( - viewModel.dependenciesInboundTable.bodyRows - .map { - when (val source = it.columns[0]) { - is TableViewModel.TextCellViewModel -> source.title - is TableViewModel.LinkCellViewModel -> source.link.title - is TableViewModel.ExternalLinkCellViewModel -> source.link.title - } - } - ).containsExactly( - "Software system 2", "Software system 3" - ) + assertThat(viewModel.dependenciesInboundTable.bodyRows.extractTitle()) + .containsExactly("Software system 2", "Software system 3") // Outbound Table - assertThat( - viewModel.dependenciesInboundTable.bodyRows - .map { - when (val source = it.columns[0]) { - is TableViewModel.TextCellViewModel -> source.title - is TableViewModel.LinkCellViewModel -> source.link.title - is TableViewModel.ExternalLinkCellViewModel -> source.link.title - } - } - ).containsExactly( - "Software system 2", "Software system 3" - ) + assertThat(viewModel.dependenciesInboundTable.bodyRows.extractTitle()) + .containsExactly("Software system 2", "Software system 3") } private fun TableViewModel.TableViewInitializerContext.dependenciesTableHeader() { @@ -141,4 +126,12 @@ class SoftwareSystemDependenciesPageViewModelTest : ViewModelTest() { headerCell("Technology"), ) } + + private fun List.extractTitle() = map { + when (val source = it.columns[0]) { + is TableViewModel.TextCellViewModel -> source.title + is TableViewModel.LinkCellViewModel -> source.link.title + is TableViewModel.ExternalLinkCellViewModel -> source.link.title + } + } }