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
+    }
+}

Reply via email to