diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java index b4fae61db4..5c20e9750d 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionConfiguration.java @@ -20,9 +20,13 @@ import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.session.SessionRepository; import org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessionConfiguration; @@ -31,13 +35,23 @@ import org.springframework.session.jdbc.config.annotation.web.http.JdbcHttpSessi * * @author EddĂș MelĂ©ndez * @author Stephane Nicoll + * @author Vedran Pavic */ @Configuration +@ConditionalOnClass(JdbcTemplate.class) @ConditionalOnMissingBean(SessionRepository.class) @ConditionalOnBean(DataSource.class) @Conditional(SessionCondition.class) class JdbcSessionConfiguration { + @Bean + @ConditionalOnMissingBean + public JdbcSessionDatabaseInitializer jdbcSessionDatabaseInitializer( + SessionProperties properties, DataSource dataSource, + ResourceLoader resourceLoader) { + return new JdbcSessionDatabaseInitializer(properties, dataSource, resourceLoader); + } + @Configuration public static class SpringBootJdbcHttpSessionConfiguration extends JdbcHttpSessionConfiguration { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionDatabaseInitializer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionDatabaseInitializer.java new file mode 100644 index 0000000000..bac0adf0b6 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/JdbcSessionDatabaseInitializer.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2016 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 + * + * 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.springframework.boot.autoconfigure.session; + +import javax.annotation.PostConstruct; +import javax.sql.DataSource; + +import org.springframework.core.io.ResourceLoader; +import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.MetaDataAccessException; +import org.springframework.util.Assert; + +/** + * Initializer for Spring Session schema. + * + * @author Vedran Pavic + * @since 1.4.0 + */ +public class JdbcSessionDatabaseInitializer { + + private SessionProperties properties; + + private DataSource dataSource; + + private ResourceLoader resourceLoader; + + public JdbcSessionDatabaseInitializer(SessionProperties properties, + DataSource dataSource, ResourceLoader resourceLoader) { + Assert.notNull(properties, "SessionProperties must not be null"); + Assert.notNull(dataSource, "DataSource must not be null"); + Assert.notNull(resourceLoader, "ResourceLoader must not be null"); + this.properties = properties; + this.dataSource = dataSource; + this.resourceLoader = resourceLoader; + } + + @PostConstruct + protected void initialize() { + if (this.properties.getJdbc().getInitializer().isEnabled()) { + String platform = getDatabaseType(); + if ("hsql".equals(platform)) { + platform = "hsqldb"; + } + if ("postgres".equals(platform)) { + platform = "postgresql"; + } + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); + String schemaLocation = this.properties.getJdbc().getSchema(); + schemaLocation = schemaLocation.replace("@@platform@@", platform); + populator.addScript(this.resourceLoader.getResource(schemaLocation)); + populator.setContinueOnError(true); + DatabasePopulatorUtils.execute(populator, this.dataSource); + } + } + + private String getDatabaseType() { + try { + String databaseProductName = JdbcUtils.extractDatabaseMetaData( + this.dataSource, "getDatabaseProductName").toString(); + return JdbcUtils.commonDatabaseName(databaseProductName).toLowerCase(); + } + catch (MetaDataAccessException ex) { + throw new IllegalStateException("Unable to detect database type", ex); + } + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java index 180402d189..5fa4c60719 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/session/SessionProperties.java @@ -26,6 +26,7 @@ import org.springframework.session.data.redis.RedisFlushMode; * * @author Tommy Ludwig * @author Stephane Nicoll + * @author Vedran Pavic * @since 1.4.0 */ @ConfigurationProperties("spring.session") @@ -103,11 +104,29 @@ public class SessionProperties { public static class Jdbc { + private static final String DEFAULT_SCHEMA_LOCATION = "classpath:org/springframework/" + + "session/jdbc/schema-@@platform@@.sql"; + + /** + * Path to the SQL file to use to initialize the database schema. + */ + private String schema = DEFAULT_SCHEMA_LOCATION; + /** * Name of database table used to store sessions. */ private String tableName = "SPRING_SESSION"; + private final Initializer initializer = new Initializer(); + + public String getSchema() { + return this.schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } + public String getTableName() { return this.tableName; } @@ -116,6 +135,27 @@ public class SessionProperties { this.tableName = tableName; } + public Initializer getInitializer() { + return initializer; + } + + public static class Initializer { + + /** + * Create the required session tables on startup if necessary. + */ + private boolean enabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + } + } public static class Mongo { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 40ea8a3dc4..af368d9a6f 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -646,6 +646,12 @@ public class ServerProperties */ private int maxHttpHeaderSize = 0; // bytes + /** + * Whether requests to the context root should be redirected by appending a / to + * the path. + */ + private Boolean redirectContextRoot; + /** * Character encoding to use to decode the URI. */ @@ -742,6 +748,14 @@ public class ServerProperties this.portHeader = portHeader; } + public Boolean getRedirectContextRoot() { + return this.redirectContextRoot; + } + + public void setRedirectContextRoot(Boolean redirectContextRoot) { + this.redirectContextRoot = redirectContextRoot; + } + public String getRemoteIpHeader() { return this.remoteIpHeader; } @@ -789,6 +803,9 @@ public class ServerProperties customizeConnectionTimeout(factory, serverProperties.getConnectionTimeout()); } + if (this.redirectContextRoot != null) { + customizeRedirectContextRoot(factory, this.redirectContextRoot); + } } private void customizeConnectionTimeout( @@ -911,6 +928,19 @@ public class ServerProperties factory.addContextValves(valve); } + private void customizeRedirectContextRoot( + TomcatEmbeddedServletContainerFactory factory, + final boolean redirectContextRoot) { + factory.addContextCustomizers(new TomcatContextCustomizer() { + + @Override + public void customize(Context context) { + context.setMapperContextRootRedirectEnabled(redirectContextRoot); + } + + }); + } + public static class Accesslog { /** diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationJdbcTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationJdbcTests.java new file mode 100644 index 0000000000..10381eae15 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationJdbcTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2016 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 + * + * 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.springframework.boot.autoconfigure.session; + +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.session.jdbc.JdbcOperationsSessionRepository; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * JDBC specific tests for {@link SessionAutoConfiguration}. + * + * @author Vedran Pavic + * @author Stephane Nicoll + */ +public class SessionAutoConfigurationJdbcTests extends AbstractSessionAutoConfigurationTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void defaultConfig() { + load(Arrays.asList(EmbeddedDataSourceConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class), + "spring.session.store-type=jdbc"); + JdbcOperationsSessionRepository repository = validateSessionRepository( + JdbcOperationsSessionRepository.class); + assertThat(new DirectFieldAccessor(repository).getPropertyValue("tableName")) + .isEqualTo("SPRING_SESSION"); + assertThat(this.context.getBean(JdbcOperations.class) + .queryForList("select * from SPRING_SESSION")).isEmpty(); + } + + @Test + public void disableDatabaseInitializer() { + load(Arrays.asList(EmbeddedDataSourceConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class), + "spring.session.store-type=jdbc", + "spring.session.jdbc.initializer.enabled=false"); + JdbcOperationsSessionRepository repository = validateSessionRepository( + JdbcOperationsSessionRepository.class); + assertThat(new DirectFieldAccessor(repository).getPropertyValue("tableName")) + .isEqualTo("SPRING_SESSION"); + this.thrown.expect(BadSqlGrammarException.class); + assertThat(this.context.getBean(JdbcOperations.class) + .queryForList("select * from SPRING_SESSION")).isEmpty(); + } + + @Test + public void customTableName() { + load(Arrays.asList(EmbeddedDataSourceConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class), + "spring.session.store-type=jdbc", + "spring.session.jdbc.table-name=FOO_BAR", + "spring.session.jdbc.schema=classpath:session/custom-schema-h2.sql"); + JdbcOperationsSessionRepository repository = validateSessionRepository( + JdbcOperationsSessionRepository.class); + assertThat(new DirectFieldAccessor(repository).getPropertyValue("tableName")) + .isEqualTo("FOO_BAR"); + assertThat(this.context.getBean(JdbcOperations.class) + .queryForList("select * from FOO_BAR")).isEmpty(); + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java index 99877075e0..4519e002e3 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/session/SessionAutoConfigurationTests.java @@ -29,8 +29,6 @@ import org.junit.rules.ExpectedException; import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.BeanCreationException; import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration; import org.springframework.context.annotation.Bean; @@ -39,7 +37,6 @@ import org.springframework.session.ExpiringSession; import org.springframework.session.MapSessionRepository; import org.springframework.session.SessionRepository; import org.springframework.session.data.mongo.MongoOperationsSessionRepository; -import org.springframework.session.jdbc.JdbcOperationsSessionRepository; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; @@ -106,29 +103,6 @@ public class SessionAutoConfigurationTests extends AbstractSessionAutoConfigurat assertThat(getSessionTimeout(repository)).isNull(); } - @Test - public void jdbcSessionStore() { - load(Arrays.asList(EmbeddedDataSourceConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class), - "spring.session.store-type=jdbc"); - JdbcOperationsSessionRepository repository = validateSessionRepository( - JdbcOperationsSessionRepository.class); - assertThat(new DirectFieldAccessor(repository).getPropertyValue("tableName")) - .isEqualTo("SPRING_SESSION"); - } - - @Test - public void jdbcSessionStoreCustomTableName() { - load(Arrays.asList(EmbeddedDataSourceConfiguration.class, - DataSourceTransactionManagerAutoConfiguration.class), - "spring.session.store-type=jdbc", - "spring.session.jdbc.table-name=FOO_BAR"); - JdbcOperationsSessionRepository repository = validateSessionRepository( - JdbcOperationsSessionRepository.class); - assertThat(new DirectFieldAccessor(repository).getPropertyValue("tableName")) - .isEqualTo("FOO_BAR"); - } - @Test public void hazelcastSessionStore() { load(Collections.>singletonList(HazelcastConfiguration.class), diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index 4e609ff193..af1a001446 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -29,6 +29,7 @@ import javax.servlet.ServletException; import javax.servlet.SessionCookieConfig; import javax.servlet.SessionTrackingMode; +import org.apache.catalina.Context; import org.apache.catalina.Valve; import org.apache.catalina.valves.RemoteIpValve; import org.junit.Before; @@ -41,6 +42,7 @@ import org.springframework.beans.MutablePropertyValues; import org.springframework.boot.bind.RelaxedDataBinder; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.jetty.JettyEmbeddedServletContainerFactory; +import org.springframework.boot.context.embedded.tomcat.TomcatContextCustomizer; import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.undertow.UndertowEmbeddedServletContainerFactory; import org.springframework.boot.web.servlet.ServletContextInitializer; @@ -150,6 +152,30 @@ public class ServerPropertiesTests { .isEqualTo("10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"); } + @Test + public void redirectContextRootIsNotConfiguredByDefault() throws Exception { + bindProperties(new HashMap()); + ServerProperties.Tomcat tomcat = this.properties.getTomcat(); + assertThat(tomcat.getRedirectContextRoot()).isNull(); + } + + @Test + public void redirectContextRootCanBeConfigured() throws Exception { + Map map = new HashMap(); + map.put("server.tomcat.redirect-context-root", "false"); + bindProperties(map); + ServerProperties.Tomcat tomcat = this.properties.getTomcat(); + assertThat(tomcat.getRedirectContextRoot()).isEqualTo(false); + TomcatEmbeddedServletContainerFactory container = new TomcatEmbeddedServletContainerFactory(); + this.properties.customize(container); + Context context = mock(Context.class); + for (TomcatContextCustomizer customizer : container + .getTomcatContextCustomizers()) { + customizer.customize(context); + } + verify(context).setMapperContextRootRedirectEnabled(false); + } + @Test public void testTrailingSlashOfContextPathIsRemoved() { new RelaxedDataBinder(this.properties, "server").bind(new MutablePropertyValues( diff --git a/spring-boot-autoconfigure/src/test/resources/session/custom-schema-h2.sql b/spring-boot-autoconfigure/src/test/resources/session/custom-schema-h2.sql new file mode 100644 index 0000000000..27fd86ef43 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/resources/session/custom-schema-h2.sql @@ -0,0 +1,20 @@ +CREATE TABLE FOO_BAR ( + SESSION_ID CHAR(36), + CREATION_TIME BIGINT NOT NULL, + LAST_ACCESS_TIME BIGINT NOT NULL, + MAX_INACTIVE_INTERVAL INT NOT NULL, + PRINCIPAL_NAME VARCHAR(100), + CONSTRAINT FOO_BAR_PK PRIMARY KEY (SESSION_ID) +); + +CREATE INDEX FOO_BAR_IX1 ON FOO_BAR (LAST_ACCESS_TIME); + +CREATE TABLE FOO_BAR_ATTRIBUTES ( + SESSION_ID CHAR(36), + ATTRIBUTE_NAME VARCHAR(100), + ATTRIBUTE_BYTES LONGVARBINARY, + CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME), + CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES FOO_BAR(SESSION_ID) ON DELETE CASCADE +); + +CREATE INDEX FOO_BAR_ATTRIBUTES_IX1 ON FOO_BAR_ATTRIBUTES (SESSION_ID); diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index cd197f2819..74c3045353 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -214,6 +214,7 @@ content into your application; rather pick only the properties that you need. server.tomcat.port-header=X-Forwarded-Port # Name of the HTTP header used to override the original port value. server.tomcat.protocol-header= # Header that holds the incoming protocol, usually named "X-Forwarded-Proto". server.tomcat.protocol-header-https-value=https # Value of the protocol header that indicates that the incoming request uses SSL. + server.tomcat.redirect-context-root= # Whether requests to the context root should be redirected by appending a / to the path. server.tomcat.remote-ip-header= # Name of the http header from which the remote ip is extracted. For instance `X-FORWARDED-FOR` server.tomcat.uri-encoding=UTF-8 # Character encoding to use to decode the URI. server.undertow.accesslog.dir= # Undertow access log directory. @@ -366,6 +367,8 @@ content into your application; rather pick only the properties that you need. # SPRING SESSION ({sc-spring-boot-autoconfigure}/session/SessionProperties.{sc-ext}[SessionProperties]) spring.session.hazelcast.map-name=spring:session:sessions # Name of the map used to store sessions. + spring.session.jdbc.initializer.enabled=true # Create the required session tables on startup if necessary. + spring.session.jdbc.schema=classpath:org/springframework/session/jdbc/schema-@@platform@@.sql # Path to the SQL file to use to initialize the database schema. spring.session.jdbc.table-name=SPRING_SESSION # Name of database table used to store sessions. spring.session.mongo.collection-name=sessions # Collection name used to store sessions. spring.session.redis.flush-mode= # Flush mode for the Redis sessions. diff --git a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java index 32d44e1081..a7698213c3 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/embedded/tomcat/TomcatEmbeddedServletContainerFactory.java @@ -184,7 +184,6 @@ public class TomcatEmbeddedServletContainerFactory : ClassUtils.getDefaultClassLoader()); try { context.setUseRelativeRedirects(false); - context.setMapperContextRootRedirectEnabled(true); } catch (NoSuchMethodError ex) { // Tomcat is < 8.0.30. Continue