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 8be5c47e546a7eec9a74bd13812bb106d11c38ac
Author: Lukasz Lenart <[email protected]>
AuthorDate: Mon Nov 2 08:06:20 2026 +0100

    WW-4164 feat(core): build action URLs with the target action's extension
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../org/apache/struts2/components/Component.java   |  12 +++
 .../struts2/components/ServletUrlRenderer.java     |   1 +
 .../dispatcher/mapper/ActionExtensionResolver.java |  92 ++++++++++++++++
 .../org/apache/struts2/result/PostbackResult.java  |  15 ++-
 .../result/ServletActionRedirectResult.java        |  17 ++-
 core/src/main/resources/struts-beans.xml           |   1 +
 .../mapper/ActionExtensionResolverTest.java        | 116 +++++++++++++++++++++
 .../apache/struts2/result/PostbackResultTest.java  |  33 ++++++
 .../result/ServletActionRedirectResultTest.java    |  33 ++++++
 .../org/apache/struts2/views/jsp/URLTagTest.java   |  18 ++++
 .../apache/struts2/views/jsp/ui/FormTagTest.java   |  18 ++++
 .../struts2/json/JSONActionRedirectResultTest.java |   4 +
 12 files changed, 358 insertions(+), 2 deletions(-)

diff --git a/core/src/main/java/org/apache/struts2/components/Component.java 
b/core/src/main/java/org/apache/struts2/components/Component.java
index 10e6373cc..d1e9f7d2c 100644
--- a/core/src/main/java/org/apache/struts2/components/Component.java
+++ b/core/src/main/java/org/apache/struts2/components/Component.java
@@ -31,6 +31,7 @@ import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.struts2.StrutsConstants;
 import org.apache.struts2.StrutsException;
+import org.apache.struts2.dispatcher.mapper.ActionExtensionResolver;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
 import org.apache.struts2.dispatcher.mapper.ActionMapping;
 import org.apache.struts2.util.ComponentUtils;
