This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch WW-5640-webjars-support
in repository https://gitbox.apache.org/repos/asf/struts.git

commit fc796e46aa27eb96db1a04cced20ac93a53afd65
Author: Lukasz Lenart <[email protected]>
AuthorDate: Wed Jul 1 11:40:39 2026 +0200

    WW-5640 feat: add <s:webjar> tag and <@s.webjar> macro
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../java/org/apache/struts2/components/WebJar.java | 95 ++++++++++++++++++++++
 .../views/freemarker/tags/StrutsModels.java        |  8 ++
 .../struts2/views/freemarker/tags/WebJarModel.java | 37 +++++++++
 .../org/apache/struts2/views/jsp/WebJarTag.java    | 48 +++++++++++
 .../org/apache/struts2/components/WebJarTest.java  | 81 ++++++++++++++++++
 5 files changed, 269 insertions(+)

diff --git a/core/src/main/java/org/apache/struts2/components/WebJar.java 
b/core/src/main/java/org/apache/struts2/components/WebJar.java
new file mode 100644
index 000000000..2041fe390
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/components/WebJar.java
@@ -0,0 +1,95 @@
+/*
+ * 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.components;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts2.inject.Inject;
+import org.apache.struts2.util.ValueStack;
+import org.apache.struts2.views.annotations.StrutsTag;
+import org.apache.struts2.views.annotations.StrutsTagAttribute;
+import org.apache.struts2.webjars.WebJarUrlProvider;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Optional;
+
+/**
+ * <p>Resolves a version-less WebJar resource path to a servable URL and 
writes it to the output
+ * (or stores it in a variable when {@code var} is set). Compose it with 
{@code <s:script>}/{@code <s:link>}
+ * or a raw {@code <link>}/{@code <script>} element.</p>
+ *
+ * <b>Examples</b>
+ * <pre>
+ *   &lt;link rel="stylesheet" href="&lt;s:webjar 
path="bootstrap/css/bootstrap.min.css"/&gt;"&gt;
+ *   &lt;@s.webjar path="jquery/jquery.min.js"/&gt;
+ * </pre>
+ */
+@StrutsTag(
+    name = "webjar",
+    tldTagClass = "org.apache.struts2.views.jsp.WebJarTag",
+    description = "Resolve a version-less WebJar resource path to a servable 
URL")
+public class WebJar extends ContextBean {
+
+    private static final Logger LOG = LogManager.getLogger(WebJar.class);
+
+    protected String path;
+
+    private final HttpServletRequest request;
+    private WebJarUrlProvider webJarUrlProvider;
+
+    public WebJar(ValueStack stack, HttpServletRequest request) {
+        super(stack);
+        this.request = request;
+    }
+
+    @Inject
+    public void setWebJarUrlProvider(WebJarUrlProvider webJarUrlProvider) {
+        this.webJarUrlProvider = webJarUrlProvider;
+    }
+
+    @Override
+    public boolean end(Writer writer, String body) {
+        String logicalPath = findString(path);
+        Optional<String> url = (logicalPath == null)
+            ? Optional.empty()
+            : webJarUrlProvider.resolveUrl(logicalPath, request);
+
+        if (url.isPresent()) {
+            if (StringUtils.isNotBlank(getVar())) {
+                putInContext(url.get());
+            } else {
+                try {
+                    writer.write(url.get());
+                } catch (IOException e) {
+                    LOG.info("Could not write WebJar URL for path '{}'", path, 
e);
+                }
+            }
+        }
+        return super.end(writer, body);
+    }
+
+    @StrutsTagAttribute(required = true,
+        description = "The version-less WebJar resource path, e.g. 
bootstrap/css/bootstrap.min.css")
+    public void setPath(String path) {
+        this.path = path;
+    }
+}
diff --git 
a/core/src/main/java/org/apache/struts2/views/freemarker/tags/StrutsModels.java 
b/core/src/main/java/org/apache/struts2/views/freemarker/tags/StrutsModels.java
index dea1ac8ee..cdfec8138 100644
--- 
a/core/src/main/java/org/apache/struts2/views/freemarker/tags/StrutsModels.java
+++ 
b/core/src/main/java/org/apache/struts2/views/freemarker/tags/StrutsModels.java
@@ -53,6 +53,7 @@ public class StrutsModels {
     protected RadioModel radio;
     protected SelectModel select;
     protected SetModel set;
+    protected WebJarModel webjar;
     protected SubmitModel submit;
     protected ResetModel reset;
     protected TextAreaModel textarea;
@@ -339,6 +340,13 @@ public class StrutsModels {
         return set;
     }
 
+    public WebJarModel getWebjar() {
+        if (webjar == null) {
+            webjar = new WebJarModel(stack, req, res);
+        }
+        return webjar;
+    }
+
     public PropertyModel getProperty() {
         if (property == null) {
             property = new PropertyModel(stack, req, res);
diff --git 
a/core/src/main/java/org/apache/struts2/views/freemarker/tags/WebJarModel.java 
b/core/src/main/java/org/apache/struts2/views/freemarker/tags/WebJarModel.java
new file mode 100644
index 000000000..70e8bb7d1
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/views/freemarker/tags/WebJarModel.java
@@ -0,0 +1,37 @@
+/*
+ * 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.freemarker.tags;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.struts2.components.Component;
+import org.apache.struts2.components.WebJar;
+import org.apache.struts2.util.ValueStack;
+
+public class WebJarModel extends TagModel {
+
+    public WebJarModel(ValueStack stack, HttpServletRequest req, 
HttpServletResponse res) {
+        super(stack, req, res);
+    }
+
+    @Override
+    protected Component getBean() {
+        return new WebJar(stack, req);
+    }
+}
diff --git a/core/src/main/java/org/apache/struts2/views/jsp/WebJarTag.java 
b/core/src/main/java/org/apache/struts2/views/jsp/WebJarTag.java
new file mode 100644
index 000000000..2747b4f87
--- /dev/null
+++ b/core/src/main/java/org/apache/struts2/views/jsp/WebJarTag.java
@@ -0,0 +1,48 @@
+/*
+ * 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.jsp;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.apache.struts2.components.Component;
+import org.apache.struts2.components.WebJar;
+import org.apache.struts2.util.ValueStack;
+
+public class WebJarTag extends ContextBeanTag {
+
+    private static final long serialVersionUID = 1L;
+
+    protected String path;
+
+    @Override
+    public Component getBean(ValueStack stack, HttpServletRequest req, 
HttpServletResponse res) {
+        return new WebJar(stack, req);
+    }
+
+    @Override
+    protected void populateParams() {
+        super.populateParams();
+        WebJar webJar = (WebJar) component;
+        webJar.setPath(path);
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+}
diff --git a/core/src/test/java/org/apache/struts2/components/WebJarTest.java 
b/core/src/test/java/org/apache/struts2/components/WebJarTest.java
new file mode 100644
index 000000000..c7fb69874
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/components/WebJarTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.components;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.apache.struts2.util.ValueStack;
+import org.apache.struts2.webjars.WebJarUrlProvider;
+import org.junit.Test;
+
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class WebJarTest {
+
+    private WebJar newComponent(ValueStack stack, HttpServletRequest request, 
WebJarUrlProvider provider) {
+        Map<String, Object> context = new HashMap<>();
+        when(stack.getContext()).thenReturn(context);
+        WebJar webJar = new WebJar(stack, request);
+        webJar.setWebJarUrlProvider(provider);
+        return webJar;
+    }
+
+    @Test
+    public void writesResolvedUrl() {
+        ValueStack stack = mock(ValueStack.class);
+        
when(stack.findString("jquery/jquery.min.js")).thenReturn("jquery/jquery.min.js");
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        WebJarUrlProvider provider = mock(WebJarUrlProvider.class);
+        when(provider.resolveUrl("jquery/jquery.min.js", request))
+            
.thenReturn(Optional.of("/myapp/static/webjars/jquery/3.7.1/jquery.min.js"));
+
+        WebJar webJar = newComponent(stack, request, provider);
+        webJar.setPath("jquery/jquery.min.js");
+        StringWriter writer = new StringWriter();
+
+        webJar.start(writer);
+        webJar.end(writer, "");
+
+        
assertThat(writer.toString()).isEqualTo("/myapp/static/webjars/jquery/3.7.1/jquery.min.js");
+    }
+
+    @Test
+    public void unresolvedPathWritesNothing() {
+        ValueStack stack = mock(ValueStack.class);
+        when(stack.findString("nope/x.js")).thenReturn("nope/x.js");
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        WebJarUrlProvider provider = mock(WebJarUrlProvider.class);
+        when(provider.resolveUrl("nope/x.js", 
request)).thenReturn(Optional.empty());
+
+        WebJar webJar = newComponent(stack, request, provider);
+        webJar.setPath("nope/x.js");
+        StringWriter writer = new StringWriter();
+
+        webJar.start(writer);
+        webJar.end(writer, "");
+
+        assertThat(writer.toString()).isEmpty();
+    }
+}

Reply via email to