From 2b5386ad9863182d90b1ab6f6dc7d36cda281ef7 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Mon, 8 Feb 2016 15:34:03 -0600 Subject: [PATCH] Polish GemFire findByIndexNameAndValue Issue gh-353 --- docs/build.gradle | 5 +- docs/src/docs/asciidoc/index.adoc | 133 ++++-- .../docs/AbstractGemFireIntegrationTests.java | 449 ++++++++++++++++++ .../HttpSessionGemFireIndexingITests.java | 120 +++++ .../GemFireHttpSessionConfig.java | 57 +++ ...ttpSessionGemFireIndexingCustomITests.java | 58 +++ .../AbstractHttpSessionListenerTests.java | 1 - spring-session/build.gradle | 2 +- ...ionsSessionRepositoryIntegrationTests.java | 3 +- 9 files changed, 773 insertions(+), 55 deletions(-) create mode 100644 docs/src/integration-test/java/docs/AbstractGemFireIntegrationTests.java create mode 100644 docs/src/integration-test/java/docs/http/HttpSessionGemFireIndexingITests.java create mode 100644 docs/src/integration-test/java/docs/http/gemfire/indexablesessionattributes/GemFireHttpSessionConfig.java create mode 100644 docs/src/integration-test/java/docs/http/gemfire/indexablesessionattributes/HttpSessionGemFireIndexingCustomITests.java diff --git a/docs/build.gradle b/docs/build.gradle index 433293a8..05389639 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -9,7 +9,7 @@ buildscript { } apply plugin: 'org.kordamp.gradle.livereload' -apply plugin: 'java' +apply from: JAVA_GRADLE apply plugin: 'org.asciidoctor.convert' liveReload { @@ -28,11 +28,13 @@ tasks.findByPath("artifactoryPublish")?.enabled = false dependencies { testCompile project(':spring-session'), + "org.springframework.data:spring-data-gemfire:$springDataGemFireVersion", "org.springframework.data:spring-data-redis:$springDataRedisVersion", "org.springframework.data:spring-data-gemfire:$springDataGemFireVersion", "org.springframework:spring-websocket:${springVersion}", "org.springframework:spring-messaging:${springVersion}", "org.springframework.security:spring-security-web:${springSecurityVersion}", + "org.springframework.security:spring-security-test:${springSecurityVersion}", 'junit:junit:4.11', 'org.mockito:mockito-core:1.9.5', "org.springframework:spring-test:$springVersion", @@ -54,6 +56,7 @@ asciidoctor { 'spring-session-version' : version, 'spring-version' : springVersion, 'hazelcast-version' : hazelcastVersion, + 'docs-itest-dir' : rootProject.projectDir.path + '/docs/src/integration-test/java/', 'docs-test-dir' : rootProject.projectDir.path + '/docs/src/test/java/', 'docs-test-resources-dir' : rootProject.projectDir.path + '/docs/src/test/resources/', 'samples-dir' : rootProject.projectDir.path + '/samples/', diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index cf0aeafd..2b275425 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -257,56 +257,6 @@ 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 @@ -812,6 +762,89 @@ redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed "\xac\xed\x00\x05t\x00\x03rob" ---- +[[api-gemfireoperationssessionrepository]] +=== GemFireOperationsSessionRepository + +`GemFireOperationsSessionRepository` is a `SessionRepository` that is implemented using Spring Data's `GemFireOperationsSessionRepository`. +In a web environment, this is typically used in combination with `SessionRepositoryFilter`. +The implementation supports `SessionDestroyedEvent` and `SessionCreatedEvent` through `SessionMessageListener`. + +[[api-gemfireoperationssessionrepository-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 principal name. There are two different buit in +strategies for finding the principal name. The first strategy is that the value of the session attribute with the name +`FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME` will be indexed to the same index name. For example: + +[source,java,indent=0] +---- +include::{docs-itest-dir}docs/http/HttpSessionGemFireIndexingITests.java[tags=findbyindexname-set] +include::{docs-itest-dir}docs/http/HttpSessionGemFireIndexingITests.java[tags=findbyindexname-get] +---- + +[[api-gemfireoperationssessionrepository-indexing-security]] +==== Using Indexes with GemFire & Spring Security + +Alternatively, Spring Session Data GemFire will map Spring Security's current `Authentication#getName()` to the index +`FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME`. For example, if you are using Spring Security you can +find the current user's sessions using: + +[source,java,indent=0] +---- +include::{docs-itest-dir}docs/http/HttpSessionGemFireIndexingITests.java[tags=findbyspringsecurityindexname-context] +include::{docs-itest-dir}docs/http/HttpSessionGemFireIndexingITests.java[tags=findbyspringsecurityindexname-get] +---- + +[[api-gemfireoperationssessionrepository-indexing-custom]] +==== Using Custom Indexes with GemFire + +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, the following configuration: + +[source,java,indent=0] +---- +include::{docs-itest-dir}docs/http/gemfire/indexablesessionattributes/GemFireHttpSessionConfig.java[tags=class-start] + // ... +} +---- + +will allow searching for sessions using the following: + +[source,java,indent=0] +---- +include::{docs-itest-dir}docs/http/gemfire/indexablesessionattributes/HttpSessionGemFireIndexingCustomITests.java[tags=findbyindexname-set] +include::{docs-itest-dir}docs/http/gemfire/indexablesessionattributes/HttpSessionGemFireIndexingCustomITests.java[tags=findbyindexname-get] +---- + +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]. + + [[api-mapsessionrepository]] === MapSessionRepository diff --git a/docs/src/integration-test/java/docs/AbstractGemFireIntegrationTests.java b/docs/src/integration-test/java/docs/AbstractGemFireIntegrationTests.java new file mode 100644 index 00000000..8bcf2425 --- /dev/null +++ b/docs/src/integration-test/java/docs/AbstractGemFireIntegrationTests.java @@ -0,0 +1,449 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package docs; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Before; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationListener; +import org.springframework.session.ExpiringSession; +import org.springframework.session.data.gemfire.GemFireOperationsSessionRepository; +import org.springframework.session.data.gemfire.support.GemFireUtils; +import org.springframework.session.events.AbstractSessionEvent; + +import com.gemstone.gemfire.cache.Cache; +import com.gemstone.gemfire.cache.CacheClosedException; +import com.gemstone.gemfire.cache.DataPolicy; +import com.gemstone.gemfire.cache.ExpirationAction; +import com.gemstone.gemfire.cache.ExpirationAttributes; +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; + +/** + * AbstractGemFireIntegrationTests is an abstract base class encapsulating common operations for writing + * Spring Session GemFire integration tests. + * + * @author John Blum + * @see org.springframework.session.ExpiringSession + * @see org.springframework.session.events.AbstractSessionEvent + * @see com.gemstone.gemfire.cache.Cache + * @see com.gemstone.gemfire.cache.DataPolicy + * @see com.gemstone.gemfire.cache.ExpirationAttributes + * @see com.gemstone.gemfire.cache.GemFireCache + * @see com.gemstone.gemfire.cache.Region + * @see com.gemstone.gemfire.cache.client.ClientCache + * @see com.gemstone.gemfire.cache.server.CacheServer + * @since 1.1.0 + */ +public class AbstractGemFireIntegrationTests { + public static final String GEMFIRE_LOG_LEVEL = System.getProperty( + "spring.session.data.gemfire.log-level", "warning"); + + protected static final boolean DEFAULT_ENABLE_QUERY_DEBUGGING = false; + protected static final boolean GEMFIRE_QUERY_DEBUG = Boolean.getBoolean("spring.session.data.gemfire.query.debug"); + + protected static final int DEFAULT_GEMFIRE_SERVER_PORT = CacheServer.DEFAULT_PORT; + + protected static final long DEFAULT_WAIT_DURATION = TimeUnit.SECONDS.toMillis(20); + protected static final long DEFAULT_WAIT_INTERVAL = 500l; + + protected static final File WORKING_DIRECTORY = new File(System.getProperty("user.dir")); + + protected static final String DEFAULT_PROCESS_CONTROL_FILENAME = "process.ctl"; + + protected static final String GEMFIRE_LOG_FILE_NAME = System.getProperty( + "spring.session.data.gemfire.log-file", "server.log"); + + + @Autowired + protected Cache gemfireCache; + + @Autowired + protected GemFireOperationsSessionRepository sessionRepository; + + @Before + public void setup() { + System.setProperty("gemfire.Query.VERBOSE", String.valueOf(isQueryDebuggingEnabled())); + } + + /* (non-Javadoc) */ + protected static File createDirectory(String pathname) { + File directory = new File(WORKING_DIRECTORY, pathname); + + assertThat(directory.isDirectory() || directory.mkdirs()).as( + String.format("Failed to create directory (%1$s)", directory)).isTrue(); + + directory.deleteOnExit(); + + return directory; + } + + /* (non-Javadoc) */ + protected static List createJavaProcessCommandLine(Class type, String... args) { + List commandLine = new ArrayList(); + + String javaHome = System.getProperty("java.home"); + String javaExe = new File(new File(javaHome, "bin"), "java").getAbsolutePath(); + + commandLine.add(javaExe); + commandLine.add("-server"); + commandLine.add("-ea"); + commandLine.add(String.format("-Dgemfire.log-file=%1$s", GEMFIRE_LOG_FILE_NAME)); + commandLine.add(String.format("-Dgemfire.log-level=%1$s", GEMFIRE_LOG_LEVEL)); + commandLine.add(String.format("-Dgemfire.Query.VERBOSE=%1$s", GEMFIRE_QUERY_DEBUG)); + commandLine.addAll(extractJvmArguments(args)); + commandLine.add("-classpath"); + commandLine.add(System.getProperty("java.class.path")); + commandLine.add(type.getName()); + commandLine.addAll(extractProgramArguments(args)); + + // System.err.printf("Java process command-line is (%1$s)%n", commandLine); + + return commandLine; + } + + /* (non-Javadoc) */ + protected static List extractJvmArguments(final String... args) { + List jvmArgs = new ArrayList(args.length); + + for (String arg : args) { + if (arg.startsWith("-")) { + jvmArgs.add(arg); + } + } + + return jvmArgs; + } + + /* (non-Javadoc) */ + protected static List extractProgramArguments(final String... args) { + List jvmArgs = new ArrayList(args.length); + + for (String arg : args) { + if (!arg.startsWith("-")) { + jvmArgs.add(arg); + } + } + + return jvmArgs; + } + + /* (non-Javadoc) */ + protected static Process run(Class type, File directory, String... args) throws IOException { + return new ProcessBuilder() + .command(createJavaProcessCommandLine(type, args)) + .directory(directory) + .start(); + } + + /* (non-Javadoc) */ + protected static boolean waitForCacheServerToStart(CacheServer cacheServer) { + return waitForCacheServerToStart(cacheServer, DEFAULT_WAIT_DURATION); + } + + /* (non-Javadoc) */ + protected static boolean waitForCacheServerToStart(CacheServer cacheServer, long duration) { + return waitForCacheServerToStart(cacheServer.getBindAddress(), cacheServer.getPort(), duration); + } + + /* (non-Javadoc) */ + protected static boolean waitForCacheServerToStart(String host, int port) { + return waitForCacheServerToStart(host, port, DEFAULT_WAIT_DURATION); + } + + /* (non-Javadoc) */ + protected static boolean waitForCacheServerToStart(final String host, final int port, long duration) { + return waitOnCondition(new Condition() { + AtomicBoolean connected = new AtomicBoolean(false); + + public boolean evaluate() { + Socket socket = null; + + try { + if (!connected.get()) { + socket = new Socket(host, port); + connected.set(true); + } + } + catch (IOException ignore) { + } + finally { + GemFireUtils.close(socket); + } + + return connected.get(); + } + }, duration); + } + + // NOTE this method would not be necessary except Spring Sessions' build does not fork the test JVM + // for every test class. + /* (non-Javadoc) */ + protected static boolean waitForClientCacheToClose() { + return waitForClientCacheToClose(DEFAULT_WAIT_DURATION); + } + + /* (non-Javadoc) */ + protected static boolean waitForClientCacheToClose(long duration) { + try { + final ClientCache clientCache = ClientCacheFactory.getAnyInstance(); + + clientCache.close(); + + waitOnCondition(new Condition() { + public boolean evaluate() { + return clientCache.isClosed(); + } + }, duration); + + return clientCache.isClosed(); + } + catch (CacheClosedException ignore) { + return true; + } + + } + + /* (non-Javadoc) */ + protected static boolean waitForProcessToStart(Process process, File directory) { + return waitForProcessToStart(process, directory, DEFAULT_WAIT_DURATION); + } + + /* (non-Javadoc) */ + @SuppressWarnings("all") + protected static boolean waitForProcessToStart(Process process, File directory, long duration) { + final File processControl = new File(directory, DEFAULT_PROCESS_CONTROL_FILENAME); + + waitOnCondition(new Condition() { + public boolean evaluate() { + return processControl.isFile(); + } + }, duration); + + return process.isAlive(); + } + + /* (non-Javadoc) */ + protected static int waitForProcessToStop(Process process, File directory) { + return waitForProcessToStop(process, directory, DEFAULT_WAIT_DURATION); + } + + /* (non-Javadoc) */ + protected static int waitForProcessToStop(Process process, File directory, long duration) { + final long timeout = (System.currentTimeMillis() + duration); + + try { + while (process.isAlive() && System.currentTimeMillis() < timeout) { + if (process.waitFor(DEFAULT_WAIT_INTERVAL, TimeUnit.MILLISECONDS)) { + return process.exitValue(); + } + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return (process.isAlive() ? -1 : process.exitValue()); + } + + /* (non-Javadoc) */ + protected static boolean waitOnCondition(Condition condition) { + return waitOnCondition(condition, DEFAULT_WAIT_DURATION); + } + + /* (non-Javadoc) */ + @SuppressWarnings("all") + protected static boolean waitOnCondition(Condition condition, long duration) { + final long timeout = (System.currentTimeMillis() + duration); + + try { + while (!condition.evaluate() && System.currentTimeMillis() < timeout) { + synchronized (condition) { + TimeUnit.MILLISECONDS.timedWait(condition, DEFAULT_WAIT_INTERVAL); + } + } + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + return condition.evaluate(); + } + + /* (non-Javadoc) */ + protected static File writeProcessControlFile(File path) throws IOException { + assertThat(path != null && path.isDirectory()).isTrue(); + + File processControl = new File(path, DEFAULT_PROCESS_CONTROL_FILENAME); + + assertThat(processControl.createNewFile()).isTrue(); + + processControl.deleteOnExit(); + + return processControl; + } + + /* (non-Javadoc) */ + protected void assertRegion(Region actualRegion, String expectedName, DataPolicy expectedDataPolicy) { + assertThat(actualRegion).isNotNull(); + assertThat(actualRegion.getName()).isEqualTo(expectedName); + assertThat(actualRegion.getFullPath()).isEqualTo(GemFireUtils.toRegionPath(expectedName)); + assertThat(actualRegion.getAttributes()).isNotNull(); + 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); + } + + /* (non-Javadoc) */ + protected void assertEntryIdleTimeout(ExpirationAttributes actualExpirationAttributes, + ExpirationAction expectedAction, int expectedTimeout) { + assertThat(actualExpirationAttributes).isNotNull(); + assertThat(actualExpirationAttributes.getAction()).isEqualTo(expectedAction); + assertThat(actualExpirationAttributes.getTimeout()).isEqualTo(expectedTimeout); + } + + /* (non-Javadoc) */ + protected boolean enableQueryDebugging() { + return DEFAULT_ENABLE_QUERY_DEBUGGING; + } + + /* (non-Javadoc) */ + protected boolean isQueryDebuggingEnabled() { + return (GEMFIRE_QUERY_DEBUG || enableQueryDebugging()); + } + + /* (non-Javadoc) */ + protected List listRegions(GemFireCache gemfireCache) { + Set> regions = gemfireCache.rootRegions(); + + List regionList = new ArrayList(regions.size()); + + for (Region region : regions) { + regionList.add(region.getFullPath()); + } + + return regionList; + } + + /* (non-Javadoc) */ + @SuppressWarnings("unchecked") + protected T createSession() { + T expiringSession = (T) sessionRepository.createSession(); + assertThat(expiringSession).isNotNull(); + return expiringSession; + } + + /* (non-Javadoc) */ + @SuppressWarnings("unchecked") + protected T createSession(String principalName) { + GemFireOperationsSessionRepository.GemFireSession session = createSession(); + session.setPrincipalName(principalName); + return (T) session; + } + + /* (non-Javadoc) */ + protected T expire(T session) { + session.setLastAccessedTime(0l); + return session; + } + + /* (non-Javadoc) */ + @SuppressWarnings("unchecked") + protected T get(String sessionId) { + return (T) sessionRepository.getSession(sessionId); + } + + /* (non-Javadoc) */ + protected T save(T session) { + sessionRepository.save(session); + return session; + } + + /* (non-Javadoc) */ + protected T touch(T session) { + session.setLastAccessedTime(System.currentTimeMillis()); + return session; + } + + /** + * The SessionEventListener class is a Spring {@link ApplicationListener} listening for Spring HTTP Session + * application events. + * + * @see org.springframework.context.ApplicationListener + * @see org.springframework.session.events.AbstractSessionEvent + */ + public static class SessionEventListener implements ApplicationListener { + + private volatile AbstractSessionEvent sessionEvent; + + /* (non-Javadoc) */ + @SuppressWarnings("unchecked") + public T getSessionEvent() { + T sessionEvent = (T) this.sessionEvent; + this.sessionEvent = null; + return sessionEvent; + } + + /* (non-Javadoc) */ + public void onApplicationEvent(AbstractSessionEvent event) { + sessionEvent = event; + } + + /* (non-Javadoc) */ + public T waitForSessionEvent(long duration) { + waitOnCondition(new Condition() { + public boolean evaluate() { + return (sessionEvent != null); + } + }, duration); + + return getSessionEvent(); + } + } + + /** + * The Condition interface defines a logical condition that must be satisfied before it is safe to proceed. + */ + protected interface Condition { + boolean evaluate(); + } + +} diff --git a/docs/src/integration-test/java/docs/http/HttpSessionGemFireIndexingITests.java b/docs/src/integration-test/java/docs/http/HttpSessionGemFireIndexingITests.java new file mode 100644 index 00000000..e893774d --- /dev/null +++ b/docs/src/integration-test/java/docs/http/HttpSessionGemFireIndexingITests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docs.http; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import java.util.Properties; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.CacheFactoryBean; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.session.ExpiringSession; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import docs.AbstractGemFireIntegrationTests; + +/** + * @author Rob Winch + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class HttpSessionGemFireIndexingITests extends AbstractGemFireIntegrationTests { + + @Test + public void findByIndexName() { + ExpiringSession session = sessionRepository.createSession(); + String username = "HttpSessionGemFireIndexingITests-findByIndexName-username"; + + // tag::findbyindexname-set[] + String indexName = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; + session.setAttribute(indexName, username); + // end::findbyindexname-set[] + + sessionRepository.save(session); + + // tag::findbyindexname-get[] + Map idToSessions = sessionRepository.findByIndexNameAndIndexValue(indexName, username); + // end::findbyindexname-get[] + + assertThat(idToSessions.keySet()).containsOnly(session.getId()); + + sessionRepository.delete(session.getId()); + } + + @Test + @WithMockUser("HttpSessionGemFireIndexingITests-findBySpringSecurityIndexName") + public void findBySpringSecurityIndexName() { + ExpiringSession session = sessionRepository.createSession(); + + // tag::findbyspringsecurityindexname-context[] + SecurityContext context = SecurityContextHolder.getContext(); + Authentication authentication = context.getAuthentication(); + // end::findbyspringsecurityindexname-context[] + + session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context); + sessionRepository.save(session); + + // tag::findbyspringsecurityindexname-get[] + String indexName = FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; + Map idToSessions = sessionRepository.findByIndexNameAndIndexValue(indexName, authentication.getName()); + // end::findbyspringsecurityindexname-get[] + + assertThat(idToSessions.keySet()).containsOnly(session.getId()); + + sessionRepository.delete(session.getId()); + } + + @Configuration + @EnableGemFireHttpSession + static class Config { + + @Bean + Properties gemfireProperties() { + Properties gemfireProperties = new Properties(); + + gemfireProperties.setProperty("name", Config.class.getName()); + gemfireProperties.setProperty("mcast-port", "0"); + gemfireProperties.setProperty("log-level", GEMFIRE_LOG_LEVEL); + + return gemfireProperties; + } + + @Bean + CacheFactoryBean gemfireCache() { + CacheFactoryBean gemfireCache = new CacheFactoryBean(); + + gemfireCache.setClose(true); + gemfireCache.setLazyInitialize(false); + gemfireCache.setProperties(gemfireProperties()); + gemfireCache.setUseBeanFactoryLocator(false); + + return gemfireCache; + } + } +} diff --git a/docs/src/integration-test/java/docs/http/gemfire/indexablesessionattributes/GemFireHttpSessionConfig.java b/docs/src/integration-test/java/docs/http/gemfire/indexablesessionattributes/GemFireHttpSessionConfig.java new file mode 100644 index 00000000..13ad608c --- /dev/null +++ b/docs/src/integration-test/java/docs/http/gemfire/indexablesessionattributes/GemFireHttpSessionConfig.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docs.http.gemfire.indexablesessionattributes; + +import java.util.Properties; + +import org.springframework.context.annotation.Bean; +import org.springframework.data.gemfire.CacheFactoryBean; +import org.springframework.session.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession; + +import docs.AbstractGemFireIntegrationTests; + +/** + * @author Rob Winch + * + */ +// tag::class-start[] +@EnableGemFireHttpSession(indexableSessionAttributes = { "name1", "name2", "name3" }) +public class GemFireHttpSessionConfig { +// end::class-start[] + + @Bean + Properties gemfireProperties() { + Properties gemfireProperties = new Properties(); + + gemfireProperties.setProperty("name", GemFireHttpSessionConfig.class.getName()); + gemfireProperties.setProperty("mcast-port", "0"); + gemfireProperties.setProperty("log-level", AbstractGemFireIntegrationTests.GEMFIRE_LOG_LEVEL); + + return gemfireProperties; + } + + @Bean + CacheFactoryBean gemfireCache() { + CacheFactoryBean gemfireCache = new CacheFactoryBean(); + + gemfireCache.setClose(true); + gemfireCache.setLazyInitialize(false); + gemfireCache.setProperties(gemfireProperties()); + gemfireCache.setUseBeanFactoryLocator(false); + + return gemfireCache; + } +} diff --git a/docs/src/integration-test/java/docs/http/gemfire/indexablesessionattributes/HttpSessionGemFireIndexingCustomITests.java b/docs/src/integration-test/java/docs/http/gemfire/indexablesessionattributes/HttpSessionGemFireIndexingCustomITests.java new file mode 100644 index 00000000..780c2889 --- /dev/null +++ b/docs/src/integration-test/java/docs/http/gemfire/indexablesessionattributes/HttpSessionGemFireIndexingCustomITests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package docs.http.gemfire.indexablesessionattributes; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.session.ExpiringSession; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import docs.AbstractGemFireIntegrationTests; + +/** + * @author Rob Winch + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes=GemFireHttpSessionConfig.class) +public class HttpSessionGemFireIndexingCustomITests extends AbstractGemFireIntegrationTests { + + @Test + public void findByIndexName() { + ExpiringSession session = sessionRepository.createSession(); + String attrValue = "HttpSessionGemFireIndexingCustomITests-findByIndexName"; + + // tag::findbyindexname-set[] + String indexName = "name1"; + session.setAttribute(indexName, attrValue); + // end::findbyindexname-set[] + + sessionRepository.save(session); + + // tag::findbyindexname-get[] + Map idToSessions = sessionRepository.findByIndexNameAndIndexValue(indexName, attrValue); + // end::findbyindexname-get[] + + assertThat(idToSessions.keySet()).containsOnly(session.getId()); + + sessionRepository.delete(session.getId()); + } +} diff --git a/docs/src/test/java/docs/http/AbstractHttpSessionListenerTests.java b/docs/src/test/java/docs/http/AbstractHttpSessionListenerTests.java index 32bbd4f5..4a7fb67a 100644 --- a/docs/src/test/java/docs/http/AbstractHttpSessionListenerTests.java +++ b/docs/src/test/java/docs/http/AbstractHttpSessionListenerTests.java @@ -69,7 +69,6 @@ public abstract class AbstractHttpSessionListenerTests { /* (non-Javadoc) * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent) */ - @Override public void onApplicationEvent(SessionDestroyedEvent event) { this.event = event; } diff --git a/spring-session/build.gradle b/spring-session/build.gradle index 8587b73e..f608fdbf 100644 --- a/spring-session/build.gradle +++ b/spring-session/build.gradle @@ -17,7 +17,7 @@ configurations { dependencies { optional "org.springframework.data:spring-data-redis:$springDataRedisVersion", "com.hazelcast:hazelcast:$hazelcastVersion", - "org.springframework.data:spring-data-gemfire:$springDataGemFireVersion", + "org.springframework.data:spring-data-gemfire:$springDataGemFireVersion", "org.springframework:spring-context:$springVersion", "org.springframework:spring-web:$springVersion", "org.springframework:spring-messaging:$springVersion", 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 daa98c6c..291f4a1c 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 @@ -111,7 +111,7 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract return doFindByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName); } - @SuppressWarnings({ "unused", "unchecked" }) + @SuppressWarnings({ "unchecked" }) protected Map doFindByPrincipalName(String regionName, String principalName) { try { Region region = gemfireCache.getRegion(regionName); @@ -327,7 +327,6 @@ public class GemFireOperationsSessionRepositoryIntegrationTests extends Abstract @EnableGemFireHttpSession(regionName = SPRING_SESSION_GEMFIRE_REGION_NAME, maxInactiveIntervalInSeconds = MAX_INACTIVE_INTERVAL_IN_SECONDS) - @SuppressWarnings("unused") static class SpringSessionGemFireConfiguration { @Bean