This is an automated email from the ASF dual-hosted git repository. lukaszlenart pushed a commit to branch WW-5659-lazy-params-request-scoping in repository https://gitbox.apache.org/repos/asf/struts.git
commit 687963abae8c79e708f5a62b06ca70c32649839c Author: Lukasz Lenart <[email protected]> AuthorDate: Mon Jul 27 10:23:20 2026 +0200 WW-5659 feat(core): resolve lazy params into a holder instead of the interceptor --- .../apache/struts2/interceptor/WithLazyParams.java | 49 ++++++++ .../struts2/interceptor/LazyParamInjectorTest.java | 134 +++++++++++++++++++++ 2 files changed, 183 insertions(+) diff --git a/core/src/main/java/org/apache/struts2/interceptor/WithLazyParams.java b/core/src/main/java/org/apache/struts2/interceptor/WithLazyParams.java index cdfb3a8ad..8c74b398d 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/WithLazyParams.java +++ b/core/src/main/java/org/apache/struts2/interceptor/WithLazyParams.java @@ -18,12 +18,15 @@ */ package org.apache.struts2.interceptor; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.struts2.ActionContext; import org.apache.struts2.inject.Inject; import org.apache.struts2.ognl.OgnlUtil; import org.apache.struts2.util.TextParseUtil; import org.apache.struts2.util.TextParser; import org.apache.struts2.util.ValueStack; +import org.apache.struts2.util.reflection.ReflectionException; import org.apache.struts2.util.reflection.ReflectionProvider; import java.util.Map; @@ -49,6 +52,8 @@ public interface WithLazyParams { class LazyParamInjector { + private static final Logger LOG = LogManager.getLogger(LazyParamInjector.class); + protected OgnlUtil ognlUtil; protected TextParser textParser; protected ReflectionProvider reflectionProvider; @@ -82,5 +87,49 @@ public interface WithLazyParams { } return interceptor; } + + /** + * Resolves configured params into a per-invocation holder, leaving the interceptor untouched. + * <p> + * A {@code ${...}} expression that cannot be resolved is not written: the holder keeps its + * seeded configuration value and is notified via {@link InterceptorParams#unresolved(String)}, + * so a broken expression cannot silently relax a validation policy. + * + * @since 7.3.0 + */ + public <P extends InterceptorParams> P resolveInto(P target, Map<String, String> params, ActionContext invocationContext) { + for (Map.Entry<String, String> entry : params.entrySet()) { + String paramName = entry.getKey(); + String rawValue = entry.getValue(); + Object paramValue = textParser.evaluate(new char[]{'$'}, rawValue, valueEvaluator, TextParser.DEFAULT_LOOP_COUNT); + + if (isUnresolved(rawValue, paramValue)) { + LOG.warn("Param [{}] of [{}] could not be resolved from expression [{}]; keeping the configured value", + paramName, target.getClass().getName(), rawValue); + target.unresolved(paramName); + continue; + } + try { + // throwPropertyExceptions=true so a param with no matching property on the holder is + // reported rather than silently ignored; OgnlUtil only warns in devMode otherwise + ognlUtil.setProperty(paramName, paramValue, target, invocationContext.getContextMap(), true); + } catch (ReflectionException e) { + LOG.warn("Param [{}] cannot be applied to [{}]; check the interceptor configuration", + paramName, target.getClass().getName(), e); + } + } + return target; + } + + /** + * {@link org.apache.struts2.util.OgnlTextParser} yields an empty string for an expression that + * does not resolve and gives no other signal, so the raw template is needed to tell that apart + * from a legitimately empty value. + */ + private boolean isUnresolved(String rawValue, Object paramValue) { + return rawValue != null + && rawValue.contains("${") + && (paramValue == null || paramValue.toString().isEmpty()); + } } } diff --git a/core/src/test/java/org/apache/struts2/interceptor/LazyParamInjectorTest.java b/core/src/test/java/org/apache/struts2/interceptor/LazyParamInjectorTest.java new file mode 100644 index 000000000..8939c1313 --- /dev/null +++ b/core/src/test/java/org/apache/struts2/interceptor/LazyParamInjectorTest.java @@ -0,0 +1,134 @@ +/* + * 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.interceptor; + +import org.apache.struts2.ActionContext; +import org.apache.struts2.StrutsInternalTestCase; +import org.apache.struts2.util.ValueStack; +import org.apache.struts2.util.ValueStackFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LazyParamInjectorTest extends StrutsInternalTestCase { + + public static class Holder extends DisableParams { + private String name; + private Long size; + private final List<String> unresolvedCalls = new ArrayList<>(); + + public void setName(String name) { this.name = name; } + public void setSize(Long size) { this.size = size; } + public String getName() { return name; } + public Long getSize() { return size; } + public List<String> getUnresolvedCalls() { return unresolvedCalls; } + + @Override + public void unresolved(String paramName) { unresolvedCalls.add(paramName); } + } + + public static class Bean { + public String getLabel() { return "resolved-label"; } + public Long getLimit() { return 4096L; } + } + + private ActionContext context; + private WithLazyParams.LazyParamInjector injector; + + @Override + protected void setUp() throws Exception { + super.setUp(); + ValueStack stack = container.getInstance(ValueStackFactory.class).createValueStack(); + stack.push(new Bean()); + context = ActionContext.of(stack.getContext()).withContainer(container).withValueStack(stack).bind(); + injector = new WithLazyParams.LazyParamInjector(stack); + container.inject(injector); + } + + @Override + protected void tearDown() throws Exception { + ActionContext.clear(); + super.tearDown(); + } + + public void testResolvesExpressionsIntoTheHolder() { + Map<String, String> params = new HashMap<>(); + params.put("name", "${label}"); + + Holder holder = injector.resolveInto(new Holder(), params, context); + + assertThat(holder.getName()).isEqualTo("resolved-label"); + assertThat(holder.getUnresolvedCalls()).isEmpty(); + } + + public void testAppliesOgnlTypeConversion() { + Map<String, String> params = new HashMap<>(); + params.put("size", "${limit}"); + + Holder holder = injector.resolveInto(new Holder(), params, context); + + assertThat(holder.getSize()).isEqualTo(4096L); + } + + public void testPassesLiteralValuesThrough() { + Map<String, String> params = new HashMap<>(); + params.put("name", "plain-text"); + + Holder holder = injector.resolveInto(new Holder(), params, context); + + assertThat(holder.getName()).isEqualTo("plain-text"); + assertThat(holder.getUnresolvedCalls()).isEmpty(); + } + + public void testUnresolvableExpressionSkipsWriteAndNotifiesHolder() { + Holder seeded = new Holder(); + seeded.setName("seeded-value"); + + Map<String, String> params = new HashMap<>(); + params.put("name", "${noSuchProperty}"); + + Holder holder = injector.resolveInto(seeded, params, context); + + assertThat(holder.getName()).isEqualTo("seeded-value"); + assertThat(holder.getUnresolvedCalls()).containsExactly("name"); + } + + public void testResolvesDisabledOntoDisableParams() { + Map<String, String> params = new HashMap<>(); + params.put("disabled", "true"); + + Holder holder = injector.resolveInto(new Holder(), params, context); + + assertThat(holder.isDisabled()).isTrue(); + } + + public void testUnknownParamIsIgnoredWithoutFailingTheInvocation() { + Map<String, String> params = new HashMap<>(); + params.put("noSuchParam", "whatever"); + + Holder holder = injector.resolveInto(new Holder(), params, context); + + assertThat(holder.getName()).isNull(); + assertThat(holder.getSize()).isNull(); + } +}
