This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 4b81e135a54c7ead2f1018e3813e74e02797013f
Author: Lukasz Lenart <[email protected]>
AuthorDate: Fri Oct 23 12:33:19 2026 +0200

    WW-5791 docs: implementation plan for Struts Boot
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../plans/2026-10-23-WW-5791-struts-boot.md        | 444 +++++++++++++++++++++
 1 file changed, 444 insertions(+)

diff --git a/docs/superpowers/plans/2026-10-23-WW-5791-struts-boot.md 
b/docs/superpowers/plans/2026-10-23-WW-5791-struts-boot.md
new file mode 100644
index 000000000..9979d3679
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-23-WW-5791-struts-boot.md
@@ -0,0 +1,444 @@
+# WW-5791 Struts Boot Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use 
superpowers:subagent-driven-development (recommended) or 
superpowers:executing-plans to implement this plan task-by-task. Steps use 
checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** A newcomer adds one dependency, registers an action in Java, runs 
`main()`, and gets a FreeMarker page from embedded Tomcat. No XML, no 
Convention, no Spring.
+
+**Architecture:**
+- A new top-level `boot/` module with three parts:
+  - a fluent registry that only records definitions
+  - a `ServletContextAwareConfigurationProvider` that turns those definitions 
into ordinary `PackageConfig`s
+  - a launcher that starts Tomcat, registers `StrutsPrepareAndExecuteFilter` 
with `configProviders=<provider>`, and passes the registry to the provider 
through a servlet-context attribute
+- Core is untouched.
+
+**Tech Stack:**
+- Java 17
+- Struts core 7.5.0-SNAPSHOT
+- `tomcat-embed-core`: 10.1.x by default, 11.0.x under `-Pjakartaee11`
+- FreeMarker
+- JUnit 4, AssertJ, `java.net.http.HttpClient`
+- `maven-shade-plugin` for the example app
+
+**Spec:** `docs/superpowers/specs/2026-10-23-WW-5791-struts-boot-design.md`
+
+## Global Constraints
+
+- No file under `core/` changes. If something can't be done through existing 
extension points, stop and raise it.
+- No Spring artifact in `struts2-boot-starter`'s dependency tree. Check with 
`./mvnw -pl boot dependency:tree`.
+- Registry config must equal what the equivalent `struts.xml` would produce. 
Never relax strict method invocation, `@StrutsParameter` or the allowlist. 
Never set `struts.enable.SlashesInActionNames` or `struts.devMode`.
+- Never read `application.properties`. Settings come from `struts.properties`, 
with Boot keys `struts.boot.port` (default `8080`) and 
`struts.boot.context-path` (default `""`). `--key=value` arguments override the 
file.
+- FreeMarker only. Result paths resolve to `/templates/` + path on the 
classpath.
+- Tomcat follows core's Jakarta line. Root property `tomcat-embed.version` is 
`10.1.59`, and `11.0.27` in the `jakartaee11` profile.
+- Configuration errors are `org.apache.struts2.config.ConfigurationException`, 
thrown at startup.
+- Tests are JUnit 4 (`org.junit.Test`). JUnit 5 is never used.
+- Package: `org.apache.struts2.boot`. Artifact: 
`org.apache.struts:struts2-boot-starter`.
+- Tomcat base dir: `Files.createTempDirectory("struts-boot-" + 
UUID.randomUUID())`, deleted recursively on close.
+- Default to no comments (see `~/.claude/CLAUDE.md`). Every new file carries 
the ASF license header, copied from any existing source file.
+
+## Review Focus
+
+1. **Struts tags inside a template.** `<@s.property value="message"/>` must 
render. `FreemarkerManager` builds a `TaglibFactory`, which needs 
`jakarta.servlet.jsp` classes that `tomcat-embed-core` doesn't ship. Pinned in 
Task 4 `testStrutsTagRenders`.
+2. **Startup failure leaves nothing behind.** A `ConfigurationException` or a 
busy port must not leave a running Tomcat, a hung JVM or a leftover temp dir. 
Pinned in Task 4 `testConfigurationErrorStopsTomcatAndCleansBaseDir` and 
`testBusyPortFailsCleanly`.
+3. **Non-empty context path.** Actions answer under `/ctx/...`, and 
`redirectAction` carries the context path. Pinned in Task 4 
`testContextPathAppliesToActionsAndRedirects`.
+4. **`namespace("/admin")` called twice.** Both calls land in one package, and 
a duplicate name across the two calls throws. Pinned in Task 1 
`testNamespaceDeclaredTwiceMerges` and 
`testDuplicateAcrossRepeatedNamespaceThrows`.
+5. **Argument precedence.** `--struts.boot.port=9090` beats 
`struts.boot.port=8081` in `struts.properties`, and unknown arguments are 
ignored rather than fatal. Pinned in Task 3.
+
+---
+
+## File Structure
+
+```
+pom.xml                                   modify: <module>boot</module>; 
tomcat-embed.version property + jakartaee11 override
+parent/pom.xml                            modify: dependencyManagement for 
tomcat-embed-core, struts2-boot-starter
+bom/pom.xml                               modify: struts2-boot-starter entry
+boot/pom.xml                              create
+boot/src/main/java/org/apache/struts2/boot/
+  StrutsBootApplication.java              user-facing: void 
actions(ActionRegistry)
+  ActionRegistry.java                     user-facing: top-level actions + 
namespace(...)
+  NamespaceActions.java                   user-facing: actions + interceptors 
inside one namespace
+  ActionDefinition.java                   user-facing: fluent per-action calls
+  HttpMethods.java                        user-facing: static GET/POST/... 
aliases for HttpMethod
+  RegistryConfigurationProvider.java      internal: registry -> PackageConfig, 
all validation
+  BootSettings.java                       internal: struts.properties + args
+  StrutsBoot.java                         user-facing: run(...) / start(...)
+  StrutsBootServer.java                   user-facing: running-server handle
+boot/src/test/java/org/apache/struts2/boot/...      tests + fixtures
+boot/src/test/resources/templates/...               fixture templates
+apps/pom.xml                              modify: <module>boot-hello</module>
+apps/boot-hello/...                       create: example app, shade config, 
smoke test
+```
+
+---
+
+### Task 1: Module scaffolding and the fluent registry
+
+The registry only records definitions. It checks the rules that need no Struts 
configuration: slashes, `.ftl`, and duplicates inside the registry. Everything 
that needs Struts belongs to Task 2.
+
+**Files:**
+- Modify: `pom.xml`
+  - add `<module>boot</module>` between `plugins` and `apps`
+  - add `<tomcat-embed.version>10.1.59</tomcat-embed.version>` to the 
properties
+  - add `<tomcat-embed.version>11.0.27</tomcat-embed.version>` to the 
`jakartaee11` profile properties
+- Modify: `parent/pom.xml`: in `dependencyManagement`, add 
`org.apache.tomcat.embed:tomcat-embed-core:${tomcat-embed.version}` and 
`org.apache.struts:struts2-boot-starter:${project.version}`
+- Modify: `bom/pom.xml`: add `struts2-boot-starter`, following the shape of 
the existing entries
+- Create: `boot/pom.xml`
+  - parent `struts2-parent`, `relativePath` `../parent/pom.xml`
+  - artifactId `struts2-boot-starter`, name `Struts 2 Boot Starter`
+  - compile dependencies: `struts2-core`, `tomcat-embed-core`
+  - test dependencies: junit, assertj-core
+- Create: `StrutsBootApplication.java`, `ActionRegistry.java`, 
`NamespaceActions.java`, `ActionDefinition.java`, `HttpMethods.java`
+- Test: `boot/src/test/java/org/apache/struts2/boot/ActionRegistryTest.java`
+
+`plugins/jasperreports7` keeps its own `tomcat-embed.version` property. Leave 
it alone.
+
+**Interfaces:**
+- Produces:
+  - `interface StrutsBootApplication { void actions(ActionRegistry actions); }`
+  - `final class ActionRegistry`:
+    - `public static final String SERVLET_CONTEXT_ATTRIBUTE = 
ActionRegistry.class.getName()`
+    - `ActionDefinition action(String name, Class<?> actionClass)`: namespace 
`""`
+    - `ActionRegistry namespace(String namespace, Consumer<NamespaceActions> 
body)`
+    - `Collection<NamespaceActions> namespaces()`: insertion order; `""` is 
included only when used
+  - `final class NamespaceActions`:
+    - `ActionDefinition action(String name, Class<?> actionClass)`
+    - `NamespaceActions interceptors(String stackName)`
+    - `String namespace()`
+    - `Optional<String> interceptorStack()`
+    - `List<ActionDefinition> actions()`
+  - `final class ActionDefinition`:
+    - fluent methods, each returning `this`:
+      - `method(String)`
+      - `success(String templatePath)`
+      - `result(String name, String templatePath)`
+      - `redirectAction(String resultName, String actionName)`
+      - `httpMethods(HttpMethod...)`
+      - `interceptors(String stackName)`
+    - read accessors: `name()`, `actionClass()`, `methodName()` (default 
`"execute"`), `results()` (`Map<String, ResultDefinition>`, insertion order), 
`httpMethodSet()`, `interceptorStack()` (`Optional<String>`)
+  - `record ResultDefinition(Kind kind, String value)` nested in 
`ActionDefinition`, with `enum Kind { TEMPLATE, REDIRECT_ACTION }`
+  - `final class HttpMethods`: `public static final HttpMethod GET = 
HttpMethod.GET`, and the same for `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, 
`OPTIONS`. Users can `import static ...HttpMethods.POST`.
+
+- [ ] **Step 1: Write the failing tests** in `ActionRegistryTest`:
+
+```java
+@Test public void testTopLevelActionGoesToEmptyNamespace() {
+    ActionRegistry r = new ActionRegistry();
+    r.action("hello", Object.class).success("hello.ftl");
+    
assertThat(r.namespaces()).extracting(NamespaceActions::namespace).containsExactly("");
+    ActionDefinition a = r.namespaces().iterator().next().actions().get(0);
+    assertThat(a.methodName()).isEqualTo("execute");
+    assertThat(a.results()).containsEntry("success", new 
ResultDefinition(Kind.TEMPLATE, "hello.ftl"));
+}
+@Test public void testNamespaceDeclaredTwiceMerges() {
+    ActionRegistry r = new ActionRegistry();
+    r.namespace("/admin", n -> n.action("a", Object.class));
+    r.namespace("/admin", n -> n.action("b", Object.class));
+    assertThat(r.namespaces()).hasSize(1);
+    
assertThat(r.namespaces().iterator().next().actions()).extracting(ActionDefinition::name).containsExactly("a",
 "b");
+}
+@Test public void testDuplicateAcrossRepeatedNamespaceThrows() {
+    ActionRegistry r = new ActionRegistry();
+    r.namespace("/admin", n -> n.action("a", Object.class));
+    assertThatThrownBy(() -> r.namespace("/admin", n -> n.action("a", 
Object.class)))
+        
.isInstanceOf(ConfigurationException.class).hasMessageContaining("/admin").hasMessageContaining("a");
+}
+@Test public void testDuplicateTopLevelThrows()   // two r.action("hello", 
...) -> ConfigurationException
+@Test public void testSlashInActionNameThrows() { // r.action("users/save", 
...) -> ConfigurationException, message contains "namespace"
+@Test public void testNonFtlTemplateThrows()      // .success("hello.jsp") -> 
ConfigurationException, message contains "FreeMarker"
+@Test public void testFluentCallsAreRecorded() {
+    ActionDefinition a = new ActionRegistry().action("save", Object.class)
+        .method("save").httpMethods(HttpMethods.POST)
+        .result("input", "form.ftl").redirectAction("success", 
"users").interceptors("paramsPrepareParamsStack");
+    assertThat(a.methodName()).isEqualTo("save");
+    assertThat(a.httpMethodSet()).containsExactly(HttpMethod.POST);
+    assertThat(a.results()).containsEntry("success", new 
ResultDefinition(Kind.REDIRECT_ACTION, "users"));
+    assertThat(a.interceptorStack()).contains("paramsPrepareParamsStack");
+}
+```
+
+- [ ] **Step 2: Run them to verify they fail**
+  - Run: `./mvnw test -DskipAssembly -pl boot -am -Dtest=ActionRegistryTest 
-Dsurefire.failIfNoSpecifiedTests=false > target/boot-t1.log 2>&1; echo $?`
+  - Expected: non-zero exit, with a compilation failure for the missing 
classes.
+
+- [ ] **Step 3: Implement** the five classes, the POM changes and the module.
+  - Duplicate and slash checks run when `action(...)` is called.
+  - The `.ftl` check runs in `result(...)` and `success(...)`.
+
+- [ ] **Step 4: Run the tests to verify they pass**
+  - Same command. Expected: exit 0, `Tests run: 7, Failures: 0`.
+  - Then run `./mvnw -pl boot dependency:tree | grep -i spring`. Expected: no 
output.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add pom.xml parent/pom.xml bom/pom.xml boot/
+git commit -m "WW-5791 feat(boot): add struts2-boot-starter module with fluent 
action registry"
+```
+
+---
+
+### Task 2: Registry → PackageConfig provider
+
+**Files:**
+- Create: 
`boot/src/main/java/org/apache/struts2/boot/RegistryConfigurationProvider.java`
+- Test: 
`boot/src/test/java/org/apache/struts2/boot/RegistryConfigurationProviderTest.java`
+- Test fixtures:
+  - `boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java`: 
methods `execute`, `list`, `save`, `delete`
+  - `boot/src/test/resources/templates/hello.ftl`
+  - `boot/src/test/resources/templates/admin/users.ftl`
+  - `boot/src/test/resources/struts-boot-clash.xml`: package `xml-pkg`, 
namespace `/admin`, action `users`
+
+**Interfaces:**
+- Consumes: `ActionRegistry`, `NamespaceActions`, `ActionDefinition`, 
`ResultDefinition` from Task 1.
+- Produces: `public class RegistryConfigurationProvider implements 
ServletContextAwareConfigurationProvider`, with:
+  - `public RegistryConfigurationProvider()`: used by `Dispatcher` through 
`configProviders`. `initWithContext` reads 
`ActionRegistry.SERVLET_CONTEXT_ATTRIBUTE`. If the attribute is missing, it 
throws `ConfigurationException`.
+  - `RegistryConfigurationProvider(ActionRegistry registry)`: package-private, 
for tests.
+  - Package names: `struts-boot` for namespace `""`, and `"struts-boot" + 
namespace` otherwise, for example `struts-boot/admin`.
+
+Translation rules (from the spec):
+- Each package uses 
`addParent(configuration.getPackageConfig("struts-default"))` and the 
definition's namespace. `strictMethodInvocation` keeps its default (`true`).
+- Each action is `new ActionConfig.Builder(pkgName, name, 
actionClass.getName())`, with `methodName(m)` and `addAllowedMethod(m)`.
+  - If `httpMethodSet()` is non-empty, call `addHttpMethods(set)`.
+  - The interceptor stack comes from the action first, then the namespace. If 
either is set, call 
`addInterceptors(InterceptorBuilder.constructInterceptorReference(pkgBuilder, 
stack, Map.of(), null, objectFactory))`. If neither is set, add no 
interceptors, so the package default (`defaultStack`) applies.
+- A `TEMPLATE` result becomes `new ResultConfig.Builder(name, 
freemarkerClass).addParam("location", "/templates/" + path)`.
+- A `REDIRECT_ACTION` result becomes `new ResultConfig.Builder(name, 
redirectActionClass).addParam("actionName", target)`.
+- Result classes come from 
`strutsDefault.getAllResultTypeConfigs().get("freemarker" | 
"redirectAction").getClassName()`.
+
+Validation, each a `ConfigurationException` thrown from `loadPackages()`:
+- The template `templates/<path>` is missing on the classpath. Check with 
`ClassLoaderUtil.getResource`.
+- An unknown stack name. `constructInterceptorReference` already throws this.
+- A non-abstract package already in `configuration.getPackageConfigs()` has 
the same namespace and an action with the same name. The message names the 
namespace, the action and the XML package.
+
+- [ ] **Step 1: Write the failing tests.**
+
+Build the configuration the way `ScopedConstantsSubstitutionTest` does:
+
+```java
+private Configuration load(ActionRegistry registry, String... extraXml) {
+    ConfigurationManager cm = new ConfigurationManager(Container.DEFAULT_NAME);
+    cm.addContainerProvider(new DefaultPropertiesProvider());
+    cm.addContainerProvider(new 
StrutsXmlConfigurationProvider("struts-default.xml"));
+    for (String xml : extraXml) cm.addContainerProvider(new 
StrutsXmlConfigurationProvider(xml));
+    cm.addContainerProvider(new StrutsBeanSelectionProvider());
+    cm.addContainerProvider(new RegistryConfigurationProvider(registry));
+    return cm.getConfiguration();
+}
+```
+
+Write these tests:
+- `testPackageExtendsStrutsDefaultWithNamespace`: 
`getPackageConfig("struts-boot/admin")` has namespace `/admin`, parent 
`struts-default`, and `isStrictMethodInvocation()` true.
+- `testMethodIsAddedToAllowedMethods`: action `users` with `.method("list")` 
gives `getAllowedMethods().isAllowed("list")` true and `isAllowed("delete")` 
false.
+- `testTemplateResultIsAbsoluteFreemarker`: the `success` result's class name 
equals struts-default's `freemarker` type, and its `location` param is 
`/templates/admin/users.ftl`.
+- `testRedirectActionResult`: the class is the `redirectAction` type, and the 
`actionName` param is `users`.
+- `testHttpMethodsCarried`: `.httpMethods(POST)` gives `getHttpMethods()` 
containing exactly `POST`.
+- `testNoStackMeansPackageDefault`: the action's `getInterceptors()` is empty, 
and the `RuntimeConfiguration`'s resolved config for the action has a non-empty 
interceptor list.
+- `testNamedStackResolved`: `.interceptors("basicStack")` gives a non-empty 
interceptor list.
+- `testUnknownStackThrows`: `.interceptors("noSuchStack")` makes `load(...)` 
throw `ConfigurationException`.
+- `testMissingTemplateThrows`: `.success("missing.ftl")` throws a 
`ConfigurationException` whose message contains `templates/missing.ftl`.
+- `testClashWithXmlThrows`: the registry defines `users` in `/admin`, and 
`load(registry, "struts-boot-clash.xml")` throws a `ConfigurationException` 
whose message contains `xml-pkg` and `users`.
+- `testSameNamespaceAsXmlWithDistinctNamesLoads`: the registry defines `list` 
in `/admin` alongside the XML `users`, and both actions resolve through 
`getRuntimeConfiguration().getActionConfig("/admin", ...)`.
+
+- [ ] **Step 2: Run them to verify they fail**
+  - Run: `./mvnw test -DskipAssembly -pl boot 
-Dtest=RegistryConfigurationProviderTest > target/boot-t2.log 2>&1; echo $?`
+  - Expected: non-zero exit, because the provider class doesn't exist yet.
+
+- [ ] **Step 3: Implement `RegistryConfigurationProvider`.**
+  - `init(Configuration)` stores the configuration.
+  - `register(...)` is a no-op.
+  - `needsReload()` returns `false`.
+  - `loadPackages()` applies the rules above, using 
`configuration.getContainer().getInstance(ObjectFactory.class)` as the 
`ObjectFactory`.
+
+- [ ] **Step 4: Run the tests to verify they pass**
+  - Same command. Expected: exit 0, `Failures: 0`, 11 tests.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add boot/
+git commit -m "WW-5791 feat(boot): translate the action registry into Struts 
package configs"
+```
+
+---
+
+### Task 3: Boot settings
+
+**Files:**
+- Create: `boot/src/main/java/org/apache/struts2/boot/BootSettings.java`
+- Test: `boot/src/test/java/org/apache/struts2/boot/BootSettingsTest.java`
+
+**Interfaces:**
+- Produces `final class BootSettings` with:
+  - `static BootSettings load(String[] args)`, which reads `struts.properties` 
from the context class loader
+  - `static BootSettings load(Properties fileProperties, String[] args)`, for 
tests
+  - `int port()`
+  - `String contextPath()`
+- Precedence: `--struts.boot.port=N` arguments, then `struts.properties`, then 
the defaults (`8080`, `""`).
+- An argument that doesn't match `--struts.boot.<key>=<value>` is ignored.
+- A port that isn't numeric throws `ConfigurationException`.
+
+- [ ] **Step 1: Write the failing tests**
+  - `testDefaults`: port is `8080` and context path is `""`.
+  - `testFileValue`: file `struts.boot.port=8081` gives `8081`.
+  - `testArgumentBeatsFile`: file `8081` and arg `--struts.boot.port=9090` 
give `9090`.
+  - `testUnknownArgumentsIgnored`: args `--foo=bar` and `positional` give the 
defaults with no exception.
+  - `testContextPathFromArgs`: `--struts.boot.context-path=/ctx` gives `/ctx`.
+  - `testNonNumericPortThrows`: `--struts.boot.port=abc` throws 
`ConfigurationException`.
+
+- [ ] **Step 2: Run them to verify they fail**
+  - Run: `./mvnw test -DskipAssembly -pl boot -Dtest=BootSettingsTest > 
target/boot-t3.log 2>&1; echo $?`
+  - Expected: non-zero exit (compilation failure).
+
+- [ ] **Step 3: Implement `BootSettings`.**
+
+- [ ] **Step 4: Run the tests to verify they pass**
+  - Same command. Expected: exit 0, 6 tests, `Failures: 0`.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add boot/
+git commit -m "WW-5791 feat(boot): read port and context path from 
struts.properties and arguments"
+```
+
+---
+
+### Task 4: Launcher on embedded Tomcat, end to end
+
+**Files:**
+- Create: `boot/src/main/java/org/apache/struts2/boot/StrutsBoot.java`, 
`boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java`
+- Modify: `boot/pom.xml`. Add `jakarta.servlet.jsp:jakarta.servlet.jsp-api` at 
runtime scope, with the version the parent already manages, only if 
`testStrutsTagRenders` fails without it (see Review Focus 1).
+- Test: `boot/src/test/java/org/apache/struts2/boot/StrutsBootTomcatTest.java`
+- Test fixtures:
+  - `fixture/HelloApp.java`: `hello` → `hello.ftl`; `/admin` with `users` 
(`list`) → `admin/users.ftl`, and `saveUser` (`save`, `POST` only) with 
redirect `success` → `users`
+  - `fixture/HelloAction.java`: `getMessage()` returns `"Hello Boot"`
+  - `fixture/BrokenApp.java`: registers `.success("missing.ftl")`
+  - `hello.ftl`, containing `<@s.property value="message"/>`
+  - `boot/src/test/resources/struts.xml`: package `xml-pkg`, namespace `/xml`, 
action `legacy` with a freemarker result `/templates/hello.ftl`
+  - `boot/src/test/resources/struts.properties`: 
`struts.enable.DynamicMethodInvocation=true`
+
+**Interfaces:**
+- Consumes `ActionRegistry` (Task 1), `RegistryConfigurationProvider` (Task 
2), and `BootSettings` (Task 3).
+- Produces:
+  - `public final class StrutsBoot`:
+    - `static void run(Class<? extends StrutsBootApplication> app, String... 
args)`: starts the server and blocks on `await()`. On a startup failure it 
rethrows, which a newcomer's `main` reports as a non-zero exit.
+    - `static StrutsBootServer start(Class<? extends StrutsBootApplication> 
app, String... args)`: returns once Tomcat is serving.
+  - `public final class StrutsBootServer implements AutoCloseable`:
+    - `int port()`: the actual bound port, which matters when 
`--struts.boot.port=0`
+    - `String baseUrl()`: `"http://127.0.0.1:"; + port + contextPath`
+    - `Path baseDir()`
+    - `void await()`
+    - `void close()`: stops and destroys Tomcat, then deletes `baseDir`
+
+`start` sequence, following the pattern in `JasperReport7TomcatTest`:
+1. Instantiate the app with its no-arg constructor and call 
`actions(registry)`.
+2. Create the temp base dir.
+3. Create `new Tomcat()` with `setBaseDir` and `setPort`, then 
`addContext(contextPath, baseDir)`. Put the registry into the context's servlet 
context under `SERVLET_CONTEXT_ATTRIBUTE` (through a 
`ServletContainerInitializer` or `context.getServletContext().setAttribute` 
before start).
+4. Add a `FilterDef` for `StrutsPrepareAndExecuteFilter` with init param 
`configProviders=RegistryConfigurationProvider.class.getName()`, mapped to `/*`.
+5. Start Tomcat. If the context fails to start (`context.getState() != 
LifecycleState.STARTED`), or anything throws, call `close()` and throw a 
`ConfigurationException` that carries the cause.
+6. Register a shutdown hook that calls `close()`. `close()` must be idempotent.
+
+Tests: the class starts one `@BeforeClass` server with 
`StrutsBoot.start(HelloApp.class, "--struts.boot.port=0")` and calls `close()` 
in `@AfterClass`. Each failure-path test starts its own server.
+- `testRendersFreemarkerTemplate`: `GET /hello.action` returns 200.
+- `testStrutsTagRenders`: the same response body contains `Hello Boot`.
+- `testPostOnlyRejectsGet`:
+  - `GET /admin/saveUser.action` returns 405.
+  - `POST` to the same URL returns 302, with a `Location` ending in 
`/admin/users.action`.
+  - Because the POST reaches the action, the GET rejection comes from the 
method restriction rather than a 404.
+- `testUnregisteredMethodNotInvokable`: with DMI on through the fixture 
`struts.properties`, `GET /admin/users!delete.action` does not invoke `delete` 
(`UserAction` records invocations in a static flag the test resets). The 
response is not 200.
+- `testXmlAndRegistryCoexist`: both `GET /xml/legacy.action` and `GET 
/hello.action` return 200.
+- `testConfigurationErrorStopsTomcatAndCleansBaseDir`:
+  - `StrutsBoot.start(BrokenApp.class, "--struts.boot.port=0")` throws a 
`ConfigurationException` whose message contains `templates/missing.ftl`.
+  - The temp-dir glob `struts-boot-*` gains no new directory. Snapshot it 
before and after.
+- `testBusyPortFailsCleanly`:
+  - Hold a `ServerSocket` on a free port, then `start` with that port. It 
throws.
+  - No new `struts-boot-*` temp dir remains.
+- `testContextPathAppliesToActionsAndRedirects`:
+  - Start with `--struts.boot.port=0 --struts.boot.context-path=/ctx`.
+  - `GET /ctx/hello.action` returns 200.
+  - `POST /ctx/admin/saveUser.action` returns a `Location` containing 
`/ctx/admin/users.action`.
+- `testCloseDeletesBaseDir`: after `close()`, `Files.exists(server.baseDir())` 
is false.
+
+Use `HttpClient` with `followRedirects(NEVER)`.
+
+- [ ] **Step 1: Write the failing tests and fixtures.**
+
+- [ ] **Step 2: Run them to verify they fail**
+  - Run: `./mvnw test -DskipAssembly -pl boot -Dtest=StrutsBootTomcatTest > 
target/boot-t4.log 2>&1; echo $?`
+  - Expected: non-zero exit (compilation failure).
+
+- [ ] **Step 3: Implement `StrutsBoot` and `StrutsBootServer`.**
+  - If `testStrutsTagRenders` fails with `NoClassDefFoundError: 
jakarta/servlet/jsp/...`, add the runtime dependency listed under Files.
+
+- [ ] **Step 4: Run the tests to verify they pass**
+  - Run the same command, then the whole module under both Jakarta lines:
+    - `./mvnw test -DskipAssembly -pl boot > target/boot-ee10.log 2>&1; echo 
$?`
+    - `./mvnw test -DskipAssembly -pl boot -Pjakartaee11 > 
target/boot-ee11.log 2>&1; echo $?`
+  - Expected: exit 0 for both, with `Failures: 0, Errors: 0`.
+  - Then run `./mvnw -pl boot dependency:tree -Pjakartaee11 | grep 
tomcat-embed-core`. Expected: `11.0.27`. Without the profile, expected: 
`10.1.59`.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add boot/
+git commit -m "WW-5791 feat(boot): launch Struts on embedded Tomcat from 
main()"
+```
+
+---
+
+### Task 5: Example app and runnable jar
+
+**Files:**
+- Modify: `apps/pom.xml`: add `<module>boot-hello</module>`
+- Create: `apps/boot-hello/pom.xml`
+  - artifactId `struts2-boot-hello`, packaging `jar`
+  - dependency `struts2-boot-starter`
+  - `maven-shade-plugin` bound to `package`, configured with:
+    - a `ManifestResourceTransformer` whose `mainClass` is 
`org.apache.struts2.boot.hello.HelloApp`
+    - a `ServicesResourceTransformer`
+    - signature-file excludes (`META-INF/*.SF`, `*.DSA`, `*.RSA`)
+- Create: 
`apps/boot-hello/src/main/java/org/apache/struts2/boot/hello/HelloApp.java` and 
`HelloAction.java`. This is the spec's newcomer example, trimmed to `hello` 
only.
+- Create: `apps/boot-hello/src/main/resources/templates/hello.ftl`
+- Create: `apps/boot-hello/README.md`, covering the three newcomer steps, 
`java -jar`, `struts.boot.*` settings, and the shading limitation with plugins 
(two `struts-plugin.xml` files overwrite each other)
+- Test: 
`apps/boot-hello/src/test/java/org/apache/struts2/boot/hello/HelloAppTest.java`
+
+**Interfaces:**
+- Consumes `StrutsBoot.start`, `StrutsBootServer` (Task 4).
+
+- [ ] **Step 1: Write the failing smoke test.** `testHelloPageRenders` starts 
`HelloApp` on port 0, sends `GET /hello.action`, and expects 200 with the 
template's text.
+
+- [ ] **Step 2: Run it to verify it fails**
+  - Run: `./mvnw test -DskipAssembly -pl apps/boot-hello -am > 
target/boot-t5.log 2>&1; echo $?`
+  - Expected: non-zero exit.
+
+- [ ] **Step 3: Implement the app, the POM and the README.**
+
+- [ ] **Step 4: Verify the test and the runnable jar**
+  - Rerun the Step 2 command. Expected: exit 0.
+  - Build the jar: `./mvnw package -DskipAssembly -DskipTests -pl 
apps/boot-hello -am > target/boot-pkg.log 2>&1; echo $?`
+  - Start it: `java -jar 
apps/boot-hello/target/struts2-boot-hello-7.5.0-SNAPSHOT.jar 
--struts.boot.port=18080 &`
+  - Fetch the page: `curl -s -o /dev/null -w '%{http_code}' 
http://127.0.0.1:18080/hello.action`. Expected: `200`.
+  - Kill the process. The `struts-boot-*` temp dir from this run is gone.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add apps/pom.xml apps/boot-hello/
+git commit -m "WW-5791 feat(boot): add boot-hello example app with runnable 
jar"
+```
+
+---
+
+### Task 6: Full reactor check
+
+- [ ] **Step 1:** `./mvnw clean install -DskipAssembly > 
target/boot-reactor.log 2>&1; echo $?`. Expected: exit 0.
+  - The Tiles plugin may fail on unmodified `main` (see memory: 
velocity-tools). If it does, confirm that on `main` before blaming this branch.
+- [ ] **Step 2:** `./mvnw clean install -DskipAssembly -Pjakartaee11 > 
target/boot-reactor-ee11.log 2>&1; echo $?`. Expected: exit 0.
+- [ ] **Step 3:** `git status --short`. Expected: no stray `??` files from 
review agents.
+- [ ] **Step 4:** Read `THREAT_MODEL.md` and confirm Boot stays inside the 
framework's existing trust boundaries: the registry produces nothing 
`struts.xml` couldn't, and the Tomcat defaults are stated in the README. Do 
this before any PR.
+
+## Out of scope for this plan
+
+- struts-site documentation
+- the assembly module
+- the 7.5.0 Version Notes entry
+
+These follow after merge.

Reply via email to