Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,17 @@
<artifactId>caffeine</artifactId>
</dependency>

<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-lite</artifactId>
</dependency>

<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/java/org/apache/struts2/StrutsConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,16 @@ public final class StrutsConstants {
*/
public static final String STRUTS_UI_STATIC_CONTENT_PATH = "struts.ui.staticContentPath";

/**
* Whether WebJars support is enabled (serving and URL building)
*/
public static final String STRUTS_WEBJARS_ENABLED = "struts.webjars.enabled";

/**
* Optional comma-separated allowlist of WebJar names permitted to be served (empty = all)
*/
public static final String STRUTS_WEBJARS_ALLOWLIST = "struts.webjars.allowlist";

/**
* A global flag to enable/disable html body escaping in tags, can be overwritten per tag
*/
Expand Down Expand Up @@ -434,6 +444,11 @@ public final class StrutsConstants {
*/
public static final String STRUTS_STATIC_CONTENT_LOADER = "struts.staticContentLoader";

/**
* The {@link org.apache.struts2.webjars.WebJarUrlProvider} implementation class
*/
public static final String STRUTS_WEBJARS_URL_PROVIDER = "struts.webjars.urlProvider";

/**
* The {@link org.apache.struts2.UnknownHandlerManager} implementation class
*/
Expand Down
95 changes: 95 additions & 0 deletions core/src/main/java/org/apache/struts2/components/WebJar.java
Original file line number Diff line number Diff line change
@@ -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.error("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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
import org.apache.struts2.validator.ActionValidatorManager;
import org.apache.struts2.views.freemarker.FreemarkerManager;
import org.apache.struts2.views.util.UrlHelper;
import org.apache.struts2.webjars.WebJarUrlProvider;

/**
* Selects the implementations of key framework extension points, using the loaded
Expand Down Expand Up @@ -430,6 +431,7 @@ public void register(ContainerBuilder builder, LocatableProperties props) {
alias(PatternMatcher.class, StrutsConstants.STRUTS_PATTERNMATCHER, builder, props);
alias(ContentTypeMatcher.class, StrutsConstants.STRUTS_CONTENT_TYPE_MATCHER, builder, props);
alias(StaticContentLoader.class, StrutsConstants.STRUTS_STATIC_CONTENT_LOADER, builder, props);
alias(WebJarUrlProvider.class, StrutsConstants.STRUTS_WEBJARS_URL_PROVIDER, builder, props);
alias(UnknownHandlerManager.class, StrutsConstants.STRUTS_UNKNOWN_HANDLER_MANAGER, builder, props);
alias(UrlHelper.class, StrutsConstants.STRUTS_URL_HELPER, builder, props);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
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;
Expand All @@ -39,6 +40,8 @@
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.StringTokenizer;

/**
Expand Down Expand Up @@ -70,6 +73,8 @@
*/
public class DefaultStaticContentLoader implements StaticContentLoader {

protected static final String WEBJARS_REQUEST_PREFIX = "/webjars/";

/**
* Provide a logging instance.
*/
Expand Down Expand Up @@ -107,6 +112,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.
*
Expand Down Expand Up @@ -209,6 +221,14 @@ protected List<String> parse(String packages) {
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) {
Expand All @@ -231,6 +251,32 @@ public void findStaticResource(String path, HttpServletRequest request, HttpServ
}
}

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) {
Expand Down Expand Up @@ -321,32 +367,43 @@ protected String buildPath(String name, String packagePrefix) throws Unsupported
}


/**
* Maps a lower-case file extension to its content type. Not using the code provided by
* activation.jar to avoid adding yet another dependency; this covers the files we serve up
* (Struts' own static assets plus WebJar assets).
*/
private static final Map<String, String> CONTENT_TYPES = Map.ofEntries(
Map.entry("js", "text/javascript"),
Map.entry("mjs", "text/javascript"),
Map.entry("css", "text/css"),
Map.entry("html", "text/html"),
Map.entry("txt", "text/plain"),
Map.entry("gif", "image/gif"),
Map.entry("jpg", "image/jpeg"),
Map.entry("jpeg", "image/jpeg"),
Map.entry("png", "image/png"),
Map.entry("svg", "image/svg+xml"),
Map.entry("ico", "image/x-icon"),
Map.entry("woff2", "font/woff2"),
Map.entry("woff", "font/woff"),
Map.entry("ttf", "font/ttf"),
Map.entry("otf", "font/otf"),
Map.entry("eot", "application/vnd.ms-fontobject"),
Map.entry("json", "application/json"),
Map.entry("map", "application/json"));

/**
* Determine the content type for the resource name.
*
* @param name The resource name
* @return The mime type
* @return The mime type, or {@code null} if the extension is unknown
*/
protected String getContentType(String name) {
// NOT using the code provided activation.jar to avoid adding yet another dependency
// this is generally OK, since these are the main files we server up
if (name.endsWith(".js")) {
return "text/javascript";
} else if (name.endsWith(".css")) {
return "text/css";
} else if (name.endsWith(".html")) {
return "text/html";
} else if (name.endsWith(".txt")) {
return "text/plain";
} else if (name.endsWith(".gif")) {
return "image/gif";
} else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
return "image/jpeg";
} else if (name.endsWith(".png")) {
return "image/png";
} else {
int dot = name.lastIndexOf('.');
if (dot < 0) {
return null;
}
return CONTENT_TYPES.get(name.substring(dot + 1));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -339,6 +340,13 @@ public SetModel getSet() {
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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading