diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5eb9685e..86828b7a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,5 +11,5 @@ jobs: - uses: actions/checkout@v1 - uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - run: sbt javafmtCheckAll scalafmtCheckAll "scalafixAll --check" test publishLocal scripted diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff0eaa10..c9d699c4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,3 +100,13 @@ $ git push --tags origin Update the [Giter8 template](https://github.com/earldouglas/sbt-war.g8) to use the new version. + +## Reference + +* Servlet version history: + +* Jetty version history: + +* Tomcat version history: + + diff --git a/build.sbt b/build.sbt index b0e8a27d..b140c7ca 100644 --- a/build.sbt +++ b/build.sbt @@ -20,14 +20,14 @@ lazy val warRunner_3_0 = Compile / compile / javacOptions ++= Seq( "-source", - "11", + "17", "-target", - "11", + "17", "-g:lines" ), crossPaths := false, // exclude Scala suffix from artifact names autoScalaLibrary := false, // exclude scala-library from dependencies - libraryDependencies += "com.github.jsimone" % "webapp-runner" % "7.0.91.0" + libraryDependencies += "org.eclipse.jetty" % "jetty-webapp" % "8.1.22.v20160922" ) lazy val warRunner_3_1 = @@ -39,14 +39,14 @@ lazy val warRunner_3_1 = Compile / compile / javacOptions ++= Seq( "-source", - "11", + "17", "-target", - "11", + "17", "-g:lines" ), crossPaths := false, // exclude Scala suffix from artifact names autoScalaLibrary := false, // exclude scala-library from dependencies - libraryDependencies += "com.heroku" % "webapp-runner" % "8.5.68.1" + libraryDependencies += "org.eclipse.jetty" % "jetty-webapp" % "9.4.56.v20240826" ) lazy val warRunner_4_0 = @@ -58,14 +58,33 @@ lazy val warRunner_4_0 = Compile / compile / javacOptions ++= Seq( "-source", - "11", + "17", "-target", - "11", + "17", "-g:lines" ), crossPaths := false, // exclude Scala suffix from artifact names autoScalaLibrary := false, // exclude scala-library from dependencies - libraryDependencies += "com.heroku" % "webapp-runner" % "9.0.96.0" + libraryDependencies += "org.eclipse.jetty" % "jetty-webapp" % "10.0.24" + ) + +lazy val warRunner_5_0 = + project + .in(file("runners/5.0")) + .settings( + name := "war-runner", + version := version.value + "_5.0", + Compile / compile / javacOptions ++= + Seq( + "-source", + "17", + "-target", + "17", + "-g:lines" + ), + crossPaths := false, // exclude Scala suffix from artifact names + autoScalaLibrary := false, // exclude scala-library from dependencies + libraryDependencies += "org.eclipse.jetty" % "jetty-webapp" % "11.0.24" ) lazy val warRunner_6_0 = @@ -77,14 +96,14 @@ lazy val warRunner_6_0 = Compile / compile / javacOptions ++= Seq( "-source", - "11", + "17", "-target", - "11", + "17", "-g:lines" ), crossPaths := false, // exclude Scala suffix from artifact names autoScalaLibrary := false, // exclude scala-library from dependencies - libraryDependencies += "com.heroku" % "webapp-runner" % "10.1.31.0" + libraryDependencies += "org.eclipse.jetty.ee10" % "jetty-ee10-webapp" % "12.0.15" ) lazy val sbtWar = @@ -113,7 +132,8 @@ lazy val sbtWar = warRunner_3_0, warRunner_3_1, warRunner_4_0, - warRunner_6_0 + warRunner_5_0, + warRunner_6_0, ) // Publish to Sonatype, https://www.scala-sbt.org/release/docs/Using-Sonatype.html diff --git a/runners/3.0/src/main/java/com/earldouglas/WarRunner.java b/runners/3.0/src/main/java/com/earldouglas/WarRunner.java index a9b9f444..61565245 100644 --- a/runners/3.0/src/main/java/com/earldouglas/WarRunner.java +++ b/runners/3.0/src/main/java/com/earldouglas/WarRunner.java @@ -1,5 +1,12 @@ package com.earldouglas; +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.server.Server; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + public class WarRunner { /** @@ -12,11 +19,21 @@ public static void main(final String[] args) throws Exception { final WarConfiguration warConfiguration = WarConfiguration.load(args[0]); - final String[] warRunnerArgs = - new String[] { - "--port", Integer.toString(warConfiguration.port), warConfiguration.warFile.getPath(), - }; + final Path warPath = + Paths + .get(warConfiguration.warFile.getPath()) + .toAbsolutePath() + .normalize(); + + final Server server = new Server(warConfiguration.port); + + final WebAppContext webapp = new WebAppContext(); + webapp.setContextPath("/"); + webapp.setWar(warPath.toUri().toASCIIString()); + + server.setHandler(webapp); - webapp.runner.launch.Main.main(warRunnerArgs); + server.start(); + server.join(); } } diff --git a/runners/3.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml b/runners/3.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..d6a9c866 --- /dev/null +++ b/runners/3.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,23 @@ + + + Hello, Servlet 3.0 + + + HelloServlet + com.earldouglas.HelloServlet + + + + HelloServlet + /hello + + + + default + / + + + diff --git a/runners/3.0/src/test/java/com/earldouglas/HelloServlet.java b/runners/3.0/src/test/java/com/earldouglas/HelloServlet.java new file mode 100644 index 00000000..9799d9f2 --- /dev/null +++ b/runners/3.0/src/test/java/com/earldouglas/HelloServlet.java @@ -0,0 +1,21 @@ +package com.earldouglas; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class HelloServlet extends HttpServlet { + + @Override + protected void doGet( + final HttpServletRequest request, + final HttpServletResponse response + ) throws IOException { + response.setContentType("text/plain"); + response.getWriter().println("Hello, world!"); + } +} diff --git a/runners/3.0/src/test/scala/com/earldouglas/WarRunnerTest.scala b/runners/3.0/src/test/scala/com/earldouglas/WarRunnerTest.scala index 955f1e4b..02ef8ea3 100644 --- a/runners/3.0/src/test/scala/com/earldouglas/WarRunnerTest.scala +++ b/runners/3.0/src/test/scala/com/earldouglas/WarRunnerTest.scala @@ -11,6 +11,8 @@ class WarRunnerTest override def beforeAll(): Unit = { + new com.earldouglas.HelloServlet() + val thread: Thread = new Thread { override def run(): Unit = { @@ -44,7 +46,7 @@ class WarRunnerTest awaitOpen(8988) } - test("/foo.html") { + test("GET /foo.html") { val expected: HttpClient.Response = HttpClient.Response( @@ -71,7 +73,7 @@ class WarRunnerTest ) shouldBe expected } - test("/bar.html") { + test("GET /bar.html") { val expected: HttpClient.Response = HttpClient.Response( @@ -98,7 +100,7 @@ class WarRunnerTest ) shouldBe expected } - test("/baz/raz.css") { + test("GET /baz/raz.css") { val expected: HttpClient.Response = HttpClient.Response( @@ -124,4 +126,37 @@ class WarRunnerTest } ) shouldBe expected } + + test("GET /hello") { + + val expected: HttpClient.Response = + HttpClient.Response( + status = 200, + headers = Map( + "Content-Type" -> "text/plain" + ), + body = """|Hello, world! + |""".stripMargin + ) + + val obtained: HttpClient.Response = + HttpClient.request( + method = "GET", + url = "http://localhost:8988/hello", + headers = Map.empty, + body = None + ) + + obtained.copy( + headers = + obtained + .headers + .filter { case (k, _) => + k == "Content-Type" + } + .map { case (k, v) => + (k, v.replaceAll(";charset=.*", "")) + } + ) shouldBe expected + } } diff --git a/runners/3.1/src/main/java/com/earldouglas/WarRunner.java b/runners/3.1/src/main/java/com/earldouglas/WarRunner.java index a9b9f444..61565245 100644 --- a/runners/3.1/src/main/java/com/earldouglas/WarRunner.java +++ b/runners/3.1/src/main/java/com/earldouglas/WarRunner.java @@ -1,5 +1,12 @@ package com.earldouglas; +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.server.Server; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + public class WarRunner { /** @@ -12,11 +19,21 @@ public static void main(final String[] args) throws Exception { final WarConfiguration warConfiguration = WarConfiguration.load(args[0]); - final String[] warRunnerArgs = - new String[] { - "--port", Integer.toString(warConfiguration.port), warConfiguration.warFile.getPath(), - }; + final Path warPath = + Paths + .get(warConfiguration.warFile.getPath()) + .toAbsolutePath() + .normalize(); + + final Server server = new Server(warConfiguration.port); + + final WebAppContext webapp = new WebAppContext(); + webapp.setContextPath("/"); + webapp.setWar(warPath.toUri().toASCIIString()); + + server.setHandler(webapp); - webapp.runner.launch.Main.main(warRunnerArgs); + server.start(); + server.join(); } } diff --git a/runners/3.1/src/main/java/com/earldouglas/WebappComponentsConfiguration.java b/runners/3.1/src/main/java/com/earldouglas/WebappComponentsConfiguration.java deleted file mode 100644 index df85305b..00000000 --- a/runners/3.1/src/main/java/com/earldouglas/WebappComponentsConfiguration.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.earldouglas; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; - -/** Specifies server settings and components locations for running a webapp in-place. */ -public class WebappComponentsConfiguration { - - /** The hostname to use for the server, e.g. "localhost" */ - public final String hostname; - - /** The port to use for the server, e.g. 8080 */ - public final int port; - - /** - * The context path to use for the webapp. - * - *

For the root context path, use the empty string "". - */ - public final String contextPath; - - /** - * An empty directory that Tomcat requires to run. Represents the resources directory, but it can - * be any empty directory. - */ - public final File emptyWebappDir; - - /** - * An empty directory that Tomcat requires to run. Represents the WEB-INF/classes directory, but - * it can be any empty directory. - */ - public final File emptyClassesDir; - - /** - * The map of resources to serve. - * - *

The mapping is from source to destination, where: - * - *

    - *
  • source is the relative path within the webapp (e.g. index.html, WEB-INF/web.xml) - *
  • destination is the file on disk to serve - *
- */ - public final Map resourceMap; - - /** - * Read configuration from a file at the specified location. - * - * @param configurationFilename the configuration filename to load - * @throws IOException if something goes wrong - * @return WebappComponentsConfiguration a loaded configuration - */ - public static WebappComponentsConfiguration load(final String configurationFilename) - throws IOException { - return WebappComponentsConfiguration.load(new File(configurationFilename)); - } - - private static Map parseResourceMap(final String raw) throws IOException { - - final Map resourceMap = new HashMap(); - - final String[] rows = raw.split(","); - - for (int rowIndex = 0; rowIndex < rows.length; rowIndex++) { - final String[] columns = rows[rowIndex].split("->"); - resourceMap.put(columns[0], new File(columns[1])); - } - - return resourceMap; - } - - /** - * Read configuration from a file at the specified location. - * - *

The format of the file is a Properties file with the following fields: - * - *

    - *
  • hostname - *
  • port - *
  • contextPath - *
  • emptyWebappDir - *
  • emptyClassesDir - *
  • resourceMap - *
- * - * The resourceMap field is a list, concatenated by commas, of source/destination pairs, delimited - * by ->. - * - *

Example: - * - *

-   * hostname=localhost
-   * port=8989
-   * contextPath=
-   * emptyWebappDir=target/empty
-   * emptyClassesDir=target/empty
-   * resourceMap=bar.html->src/test/fakeproject/src/main/webapp/bar.html,\
-   *             foo.html->src/test/fakeproject/src/main/webapp/foo.html,\
-   *             baz/raz.css->src/test/fakeproject/src/main/webapp/baz/raz.css
-   * 
- * - * @param configurationFile the configuration file to load - * @throws IOException if something goes wrong - * @return WebappComponentsConfiguration a loaded configuration - */ - public static WebappComponentsConfiguration load(final File configurationFile) - throws IOException { - - final InputStream inputStream = new FileInputStream(configurationFile); - - final Properties properties = new Properties(); - properties.load(inputStream); - - final Map resourceMap = parseResourceMap(properties.getProperty("resourceMap")); - - return new WebappComponentsConfiguration( - properties.getProperty("hostname"), - Integer.parseInt(properties.getProperty("port")), - properties.getProperty("contextPath"), - new File(properties.getProperty("emptyWebappDir")), - new File(properties.getProperty("emptyClassesDir")), - resourceMap); - } - - /** - * Construct a new configuration from the given parameters. - * - * @param hostname the hostname to use for the server, e.g. "localhost" - * @param port the port to use for the server, e.g. 8080 - * @param contextPath the context path to use for the webapp, e.g. "" - * @param emptyWebappDir an empty directory that Tomcat requires to run - * @param emptyClassesDir an empty directory that Tomcat requires to run - * @param resourceMap the map of resources to serve - */ - public WebappComponentsConfiguration( - final String hostname, - final int port, - final String contextPath, - final File emptyWebappDir, - final File emptyClassesDir, - final Map resourceMap) { - this.hostname = hostname; - this.port = port; - this.contextPath = contextPath; - this.emptyWebappDir = emptyWebappDir; - this.emptyClassesDir = emptyClassesDir; - this.resourceMap = resourceMap; - } -} diff --git a/runners/3.1/src/main/java/com/earldouglas/WebappComponentsRunner.java b/runners/3.1/src/main/java/com/earldouglas/WebappComponentsRunner.java index dbd59381..519d0007 100644 --- a/runners/3.1/src/main/java/com/earldouglas/WebappComponentsRunner.java +++ b/runners/3.1/src/main/java/com/earldouglas/WebappComponentsRunner.java @@ -1,48 +1,10 @@ package com.earldouglas; -import java.io.File; import java.io.IOException; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import org.apache.catalina.Context; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.WebResourceRoot; -import org.apache.catalina.connector.Connector; -import org.apache.catalina.startup.Tomcat; -import org.apache.catalina.webresources.DirResourceSet; -import org.apache.catalina.webresources.FileResourceSet; -import org.apache.catalina.webresources.StandardRoot; -/** - * Launches a webapp composed of in-place resources, classes, and libraries. - * - *

emptyWebappDir and emptyClassesDir need to point to one or two empty directories. They're not - * used to serve any content, but they are required by Tomcat's internals. - * - *

To use a root context path (i.e. /), set contextPath to the empty string for some reason. - */ +/** Launches a webapp composed of in-place resources, classes, and libraries. */ public class WebappComponentsRunner { - private static File mkdir(final File file) throws IOException { - if (file.exists()) { - if (!file.isDirectory()) { - throw new FileAlreadyExistsException(file.getPath()); - } else { - return file; - } - } else { - final Path path = FileSystems.getDefault().getPath(file.getPath()); - try { - Files.createDirectory(path); - return file; - } catch (FileAlreadyExistsException e) { - return file; - } - } - } - /** * Load configuration from the file in the first argument, and use it to start a new * WebappComponentsRunner. @@ -51,105 +13,6 @@ private static File mkdir(final File file) throws IOException { * @throws IOException if something goes wrong */ public static void main(final String[] args) throws IOException { - - final WebappComponentsConfiguration webappComponentsConfiguration = - WebappComponentsConfiguration.load(args[0]); - - final WebappComponentsRunner webappComponentsRunner = - new WebappComponentsRunner(webappComponentsConfiguration); - - webappComponentsRunner.start.run(); - webappComponentsRunner.join.run(); - } - - /** A handle for starting the instance's server. */ - public final Runnable start; - - /** A handle for joining the instance's server. */ - public final Runnable join; - - /** A handle for stopping the instance's server. */ - public final Runnable stop; - - /** - * Construct a new WebappComponentsRunner using the given configuration. - * - * @param configuration the configuration to run - * @throws IOException if something goes wrong - */ - public WebappComponentsRunner(final WebappComponentsConfiguration configuration) - throws IOException { - - mkdir(configuration.emptyWebappDir); - mkdir(configuration.emptyClassesDir); - - final Tomcat tomcat = new Tomcat(); - tomcat.setHostname(configuration.hostname); - - final Connector connector = new Connector(); - connector.setPort(configuration.port); - tomcat.setConnector(connector); - - final Context context = - tomcat.addWebapp(configuration.contextPath, configuration.emptyWebappDir.getAbsolutePath()); - - final WebResourceRoot webResourceRoot = new StandardRoot(context); - - webResourceRoot.addJarResources( - new DirResourceSet( - webResourceRoot, - "/WEB-INF/classes", - configuration.emptyClassesDir.getAbsolutePath(), - "/")); - - configuration.resourceMap.forEach( - (path, file) -> { - if (file.exists() && file.isFile()) { - webResourceRoot.addJarResources( - new FileResourceSet(webResourceRoot, "/" + path, file.getAbsolutePath(), "/")); - } - }); - - context.setResources(webResourceRoot); - - start = - new Runnable() { - @Override - public void run() { - try { - tomcat.start(); - } catch (final LifecycleException e) { - throw new RuntimeException(e); - } - } - }; - - join = - new Runnable() { - @Override - public void run() { - tomcat.getServer().await(); - } - }; - - stop = - new Runnable() { - @Override - public void run() { - try { - - connector.stop(); - context.stop(); - tomcat.stop(); - - connector.destroy(); - context.destroy(); - tomcat.destroy(); - - } catch (final LifecycleException e) { - throw new RuntimeException(e); - } - } - }; + throw new RuntimeException("unsupported"); } } diff --git a/runners/3.1/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml b/runners/3.1/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..b67be7a2 --- /dev/null +++ b/runners/3.1/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,23 @@ + + + Hello, Servlet 3.1 + + + HelloServlet + com.earldouglas.HelloServlet + + + + HelloServlet + /hello + + + + default + / + + + diff --git a/runners/3.1/src/test/java/com/earldouglas/HelloServlet.java b/runners/3.1/src/test/java/com/earldouglas/HelloServlet.java new file mode 100644 index 00000000..9799d9f2 --- /dev/null +++ b/runners/3.1/src/test/java/com/earldouglas/HelloServlet.java @@ -0,0 +1,21 @@ +package com.earldouglas; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class HelloServlet extends HttpServlet { + + @Override + protected void doGet( + final HttpServletRequest request, + final HttpServletResponse response + ) throws IOException { + response.setContentType("text/plain"); + response.getWriter().println("Hello, world!"); + } +} diff --git a/runners/3.1/src/test/scala/com/earldouglas/WarRunnerTest.scala b/runners/3.1/src/test/scala/com/earldouglas/WarRunnerTest.scala index 955f1e4b..02ef8ea3 100644 --- a/runners/3.1/src/test/scala/com/earldouglas/WarRunnerTest.scala +++ b/runners/3.1/src/test/scala/com/earldouglas/WarRunnerTest.scala @@ -11,6 +11,8 @@ class WarRunnerTest override def beforeAll(): Unit = { + new com.earldouglas.HelloServlet() + val thread: Thread = new Thread { override def run(): Unit = { @@ -44,7 +46,7 @@ class WarRunnerTest awaitOpen(8988) } - test("/foo.html") { + test("GET /foo.html") { val expected: HttpClient.Response = HttpClient.Response( @@ -71,7 +73,7 @@ class WarRunnerTest ) shouldBe expected } - test("/bar.html") { + test("GET /bar.html") { val expected: HttpClient.Response = HttpClient.Response( @@ -98,7 +100,7 @@ class WarRunnerTest ) shouldBe expected } - test("/baz/raz.css") { + test("GET /baz/raz.css") { val expected: HttpClient.Response = HttpClient.Response( @@ -124,4 +126,37 @@ class WarRunnerTest } ) shouldBe expected } + + test("GET /hello") { + + val expected: HttpClient.Response = + HttpClient.Response( + status = 200, + headers = Map( + "Content-Type" -> "text/plain" + ), + body = """|Hello, world! + |""".stripMargin + ) + + val obtained: HttpClient.Response = + HttpClient.request( + method = "GET", + url = "http://localhost:8988/hello", + headers = Map.empty, + body = None + ) + + obtained.copy( + headers = + obtained + .headers + .filter { case (k, _) => + k == "Content-Type" + } + .map { case (k, v) => + (k, v.replaceAll(";charset=.*", "")) + } + ) shouldBe expected + } } diff --git a/runners/3.1/src/test/scala/com/earldouglas/WebappComponentsConfigurationTest.scala b/runners/3.1/src/test/scala/com/earldouglas/WebappComponentsConfigurationTest.scala deleted file mode 100644 index fd6dc832..00000000 --- a/runners/3.1/src/test/scala/com/earldouglas/WebappComponentsConfigurationTest.scala +++ /dev/null @@ -1,32 +0,0 @@ -package com.earldouglas - -import org.scalatest.BeforeAndAfterAll -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -import java.io.File -import scala.collection.JavaConverters._ - -class WebappComponentsConfigurationTest - extends AnyFunSuite - with Matchers - with BeforeAndAfterAll { - - test("load") { - - val configuration: WebappComponentsConfiguration = - WebappComponentsConfiguration - .load("src/test/resources/webapp-components.properties") - - configuration.hostname shouldBe "localhost" - configuration.port shouldBe 8989 - configuration.emptyWebappDir shouldBe (new File("target/empty")) - configuration.emptyClassesDir shouldBe (new File("target/empty")) - configuration.resourceMap.asScala shouldBe - List("bar.html", "foo.html", "baz/raz.css") - .map(x => - (x -> new File(s"src/test/fakeproject/src/main/webapp/${x}")) - ) - .toMap - } -} diff --git a/runners/3.1/src/test/scala/com/earldouglas/WebappComponentsRunnerTest.scala b/runners/3.1/src/test/scala/com/earldouglas/WebappComponentsRunnerTest.scala deleted file mode 100644 index 79a75bbd..00000000 --- a/runners/3.1/src/test/scala/com/earldouglas/WebappComponentsRunnerTest.scala +++ /dev/null @@ -1,107 +0,0 @@ -package com.earldouglas - -import org.scalatest.BeforeAndAfterAll -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class WebappComponentsRunnerTest - extends AnyFunSuite - with Matchers - with BeforeAndAfterAll { - - lazy val configuration: WebappComponentsConfiguration = - WebappComponentsConfiguration - .load("src/test/resources/webapp-components.properties") - - lazy val runner: WebappComponentsRunner = - new WebappComponentsRunner(configuration) - - override def beforeAll(): Unit = { - runner.start.run() - } - - override def afterAll(): Unit = { - runner.stop.run() - } - - test("/foo.html") { - - val expected: HttpClient.Response = - HttpClient.Response( - status = 200, - headers = Map( - "Content-Type" -> "text/html" - ), - body = """|foo - |""".stripMargin - ) - - val obtained: HttpClient.Response = - HttpClient.request( - method = "GET", - url = "http://localhost:8989/foo.html", - headers = Map.empty, - body = None - ) - - obtained.copy( - headers = obtained.headers.filter { case (k, _) => - k == "Content-Type" - } - ) shouldBe expected - } - - test("/bar.html") { - - val expected: HttpClient.Response = - HttpClient.Response( - status = 200, - headers = Map( - "Content-Type" -> "text/html" - ), - body = """|bar - |""".stripMargin - ) - - val obtained: HttpClient.Response = - HttpClient.request( - method = "GET", - url = "http://localhost:8989/bar.html", - headers = Map.empty, - body = None - ) - - obtained.copy( - headers = obtained.headers.filter { case (k, _) => - k == "Content-Type" - } - ) shouldBe expected - } - - test("/baz/raz.css") { - - val expected: HttpClient.Response = - HttpClient.Response( - status = 200, - headers = Map( - "Content-Type" -> "text/css" - ), - body = """|div.raz { font-weight: bold; } - |""".stripMargin - ) - - val obtained: HttpClient.Response = - HttpClient.request( - method = "GET", - url = "http://localhost:8989/baz/raz.css", - headers = Map.empty, - body = None - ) - - obtained.copy( - headers = obtained.headers.filter { case (k, _) => - k == "Content-Type" - } - ) shouldBe expected - } -} diff --git a/runners/4.0/src/main/java/com/earldouglas/WarRunner.java b/runners/4.0/src/main/java/com/earldouglas/WarRunner.java index a9b9f444..61565245 100644 --- a/runners/4.0/src/main/java/com/earldouglas/WarRunner.java +++ b/runners/4.0/src/main/java/com/earldouglas/WarRunner.java @@ -1,5 +1,12 @@ package com.earldouglas; +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.server.Server; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + public class WarRunner { /** @@ -12,11 +19,21 @@ public static void main(final String[] args) throws Exception { final WarConfiguration warConfiguration = WarConfiguration.load(args[0]); - final String[] warRunnerArgs = - new String[] { - "--port", Integer.toString(warConfiguration.port), warConfiguration.warFile.getPath(), - }; + final Path warPath = + Paths + .get(warConfiguration.warFile.getPath()) + .toAbsolutePath() + .normalize(); + + final Server server = new Server(warConfiguration.port); + + final WebAppContext webapp = new WebAppContext(); + webapp.setContextPath("/"); + webapp.setWar(warPath.toUri().toASCIIString()); + + server.setHandler(webapp); - webapp.runner.launch.Main.main(warRunnerArgs); + server.start(); + server.join(); } } diff --git a/runners/4.0/src/main/java/com/earldouglas/WebappComponentsConfiguration.java b/runners/4.0/src/main/java/com/earldouglas/WebappComponentsConfiguration.java deleted file mode 100644 index df85305b..00000000 --- a/runners/4.0/src/main/java/com/earldouglas/WebappComponentsConfiguration.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.earldouglas; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; - -/** Specifies server settings and components locations for running a webapp in-place. */ -public class WebappComponentsConfiguration { - - /** The hostname to use for the server, e.g. "localhost" */ - public final String hostname; - - /** The port to use for the server, e.g. 8080 */ - public final int port; - - /** - * The context path to use for the webapp. - * - *

For the root context path, use the empty string "". - */ - public final String contextPath; - - /** - * An empty directory that Tomcat requires to run. Represents the resources directory, but it can - * be any empty directory. - */ - public final File emptyWebappDir; - - /** - * An empty directory that Tomcat requires to run. Represents the WEB-INF/classes directory, but - * it can be any empty directory. - */ - public final File emptyClassesDir; - - /** - * The map of resources to serve. - * - *

The mapping is from source to destination, where: - * - *

    - *
  • source is the relative path within the webapp (e.g. index.html, WEB-INF/web.xml) - *
  • destination is the file on disk to serve - *
- */ - public final Map resourceMap; - - /** - * Read configuration from a file at the specified location. - * - * @param configurationFilename the configuration filename to load - * @throws IOException if something goes wrong - * @return WebappComponentsConfiguration a loaded configuration - */ - public static WebappComponentsConfiguration load(final String configurationFilename) - throws IOException { - return WebappComponentsConfiguration.load(new File(configurationFilename)); - } - - private static Map parseResourceMap(final String raw) throws IOException { - - final Map resourceMap = new HashMap(); - - final String[] rows = raw.split(","); - - for (int rowIndex = 0; rowIndex < rows.length; rowIndex++) { - final String[] columns = rows[rowIndex].split("->"); - resourceMap.put(columns[0], new File(columns[1])); - } - - return resourceMap; - } - - /** - * Read configuration from a file at the specified location. - * - *

The format of the file is a Properties file with the following fields: - * - *

    - *
  • hostname - *
  • port - *
  • contextPath - *
  • emptyWebappDir - *
  • emptyClassesDir - *
  • resourceMap - *
- * - * The resourceMap field is a list, concatenated by commas, of source/destination pairs, delimited - * by ->. - * - *

Example: - * - *

-   * hostname=localhost
-   * port=8989
-   * contextPath=
-   * emptyWebappDir=target/empty
-   * emptyClassesDir=target/empty
-   * resourceMap=bar.html->src/test/fakeproject/src/main/webapp/bar.html,\
-   *             foo.html->src/test/fakeproject/src/main/webapp/foo.html,\
-   *             baz/raz.css->src/test/fakeproject/src/main/webapp/baz/raz.css
-   * 
- * - * @param configurationFile the configuration file to load - * @throws IOException if something goes wrong - * @return WebappComponentsConfiguration a loaded configuration - */ - public static WebappComponentsConfiguration load(final File configurationFile) - throws IOException { - - final InputStream inputStream = new FileInputStream(configurationFile); - - final Properties properties = new Properties(); - properties.load(inputStream); - - final Map resourceMap = parseResourceMap(properties.getProperty("resourceMap")); - - return new WebappComponentsConfiguration( - properties.getProperty("hostname"), - Integer.parseInt(properties.getProperty("port")), - properties.getProperty("contextPath"), - new File(properties.getProperty("emptyWebappDir")), - new File(properties.getProperty("emptyClassesDir")), - resourceMap); - } - - /** - * Construct a new configuration from the given parameters. - * - * @param hostname the hostname to use for the server, e.g. "localhost" - * @param port the port to use for the server, e.g. 8080 - * @param contextPath the context path to use for the webapp, e.g. "" - * @param emptyWebappDir an empty directory that Tomcat requires to run - * @param emptyClassesDir an empty directory that Tomcat requires to run - * @param resourceMap the map of resources to serve - */ - public WebappComponentsConfiguration( - final String hostname, - final int port, - final String contextPath, - final File emptyWebappDir, - final File emptyClassesDir, - final Map resourceMap) { - this.hostname = hostname; - this.port = port; - this.contextPath = contextPath; - this.emptyWebappDir = emptyWebappDir; - this.emptyClassesDir = emptyClassesDir; - this.resourceMap = resourceMap; - } -} diff --git a/runners/4.0/src/main/java/com/earldouglas/WebappComponentsRunner.java b/runners/4.0/src/main/java/com/earldouglas/WebappComponentsRunner.java index dbd59381..519d0007 100644 --- a/runners/4.0/src/main/java/com/earldouglas/WebappComponentsRunner.java +++ b/runners/4.0/src/main/java/com/earldouglas/WebappComponentsRunner.java @@ -1,48 +1,10 @@ package com.earldouglas; -import java.io.File; import java.io.IOException; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import org.apache.catalina.Context; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.WebResourceRoot; -import org.apache.catalina.connector.Connector; -import org.apache.catalina.startup.Tomcat; -import org.apache.catalina.webresources.DirResourceSet; -import org.apache.catalina.webresources.FileResourceSet; -import org.apache.catalina.webresources.StandardRoot; -/** - * Launches a webapp composed of in-place resources, classes, and libraries. - * - *

emptyWebappDir and emptyClassesDir need to point to one or two empty directories. They're not - * used to serve any content, but they are required by Tomcat's internals. - * - *

To use a root context path (i.e. /), set contextPath to the empty string for some reason. - */ +/** Launches a webapp composed of in-place resources, classes, and libraries. */ public class WebappComponentsRunner { - private static File mkdir(final File file) throws IOException { - if (file.exists()) { - if (!file.isDirectory()) { - throw new FileAlreadyExistsException(file.getPath()); - } else { - return file; - } - } else { - final Path path = FileSystems.getDefault().getPath(file.getPath()); - try { - Files.createDirectory(path); - return file; - } catch (FileAlreadyExistsException e) { - return file; - } - } - } - /** * Load configuration from the file in the first argument, and use it to start a new * WebappComponentsRunner. @@ -51,105 +13,6 @@ private static File mkdir(final File file) throws IOException { * @throws IOException if something goes wrong */ public static void main(final String[] args) throws IOException { - - final WebappComponentsConfiguration webappComponentsConfiguration = - WebappComponentsConfiguration.load(args[0]); - - final WebappComponentsRunner webappComponentsRunner = - new WebappComponentsRunner(webappComponentsConfiguration); - - webappComponentsRunner.start.run(); - webappComponentsRunner.join.run(); - } - - /** A handle for starting the instance's server. */ - public final Runnable start; - - /** A handle for joining the instance's server. */ - public final Runnable join; - - /** A handle for stopping the instance's server. */ - public final Runnable stop; - - /** - * Construct a new WebappComponentsRunner using the given configuration. - * - * @param configuration the configuration to run - * @throws IOException if something goes wrong - */ - public WebappComponentsRunner(final WebappComponentsConfiguration configuration) - throws IOException { - - mkdir(configuration.emptyWebappDir); - mkdir(configuration.emptyClassesDir); - - final Tomcat tomcat = new Tomcat(); - tomcat.setHostname(configuration.hostname); - - final Connector connector = new Connector(); - connector.setPort(configuration.port); - tomcat.setConnector(connector); - - final Context context = - tomcat.addWebapp(configuration.contextPath, configuration.emptyWebappDir.getAbsolutePath()); - - final WebResourceRoot webResourceRoot = new StandardRoot(context); - - webResourceRoot.addJarResources( - new DirResourceSet( - webResourceRoot, - "/WEB-INF/classes", - configuration.emptyClassesDir.getAbsolutePath(), - "/")); - - configuration.resourceMap.forEach( - (path, file) -> { - if (file.exists() && file.isFile()) { - webResourceRoot.addJarResources( - new FileResourceSet(webResourceRoot, "/" + path, file.getAbsolutePath(), "/")); - } - }); - - context.setResources(webResourceRoot); - - start = - new Runnable() { - @Override - public void run() { - try { - tomcat.start(); - } catch (final LifecycleException e) { - throw new RuntimeException(e); - } - } - }; - - join = - new Runnable() { - @Override - public void run() { - tomcat.getServer().await(); - } - }; - - stop = - new Runnable() { - @Override - public void run() { - try { - - connector.stop(); - context.stop(); - tomcat.stop(); - - connector.destroy(); - context.destroy(); - tomcat.destroy(); - - } catch (final LifecycleException e) { - throw new RuntimeException(e); - } - } - }; + throw new RuntimeException("unsupported"); } } diff --git a/runners/4.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml b/runners/4.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..b59892b6 --- /dev/null +++ b/runners/4.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,23 @@ + + + Hello, Servlet 4.0 + + + HelloServlet + com.earldouglas.HelloServlet + + + + HelloServlet + /hello + + + + default + / + + + diff --git a/runners/4.0/src/test/java/com/earldouglas/HelloServlet.java b/runners/4.0/src/test/java/com/earldouglas/HelloServlet.java new file mode 100644 index 00000000..9799d9f2 --- /dev/null +++ b/runners/4.0/src/test/java/com/earldouglas/HelloServlet.java @@ -0,0 +1,21 @@ +package com.earldouglas; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class HelloServlet extends HttpServlet { + + @Override + protected void doGet( + final HttpServletRequest request, + final HttpServletResponse response + ) throws IOException { + response.setContentType("text/plain"); + response.getWriter().println("Hello, world!"); + } +} diff --git a/runners/4.0/src/test/scala/com/earldouglas/WarRunnerTest.scala b/runners/4.0/src/test/scala/com/earldouglas/WarRunnerTest.scala index 955f1e4b..02ef8ea3 100644 --- a/runners/4.0/src/test/scala/com/earldouglas/WarRunnerTest.scala +++ b/runners/4.0/src/test/scala/com/earldouglas/WarRunnerTest.scala @@ -11,6 +11,8 @@ class WarRunnerTest override def beforeAll(): Unit = { + new com.earldouglas.HelloServlet() + val thread: Thread = new Thread { override def run(): Unit = { @@ -44,7 +46,7 @@ class WarRunnerTest awaitOpen(8988) } - test("/foo.html") { + test("GET /foo.html") { val expected: HttpClient.Response = HttpClient.Response( @@ -71,7 +73,7 @@ class WarRunnerTest ) shouldBe expected } - test("/bar.html") { + test("GET /bar.html") { val expected: HttpClient.Response = HttpClient.Response( @@ -98,7 +100,7 @@ class WarRunnerTest ) shouldBe expected } - test("/baz/raz.css") { + test("GET /baz/raz.css") { val expected: HttpClient.Response = HttpClient.Response( @@ -124,4 +126,37 @@ class WarRunnerTest } ) shouldBe expected } + + test("GET /hello") { + + val expected: HttpClient.Response = + HttpClient.Response( + status = 200, + headers = Map( + "Content-Type" -> "text/plain" + ), + body = """|Hello, world! + |""".stripMargin + ) + + val obtained: HttpClient.Response = + HttpClient.request( + method = "GET", + url = "http://localhost:8988/hello", + headers = Map.empty, + body = None + ) + + obtained.copy( + headers = + obtained + .headers + .filter { case (k, _) => + k == "Content-Type" + } + .map { case (k, v) => + (k, v.replaceAll(";charset=.*", "")) + } + ) shouldBe expected + } } diff --git a/runners/4.0/src/test/scala/com/earldouglas/WebappComponentsConfigurationTest.scala b/runners/4.0/src/test/scala/com/earldouglas/WebappComponentsConfigurationTest.scala deleted file mode 100644 index fd6dc832..00000000 --- a/runners/4.0/src/test/scala/com/earldouglas/WebappComponentsConfigurationTest.scala +++ /dev/null @@ -1,32 +0,0 @@ -package com.earldouglas - -import org.scalatest.BeforeAndAfterAll -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -import java.io.File -import scala.collection.JavaConverters._ - -class WebappComponentsConfigurationTest - extends AnyFunSuite - with Matchers - with BeforeAndAfterAll { - - test("load") { - - val configuration: WebappComponentsConfiguration = - WebappComponentsConfiguration - .load("src/test/resources/webapp-components.properties") - - configuration.hostname shouldBe "localhost" - configuration.port shouldBe 8989 - configuration.emptyWebappDir shouldBe (new File("target/empty")) - configuration.emptyClassesDir shouldBe (new File("target/empty")) - configuration.resourceMap.asScala shouldBe - List("bar.html", "foo.html", "baz/raz.css") - .map(x => - (x -> new File(s"src/test/fakeproject/src/main/webapp/${x}")) - ) - .toMap - } -} diff --git a/runners/4.0/src/test/scala/com/earldouglas/WebappComponentsRunnerTest.scala b/runners/4.0/src/test/scala/com/earldouglas/WebappComponentsRunnerTest.scala deleted file mode 100644 index 79a75bbd..00000000 --- a/runners/4.0/src/test/scala/com/earldouglas/WebappComponentsRunnerTest.scala +++ /dev/null @@ -1,107 +0,0 @@ -package com.earldouglas - -import org.scalatest.BeforeAndAfterAll -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -class WebappComponentsRunnerTest - extends AnyFunSuite - with Matchers - with BeforeAndAfterAll { - - lazy val configuration: WebappComponentsConfiguration = - WebappComponentsConfiguration - .load("src/test/resources/webapp-components.properties") - - lazy val runner: WebappComponentsRunner = - new WebappComponentsRunner(configuration) - - override def beforeAll(): Unit = { - runner.start.run() - } - - override def afterAll(): Unit = { - runner.stop.run() - } - - test("/foo.html") { - - val expected: HttpClient.Response = - HttpClient.Response( - status = 200, - headers = Map( - "Content-Type" -> "text/html" - ), - body = """|foo - |""".stripMargin - ) - - val obtained: HttpClient.Response = - HttpClient.request( - method = "GET", - url = "http://localhost:8989/foo.html", - headers = Map.empty, - body = None - ) - - obtained.copy( - headers = obtained.headers.filter { case (k, _) => - k == "Content-Type" - } - ) shouldBe expected - } - - test("/bar.html") { - - val expected: HttpClient.Response = - HttpClient.Response( - status = 200, - headers = Map( - "Content-Type" -> "text/html" - ), - body = """|bar - |""".stripMargin - ) - - val obtained: HttpClient.Response = - HttpClient.request( - method = "GET", - url = "http://localhost:8989/bar.html", - headers = Map.empty, - body = None - ) - - obtained.copy( - headers = obtained.headers.filter { case (k, _) => - k == "Content-Type" - } - ) shouldBe expected - } - - test("/baz/raz.css") { - - val expected: HttpClient.Response = - HttpClient.Response( - status = 200, - headers = Map( - "Content-Type" -> "text/css" - ), - body = """|div.raz { font-weight: bold; } - |""".stripMargin - ) - - val obtained: HttpClient.Response = - HttpClient.request( - method = "GET", - url = "http://localhost:8989/baz/raz.css", - headers = Map.empty, - body = None - ) - - obtained.copy( - headers = obtained.headers.filter { case (k, _) => - k == "Content-Type" - } - ) shouldBe expected - } -} diff --git a/runners/5.0/src/main/java/com/earldouglas/WarConfiguration.java b/runners/5.0/src/main/java/com/earldouglas/WarConfiguration.java new file mode 100644 index 00000000..974c12e7 --- /dev/null +++ b/runners/5.0/src/main/java/com/earldouglas/WarConfiguration.java @@ -0,0 +1,71 @@ +package com.earldouglas; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class WarConfiguration { + + /** The port to use for the server, e.g. 8080 */ + public final int port; + + /** The .war file to serve. */ + public final File warFile; + + /** + * Read configuration from a file at the specified location. + * + * @param configurationFilename the configuration filename to load + * @throws IOException if something goes wrong + * @return WarConfiguration a loaded configuration + */ + public static WarConfiguration load(final String configurationFilename) throws IOException { + return WarConfiguration.load(new File(configurationFilename)); + } + + /** + * Read configuration from a file at the specified location. + * + *

The format of the file is a Properties file with the following fields: + * + *

    + *
  • port + *
  • warFile + *
+ * + *

Example: + * + *

+   * port=8989
+   * warFile=path/to/warfile.war
+   * 
+ * + * @param configurationFile the configuration file to load + * @throws IOException if something goes wrong + * @return WarConfiguration a loaded configuration + */ + public static WarConfiguration load(final File configurationFile) throws IOException { + + final InputStream inputStream = new FileInputStream(configurationFile); + + final Properties properties = new Properties(); + properties.load(inputStream); + + return new WarConfiguration( + Integer.parseInt(properties.getProperty("port")), + new File(properties.getProperty("warFile"))); + } + + /** + * Construct a new configuration from the given parameters. + * + * @param port the port to use for the server, e.g. 8080 + * @param warFile the .war file to serve + */ + public WarConfiguration(final int port, final File warFile) { + this.port = port; + this.warFile = warFile; + } +} diff --git a/runners/5.0/src/main/java/com/earldouglas/WarRunner.java b/runners/5.0/src/main/java/com/earldouglas/WarRunner.java new file mode 100644 index 00000000..61565245 --- /dev/null +++ b/runners/5.0/src/main/java/com/earldouglas/WarRunner.java @@ -0,0 +1,39 @@ +package com.earldouglas; + +import org.eclipse.jetty.webapp.WebAppContext; +import org.eclipse.jetty.server.Server; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class WarRunner { + + /** + * Load configuration from the file in the first argument, and use it to start a new WarRunner. + * + * @param args the configuration filename to load and run + * @throws Exception if something goes wrong + */ + public static void main(final String[] args) throws Exception { + + final WarConfiguration warConfiguration = WarConfiguration.load(args[0]); + + final Path warPath = + Paths + .get(warConfiguration.warFile.getPath()) + .toAbsolutePath() + .normalize(); + + final Server server = new Server(warConfiguration.port); + + final WebAppContext webapp = new WebAppContext(); + webapp.setContextPath("/"); + webapp.setWar(warPath.toUri().toASCIIString()); + + server.setHandler(webapp); + + server.start(); + server.join(); + } +} diff --git a/runners/5.0/src/main/java/com/earldouglas/WebappComponentsRunner.java b/runners/5.0/src/main/java/com/earldouglas/WebappComponentsRunner.java new file mode 100644 index 00000000..519d0007 --- /dev/null +++ b/runners/5.0/src/main/java/com/earldouglas/WebappComponentsRunner.java @@ -0,0 +1,18 @@ +package com.earldouglas; + +import java.io.IOException; + +/** Launches a webapp composed of in-place resources, classes, and libraries. */ +public class WebappComponentsRunner { + + /** + * Load configuration from the file in the first argument, and use it to start a new + * WebappComponentsRunner. + * + * @param args the configuration filename to load and run + * @throws IOException if something goes wrong + */ + public static void main(final String[] args) throws IOException { + throw new RuntimeException("unsupported"); + } +} diff --git a/runners/5.0/src/test/fakeproject/classes/bar.class b/runners/5.0/src/test/fakeproject/classes/bar.class new file mode 100644 index 00000000..e69de29b diff --git a/runners/5.0/src/test/fakeproject/classes/foo.class b/runners/5.0/src/test/fakeproject/classes/foo.class new file mode 100644 index 00000000..e69de29b diff --git a/runners/5.0/src/test/fakeproject/lib/baz.jar b/runners/5.0/src/test/fakeproject/lib/baz.jar new file mode 100644 index 00000000..e69de29b diff --git a/runners/5.0/src/test/fakeproject/lib/raz.jar b/runners/5.0/src/test/fakeproject/lib/raz.jar new file mode 100644 index 00000000..e69de29b diff --git a/runners/5.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml b/runners/5.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..c2558bde --- /dev/null +++ b/runners/5.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,23 @@ + + + Hello, Servlet 5.0 + + + HelloServlet + com.earldouglas.HelloServlet + + + + HelloServlet + /hello + + + + default + / + + + diff --git a/runners/5.0/src/test/fakeproject/src/main/webapp/bar.html b/runners/5.0/src/test/fakeproject/src/main/webapp/bar.html new file mode 100644 index 00000000..b685fe79 --- /dev/null +++ b/runners/5.0/src/test/fakeproject/src/main/webapp/bar.html @@ -0,0 +1 @@ +bar diff --git a/runners/5.0/src/test/fakeproject/src/main/webapp/baz/raz.css b/runners/5.0/src/test/fakeproject/src/main/webapp/baz/raz.css new file mode 100644 index 00000000..9ae483b4 --- /dev/null +++ b/runners/5.0/src/test/fakeproject/src/main/webapp/baz/raz.css @@ -0,0 +1 @@ +div.raz { font-weight: bold; } diff --git a/runners/5.0/src/test/fakeproject/src/main/webapp/foo.html b/runners/5.0/src/test/fakeproject/src/main/webapp/foo.html new file mode 100644 index 00000000..f2a826f7 --- /dev/null +++ b/runners/5.0/src/test/fakeproject/src/main/webapp/foo.html @@ -0,0 +1 @@ +foo diff --git a/runners/5.0/src/test/java/com/earldouglas/HelloServlet.java b/runners/5.0/src/test/java/com/earldouglas/HelloServlet.java new file mode 100644 index 00000000..cc37c14a --- /dev/null +++ b/runners/5.0/src/test/java/com/earldouglas/HelloServlet.java @@ -0,0 +1,21 @@ +package com.earldouglas; + +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class HelloServlet extends HttpServlet { + + @Override + protected void doGet( + final HttpServletRequest request, + final HttpServletResponse response + ) throws IOException { + response.setContentType("text/plain"); + response.getWriter().println("Hello, world!"); + } +} diff --git a/runners/5.0/src/test/resources/war.properties b/runners/5.0/src/test/resources/war.properties new file mode 100644 index 00000000..a9c2d083 --- /dev/null +++ b/runners/5.0/src/test/resources/war.properties @@ -0,0 +1,2 @@ +port=8988 +warFile=src/test/fakeproject/src/main/webapp diff --git a/runners/5.0/src/test/resources/webapp-components.properties b/runners/5.0/src/test/resources/webapp-components.properties new file mode 100644 index 00000000..14c2e0d7 --- /dev/null +++ b/runners/5.0/src/test/resources/webapp-components.properties @@ -0,0 +1,8 @@ +hostname=localhost +port=8989 +contextPath= +emptyWebappDir=target/empty +emptyClassesDir=target/empty +resourceMap=bar.html->src/test/fakeproject/src/main/webapp/bar.html,\ + foo.html->src/test/fakeproject/src/main/webapp/foo.html,\ + baz/raz.css->src/test/fakeproject/src/main/webapp/baz/raz.css diff --git a/runners/5.0/src/test/scala/com/earldouglas/HttpClient.scala b/runners/5.0/src/test/scala/com/earldouglas/HttpClient.scala new file mode 100644 index 00000000..61789759 --- /dev/null +++ b/runners/5.0/src/test/scala/com/earldouglas/HttpClient.scala @@ -0,0 +1,64 @@ +package com.earldouglas + +import java.net.HttpURLConnection +import java.net.URI +import scala.collection.JavaConverters._ +import scala.io.Source + +object HttpClient { + + case class Response( + status: Int, + headers: Map[String, String], + body: String + ) + + def request( + method: String, + url: String, + headers: Map[String, String], + body: Option[String] + ): Response = { + + val c = + new URI(url) + .toURL() + .openConnection() + .asInstanceOf[HttpURLConnection] + + c.setInstanceFollowRedirects(false) + c.setRequestMethod(method) + c.setDoInput(true) + c.setDoOutput(body.isDefined) + + headers foreach { case (k, v) => + c.setRequestProperty(k, v) + } + + body foreach { b => + c.getOutputStream.write(b.getBytes("UTF-8")) + } + + val response = + Response( + status = c.getResponseCode(), + headers = c + .getHeaderFields() + .asScala + .filter({ case (k, _) => k != null }) + .map({ case (k, v) => (k, v.asScala.mkString(",")) }) + .toMap - "Date" - "Content-Length" - "Server", + body = Source.fromInputStream { + if (c.getResponseCode() < 400) { + c.getInputStream + } else { + c.getErrorStream + } + }.mkString + ) + + c.disconnect() + + response + } +} diff --git a/runners/5.0/src/test/scala/com/earldouglas/WarConfigurationTest.scala b/runners/5.0/src/test/scala/com/earldouglas/WarConfigurationTest.scala new file mode 100644 index 00000000..1b04488f --- /dev/null +++ b/runners/5.0/src/test/scala/com/earldouglas/WarConfigurationTest.scala @@ -0,0 +1,24 @@ +package com.earldouglas + +import org.scalatest.BeforeAndAfterAll +import org.scalatest.funsuite.AnyFunSuite +import org.scalatest.matchers.should.Matchers + +import java.io.File + +class WarConfigurationTest + extends AnyFunSuite + with Matchers + with BeforeAndAfterAll { + + test("load") { + + val configuration: WarConfiguration = + WarConfiguration.load("src/test/resources/war.properties") + + configuration.port shouldBe 8988 + + configuration.warFile shouldBe + new File("src/test/fakeproject/src/main/webapp") + } +} diff --git a/runners/6.0/src/test/scala/com/earldouglas/WebappComponentsRunnerTest.scala b/runners/5.0/src/test/scala/com/earldouglas/WarRunnerTest.scala similarity index 50% rename from runners/6.0/src/test/scala/com/earldouglas/WebappComponentsRunnerTest.scala rename to runners/5.0/src/test/scala/com/earldouglas/WarRunnerTest.scala index 79a75bbd..02ef8ea3 100644 --- a/runners/6.0/src/test/scala/com/earldouglas/WebappComponentsRunnerTest.scala +++ b/runners/5.0/src/test/scala/com/earldouglas/WarRunnerTest.scala @@ -4,27 +4,49 @@ import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers -class WebappComponentsRunnerTest +class WarRunnerTest extends AnyFunSuite with Matchers with BeforeAndAfterAll { - lazy val configuration: WebappComponentsConfiguration = - WebappComponentsConfiguration - .load("src/test/resources/webapp-components.properties") + override def beforeAll(): Unit = { - lazy val runner: WebappComponentsRunner = - new WebappComponentsRunner(configuration) + new com.earldouglas.HelloServlet() - override def beforeAll(): Unit = { - runner.start.run() - } + val thread: Thread = + new Thread { + override def run(): Unit = { + WarRunner.main(Array("src/test/resources/war.properties")) + } + } + thread.start() + + def isOpen(port: Int): Boolean = + try { + import java.net.Socket + import java.net.InetSocketAddress + val socket: Socket = new Socket() + socket.connect(new InetSocketAddress("localhost", port)) + socket.close() + true + } catch { + case e: Exception => false + } + + def awaitOpen(port: Int, retries: Int = 40): Unit = + if (!isOpen(port)) { + if (retries > 0) { + Thread.sleep(250) + awaitOpen(port, retries - 1) + } else { + throw new Exception(s"expected port $port to be open") + } + } - override def afterAll(): Unit = { - runner.stop.run() + awaitOpen(8988) } - test("/foo.html") { + test("GET /foo.html") { val expected: HttpClient.Response = HttpClient.Response( @@ -39,7 +61,7 @@ class WebappComponentsRunnerTest val obtained: HttpClient.Response = HttpClient.request( method = "GET", - url = "http://localhost:8989/foo.html", + url = "http://localhost:8988/foo.html", headers = Map.empty, body = None ) @@ -51,7 +73,7 @@ class WebappComponentsRunnerTest ) shouldBe expected } - test("/bar.html") { + test("GET /bar.html") { val expected: HttpClient.Response = HttpClient.Response( @@ -66,7 +88,7 @@ class WebappComponentsRunnerTest val obtained: HttpClient.Response = HttpClient.request( method = "GET", - url = "http://localhost:8989/bar.html", + url = "http://localhost:8988/bar.html", headers = Map.empty, body = None ) @@ -78,7 +100,7 @@ class WebappComponentsRunnerTest ) shouldBe expected } - test("/baz/raz.css") { + test("GET /baz/raz.css") { val expected: HttpClient.Response = HttpClient.Response( @@ -93,7 +115,7 @@ class WebappComponentsRunnerTest val obtained: HttpClient.Response = HttpClient.request( method = "GET", - url = "http://localhost:8989/baz/raz.css", + url = "http://localhost:8988/baz/raz.css", headers = Map.empty, body = None ) @@ -104,4 +126,37 @@ class WebappComponentsRunnerTest } ) shouldBe expected } + + test("GET /hello") { + + val expected: HttpClient.Response = + HttpClient.Response( + status = 200, + headers = Map( + "Content-Type" -> "text/plain" + ), + body = """|Hello, world! + |""".stripMargin + ) + + val obtained: HttpClient.Response = + HttpClient.request( + method = "GET", + url = "http://localhost:8988/hello", + headers = Map.empty, + body = None + ) + + obtained.copy( + headers = + obtained + .headers + .filter { case (k, _) => + k == "Content-Type" + } + .map { case (k, v) => + (k, v.replaceAll(";charset=.*", "")) + } + ) shouldBe expected + } } diff --git a/runners/6.0/src/main/java/com/earldouglas/WarRunner.java b/runners/6.0/src/main/java/com/earldouglas/WarRunner.java index a9b9f444..81d4f7f0 100644 --- a/runners/6.0/src/main/java/com/earldouglas/WarRunner.java +++ b/runners/6.0/src/main/java/com/earldouglas/WarRunner.java @@ -1,5 +1,12 @@ package com.earldouglas; +import org.eclipse.jetty.ee10.webapp.WebAppContext; +import org.eclipse.jetty.server.Server; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + public class WarRunner { /** @@ -12,11 +19,21 @@ public static void main(final String[] args) throws Exception { final WarConfiguration warConfiguration = WarConfiguration.load(args[0]); - final String[] warRunnerArgs = - new String[] { - "--port", Integer.toString(warConfiguration.port), warConfiguration.warFile.getPath(), - }; + final Path warPath = + Paths + .get(warConfiguration.warFile.getPath()) + .toAbsolutePath() + .normalize(); + + final Server server = new Server(warConfiguration.port); + + final WebAppContext webapp = new WebAppContext(); + webapp.setContextPath("/"); + webapp.setWar(warPath.toUri().toASCIIString()); + + server.setHandler(webapp); - webapp.runner.launch.Main.main(warRunnerArgs); + server.start(); + server.join(); } } diff --git a/runners/6.0/src/main/java/com/earldouglas/WebappComponentsConfiguration.java b/runners/6.0/src/main/java/com/earldouglas/WebappComponentsConfiguration.java deleted file mode 100644 index df85305b..00000000 --- a/runners/6.0/src/main/java/com/earldouglas/WebappComponentsConfiguration.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.earldouglas; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; - -/** Specifies server settings and components locations for running a webapp in-place. */ -public class WebappComponentsConfiguration { - - /** The hostname to use for the server, e.g. "localhost" */ - public final String hostname; - - /** The port to use for the server, e.g. 8080 */ - public final int port; - - /** - * The context path to use for the webapp. - * - *

For the root context path, use the empty string "". - */ - public final String contextPath; - - /** - * An empty directory that Tomcat requires to run. Represents the resources directory, but it can - * be any empty directory. - */ - public final File emptyWebappDir; - - /** - * An empty directory that Tomcat requires to run. Represents the WEB-INF/classes directory, but - * it can be any empty directory. - */ - public final File emptyClassesDir; - - /** - * The map of resources to serve. - * - *

The mapping is from source to destination, where: - * - *

    - *
  • source is the relative path within the webapp (e.g. index.html, WEB-INF/web.xml) - *
  • destination is the file on disk to serve - *
- */ - public final Map resourceMap; - - /** - * Read configuration from a file at the specified location. - * - * @param configurationFilename the configuration filename to load - * @throws IOException if something goes wrong - * @return WebappComponentsConfiguration a loaded configuration - */ - public static WebappComponentsConfiguration load(final String configurationFilename) - throws IOException { - return WebappComponentsConfiguration.load(new File(configurationFilename)); - } - - private static Map parseResourceMap(final String raw) throws IOException { - - final Map resourceMap = new HashMap(); - - final String[] rows = raw.split(","); - - for (int rowIndex = 0; rowIndex < rows.length; rowIndex++) { - final String[] columns = rows[rowIndex].split("->"); - resourceMap.put(columns[0], new File(columns[1])); - } - - return resourceMap; - } - - /** - * Read configuration from a file at the specified location. - * - *

The format of the file is a Properties file with the following fields: - * - *

    - *
  • hostname - *
  • port - *
  • contextPath - *
  • emptyWebappDir - *
  • emptyClassesDir - *
  • resourceMap - *
- * - * The resourceMap field is a list, concatenated by commas, of source/destination pairs, delimited - * by ->. - * - *

Example: - * - *

-   * hostname=localhost
-   * port=8989
-   * contextPath=
-   * emptyWebappDir=target/empty
-   * emptyClassesDir=target/empty
-   * resourceMap=bar.html->src/test/fakeproject/src/main/webapp/bar.html,\
-   *             foo.html->src/test/fakeproject/src/main/webapp/foo.html,\
-   *             baz/raz.css->src/test/fakeproject/src/main/webapp/baz/raz.css
-   * 
- * - * @param configurationFile the configuration file to load - * @throws IOException if something goes wrong - * @return WebappComponentsConfiguration a loaded configuration - */ - public static WebappComponentsConfiguration load(final File configurationFile) - throws IOException { - - final InputStream inputStream = new FileInputStream(configurationFile); - - final Properties properties = new Properties(); - properties.load(inputStream); - - final Map resourceMap = parseResourceMap(properties.getProperty("resourceMap")); - - return new WebappComponentsConfiguration( - properties.getProperty("hostname"), - Integer.parseInt(properties.getProperty("port")), - properties.getProperty("contextPath"), - new File(properties.getProperty("emptyWebappDir")), - new File(properties.getProperty("emptyClassesDir")), - resourceMap); - } - - /** - * Construct a new configuration from the given parameters. - * - * @param hostname the hostname to use for the server, e.g. "localhost" - * @param port the port to use for the server, e.g. 8080 - * @param contextPath the context path to use for the webapp, e.g. "" - * @param emptyWebappDir an empty directory that Tomcat requires to run - * @param emptyClassesDir an empty directory that Tomcat requires to run - * @param resourceMap the map of resources to serve - */ - public WebappComponentsConfiguration( - final String hostname, - final int port, - final String contextPath, - final File emptyWebappDir, - final File emptyClassesDir, - final Map resourceMap) { - this.hostname = hostname; - this.port = port; - this.contextPath = contextPath; - this.emptyWebappDir = emptyWebappDir; - this.emptyClassesDir = emptyClassesDir; - this.resourceMap = resourceMap; - } -} diff --git a/runners/6.0/src/main/java/com/earldouglas/WebappComponentsRunner.java b/runners/6.0/src/main/java/com/earldouglas/WebappComponentsRunner.java index dbd59381..519d0007 100644 --- a/runners/6.0/src/main/java/com/earldouglas/WebappComponentsRunner.java +++ b/runners/6.0/src/main/java/com/earldouglas/WebappComponentsRunner.java @@ -1,48 +1,10 @@ package com.earldouglas; -import java.io.File; import java.io.IOException; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import org.apache.catalina.Context; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.WebResourceRoot; -import org.apache.catalina.connector.Connector; -import org.apache.catalina.startup.Tomcat; -import org.apache.catalina.webresources.DirResourceSet; -import org.apache.catalina.webresources.FileResourceSet; -import org.apache.catalina.webresources.StandardRoot; -/** - * Launches a webapp composed of in-place resources, classes, and libraries. - * - *

emptyWebappDir and emptyClassesDir need to point to one or two empty directories. They're not - * used to serve any content, but they are required by Tomcat's internals. - * - *

To use a root context path (i.e. /), set contextPath to the empty string for some reason. - */ +/** Launches a webapp composed of in-place resources, classes, and libraries. */ public class WebappComponentsRunner { - private static File mkdir(final File file) throws IOException { - if (file.exists()) { - if (!file.isDirectory()) { - throw new FileAlreadyExistsException(file.getPath()); - } else { - return file; - } - } else { - final Path path = FileSystems.getDefault().getPath(file.getPath()); - try { - Files.createDirectory(path); - return file; - } catch (FileAlreadyExistsException e) { - return file; - } - } - } - /** * Load configuration from the file in the first argument, and use it to start a new * WebappComponentsRunner. @@ -51,105 +13,6 @@ private static File mkdir(final File file) throws IOException { * @throws IOException if something goes wrong */ public static void main(final String[] args) throws IOException { - - final WebappComponentsConfiguration webappComponentsConfiguration = - WebappComponentsConfiguration.load(args[0]); - - final WebappComponentsRunner webappComponentsRunner = - new WebappComponentsRunner(webappComponentsConfiguration); - - webappComponentsRunner.start.run(); - webappComponentsRunner.join.run(); - } - - /** A handle for starting the instance's server. */ - public final Runnable start; - - /** A handle for joining the instance's server. */ - public final Runnable join; - - /** A handle for stopping the instance's server. */ - public final Runnable stop; - - /** - * Construct a new WebappComponentsRunner using the given configuration. - * - * @param configuration the configuration to run - * @throws IOException if something goes wrong - */ - public WebappComponentsRunner(final WebappComponentsConfiguration configuration) - throws IOException { - - mkdir(configuration.emptyWebappDir); - mkdir(configuration.emptyClassesDir); - - final Tomcat tomcat = new Tomcat(); - tomcat.setHostname(configuration.hostname); - - final Connector connector = new Connector(); - connector.setPort(configuration.port); - tomcat.setConnector(connector); - - final Context context = - tomcat.addWebapp(configuration.contextPath, configuration.emptyWebappDir.getAbsolutePath()); - - final WebResourceRoot webResourceRoot = new StandardRoot(context); - - webResourceRoot.addJarResources( - new DirResourceSet( - webResourceRoot, - "/WEB-INF/classes", - configuration.emptyClassesDir.getAbsolutePath(), - "/")); - - configuration.resourceMap.forEach( - (path, file) -> { - if (file.exists() && file.isFile()) { - webResourceRoot.addJarResources( - new FileResourceSet(webResourceRoot, "/" + path, file.getAbsolutePath(), "/")); - } - }); - - context.setResources(webResourceRoot); - - start = - new Runnable() { - @Override - public void run() { - try { - tomcat.start(); - } catch (final LifecycleException e) { - throw new RuntimeException(e); - } - } - }; - - join = - new Runnable() { - @Override - public void run() { - tomcat.getServer().await(); - } - }; - - stop = - new Runnable() { - @Override - public void run() { - try { - - connector.stop(); - context.stop(); - tomcat.stop(); - - connector.destroy(); - context.destroy(); - tomcat.destroy(); - - } catch (final LifecycleException e) { - throw new RuntimeException(e); - } - } - }; + throw new RuntimeException("unsupported"); } } diff --git a/runners/6.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml b/runners/6.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..bfb9a991 --- /dev/null +++ b/runners/6.0/src/test/fakeproject/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,23 @@ + + + Hello, Servlet 6.0 + + + HelloServlet + com.earldouglas.HelloServlet + + + + HelloServlet + /hello + + + + default + / + + + diff --git a/runners/6.0/src/test/java/com/earldouglas/HelloServlet.java b/runners/6.0/src/test/java/com/earldouglas/HelloServlet.java new file mode 100644 index 00000000..cc37c14a --- /dev/null +++ b/runners/6.0/src/test/java/com/earldouglas/HelloServlet.java @@ -0,0 +1,21 @@ +package com.earldouglas; + +import jakarta.servlet.ServletConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.IOException; + +public class HelloServlet extends HttpServlet { + + @Override + protected void doGet( + final HttpServletRequest request, + final HttpServletResponse response + ) throws IOException { + response.setContentType("text/plain"); + response.getWriter().println("Hello, world!"); + } +} diff --git a/runners/6.0/src/test/scala/com/earldouglas/WarRunnerTest.scala b/runners/6.0/src/test/scala/com/earldouglas/WarRunnerTest.scala index 955f1e4b..02ef8ea3 100644 --- a/runners/6.0/src/test/scala/com/earldouglas/WarRunnerTest.scala +++ b/runners/6.0/src/test/scala/com/earldouglas/WarRunnerTest.scala @@ -11,6 +11,8 @@ class WarRunnerTest override def beforeAll(): Unit = { + new com.earldouglas.HelloServlet() + val thread: Thread = new Thread { override def run(): Unit = { @@ -44,7 +46,7 @@ class WarRunnerTest awaitOpen(8988) } - test("/foo.html") { + test("GET /foo.html") { val expected: HttpClient.Response = HttpClient.Response( @@ -71,7 +73,7 @@ class WarRunnerTest ) shouldBe expected } - test("/bar.html") { + test("GET /bar.html") { val expected: HttpClient.Response = HttpClient.Response( @@ -98,7 +100,7 @@ class WarRunnerTest ) shouldBe expected } - test("/baz/raz.css") { + test("GET /baz/raz.css") { val expected: HttpClient.Response = HttpClient.Response( @@ -124,4 +126,37 @@ class WarRunnerTest } ) shouldBe expected } + + test("GET /hello") { + + val expected: HttpClient.Response = + HttpClient.Response( + status = 200, + headers = Map( + "Content-Type" -> "text/plain" + ), + body = """|Hello, world! + |""".stripMargin + ) + + val obtained: HttpClient.Response = + HttpClient.request( + method = "GET", + url = "http://localhost:8988/hello", + headers = Map.empty, + body = None + ) + + obtained.copy( + headers = + obtained + .headers + .filter { case (k, _) => + k == "Content-Type" + } + .map { case (k, v) => + (k, v.replaceAll(";charset=.*", "")) + } + ) shouldBe expected + } } diff --git a/runners/6.0/src/test/scala/com/earldouglas/WebappComponentsConfigurationTest.scala b/runners/6.0/src/test/scala/com/earldouglas/WebappComponentsConfigurationTest.scala deleted file mode 100644 index fd6dc832..00000000 --- a/runners/6.0/src/test/scala/com/earldouglas/WebappComponentsConfigurationTest.scala +++ /dev/null @@ -1,32 +0,0 @@ -package com.earldouglas - -import org.scalatest.BeforeAndAfterAll -import org.scalatest.funsuite.AnyFunSuite -import org.scalatest.matchers.should.Matchers - -import java.io.File -import scala.collection.JavaConverters._ - -class WebappComponentsConfigurationTest - extends AnyFunSuite - with Matchers - with BeforeAndAfterAll { - - test("load") { - - val configuration: WebappComponentsConfiguration = - WebappComponentsConfiguration - .load("src/test/resources/webapp-components.properties") - - configuration.hostname shouldBe "localhost" - configuration.port shouldBe 8989 - configuration.emptyWebappDir shouldBe (new File("target/empty")) - configuration.emptyClassesDir shouldBe (new File("target/empty")) - configuration.resourceMap.asScala shouldBe - List("bar.html", "foo.html", "baz/raz.css") - .map(x => - (x -> new File(s"src/test/fakeproject/src/main/webapp/${x}")) - ) - .toMap - } -} diff --git a/shell.nix b/shell.nix index 3d4d62d0..5b81715f 100644 --- a/shell.nix +++ b/shell.nix @@ -1,6 +1,6 @@ { pkgs ? import {} }: let - jdk = pkgs.jdk11; + jdk = pkgs.jdk17; in pkgs.mkShell { nativeBuildInputs = [ diff --git a/src/main/scala/com/earldouglas/sbt/war/WebappComponentsPlugin.scala b/src/main/scala/com/earldouglas/sbt/war/WebappComponentsPlugin.scala index e828f903..4518c1f7 100644 --- a/src/main/scala/com/earldouglas/sbt/war/WebappComponentsPlugin.scala +++ b/src/main/scala/com/earldouglas/sbt/war/WebappComponentsPlugin.scala @@ -64,6 +64,8 @@ object WebappComponentsPlugin extends AutoPlugin { "javax.servlet" % "javax.servlet-api" % "3.1.0" case "4.0" => "jakarta.servlet" % "jakarta.servlet-api" % "4.0.4" + case "5.0" => + "jakarta.servlet" % "jakarta.servlet-api" % "5.0.0" case "6.0" => "jakarta.servlet" % "jakarta.servlet-api" % "6.0.0" }