@@ -81,6 +82,7 @@ public class Component {
     protected ValueStack stack;
     protected Map<String, Object> attributes;
     protected ActionMapper actionMapper;
+    protected ActionExtensionResolver actionExtensionResolver;
     protected boolean throwExceptionOnELFailure;
     protected boolean performClearTagStateForTagPoolingServers = false;
     private UrlHelper urlHelper;
@@ -121,6 +123,15 @@ public class Component {
         this.actionMapper = mapper;
     }
 
+    /**
+     * @param actionExtensionResolver chooses the extension of generated 
action URLs
+     * @since 7.5.0
+     */
+    @Inject
+    public void setActionExtensionResolver(ActionExtensionResolver 
actionExtensionResolver) {
+        this.actionExtensionResolver = actionExtensionResolver;
+    }
+
     @Inject(StrutsConstants.STRUTS_EL_THROW_EXCEPTION)
     public void setThrowExceptionsOnELFailure(String throwException) {
         this.throwExceptionOnELFailure = 
BooleanUtils.toBoolean(throwException);
@@ -422,6 +433,7 @@ public class Component {
         String finalMethod = method != null ? findString(method) : null;
         String finalNamespace = determineNamespace(namespace, getStack(), req);
         ActionMapping mapping = new ActionMapping(finalAction, finalNamespace, 
finalMethod, parameters);
+        actionExtensionResolver.applyTo(mapping);
         String uri = actionMapper.getUriFromActionMapping(mapping);
         return urlHelper.buildUrl(uri, req, res, parameters, scheme, 
includeContext, encodeResult, forceAddSchemeHostAndPort, escapeAmp);
     }
diff --git 
a/core/src/main/java/org/apache/struts2/components/ServletUrlRenderer.java 
b/core/src/main/java/org/apache/struts2/components/ServletUrlRenderer.java
index c6bfec306..438fe72ae 100644
--- a/core/src/main/java/org/apache/struts2/components/ServletUrlRenderer.java
+++ b/core/src/main/java/org/apache/struts2/components/ServletUrlRenderer.java
@@ -176,6 +176,7 @@ public class ServletUrlRenderer implements UrlRenderer {
         if (actionConfig != null) {
 
             ActionMapping mapping = new ActionMapping(actionName, namespace, 
actionMethod, formComponent.attributes);
+            formComponent.actionExtensionResolver.applyTo(mapping);
             String result = 
urlHelper.buildUrl(formComponent.actionMapper.getUriFromActionMapping(mapping),
                 formComponent.request, formComponent.response, 
queryStringResult.getQueryParams(), scheme, formComponent.includeContext, true, 
false, false);
             formComponent.addParameter("action", result);
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java
new file mode 100644
index 000000000..b4d450cd8
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolver.java
@@ -0,0 +1,92 @@
+/*
+ * 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.dispatcher.mapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.struts2.ActionContext;
+import org.apache.struts2.StrutsConstants;
+import org.apache.struts2.config.Configuration;
+import org.apache.struts2.config.entities.ActionConfig;
+import org.apache.struts2.inject.Inject;
+
+import java.util.List;
+
+/**
+ * Chooses the extension of a generated action URL from the extensions the 
target action accepts, so links,
+ * forms and redirects point at an extension the target can be reached by.
+ *
+ * @since 7.5.0
+ */
+public class ActionExtensionResolver {
+
+    private Configuration configuration;
+    private List<String> extensions;
+
+    /**
+     * @param configuration the configuration used to look up the target action
+     * @since 7.5.0
+     */
+    @Inject
+    public void setConfiguration(Configuration configuration) {
+        this.configuration = configuration;
+    }
+
+    /**
+     * @param extensions the global {@code struts.action.extension} list
+     * @since 7.5.0
+     */
+    @Inject(StrutsConstants.STRUTS_ACTION_EXTENSION)
+    public void setExtensions(String extensions) {
+        this.extensions = StringUtils.isNotEmpty(extensions) ? 
ActionExtensions.parse(extensions) : null;
+    }
+
+    /**
+     * Sets the extension of {@code target} to the current request's extension 
when the target accepts it, otherwise
+     * to the target's first accepted extension, which is also used when the 
current request has no extension, as in
+     * {@link DefaultActionMapper#lookupExtension(String)}. Leaves {@code 
target} unchanged when it already has an extension,
+     * when the target action is unknown, or when neither the target nor the 
global constant declares extensions.
+     *
+     * @param target the mapping of the action the URL points at
+     * @since 7.5.0
+     */
+    public void applyTo(ActionMapping target) {
+        if (target.getExtension() != null) {
+            return;
+        }
+        ActionConfig actionConfig = configuration.getRuntimeConfiguration()
+            .getActionConfig(target.getNamespace(), 
StringUtils.substringBefore(target.getName(), "!"));
+        if (actionConfig == null) {
+            return;
+        }
+        List<String> effective = actionConfig.getExtensions() != null ? 
actionConfig.getExtensions() : extensions;
+        if (effective == null) {
+            return;
+        }
+        String current = currentExtension();
+        target.setExtension(current == null ? effective.get(0) : 
ActionExtensions.select(effective, current));
+    }
+
+    private String currentExtension() {
+        ActionContext context = ActionContext.getContext();
+        if (context == null || context.getActionMapping() == null) {
+            return null;
+        }
+        return context.getActionMapping().getExtension();
+    }
+}
diff --git a/core/src/main/java/org/apache/struts2/result/PostbackResult.java 
b/core/src/main/java/org/apache/struts2/result/PostbackResult.java
index 519feee9d..0ac987adb 100644
--- a/core/src/main/java/org/apache/struts2/result/PostbackResult.java
+++ b/core/src/main/java/org/apache/struts2/result/PostbackResult.java
@@ -24,6 +24,7 @@ import org.apache.struts2.inject.Inject;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import org.apache.commons.text.StringEscapeUtils;
+import org.apache.struts2.dispatcher.mapper.ActionExtensionResolver;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
 import org.apache.struts2.dispatcher.mapper.ActionMapping;
 
@@ -86,6 +87,7 @@ public class PostbackResult extends StrutsResultSupport {
     private boolean cache = true;
 
     protected ActionMapper actionMapper;
+    private transient ActionExtensionResolver actionExtensionResolver;
 
     @Override
     protected void doExecute(String finalLocation, ActionInvocation 
invocation) throws Exception {
@@ -152,7 +154,9 @@ public class PostbackResult extends StrutsResultSupport {
             } else {
                 method = conditionalParse(method, invocation);
             }
-            postbackUri = request.getContextPath() + 
actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, 
method, null));
+            ActionMapping mapping = new ActionMapping(actionName, namespace, 
method, null);
+            actionExtensionResolver.applyTo(mapping);
+            postbackUri = request.getContextPath() + 
actionMapper.getUriFromActionMapping(mapping);
         } else {
             String location = getLocation();
             // Do not prepend if the URL is a FQN
@@ -173,6 +177,15 @@ public class PostbackResult extends StrutsResultSupport {
         this.actionMapper = mapper;
     }
 
+    /**
+     * @param actionExtensionResolver chooses the extension of the target 
action URL
+     * @since 7.5.0
+     */
+    @Inject
+    public void setActionExtensionResolver(ActionExtensionResolver 
actionExtensionResolver) {
+        this.actionExtensionResolver = actionExtensionResolver;
+    }
+
     /**
      * Sets the name of the destination action.
      *
diff --git 
a/core/src/main/java/org/apache/struts2/result/ServletActionRedirectResult.java 
b/core/src/main/java/org/apache/struts2/result/ServletActionRedirectResult.java
index fe78231ae..b4cfd356d 100644
--- 
a/core/src/main/java/org/apache/struts2/result/ServletActionRedirectResult.java
+++ 
b/core/src/main/java/org/apache/struts2/result/ServletActionRedirectResult.java
@@ -19,8 +19,10 @@
 package org.apache.struts2.result;
 
 import org.apache.struts2.ActionInvocation;
+import org.apache.struts2.dispatcher.mapper.ActionExtensionResolver;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
 import org.apache.struts2.dispatcher.mapper.ActionMapping;
+import org.apache.struts2.inject.Inject;
 import org.apache.struts2.util.reflection.ReflectionExceptionHandler;
 
 import java.io.Serial;
@@ -132,6 +134,8 @@ public class ServletActionRedirectResult extends 
ServletRedirectResult implement
     protected String namespace;
     protected String method;
 
+    private transient ActionExtensionResolver actionExtensionResolver;
+
     public ServletActionRedirectResult() {
         super();
     }
@@ -176,13 +180,24 @@ public class ServletActionRedirectResult extends 
ServletRedirectResult implement
             method = conditionalParse(method, invocation);
         }
 
-        String tmpLocation = actionMapper.getUriFromActionMapping(new 
ActionMapping(actionName, namespace, method, null));
+        ActionMapping mapping = new ActionMapping(actionName, namespace, 
method, null);
+        actionExtensionResolver.applyTo(mapping);
+        String tmpLocation = actionMapper.getUriFromActionMapping(mapping);
 
         setLocation(tmpLocation);
 
         super.execute(invocation);
     }
 
+    /**
+     * @param actionExtensionResolver chooses the extension of the target 
action URL
+     * @since 7.5.0
+     */
+    @Inject
+    public void setActionExtensionResolver(ActionExtensionResolver 
actionExtensionResolver) {
+        this.actionExtensionResolver = actionExtensionResolver;
+    }
+
     /**
      * Sets the action name
      *
diff --git a/core/src/main/resources/struts-beans.xml 
b/core/src/main/resources/struts-beans.xml
index 922dc91d7..067bba743 100644
--- a/core/src/main/resources/struts-beans.xml
+++ b/core/src/main/resources/struts-beans.xml
@@ -119,6 +119,7 @@
     <bean 
type="org.apache.struts2.conversion.UserConversionPropertiesProvider" 
name="struts"
           
class="org.apache.struts2.conversion.StrutsConversionPropertiesProcessor"/>
     <bean 
class="org.apache.struts2.conversion.UserConversionPropertiesProcessor" 
scope="singleton"/>
+    <bean class="org.apache.struts2.dispatcher.mapper.ActionExtensionResolver" 
scope="singleton"/>
 
     <bean class="org.apache.struts2.conversion.impl.XWorkBasicConverter"/>
 
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java
new file mode 100644
index 000000000..061302865
--- /dev/null
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/mapper/ActionExtensionResolverTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.dispatcher.mapper;
+
+import org.apache.struts2.ActionContext;
+import org.apache.struts2.StrutsInternalTestCase;
+import org.apache.struts2.config.StrutsXmlConfigurationProvider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ActionExtensionResolverTest extends StrutsInternalTestCase {
+
+    private static final String CONFIG = 
"org/apache/struts2/config/providers/struts-extension.xml";
+
+    private ActionExtensionResolver resolver;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        loadConfigurationProviders(new StrutsXmlConfigurationProvider(CONFIG));
+        resolver = new ActionExtensionResolver();
+        resolver.setConfiguration(configuration);
+        resolver.setExtensions("action,");
+    }
+
+    public void testDeclaredTargetUsesFirstWhenCurrentNotAllowed() {
+        withCurrentExtension("action");
+
+        assertThat(resolve("/child", "inherits")).isEqualTo("html");
+    }
+
+    public void testDeclaredTargetKeepsAllowedCurrent() {
+        withCurrentExtension("json");
+
+        assertThat(resolve("/child", "own")).isEqualTo("json");
+    }
+
+    public void testUndeclaredTargetFromDeclaredOnlyPage() {
+        withCurrentExtension("html");
+
+        assertThat(resolve("/plain", "undeclared")).isEqualTo("action");
+    }
+
+    public void testUndeclaredTargetKeepsGlobalCurrent() {
+        withCurrentExtension("");
+
+        assertThat(resolve("/plain", "undeclared")).isEmpty();
+    }
+
+    public void testExplicitExtensionUntouched() {
+        withCurrentExtension("action");
+        ActionMapping target = new ActionMapping("inherits", "/child", null, 
null);
+        target.setExtension("");
+
+        resolver.applyTo(target);
+
+        assertThat(target.getExtension()).isEmpty();
+    }
+
+    public void testUnknownActionUntouched() {
+        withCurrentExtension("action");
+
+        assertThat(resolve("/nowhere", "x")).isNull();
+    }
+
+    public void testDmiNameResolvesActionConfig() {
+        withCurrentExtension("action");
+
+        assertThat(resolve("/child", "inherits!foo")).isEqualTo("html");
+    }
+
+    public void testWithoutCurrentMappingUsesFirstAllowed() {
+        assertThat(resolve("/child", "inherits")).isEqualTo("html");
+    }
+
+    public void testRequestWithoutExtensionUsesGlobalDefault() {
+        withCurrentExtension(null);
+
+        assertThat(resolve("/plain", "undeclared")).isEqualTo("action");
+    }
+
+    public void testNoDeclaredAndNoGlobalListUntouched() {
+        resolver.setExtensions("");
+        withCurrentExtension("action");
+
+        assertThat(resolve("/plain", "undeclared")).isNull();
+    }
+
+    private String resolve(String namespace, String name) {
+        ActionMapping target = new ActionMapping(name, namespace, null, null);
+        resolver.applyTo(target);
+        return target.getExtension();
+    }
+
+    private void withCurrentExtension(String extension) {
+        ActionMapping current = new ActionMapping("current", "/", null, null);
+        current.setExtension(extension);
+        ActionContext.getContext().withActionMapping(current);
+    }
+}
diff --git 
a/core/src/test/java/org/apache/struts2/result/PostbackResultTest.java 
b/core/src/test/java/org/apache/struts2/result/PostbackResultTest.java
index c17bfdbd3..1d1e44d27 100644
--- a/core/src/test/java/org/apache/struts2/result/PostbackResultTest.java
+++ b/core/src/test/java/org/apache/struts2/result/PostbackResultTest.java
@@ -23,7 +23,9 @@ import org.apache.struts2.ActionInvocation;
 import org.apache.struts2.ActionProxy;
 import org.apache.struts2.ServletActionContext;
 import org.apache.struts2.StrutsInternalTestCase;
+import org.apache.struts2.dispatcher.mapper.ActionExtensionResolver;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
 import org.apache.struts2.dispatcher.mapper.DefaultActionMapper;
 import org.apache.struts2.util.ValueStack;
 import org.easymock.IMocksControl;
@@ -58,6 +60,7 @@ public class PostbackResultTest extends 
StrutsInternalTestCase {
 
         control.replay();
         result.setActionMapper(container.getInstance(ActionMapper.class));
+        
result.setActionExtensionResolver(container.getInstance(ActionExtensionResolver.class));
         result.execute(mockInvocation);
         assertEquals("<!DOCTYPE html><html><body><form 
action=\"${1-1}/myAction0.action\" method=\"POST\">" +
                 
"<script>setTimeout(function(){document.forms[0].submit();},0);</script></html>",
 res.getContentAsString());
@@ -86,6 +89,7 @@ public class PostbackResultTest extends 
StrutsInternalTestCase {
 
         control.replay();
         result.setActionMapper(container.getInstance(ActionMapper.class));
+        
result.setActionExtensionResolver(container.getInstance(ActionExtensionResolver.class));
         result.execute(mockInvocation);
         assertEquals("<!DOCTYPE html><html><body><form 
action=\"myNamespace0/myAction0.action\" method=\"POST\">" +
                 
"<script>setTimeout(function(){document.forms[0].submit();},0);</script></html>",
 res.getContentAsString());
@@ -120,6 +124,7 @@ public class PostbackResultTest extends 
StrutsInternalTestCase {
         DefaultActionMapper mapper = (DefaultActionMapper) 
container.getInstance(ActionMapper.class);
         mapper.setAllowDynamicMethodCalls("true");
         result.setActionMapper(mapper);
+        
result.setActionExtensionResolver(container.getInstance(ActionExtensionResolver.class));
         result.execute(mockInvocation);
         assertEquals("<!DOCTYPE html><html><body><form 
action=\"/myNamespace${1-1}/myAction${1-1}!myMethod${1-1}.action\" 
method=\"POST\">" +
                 
"<script>setTimeout(function(){document.forms[0].submit();},0);</script></html>",
 res.getContentAsString());
@@ -249,4 +254,32 @@ public class PostbackResultTest extends 
StrutsInternalTestCase {
         control.verify();
     }
 
+    public void testPostbackUsesDeclaredExtension() throws Exception {
+        
initDispatcherWithConfigs("struts-default.xml,org/apache/struts2/config/providers/struts-extension.xml");
+        ActionContext context = ActionContext.getContext();
+        MockHttpServletRequest req = new MockHttpServletRequest();
+        MockHttpServletResponse res = new MockHttpServletResponse();
+        context.put(ServletActionContext.HTTP_REQUEST, req);
+        context.put(ServletActionContext.HTTP_RESPONSE, res);
+        ActionMapping current = new ActionMapping("current", "/", null, null);
+        current.setExtension("action");
+        context.withActionMapping(current);
+
+        PostbackResult result = new PostbackResult();
+        result.setActionName("inherits");
+        result.setNamespace("/child");
+        result.setPrependServletContext(false);
+
+        IMocksControl control = createControl();
+        ActionInvocation mockInvocation = 
control.createMock(ActionInvocation.class);
+        
expect(mockInvocation.getInvocationContext()).andReturn(context).anyTimes();
+        
expect(mockInvocation.getStack()).andReturn(context.getValueStack()).anyTimes();
+
+        control.replay();
+        container.inject(result);
+        result.execute(mockInvocation);
+
+        assertTrue(res.getContentAsString(), 
res.getContentAsString().contains("<form action=\"/child/inherits.html\""));
+    }
+
 }
diff --git 
a/core/src/test/java/org/apache/struts2/result/ServletActionRedirectResultTest.java
 
b/core/src/test/java/org/apache/struts2/result/ServletActionRedirectResultTest.java
index 9436422d5..337ce3188 100644
--- 
a/core/src/test/java/org/apache/struts2/result/ServletActionRedirectResultTest.java
+++ 
b/core/src/test/java/org/apache/struts2/result/ServletActionRedirectResultTest.java
@@ -28,6 +28,7 @@ import org.apache.struts2.util.ValueStack;
 import org.apache.struts2.ServletActionContext;
 import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
 import org.apache.struts2.dispatcher.mapper.DefaultActionMapper;
 import org.apache.struts2.url.StrutsQueryStringBuilder;
 import org.apache.struts2.url.StrutsUrlEncoder;
@@ -303,4 +304,36 @@ public class ServletActionRedirectResultTest extends 
StrutsInternalTestCase {
         assertNotNull(result);
     }
 
+    public void testRedirectUsesDeclaredExtension() throws Exception {
+        
initDispatcherWithConfigs("struts-default.xml,org/apache/struts2/config/providers/struts-extension.xml");
+        ActionContext context = ActionContext.getContext();
+        MockHttpServletRequest req = new MockHttpServletRequest();
+        MockHttpServletResponse res = new MockHttpServletResponse();
+        context.put(ServletActionContext.HTTP_REQUEST, req);
+        context.put(ServletActionContext.HTTP_RESPONSE, res);
+        ActionMapping current = new ActionMapping("current", "/", null, null);
+        current.setExtension("action");
+        context.withActionMapping(current);
+
+        ServletActionRedirectResult result = new ServletActionRedirectResult();
+        result.setActionName("inherits");
+        result.setNamespace("/child");
+        result.setPrependServletContext(false);
+
+        IMocksControl control = createControl();
+        ActionProxy mockActionProxy = control.createMock(ActionProxy.class);
+        ActionInvocation mockInvocation = 
control.createMock(ActionInvocation.class);
+        
expect(mockInvocation.getProxy()).andReturn(mockActionProxy).anyTimes();
+        
expect(mockInvocation.getResultCode()).andReturn("myResult").anyTimes();
+        expect(mockActionProxy.getConfig()).andReturn(new 
ActionConfig.Builder("", "", "").build()).anyTimes();
+        
expect(mockInvocation.getInvocationContext()).andReturn(context).anyTimes();
+        
expect(mockInvocation.getStack()).andReturn(context.getValueStack()).anyTimes();
+
+        control.replay();
+        container.inject(result);
+        result.execute(mockInvocation);
+
+        assertEquals("/child/inherits.html", res.getRedirectedUrl());
+    }
+
 }
diff --git a/core/src/test/java/org/apache/struts2/views/jsp/URLTagTest.java 
b/core/src/test/java/org/apache/struts2/views/jsp/URLTagTest.java
index 276d9163c..588134809 100644
--- a/core/src/test/java/org/apache/struts2/views/jsp/URLTagTest.java
+++ b/core/src/test/java/org/apache/struts2/views/jsp/URLTagTest.java
@@ -2104,6 +2104,24 @@ public class URLTagTest extends AbstractUITagTest {
         assertEquals("/books#v=twopage&q&f=false", writer.toString());
     }
 
+    public void testActionUrlUsesDeclaredExtension() throws Exception {
+        
initDispatcherWithConfigs("struts-default.xml,org/apache/struts2/config/providers/struts-extension.xml");
+        createMocks();
+        ActionMapping current = new ActionMapping("current", "/", null, null);
+        current.setExtension("action");
+        ActionContext.getContext().withActionMapping(current);
+
+        URLTag urlTag = new URLTag();
+        urlTag.setPageContext(pageContext);
+        urlTag.setAction("inherits");
+        urlTag.setNamespace("/child");
+
+        urlTag.doStartTag();
+        urlTag.doEndTag();
+
+        assertEquals("/child/inherits.html", writer.toString());
+    }
+
     @Override
     protected void setUp() throws Exception {
         super.setUp();
diff --git 
a/core/src/test/java/org/apache/struts2/views/jsp/ui/FormTagTest.java 
b/core/src/test/java/org/apache/struts2/views/jsp/ui/FormTagTest.java
index b8a89e712..80444b45a 100644
--- a/core/src/test/java/org/apache/struts2/views/jsp/ui/FormTagTest.java
+++ b/core/src/test/java/org/apache/struts2/views/jsp/ui/FormTagTest.java
@@ -27,6 +27,7 @@ import org.apache.struts2.StrutsConstants;
 import org.apache.struts2.TestAction;
 import org.apache.struts2.TestConfigurationProvider;
 import org.apache.struts2.dispatcher.mapper.ActionMapper;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
 import org.apache.struts2.dispatcher.mapper.DefaultActionMapper;
 import org.apache.struts2.views.jsp.AbstractUITagTest;
 import org.apache.struts2.views.jsp.ActionTag;
@@ -40,6 +41,23 @@ import java.util.HashMap;
  */
 public class FormTagTest extends AbstractUITagTest {
 
+    public void testFormActionUsesDeclaredExtension() throws Exception {
+        
initDispatcherWithConfigs("struts-default.xml,org/apache/struts2/config/providers/struts-extension.xml");
+        createMocks();
+        ActionMapping current = new ActionMapping("current", "/", null, null);
+        current.setExtension("action");
+        ActionContext.getContext().withActionMapping(current);
+
+        FormTag tag = new FormTag();
+        tag.setPageContext(pageContext);
+        tag.setNamespace("/child");
+        tag.setAction("inherits");
+
+        tag.doStartTag();
+        tag.doEndTag();
+
+        assertTrue(writer.toString(), 
writer.toString().contains("action=\"/child/inherits.html\""));
+    }
 
     public void testFormWithActionAttributeContainingQueryString() throws 
Exception {
         FormTag tag = new FormTag();
diff --git 
a/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java
 
b/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java
index a1c7c44fa..3204ae5e6 100644
--- 
a/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java
+++ 
b/plugins/json/src/test/java/org/apache/struts2/json/JSONActionRedirectResultTest.java
@@ -24,6 +24,7 @@ import org.apache.struts2.mock.MockActionInvocation;
 import org.apache.struts2.mock.MockActionProxy;
 import org.apache.struts2.util.ValueStack;
 import org.apache.struts2.StrutsStatics;
+import org.apache.struts2.dispatcher.mapper.ActionExtensionResolver;
 import org.apache.struts2.dispatcher.mapper.DefaultActionMapper;
 import org.apache.struts2.junit.StrutsTestCase;
 import org.apache.struts2.url.QueryStringBuilder;
@@ -49,6 +50,7 @@ public class JSONActionRedirectResultTest extends 
StrutsTestCase {
         JSONActionRedirectResult result = new JSONActionRedirectResult();
         result.setActionName("targetAction");
         result.setActionMapper(actionMapper);
+        
result.setActionExtensionResolver(container.getInstance(ActionExtensionResolver.class));
         result.setQueryStringBuilder(queryStringBuilder);
 
         Object action = new Object();
@@ -68,6 +70,7 @@ public class JSONActionRedirectResultTest extends 
StrutsTestCase {
         JSONActionRedirectResult result = new JSONActionRedirectResult();
         result.setActionName("targetAction");
         result.setActionMapper(actionMapper);
+        
result.setActionExtensionResolver(container.getInstance(ActionExtensionResolver.class));
         result.setQueryStringBuilder(queryStringBuilder);
 
         request.setParameter("struts.enableJSONValidation", "true");
@@ -88,6 +91,7 @@ public class JSONActionRedirectResultTest extends 
StrutsTestCase {
         JSONActionRedirectResult result = new JSONActionRedirectResult();
         result.setActionName("targetAction");
         result.setActionMapper(actionMapper);
+        
result.setActionExtensionResolver(container.getInstance(ActionExtensionResolver.class));
         result.setQueryStringBuilder(queryStringBuilder);
 
         request.setParameter("struts.enableJSONValidation", "true");

Reply via email to