10 changed files with 635 additions and 2 deletions
@ -0,0 +1,107 @@
@@ -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<JakartaWebServlet> h2Console(H2ConsoleProperties properties, |
||||
ObjectProvider<DataSource> dataSource) { |
||||
String path = properties.getPath(); |
||||
String urlMapping = path + (path.endsWith("/") ? "*" : "/*"); |
||||
ServletRegistrationBean<JakartaWebServlet> registration = new ServletRegistrationBean<>(new JakartaWebServlet(), |
||||
urlMapping); |
||||
configureH2ConsoleSettings(registration, properties.getSettings()); |
||||
if (logger.isInfoEnabled()) { |
||||
logDataSources(dataSource, path); |
||||
} |
||||
return registration; |
||||
} |
||||
|
||||
private void logDataSources(ObjectProvider<DataSource> dataSource, String path) { |
||||
List<String> 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<JakartaWebServlet> registration, |
||||
Settings settings) { |
||||
if (settings.isTrace()) { |
||||
registration.addInitParameter("trace", ""); |
||||
} |
||||
if (settings.isWebAllowOthers()) { |
||||
registration.addInitParameter("webAllowOthers", ""); |
||||
} |
||||
if (settings.getWebAdminPassword() != null) { |
||||
registration.addInitParameter("webAdminPassword", settings.getWebAdminPassword()); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,111 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,20 @@
@@ -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; |
||||
@ -0,0 +1,190 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,51 @@
@@ -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 '/'"); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue