Add JdbcOperationsSessionRepository

This commit provides implementation of SessionRepository based
on Spring's JdbcOperations interface.

@EnableJdbcHttpSession annotation is provided to ease the
configuration, together with spring-session-jdbc BOM and schema
creation scripts for all major databases.

Fixes gh-364
This commit is contained in:
Vedran Pavic
2016-02-22 22:47:23 +01:00
committed by Rob Winch
parent 091d0d8d9f
commit cd38e307e0
37 changed files with 2363 additions and 1 deletions

View File

@@ -0,0 +1,317 @@
/*
* Copyright 2014-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.session.jdbc;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.session.FindByIndexNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.AdditionalMatchers.and;
import static org.mockito.AdditionalMatchers.not;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.contains;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isA;
import static org.mockito.Matchers.startsWith;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
/**
* Tests for {@link JdbcOperationsSessionRepository}.
*
* @author Vedran Pavic
* @since 1.2.0
*/
@RunWith(MockitoJUnitRunner.class)
public class JdbcOperationsSessionRepositoryTests {
private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
@Rule
public ExpectedException thrown = ExpectedException.none();
@Mock
private DataSource dataSource;
@Mock
private JdbcOperations jdbcOperations;
private JdbcOperationsSessionRepository repository;
@Before
public void setUp() {
this.repository = new JdbcOperationsSessionRepository(this.jdbcOperations);
}
@Test
public void constructorDataSource() {
JdbcOperationsSessionRepository repository =
new JdbcOperationsSessionRepository(this.dataSource);
assertThat(ReflectionTestUtils.getField(repository, "jdbcOperations")).isNotNull();
}
@Test
public void constructorNullDataSource() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Property 'dataSource' is required");
new JdbcOperationsSessionRepository((DataSource) null);
}
@Test
public void constructorNullJdbcOperations() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("JdbcOperations must not be null");
new JdbcOperationsSessionRepository((JdbcOperations) null);
}
@Test
public void setTableNameNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Table name must not be empty");
this.repository.setTableName(null);
}
@Test
public void setTableNameEmpty() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Table name must not be empty");
this.repository.setTableName(" ");
}
@Test
public void setLobHandlerNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("LobHandler must not be null");
this.repository.setLobHandler(null);
}
@Test
public void setSerializingConverterNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("SerializingConverter must not be null");
this.repository.setSerializingConverter(null);
}
@Test
public void setDeserializingConverterNull() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("DeserializingConverter must not be null");
this.repository.setDeserializingConverter(null);
}
@Test
public void createSessionDefaultMaxInactiveInterval() throws Exception {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
assertThat(session.isNew()).isTrue();
assertThat(session.getMaxInactiveIntervalInSeconds()).isEqualTo(
new MapSession().getMaxInactiveIntervalInSeconds());
verifyZeroInteractions(this.jdbcOperations);
}
@Test
public void createSessionCustomMaxInactiveInterval() throws Exception {
int interval = 1;
this.repository.setDefaultMaxInactiveInterval(interval);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
assertThat(session.isNew()).isTrue();
assertThat(session.getMaxInactiveIntervalInSeconds()).isEqualTo(interval);
verifyZeroInteractions(this.jdbcOperations);
}
@Test
public void saveNew() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.createSession();
this.repository.save(session);
assertThat(session.isNew()).isFalse();
verify(this.jdbcOperations, times(1)).update(startsWith("INSERT"), isA(PreparedStatementSetter.class));
}
@Test
public void saveUpdatedAttributes() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession());
session.setAttribute("testName", "testValue");
this.repository.save(session);
assertThat(session.isNew()).isFalse();
verify(this.jdbcOperations, times(1))
.update(and(startsWith("UPDATE"), contains("SESSION_BYTES")), isA(PreparedStatementSetter.class));
}
@Test
public void saveUpdatedLastAccessedTime() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession());
session.setLastAccessedTime(System.currentTimeMillis());
this.repository.save(session);
assertThat(session.isNew()).isFalse();
verify(this.jdbcOperations, times(1))
.update(and(startsWith("UPDATE"), not(contains("SESSION_BYTES"))), isA(PreparedStatementSetter.class));
}
@Test
public void saveUnchanged() {
JdbcOperationsSessionRepository.JdbcSession session = this.repository.new JdbcSession(new MapSession());
this.repository.save(session);
assertThat(session.isNew()).isFalse();
verifyZeroInteractions(this.jdbcOperations);
}
@Test
public void getSessionNotFound() {
String sessionId = "testSessionId";
JdbcOperationsSessionRepository.JdbcSession session = this.repository.getSession(sessionId);
assertThat(session).isNull();
verify(this.jdbcOperations, times(1)).queryForObject(
startsWith("SELECT"), eq(new Object[] { sessionId }), isA(RowMapper.class));
}
@Test
public void getSessionExpired() {
MapSession expired = new MapSession();
expired.setMaxInactiveIntervalInSeconds(0);
when(this.jdbcOperations.queryForObject(startsWith("SELECT"), eq(new Object[] { expired.getId() }), isA(RowMapper.class)))
.thenReturn(expired);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.getSession(expired.getId());
assertThat(session).isNull();
verify(this.jdbcOperations, times(1)).queryForObject(
startsWith("SELECT"), eq(new Object[] { expired.getId() }), isA(RowMapper.class));
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), eq(expired.getId()));
}
@Test
public void getSessionFound() {
MapSession saved = new MapSession();
saved.setAttribute("savedName", "savedValue");
when(this.jdbcOperations.queryForObject(startsWith("SELECT"), eq(new Object[] { saved.getId() }), isA(RowMapper.class)))
.thenReturn(saved);
JdbcOperationsSessionRepository.JdbcSession session = this.repository.getSession(saved.getId());
assertThat(session.getId()).isEqualTo(saved.getId());
assertThat(session.isNew()).isFalse();
assertThat(session.getAttribute("savedName")).isEqualTo("savedValue");
verify(this.jdbcOperations, times(1)).queryForObject(
startsWith("SELECT"), eq(new Object[] { saved.getId() }), isA(RowMapper.class));
}
@Test
public void delete() {
String sessionId = "testSessionId";
this.repository.delete(sessionId);
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), eq(sessionId));
}
@Test
public void findByIndexNameAndIndexValueUnknownIndexName() {
String indexValue = "testIndexValue";
Map<String, JdbcOperationsSessionRepository.JdbcSession> sessions =
this.repository.findByIndexNameAndIndexValue("testIndexName", indexValue);
assertThat(sessions).isEmpty();
verifyZeroInteractions(this.jdbcOperations);
}
@Test
public void findByIndexNameAndIndexValuePrincipalIndexNameNotFound() {
String principal = "username";
Map<String, JdbcOperationsSessionRepository.JdbcSession> sessions = this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).isEmpty();
verify(this.jdbcOperations, times(1)).query(
startsWith("SELECT"), eq(new Object[] { principal }), isA(RowMapper.class));
}
@Test
public void findByIndexNameAndIndexValuePrincipalIndexNameFound() {
String principal = "username";
Authentication authentication = new UsernamePasswordAuthenticationToken(
principal, "notused", AuthorityUtils.createAuthorityList("ROLE_USER"));
List<MapSession> saved = new ArrayList<MapSession>(2);
MapSession saved1 = new MapSession();
saved1.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved1);
MapSession saved2 = new MapSession();
saved2.setAttribute(SPRING_SECURITY_CONTEXT, authentication);
saved.add(saved2);
when(this.jdbcOperations.query(startsWith("SELECT"), eq(new Object[] { principal }), isA(RowMapper.class)))
.thenReturn(saved);
Map<String, JdbcOperationsSessionRepository.JdbcSession> sessions = this.repository
.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principal);
assertThat(sessions).hasSize(2);
verify(this.jdbcOperations, times(1)).query(
startsWith("SELECT"), eq(new Object[] { principal }), isA(RowMapper.class));
}
@Test
public void cleanupExpiredSessions() {
this.repository.cleanUpExpiredSessions();
verify(this.jdbcOperations, times(1)).update(startsWith("DELETE"), anyLong());
}
}

