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 e48cca805dd061643bcddba88ddeb8b5f3d6c0eb Author: Lukasz Lenart <[email protected]> AuthorDate: Wed Jul 1 11:34:12 2026 +0200 WW-5640 feat: serve webjar assets via static content loader Co-Authored-By: Claude Opus 4.8 <[email protected]> --- .../dispatcher/DefaultStaticContentLoader.java | 45 ++++++++++++++++ .../DefaultStaticContentLoaderWebJarTest.java | 62 ++++++++++++++++++++++ .../dispatcher/WebJarTestServletOutputStream.java | 50 +++++++++++++++++ 3 files changed, 157 insertions(+) diff --git a/core/src/main/java/org/apache/struts2/dispatcher/DefaultStaticContentLoader.java b/core/src/main/java/org/apache/struts2/dispatcher/DefaultStaticContentLoader.java index 641872145..ebc534d10 100644 --- a/core/src/main/java/org/apache/struts2/dispatcher/DefaultStaticContentLoader.java +++ b/core/src/main/java/org/apache/struts2/dispatcher/DefaultStaticContentLoader.java @@ -27,6 +27,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.struts2.StrutsConstants; +import org.apache.struts2.webjars.WebJarUrlProvider; import java.io.IOException; import java.io.InputStream; @@ -39,6 +40,7 @@ import java.util.Calendar; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Optional; import java.util.StringTokenizer; /** @@ -70,6 +72,8 @@ import java.util.StringTokenizer; */ public class DefaultStaticContentLoader implements StaticContentLoader { + protected static final String WEBJARS_REQUEST_PREFIX = "/webjars/"; + /** * Provide a logging instance. */ @@ -107,6 +111,13 @@ public class DefaultStaticContentLoader implements StaticContentLoader { protected boolean devMode; + protected WebJarUrlProvider webJarUrlProvider; + + @Inject + public void setWebJarUrlProvider(WebJarUrlProvider webJarUrlProvider) { + this.webJarUrlProvider = webJarUrlProvider; + } + /** * Modify state of StrutsConstants.STRUTS_SERVE_STATIC_CONTENT setting. * @@ -209,6 +220,14 @@ public class DefaultStaticContentLoader implements StaticContentLoader { public void findStaticResource(String path, HttpServletRequest request, HttpServletResponse response) throws IOException { String name = cleanupPath(path); + + if (name.startsWith(WEBJARS_REQUEST_PREFIX)) { + if (!findWebJarResource(name, path, request, response)) { + sendNotFound(response); + } + return; + } + for (String pathPrefix : pathPrefixes) { URL resourceUrl = findResource(buildPath(name, pathPrefix)); if (resourceUrl != null) { @@ -231,6 +250,32 @@ public class DefaultStaticContentLoader implements StaticContentLoader { } } + sendNotFound(response); + } + + /** + * Resolve and serve a WebJar asset requested under {@code <staticContentPath>/webjars/**}. + * + * @param name the request path with the static-content prefix stripped, e.g. {@code /webjars/jquery/jquery.min.js} + * @param path the original request path (used for content-type detection) + * @return true if the asset was resolved and streamed; false otherwise (caller sends 404) + */ + protected boolean findWebJarResource(String name, String path, HttpServletRequest request, HttpServletResponse response) + throws IOException { + String logicalPath = name.substring(WEBJARS_REQUEST_PREFIX.length()); + Optional<String> resource = webJarUrlProvider.resolveResourcePath(logicalPath); + if (resource.isEmpty()) { + return false; + } + URL resourceUrl = findResource(resource.get()); + if (resourceUrl == null) { + return false; + } + process(resourceUrl.openStream(), path, request, response); + return true; + } + + protected void sendNotFound(HttpServletResponse response) { try { response.sendError(HttpServletResponse.SC_NOT_FOUND); } catch (IOException e1) { diff --git a/core/src/test/java/org/apache/struts2/dispatcher/DefaultStaticContentLoaderWebJarTest.java b/core/src/test/java/org/apache/struts2/dispatcher/DefaultStaticContentLoaderWebJarTest.java index 49488464c..447a485a6 100644 --- a/core/src/test/java/org/apache/struts2/dispatcher/DefaultStaticContentLoaderWebJarTest.java +++ b/core/src/test/java/org/apache/struts2/dispatcher/DefaultStaticContentLoaderWebJarTest.java @@ -18,9 +18,17 @@ */ package org.apache.struts2.dispatcher; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.junit.Test; +import java.io.ByteArrayOutputStream; + import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class DefaultStaticContentLoaderWebJarTest { @@ -54,4 +62,58 @@ public class DefaultStaticContentLoaderWebJarTest { assertThat(loader.type("x.png")).isEqualTo("image/png"); assertThat(loader.type("x.unknown")).isNull(); } + + private DefaultStaticContentLoader newLoader(boolean enabled) { + DefaultStaticContentLoader loader = new DefaultStaticContentLoader(); + loader.setServeStaticContent("true"); + loader.setStaticContentPath("/static"); + loader.setServeStaticBrowserCache("true"); + loader.setEncoding("UTF-8"); + org.apache.struts2.webjars.DefaultWebJarUrlProvider provider = + new org.apache.struts2.webjars.DefaultWebJarUrlProvider(); + provider.setEnabled(String.valueOf(enabled)); + provider.setAllowlist(""); + provider.setStaticContentPath("/static"); + loader.setWebJarUrlProvider(provider); + return loader; + } + + @Test + public void servesKnownWebJarAssetWithContentType() throws Exception { + DefaultStaticContentLoader loader = newLoader(true); + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + ByteArrayOutputStream captured = new ByteArrayOutputStream(); + when(response.getOutputStream()) + .thenReturn(new WebJarTestServletOutputStream(captured)); + + loader.findStaticResource("/static/webjars/jquery/jquery.min.js", request, response); + + verify(response).setContentType("text/javascript"); + verify(response, never()) + .sendError(HttpServletResponse.SC_NOT_FOUND); + assertThat(captured.size()).isGreaterThan(0); + } + + @Test + public void unknownWebJarAssetReturns404() throws Exception { + DefaultStaticContentLoader loader = newLoader(true); + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + loader.findStaticResource("/static/webjars/nope/nope.js", request, response); + + verify(response).sendError(HttpServletResponse.SC_NOT_FOUND); + } + + @Test + public void disabledWebJarsReturns404() throws Exception { + DefaultStaticContentLoader loader = newLoader(false); + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + loader.findStaticResource("/static/webjars/jquery/jquery.min.js", request, response); + + verify(response).sendError(HttpServletResponse.SC_NOT_FOUND); + } } diff --git a/core/src/test/java/org/apache/struts2/dispatcher/WebJarTestServletOutputStream.java b/core/src/test/java/org/apache/struts2/dispatcher/WebJarTestServletOutputStream.java new file mode 100644 index 000000000..2ba8c8e6c --- /dev/null +++ b/core/src/test/java/org/apache/struts2/dispatcher/WebJarTestServletOutputStream.java @@ -0,0 +1,50 @@ +/* + * 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.dispatcher; + +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; + +import java.io.IOException; +import java.io.OutputStream; + +/** Minimal ServletOutputStream backed by an OutputStream, for tests. */ +public class WebJarTestServletOutputStream extends ServletOutputStream { + + private final OutputStream delegate; + + public WebJarTestServletOutputStream(OutputStream delegate) { + this.delegate = delegate; + } + + @Override + public void write(int b) throws IOException { + delegate.write(b); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) { + // no-op + } +}
