fix(core): enable WithLazyParams interceptor configuration in stacks (#1414)

Resolves WW-5586

Previously, interceptors implementing WithLazyParams could not be
configured via interceptor stack parameters because DefaultInterceptorFactory
skipped property setting entirely for these interceptors.

Changes:
- DefaultInterceptorFactory: Always set properties for all interceptors
- WithLazyParams: Updated JavaDoc to clarify dual initialization
- Added test for stack parameter configuration with WithLazyParams

Expression parameters are re-evaluated at invocation time via LazyParamInjector,
while static parameters remain unchanged. This enables both static configuration
(e.g., allowedTypes) and dynamic expressions (e.g., ${maxUploadSize}) in the
same interceptor stack reference.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Lukasz Lenart
2025-11-17 15:24:09 +01:00
committed by GitHub
parent 3e7710c583
commit 6cfd34c945
5 changed files with 142 additions and 82 deletions
@@ -68,11 +68,10 @@ public class DefaultInterceptorFactory implements InterceptorFactory {
throw new ConfigurationException("Class [" + interceptorClassName + "] does not implement Interceptor", interceptorConfig);
}
reflectionProvider.setProperties(params, interceptor);
if (interceptor instanceof WithLazyParams) {
LOG.debug("Interceptor {} is marked with interface {} and params will be set during action invocation",
LOG.debug("Interceptor {} implements {} - expression parameters will be re-evaluated during action invocation",
interceptorClassName, WithLazyParams.class.getName());
} else {
reflectionProvider.setProperties(params, interceptor);
}
interceptor.init();
@@ -29,10 +29,19 @@ import org.apache.struts2.util.reflection.ReflectionProvider;
import java.util.Map;
/**
* Interceptors marked with this interface won't be fully initialised during initialisation.
* Appropriated params will be injected just before usage of the interceptor.
*
* Please be aware that in such case {@link Interceptor#init()} method must be prepared for this.
* Interceptors marked with this interface support dynamic parameter evaluation at action invocation time.
* Parameters are set during interceptor creation (factory time), then re-evaluated during each action
* invocation to resolve expressions like ${someValue}.
* <p>
* This enables both:
* <ul>
* <li>Static configuration in interceptor stacks (e.g., allowedTypes="image/png,image/jpeg")</li>
* <li>Dynamic expressions evaluated per-request (e.g., maximumSize="${maxUploadSize}")</li>
* </ul>
* <p>
* The {@link Interceptor#init()} method is called after initial parameter setting, so interceptors
* can rely on configured values during initialization. Expression parameters (containing ${...})
* are re-evaluated at invocation time via {@link LazyParamInjector}.
*
* @since 2.5.9
*/
@@ -68,7 +77,7 @@ public interface WithLazyParams {
public Interceptor injectParams(Interceptor interceptor, Map<String, String> params, ActionContext invocationContext) {
for (Map.Entry<String, String> entry : params.entrySet()) {
Object paramValue = textParser.evaluate(new char[]{ '$' }, entry.getValue(), valueEvaluator, TextParser.DEFAULT_LOOP_COUNT);
Object paramValue = textParser.evaluate(new char[]{'$'}, entry.getValue(), valueEvaluator, TextParser.DEFAULT_LOOP_COUNT);
ognlUtil.setProperty(entry.getKey(), paramValue, interceptor, invocationContext.getContextMap());
}
return interceptor;
@@ -384,6 +384,33 @@ public class DefaultActionInvocationTest extends XWorkTestCase {
assertEquals("this is blah", action.getName());
}
/**
* Test for WW-5586: WithLazyParams interceptors can be configured in interceptor stacks
* with both static parameters and dynamic expression parameters.
*/
public void testInvokeWithLazyParamsStackConfiguration() throws Exception {
HashMap<String, Object> params = new HashMap<>();
params.put("blah", "dynamic value");
ActionContext extraContext = ActionContext.of()
.withParameters(HttpParameters.create(params).build());
DefaultActionInvocation defaultActionInvocation = new DefaultActionInvocation(extraContext.getContextMap(), true);
container.inject(defaultActionInvocation);
ActionProxy actionProxy = actionProxyFactory.createActionProxy("", "LazyFooWithStackParams", null, extraContext.getContextMap());
defaultActionInvocation.init(actionProxy);
defaultActionInvocation.invoke();
SimpleAction action = (SimpleAction) defaultActionInvocation.getAction();
// Verify expression parameter is evaluated at invocation time
assertEquals("dynamic value", action.getName());
// Verify static parameter is set and not evaluated as expression
assertEquals("static value", action.getBlah());
}
public void testInvokeWithAsyncManager() throws Exception {
DefaultActionInvocation dai = new DefaultActionInvocation(new HashMap<>(), false);
dai.stack = container.getInstance(ValueStackFactory.class).createValueStack();
@@ -458,7 +485,7 @@ public class DefaultActionInvocationTest extends XWorkTestCase {
public void testActionEventListener() throws Exception {
ActionProxy actionProxy = actionProxyFactory.createActionProxy("",
"ExceptionFoo", "exceptionMethod", new HashMap<>());
"ExceptionFoo", "exceptionMethod", new HashMap<>());
DefaultActionInvocation defaultActionInvocation = (DefaultActionInvocation) actionProxy.getInvocation();
SimpleActionEventListener actionEventListener = new SimpleActionEventListener("prepared", "exceptionHandled");
@@ -21,20 +21,36 @@ package org.apache.struts2.mock;
import org.apache.struts2.ActionInvocation;
import org.apache.struts2.SimpleAction;
import org.apache.struts2.interceptor.AbstractInterceptor;
import org.apache.struts2.interceptor.Interceptor;
import org.apache.struts2.interceptor.WithLazyParams;
public class MockLazyInterceptor extends AbstractInterceptor implements WithLazyParams {
private String foo = "";
private String bar = "";
public void setFoo(String foo) {
this.foo = foo;
}
public String getFoo() {
return foo;
}
public void setBar(String bar) {
this.bar = bar;
}
public String getBar() {
return bar;
}
public String intercept(ActionInvocation invocation) throws Exception {
if (invocation.getAction() instanceof SimpleAction) {
((SimpleAction) invocation.getAction()).setName(foo);
// Only set blah if bar is configured (not empty)
if (bar != null && !bar.isEmpty()) {
((SimpleAction) invocation.getAction()).setBlah(bar);
}
}
return invocation.invoke();
}
+81 -72
View File
@@ -45,7 +45,7 @@
</action>
<action name="ExceptionFoo" class="org.apache.struts2.SimpleAction" method="exceptionMethod">
<result name="exceptionHandled" type="void" />
<result name="exceptionHandled" type="void"/>
<interceptor-ref name="debugStack"/>
<interceptor-ref name="defaultStack"/>
</action>
@@ -56,77 +56,86 @@
</action>
<action name="LazyFoo" class="org.apache.struts2.SimpleAction">
<result name="error" type="void" />
<interceptor-ref name="params"/>
<interceptor-ref name="lazy">
<param name="foo">${blah}</param>
</interceptor-ref>
<result name="error" type="void"/>
<interceptor-ref name="params"/>
<interceptor-ref name="lazy">
<param name="foo">${blah}</param>
</interceptor-ref>
</action>
<action name="LazyFooWithStackParams" class="org.apache.struts2.SimpleAction">
<result name="error" type="void"/>
<interceptor-ref name="params"/>
<interceptor-ref name="lazy">
<param name="foo">${blah}</param>
<param name="bar">static value</param>
</interceptor-ref>
</action>
<action name="WildCard" class="org.apache.struts2.SimpleAction">
<param name="foo">17</param>
<param name="bar">23</param>
<result name="success" type="void" />
<result name="*" type="mock" />
<result name="success" type="void"/>
<result name="*" type="mock"/>
<interceptor-ref name="defaultStack"/>
</action>
<action name="WildCardInput" class="org.apache.struts2.SimpleAction" method="input">
<param name="foo">17</param>
<param name="bar">23</param>
<result name="success" type="void" />
<result name="*" type="mock" />
<result name="success" type="void"/>
<result name="*" type="mock"/>
<interceptor-ref name="defaultStack"/>
</action>
<action name="WildCardError" class="org.apache.struts2.SimpleAction">
<result name="success" type="void" />
<result name="*" type="mock" />
<result name="success" type="void"/>
<result name="*" type="mock"/>
<interceptor-ref name="defaultStack"/>
</action>
<action name="WildCard/*/*" class="org.apache.struts2.{1}Action"
method="{2}">
<action name="WildCard/*/*" class="org.apache.struts2.{1}Action"
method="{2}">
<param name="foo">{1}</param>
<param name="bar">{2}</param>
<result name="success" type="void" />
<result name="success" type="void"/>
<interceptor-ref name="defaultStack"/>
</action>
<action name="aliasTest" class="org.apache.struts2.SimpleAction">
<param name="aliases">#{ "aliasSource" : "aliasDest", "bar":"baz", "notExisting":"blah" }</param>
<interceptor-ref name="params"/>
<interceptor-ref name="alias"/>
<result name="success" type="mock" />
<param name="aliases">#{ "aliasSource" : "aliasDest", "bar":"baz", "notExisting":"blah" }</param>
<interceptor-ref name="params"/>
<interceptor-ref name="alias"/>
<result name="success" type="mock"/>
</action>
<action name="dynamicAliasTest" class="org.apache.struts2.SimpleAction">
<param name="aliases">#{ #parameters['name'] : #parameters['value'] }</param>
<interceptor-ref name="params"/>
<interceptor-ref name="alias"/>
<result name="success" type="mock" />
<result name="success" type="mock"/>
</action>
<action name="packagelessAction" class="PackagelessAction">
</action>
<action name="Bar" class="org.apache.struts2.SimpleAction">
<param name="foo">17</param>
<param name="foo">17</param>
<param name="bar">23</param>
<result name="error" type="mock" />
<result name="error" type="mock"/>
</action>
<action name="MyBean" class="org.apache.struts2.util.MyBeanAction">
<interceptor-ref name="debugStack"/>
<interceptor-ref name="defaultStack"/>
<result name="success" type="mock" />
<result name="success" type="mock"/>
</action>
<action name="TestInterceptorParam" class="org.apache.struts2.SimpleAction">
<interceptor-ref name="test">
<param name="expectedFoo">expectedFoo</param>
</interceptor-ref>
<result name="error" type="mock" />
<result name="error" type="mock"/>
</action>
<action name="TestInterceptorParamOverride" class="org.apache.struts2.SimpleAction">
@@ -134,18 +143,18 @@
<param name="foo">foo123</param>
<param name="expectedFoo">foo123</param>
</interceptor-ref>
<result name="error" type="mock" />
<result name="error" type="mock"/>
</action>
<action name="TestModelDrivenValidation" class="org.apache.struts2.ModelDrivenAction">
<interceptor-ref name="defaultStack"/>
<interceptor-ref name="validation"/>
<result name="success" type="mock" />
<result name="success" type="mock"/>
</action>
<!-- chain resursion detection -->
<action name="InfiniteRecursionChain" class="org.apache.struts2.ActionSupport">
<result name="success" type="chain">InfiniteRecursionChain</result>
<result name="success" type="chain">InfiniteRecursionChain</result>
</action>
<action name="chain_with_namespace" class="org.apache.struts2.SimpleAction">
<result name="error" type="chain">
@@ -163,9 +172,9 @@
<interceptor-ref name="defaultStack"/>
</action>
</package>
</package>
<package name="bar" extends="default" namespace="/foo/bar">
<package name="bar" extends="default" namespace="/foo/bar">
<interceptors>
<interceptor-stack name="barDefaultStack">
<interceptor-ref name="debugStack"/>
@@ -179,14 +188,14 @@
<action name="Bar" class="org.apache.struts2.SimpleAction">
<interceptor-ref name="barDefaultStack"/>
<result name="error" type="mock" />
<result name="error" type="mock"/>
</action>
<action name="TestInterceptorParamInheritance" class="org.apache.struts2.SimpleAction">
<interceptor-ref name="test">
<param name="expectedFoo">expectedFoo</param>
</interceptor-ref>
<result name="error" type="mock" />
<result name="error" type="mock"/>
</action>
<action name="TestInterceptorParamInehritanceOverride" class="org.apache.struts2.SimpleAction">
@@ -194,7 +203,7 @@
<param name="foo">foo123</param>
<param name="expectedFoo">foo123</param>
</interceptor-ref>
<result name="error" type="mock" />
<result name="error" type="mock"/>
</action>
</package>
@@ -210,7 +219,7 @@
<result name="error" type="chain">
<param name="actionName">bar</param>
</result>
<result name="success" type="mock" />
<result name="success" type="mock"/>
<interceptor-ref name="staticParams"/>
</action>
<action name="myCommand" class="org.apache.struts2.SimpleAction" method="commandMethod">
@@ -222,10 +231,10 @@
<interceptor-ref name="logger"/>
</action>
<action name="doMethodTest" class="org.apache.struts2.SimpleAction" method="input">
<result name="input" type="mock" />
<result name="input" type="mock"/>
</action>
<action name="unknownMethodTest" class="org.apache.struts2.SimpleAction">
<result name="found" type="mock" />
<result name="found" type="mock"/>
</action>
<action name="resultAction" class="org.apache.struts2.SimpleAction" method="resultAction">
<param name="bar">456</param>
@@ -238,7 +247,7 @@
<package name="wildcardNamespaces" extends="default" namespace="/animals/*">
<action name="commandTest" class="org.apache.struts2.SimpleAction">
<result name="success" type="mock" />
<result name="success" type="mock"/>
<interceptor-ref name="staticParams"/>
</action>
</package>
@@ -253,47 +262,47 @@
</package>
<package name="Abstract-crud" extends="default">
<!-- edit is often used as the create/view -->
<default-class-ref class="org.apache.struts2.SimpleAction"/>
<action name="edit" >
<result name="input" type="mock">edit.vm</result>
<result name="success" type="mock">edit.vm</result>
<result name="error" type="mock">edit.vm</result>
</action>
<action name="save" >
<result name="input" type="mock">edit.vm</result>
<result name="success" type="chain">list</result>
<result name="error" type="mock">edit.vm</result>
<result name="cancel" type="mock">list.action</result>
</action>
<action name="list">
<result name="success" type="mock">list</result>
</action>
<action name="delete">
<result name="success" type="mock">list</result>
</action>
<package name="Abstract-crud" extends="default">
<!-- edit is often used as the create/view -->
<default-class-ref class="org.apache.struts2.SimpleAction"/>
<action name="edit">
<result name="input" type="mock">edit.vm</result>
<result name="success" type="mock">edit.vm</result>
<result name="error" type="mock">edit.vm</result>
</action>
<action name="save">
<result name="input" type="mock">edit.vm</result>
<result name="success" type="chain">list</result>
<result name="error" type="mock">edit.vm</result>
<result name="cancel" type="mock">list.action</result>
</action>
<action name="list">
<result name="success" type="mock">list</result>
</action>
<action name="delete">
<result name="success" type="mock">list</result>
</action>
</package>
</package>
<package name="Example" extends="Abstract-crud" namespace="/example">
<default-class-ref class="org.apache.struts2.ModelDrivenAction" />
</package>
<package name="Example" extends="Abstract-crud" namespace="/example">
<default-class-ref class="org.apache.struts2.ModelDrivenAction"/>
</package>
<package name="Example2" extends="Abstract-crud" namespace="/example2">
<default-class-ref class="org.apache.struts2.ModelDrivenAction" />
<action name="override">
<result name="success" type="mock">somethingelse.vm</result>
</action>
</package>
<package name="Example2" extends="Abstract-crud" namespace="/example2">
<default-class-ref class="org.apache.struts2.ModelDrivenAction"/>
<action name="override">
<result name="success" type="mock">somethingelse.vm</result>
</action>
</package>
<package name="SubItem" extends="Abstract-crud" namespace="/example2/subItem">
<default-class-ref class="org.apache.struts2.ModelDrivenAction" />
</package>
<package name="SubItem" extends="Abstract-crud" namespace="/example2/subItem">
<default-class-ref class="org.apache.struts2.ModelDrivenAction"/>
</package>
<package name="Example3" extends="Abstract-crud" namespace="/example3">
<!-- default-class-ref is expected to be inherited -->
</package>
<package name="Example3" extends="Abstract-crud" namespace="/example3">
<!-- default-class-ref is expected to be inherited -->
</package>
<include file="includeTest.xml"/>
</struts>