diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index d5c2bef..cf0aeaf 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -257,6 +257,56 @@ Guide when integrating with your own application. include::guides/httpsession-gemfire-p2p-xml.adoc[tags=config,leveloffset=+3] +[[httpsessoin-gemfire-indexing]] +==== Using Indexes with GemFire + +While best practices concerning the proper definition of indexes that positively impact GemFire's performance is beyond +the scope of this document, it is important to realize that Spring Session Data GemFire creates and uses indexes to +query and find Sessions efficiently. + +Out-of-the-box, Spring Session Data GemFire creates 1 Hash-typed Index on the implementing Session's `principalName` +property, which corresponds to the Session attribute... + +---- + org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME +---- + +This enables developers using the `GemFireOperationsSessionRepository` programmatically to query and find all Sessions +with a given principal name efficiently. + +Additionally, Spring Session Data GemFire will create a Range-based Index on the implementing Session's Map-type +`attributes` property (i.e. on any arbitrary Session attribute) when a developer identifies 1 or more named Session +attributes that should be indexed by GemFire. + +Sessions attributes to index can be specified with the `indexableSessionAttributes` attribute on the `@EnableGemFireHttpSession` +annotation. A developer adds this annotation to their Spring application `@Configuration` class when s/he wishes to +enable Spring Session support for HttpSession backed by GemFire. + +For example: + +.ApplicationConfiguration.java +---- +@EnableGemFireHttpSession(indexableSessionAttributes = { "name1", "name2", "name3" }) +class ApplicationConfiguration { + + // @Bean definition methods implemented here +} +---- + +NOTE: Only Session attribute names identified in the `@EnableGemFireHttpSession` annotation's `indexableSessionAttributes` +attribute will have an Index defined. All other Session attributes will not be indexed. + +However, there is one caveat. Any values stored in indexable Session attributes must implement the `java.lang.Comparable` +interface. If those object values do not implement `Comparable`, then GemFire will throw an error on startup when the +Index is defined for Regions with persistent Session data, or when an attempt is made at runtime to assign the indexable +Session attribute a value that is not `Comparable` and the Session is saved to GemFire. + +NOTE: Any Session attribute that is not indexed may store non-`Comparable` values. + +To learn more about GemFire's Range-based Indexes, see http://gemfire.docs.pivotal.io/docs-gemfire/latest/developing/query_index/creating_map_indexes.html[Creating Indexes on Map Fields]. + +To learn more about GemFire Indexing in general, see http://gemfire.docs.pivotal.io/docs-gemfire/latest/developing/query_index/query_index.html[Working with Indexes]. + [[httpsession-how]] === How HttpSession Integration Works diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/AbstractGemFireIntegrationTests.java b/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/AbstractGemFireIntegrationTests.java index bc7021a..804296f 100644 --- a/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/AbstractGemFireIntegrationTests.java +++ b/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/AbstractGemFireIntegrationTests.java @@ -43,6 +43,7 @@ import com.gemstone.gemfire.cache.GemFireCache; import com.gemstone.gemfire.cache.Region; import com.gemstone.gemfire.cache.client.ClientCache; import com.gemstone.gemfire.cache.client.ClientCacheFactory; +import com.gemstone.gemfire.cache.query.Index; import com.gemstone.gemfire.cache.server.CacheServer; /** @@ -105,7 +106,7 @@ public class AbstractGemFireIntegrationTests { } /* (non-Javadoc) */ - protected static List createJavaProcessCommandLine(Class type, String... args) { + protected static List createJavaProcessCommandLine(Class type, String... args) { List commandLine = new ArrayList(); String javaHome = System.getProperty("java.home"); @@ -155,7 +156,7 @@ public class AbstractGemFireIntegrationTests { } /* (non-Javadoc) */ - protected static Process run(Class type, File directory, String... args) throws IOException { + protected static Process run(Class type, File directory, String... args) throws IOException { return new ProcessBuilder() .command(createJavaProcessCommandLine(type, args)) .directory(directory) @@ -318,6 +319,13 @@ public class AbstractGemFireIntegrationTests { assertThat(actualRegion.getAttributes().getDataPolicy()).isEqualTo(expectedDataPolicy); } + /* (non-Javadoc) */ + protected void assertIndex(Index index, String expectedExpression, String expectedFromClause) { + assertThat(index).isNotNull(); + assertThat(index.getIndexedExpression()).isEqualTo(expectedExpression); + assertThat(index.getFromClause()).isEqualTo(expectedFromClause); + } + /* (non-Javadoc) */ protected void assertEntryIdleTimeout(Region region, ExpirationAction expectedAction, int expectedTimeout) { assertEntryIdleTimeout(region.getAttributes().getEntryIdleTimeout(), expectedAction, expectedTimeout); diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryIntegrationTests.java b/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryIntegrationTests.java index 07ece8b..daa98c6 100644 --- a/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryIntegrationTests.java +++ b/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.session.data.gemfire; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; import static org.springframework.session.data.gemfire.GemFireOperationsSessionRepository.GemFireSession; import java.util.Arrays; @@ -72,11 +73,11 @@ import com.gemstone.gemfire.pdx.PdxWriter; @ContextConfiguration @WebAppConfiguration public class GemFireOperationsSessionRepositoryIntegrationTests extends AbstractGemFireIntegrationTests { - private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; private static final int MAX_INACTIVE_INTERVAL_IN_SECONDS = 300; private static final String GEMFIRE_LOG_LEVEL = "warning"; + private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; private static final String SPRING_SESSION_GEMFIRE_REGION_NAME = "TestPartitionedSessions"; SecurityContext context; @@ -102,11 +103,15 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract assertEntryIdleTimeout(sessionRegion, ExpirationAction.INVALIDATE, MAX_INACTIVE_INTERVAL_IN_SECONDS); } - protected Map doFindByPrincipalName(String principalName) { - return gemfireSessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName); + protected Map doFindByIndexNameAndIndexValue(String indexName, String indexValue) { + return gemfireSessionRepository.findByIndexNameAndIndexValue(indexName, indexValue); } - @SuppressWarnings("unchecked") + protected Map doFindByPrincipalName(String principalName) { + return doFindByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName); + } + + @SuppressWarnings({ "unused", "unchecked" }) protected Map doFindByPrincipalName(String regionName, String principalName) { try { Region region = gemfireCache.getRegion(regionName); @@ -115,7 +120,8 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract QueryService queryService = region.getRegionService().getQueryService(); - String queryString = String.format("SELECT s FROM %1$s s WHERE s.principalName = $1", region.getFullPath()); + String queryString = String.format(GemFireOperationsSessionRepository.FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, + region.getFullPath()); Query query = queryService.newQuery(queryString); @@ -140,6 +146,56 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract return true; } + protected ExpiringSession setAttribute(ExpiringSession session, String attributeName, Object attributeValue) { + session.setAttribute(attributeName, attributeValue); + return session; + } + + @Test + public void findSessionsByIndexedSessionAttributeNameValues() { + ExpiringSession johnBlumSession = save(touch(setAttribute(createSession("johnBlum"), "vip", "yes"))); + ExpiringSession robWinchSession = save(touch(setAttribute(createSession("robWinch"), "vip", "yes"))); + ExpiringSession jonDoeSession = save(touch(setAttribute(createSession("jonDoe"), "vip", "no"))); + ExpiringSession pieDoeSession = save(touch(setAttribute(createSession("pieDoe"), "viper", "true"))); + ExpiringSession sourDoeSession = save(touch(createSession("sourDoe"))); + + assertThat(get(johnBlumSession.getId())).isEqualTo(johnBlumSession); + assertThat(johnBlumSession.getAttribute("vip")).isEqualTo("yes"); + assertThat(get(robWinchSession.getId())).isEqualTo(robWinchSession); + assertThat(robWinchSession.getAttribute("vip")).isEqualTo("yes"); + assertThat(get(jonDoeSession.getId())).isEqualTo(jonDoeSession); + assertThat(jonDoeSession.getAttribute("vip")).isEqualTo("no"); + assertThat(get(pieDoeSession.getId())).isEqualTo(pieDoeSession); + assertThat(pieDoeSession.getAttributeNames().contains("vip")).isFalse(); + assertThat(get(sourDoeSession.getId())).isEqualTo(sourDoeSession); + assertThat(sourDoeSession.getAttributeNames().contains("vip")).isFalse(); + + Map vipSessions = doFindByIndexNameAndIndexValue("vip", "yes"); + + assertThat(vipSessions).isNotNull(); + assertThat(vipSessions.size()).isEqualTo(2); + assertThat(vipSessions.get(johnBlumSession.getId())).isEqualTo(johnBlumSession); + assertThat(vipSessions.get(robWinchSession.getId())).isEqualTo(robWinchSession); + assertThat(vipSessions.containsKey(jonDoeSession.getId())); + assertThat(vipSessions.containsKey(pieDoeSession.getId())); + assertThat(vipSessions.containsKey(sourDoeSession.getId())); + + Map nonVipSessions = doFindByIndexNameAndIndexValue("vip", "no"); + + assertThat(nonVipSessions).isNotNull(); + assertThat(nonVipSessions.size()).isEqualTo(1); + assertThat(nonVipSessions.get(jonDoeSession.getId())).isEqualTo(jonDoeSession); + assertThat(nonVipSessions.containsKey(johnBlumSession.getId())); + assertThat(nonVipSessions.containsKey(robWinchSession.getId())); + assertThat(nonVipSessions.containsKey(pieDoeSession.getId())); + assertThat(nonVipSessions.containsKey(sourDoeSession.getId())); + + Map noSessions = doFindByIndexNameAndIndexValue("nonExistingAttribute", "test"); + + assertThat(noSessions).isNotNull(); + assertThat(noSessions.isEmpty()).isTrue(); + } + @Test public void findSessionsByPrincipalName() { ExpiringSession sessionOne = save(touch(createSession("robWinch"))); @@ -212,7 +268,7 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract } @Test - public void doesNotFindAfterPrincipalRemoved() { + public void findsNoSessionsAfterPrincipalIsRemoved() { String username = "doesNotFindAfterPrincipalRemoved"; ExpiringSession session = save(touch(createSession(username))); session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, null); @@ -271,6 +327,7 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract @EnableGemFireHttpSession(regionName = SPRING_SESSION_GEMFIRE_REGION_NAME, maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS) + @SuppressWarnings("unused") static class SpringSessionGemFireConfiguration { @Bean @@ -288,6 +345,7 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract CacheFactoryBean gemfireCache() { CacheFactoryBean gemfireCache = new CacheFactoryBean(); + gemfireCache.setClose(true); gemfireCache.setLazyInitialize(false); gemfireCache.setProperties(gemfireProperties()); gemfireCache.setUseBeanFactoryLocator(false); @@ -296,7 +354,7 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract } } - public static class Person implements PdxSerializable { + public static class Person implements Comparable, PdxSerializable { private String firstName; private String lastName; @@ -336,6 +394,12 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract this.lastName = pdxReader.readString("lastName"); } + @SuppressWarnings("all") + public int compareTo(final Person person) { + int compareValue = getLastName().compareTo(person.getLastName()); + return (compareValue != 0 ? compareValue : getFirstName().compareTo(person.getFirstName())); + } + @Override public boolean equals(final Object obj) { if (obj == this) { diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionJavaConfigurationTests.java b/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionJavaConfigurationTests.java new file mode 100644 index 0000000..8ce6e03 --- /dev/null +++ b/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionJavaConfigurationTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2015 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.gemfire.config.annotation.web.http; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Properties; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.data.gemfire.CacheFactoryBean; +import org.springframework.session.ExpiringSession; +import org.springframework.session.data.gemfire.AbstractGemFireIntegrationTests; +import org.springframework.session.data.gemfire.support.GemFireUtils; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +import com.gemstone.gemfire.cache.Cache; +import com.gemstone.gemfire.cache.DataPolicy; +import com.gemstone.gemfire.cache.ExpirationAction; +import com.gemstone.gemfire.cache.Region; +import com.gemstone.gemfire.cache.RegionShortcut; +import com.gemstone.gemfire.cache.query.Index; +import com.gemstone.gemfire.cache.query.QueryService; + +/** + * The GemFireHttpSessionJavaConfigurationTests class... + * + * @author John Blum + * @since 1.0.0 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +@DirtiesContext +@WebAppConfiguration +public class GemFireHttpSessionJavaConfigurationTests extends AbstractGemFireIntegrationTests { + + @Autowired + private Cache gemfireCache; + + protected Region assertCacheAndRegion(Cache gemfireCache, String regionName, DataPolicy dataPolicy) { + assertThat(GemFireUtils.isPeer(gemfireCache)).isTrue(); + + Region region = gemfireCache.getRegion(regionName); + + assertRegion(region, regionName, dataPolicy); + + return region; + } + + @Test + public void gemfireCacheConfigurationIsValid() { + Region example = assertCacheAndRegion(gemfireCache, "Example", + DataPolicy.REPLICATE); + + assertEntryIdleTimeout(example, ExpirationAction.INVALIDATE, 900); + } + + @Test + public void verifyGemFireExampleCacheRegionPrincipalNameIndexWasCreatedSuccessfully() { + Region example = assertCacheAndRegion(gemfireCache, "Example", + DataPolicy.REPLICATE); + + QueryService queryService = example.getRegionService().getQueryService(); + + assertThat(queryService).isNotNull(); + + Index principalNameIndex = queryService.getIndex(example, "principalNameIndex"); + + assertIndex(principalNameIndex, "principalName", example.getFullPath()); + } + + @Test + public void verifyGemFireExampleCacheRegionSessionAttributesIndexWasNotCreated() { + Region example = assertCacheAndRegion(gemfireCache, "Example", + DataPolicy.REPLICATE); + + QueryService queryService = example.getRegionService().getQueryService(); + + assertThat(queryService).isNotNull(); + + Index sessionAttributesIndex = queryService.getIndex(example, "sessionAttributesIndex"); + + assertThat(sessionAttributesIndex).isNull(); + } + + @EnableGemFireHttpSession(indexableSessionAttributes = {}, maxInactiveIntervalInSeconds = 900, + regionName = "Example", serverRegionShortcut = RegionShortcut.REPLICATE) + public static class GemFireConfiguration { + + @Bean + Properties gemfireProperties() { + Properties gemfireProperties = new Properties(); + gemfireProperties.setProperty("name", GemFireHttpSessionJavaConfigurationTests.class.getName()); + gemfireProperties.setProperty("mcast-port", "0"); + gemfireProperties.setProperty("log-level", "warning"); + return gemfireProperties; + } + + @Bean + CacheFactoryBean gemfireCache() { + CacheFactoryBean cacheFactory = new CacheFactoryBean(); + + cacheFactory.setClose(true); + cacheFactory.setProperties(gemfireProperties()); + cacheFactory.setUseBeanFactoryLocator(false); + + return cacheFactory; + } + } + +} diff --git a/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationXmlTests.java b/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionXmlConfigurationTests.java similarity index 55% rename from spring-session/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationXmlTests.java rename to spring-session/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionXmlConfigurationTests.java index 99847d1..af0e51a 100644 --- a/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationXmlTests.java +++ b/spring-session/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionXmlConfigurationTests.java @@ -23,6 +23,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.session.ExpiringSession; import org.springframework.session.data.gemfire.AbstractGemFireIntegrationTests; +import org.springframework.session.data.gemfire.support.GemFireUtils; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @@ -31,9 +32,11 @@ import com.gemstone.gemfire.cache.Cache; import com.gemstone.gemfire.cache.DataPolicy; import com.gemstone.gemfire.cache.ExpirationAction; import com.gemstone.gemfire.cache.Region; +import com.gemstone.gemfire.cache.query.Index; +import com.gemstone.gemfire.cache.query.QueryService; /** - * The GemFireHttpSessionConfigurationXmlTests class is a test suite of test cases testing the configuration of + * The GemFireHttpSessionXmlConfigurationTests class is a test suite of test cases testing the configuration of * Spring Session backed by GemFire using XML configuration meta-data. * * @author John Blum @@ -48,19 +51,53 @@ import com.gemstone.gemfire.cache.Region; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @WebAppConfiguration -public class GemFireHttpSessionConfigurationXmlTests extends AbstractGemFireIntegrationTests { +public class GemFireHttpSessionXmlConfigurationTests extends AbstractGemFireIntegrationTests { @Autowired private Cache gemfireCache; + protected Region assertCacheAndRegion(Cache gemfireCache, String regionName, DataPolicy dataPolicy) { + assertThat(GemFireUtils.isPeer(gemfireCache)).isTrue(); + + Region region = gemfireCache.getRegion(regionName); + + assertRegion(region, regionName, dataPolicy); + + return region; + } + @Test public void gemfireCacheConfigurationIsValid() { - assertThat(gemfireCache).isNotNull(); + Region example = assertCacheAndRegion(gemfireCache, "Example", DataPolicy.NORMAL); - Region example = gemfireCache.getRegion("Example"); - - assertRegion(example, "Example", DataPolicy.NORMAL); assertEntryIdleTimeout(example, ExpirationAction.INVALIDATE, 3600); } + @Test + public void verifyGemFireExampleCacheRegionPrincipalNameIndexWasCreatedSuccessfully() { + Region example = assertCacheAndRegion(gemfireCache, "Example", DataPolicy.NORMAL); + + QueryService queryService = example.getRegionService().getQueryService(); + + assertThat(queryService).isNotNull(); + + Index principalNameIndex = queryService.getIndex(example, "principalNameIndex"); + + assertIndex(principalNameIndex, "principalName", example.getFullPath()); + } + + @Test + public void verifyGemFireExampleCacheRegionSessionAttributesIndexWasCreatedSuccessfully() { + Region example = assertCacheAndRegion(gemfireCache, "Example", DataPolicy.NORMAL); + + QueryService queryService = example.getRegionService().getQueryService(); + + assertThat(queryService).isNotNull(); + + Index sessionAttributesIndex = queryService.getIndex(example, "sessionAttributesIndex"); + + assertIndex(sessionAttributesIndex, "s.attributes['one', 'two', 'three']", + String.format("%1$s s", example.getFullPath())); + } + } diff --git a/spring-session/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java b/spring-session/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java index 86cd377..0680e29 100644 --- a/spring-session/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java +++ b/spring-session/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java @@ -21,10 +21,13 @@ import java.io.DataOutput; import java.io.IOException; import java.text.DateFormat; import java.text.SimpleDateFormat; +import java.util.AbstractMap; +import java.util.AbstractSet; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.UUID; @@ -325,6 +328,8 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi protected static final DateFormat TO_STRING_DATE_FORMAT = new SimpleDateFormat("YYYY-MM-dd-HH-mm-ss"); + protected static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; + static { Instantiator.register(new Instantiator(GemFireSession.class, 800813552) { @Override public DataSerializable newInstance() { @@ -333,10 +338,6 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi }); } - private String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; - - private SpelExpressionParser parser = new SpelExpressionParser(); - private transient boolean delta = false; private int maxInactiveIntervalInSeconds; @@ -346,6 +347,8 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi private transient final GemFireSessionAttributes sessionAttributes = new GemFireSessionAttributes(this); + private transient final SpelExpressionParser parser = new SpelExpressionParser(); + private String id; /* (non-Javadoc) */ @@ -426,6 +429,11 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi return sessionAttributes.getAttributeNames(); } + /* (non-Javadoc) */ + public GemFireSessionAttributes getAttributes() { + return sessionAttributes; + } + /* (non-Javadoc) */ public synchronized boolean isExpired() { long lastAccessedTime = getLastAccessedTime(); @@ -464,21 +472,23 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi /* (non-Javadoc) */ public synchronized void setPrincipalName(String principalName) { - setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName); + setAttribute(PRINCIPAL_NAME_INDEX_NAME, principalName); } /* (non-Javadoc) */ public synchronized String getPrincipalName() { - String principalName = getAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME); - if(principalName != null) { - return principalName; + String principalName = getAttribute(PRINCIPAL_NAME_INDEX_NAME); + + if (principalName == null) { + Object authentication = getAttribute(SPRING_SECURITY_CONTEXT); + + if (authentication != null) { + Expression expression = parser.parseExpression("authentication?.name"); + principalName = expression.getValue(authentication, String.class); + } } - Object authentication = getAttribute(SPRING_SECURITY_CONTEXT); - if(authentication != null) { - Expression expression = parser.parseExpression("authentication?.name"); - return expression.getValue(authentication, String.class); - } - return null; + + return principalName; } /* (non-Javadoc) */ @@ -597,17 +607,20 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi } /** - * The GemFireSessionAttributes class is a container for a Session attributes that implements both + * The GemFireSessionAttributes class is a container for Session attributes that implements both * the {@link DataSerializable} and {@link Delta} GemFire interfaces for efficient storage and distribution - * (replication) in GemFire. + * (replication) in GemFire. Additionally, GemFireSessionAttributes extends {@link AbstractMap} providing + * {@link Map}-like behavior since attributes of a Session are effectively a name to value mapping. * + * @see java.util.AbstractMap * @see com.gemstone.gemfire.DataSerializable * @see com.gemstone.gemfire.DataSerializer * @see com.gemstone.gemfire.Delta * @see com.gemstone.gemfire.Instantiator */ @SuppressWarnings("serial") - public static class GemFireSessionAttributes implements DataSerializable, Delta { + public static class GemFireSessionAttributes extends AbstractMap + implements DataSerializable, Delta { protected static final boolean DEFAULT_ALLOW_JAVA_SERIALIZATION = true; @@ -677,6 +690,21 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi return DEFAULT_ALLOW_JAVA_SERIALIZATION; } + /* (non-Javadoc); NOTE: entrySet implementation is not Thread-safe. */ + @Override + @SuppressWarnings("all") + public Set> entrySet() { + return new AbstractSet>() { + @Override public Iterator> iterator() { + return Collections.unmodifiableMap(sessionAttributes).entrySet().iterator(); + } + + @Override public int size() { + return sessionAttributes.size(); + } + }; + } + /* (non-Javadoc) */ public void from(Session session) { synchronized (lock) { diff --git a/spring-session/src/main/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepository.java b/spring-session/src/main/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepository.java index bcafc0e..dfa4985 100644 --- a/spring-session/src/main/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepository.java +++ b/spring-session/src/main/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepository.java @@ -16,7 +16,6 @@ package org.springframework.session.data.gemfire; -import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -38,6 +37,10 @@ import com.gemstone.gemfire.cache.query.SelectResults; */ public class GemFireOperationsSessionRepository extends AbstractGemFireOperationsSessionRepository { + // GemFire OQL query used to lookup Sessions by arbitrary attributes. + protected static final String FIND_SESSIONS_BY_INDEX_NAME_VALUE_QUERY = + "SELECT s FROM %1$s s WHERE s.attributes['%2$s'] = $1"; + // GemFire OQL query used to look up Sessions by principal name. protected static final String FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY = "SELECT s FROM %1$s s WHERE s.principalName = $1"; @@ -54,19 +57,18 @@ public class GemFireOperationsSessionRepository extends AbstractGemFireOperation } /** - * Looks up all the available Sessions tied to the specific user identified by principal name. + * Looks up all available Sessions with the particular attribute indexed by name having the given value. * - * @param indexName the name of the indexed value (i.e. FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME). - * @param indexValue the value of the index to search for (i.e. username) to search for all existing Spring Sessions. + * @param indexName name of the indexed Session attribute. + * (e.g. {@link org.springframework.session.FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME}). + * @param indexValue value of the indexed Session attribute to search on (e.g. username). * @return a mapping of Session ID to Session instances. * @see org.springframework.session.ExpiringSession + * @see java.util.Map + * @see #prepareQuery(String) */ public Map findByIndexNameAndIndexValue(String indexName, String indexValue) { - if(!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) { - return Collections.emptyMap(); - } - SelectResults results = getTemplate().find(String.format( - FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, getFullyQualifiedRegionName()), indexValue); + SelectResults results = getTemplate().find(prepareQuery(indexName), indexValue); Map sessions = new HashMap(results.size()); @@ -77,6 +79,18 @@ public class GemFireOperationsSessionRepository extends AbstractGemFireOperation return sessions; } + /** + * Prepares the appropriate GemFire OQL query based on the indexed Session attribute name. + * + * @param indexName a String indicating the name of the indexed Session attribute. + * @return an appropriate GemFire OQL statement for querying on a particular indexed Session attribute. + */ + protected String prepareQuery(String indexName) { + return (PRINCIPAL_NAME_INDEX_NAME.equals(indexName) + ? String.format(FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, getFullyQualifiedRegionName()) + : String.format(FIND_SESSIONS_BY_INDEX_NAME_VALUE_QUERY, getFullyQualifiedRegionName(), indexName)); + } + /** * Constructs a new {@link ExpiringSession} instance backed by GemFire. * diff --git a/spring-session/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java b/spring-session/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java index 674ebcd..a622956 100644 --- a/spring-session/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java +++ b/spring-session/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java @@ -116,6 +116,14 @@ public @interface EnableGemFireHttpSession { */ ClientRegionShortcut clientRegionShortcut() default ClientRegionShortcut.PROXY; + /** + * Identifies the Session attributes by name that should be indexed for query operations. + * For instance, find all Sessions in GemFire having attribute A defined with value X. + * + * @return an array of Strings identifying the names of Session attributes to index. + */ + String[] indexableSessionAttributes() default {}; + /** * Defines the maximum interval in seconds that a Session can remain inactive before it is considered expired. * Defaults to 1800 seconds, or 30 minutes. diff --git a/spring-session/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java b/spring-session/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java index 1147866..8ec6f60 100644 --- a/spring-session/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java +++ b/spring-session/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.session.data.gemfire.AbstractGemFireOperationsSession import org.springframework.session.data.gemfire.GemFireOperationsSessionRepository; import org.springframework.session.data.gemfire.config.annotation.web.http.support.GemFireCacheTypeAwareRegionFactoryBean; import org.springframework.session.data.gemfire.support.GemFireUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import com.gemstone.gemfire.cache.ExpirationAction; @@ -68,6 +69,8 @@ import com.gemstone.gemfire.cache.client.ClientRegionShortcut; * @see com.gemstone.gemfire.cache.GemFireCache * @see com.gemstone.gemfire.cache.Region * @see com.gemstone.gemfire.cache.RegionAttributes + * @see com.gemstone.gemfire.cache.RegionShortcut + * @see com.gemstone.gemfire.cache.client.ClientRegionShortcut * @since 1.1.0 */ @Configuration @@ -85,6 +88,8 @@ public class GemFireHttpSessionConfiguration extends SpringHttpSessionConfigurat public static final String DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME = "ClusteredSpringSessions"; + public static final String[] DEFAULT_INDEXABLE_SESSION_ATTRIBUTES = new String[0]; + private int maxInactiveIntervalInSeconds = DEFAULT_MAX_INACTIVE_INTERVAL_IN_SECONDS; private ClassLoader beanClassLoader; @@ -95,6 +100,8 @@ public class GemFireHttpSessionConfiguration extends SpringHttpSessionConfigurat private String springSessionGemFireRegionName = DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME; + private String[] indexableSessionAttributes = DEFAULT_INDEXABLE_SESSION_ATTRIBUTES; + /** * Sets a reference to the {@link ClassLoader} used to load bean definition class types in a Spring context. * @@ -133,11 +140,55 @@ public class GemFireHttpSessionConfiguration extends SpringHttpSessionConfigurat * * @return the ClientRegionShortcut used to configure the GemFire ClientCache Region. * @see com.gemstone.gemfire.cache.client.ClientRegionShortcut + * @see EnableGemFireHttpSession#clientRegionShortcut() */ protected ClientRegionShortcut getClientRegionShortcut() { return (clientRegionShortcut != null ? clientRegionShortcut : DEFAULT_CLIENT_REGION_SHORTCUT); } + /** + * Sets the names of all Session attributes that should be indexed by GemFire. + * + * @param indexableSessionAttributes an array of Strings indicating the names of all Session attributes + * for which an Index will be created by GemFire. + */ + public void setIndexableSessionAttributes(String[] indexableSessionAttributes) { + this.indexableSessionAttributes = indexableSessionAttributes; + } + + /** + * Get the names of all Session attributes that should be indexed by GemFire. + * + * @return an array of Strings indicating the names of all Session attributes for which an Index + * will be created by GemFire. Defaults to an empty String array if unspecified. + * @see EnableGemFireHttpSession#indexableSessionAttributes() + */ + protected String[] getIndexableSessionAttributes() { + return (indexableSessionAttributes != null ? indexableSessionAttributes : DEFAULT_INDEXABLE_SESSION_ATTRIBUTES); + } + + /** + * Gets the names of all Session attributes that will be indexed by GemFire as single String value constituting + * the Index expression of the Index definition. + * + * @return a String composed of all the named Session attributes on which a GemFire Index will be created + * as an Index definition expression. If the indexable Session attributes were not specified, then the + * wildcard ("*") is returned. + * @see com.gemstone.gemfire.cache.query.Index#getIndexedExpression() + */ + protected String getIndexableSessionAttributesAsGemFireIndexExpression() { + StringBuilder builder = new StringBuilder(); + + for (String sessionAttribute : getIndexableSessionAttributes()) { + builder.append(builder.length() > 0 ? ", " : ""); + builder.append(String.format("'%1$s'", sessionAttribute)); + } + + String indexExpression = builder.toString(); + + return (indexExpression.isEmpty() ? "*" : indexExpression); + } + /** * Sets the maximum interval in seconds in which a Session can remain inactive before it is considered expired. * @@ -153,6 +204,7 @@ public class GemFireHttpSessionConfiguration extends SpringHttpSessionConfigurat * * @return an integer value specifying the maximum interval in seconds that a Session can remain inactive * before it is considered expired. + * @see EnableGemFireHttpSession#maxInactiveIntervalInSeconds() */ protected int getMaxInactiveIntervalInSeconds() { return maxInactiveIntervalInSeconds; @@ -174,6 +226,7 @@ public class GemFireHttpSessionConfiguration extends SpringHttpSessionConfigurat * * @return the RegionShortcut used to configure the GemFire Cache Region. * @see com.gemstone.gemfire.cache.RegionShortcut + * @see EnableGemFireHttpSession#serverRegionShortcut() */ protected RegionShortcut getServerRegionShortcut() { return (serverRegionShortcut != null ? serverRegionShortcut : DEFAULT_SERVER_REGION_SHORTCUT); @@ -195,6 +248,7 @@ public class GemFireHttpSessionConfiguration extends SpringHttpSessionConfigurat * @return a String specifying the name of the GemFire (Client)Cache Region * used to store the Session. * @see com.gemstone.gemfire.cache.Region#getName() + * @see EnableGemFireHttpSession#regionName() */ protected String getSpringSessionGemFireRegionName() { return (StringUtils.hasText(springSessionGemFireRegionName) ? springSessionGemFireRegionName @@ -216,6 +270,9 @@ public class GemFireHttpSessionConfiguration extends SpringHttpSessionConfigurat setClientRegionShortcut(ClientRegionShortcut.class.cast(enableGemFireHttpSessionAnnotationAttributes.getEnum( "clientRegionShortcut"))); + setIndexableSessionAttributes(enableGemFireHttpSessionAnnotationAttributes.getStringArray( + "indexableSessionAttributes")); + setMaxInactiveIntervalInSeconds(enableGemFireHttpSessionAnnotationAttributes.getNumber( "maxInactiveIntervalInSeconds").intValue()); @@ -359,7 +416,7 @@ public class GemFireHttpSessionConfiguration extends SpringHttpSessionConfigurat }; index.setCache(gemfireCache); - index.setName("principalNameIdx"); + index.setName("principalNameIndex"); index.setExpression("principalName"); index.setFrom(GemFireUtils.toRegionPath(getSpringSessionGemFireRegionName())); index.setOverride(true); @@ -368,4 +425,34 @@ public class GemFireHttpSessionConfiguration extends SpringHttpSessionConfigurat return index; } + /** + * Defines a Spring GemFire Index bean on the GemFire cache {@link Region} storing and managing Sessions, + * specifically on Session attributes for quick lookup and queries on Session attribute names with a given value. + * This index will only be created on a server @{link Region}. + * + * @param gemfireCache a reference to the GemFire cache. + * @return a IndexFactoryBean creating an GemFire Index on attributes of Sessions stored in the GemFire cache {@link Region}. + * @see org.springframework.data.gemfire.IndexFactoryBean + * @see com.gemstone.gemfire.cache.GemFireCache + */ + @Bean + @DependsOn(DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME) + public IndexFactoryBean sessionAttributesIndex(final GemFireCache gemfireCache) { + IndexFactoryBean index = new IndexFactoryBean() { + @Override public void afterPropertiesSet() throws Exception { + if (GemFireUtils.isPeer(gemfireCache) && !ObjectUtils.isEmpty(getIndexableSessionAttributes())) { + super.afterPropertiesSet(); + } + } + }; + + index.setCache(gemfireCache); + index.setName("sessionAttributesIndex"); + index.setExpression(String.format("s.attributes[%1$s]", getIndexableSessionAttributesAsGemFireIndexExpression())); + index.setFrom(String.format("%1$s s", GemFireUtils.toRegionPath(getSpringSessionGemFireRegionName()))); + index.setOverride(true); + + return index; + } + } diff --git a/spring-session/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTest.java b/spring-session/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTest.java index 5b59cd8..eae5a11 100644 --- a/spring-session/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTest.java +++ b/spring-session/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTest.java @@ -1268,6 +1268,55 @@ public class AbstractGemFireOperationsSessionRepositoryTest { verify(mockDataInput, times(1)).readUTF(); } + @Test + public void sessionAttributesEntrySetIteratesAttributeNameValues() { + GemFireSessionAttributes sessionAttributes = new GemFireSessionAttributes(); + + sessionAttributes.setAttribute("keyOne", "valueOne"); + sessionAttributes.setAttribute("keyTwo", "valueTwo"); + + Set> sessionAttributeEntries = sessionAttributes.entrySet(); + + assertThat(sessionAttributeEntries).isNotNull(); + assertThat(sessionAttributeEntries.size()).isEqualTo(2); + + Set expectedNames = asSet("keyOne", "keyTwo"); + Set expectedValues = asSet("valueOne", "valueTwo"); + + for (Map.Entry entry : sessionAttributeEntries) { + expectedNames.remove(entry.getKey()); + expectedValues.remove(entry.getValue()); + } + + assertThat(expectedNames.isEmpty()).isTrue(); + assertThat(expectedValues.isEmpty()).isTrue(); + + sessionAttributes.setAttribute("keyThree", "valueThree"); + + assertThat(sessionAttributeEntries.size()).isEqualTo(3); + + expectedNames = asSet("keyOne", "keyTwo"); + expectedValues = asSet("valueOne", "valueTwo"); + + for (Map.Entry entry : sessionAttributeEntries) { + expectedNames.remove(entry.getKey()); + expectedValues.remove(entry.getValue()); + } + + assertThat(expectedNames.isEmpty()).isTrue(); + assertThat(expectedValues.isEmpty()).isTrue(); + + sessionAttributes.removeAttribute("keyOne"); + sessionAttributes.removeAttribute("keyTwo"); + + assertThat(sessionAttributeEntries.size()).isEqualTo(1); + + Map.Entry entry = sessionAttributeEntries.iterator().next(); + + assertThat(entry.getKey()).isEqualTo("keyThree"); + assertThat(entry.getValue()).isEqualTo("valueThree"); + } + @Test public void sessionWithAttributesAreThreadSafe() throws Throwable { TestFramework.runOnce(new ThreadSafeSessionTest()); diff --git a/spring-session/src/test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryTest.java b/spring-session/src/test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryTest.java index 788b41c..ee23dd0 100644 --- a/spring-session/src/test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryTest.java +++ b/spring-session/src/test/java/org/springframework/session/data/gemfire/GemFireOperationsSessionRepositoryTest.java @@ -27,6 +27,9 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; +import static org.springframework.session.data.gemfire.GemFireOperationsSessionRepository.FIND_SESSIONS_BY_INDEX_NAME_VALUE_QUERY; +import static org.springframework.session.data.gemfire.GemFireOperationsSessionRepository.FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY; import static org.springframework.session.data.gemfire.GemFireOperationsSessionRepository.GemFireSession; import java.util.Arrays; @@ -47,7 +50,6 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.gemfire.GemfireAccessor; import org.springframework.data.gemfire.GemfireOperations; import org.springframework.session.ExpiringSession; -import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.events.AbstractSessionEvent; import org.springframework.session.events.SessionDeletedEvent; @@ -66,6 +68,7 @@ import com.gemstone.gemfire.cache.query.SelectResults; * @see org.mockito.Mockito * @see org.mockito.runners.MockitoJUnitRunner * @see org.springframework.session.data.gemfire.GemFireOperationsSessionRepository + * @see com.gemstone.gemfire.cache.Region * @since 1.1.0 */ @RunWith(MockitoJUnitRunner.class) @@ -110,6 +113,36 @@ public class GemFireOperationsSessionRepositoryTest { verify(mockTemplate, times(1)).getRegion(); } + @Test + @SuppressWarnings("unchecked") + public void findByIndexNameValueFindsMatchingSession() { + ExpiringSession mockSession = mock(ExpiringSession.class, "MockSession"); + + when(mockSession.getId()).thenReturn("1"); + + SelectResults mockSelectResults = mock(SelectResults.class); + + when(mockSelectResults.asList()).thenReturn(Collections.singletonList(mockSession)); + + String indexName = "vip"; + String indexValue = "rwinch"; + + String expectedQql = String.format(FIND_SESSIONS_BY_INDEX_NAME_VALUE_QUERY, + sessionRepository.getFullyQualifiedRegionName(), indexName); + + when(mockTemplate.find(eq(expectedQql), eq(indexValue))).thenReturn(mockSelectResults); + + Map sessions = sessionRepository.findByIndexNameAndIndexValue(indexName, indexValue); + + assertThat(sessions).isNotNull(); + assertThat(sessions.size()).isEqualTo(1); + assertThat(sessions.get("1")).isEqualTo(mockSession); + + verify(mockTemplate, times(1)).find(eq(expectedQql), eq(indexValue)); + verify(mockSelectResults, times(1)).asList(); + verify(mockSession, times(1)).getId(); + } + @Test @SuppressWarnings("unchecked") public void findByPrincipalNameFindsMatchingSessions() throws Exception { @@ -127,12 +160,13 @@ public class GemFireOperationsSessionRepositoryTest { String principalName = "jblum"; - String expectedOql = String.format(GemFireOperationsSessionRepository.FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, + String expectedOql = String.format(FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, sessionRepository.getFullyQualifiedRegionName()); when(mockTemplate.find(eq(expectedOql), eq(principalName))).thenReturn(mockSelectResults); - Map sessions = sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName); + Map sessions = sessionRepository.findByIndexNameAndIndexValue( + PRINCIPAL_NAME_INDEX_NAME, principalName); assertThat(sessions).isNotNull(); assertThat(sessions.size()).isEqualTo(3); @@ -156,12 +190,13 @@ public class GemFireOperationsSessionRepositoryTest { String principalName = "jblum"; - String expectedOql = String.format(GemFireOperationsSessionRepository.FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, + String expectedOql = String.format(FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, sessionRepository.getFullyQualifiedRegionName()); when(mockTemplate.find(eq(expectedOql), eq(principalName))).thenReturn(mockSelectResults); - Map sessions = sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName); + Map sessions = sessionRepository.findByIndexNameAndIndexValue( + PRINCIPAL_NAME_INDEX_NAME, principalName); assertThat(sessions).isNotNull(); assertThat(sessions.isEmpty()).isTrue(); @@ -170,6 +205,25 @@ public class GemFireOperationsSessionRepositoryTest { verify(mockSelectResults, times(1)).asList(); } + @Test + public void prepareQueryReturnsPrincipalNameOql() { + String actualQql = sessionRepository.prepareQuery(PRINCIPAL_NAME_INDEX_NAME); + String expectedOql = String.format(FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, + sessionRepository.getFullyQualifiedRegionName()); + + assertThat(actualQql).isEqualTo(expectedOql); + } + + @Test + public void prepareQueryReturnsIndexNameValueOql() { + String attributeName = "testAttributeName"; + String actualOql = sessionRepository.prepareQuery(attributeName); + String expectedOql = String.format(FIND_SESSIONS_BY_INDEX_NAME_VALUE_QUERY, + sessionRepository.getFullyQualifiedRegionName(), attributeName); + + assertThat(actualOql).isEqualTo(expectedOql); + } + @Test public void createProperlyInitializedSession() { final long beforeOrAtCreationTime = System.currentTimeMillis(); diff --git a/spring-session/src/test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationTest.java b/spring-session/src/test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationTest.java index 8c82666..585e319 100644 --- a/spring-session/src/test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationTest.java +++ b/spring-session/src/test/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationTest.java @@ -47,6 +47,9 @@ import com.gemstone.gemfire.cache.client.ClientRegionShortcut; * @author John Blum * @see org.junit.Test * @see org.mockito.Mockito + * @see org.springframework.data.gemfire.GemfireOperations + * @see org.springframework.data.gemfire.GemfireTemplate + * @see org.springframework.session.data.gemfire.GemFireOperationsSessionRepository * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration * @see com.gemstone.gemfire.cache.Cache * @see com.gemstone.gemfire.cache.GemFireCache @@ -58,6 +61,10 @@ public class GemFireHttpSessionConfigurationTest { private GemFireHttpSessionConfiguration gemfireConfiguration; + protected T[] toArray(T... array) { + return array; + } + @Before public void setup() { gemfireConfiguration = new GemFireHttpSessionConfiguration(); @@ -91,6 +98,29 @@ public class GemFireHttpSessionConfigurationTest { GemFireHttpSessionConfiguration.DEFAULT_CLIENT_REGION_SHORTCUT); } + @Test + public void setAndGetIndexableSessionAttributes() { + assertThat(gemfireConfiguration.getIndexableSessionAttributes()).isEqualTo( + GemFireHttpSessionConfiguration.DEFAULT_INDEXABLE_SESSION_ATTRIBUTES); + + gemfireConfiguration.setIndexableSessionAttributes(toArray("one", "two", "three")); + + assertThat(gemfireConfiguration.getIndexableSessionAttributes()).isEqualTo(toArray("one", "two", "three")); + assertThat(gemfireConfiguration.getIndexableSessionAttributesAsGemFireIndexExpression()) + .isEqualTo("'one', 'two', 'three'"); + + gemfireConfiguration.setIndexableSessionAttributes(toArray("one")); + + assertThat(gemfireConfiguration.getIndexableSessionAttributes()).isEqualTo(toArray("one")); + assertThat(gemfireConfiguration.getIndexableSessionAttributesAsGemFireIndexExpression()).isEqualTo("'one'"); + + gemfireConfiguration.setIndexableSessionAttributes(null); + + assertThat(gemfireConfiguration.getIndexableSessionAttributes()).isEqualTo( + GemFireHttpSessionConfiguration.DEFAULT_INDEXABLE_SESSION_ATTRIBUTES); + assertThat(gemfireConfiguration.getIndexableSessionAttributesAsGemFireIndexExpression()).isEqualTo("*"); + } + @Test public void setAndGetMaxInactiveIntervalInSeconds() { assertThat(gemfireConfiguration.getMaxInactiveIntervalInSeconds()).isEqualTo( @@ -160,6 +190,7 @@ public class GemFireHttpSessionConfigurationTest { Map annotationAttributes = new HashMap(4); annotationAttributes.put("clientRegionShortcut", ClientRegionShortcut.CACHING_PROXY); + annotationAttributes.put("indexableSessionAttributes", toArray("one", "two", "three")); annotationAttributes.put("maxInactiveIntervalInSeconds", 600); annotationAttributes.put("serverRegionShortcut", RegionShortcut.REPLICATE); annotationAttributes.put("regionName", "TEST"); @@ -170,6 +201,7 @@ public class GemFireHttpSessionConfigurationTest { gemfireConfiguration.setImportMetadata(mockAnnotationMetadata); assertThat(gemfireConfiguration.getClientRegionShortcut()).isEqualTo(ClientRegionShortcut.CACHING_PROXY); + assertThat(gemfireConfiguration.getIndexableSessionAttributes()).isEqualTo(toArray("one", "two", "three")); assertThat(gemfireConfiguration.getMaxInactiveIntervalInSeconds()).isEqualTo(600); assertThat(gemfireConfiguration.getServerRegionShortcut()).isEqualTo(RegionShortcut.REPLICATE); assertThat(gemfireConfiguration.getSpringSessionGemFireRegionName()).isEqualTo("TEST"); diff --git a/spring-session/src/test/resources/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationXmlTests-context.xml b/spring-session/src/test/resources/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionXmlConfigurationTests-context.xml similarity index 80% rename from spring-session/src/test/resources/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationXmlTests-context.xml rename to spring-session/src/test/resources/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionXmlConfigurationTests-context.xml index 7602d2d..8afdec1 100644 --- a/spring-session/src/test/resources/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfigurationXmlTests-context.xml +++ b/spring-session/src/test/resources/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionXmlConfigurationTests-context.xml @@ -11,15 +11,15 @@ "> - GemFireHttpSessionConfigurationXmlTests + GemFireHttpSessionXmlConfigurationTests 0 warning - +