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

lukaszlenart pushed a commit to branch WW-5791-struts-boot
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 9c63f6ef8701d6949cd4af4609c0a2aa7adfe44d
Author: Lukasz Lenart <[email protected]>
AuthorDate: Fri Oct 23 12:49:33 2026 +0200

    WW-5791 feat(boot): launch Struts on embedded Tomcat from main()
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 boot/pom.xml                                       |   2 +-
 .../boot/RegistryConfigurationProvider.java        |  12 ++
 .../java/org/apache/struts2/boot/StrutsBoot.java   |  50 +++++
 .../org/apache/struts2/boot/StrutsBootServer.java  | 206 +++++++++++++++++++++
 .../boot/RegistryConfigurationProviderTest.java    |   9 +
 .../apache/struts2/boot/StrutsBootTomcatTest.java  | 175 +++++++++++++++++
 .../fixture/{UserAction.java => BrokenApp.java}    |  20 +-
 .../fixture/{UserAction.java => HelloAction.java}  |  14 +-
 .../fixture/{UserAction.java => HelloApp.java}     |  24 ++-
 .../apache/struts2/boot/fixture/UserAction.java    |   3 +
 boot/src/test/resources/struts.properties          |  17 ++
 .../UserAction.java => resources/struts.xml}       |  33 ++--
 boot/src/test/resources/templates/hello.ftl        |   2 +-
 13 files changed, 507 insertions(+), 60 deletions(-)

diff --git a/boot/pom.xml b/boot/pom.xml
index 93242450d..76593c0ed 100644
--- a/boot/pom.xml
+++ b/boot/pom.xml
@@ -44,7 +44,7 @@
         <dependency>
             <groupId>jakarta.servlet.jsp</groupId>
             <artifactId>jakarta.servlet.jsp-api</artifactId>
-            <scope>test</scope>
+            <scope>runtime</scope>
         </dependency>
         <dependency>
             <groupId>junit</groupId>
diff --git 
a/boot/src/main/java/org/apache/struts2/boot/RegistryConfigurationProvider.java 
b/boot/src/main/java/org/apache/struts2/boot/RegistryConfigurationProvider.java
index c164f59bb..ccfb87ff1 100644
--- 
a/boot/src/main/java/org/apache/struts2/boot/RegistryConfigurationProvider.java
+++ 
b/boot/src/main/java/org/apache/struts2/boot/RegistryConfigurationProvider.java
@@ -22,19 +22,23 @@ import jakarta.servlet.ServletContext;
 import org.apache.struts2.ObjectFactory;
 import org.apache.struts2.config.Configuration;
 import org.apache.struts2.config.ConfigurationException;
+import org.apache.struts2.config.ConfigurationUtil;
 import org.apache.struts2.config.ServletContextAwareConfigurationProvider;
 import org.apache.struts2.config.entities.ActionConfig;
 import org.apache.struts2.config.entities.PackageConfig;
 import org.apache.struts2.config.entities.ResultConfig;
 import org.apache.struts2.config.providers.InterceptorBuilder;
 import org.apache.struts2.inject.ContainerBuilder;
+import org.apache.struts2.ognl.ProviderAllowlist;
 import org.apache.struts2.util.ClassLoaderUtil;
 import org.apache.struts2.util.location.LocatableProperties;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 
 public class RegistryConfigurationProvider implements 
