diff --git a/pom.xml b/pom.xml index 8eabe76..505c401 100644 --- a/pom.xml +++ b/pom.xml @@ -6,6 +6,12 @@ spring-session-data-mongodb 2.0.0.BUILD-SNAPSHOT + + org.springframework.data + spring-data-mongodb-parent + 2.0.0.BUILD-SNAPSHOT + + Spring Session MongoDB https://github.com/spring-projects/spring-session-data-mongodb @@ -66,18 +72,19 @@ + 1.8 UTF-8 + 3.6.2 1.50.5 1.3 2.9.0.pr3 4.12 2.7.22 + Bismuth-M2 5.0.0.RC2 Kay-M4 - 2.0.0.BUILD-SNAPSHOT - 1.8 - 3.6.2 5.0.0.M2 + 2.0.0.BUILD-SNAPSHOT @@ -423,6 +430,14 @@ pom import + + + io.projectreactor + reactor-bom + ${reactor.version} + pom + import + @@ -437,14 +452,36 @@ org.springframework.data spring-data-mongodb + + + org.mongodb + mongo-java-driver + + + org.slf4j + jcl-over-slf4j + + + + org.mongodb + mongodb-driver + ${mongo} + + com.fasterxml.jackson.core jackson-databind ${jackson.version} + + io.projectreactor + reactor-core + true + + @@ -498,6 +535,33 @@ test + + io.projectreactor + reactor-test + test + + + + org.mongodb + mongodb-driver + ${mongo} + test + + + + org.mongodb + mongodb-driver-async + ${mongo} + test + + + + org.mongodb + mongodb-driver-reactivestreams + ${mongo.reactivestreams} + test + + javax.servlet javax.servlet-api diff --git a/src/main/java/org/springframework/session/data/mongo/AbstractMongoSessionConverter.java b/src/main/java/org/springframework/session/data/mongo/AbstractMongoSessionConverter.java index 3832c23..25df507 100644 --- a/src/main/java/org/springframework/session/data/mongo/AbstractMongoSessionConverter.java +++ b/src/main/java/org/springframework/session/data/mongo/AbstractMongoSessionConverter.java @@ -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); } } diff --git a/src/main/java/org/springframework/session/data/mongo/AuthenticationParser.java b/src/main/java/org/springframework/session/data/mongo/AuthenticationParser.java index ee82371..816fb04 100644 --- a/src/main/java/org/springframework/session/data/mongo/AuthenticationParser.java +++ b/src/main/java/org/springframework/session/data/mongo/AuthenticationParser.java @@ -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 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() {} diff --git a/src/main/java/org/springframework/session/data/mongo/JdkMongoSessionConverter.java b/src/main/java/org/springframework/session/data/mongo/JdkMongoSessionConverter.java index 582d5c5..ae1e922 100644 --- a/src/main/java/org/springframework/session/data/mongo/JdkMongoSessionConverter.java +++ b/src/main/java/org/springframework/session/data/mongo/JdkMongoSessionConverter.java @@ -132,7 +132,7 @@ public class JdkMongoSessionConverter extends AbstractMongoSessionConverter { Map 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); diff --git a/src/main/java/org/springframework/session/data/mongo/MongoOperationsSessionRepository.java b/src/main/java/org/springframework/session/data/mongo/MongoOperationsSessionRepository.java index b5a3dac..8ac2a69 100644 --- a/src/main/java/org/springframework/session/data/mongo/MongoOperationsSessionRepository.java +++ b/src/main/java/org/springframework/session/data/mongo/MongoOperationsSessionRepository.java @@ -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 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; } diff --git a/src/main/java/org/springframework/session/data/mongo/MongoSession.java b/src/main/java/org/springframework/session/data/mongo/MongoSession.java index 30505f2..0bece85 100644 --- a/src/main/java/org/springframework/session/data/mongo/MongoSession.java +++ b/src/main/java/org/springframework/session/data/mongo/MongoSession.java @@ -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 Optional getAttribute(String attributeName) { - return Optional.ofNullable((T) this.attrs.get(coverDot(attributeName))); + @Override + public T getAttribute(String attributeName) { + return (T) this.attrs.get(coverDot(attributeName)); } public Set getAttributeNames() { diff --git a/src/main/java/org/springframework/session/data/mongo/MongoSessionUtils.java b/src/main/java/org/springframework/session/data/mongo/MongoSessionUtils.java new file mode 100644 index 0000000..e8f2707 --- /dev/null +++ b/src/main/java/org/springframework/session/data/mongo/MongoSessionUtils.java @@ -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)); + } + +} diff --git a/src/main/java/org/springframework/session/data/mongo/ReactiveMongoOperationsSessionRepository.java b/src/main/java/org/springframework/session/data/mongo/ReactiveMongoOperationsSessionRepository.java new file mode 100644 index 0000000..c1e3c15 --- /dev/null +++ b/src/main/java/org/springframework/session/data/mongo/ReactiveMongoOperationsSessionRepository.java @@ -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 { + + /** + * 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}. + *

+ * 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. + *

+ * + * @return a new {@link MongoSession} that is capable of being persisted by this + * {@link ReactorSessionRepository} + */ + @Override + public Mono 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. + *

+ * 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. + *

+ * + * @param session the {@link MongoSession} to save + */ + @Override + public Mono 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 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 delete(String id) { + return this.mongoOperations.remove(findSession(id), this.collectionName).then(); + } + + Mono 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; + } + +} diff --git a/src/test/java/org/springframework/session/data/mongo/AuthenticationParserTest.java b/src/test/java/org/springframework/session/data/mongo/AuthenticationParserTest.java index 1ccd6b1..f4d76a6 100644 --- a/src/test/java/org/springframework/session/data/mongo/AuthenticationParserTest.java +++ b/src/test/java/org/springframework/session/data/mongo/AuthenticationParserTest.java @@ -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); diff --git a/src/test/java/org/springframework/session/data/mongo/ReactiveMongoOperationsSessionRepositoryTest.java b/src/test/java/org/springframework/session/data/mongo/ReactiveMongoOperationsSessionRepositoryTest.java new file mode 100644 index 0000000..2b3ca4e --- /dev/null +++ b/src/test/java/org/springframework/session/data/mongo/ReactiveMongoOperationsSessionRepositoryTest.java @@ -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 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 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) 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; + }); + } +} diff --git a/src/test/java/org/springframework/session/data/mongo/integration/AbstractMongoRepositoryITest.java b/src/test/java/org/springframework/session/data/mongo/integration/AbstractMongoRepositoryITest.java index ce799c6..9c74dce 100644 --- a/src/test/java/org/springframework/session/data/mongo/integration/AbstractMongoRepositoryITest.java +++ b/src/test/java/org/springframework/session/data/mongo/integration/AbstractMongoRepositoryITest.java @@ -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.getAttribute("a")).isEqualTo(Optional.of("b")); - assertThat(session.getAttribute("1")).isEqualTo(Optional.of("2")); + assertThat(session.getAttribute("a")).isEqualTo("b"); + assertThat(session.getAttribute("1")).isEqualTo("2"); this.repository.deleteById(toSave.getId()); }