From 8853c4db8c69bbc5040edbf79c75cf95697a50b3 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 12 Sep 2026 11:01:10 +0200 Subject: [PATCH 1/2] WW-5729 fix(jasperreports): override HashMap.get/containsKey in ValueStackShadowMap The two methods took a String parameter, so they overloaded rather than overrode HashMap.get(Object) and containsKey(Object). JasperReports holds the parameter map as java.util.Map and calls the Object variants, which meant the value-stack fallback the class exists for never ran when a report was filled: only the entries the result put explicitly reached JasperReports, and a declared parameter exposed as an action property rendered as null. The Object overrides were the original code; 2908d746c (2019, "Minor code optimization") narrowed them to String when it added the generics, and the regression first shipped in 6.0.0. The jasperreports7 copy already declares both as @Override with an Object parameter. Both copies now let an explicit map entry win even when its value is null, so containsKey and get agree and JasperReports skips the default value expression consistently. The String overloads stay in the 6.x plugin as deprecated delegates for binary compatibility. Co-Authored-By: Claude Opus 5 (1M context) --- .../jasperreports/ValueStackShadowMap.java | 33 ++++++++++++++----- .../JasperReportsResultTest.java | 33 +++++++++++++++++++ .../jasperreports7/ValueStackShadowMap.java | 11 +++---- .../JasperReport7ResultTest.java | 23 +++++++++++++ 4 files changed, 84 insertions(+), 16 deletions(-) diff --git a/plugins/jasperreports/src/main/java/org/apache/struts2/views/jasperreports/ValueStackShadowMap.java b/plugins/jasperreports/src/main/java/org/apache/struts2/views/jasperreports/ValueStackShadowMap.java index 7e78edf755..babc7ddafc 100644 --- a/plugins/jasperreports/src/main/java/org/apache/struts2/views/jasperreports/ValueStackShadowMap.java +++ b/plugins/jasperreports/src/main/java/org/apache/struts2/views/jasperreports/ValueStackShadowMap.java @@ -53,10 +53,11 @@ public ValueStackShadowMap(ValueStack valueStack) { * @return true, if contains key, false otherwise. * @see java.util.HashMap#containsKey */ - public boolean containsKey(String key) { + @Override + public boolean containsKey(Object key) { boolean hasKey = super.containsKey(key); - if (!hasKey && valueStack.findValue(key) != null) { + if (!hasKey && key != null && valueStack.findValue(key.toString()) != null) { hasKey = true; } @@ -67,16 +68,30 @@ public boolean containsKey(String key) { * Implementation of get(), overriding HashMap implementation. * * @param key - The key to get in HashMap and if not found there from the valueStack. - * @return value - The object from HashMap or if null, from the valueStack. + * @return value - The object from HashMap or, if the key is absent, from the valueStack. * @see java.util.HashMap#get */ - public Object get(String key) { - Object value = super.get(key); - - if ((value == null)) { - value = valueStack.findValue((String) key); + @Override + public Object get(Object key) { + if (key == null || super.containsKey(key)) { + return super.get(key); } + return valueStack.findValue(key.toString()); + } + + /** + * @deprecated since 7.4.0, use {@link #containsKey(Object)} + */ + @Deprecated(since = "7.4.0", forRemoval = true) + public boolean containsKey(String key) { + return containsKey((Object) key); + } - return value; + /** + * @deprecated since 7.4.0, use {@link #get(Object)} + */ + @Deprecated(since = "7.4.0", forRemoval = true) + public Object get(String key) { + return get((Object) key); } } diff --git a/plugins/jasperreports/src/test/java/org/apache/struts2/views/jasperreports/JasperReportsResultTest.java b/plugins/jasperreports/src/test/java/org/apache/struts2/views/jasperreports/JasperReportsResultTest.java index beb563c122..82eb41d1fa 100644 --- a/plugins/jasperreports/src/test/java/org/apache/struts2/views/jasperreports/JasperReportsResultTest.java +++ b/plugins/jasperreports/src/test/java/org/apache/struts2/views/jasperreports/JasperReportsResultTest.java @@ -171,6 +171,39 @@ public void testReportParametersExpressionAccepted() throws Exception { assertTrue(response.getContentAsString().contains("Qux Report")); } + public void testDeclaredParameterResolvedFromValueStack() throws Exception { + result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}"); + stack.push(new Object() { + public String getTitle() { + return "Shadow"; + } + }); + + result.execute(this.invocation); + + assertTrue(response.getContentAsString().contains("Shadow Report")); + } + + public void testExplicitNullParameterIsNotShadowedByValueStack() throws Exception { + result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}"); + stack.push(new Object() { + public String getTitle() { + return "Shadow"; + } + + public Map getReportParameters() { + Map params = new HashMap<>(); + params.put("title", null); + return params; + } + }); + result.setReportParameters("reportParameters"); + + result.execute(this.invocation); + + assertTrue(response.getContentAsString().contains("null Report")); + } + public void testFillFromReportParametersWithoutDataSourceOrConnection() throws Exception { stack.push(new Object() { public Map getReportParameters() { diff --git a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/ValueStackShadowMap.java b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/ValueStackShadowMap.java index 12aa3c871b..1022a64078 100644 --- a/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/ValueStackShadowMap.java +++ b/plugins/jasperreports7/src/main/java/org/apache/struts2/views/jasperreports7/ValueStackShadowMap.java @@ -66,17 +66,14 @@ public boolean containsKey(Object key) { * Implementation of get(), overriding HashMap implementation. * * @param key - The key to get in HashMap and if not found there from the valueStack. - * @return value - The object from HashMap or if null, from the valueStack. + * @return value - The object from HashMap or, if the key is absent, from the valueStack. * @see java.util.HashMap#get */ @Override public Object get(Object key) { - Object value = super.get(key); - - if (key != null && value == null) { - value = valueStack.findValue(key.toString()); + if (key == null || super.containsKey(key)) { + return super.get(key); } - - return value; + return valueStack.findValue(key.toString()); } } diff --git a/plugins/jasperreports7/src/test/java/org/apache/struts2/views/jasperreports7/JasperReport7ResultTest.java b/plugins/jasperreports7/src/test/java/org/apache/struts2/views/jasperreports7/JasperReport7ResultTest.java index d1f82d5dc0..c334dd6e8f 100644 --- a/plugins/jasperreports7/src/test/java/org/apache/struts2/views/jasperreports7/JasperReport7ResultTest.java +++ b/plugins/jasperreports7/src/test/java/org/apache/struts2/views/jasperreports7/JasperReport7ResultTest.java @@ -197,6 +197,29 @@ public Map getReportParameters() { assertThat(response.getContentAsString()).contains("Baz Report"); } + public void testExplicitNullParameterIsNotShadowedByValueStack() throws Exception { + // given + result.setDataSource("{#{'firstName':'ignore', 'lastName':'ignore'}}"); + stack.push(new Object() { + public String getTitle() { + return "Shadow"; + } + + public Map getReportParameters() { + Map params = new HashMap<>(); + params.put("title", null); + return params; + } + }); + result.setReportParameters("reportParameters"); + + // when + result.execute(this.invocation); + + // then + assertThat(response.getContentAsString()).contains("null Report"); + } + public void testFillFromReportParametersWithoutDataSourceOrConnection() throws Exception { // given stack.push(new Object() { From 79688ec32406611ca37edd0f22ca54ea0ff2df28 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sat, 12 Sep 2026 11:20:12 +0200 Subject: [PATCH 2/2] WW-5729 test(jasperreports): cover ValueStackShadowMap directly The deprecated String delegates are reachable only by callers holding a ValueStackShadowMap reference, so the result-level tests never hit them. Co-Authored-By: Claude Opus 5 (1M context) --- .../ValueStackShadowMapTest.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 plugins/jasperreports/src/test/java/org/apache/struts2/views/jasperreports/ValueStackShadowMapTest.java diff --git a/plugins/jasperreports/src/test/java/org/apache/struts2/views/jasperreports/ValueStackShadowMapTest.java b/plugins/jasperreports/src/test/java/org/apache/struts2/views/jasperreports/ValueStackShadowMapTest.java new file mode 100644 index 0000000000..b6c905ec71 --- /dev/null +++ b/plugins/jasperreports/src/test/java/org/apache/struts2/views/jasperreports/ValueStackShadowMapTest.java @@ -0,0 +1,71 @@ +/* + * 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.views.jasperreports; + +import org.apache.struts2.ActionContext; +import org.apache.struts2.junit.StrutsTestCase; +import org.apache.struts2.util.ValueStack; + +import java.util.Map; + +public class ValueStackShadowMapTest extends StrutsTestCase { + + private ValueStackShadowMap map; + + public void testFallsBackToValueStackThroughMapInterface() { + Map asMap = map; + + assertTrue(asMap.containsKey("title")); + assertEquals("Shadow", asMap.get("title")); + assertFalse(asMap.containsKey("missing")); + assertNull(asMap.get("missing")); + } + + public void testExplicitEntryWinsOverValueStack() { + map.put("title", "Explicit"); + map.put("other", null); + + assertEquals("Explicit", map.get((Object) "title")); + assertTrue(map.containsKey((Object) "other")); + assertNull(map.get((Object) "other")); + } + + public void testNullKeyDoesNotReachValueStack() { + assertFalse(map.containsKey((Object) null)); + assertNull(map.get((Object) null)); + } + + @SuppressWarnings("removal") + public void testStringOverloadsDelegateToOverrides() { + assertTrue(map.containsKey("title")); + assertEquals("Shadow", map.get("title")); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + ValueStack stack = ActionContext.getContext().getValueStack(); + stack.push(new Object() { + public String getTitle() { + return "Shadow"; + } + }); + map = new ValueStackShadowMap(stack); + } +}