From e23398b89041414c39c997450e798ced418faf63 Mon Sep 17 00:00:00 2001 From: Jakub Kubrynski Date: Sun, 13 Mar 2016 23:09:17 +0100 Subject: [PATCH] Add JacksonMongoSessionConverter Fixes gh-416 --- ...ava => AbstractMongoRepositoryITests.java} | 66 +--------- .../mongo/MongoRepositoryJacksonITests.java | 70 ++++++++++ ...MongoRepositoryJdkSerializationITests.java | 91 +++++++++++++ .../mongo/AbstractMongoSessionConverter.java | 42 ++++++ .../mongo/JacksonMongoSessionConverter.java | 121 ++++++++++++++++++ .../data/mongo/JdkMongoSessionConverter.java | 64 +++------ .../data/mongo/MongoExpiringSession.java | 29 ++++- .../MongoOperationsSessionRepository.java | 3 +- .../data/mongo/SessionConverterProvider.java | 41 ++++++ .../JacksonMongoSessionConverterTest.java | 53 ++++++++ 10 files changed, 462 insertions(+), 118 deletions(-) rename spring-session/src/integration-test/java/org/springframework/session/data/mongo/{MongoRepositoryITests.java => AbstractMongoRepositoryITests.java} (84%) create mode 100644 spring-session/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJacksonITests.java create mode 100644 spring-session/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJdkSerializationITests.java create mode 100644 spring-session/src/main/java/org/springframework/session/data/mongo/JacksonMongoSessionConverter.java create mode 100644 spring-session/src/main/java/org/springframework/session/data/mongo/SessionConverterProvider.java create mode 100644 spring-session/src/test/java/org/springframework/session/data/mongo/JacksonMongoSessionConverterTest.java diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryITests.java b/spring-session/src/integration-test/java/org/springframework/session/data/mongo/AbstractMongoRepositoryITests.java similarity index 84% rename from spring-session/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryITests.java rename to spring-session/src/integration-test/java/org/springframework/session/data/mongo/AbstractMongoRepositoryITests.java index 4594b97..2477815 100644 --- a/spring-session/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryITests.java +++ b/spring-session/src/integration-test/java/org/springframework/session/data/mongo/AbstractMongoRepositoryITests.java @@ -15,19 +15,13 @@ */ package org.springframework.session.data.mongo; -import java.net.UnknownHostException; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; -import com.mongodb.MongoClient; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; @@ -36,20 +30,17 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.Session; import org.springframework.session.data.AbstractITests; -import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession; -import org.springframework.test.context.ContextConfiguration; import static org.assertj.core.api.Assertions.assertThat; /** * @author Jakub Kubrynski */ -@ContextConfiguration -public class MongoRepositoryITests extends AbstractITests { +abstract public class AbstractMongoRepositoryITests extends AbstractITests { - private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; + protected static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; - private static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; + protected static final String INDEX_NAME = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; @Autowired protected MongoOperationsSessionRepository repository; @@ -290,26 +281,6 @@ public class MongoRepositoryITests extends AbstractITests { assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); } - @Test - public void findByPrincipalNameNoSecurityPrincipalNameChangeReload() - throws Exception { - MongoExpiringSession toSave = this.repository.createSession(); - toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); - - this.repository.save(toSave); - - toSave = this.repository.getSession(toSave.getId()); - - toSave.setAttribute("other", "value"); - this.repository.save(toSave); - - Map findByPrincipalName = this.repository - .findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName()); - - assertThat(findByPrincipalName).hasSize(1); - assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); - } - @Test public void findByDeletedSecurityPrincipalName() throws Exception { MongoExpiringSession toSave = this.repository.createSession(); @@ -347,23 +318,6 @@ public class MongoRepositoryITests extends AbstractITests { assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); } - @Test - public void findByDeletedSecurityPrincipalNameReload() throws Exception { - MongoExpiringSession toSave = this.repository.createSession(); - toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); - - this.repository.save(toSave); - - MongoExpiringSession getSession = this.repository.getSession(toSave.getId()); - getSession.setAttribute(INDEX_NAME, null); - this.repository.save(getSession); - - Map findByPrincipalName = this.repository - .findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName()); - - assertThat(findByPrincipalName).isEmpty(); - } - @Test public void findByChangedSecurityPrincipalNameReload() throws Exception { MongoExpiringSession toSave = this.repository.createSession(); @@ -402,22 +356,12 @@ public class MongoRepositoryITests extends AbstractITests { assertThat(expiredSessionFromDb).isNull(); } - private String getSecurityName() { + protected String getSecurityName() { return this.context.getAuthentication().getName(); } - private String getChangedSecurityName() { + protected String getChangedSecurityName() { return this.changedContext.getAuthentication().getName(); } - @Configuration - @EnableMongoHttpSession - static class Config { - - @Bean - public MongoOperations mongoOperations() throws UnknownHostException { - return new MongoTemplate(new MongoClient(), "test"); - } - - } } diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJacksonITests.java b/spring-session/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJacksonITests.java new file mode 100644 index 0000000..7170bc1 --- /dev/null +++ b/spring-session/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJacksonITests.java @@ -0,0 +1,70 @@ +/* + * 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.data.mongo; + +import java.net.UnknownHostException; +import java.util.Map; +import java.util.UUID; + +import com.mongodb.MongoClient; +import org.junit.Test; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jakub Kubrynski + */ +@ContextConfiguration +public class MongoRepositoryJacksonITests extends AbstractMongoRepositoryITests { + + @Test + public void findByCustomIndex() throws Exception { + MongoExpiringSession toSave = this.repository.createSession(); + String cartId = "cart-" + UUID.randomUUID(); + toSave.setAttribute("cartId", cartId); + + this.repository.save(toSave); + + Map findByCartId = this.repository + .findByIndexNameAndIndexValue("cartId", cartId); + + assertThat(findByCartId).hasSize(1); + assertThat(findByCartId.keySet()).containsOnly(toSave.getId()); + } + + @Configuration + @EnableMongoHttpSession + static class Config { + + @Bean + public MongoOperations mongoOperations() throws UnknownHostException { + return new MongoTemplate(new MongoClient(), "test"); + } + + @Bean + public AbstractMongoSessionConverter mongoSessionConverter() { + return new JacksonMongoSessionConverter(); + } + + } +} diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJdkSerializationITests.java b/spring-session/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJdkSerializationITests.java new file mode 100644 index 0000000..99dd16b --- /dev/null +++ b/spring-session/src/integration-test/java/org/springframework/session/data/mongo/MongoRepositoryJdkSerializationITests.java @@ -0,0 +1,91 @@ +/* + * 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.data.mongo; + +import java.net.UnknownHostException; +import java.util.Map; + +import com.mongodb.MongoClient; +import org.junit.Test; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.session.data.mongo.config.annotation.web.http.EnableMongoHttpSession; +import org.springframework.test.context.ContextConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jakub Kubrynski + */ +@ContextConfiguration +public class MongoRepositoryJdkSerializationITests extends AbstractMongoRepositoryITests { + + @Test + public void findByDeletedSecurityPrincipalNameReload() throws Exception { + MongoExpiringSession toSave = this.repository.createSession(); + toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); + + this.repository.save(toSave); + + MongoExpiringSession getSession = this.repository.getSession(toSave.getId()); + getSession.setAttribute(INDEX_NAME, null); + this.repository.save(getSession); + + Map findByPrincipalName = this.repository + .findByIndexNameAndIndexValue(INDEX_NAME, getChangedSecurityName()); + + assertThat(findByPrincipalName).isEmpty(); + } + + @Test + public void findByPrincipalNameNoSecurityPrincipalNameChangeReload() + throws Exception { + MongoExpiringSession toSave = this.repository.createSession(); + toSave.setAttribute(SPRING_SECURITY_CONTEXT, this.context); + + this.repository.save(toSave); + + toSave = this.repository.getSession(toSave.getId()); + + toSave.setAttribute("other", "value"); + this.repository.save(toSave); + + Map findByPrincipalName = this.repository + .findByIndexNameAndIndexValue(INDEX_NAME, getSecurityName()); + + assertThat(findByPrincipalName).hasSize(1); + assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId()); + } + + @Configuration + @EnableMongoHttpSession + static class Config { + + @Bean + public MongoOperations mongoOperations() throws UnknownHostException { + return new MongoTemplate(new MongoClient(), "test"); + } + + @Bean + public AbstractMongoSessionConverter mongoSessionConverter() { + return new JdkMongoSessionConverter(); + } + + } +} diff --git a/spring-session/src/main/java/org/springframework/session/data/mongo/AbstractMongoSessionConverter.java b/spring-session/src/main/java/org/springframework/session/data/mongo/AbstractMongoSessionConverter.java index e39d60b..a0e6b96 100644 --- a/spring-session/src/main/java/org/springframework/session/data/mongo/AbstractMongoSessionConverter.java +++ b/spring-session/src/main/java/org/springframework/session/data/mongo/AbstractMongoSessionConverter.java @@ -15,17 +15,23 @@ */ package org.springframework.session.data.mongo; +import java.util.Collections; import java.util.List; +import java.util.Set; +import com.mongodb.DBObject; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.IndexOperations; import org.springframework.data.mongodb.core.index.Index; import org.springframework.data.mongodb.core.index.IndexInfo; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.Session; /** * Base class for serializing and deserializing session objects. To create custom @@ -40,6 +46,7 @@ public abstract class AbstractMongoSessionConverter implements GenericConverter private static final Log LOG = LogFactory.getLog(AbstractMongoSessionConverter.class); protected static final String EXPIRE_AT_FIELD_NAME = "expireAt"; + private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; /** * Returns query to be executed to return sessions based on a particular index. @@ -72,4 +79,39 @@ public abstract class AbstractMongoSessionConverter implements GenericConverter .ensureIndex(new Index(EXPIRE_AT_FIELD_NAME, Sort.Direction.ASC) .named(EXPIRE_AT_FIELD_NAME).expire(0)); } + + protected String extractPrincipal(Session expiringSession) { + String resolvedPrincipal = AuthenticationParser + .extractName(expiringSession.getAttribute(SPRING_SECURITY_CONTEXT)); + if (resolvedPrincipal != null) { + return resolvedPrincipal; + } + else { + return expiringSession.getAttribute( + FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME); + } + } + + public Set getConvertibleTypes() { + return Collections.singleton( + new ConvertiblePair(DBObject.class, MongoExpiringSession.class)); + } + + public Object convert(Object source, TypeDescriptor sourceType, + TypeDescriptor targetType) { + if (source == null) { + return null; + } + + if (DBObject.class.isAssignableFrom(sourceType.getType())) { + return convert((DBObject) source); + } + else { + return convert((MongoExpiringSession) source); + } + } + + protected abstract DBObject convert(MongoExpiringSession session); + + protected abstract MongoExpiringSession convert(DBObject sessionWrapper); } diff --git a/spring-session/src/main/java/org/springframework/session/data/mongo/JacksonMongoSessionConverter.java b/spring-session/src/main/java/org/springframework/session/data/mongo/JacksonMongoSessionConverter.java new file mode 100644 index 0000000..24b770f --- /dev/null +++ b/spring-session/src/main/java/org/springframework/session/data/mongo/JacksonMongoSessionConverter.java @@ -0,0 +1,121 @@ +/* + * 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.data.mongo; + +import java.io.IOException; +import java.util.Collections; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.mongodb.DBObject; +import com.mongodb.util.JSON; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.session.FindByIndexNameSessionRepository; + +/** + * {@code AbstractMongoSessionConverter} implementation using Jackson. + * + * @author Jakub Kubrynski + * @since 1.2 + */ +public class JacksonMongoSessionConverter extends AbstractMongoSessionConverter { + + private static final Log LOG = LogFactory.getLog(JacksonMongoSessionConverter.class); + + private static final String ATTRS_FIELD_NAME = "attrs."; + private static final String PRINCIPAL_FIELD_NAME = "principal"; + + private final ObjectMapper objectMapper; + + public JacksonMongoSessionConverter() { + this(Collections.emptyList()); + } + + public JacksonMongoSessionConverter(Iterable modules) { + this.objectMapper = buildObjectMapper(); + this.objectMapper.registerModules(modules); + } + + protected Query getQueryForIndex(String indexName, Object indexValue) { + if (FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME + .equals(indexName)) { + return Query.query(Criteria.where(PRINCIPAL_FIELD_NAME).is(indexValue)); + } + return Query.query(Criteria.where(ATTRS_FIELD_NAME + + MongoExpiringSession.coverDot(indexName)).is(indexValue)); + } + + private ObjectMapper buildObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + + // serialize fields instead of properties + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); + objectMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + + // ignore unresolved fields (mostly 'principal') + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + objectMapper.setPropertyNamingStrategy(new MongoIdNamingStrategy()); + return objectMapper; + } + + @Override + protected DBObject convert(MongoExpiringSession source) { + try { + DBObject dbSession = (DBObject) JSON.parse(this.objectMapper.writeValueAsString(source)); + dbSession.put(PRINCIPAL_FIELD_NAME, extractPrincipal(source)); + return dbSession; + } + catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot convert MongoExpiringSession", e); + } + } + + @Override + protected MongoExpiringSession convert(DBObject source) { + String json = JSON.serialize(source); + try { + return this.objectMapper.readValue(json, MongoExpiringSession.class); + } + catch (IOException e) { + LOG.error("Error during Mongo Session deserialization", e); + return null; + } + } + + private static class MongoIdNamingStrategy extends PropertyNamingStrategy.PropertyNamingStrategyBase { + + @Override + public String translate(String propertyName) { + if (propertyName.equals("id")) { + return "_id"; + } + else if (propertyName.equals("_id")) { + return "id"; + } + return propertyName; + } + } +} diff --git a/spring-session/src/main/java/org/springframework/session/data/mongo/JdkMongoSessionConverter.java b/spring-session/src/main/java/org/springframework/session/data/mongo/JdkMongoSessionConverter.java index 52b56b3..4273ded 100644 --- a/spring-session/src/main/java/org/springframework/session/data/mongo/JdkMongoSessionConverter.java +++ b/spring-session/src/main/java/org/springframework/session/data/mongo/JdkMongoSessionConverter.java @@ -20,26 +20,22 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; -import java.util.Set; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.core.convert.TypeDescriptor; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.Session; /** - * {@code AbstractMongoSessionConverter} implementation transforming. - * {@code MongoExpiringSession} to/from a BSON object using standard Java serialization + * {@code AbstractMongoSessionConverter} implementation using standard Java serialization. * * @author Jakub Kubrynski * @since 1.2 @@ -55,7 +51,6 @@ class JdkMongoSessionConverter extends AbstractMongoSessionConverter { private static final String ATTRIBUTES = "attr"; private static final String PRINCIPAL_FIELD_NAME = "principal"; - private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; @Override public Query getQueryForIndex(String indexName, Object indexValue) { @@ -66,26 +61,8 @@ class JdkMongoSessionConverter extends AbstractMongoSessionConverter { return null; } - public Set getConvertibleTypes() { - return Collections.singleton( - new ConvertiblePair(DBObject.class, MongoExpiringSession.class)); - } - - public Object convert(Object source, TypeDescriptor sourceType, - TypeDescriptor targetType) { - if (source == null) { - return null; - } - - if (DBObject.class.isAssignableFrom(sourceType.getType())) { - return convert((DBObject) source); - } - else { - return convert((MongoExpiringSession) source); - } - } - - private DBObject convert(MongoExpiringSession session) { + @Override + protected DBObject convert(MongoExpiringSession session) { BasicDBObject basicDBObject = new BasicDBObject(); basicDBObject.put(ID, session.getId()); basicDBObject.put(CREATION_TIME, session.getCreationTime()); @@ -97,6 +74,18 @@ class JdkMongoSessionConverter extends AbstractMongoSessionConverter { return basicDBObject; } + @Override + protected MongoExpiringSession convert(DBObject sessionWrapper) { + MongoExpiringSession session = new MongoExpiringSession( + (String) sessionWrapper.get(ID), + (Integer) sessionWrapper.get(MAX_INTERVAL)); + session.setCreationTime((Long) sessionWrapper.get(CREATION_TIME)); + session.setLastAccessedTime((Long) sessionWrapper.get(LAST_ACCESSED_TIME)); + session.setExpireAt((Date) sessionWrapper.get(EXPIRE_AT_FIELD_NAME)); + deserializeAttributes(sessionWrapper, session); + return session; + } + private byte[] serializeAttributes(Session session) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -115,29 +104,6 @@ class JdkMongoSessionConverter extends AbstractMongoSessionConverter { } } - private String extractPrincipal(Session expiringSession) { - String resolvedPrincipal = AuthenticationParser - .extractName(expiringSession.getAttribute(SPRING_SECURITY_CONTEXT)); - if (resolvedPrincipal != null) { - return resolvedPrincipal; - } - else { - return expiringSession.getAttribute( - FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME); - } - } - - private MongoExpiringSession convert(DBObject sessionWrapper) { - MongoExpiringSession session = new MongoExpiringSession( - (String) sessionWrapper.get(ID), - (Integer) sessionWrapper.get(MAX_INTERVAL)); - session.setCreationTime((Long) sessionWrapper.get(CREATION_TIME)); - session.setLastAccessedTime((Long) sessionWrapper.get(LAST_ACCESSED_TIME)); - session.setExpireAt((Date) sessionWrapper.get(EXPIRE_AT_FIELD_NAME)); - deserializeAttributes(sessionWrapper, session); - return session; - } - @SuppressWarnings("unchecked") private void deserializeAttributes(DBObject sessionWrapper, Session session) { try { diff --git a/spring-session/src/main/java/org/springframework/session/data/mongo/MongoExpiringSession.java b/spring-session/src/main/java/org/springframework/session/data/mongo/MongoExpiringSession.java index 7724e2a..57945a1 100644 --- a/spring-session/src/main/java/org/springframework/session/data/mongo/MongoExpiringSession.java +++ b/spring-session/src/main/java/org/springframework/session/data/mongo/MongoExpiringSession.java @@ -17,6 +17,7 @@ package org.springframework.session.data.mongo; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -32,6 +33,11 @@ import org.springframework.session.ExpiringSession; */ public class MongoExpiringSession implements ExpiringSession { + /** + * Mongo doesn't support {@literal dot} in field names. We replace it with very rarely used character + */ + private static final char DOT_COVER_CHAR = '\uF607'; + private final String id; private long created = System.currentTimeMillis(); private long accessed; @@ -59,24 +65,28 @@ public class MongoExpiringSession implements ExpiringSession { @SuppressWarnings("unchecked") public T getAttribute(String attributeName) { - return (T) this.attrs.get(attributeName); + return (T) this.attrs.get(coverDot(attributeName)); } public Set getAttributeNames() { - return this.attrs.keySet(); + HashSet result = new HashSet(); + for (String key : this.attrs.keySet()) { + result.add(uncoverDot(key)); + } + return result; } public void setAttribute(String attributeName, Object attributeValue) { if (attributeValue == null) { - removeAttribute(attributeName); + removeAttribute(coverDot(attributeName)); } else { - this.attrs.put(attributeName, attributeValue); + this.attrs.put(coverDot(attributeName), attributeValue); } } public void removeAttribute(String attributeName) { - this.attrs.remove(attributeName); + this.attrs.remove(coverDot(attributeName)); } public long getCreationTime() { @@ -117,6 +127,14 @@ public class MongoExpiringSession implements ExpiringSession { this.expireAt = expireAt; } + static String coverDot(String attributeName) { + return attributeName.replace('.', DOT_COVER_CHAR); + } + + static String uncoverDot(String attributeName) { + return attributeName.replace(DOT_COVER_CHAR, '.'); + } + @Override public boolean equals(Object o) { if (this == o) { @@ -129,7 +147,6 @@ public class MongoExpiringSession implements ExpiringSession { MongoExpiringSession that = (MongoExpiringSession) o; return this.id.equals(that.id); - } @Override diff --git a/spring-session/src/main/java/org/springframework/session/data/mongo/MongoOperationsSessionRepository.java b/spring-session/src/main/java/org/springframework/session/data/mongo/MongoOperationsSessionRepository.java index f33b7e5..a6edc68 100644 --- a/spring-session/src/main/java/org/springframework/session/data/mongo/MongoOperationsSessionRepository.java +++ b/spring-session/src/main/java/org/springframework/session/data/mongo/MongoOperationsSessionRepository.java @@ -19,7 +19,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; - import javax.annotation.PostConstruct; import com.mongodb.DBObject; @@ -56,7 +55,7 @@ public class MongoOperationsSessionRepository private final MongoOperations mongoOperations; - private AbstractMongoSessionConverter mongoSessionConverter = new JdkMongoSessionConverter(); + private AbstractMongoSessionConverter mongoSessionConverter = SessionConverterProvider.get(); private Integer maxInactiveIntervalInSeconds = DEFAULT_INACTIVE_INTERVAL; private String collectionName = DEFAULT_COLLECTION_NAME; diff --git a/spring-session/src/main/java/org/springframework/session/data/mongo/SessionConverterProvider.java b/spring-session/src/main/java/org/springframework/session/data/mongo/SessionConverterProvider.java new file mode 100644 index 0000000..c17be3b --- /dev/null +++ b/spring-session/src/main/java/org/springframework/session/data/mongo/SessionConverterProvider.java @@ -0,0 +1,41 @@ +/* + * 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.data.mongo; + +import org.springframework.util.ClassUtils; + +/** + * Provider choosing proper AbstractMongoSessionConverter. + * + * @author Jakub Kubrynski + */ +final class SessionConverterProvider { + + private static final String JACKSON_CLASS_NAME = "com.fasterxml.jackson.databind.ObjectMapper"; + + private SessionConverterProvider() { + } + + static AbstractMongoSessionConverter get() { + if (ClassUtils.isPresent(JACKSON_CLASS_NAME, null)) { + return new JacksonMongoSessionConverter(); + } + else { + return new JdkMongoSessionConverter(); + } + } + +} diff --git a/spring-session/src/test/java/org/springframework/session/data/mongo/JacksonMongoSessionConverterTest.java b/spring-session/src/test/java/org/springframework/session/data/mongo/JacksonMongoSessionConverterTest.java new file mode 100644 index 0000000..dc3bbf1 --- /dev/null +++ b/spring-session/src/test/java/org/springframework/session/data/mongo/JacksonMongoSessionConverterTest.java @@ -0,0 +1,53 @@ +/* + * 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.data.mongo; + +import com.mongodb.DBObject; +import org.junit.Test; + +import org.springframework.data.mongodb.core.query.Query; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jakub Kubrynski + */ +public class JacksonMongoSessionConverterTest { + + JacksonMongoSessionConverter sut = new JacksonMongoSessionConverter(); + + @Test + public void shouldSaveIdField() throws Exception { + //given + MongoExpiringSession session = new MongoExpiringSession(); + + //when + DBObject convert = this.sut.convert(session); + + //then + assertThat(convert.get("_id")).isEqualTo(session.getId()); + assertThat(convert.get("id")).isNull(); + } + + @Test + public void shouldQueryAgainstAttribute() throws Exception { + //when + Query cart = this.sut.getQueryForIndex("cart", "my-cart"); + + //then + assertThat(cart.getQueryObject().get("attrs.cart")).isEqualTo("my-cart"); + } +}