View File

@@ -0,0 +1,224 @@
/*
* Copyright 2014-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.session.jdbc.config.annotation.web.http;
import javax.sql.DataSource;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.session.jdbc.JdbcOperationsSessionRepository;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link JdbcHttpSessionConfiguration}.
*
* @author Vedran Pavic
* @since 1.2.0
*/
public class JdbcHttpSessionConfigurationTests {
private static final String TABLE_NAME = "TEST_SESSION";
private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 600;
private static final String TABLE_NAME_SYSTEM_PROPERTY = "spring.session.jdbc.tableName";
@Rule
public final ExpectedException thrown = ExpectedException.none();
private AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@After
public void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void noDataSourceConfiguration() {
this.thrown.expect(UnsatisfiedDependencyException.class);
this.thrown.expectMessage("springSessionJdbcOperations");
registerAndRefresh(EmptyConfiguration.class);
}
@Test
public void defaultConfiguration() {
registerAndRefresh(DefaultConfiguration.class);
assertThat(this.context.getBean(JdbcOperationsSessionRepository.class)).isNotNull();
}
@Test
public void customTableName() {
registerAndRefresh(CustomTableNameConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "tableName")).isEqualTo(TABLE_NAME);
}
@Test
public void customTableNameSystemProperty() {
System.setProperty(TABLE_NAME_SYSTEM_PROPERTY, TABLE_NAME);
try {
registerAndRefresh(DefaultConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "tableName")).isEqualTo(TABLE_NAME);
}
finally {
System.clearProperty(TABLE_NAME_SYSTEM_PROPERTY);
}
}
@Test
public void customMaxInactiveIntervalInSeconds() {
registerAndRefresh(CustomMaxInactiveIntervalInSecondsConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
assertThat(repository).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "defaultMaxInactiveInterval"))
.isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
}
@Test
public void customLobHandlerConfiguration() {
registerAndRefresh(CustomLobHandlerConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
LobHandler lobHandler = this.context.getBean(LobHandler.class);
assertThat(repository).isNotNull();
assertThat(lobHandler).isNotNull();
assertThat(ReflectionTestUtils.getField(repository, "lobHandler")).isEqualTo(lobHandler);
}
@Test
@SuppressWarnings("unchecked")
public void customSerializingConverterConfiguration() {
registerAndRefresh(CustomSerializingConverterConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
Converter<Object, byte[]> serializingConverter =
(Converter<Object, byte[]>) this.context.getBean("springSessionSerializingConverter");
assertThat(repository).isNotNull();
assertThat(serializingConverter).isNotNull();
Converter<Object, byte[]> sc =
(Converter<Object, byte[]>) ReflectionTestUtils.getField(repository, "serializingConverter");
assertThat(sc).isEqualTo(serializingConverter);
}
@Test
@SuppressWarnings("unchecked")
public void customDeserializingConverterConfiguration() {
registerAndRefresh(CustomDeserializingConverterConfiguration.class);
JdbcOperationsSessionRepository repository = this.context.getBean(JdbcOperationsSessionRepository.class);
Converter<byte[], Object> deserializingConverter =
(Converter<byte[], Object>) this.context.getBean("springSessionDeserializingConverter");
assertThat(repository).isNotNull();
assertThat(deserializingConverter).isNotNull();
Converter<byte[], Object> dc =
(Converter<byte[], Object>) ReflectionTestUtils.getField(repository, "deserializingConverter");
assertThat(dc).isEqualTo(deserializingConverter);
}
private void registerAndRefresh(Class<?>... annotatedClasses) {
this.context.register(annotatedClasses);
this.context.refresh();
}
@Configuration
@EnableJdbcHttpSession
static class EmptyConfiguration {
}
static class BaseConfiguration {
@Bean
public DataSource dataSource() {
return mock(DataSource.class);
}
}
@Configuration
@EnableJdbcHttpSession
static class DefaultConfiguration extends BaseConfiguration {
}
@Configuration
@EnableJdbcHttpSession(tableName = TABLE_NAME)
static class CustomTableNameConfiguration extends BaseConfiguration {
}
@Configuration
@EnableJdbcHttpSession(maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS)
static class CustomMaxInactiveIntervalInSecondsConfiguration extends BaseConfiguration {
}
@Configuration
@EnableJdbcHttpSession
static class CustomLobHandlerConfiguration extends BaseConfiguration {
@Bean
public LobHandler springSessionLobHandler() {
return mock(LobHandler.class);
}
}
@Configuration
@EnableJdbcHttpSession
static class CustomSerializingConverterConfiguration extends BaseConfiguration {
@Bean
@SuppressWarnings("unchecked")
public Converter<Object, byte[]> springSessionSerializingConverter() {
return mock(Converter.class);
}
}
@Configuration
@EnableJdbcHttpSession
static class CustomDeserializingConverterConfiguration extends BaseConfiguration {
@Bean
@SuppressWarnings("unchecked")
public Converter<byte[], Object> springSessionDeserializingConverter() {
return mock(Converter.class);
}
}
}