ServletContextAwareConfigurationProvider {
 
@@ -43,6 +47,7 @@ public class RegistryConfigurationProvider implements 
ServletContextAwareConfigu
 
     private ActionRegistry registry;
     private Configuration configuration;
+    private ProviderAllowlist providerAllowlist;
 
     public RegistryConfigurationProvider() {
     }
@@ -77,6 +82,9 @@ public class RegistryConfigurationProvider implements 
ServletContextAwareConfigu
 
     @Override
     public void destroy() {
+        if (providerAllowlist != null) {
+            providerAllowlist.clearAllowlist(this);
+        }
     }
 
     @Override
@@ -86,6 +94,7 @@ public class RegistryConfigurationProvider implements 
ServletContextAwareConfigu
         String freemarker = 
strutsDefault.getAllResultTypeConfigs().get("freemarker").getClassName();
         String redirectAction = 
strutsDefault.getAllResultTypeConfigs().get("redirectAction").getClassName();
         List<PackageConfig> existing = new 
ArrayList<>(configuration.getPackageConfigs().values());
+        Set<Class<?>> allowlist = new HashSet<>();
 
         for (NamespaceActions namespace : registry.namespaces()) {
             String packageName = namespace.namespace().isEmpty() ? 
PACKAGE_PREFIX : PACKAGE_PREFIX + namespace.namespace();
@@ -95,11 +104,14 @@ public class RegistryConfigurationProvider implements 
ServletContextAwareConfigu
 
             for (ActionDefinition definition : namespace.actions()) {
                 assertNoClash(existing, namespace.namespace(), 
definition.name());
+                
allowlist.addAll(ConfigurationUtil.getAllClassTypes(definition.actionClass()));
                 pkg.addActionConfig(definition.name(),
                         actionConfig(packageName, namespace, definition, pkg, 
objectFactory, freemarker, redirectAction));
             }
             configuration.addPackageConfig(packageName, pkg.build());
         }
+        providerAllowlist = 
configuration.getContainer().getInstance(ProviderAllowlist.class);
+        providerAllowlist.registerAllowlist(this, allowlist);
     }
 
     private ActionConfig actionConfig(String packageName, NamespaceActions 
namespace, ActionDefinition definition,
diff --git a/boot/src/main/java/org/apache/struts2/boot/StrutsBoot.java 
b/boot/src/main/java/org/apache/struts2/boot/StrutsBoot.java
new file mode 100644
index 000000000..103da42a1
--- /dev/null
+++ b/boot/src/main/java/org/apache/struts2/boot/StrutsBoot.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.boot;
+
+import org.apache.struts2.config.ConfigurationException;
+
+import java.lang.reflect.InvocationTargetException;
+
+public final class StrutsBoot {
+
+    private StrutsBoot() {
+    }
+
+    public static void run(Class<? extends StrutsBootApplication> app, 
String... args) {
+        start(app, args).await();
+    }
+
+    public static StrutsBootServer start(Class<? extends 
StrutsBootApplication> app, String... args) {
+        ActionRegistry registry = new ActionRegistry();
+        instantiate(app).actions(registry);
+        return StrutsBootServer.start(registry, BootSettings.load(args));
+    }
+
+    private static StrutsBootApplication instantiate(Class<? extends 
StrutsBootApplication> app) {
+        try {
+            return app.getDeclaredConstructor().newInstance();
+        } catch (InvocationTargetException e) {
+            throw new ConfigurationException("Cannot create " + app.getName() 
+ ": its constructor threw", e.getCause());
+        } catch (ReflectiveOperationException e) {
+            throw new ConfigurationException("Cannot create " + app.getName()
+                    + ": it needs a public no-argument constructor", e);
+        }
+    }
+}
diff --git a/boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java 
b/boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java
new file mode 100644
index 000000000..910dd5cc0
--- /dev/null
+++ b/boot/src/main/java/org/apache/struts2/boot/StrutsBootServer.java
@@ -0,0 +1,206 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.boot;
+
+import jakarta.servlet.FilterConfig;
+import jakarta.servlet.ServletException;
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleState;
+import org.apache.catalina.servlets.DefaultServlet;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.struts2.config.ConfigurationException;
+import org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter;
+import org.apache.tomcat.util.descriptor.web.FilterDef;
+import org.apache.tomcat.util.descriptor.web.FilterMap;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Comparator;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Stream;
+
+public final class StrutsBootServer implements AutoCloseable {
+
+    private static final String FILTER_NAME = "struts";
+
+    private final Tomcat tomcat;
+    private final Path baseDir;
+    private final String contextPath;
+    private final AtomicBoolean closed = new AtomicBoolean();
+    private final Thread shutdownHook = new Thread(this::close, 
"struts-boot-shutdown");
+
+    private StrutsBootServer(Tomcat tomcat, Path baseDir, String contextPath) {
+        this.tomcat = tomcat;
+        this.baseDir = baseDir;
+        this.contextPath = contextPath;
+    }
+
+    static StrutsBootServer start(ActionRegistry registry, BootSettings 
settings) {
+        Path baseDir = createBaseDir();
+        Tomcat tomcat = new Tomcat();
+        StrutsBootServer server = new StrutsBootServer(tomcat, baseDir, 
settings.contextPath());
+        CapturingStrutsFilter filter = new CapturingStrutsFilter();
+        try {
+            tomcat.setBaseDir(baseDir.toString());
+            tomcat.setPort(settings.port());
+            Context context = tomcat.addContext(settings.contextPath(), 
baseDir.toString());
+            context.addServletContainerInitializer(
+                    (classes, servletContext) -> 
servletContext.setAttribute(ActionRegistry.SERVLET_CONTEXT_ATTRIBUTE, registry),
+                    null);
+            Tomcat.addServlet(context, "default", new DefaultServlet());
+            context.addServletMappingDecoded("/", "default");
+            addStrutsFilter(context, filter);
+
+            tomcat.start();
+            server.assertStarted(context, filter);
+        } catch (Exception e) {
+            ConfigurationException failure = e instanceof 
ConfigurationException configurationException
+                    ? configurationException
+                    : new ConfigurationException("Struts Boot failed to start: 
" + e.getMessage(), e);
+            try {
+                server.close();
+            } catch (RuntimeException closeFailure) {
+                failure.addSuppressed(closeFailure);
+            }
+            throw failure;
+        }
+        Runtime.getRuntime().addShutdownHook(server.shutdownHook);
+        return server;
+    }
+
+    public int port() {
+        return tomcat.getConnector().getLocalPort();
+    }
+
+    public String baseUrl() {
+        return "http://127.0.0.1:"; + port() + contextPath;
+    }
+
+    public Path baseDir() {
+        return baseDir;
+    }
+
+    public void await() {
+        tomcat.getServer().await();
+    }
+
+    @Override
+    public void close() {
+        if (!closed.compareAndSet(false, true)) {
+            return;
+        }
+        try {
+            tomcat.stop();
+            tomcat.destroy();
+        } catch (Exception e) {
+            throw new ConfigurationException("Struts Boot failed to stop 
Tomcat", e);
+        } finally {
+            deleteRecursively(baseDir);
+            removeShutdownHook();
+        }
+    }
+
+    private void assertStarted(Context context, CapturingStrutsFilter filter) {
+        Throwable initFailure = filter.failure.get();
+        if (initFailure != null || context.getState() != 
LifecycleState.STARTED) {
+            String reason = initFailure == null ? "context state " + 
context.getState() : rootMessage(initFailure);
+            throw new ConfigurationException("Struts Boot failed to start: " + 
reason, initFailure);
+        }
+        if (tomcat.getConnector().getState() != LifecycleState.STARTED || 
port() < 0) {
+            throw new ConfigurationException("Struts Boot failed to start: 
connector state "
+                    + tomcat.getConnector().getState() + " on port " + 
tomcat.getConnector().getPort());
+        }
+    }
+
+    private void removeShutdownHook() {
+        if (Thread.currentThread() == shutdownHook) {
+            return;
+        }
+        try {
+            Runtime.getRuntime().removeShutdownHook(shutdownHook);
+        } catch (IllegalStateException | IllegalArgumentException ignored) {
+            // JVM already shutting down, or the hook was never registered
+        }
+    }
+
+    private static void addStrutsFilter(Context context, CapturingStrutsFilter 
filter) {
+        FilterDef filterDef = new FilterDef();
+        filterDef.setFilterName(FILTER_NAME);
+        filterDef.setFilter(filter);
+        filterDef.setFilterClass(filter.getClass().getName());
+        filterDef.addInitParameter("configProviders", 
RegistryConfigurationProvider.class.getName());
+        context.addFilterDef(filterDef);
+        FilterMap filterMap = new FilterMap();
+        filterMap.setFilterName(FILTER_NAME);
+        filterMap.addURLPattern("/*");
+        context.addFilterMap(filterMap);
+    }
+
+    private static Path createBaseDir() {
+        try {
+            return Files.createTempDirectory("struts-boot-" + 
UUID.randomUUID());
+        } catch (IOException e) {
+            throw new ConfigurationException("Cannot create the Tomcat base 
directory", e);
+        }
+    }
+
+    private static void deleteRecursively(Path dir) {
+        if (!Files.exists(dir)) {
+            return;
+        }
+        try (Stream<Path> paths = Files.walk(dir)) {
+            paths.sorted(Comparator.reverseOrder()).forEach(path -> {
+                try {
+                    Files.delete(path);
+                } catch (IOException e) {
+                    throw new UncheckedIOException(e);
+                }
+            });
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    private static String rootMessage(Throwable throwable) {
+        Throwable root = throwable;
+        while (root.getCause() != null && root.getCause() != root) {
+            root = root.getCause();
+        }
+        return root.getMessage();
+    }
+
+    private static final class CapturingStrutsFilter extends 
StrutsPrepareAndExecuteFilter {
+
+        private final AtomicReference<Throwable> failure = new 
AtomicReference<>();
+
+        @Override
+        public void init(FilterConfig filterConfig) throws ServletException {
+            try {
+                super.init(filterConfig);
+            } catch (RuntimeException | ServletException e) {
+                failure.set(e);
+                throw e;
+            }
+        }
+    }
+}
diff --git 
a/boot/src/test/java/org/apache/struts2/boot/RegistryConfigurationProviderTest.java
 
b/boot/src/test/java/org/apache/struts2/boot/RegistryConfigurationProviderTest.java
index 0e9e82307..f0683b173 100644
--- 
a/boot/src/test/java/org/apache/struts2/boot/RegistryConfigurationProviderTest.java
+++ 
b/boot/src/test/java/org/apache/struts2/boot/RegistryConfigurationProviderTest.java
@@ -29,6 +29,7 @@ import org.apache.struts2.config.entities.PackageConfig;
 import org.apache.struts2.config.entities.ResultConfig;
 import org.apache.struts2.boot.fixture.UserAction;
 import org.apache.struts2.inject.Container;
+import org.apache.struts2.ognl.ProviderAllowlist;
 import org.apache.struts2.ActionContext;
 import org.junit.After;
 import org.junit.Test;
@@ -86,6 +87,14 @@ public class RegistryConfigurationProviderTest {
         assertThat(pkg.isStrictMethodInvocation()).isTrue();
     }
 
+    @Test
+    public void testActionClassesAreAllowlisted() {
+        Configuration configuration = load(usersRegistry());
+
+        
assertThat(configuration.getContainer().getInstance(ProviderAllowlist.class).getProviderAllowlist())
+                .contains(UserAction.class);
+    }
+
     @Test
     public void testRootNamespacePackageName() {
         ActionRegistry registry = new ActionRegistry();
diff --git 
a/boot/src/test/java/org/apache/struts2/boot/StrutsBootTomcatTest.java 
b/boot/src/test/java/org/apache/struts2/boot/StrutsBootTomcatTest.java
new file mode 100644
index 000000000..be3f6d829
--- /dev/null
+++ b/boot/src/test/java/org/apache/struts2/boot/StrutsBootTomcatTest.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.boot;
+
+import org.apache.struts2.boot.fixture.BrokenApp;
+import org.apache.struts2.boot.fixture.HelloApp;
+import org.apache.struts2.boot.fixture.UserAction;
+import org.apache.struts2.config.ConfigurationException;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class StrutsBootTomcatTest {
+
+    private static final HttpClient CLIENT = HttpClient.newBuilder()
+            .followRedirects(HttpClient.Redirect.NEVER)
+            .build();
+
+    private static StrutsBootServer server;
+
+    @BeforeClass
+    public static void startServer() {
+        server = StrutsBoot.start(HelloApp.class, "--struts.boot.port=0");
+    }
+
+    @AfterClass
+    public static void stopServer() {
+        if (server != null) {
+            server.close();
+        }
+    }
+
+    @Test
+    public void testRendersFreemarkerTemplate() throws Exception {
+        assertThat(get(server.baseUrl() + 
"/hello.action").statusCode()).isEqualTo(200);
+    }
+
+    @Test
+    public void testStrutsTagRenders() throws Exception {
+        assertThat(get(server.baseUrl() + 
"/hello.action").body()).contains("Hello Boot");
+    }
+
+    @Test
+    public void testPostOnlyRejectsGet() throws Exception {
+        String url = server.baseUrl() + "/admin/saveUser.action";
+
+        assertThat(get(url).statusCode()).isEqualTo(405);
+
+        HttpResponse<String> post = post(url);
+        assertThat(post.statusCode()).isEqualTo(302);
+        
assertThat(post.headers().firstValue("Location")).hasValueSatisfying(location ->
+                assertThat(location).endsWith("/admin/users.action"));
+    }
+
+    @Test
+    public void testUnregisteredMethodNotInvokable() throws Exception {
+        UserAction.deleteInvoked = false;
+
+        HttpResponse<String> response = get(server.baseUrl() + 
"/admin/users!delete.action");
+
+        assertThat(UserAction.deleteInvoked).isFalse();
+        assertThat(response.statusCode()).isNotEqualTo(200);
+    }
+
+    @Test
+    public void testXmlAndRegistryCoexist() throws Exception {
+        assertThat(get(server.baseUrl() + 
"/xml/legacy.action").statusCode()).isEqualTo(200);
+        assertThat(get(server.baseUrl() + 
"/hello.action").statusCode()).isEqualTo(200);
+    }
+
+    @Test
+    public void testConfigurationErrorStopsTomcatAndCleansBaseDir() throws 
Exception {
+        Set<Path> before = bootTempDirs();
+
+        assertThatThrownBy(() -> StrutsBoot.start(BrokenApp.class, 
"--struts.boot.port=0"))
+                .isInstanceOf(ConfigurationException.class)
+                .satisfies(e -> assertThat(messagesOf(e)).anyMatch(message -> 
message.contains("templates/missing.ftl")));
+
+        assertThat(bootTempDirs()).isEqualTo(before);
+    }
+
+    @Test
+    public void testBusyPortFailsCleanly() throws Exception {
+        Set<Path> before = bootTempDirs();
+        try (ServerSocket busy = new ServerSocket(0)) {
+            assertThatThrownBy(() -> StrutsBoot.start(HelloApp.class, 
"--struts.boot.port=" + busy.getLocalPort()))
+                    .isInstanceOf(ConfigurationException.class);
+        }
+
+        assertThat(bootTempDirs()).isEqualTo(before);
+    }
+
+    @Test
+    public void testContextPathAppliesToActionsAndRedirects() throws Exception 
{
+        try (StrutsBootServer ctx = StrutsBoot.start(HelloApp.class, 
"--struts.boot.port=0", "--struts.boot.context-path=/ctx")) {
+            assertThat(ctx.baseUrl()).endsWith("/ctx");
+            assertThat(get("http://127.0.0.1:"; + ctx.port() + 
"/ctx/hello.action").statusCode()).isEqualTo(200);
+
+            HttpResponse<String> post = post("http://127.0.0.1:"; + ctx.port() 
+ "/ctx/admin/saveUser.action");
+            
assertThat(post.headers().firstValue("Location")).hasValueSatisfying(location ->
+                    assertThat(location).contains("/ctx/admin/users.action"));
+        }
+    }
+
+    @Test
+    public void testCloseDeletesBaseDir() {
+        StrutsBootServer other = StrutsBoot.start(HelloApp.class, 
"--struts.boot.port=0");
+        Path baseDir = other.baseDir();
+        assertThat(baseDir).exists();
+
+        other.close();
+        other.close();
+
+        assertThat(Files.exists(baseDir)).isFalse();
+    }
+
+    private static HttpResponse<String> get(String url) throws IOException, 
InterruptedException {
+        return 
CLIENT.send(HttpRequest.newBuilder(URI.create(url)).GET().build(), 
HttpResponse.BodyHandlers.ofString());
+    }
+
+    private static HttpResponse<String> post(String url) throws IOException, 
InterruptedException {
+        HttpRequest request = HttpRequest.newBuilder(URI.create(url))
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .POST(HttpRequest.BodyPublishers.ofString(""))
+                .build();
+        return CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
+    }
+
+    private static Set<Path> bootTempDirs() throws IOException {
+        try (Stream<Path> entries = 
Files.list(Path.of(System.getProperty("java.io.tmpdir")))) {
+            return entries.filter(path -> 
path.getFileName().toString().startsWith("struts-boot-")).collect(Collectors.toSet());
+        }
+    }
+
+    private static Stream<String> messagesOf(Throwable throwable) {
+        Stream.Builder<String> messages = Stream.builder();
+        for (Throwable current = throwable; current != null; current = 
current.getCause()) {
+            if (current.getMessage() != null) {
+                messages.add(current.getMessage());
+            }
+        }
+        return messages.build();
+    }
+}
diff --git a/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java 
b/boot/src/test/java/org/apache/struts2/boot/fixture/BrokenApp.java
similarity index 74%
copy from boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java
copy to boot/src/test/java/org/apache/struts2/boot/fixture/BrokenApp.java
index 56a5bde33..6712ffb24 100644
--- a/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java
+++ b/boot/src/test/java/org/apache/struts2/boot/fixture/BrokenApp.java
@@ -18,21 +18,13 @@
  */
 package org.apache.struts2.boot.fixture;
 
-public class UserAction {
+import org.apache.struts2.boot.ActionRegistry;
+import org.apache.struts2.boot.StrutsBootApplication;
 
-    public String execute() {
-        return "success";
-    }
-
-    public String list() {
-        return "success";
-    }
-
-    public String save() {
-        return "success";
-    }
+public class BrokenApp implements StrutsBootApplication {
 
-    public String delete() {
-        return "success";
+    @Override
+    public void actions(ActionRegistry actions) {
+        actions.action("broken", HelloAction.class).success("missing.ftl");
     }
 }
diff --git a/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java 
b/boot/src/test/java/org/apache/struts2/boot/fixture/HelloAction.java
similarity index 82%
copy from boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java
copy to boot/src/test/java/org/apache/struts2/boot/fixture/HelloAction.java
index 56a5bde33..82d1b3bf1 100644
--- a/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java
+++ b/boot/src/test/java/org/apache/struts2/boot/fixture/HelloAction.java
@@ -18,21 +18,13 @@
  */
 package org.apache.struts2.boot.fixture;
 
-public class UserAction {
+public class HelloAction {
 
     public String execute() {
         return "success";
     }
 
-    public String list() {
-        return "success";
-    }
-
-    public String save() {
-        return "success";
-    }
-
-    public String delete() {
-        return "success";
+    public String getMessage() {
+        return "Hello Boot";
     }
 }
diff --git a/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java 
b/boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java
similarity index 57%
copy from boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java
copy to boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java
index 56a5bde33..eaf21163a 100644
--- a/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java
+++ b/boot/src/test/java/org/apache/struts2/boot/fixture/HelloApp.java
@@ -18,21 +18,19 @@
  */
 package org.apache.struts2.boot.fixture;
 
-public class UserAction {
+import org.apache.struts2.boot.ActionRegistry;
+import org.apache.struts2.boot.StrutsBootApplication;
 
-    public String execute() {
-        return "success";
-    }
-
-    public String list() {
-        return "success";
-    }
+import static org.apache.struts2.boot.HttpMethods.POST;
 
-    public String save() {
-        return "success";
-    }
+public class HelloApp implements StrutsBootApplication {
 
-    public String delete() {
-        return "success";
+    @Override
+    public void actions(ActionRegistry actions) {
+        actions.action("hello", HelloAction.class).success("hello.ftl");
+        actions.namespace("/admin", admin -> {
+            admin.action("users", 
UserAction.class).method("list").success("admin/users.ftl");
+            admin.action("saveUser", 
UserAction.class).method("save").httpMethods(POST).redirectAction("success", 
"users");
+        });
     }
 }
diff --git a/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java 
b/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java
index 56a5bde33..512f304ed 100644
--- a/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java
+++ b/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java
@@ -20,6 +20,8 @@ package org.apache.struts2.boot.fixture;
 
 public class UserAction {
 
+    public static volatile boolean deleteInvoked;
+
     public String execute() {
         return "success";
     }
@@ -33,6 +35,7 @@ public class UserAction {
     }
 
     public String delete() {
+        deleteInvoked = true;
         return "success";
     }
 }
diff --git a/boot/src/test/resources/struts.properties 
b/boot/src/test/resources/struts.properties
new file mode 100644
index 000000000..ec4c6e8b9
--- /dev/null
+++ b/boot/src/test/resources/struts.properties
@@ -0,0 +1,17 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#  http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+struts.enable.DynamicMethodInvocation=true
diff --git a/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java 
b/boot/src/test/resources/struts.xml
similarity index 62%
copy from boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java
copy to boot/src/test/resources/struts.xml
index 56a5bde33..925f15ddc 100644
--- a/boot/src/test/java/org/apache/struts2/boot/fixture/UserAction.java
+++ b/boot/src/test/resources/struts.xml
@@ -1,3 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
  * or more contributor license agreements.  See the NOTICE file
@@ -16,23 +18,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.struts2.boot.fixture;
-
-public class UserAction {
-
-    public String execute() {
-        return "success";
-    }
-
-    public String list() {
-        return "success";
-    }
-
-    public String save() {
-        return "success";
-    }
-
-    public String delete() {
-        return "success";
-    }
-}
+-->
+<!DOCTYPE struts PUBLIC
+        "-//Apache Software Foundation//DTD Struts Configuration 6.0//EN"
+        "https://struts.apache.org/dtds/struts-6.0.dtd";>
+<struts>
+    <package name="xml-pkg" namespace="/xml" extends="struts-default">
+        <action name="legacy" 
class="org.apache.struts2.boot.fixture.HelloAction">
+            <result type="freemarker">/templates/hello.ftl</result>
+        </action>
+    </package>
+</struts>
diff --git a/boot/src/test/resources/templates/hello.ftl 
b/boot/src/test/resources/templates/hello.ftl
index 6e7c1e6ac..102b1e4a4 100644
--- a/boot/src/test/resources/templates/hello.ftl
+++ b/boot/src/test/resources/templates/hello.ftl
@@ -1 +1 @@
-<h1>hello</h1>
+<h1><@s.property value="message"/></h1>

Reply via email to