From c58dfad3914c201665b9f2425a56d709c37adf2d Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Fri, 16 Oct 2026 11:27:54 +0200 Subject: [PATCH] WW-5782 fix(tags): document and warn that s:a href ignores nested params With an explicit href the anchor tag never applies nested tags; they are only used when the URL is built from value or action. The javadoc example showed exactly that combination. The example now uses value, the javadoc and the href attribute description say nested params are not applied to href, and in devMode the tag logs a warning naming the ignored params. Rendered output is unchanged; applying params to href is WW-5783 (8.0.0). Co-Authored-By: Claude Opus 5.5 --- .../org/apache/struts2/components/Anchor.java | 11 ++-- .../src/site/resources/tags/a-attributes.html | 2 +- .../struts2/views/jsp/AnchorTagTest.java | 50 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/org/apache/struts2/components/Anchor.java b/core/src/main/java/org/apache/struts2/components/Anchor.java index 6bda43f209..9e7e09fc7a 100644 --- a/core/src/main/java/org/apache/struts2/components/Anchor.java +++ b/core/src/main/java/org/apache/struts2/components/Anchor.java @@ -38,7 +38,8 @@ * *

* A tag that creates a HTML <a >.This tag supports the same attributes as the "url" tag, - * including nested parameters using the "param" tag. + * including nested parameters using the "param" tag. Nested parameters are applied only when the URL is built + * from the "value" or "action" attributes; they are ignored when "href" is set. *

* * @@ -46,7 +47,7 @@ * *
  * 
- * <s:a id="link1" href="/do-it.action">
+ * <s:a id="link1" value="/do-it.action">
  *     <img border="none" src="<%=request.getContextPath()%>/images/delete.gif"/>
  *     <s:param name="id" value="1"/>
  * </s:a>
@@ -101,6 +102,10 @@ protected void evaluateExtraParams() {
 
         if (href != null) {
             addParameter("href", ensureAttributeSafelyNotEscaped(findString(href)));
+            if (devMode && !urlParameters.isEmpty()) {
+                LOG.warn("Nested params {} are ignored because  has href [{}], use value or action to build a URL with params",
+                        urlParameters.keySet(), href);
+            }
         } else {
             //no href, build it from URL attributes
             StringWriter sw = new StringWriter();
@@ -189,7 +194,7 @@ public UrlProvider getUrlProvider() {
         return urlProvider;
     }
 
-    @StrutsTagAttribute(description = "The URL.")
+    @StrutsTagAttribute(description = "The URL. Nested param tags are not applied, use value or action to build a URL with params")
     public void setHref(String href) {
         this.href = href;
     }
diff --git a/core/src/site/resources/tags/a-attributes.html b/core/src/site/resources/tags/a-attributes.html
index 48ba23f962..738d0bc369 100644
--- a/core/src/site/resources/tags/a-attributes.html
+++ b/core/src/site/resources/tags/a-attributes.html
@@ -123,7 +123,7 @@
         
         false
         String
-        The URL.
+        The URL. Nested param tags are not applied, use value or action to build a URL with params
     
     
         id
diff --git a/core/src/test/java/org/apache/struts2/views/jsp/AnchorTagTest.java b/core/src/test/java/org/apache/struts2/views/jsp/AnchorTagTest.java
index 7bf07bd686..1b9047a2b8 100644
--- a/core/src/test/java/org/apache/struts2/views/jsp/AnchorTagTest.java
+++ b/core/src/test/java/org/apache/struts2/views/jsp/AnchorTagTest.java
@@ -18,6 +18,8 @@
  */
 package org.apache.struts2.views.jsp;
 
+import org.apache.struts2.components.Anchor;
+import org.apache.struts2.test.LogCapture;
 import org.apache.struts2.views.jsp.ui.AnchorTag;
 import org.apache.struts2.views.jsp.ui.StrutsBodyContent;
 
@@ -1646,4 +1648,52 @@ protected void setUp() throws Exception {
         pageContext.setJspWriter(jspWriter);
     }
 
+    public void testHrefIgnoresNestedParams() throws Exception {
+        tag.setHref("/do-it.action");
+
+        try (LogCapture logs = new LogCapture(Anchor.class)) {
+            renderWithParam("id", "%{'1'}");
+
+            assertEquals(wrapWithAnchor("/do-it.action"), writer.toString());
+            assertTrue(logs.messages().toString(), logs.messages().isEmpty());
+        }
+    }
+
+    public void testHrefWithNestedParamsWarnsInDevMode() throws Exception {
+        setDevMode(true);
+        tag.setHref("/do-it.action");
+
+        try (LogCapture logs = new LogCapture(Anchor.class)) {
+            renderWithParam("id", "%{'1'}");
+
+            assertEquals(wrapWithAnchor("/do-it.action"), writer.toString());
+            assertEquals(1, logs.messages().size());
+            assertTrue(logs.messages().get(0), logs.messages().get(0).contains("[id]"));
+        }
+    }
+
+    public void testValueWithNestedParamsDoesNotWarnInDevMode() throws Exception {
+        setDevMode(true);
+        tag.setValue("/do-it.action");
+
+        try (LogCapture logs = new LogCapture(Anchor.class)) {
+            renderWithParam("id", "%{'1'}");
+
+            assertEquals(wrapWithAnchor("/do-it.action?id=1"), writer.toString());
+            assertTrue(logs.messages().toString(), logs.messages().isEmpty());
+        }
+    }
+
+    private void renderWithParam(String name, String value) throws JspException {
+        ParamTag param = new ParamTag();
+        param.setPageContext(pageContext);
+        param.setName(name);
+        param.setValue(value);
+
+        tag.doStartTag();
+        param.doStartTag();
+        param.doEndTag();
+        tag.doEndTag();
+    }
+
 }