Browse Source

Add 'WAR_SOURCE_DIRECTORY' environment variable support

Add an escape hatch for users that deviate from the standard
`src/main/webbapp` directory structure.

Fixes gh-23829
pull/48680/head
Phillip Webb 4 weeks ago
parent
commit
21cf377f7e
  1. 2
      documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc
  2. 24
      module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/DocumentRoot.java
  3. 26
      module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/servlet/DocumentRootTests.java
  4. 22
      module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProvider.java
  5. 38
      module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProviderTests.java

2
documentation/spring-boot-docs/src/docs/antora/modules/reference/pages/web/servlet.adoc

@ -745,3 +745,5 @@ JSPs are not supported when using an executable jar. @@ -745,3 +745,5 @@ JSPs are not supported when using an executable jar.
* Creating a custom `error.jsp` page does not override the default view for xref:web/servlet.adoc#web.servlet.spring-mvc.error-handling[error handling].
xref:web/servlet.adoc#web.servlet.spring-mvc.error-handling.error-pages[Custom error pages] should be used instead.
* If you run your application using `mvn spring-boot:run` or `gradle bootRun` and you deviate from the standard `src/main/webbapp` directory structure you may need to set a `WAR_SOURCE_DIRECTORY` environment variable so that Spring Boot can find your JSPs.

24
module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/DocumentRoot.java

