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>
