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();
+    }
+}

Reply via email to