@ -23,6 +23,7 @@ import java.net.URLConnection; @@ -23,6 +23,7 @@ import java.net.URLConnection;
import java.security.CodeSource;
import java.util.Arrays;
import java.util.Locale;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.jspecify.annotations.Nullable;
@ -35,14 +36,29 @@ import org.jspecify.annotations.Nullable; @@ -35,14 +36,29 @@ import org.jspecify.annotations.Nullable;
*/
public class DocumentRoot {
private static final String[] COMMON_DOC_ROOTS = { "src/main/webapp", "public", "static" };
private static final String WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE = "WAR_SOURCE_DIRECTORY";
private final Log logger;
private final File rootDirectory;
private final String[] commonDocRoots;
private @Nullable File directory;
public DocumentRoot(Log logger) {
this(logger, new File("."), System::getenv);
}
DocumentRoot(Log logger, File rootDirectory, Function<String, @Nullable String> systemEnvironment) {
this.logger = logger;
this.rootDirectory = rootDirectory;
this.commonDocRoots = new String[] { getWarSourceDirectory(systemEnvironment), "public", "static" };
}
private static String getWarSourceDirectory(Function<String, @Nullable String> systemEnvironment) {
String name = systemEnvironment.apply(WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE);
return (name != null) ? name : "src/main/webapp";
}
@Nullable File getDirectory() {
@ -137,8 +153,8 @@ public class DocumentRoot { @@ -137,8 +153,8 @@ public class DocumentRoot {
}
private @Nullable File getCommonDocumentRoot() {
for (String commonDocRoot : COMMON_DOC_ROOTS) {
File root = new File(commonDocRoot);
for (String commonDocRoot : this.commonDocRoots) {
File root = new File(this.rootDirectory, commonDocRoot);
if (root.exists() && root.isDirectory()) {
return root.getAbsoluteFile();
}
@ -147,7 +163,7 @@ public class DocumentRoot { @@ -147,7 +163,7 @@ public class DocumentRoot {
}
private void logNoDocumentRoots() {
this.logger.debug("None of the document roots " + Arrays.asList(COMMON_DOC_ROOTS)
this.logger.debug("None of the document roots " + Arrays.asList(this.commonDocRoots)
+ " point to a directory and will be ignored.");
}

26
module/spring-boot-web-server/src/test/java/org/springframework/boot/web/server/servlet/DocumentRootTests.java

@ -20,7 +20,10 @@ import java.io.File; @@ -20,7 +20,10 @@ import java.io.File;
import java.net.URL;
import java.security.CodeSource;
import java.security.cert.Certificate;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@ -34,11 +37,13 @@ import static org.assertj.core.api.Assertions.assertThat; @@ -34,11 +37,13 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
class DocumentRootTests {
private static final Log logger = LogFactory.getLog(DocumentRootTests.class);
@TempDir
@SuppressWarnings("NullAway.Init")
File tempDir;
private final DocumentRoot documentRoot = new DocumentRoot(LogFactory.getLog(getClass()));
private final DocumentRoot documentRoot = new DocumentRoot(logger);
@Test
void explodedWarFileDocumentRootWhenRunningFromExplodedWar() throws Exception {
@ -70,4 +75,23 @@ class DocumentRootTests { @@ -70,4 +75,23 @@ class DocumentRootTests {
assertThat(codeSourceArchive).isEqualTo(new File("/test/path/with space/"));
}
@Test
void getValidDirectoryWhenHasSrcMainWebApp() {
Map<String, String> systemEnvironment = new HashMap<>();
File directory = new File(this.tempDir, "src/main/webapp");
directory.mkdirs();
DocumentRoot documentRoot = new DocumentRoot(logger, this.tempDir, systemEnvironment::get);
assertThat(documentRoot.getValidDirectory()).isEqualTo(directory);
}
@Test
void getValidDirectoryWhenHasCustomSrcMainWebApp() {
Map<String, String> systemEnvironment = new HashMap<>();
systemEnvironment.put("WAR_SOURCE_DIRECTORY", "src/main/unusual");
File directory = new File(this.tempDir, "src/main/unusual");
directory.mkdirs();
DocumentRoot documentRoot = new DocumentRoot(logger, this.tempDir, systemEnvironment::get);
assertThat(documentRoot.getValidDirectory()).isEqualTo(directory);
}
}

22
module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProvider.java

@ -17,6 +17,9 @@ @@ -17,6 +17,9 @@
package org.springframework.boot.webmvc.autoconfigure;
import java.io.File;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.core.env.Environment;
@ -34,6 +37,23 @@ import org.springframework.util.ClassUtils; @@ -34,6 +37,23 @@ import org.springframework.util.ClassUtils;
*/
public class JspTemplateAvailabilityProvider implements TemplateAvailabilityProvider {
private static final String WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE = "WAR_SOURCE_DIRECTORY";
private final File warSourceDirectory;
public JspTemplateAvailabilityProvider() {
this(new File("."), System::getenv);
}
JspTemplateAvailabilityProvider(File rootDirectory, Function<String, @Nullable String> systemEnvironment) {
this.warSourceDirectory = new File(rootDirectory, getWarSourceDirectory(systemEnvironment));
}
private static String getWarSourceDirectory(Function<String, @Nullable String> systemEnvironment) {
String name = systemEnvironment.apply(WAR_SOURCE_DIRECTORY_ENVIRONMENT_VARIABLE);
return (name != null) ? name : "src/main/webapp";
}
@Override
public boolean isTemplateAvailable(String view, Environment environment, ClassLoader classLoader,
ResourceLoader resourceLoader) {
@ -42,7 +62,7 @@ public class JspTemplateAvailabilityProvider implements TemplateAvailabilityProv @@ -42,7 +62,7 @@ public class JspTemplateAvailabilityProvider implements TemplateAvailabilityProv
if (resourceLoader.getResource(resourceName).exists()) {
return true;
}
return new File("src/main/webapp", resourceName).exists();
return new File(this.warSourceDirectory, resourceName).exists();
}
return false;
}

38
module/spring-boot-webmvc/src/test/java/org/springframework/boot/webmvc/autoconfigure/JspTemplateAvailabilityProviderTests.java

@ -16,12 +16,18 @@ @@ -16,12 +16,18 @@
package org.springframework.boot.webmvc.autoconfigure;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -57,9 +63,37 @@ class JspTemplateAvailabilityProviderTests { @@ -57,9 +63,37 @@ class JspTemplateAvailabilityProviderTests {
assertThat(isTemplateAvailable("suffixed")).isTrue();
}
@Test
void availabilityOfTemplateInSrcMainWebapp(@TempDir File rootDirectory) throws Exception {
File jsp = new File(rootDirectory, "src/main/webapp/test.jsp");
jsp.getParentFile().mkdirs();
FileCopyUtils.copy(new byte[0], jsp);
Map<String, String> systemEnvironment = new HashMap<>();
JspTemplateAvailabilityProvider provider = new JspTemplateAvailabilityProvider(rootDirectory,
systemEnvironment::get);
assertThat(isTemplateAvailable(provider, "test.jsp")).isTrue();
assertThat(isTemplateAvailable(provider, "missing.jsp")).isFalse();
}
@Test
void availabilityOfTemplateInCustomSrcMainWebapp(@TempDir File rootDirectory) throws Exception {
File jsp = new File(rootDirectory, "src/main/unusual/test.jsp");
jsp.getParentFile().mkdirs();
FileCopyUtils.copy(new byte[0], jsp);
Map<String, String> systemEnvironment = new HashMap<>();
systemEnvironment.put("WAR_SOURCE_DIRECTORY", "src/main/unusual");
JspTemplateAvailabilityProvider provider = new JspTemplateAvailabilityProvider(rootDirectory,
systemEnvironment::get);
assertThat(isTemplateAvailable(provider, "test.jsp")).isTrue();
assertThat(isTemplateAvailable(provider, "missing.jsp")).isFalse();
}
private boolean isTemplateAvailable(String view) {
return this.provider.isTemplateAvailable(view, this.environment, getClass().getClassLoader(),
this.resourceLoader);
return isTemplateAvailable(this.provider, view);
}
private boolean isTemplateAvailable(JspTemplateAvailabilityProvider provider, String view) {
return provider.isTemplateAvailable(view, this.environment, getClass().getClassLoader(), this.resourceLoader);
}
}

Loading…
Cancel
Save