Add generic GemFire findByIndexNameAndIndexValue support

Fixes gh-353
This commit is contained in:
John Blum
2016-02-03 23:04:20 -08:00
committed by Rob Winch
parent 7de11753a9
commit c2b407189e
13 changed files with 611 additions and 50 deletions

View File

@@ -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<String> createJavaProcessCommandLine(Class<? extends Object> type, String... args) {
protected static List<String> createJavaProcessCommandLine(Class<?> type, String... args) {
List<String> commandLine = new ArrayList<String>();
String javaHome = System.getProperty("java.home");
@@ -155,7 +156,7 @@ public class AbstractGemFireIntegrationTests {
}
/* (non-Javadoc) */
protected static Process run(Class<? extends Object> 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);

View File

@@ -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<String, ExpiringSession> doFindByPrincipalName(String principalName) {
return gemfireSessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
protected Map<String, ExpiringSession> doFindByIndexNameAndIndexValue(String indexName, String indexValue) {
return gemfireSessionRepository.findByIndexNameAndIndexValue(indexName, indexValue);
}
@SuppressWarnings("unchecked")
protected Map<String, ExpiringSession> doFindByPrincipalName(String principalName) {
return doFindByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);
}
@SuppressWarnings({ "unused", "unchecked" })
protected Map<String, ExpiringSession> doFindByPrincipalName(String regionName, String principalName) {
try {
Region<String, ExpiringSession> 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<String, ExpiringSession> 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<String, ExpiringSession> 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<String, ExpiringSession> 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<Person>, 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) {

View File

@@ -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 <K, V> Region<K, V> assertCacheAndRegion(Cache gemfireCache, String regionName, DataPolicy dataPolicy) {
assertThat(GemFireUtils.isPeer(gemfireCache)).isTrue();
Region<K, V> region = gemfireCache.getRegion(regionName);
assertRegion(region, regionName, dataPolicy);
return region;
}
@Test
public void gemfireCacheConfigurationIsValid() {
Region<Object, ExpiringSession> example = assertCacheAndRegion(gemfireCache, "Example",
DataPolicy.REPLICATE);
assertEntryIdleTimeout(example, ExpirationAction.INVALIDATE, 900);
}
@Test
public void verifyGemFireExampleCacheRegionPrincipalNameIndexWasCreatedSuccessfully() {
Region<Object, ExpiringSession> 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<Object, ExpiringSession> 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;
}
}
}

View File

@@ -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 <K, V> Region<K, V> assertCacheAndRegion(Cache gemfireCache, String regionName, DataPolicy dataPolicy) {
assertThat(GemFireUtils.isPeer(gemfireCache)).isTrue();
Region<K, V> region = gemfireCache.getRegion(regionName);
assertRegion(region, regionName, dataPolicy);
return region;
}
@Test
public void gemfireCacheConfigurationIsValid() {
assertThat(gemfireCache).isNotNull();
Region<Object, ExpiringSession> example = assertCacheAndRegion(gemfireCache, "Example", DataPolicy.NORMAL);
Region<Object, ExpiringSession> example = gemfireCache.getRegion("Example");
assertRegion(example, "Example", DataPolicy.NORMAL);
assertEntryIdleTimeout(example, ExpirationAction.INVALIDATE, 3600);
}
@Test
public void verifyGemFireExampleCacheRegionPrincipalNameIndexWasCreatedSuccessfully() {
Region<Object, ExpiringSession> 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<Object, ExpiringSession> 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()));
}
}

View File

@@ -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<String, Object>
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<Entry<String, Object>> entrySet() {
return new AbstractSet<Entry<String, Object>>() {
@Override public Iterator<Entry<String, Object>> iterator() {
return Collections.unmodifiableMap(sessionAttributes).entrySet().iterator();
}
@Override public int size() {
return sessionAttributes.size();
}
};
}
/* (non-Javadoc) */
public void from(Session session) {
synchronized (lock) {

View File

@@ -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<String, ExpiringSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
if(!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
return Collections.emptyMap();
}
SelectResults<ExpiringSession> results = getTemplate().find(String.format(
FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, getFullyQualifiedRegionName()), indexValue);
SelectResults<ExpiringSession> results = getTemplate().find(prepareQuery(indexName), indexValue);
Map<String, ExpiringSession> sessions = new HashMap<String, ExpiringSession>(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.
*

View File

@@ -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.

View File

@@ -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;
}
}

View File

@@ -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<Map.Entry<String, Object>> sessionAttributeEntries = sessionAttributes.entrySet();
assertThat(sessionAttributeEntries).isNotNull();
assertThat(sessionAttributeEntries.size()).isEqualTo(2);
Set<String> expectedNames = asSet("keyOne", "keyTwo");
Set<?> expectedValues = asSet("valueOne", "valueTwo");
for (Map.Entry<String, Object> 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<String, Object> 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<String, ?> entry = sessionAttributeEntries.iterator().next();
assertThat(entry.getKey()).isEqualTo("keyThree");
assertThat(entry.getValue()).isEqualTo("valueThree");
}
@Test
public void sessionWithAttributesAreThreadSafe() throws Throwable {
TestFramework.runOnce(new ThreadSafeSessionTest());

View File

@@ -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<Object> mockSelectResults = mock(SelectResults.class);
when(mockSelectResults.asList()).thenReturn(Collections.<Object>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<String, ExpiringSession> 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<String, ExpiringSession> sessions = sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
Map<String, ExpiringSession> 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<String, ExpiringSession> sessions = sessionRepository.findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, principalName);
Map<String, ExpiringSession> 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();

View File

@@ -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> 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<String, Object> annotationAttributes = new HashMap<String, Object>(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");

View File

@@ -11,15 +11,15 @@
">
<util:properties id="gemfireProperties">
<prop key="name">GemFireHttpSessionConfigurationXmlTests</prop>
<prop key="name">GemFireHttpSessionXmlConfigurationTests</prop>
<prop key="mcast-port">0</prop>
<prop key="log-level">warning</prop>
</util:properties>
<gfe:cache properties-ref="gemfireProperties"/>
<gfe:cache properties-ref="gemfireProperties" close="true"/>
<bean class="org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration"
p:maxInactiveIntervalInSeconds="3600" p:springSessionGemFireRegionName="Example"
p:indexableSessionAttributes="one, two, three" p:maxInactiveIntervalInSeconds="3600" p:springSessionGemFireRegionName="Example"
p:serverRegionShortcut="#{T(com.gemstone.gemfire.cache.RegionShortcut).LOCAL}"/>
</beans>