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> + * <link rel="stylesheet" href="<s:webjar path="bootstrap/css/bootstrap.min.css"/>"> + * <@s.webjar path="jquery/jquery.min.js"/> + * </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(); + } +}
