Add reactive streams support

This commit is contained in:
Greg Turnquist
2017-06-28 16:00:43 -05:00
parent 5e8273c35c
commit 51f4f2ae23
11 changed files with 466 additions and 44 deletions

70
pom.xml
View File

@@ -6,6 +6,12 @@
<artifactId>spring-session-data-mongodb</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb-parent</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
</parent>
<name>Spring Session MongoDB</name>
<url>https://github.com/spring-projects/spring-session-data-mongodb</url>
<description>
@@ -66,18 +72,19 @@
</licenses>
<properties>
<source.level>1.8</source.level>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<assertj.version>3.6.2</assertj.version>
<flapdoodle.version>1.50.5</flapdoodle.version>
<hamcrest.version>1.3</hamcrest.version>
<jackson.version>2.9.0.pr3</jackson.version>
<junit.version>4.12</junit.version>
<mockito.version>2.7.22</mockito.version>
<reactor.version>Bismuth-M2</reactor.version>
<spring.version>5.0.0.RC2</spring.version>
<spring-data.version>Kay-M4</spring-data.version>
<spring-session.version>2.0.0.BUILD-SNAPSHOT</spring-session.version>
<source.level>1.8</source.level>
<assertj.version>3.6.2</assertj.version>
<spring-security.version>5.0.0.M2</spring-security.version>
<spring-session.version>2.0.0.BUILD-SNAPSHOT</spring-session.version>
</properties>
<profiles>
@@ -423,6 +430,14 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-bom</artifactId>
<version>${reactor.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -437,14 +452,36 @@
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
<exclusions>
<exclusion>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver</artifactId>
<version>${mongo}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<optional>true</optional>
</dependency>
<!-- Test dependencies -->
<dependency>
@@ -498,6 +535,33 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver</artifactId>
<version>${mongo}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-async</artifactId>
<version>${mongo}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-reactivestreams</artifactId>
<version>${mongo.reactivestreams}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>

View File

@@ -90,9 +90,7 @@ public abstract class AbstractMongoSessionConverter implements GenericConverter
if (resolvedPrincipal != null) {
return resolvedPrincipal;
} else {
return expiringSession.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME)
.map(Object::toString)
.orElse("");
return expiringSession.getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
}
}

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.session.data.mongo;
import java.util.Optional;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@@ -38,14 +36,15 @@ final class AuthenticationParser {
* @param authentication Authentication object
* @return principal name
*/
static String extractName(Optional<Object> authentication) {
static String extractName(Object authentication) {
return authentication
.map(auth -> {
Expression expression = PARSER.parseExpression(NAME_EXPRESSION);
return expression.getValue(auth, String.class);
})
.orElse(null);
if (authentication == null) {
return null;
}
Expression expression = PARSER.parseExpression(NAME_EXPRESSION);
return expression.getValue(authentication, String.class);
}
private AuthenticationParser() {}

View File

@@ -132,7 +132,7 @@ public class JdkMongoSessionConverter extends AbstractMongoSessionConverter {
Map<String, Object> attributes = new HashMap<>();
for (String attrName : session.getAttributeNames()) {
attributes.put(attrName, session.getAttribute(attrName).get());
attributes.put(attrName, session.getAttribute(attrName));
}
return this.serializer.convert(attributes);

View File

@@ -16,6 +16,8 @@
package org.springframework.session.data.mongo;
import static org.springframework.session.data.mongo.MongoSessionUtils.*;
import java.time.Duration;
import java.util.Collections;
import java.util.HashMap;
@@ -26,7 +28,6 @@ import javax.annotation.PostConstruct;
import org.bson.Document;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mongodb.core.IndexOperations;
import org.springframework.data.mongodb.core.MongoOperations;
import org.springframework.data.mongodb.core.query.Query;
@@ -86,7 +87,7 @@ public class MongoOperationsSessionRepository
@Override
public void save(MongoSession session) {
DBObject sessionDbObject = convertToDBObject(session);
DBObject sessionDbObject = convertToDBObject(this.mongoSessionConverter, session);
this.mongoOperations.save(sessionDbObject, this.collectionName);
}
@@ -99,7 +100,7 @@ public class MongoOperationsSessionRepository
return null;
}
MongoSession session = convertToSession(sessionWrapper);
MongoSession session = convertToSession(this.mongoSessionConverter, sessionWrapper);
if (session.isExpired()) {
deleteById(id);
@@ -132,7 +133,7 @@ public class MongoOperationsSessionRepository
List<Document> mapSessions = this.mongoOperations.find(query, Document.class, this.collectionName);
for (Document dbSession : mapSessions) {
MongoSession mapSession = convertToSession(dbSession);
MongoSession mapSession = convertToSession(this.mongoSessionConverter, dbSession);
result.put(mapSession.getId(), mapSession);
}
@@ -155,20 +156,6 @@ public class MongoOperationsSessionRepository
return this.mongoOperations.findById(id, Document.class, this.collectionName);
}
MongoSession convertToSession(Document session) {
return (MongoSession) this.mongoSessionConverter.convert(session,
TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class));
}
DBObject convertToDBObject(MongoSession session) {
return (DBObject) this.mongoSessionConverter.convert(session,
TypeDescriptor.valueOf(MongoSession.class),
TypeDescriptor.valueOf(DBObject.class));
}
public void setMongoSessionConverter(AbstractMongoSessionConverter mongoSessionConverter) {
this.mongoSessionConverter = mongoSessionConverter;
}

View File

@@ -21,7 +21,6 @@ import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
@@ -67,9 +66,9 @@ public class MongoSession implements Session {
return this.id;
}
@SuppressWarnings("unchecked")
public <T> Optional<T> getAttribute(String attributeName) {
return Optional.ofNullable((T) this.attrs.get(coverDot(attributeName)));
@Override
public <T> T getAttribute(String attributeName) {
return (T) this.attrs.get(coverDot(attributeName));
}
public Set<String> getAttributeNames() {

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2017 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.data.mongo;
import org.bson.Document;
import org.springframework.core.convert.TypeDescriptor;
import com.mongodb.DBObject;
/**
* @author Greg Turnquist
*/
public final class MongoSessionUtils {
static DBObject convertToDBObject(AbstractMongoSessionConverter mongoSessionConverter, MongoSession session) {
return (DBObject) mongoSessionConverter.convert(session,
TypeDescriptor.valueOf(MongoSession.class),
TypeDescriptor.valueOf(DBObject.class));
}
static MongoSession convertToSession(AbstractMongoSessionConverter mongoSessionConverter, Document session) {
return (MongoSession) mongoSessionConverter.convert(session,
TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class));
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2017 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.data.mongo;
import static org.springframework.session.data.mongo.MongoSessionUtils.*;
import java.time.Duration;
import org.bson.Document;
import reactor.core.publisher.Mono;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import org.springframework.session.ReactorSessionRepository;
/**
* @author Greg Turnquist
*/
public class ReactiveMongoOperationsSessionRepository implements ReactorSessionRepository<MongoSession> {
/**
* The default time period in seconds in which a session will expire.
*/
public static final int DEFAULT_INACTIVE_INTERVAL = 1800;
/**
* the default collection name for storing session.
*/
public static final String DEFAULT_COLLECTION_NAME = "sessions";
private final ReactiveMongoOperations mongoOperations;
private AbstractMongoSessionConverter mongoSessionConverter =
SessionConverterProvider.getDefaultMongoConverter();
private Integer maxInactiveIntervalInSeconds = DEFAULT_INACTIVE_INTERVAL;
private String collectionName = DEFAULT_COLLECTION_NAME;
public ReactiveMongoOperationsSessionRepository(ReactiveMongoOperations mongoOperations) {
this.mongoOperations = mongoOperations;
}
/**
* Creates a new {@link MongoSession} that is capable of being persisted by this
* {@link ReactorSessionRepository}.
* <p>
* This allows optimizations and customizations in how the {@link MongoSession} is
* persisted. For example, the implementation returned might keep track of the changes
* ensuring that only the delta needs to be persisted on a save.
* </p>
*
* @return a new {@link MongoSession} that is capable of being persisted by this
* {@link ReactorSessionRepository}
*/
@Override
public Mono<MongoSession> createSession() {
return Mono.defer(() -> {
MongoSession session = new MongoSession();
if (this.maxInactiveIntervalInSeconds != null) {
session.setMaxInactiveInterval(Duration.ofSeconds(this.maxInactiveIntervalInSeconds));
}
return Mono.just(session);
});
}
/**
* Ensures the {@link MongoSession} created by
* {@link ReactorSessionRepository#createSession()} is saved.
* <p>
* Some implementations may choose to save as the {@link MongoSession} is updated by
* returning a {@link MongoSession} that immediately persists any changes. In this case,
* this method may not actually do anything.
* </p>
*
* @param session the {@link MongoSession} to save
*/
@Override
public Mono<Void> save(MongoSession session) {
return this.mongoOperations
.save(convertToDBObject(this.mongoSessionConverter, session), this.collectionName)
.then();
}
/**
* Gets the {@link MongoSession} by the {@link MongoSession#getId()} or null if no
* {@link MongoSession} is found.
*
* @param id the {@link MongoSession#getId()} to lookup
* @return the {@link MongoSession} by the {@link MongoSession#getId()} or null if no
* {@link MongoSession} is found.
*/
@Override
public Mono<MongoSession> findById(String id) {
return findSession(id)
.map(document -> convertToSession(this.mongoSessionConverter, document))
.filter(mongoSession -> !mongoSession.isExpired())
.switchIfEmpty(Mono.defer(() -> delete(id).then(Mono.empty())));
}
/**
* Deletes the {@link MongoSession} with the given {@link MongoSession#getId()} or does nothing
* if the {@link MongoSession} is not found.
*
* @param id the {@link MongoSession#getId()} to delete
*/
@Override
public Mono<Void> delete(String id) {
return this.mongoOperations.remove(findSession(id), this.collectionName).then();
}
Mono<Document> findSession(String id) {
return this.mongoOperations.findById(id, Document.class, this.collectionName);
}
public void setMongoSessionConverter(AbstractMongoSessionConverter mongoSessionConverter) {
this.mongoSessionConverter = mongoSessionConverter;
}
public void setMaxInactiveIntervalInSeconds(Integer maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public void setCollectionName(String collectionName) {
this.collectionName = collectionName;
}
}

View File

@@ -17,8 +17,6 @@ package org.springframework.session.data.mongo;
import static org.assertj.core.api.Assertions.*;
import java.util.Optional;
import org.junit.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -39,7 +37,7 @@ public class AuthenticationParserTest {
context.setAuthentication(new UsernamePasswordAuthenticationToken(principalName, null));
// when
String extractedName = AuthenticationParser.extractName(Optional.ofNullable(context));
String extractedName = AuthenticationParser.extractName(context);
// then
assertThat(extractedName).isEqualTo(principalName);

View File

@@ -0,0 +1,191 @@
/*
* Copyright 2014-2017 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.data.mongo;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;
import static org.mockito.Mockito.verify;
import java.util.UUID;
import org.bson.Document;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.data.mongodb.core.ReactiveMongoOperations;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import com.mongodb.client.result.DeleteResult;
/**
* Tests for {@link ReactiveMongoOperationsSessionRepository}.
*
* @author Jakub Kubrynski
* @author Vedran Pavic
* @author Greg Turnquist
*/
@RunWith(MockitoJUnitRunner.Silent.class)
public class ReactiveMongoOperationsSessionRepositoryTest {
@Mock
private AbstractMongoSessionConverter converter;
@Mock
private ReactiveMongoOperations mongoOperations;
private ReactiveMongoOperationsSessionRepository repository;
@Before
public void setUp() throws Exception {
this.repository = new ReactiveMongoOperationsSessionRepository(this.mongoOperations);
this.repository.setMongoSessionConverter(this.converter);
}
@Test
public void shouldCreateSession() throws Exception {
// when
Mono<MongoSession> session = this.repository.createSession();
// then
StepVerifier.create(session)
.expectNextMatches(mongoSession -> {
assertThat(mongoSession.getId()).isNotEmpty();
assertThat(mongoSession.getMaxInactiveInterval().getSeconds())
.isEqualTo(ReactiveMongoOperationsSessionRepository.DEFAULT_INACTIVE_INTERVAL);
return true;
});
}
@Test
public void shouldCreateSessionWhenMaxInactiveIntervalNotDefined() throws Exception {
// when
this.repository.setMaxInactiveIntervalInSeconds(null);
Mono<MongoSession> session = this.repository.createSession();
// then
StepVerifier.create(session)
.expectNextMatches(mongoSession -> {
assertThat(mongoSession.getId()).isNotEmpty();
assertThat(mongoSession.getMaxInactiveInterval().getSeconds())
.isEqualTo(ReactiveMongoOperationsSessionRepository.DEFAULT_INACTIVE_INTERVAL);
return true;
});
}
@Test
public void shouldSaveSession() throws Exception {
// given
MongoSession session = new MongoSession();
BasicDBObject dbSession = new BasicDBObject();
given(this.converter.convert(session,
TypeDescriptor.valueOf(MongoSession.class),
TypeDescriptor.valueOf(DBObject.class))).willReturn(dbSession);
given(this.mongoOperations.save(dbSession, "sessions")).willReturn(Mono.just(dbSession));
// when
StepVerifier.create(this.repository.save(session))
.expectNextMatches(aVoid -> {
// then
verify(this.mongoOperations).save(dbSession, ReactiveMongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME);
return true;
});
}
@Test
public void shouldGetSession() throws Exception {
// given
String sessionId = UUID.randomUUID().toString();
Document sessionDocument = new Document();
given(this.mongoOperations.findById(sessionId, Document.class,
ReactiveMongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME)).willReturn(Mono.just(sessionDocument));
MongoSession session = new MongoSession();
given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class))).willReturn(session);
// when
StepVerifier.create(this.repository.findById(sessionId))
.expectNextMatches(retrievedSession -> {
// then
assertThat(retrievedSession).isEqualTo(session);
return true;
});
}
@Test
public void shouldHandleExpiredSession() throws Exception {
// given
String sessionId = UUID.randomUUID().toString();
Document sessionDocument = new Document();
given(this.mongoOperations.findById(sessionId, Document.class,
ReactiveMongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME)).willReturn(Mono.just(sessionDocument));
MongoSession session = mock(MongoSession.class);
given(session.isExpired()).willReturn(true);
given(this.converter.convert(sessionDocument, TypeDescriptor.valueOf(Document.class),
TypeDescriptor.valueOf(MongoSession.class))).willReturn(session);
// when
StepVerifier.create(this.repository.findById(sessionId))
.expectNextMatches(mongoSession -> {
// then
verify(this.mongoOperations).remove(any(Document.class),
eq(ReactiveMongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME));
return true;
});
}
@Test
public void shouldDeleteSession() throws Exception {
// given
String sessionId = UUID.randomUUID().toString();
Document sessionDocument = new Document();
given(this.mongoOperations.findById(eq(sessionId), eq(Document.class),
eq(ReactiveMongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME))).willReturn(Mono.just(sessionDocument));
given(this.mongoOperations.remove((Mono<? extends Object>) any(), eq("sessions"))).willReturn(Mono.just(DeleteResult.acknowledged(1)));
// when
StepVerifier.create(this.repository.delete(sessionId))
.expectNextMatches(aVoid -> {
// then
verify(this.mongoOperations).remove(any(Document.class),
eq(ReactiveMongoOperationsSessionRepository.DEFAULT_COLLECTION_NAME));
return true;
});
}
}

View File

@@ -22,7 +22,6 @@ import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import de.flapdoodle.embed.mongo.MongodExecutable;
@@ -108,8 +107,8 @@ abstract public class AbstractMongoRepositoryITest extends AbstractITest {
Session session = this.repository.findById(toSave.getId());
assertThat(session.getAttributeNames().size()).isEqualTo(2);
assertThat(session.<String>getAttribute("a")).isEqualTo(Optional.of("b"));
assertThat(session.<String>getAttribute("1")).isEqualTo(Optional.of("2"));
assertThat(session.<String>getAttribute("a")).isEqualTo("b");
assertThat(session.<String>getAttribute("1")).isEqualTo("2");
this.repository.deleteById(toSave.getId());
}