diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java new file mode 100644 index 0000000000..af39471b4d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfiguration.java @@ -0,0 +1,107 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.boot.autoconfigure.h2; + +import java.sql.Connection; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.h2.server.web.JakartaWebServlet; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; +import org.springframework.boot.autoconfigure.h2.H2ConsoleProperties.Settings; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for H2's web console. + * + * @author Andy Wilkinson + * @author Marten Deinum + * @author Stephane Nicoll + * @since 1.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = Type.SERVLET) +@ConditionalOnClass(JakartaWebServlet.class) +@ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true") +@AutoConfigureAfter(DataSourceAutoConfiguration.class) +@EnableConfigurationProperties(H2ConsoleProperties.class) +public class H2ConsoleAutoConfiguration { + + private static final Log logger = LogFactory.getLog(H2ConsoleAutoConfiguration.class); + + @Bean + public ServletRegistrationBean h2Console(H2ConsoleProperties properties, + ObjectProvider dataSource) { + String path = properties.getPath(); + String urlMapping = path + (path.endsWith("/") ? "*" : "/*"); + ServletRegistrationBean registration = new ServletRegistrationBean<>(new JakartaWebServlet(), + urlMapping); + configureH2ConsoleSettings(registration, properties.getSettings()); + if (logger.isInfoEnabled()) { + logDataSources(dataSource, path); + } + return registration; + } + + private void logDataSources(ObjectProvider dataSource, String path) { + List urls = dataSource.orderedStream().map((available) -> { + try (Connection connection = available.getConnection()) { + return "'" + connection.getMetaData().getURL() + "'"; + } + catch (Exception ex) { + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toList()); + if (!urls.isEmpty()) { + StringBuilder sb = new StringBuilder("H2 console available at '").append(path).append("'. "); + String tmp = (urls.size() > 1) ? "Databases" : "Database"; + sb.append(tmp).append(" available at "); + sb.append(String.join(", ", urls)); + logger.info(sb.toString()); + } + } + + private void configureH2ConsoleSettings(ServletRegistrationBean registration, + Settings settings) { + if (settings.isTrace()) { + registration.addInitParameter("trace", ""); + } + if (settings.isWebAllowOthers()) { + registration.addInitParameter("webAllowOthers", ""); + } + if (settings.getWebAdminPassword() != null) { + registration.addInitParameter("webAdminPassword", settings.getWebAdminPassword()); + } + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleProperties.java new file mode 100644 index 0000000000..a1dc97a034 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/H2ConsoleProperties.java @@ -0,0 +1,111 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.boot.autoconfigure.h2; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.Assert; + +/** + * Configuration properties for H2's console. + * + * @author Andy Wilkinson + * @author Marten Deinum + * @author Stephane Nicoll + * @since 1.3.0 + */ +@ConfigurationProperties(prefix = "spring.h2.console") +public class H2ConsoleProperties { + + /** + * Path at which the console is available. + */ + private String path = "/h2-console"; + + /** + * Whether to enable the console. + */ + private boolean enabled = false; + + private final Settings settings = new Settings(); + + public String getPath() { + return this.path; + } + + public void setPath(String path) { + Assert.notNull(path, "Path must not be null"); + Assert.isTrue(path.length() > 1, "Path must have length greater than 1"); + Assert.isTrue(path.startsWith("/"), "Path must start with '/'"); + this.path = path; + } + + public boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Settings getSettings() { + return this.settings; + } + + public static class Settings { + + /** + * Whether to enable trace output. + */ + private boolean trace = false; + + /** + * Whether to enable remote access. + */ + private boolean webAllowOthers = false; + + /** + * Password to access preferences and tools of H2 Console. + */ + private String webAdminPassword; + + public boolean isTrace() { + return this.trace; + } + + public void setTrace(boolean trace) { + this.trace = trace; + } + + public boolean isWebAllowOthers() { + return this.webAllowOthers; + } + + public void setWebAllowOthers(boolean webAllowOthers) { + this.webAllowOthers = webAllowOthers; + } + + public String getWebAdminPassword() { + return this.webAdminPassword; + } + + public void setWebAdminPassword(String webAdminPassword) { + this.webAdminPassword = webAdminPassword; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/package-info.java new file mode 100644 index 0000000000..23dc367447 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/h2/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed 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 + * + * https://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. + */ + +/** + * Auto-configuration for H2's Console. + */ +package org.springframework.boot.autoconfigure.h2; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/PathRequest.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/PathRequest.java index c0e5312a90..c1dfaa4831 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/PathRequest.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/servlet/PathRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,17 @@ package org.springframework.boot.autoconfigure.security.servlet; +import java.util.function.Supplier; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.boot.autoconfigure.h2.H2ConsoleProperties; import org.springframework.boot.autoconfigure.security.StaticResourceLocation; +import org.springframework.boot.security.servlet.ApplicationContextRequestMatcher; +import org.springframework.boot.web.context.WebServerApplicationContext; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.context.WebApplicationContext; /** * Factory that can be used to create a {@link RequestMatcher} for commonly used paths. @@ -40,4 +49,43 @@ public final class PathRequest { return StaticResourceRequest.INSTANCE; } + /** + * Returns a matcher that includes the H2 console location. For example: + *
+	 * PathRequest.toH2Console()
+	 * 
+ * @return the configured {@link RequestMatcher} + */ + public static H2ConsoleRequestMatcher toH2Console() { + return new H2ConsoleRequestMatcher(); + } + + /** + * The request matcher used to match against h2 console path. + */ + public static final class H2ConsoleRequestMatcher extends ApplicationContextRequestMatcher { + + private volatile RequestMatcher delegate; + + private H2ConsoleRequestMatcher() { + super(H2ConsoleProperties.class); + } + + @Override + protected boolean ignoreApplicationContext(WebApplicationContext applicationContext) { + return WebServerApplicationContext.hasServerNamespace(applicationContext, "management"); + } + + @Override + protected void initialized(Supplier h2ConsoleProperties) { + this.delegate = new AntPathRequestMatcher(h2ConsoleProperties.get().getPath() + "/**"); + } + + @Override + protected boolean matches(HttpServletRequest request, Supplier context) { + return this.delegate.matches(request); + } + + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index be53c1e5cc..0b2b304715 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -70,6 +70,7 @@ org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\ org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\ org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\ org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\ +org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\ org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\ org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\ org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java new file mode 100644 index 0000000000..1d6c943781 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsoleAutoConfigurationTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.boot.autoconfigure.h2; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.web.servlet.ServletRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link H2ConsoleAutoConfiguration} + * + * @author Andy Wilkinson + * @author Marten Deinum + * @author Stephane Nicoll + * @author Shraddha Yeole + */ +class H2ConsoleAutoConfigurationTests { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(H2ConsoleAutoConfiguration.class)); + + @Test + void consoleIsDisabledByDefault() { + this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ServletRegistrationBean.class)); + } + + @Test + void propertyCanEnableConsole() { + this.contextRunner.withPropertyValues("spring.h2.console.enabled=true").run((context) -> { + assertThat(context).hasSingleBean(ServletRegistrationBean.class); + ServletRegistrationBean registrationBean = context.getBean(ServletRegistrationBean.class); + assertThat(registrationBean.getUrlMappings()).contains("/h2-console/*"); + assertThat(registrationBean.getInitParameters()).doesNotContainKey("trace"); + assertThat(registrationBean.getInitParameters()).doesNotContainKey("webAllowOthers"); + assertThat(registrationBean.getInitParameters()).doesNotContainKey("webAdminPassword"); + }); + } + + @Test + void customPathMustBeginWithASlash() { + this.contextRunner.withPropertyValues("spring.h2.console.enabled=true", "spring.h2.console.path=custom") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).isInstanceOf(BeanCreationException.class) + .hasMessageContaining("Failed to bind properties under 'spring.h2.console'"); + }); + } + + @Test + void customPathWithTrailingSlash() { + this.contextRunner.withPropertyValues("spring.h2.console.enabled=true", "spring.h2.console.path=/custom/") + .run((context) -> { + assertThat(context).hasSingleBean(ServletRegistrationBean.class); + ServletRegistrationBean registrationBean = context.getBean(ServletRegistrationBean.class); + assertThat(registrationBean.getUrlMappings()).contains("/custom/*"); + }); + } + + @Test + void customPath() { + this.contextRunner.withPropertyValues("spring.h2.console.enabled=true", "spring.h2.console.path=/custom") + .run((context) -> { + assertThat(context).hasSingleBean(ServletRegistrationBean.class); + ServletRegistrationBean registrationBean = context.getBean(ServletRegistrationBean.class); + assertThat(registrationBean.getUrlMappings()).contains("/custom/*"); + }); + } + + @Test + void customInitParameters() { + this.contextRunner.withPropertyValues("spring.h2.console.enabled=true", "spring.h2.console.settings.trace=true", + "spring.h2.console.settings.web-allow-others=true", + "spring.h2.console.settings.web-admin-password=abcd").run((context) -> { + assertThat(context).hasSingleBean(ServletRegistrationBean.class); + ServletRegistrationBean registrationBean = context.getBean(ServletRegistrationBean.class); + assertThat(registrationBean.getUrlMappings()).contains("/h2-console/*"); + assertThat(registrationBean.getInitParameters()).containsEntry("trace", ""); + assertThat(registrationBean.getInitParameters()).containsEntry("webAllowOthers", ""); + assertThat(registrationBean.getInitParameters()).containsEntry("webAdminPassword", "abcd"); + }); + } + + @Test + @ExtendWith(OutputCaptureExtension.class) + void singleDataSourceUrlIsLoggedWhenOnlyOneAvailable(CapturedOutput output) { + this.contextRunner.withConfiguration(AutoConfigurations.of(DataSourceAutoConfiguration.class)) + .withPropertyValues("spring.h2.console.enabled=true").run((context) -> { + try (Connection connection = context.getBean(DataSource.class).getConnection()) { + assertThat(output).contains("H2 console available at '/h2-console'. Database available at '" + + connection.getMetaData().getURL() + "'"); + } + }); + } + + @Test + @ExtendWith(OutputCaptureExtension.class) + void noDataSourceIsLoggedWhenNoneAvailable(CapturedOutput output) { + this.contextRunner.withUserConfiguration(FailingDataSourceConfiguration.class) + .withPropertyValues("spring.h2.console.enabled=true") + .run((context) -> assertThat(output).doesNotContain("H2 console available")); + } + + @Test + @ExtendWith(OutputCaptureExtension.class) + void allDataSourceUrlsAreLoggedWhenMultipleAvailable(CapturedOutput output) { + this.contextRunner + .withUserConfiguration(FailingDataSourceConfiguration.class, MultiDataSourceConfiguration.class) + .withPropertyValues("spring.h2.console.enabled=true").run((context) -> assertThat(output).contains( + "H2 console available at '/h2-console'. Databases available at 'someJdbcUrl', 'anotherJdbcUrl'")); + } + + @Test + void h2ConsoleShouldNotFailIfDatabaseConnectionFails() { + this.contextRunner.withUserConfiguration(FailingDataSourceConfiguration.class) + .withPropertyValues("spring.h2.console.enabled=true") + .run((context) -> assertThat(context.isRunning()).isTrue()); + } + + @Configuration(proxyBeanMethods = false) + static class FailingDataSourceConfiguration { + + @Bean + DataSource dataSource() throws SQLException { + DataSource dataSource = mock(DataSource.class); + given(dataSource.getConnection()).willThrow(IllegalStateException.class); + return dataSource; + } + + } + + @Configuration(proxyBeanMethods = false) + static class MultiDataSourceConfiguration { + + @Bean + @Order(5) + DataSource anotherDataSource() throws SQLException { + return mockDataSource("anotherJdbcUrl"); + } + + @Bean + @Order(0) + DataSource someDataSource() throws SQLException { + return mockDataSource("someJdbcUrl"); + } + + private DataSource mockDataSource(String url) throws SQLException { + DataSource dataSource = mock(DataSource.class); + given(dataSource.getConnection()).willReturn(mock(Connection.class)); + given(dataSource.getConnection().getMetaData()).willReturn(mock(DatabaseMetaData.class)); + given(dataSource.getConnection().getMetaData().getURL()).willReturn(url); + return dataSource; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsolePropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsolePropertiesTests.java new file mode 100644 index 0000000000..2cf6f66ea9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/h2/H2ConsolePropertiesTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed 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 + * + * https://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.springframework.boot.autoconfigure.h2; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link H2ConsoleProperties}. + * + * @author Madhura Bhave + */ +class H2ConsolePropertiesTests { + + @Test + void pathMustNotBeEmpty() { + H2ConsoleProperties properties = new H2ConsoleProperties(); + assertThatIllegalArgumentException().isThrownBy(() -> properties.setPath("")) + .withMessageContaining("Path must have length greater than 1"); + } + + @Test + void pathMustHaveLengthGreaterThanOne() { + H2ConsoleProperties properties = new H2ConsoleProperties(); + assertThatIllegalArgumentException().isThrownBy(() -> properties.setPath("/")) + .withMessageContaining("Path must have length greater than 1"); + } + + @Test + void customPathMustBeginWithASlash() { + H2ConsoleProperties properties = new H2ConsoleProperties(); + assertThatIllegalArgumentException().isThrownBy(() -> properties.setPath("custom")) + .withMessageContaining("Path must start with '/'"); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/PathRequestTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/PathRequestTests.java index 6702dc5a65..7f30f8e782 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/PathRequestTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/security/servlet/PathRequestTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,17 @@ package org.springframework.boot.autoconfigure.security.servlet; +import jakarta.servlet.http.HttpServletRequest; +import org.assertj.core.api.AssertDelegateTarget; import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.h2.H2ConsoleProperties; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockServletContext; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.context.WebApplicationContext; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -32,4 +41,76 @@ class PathRequestTests { assertThat(PathRequest.toStaticResources()).isInstanceOf(StaticResourceRequest.class); } + @Test + void toH2ConsoleShouldMatchH2ConsolePath() { + RequestMatcher matcher = PathRequest.toH2Console(); + assertMatcher(matcher).matches("/h2-console"); + assertMatcher(matcher).matches("/h2-console/subpath"); + assertMatcher(matcher).doesNotMatch("/js/file.js"); + } + + @Test + void toH2ConsoleWhenManagementContextShouldNeverMatch() { + RequestMatcher matcher = PathRequest.toH2Console(); + assertMatcher(matcher, "management").doesNotMatch("/h2-console"); + assertMatcher(matcher, "management").doesNotMatch("/h2-console/subpath"); + assertMatcher(matcher, "management").doesNotMatch("/js/file.js"); + } + + private RequestMatcherAssert assertMatcher(RequestMatcher matcher) { + return assertMatcher(matcher, null); + } + + private RequestMatcherAssert assertMatcher(RequestMatcher matcher, String serverNamespace) { + TestWebApplicationContext context = new TestWebApplicationContext(serverNamespace); + context.registerBean(ServerProperties.class); + context.registerBean(H2ConsoleProperties.class); + return assertThat(new RequestMatcherAssert(context, matcher)); + } + + static class RequestMatcherAssert implements AssertDelegateTarget { + + private final WebApplicationContext context; + + private final RequestMatcher matcher; + + RequestMatcherAssert(WebApplicationContext context, RequestMatcher matcher) { + this.context = context; + this.matcher = matcher; + } + + void matches(String path) { + matches(mockRequest(path)); + } + + private void matches(HttpServletRequest request) { + assertThat(this.matcher.matches(request)).as("Matches " + getRequestPath(request)).isTrue(); + } + + void doesNotMatch(String path) { + doesNotMatch(mockRequest(path)); + } + + private void doesNotMatch(HttpServletRequest request) { + assertThat(this.matcher.matches(request)).as("Does not match " + getRequestPath(request)).isFalse(); + } + + private MockHttpServletRequest mockRequest(String path) { + MockServletContext servletContext = new MockServletContext(); + servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); + MockHttpServletRequest request = new MockHttpServletRequest(servletContext); + request.setPathInfo(path); + return request; + } + + private String getRequestPath(HttpServletRequest request) { + String url = request.getServletPath(); + if (request.getPathInfo() != null) { + url += request.getPathInfo(); + } + return url; + } + + } + } diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties index 7def0ae623..3d624c7de6 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties @@ -230,6 +230,8 @@ boot-features-spring-data-jpa-repositories=features.sql.jpa-and-spring-data.repo boot-features-creating-and-dropping-jpa-databases=features.sql.jpa-and-spring-data.creating-and-dropping boot-features-jpa-in-web-environment=features.sql.jpa-and-spring-data.open-entity-manager-in-view boot-features-data-jdbc=features.sql.jdbc +boot-features-sql-h2-console=features.sql.h2-web-console +boot-features-sql-h2-console-custom-path=features.sql.h2-web-console.custom-path boot-features-jooq=features.sql.jooq boot-features-jooq-codegen=features.sql.jooq.codegen boot-features-jooq-dslcontext=features.sql.jooq.dslcontext diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc index 824da44776..cdf909d1ed 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc @@ -303,6 +303,28 @@ TIP: For complete details of Spring Data JDBC, see the {spring-data-jdbc-docs}[r +[[data.sql.h2-web-console]] +=== Using H2's Web Console +The https://www.h2database.com[H2 database] provides a https://www.h2database.com/html/quickstart.html#h2_console[browser-based console] that Spring Boot can auto-configure for you. +The console is auto-configured when the following conditions are met: + +* You are developing a servlet-based web application. +* `com.h2database:h2` is on the classpath. +* You are using <>. + +TIP: If you are not using Spring Boot's developer tools but would still like to make use of H2's console, you can configure the configprop:spring.h2.console.enabled[] property with a value of `true`. + +NOTE: The H2 console is only intended for use during development, so you should take care to ensure that `spring.h2.console.enabled` is not set to `true` in production. + + + +[[data.sql.h2-web-console.custom-path]] +==== Changing the H2 Console's Path +By default, the console is available at `/h2-console`. +You can customize the console's path by using the configprop:spring.h2.console.path[] property. + + + [[data.sql.jooq]] === Using jOOQ jOOQ Object Oriented Querying (https://www.jooq.org/[jOOQ]) is a popular product from https://www.datageekery.com/[Data Geekery] which generates Java code from your database and lets you build type-safe SQL queries through its fluent API.