From 2de17a52f6d688b5b3307b91e01bc607a89b7637 Mon Sep 17 00:00:00 2001 From: Sebastian Laskawiec Date: Thu, 13 Oct 2016 10:39:07 +0200 Subject: [PATCH] ISPN-7009 Spring-session implementation --- .../asciidoc/upgrading/upgrading.asciidoc | 14 +- .../asciidoc/user_guide/integrations.adoc | 77 +++++++++- integrationtests/spring-boot-it/pom.xml | 145 ++++++++++++++++++ .../session/AbstractSpringSessionTCK.java | 39 +++++ .../session/configuration/SecurityConfig.java | 18 +++ .../boot/session/configuration/WebConfig.java | 28 ++++ .../embedded/EmbeddedConfiguration.java | 22 +++ .../embedded/EmbeddedSpringSessionTest.java | 12 ++ .../session/remote/RemoteConfiguration.java | 30 ++++ .../remote/RemoteSpringSessionTest.java | 45 ++++++ .../boot/session/web/TestRESTController.java | 17 ++ parent/pom.xml | 3 +- pom.xml | 1 + spring/spring4/pom.xml | 10 ++ spring/spring4/spring4-common/pom.xml | 22 +++ .../org/infinispan/spring/package-info.java | 5 + .../spring/provider/CacheDelegate.java | 10 +- .../spring/provider/SpringCache.java | 9 ++ .../AbstractApplicationPublisherBridge.java | 61 ++++++++ .../AbstractInfinispanSessionRepository.java | 95 ++++++++++++ ...finispanApplicationPublishedBridgeTCK.java | 95 ++++++++++++ .../InfinispanSessionRepositoryTCK.java | 135 ++++++++++++++++ .../spring/session/util/EventsWaiter.java | 32 ++++ spring/spring4/spring4-embedded/pom.xml | 17 ++ .../EmbeddedApplicationPublishedBridge.java | 51 ++++++ .../InfinispanEmbeddedSessionRepository.java | 43 ++++++ .../spring/session/PrincipalNameResolver.java | 42 +++++ .../EnableInfinispanEmbeddedHttpSession.java | 61 ++++++++ ...ispanEmbeddedHttpSessionConfiguration.java | 54 +++++++ ...mbeddedApplicationPublishedBridgeTest.java | 63 ++++++++ ...finispanEmbeddedSessionRepositoryTest.java | 89 +++++++++++ spring/spring4/spring4-remote/pom.xml | 24 ++- .../spring/provider/SpringRemoteCache.java | 94 ------------ .../provider/SpringRemoteCacheManager.java | 5 +- .../InfinispanRemoteSessionRepository.java | 21 +++ .../RemoteApplicationPublishedBridge.java | 50 ++++++ .../EnableInfinispanRemoteHttpSession.java | 61 ++++++++ ...inispanRemoteHttpSessionConfiguration.java | 53 +++++++ ...InfinispanRemoteSessionRepositoryTest.java | 95 ++++++++++++ .../RemoteApplicationPublishedBridgeTest.java | 75 +++++++++ 40 files changed, 1721 insertions(+), 102 deletions(-) create mode 100644 integrationtests/spring-boot-it/pom.xml create mode 100644 integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/AbstractSpringSessionTCK.java create mode 100644 integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/configuration/SecurityConfig.java create mode 100644 integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/configuration/WebConfig.java create mode 100644 integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/embedded/EmbeddedConfiguration.java create mode 100644 integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/embedded/EmbeddedSpringSessionTest.java create mode 100644 integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/remote/RemoteConfiguration.java create mode 100644 integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/remote/RemoteSpringSessionTest.java create mode 100644 integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/web/TestRESTController.java create mode 100644 spring/spring4/spring4-common/src/main/java/org/infinispan/spring/session/AbstractApplicationPublisherBridge.java create mode 100644 spring/spring4/spring4-common/src/main/java/org/infinispan/spring/session/AbstractInfinispanSessionRepository.java create mode 100644 spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/InfinispanApplicationPublishedBridgeTCK.java create mode 100644 spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/InfinispanSessionRepositoryTCK.java create mode 100644 spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/util/EventsWaiter.java create mode 100644 spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/EmbeddedApplicationPublishedBridge.java create mode 100644 spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/InfinispanEmbeddedSessionRepository.java create mode 100644 spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/PrincipalNameResolver.java create mode 100644 spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/configuration/EnableInfinispanEmbeddedHttpSession.java create mode 100644 spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/configuration/InfinispanEmbeddedHttpSessionConfiguration.java create mode 100644 spring/spring4/spring4-embedded/src/test/java/org/infinispan/spring/session/EmbeddedApplicationPublishedBridgeTest.java create mode 100644 spring/spring4/spring4-embedded/src/test/java/org/infinispan/spring/session/InfinispanEmbeddedSessionRepositoryTest.java delete mode 100644 spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/provider/SpringRemoteCache.java create mode 100644 spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/InfinispanRemoteSessionRepository.java create mode 100644 spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/RemoteApplicationPublishedBridge.java create mode 100644 spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/configuration/EnableInfinispanRemoteHttpSession.java create mode 100644 spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/configuration/InfinispanRemoteHttpSessionConfiguration.java create mode 100644 spring/spring4/spring4-remote/src/test/java/org/infinispan/spring/session/InfinispanRemoteSessionRepositoryTest.java create mode 100644 spring/spring4/spring4-remote/src/test/java/org/infinispan/spring/session/RemoteApplicationPublishedBridgeTest.java diff --git a/documentation/src/main/asciidoc/upgrading/upgrading.asciidoc b/documentation/src/main/asciidoc/upgrading/upgrading.asciidoc index a6bf08f75f46..419e6490364c 100644 --- a/documentation/src/main/asciidoc/upgrading/upgrading.asciidoc +++ b/documentation/src/main/asciidoc/upgrading/upgrading.asciidoc @@ -39,7 +39,10 @@ The interface `SegmentCompletionListener` has moved from the interface `org.infinispan.CacheStream` to the new `org.infinispan.BaseCacheStream`. === Spring module dependency changes -All Infinispan dependencies from Spring modules (both embedded and remote) are now in provided scope. One can decide whether use small jars or uber jars but they need to be added to the classpath on client side. Here is an example: +All Infinispan, Spring and Logger dependencies are now in the `provided` scope. One can decide whether to use small jars or uber jars but they need to be added to the classpath of the application. +It also gives one freedom in choosing Spring (or Spring Boot) version. + +Here is an example: [source,xml] ---- @@ -51,8 +54,17 @@ All Infinispan dependencies from Spring modules (both embedded and remote) are n org.infinispan infinispan-spring4-embedded + + org.springframework + spring-context + + + org.springframework.session + spring-session + ---- + Additionally there is no Logger implementation specified (since this may vary depending on use case). === Total order executor is not removed diff --git a/documentation/src/main/asciidoc/user_guide/integrations.adoc b/documentation/src/main/asciidoc/user_guide/integrations.adoc index c0f221b332df..f3fac2d528dd 100644 --- a/documentation/src/main/asciidoc/user_guide/integrations.adoc +++ b/documentation/src/main/asciidoc/user_guide/integrations.adoc @@ -1483,6 +1483,13 @@ Now, you will need to add Infinispan and Spring integration module to your class org.infinispan infinispan-spring4-embedded + ${version.spring} + + + + org.springframework + spring-context + ${version.spring} ---- @@ -1548,8 +1555,76 @@ void deleteBook(Integer bookId) {...} and you may rest assured that no stray books be left in your application once you decide to remove them. +==== Externalizing session using Spring Session + +link:$$http://docs.spring.io/spring-session/docs/current/reference/html5$$[Spring Session] is a very convenient way to externalize user session into Infinispan cluster. + +Spring Session integration allows to use both - embedded and client/server mode. Each mode requires using proper artifacts (`infinispan-spring4-embedded` or `infinispan-spring4-remote`). +An example is shown below: + +[source,xml] +---- + + + org.infinispan + infinispan-embedded + + + org.infinispan + infinispan-spring4-embedded + ${version.spring} + + + org.springframework + spring-context + ${version.spring} + + + org.springframework + spring-session + ${version.spring} + + + org.springframework + spring-web + ${version.spring} + + +---- + +Spring Session integration has been based on Infinispan Spring Cache support so it requires creating a `SpringEmbeddedCacheManagerFactoryBean` or `SpringRemoteCacheManagerFactoryBean`. +The next step it to use `@EnableInfinispanEmbeddedHttpSession` or `@EnableInfinispanRemoteHttpSession` configuration annotation which turns on Spring Session. + +`@EnableInfinispanEmbeddedHttpSession` or `@EnableInfinispanRemoteHttpSession` annotations have 2 optional parameters: + +* maxInactiveIntervalInSeconds - which sets session expiration time in seconds. The default is set to `1800`. +* cacheName - cache name which is used for storing sessions. The default is set to `sessions`. + +A complete, annotation based configuration example is shown below: + +[source, java] +---- +@EnableInfinispanEmbeddedHttpSession +@Configuration +public class Config { + + @Bean + public SpringEmbeddedCacheManagerFactoryBean springCacheManager() { + return new SpringEmbeddedCacheManagerFactoryBean(); + } + + //An optional configuration bean which is responsible for replacing the default cookie + //for obtaining configuration. + //For more information refer to Spring Session documentation. + @Bean + public HttpSessionStrategy httpSessionStrategy() { + return new HeaderHttpSessionStrategy(); + } +} +---- + ==== Conclusion -Hopefully you enjoyed our quick tour of Infinispan's support for Spring's cache abstraction and saw how easy it is for all your caching woes to be taken care of by Infinispan. More information may be found in Spring's link:$$http://docs.spring.io/spring-framework/docs/4.1.1.RELEASE/spring-framework-reference/html/cache.html$$[reference documentation]. Also see link:$$http://spring.io/blog/2011/02/23/spring-3-1-m1-cache-abstraction$$[this link] - a very nice posting on the official Spring blog for a somewhat more comprehensive introduction to Spring's cache abstraction. +Hopefully you enjoyed our quick tour of Infinispan's support for Spring's cache and session abstraction and saw how easy it is for all your caching woes to be taken care of by Infinispan. More information may be found in Spring's link:$$http://docs.spring.io/spring-framework/docs/4.1.1.RELEASE/spring-framework-reference/html/cache.html$$[reference documentation]. Also see link:$$http://spring.io/blog/2011/02/23/spring-3-1-m1-cache-abstraction$$[this link] - a very nice posting on the official Spring blog for a somewhat more comprehensive introduction to Spring's cache abstraction. === Infinispan modules for WildFly diff --git a/integrationtests/spring-boot-it/pom.xml b/integrationtests/spring-boot-it/pom.xml new file mode 100644 index 000000000000..b65084598397 --- /dev/null +++ b/integrationtests/spring-boot-it/pom.xml @@ -0,0 +1,145 @@ + + + 4.0.0 + + + org.infinispan + infinispan-parent + 9.0.0-SNAPSHOT + ../../parent/pom.xml + + + infinispan-spring-boot-it + Integration tests: Spring Boot + Integration tests for Infinispan and Spring Boot + + + + + 4.12 + + + + + + org.springframework.boot + spring-boot-dependencies + ${version.spring-boot} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter + test + + + org.springframework.boot + spring-boot-starter-web + test + + + org.springframework.boot + spring-boot-starter-security + test + + + org.springframework.session + spring-session + test + + + ${project.groupId} + infinispan-spring4-embedded + test + + + ${project.groupId} + infinispan-spring4-remote + test + + + ${project.groupId} + infinispan-core + test + + + ${project.groupId} + infinispan-client-hotrod + test + + + + + org.springframework.boot + spring-boot-starter-test + test + + + ${project.groupId} + infinispan-server-hotrod + test + + + junit + junit + test + ${overriden.version.junit} + + + ${project.groupId} + infinispan-server-hotrod + ${project.version} + test + + + + + + smoke + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + none + + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${suite.exclude.groups} + + ${log4j.configurationFile} + ${project.build.directory} + + false + + + + org.apache.maven.surefire + surefire-junit4 + ${version.maven.surefire} + + + + + + \ No newline at end of file diff --git a/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/AbstractSpringSessionTCK.java b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/AbstractSpringSessionTCK.java new file mode 100644 index 000000000000..8c5580b8e82d --- /dev/null +++ b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/AbstractSpringSessionTCK.java @@ -0,0 +1,39 @@ +package org.infinispan.integrationtests.spring.boot.session; + +import org.junit.Assert; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.embedded.LocalServerPort; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.HttpHeaders; +import org.springframework.session.MapSession; +import org.springframework.session.SessionRepository; + +public class AbstractSpringSessionTCK { + + @Autowired + private SessionRepository sessionRepository; + + @LocalServerPort + private int port; + + @Test + public void testCreatingSessionWhenUsingREST() throws Exception { + //given + TestRestTemplate restTemplate = new TestRestTemplate("user", "password"); + + //when + HttpHeaders httpHeaders = restTemplate.headForHeaders(getTestURL()); + + //then + Assert.assertNotNull(sessionRepository.getSession(getSessionId(httpHeaders))); + } + + private String getTestURL() { + return "http://localhost:" + port + "/test"; + } + + private String getSessionId(HttpHeaders httpHeaders) { + return httpHeaders.getValuesAsList("x-auth-token").get(0); + } +} diff --git a/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/configuration/SecurityConfig.java b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/configuration/SecurityConfig.java new file mode 100644 index 000000000000..5ea8b8bcb1fd --- /dev/null +++ b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/configuration/SecurityConfig.java @@ -0,0 +1,18 @@ +package org.infinispan.integrationtests.spring.boot.session.configuration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +/** + * Since Spring Session is heavily based on security - we need to define basic user/password. + */ +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); + } +} diff --git a/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/configuration/WebConfig.java b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/configuration/WebConfig.java new file mode 100644 index 000000000000..e39bafaf68ed --- /dev/null +++ b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/configuration/WebConfig.java @@ -0,0 +1,28 @@ +package org.infinispan.integrationtests.spring.boot.session.configuration; + +import org.infinispan.integrationtests.spring.boot.session.web.TestRESTController; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.session.web.http.HeaderHttpSessionStrategy; +import org.springframework.session.web.http.HttpSessionStrategy; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration +@EnableWebMvc +public class WebConfig { + + @Bean + public HttpSessionStrategy httpSessionStrategy() { + return new HeaderHttpSessionStrategy(); + } + + @Bean + public TestRESTController sessionCreator() { + return new TestRESTController(); + } + + @Bean + public SecurityConfig securityConfig() { + return new SecurityConfig(); + } +} diff --git a/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/embedded/EmbeddedConfiguration.java b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/embedded/EmbeddedConfiguration.java new file mode 100644 index 000000000000..83f428e2af90 --- /dev/null +++ b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/embedded/EmbeddedConfiguration.java @@ -0,0 +1,22 @@ +package org.infinispan.integrationtests.spring.boot.session.embedded; + +import org.infinispan.integrationtests.spring.boot.session.configuration.WebConfig; +import org.infinispan.spring.provider.SpringEmbeddedCacheManagerFactoryBean; +import org.infinispan.spring.session.configuration.EnableInfinispanEmbeddedHttpSession; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@EnableAutoConfiguration +@EnableInfinispanEmbeddedHttpSession +@Import(WebConfig.class) +public class EmbeddedConfiguration { + + @Bean + public SpringEmbeddedCacheManagerFactoryBean springCacheManager() { + return new SpringEmbeddedCacheManagerFactoryBean(); + } + +} diff --git a/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/embedded/EmbeddedSpringSessionTest.java b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/embedded/EmbeddedSpringSessionTest.java new file mode 100644 index 000000000000..fee5a1d53921 --- /dev/null +++ b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/embedded/EmbeddedSpringSessionTest.java @@ -0,0 +1,12 @@ +package org.infinispan.integrationtests.spring.boot.session.embedded; + +import org.infinispan.integrationtests.spring.boot.session.AbstractSpringSessionTCK; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = EmbeddedConfiguration.class, webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) +public class EmbeddedSpringSessionTest extends AbstractSpringSessionTCK { + +} diff --git a/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/remote/RemoteConfiguration.java b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/remote/RemoteConfiguration.java new file mode 100644 index 000000000000..3ac413410c63 --- /dev/null +++ b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/remote/RemoteConfiguration.java @@ -0,0 +1,30 @@ +package org.infinispan.integrationtests.spring.boot.session.remote; + +import java.net.InetSocketAddress; +import java.util.Arrays; + +import org.infinispan.integrationtests.spring.boot.session.configuration.WebConfig; +import org.infinispan.spring.provider.SpringRemoteCacheManagerFactoryBean; +import org.infinispan.spring.session.configuration.EnableInfinispanRemoteHttpSession; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.util.SocketUtils; + +@Configuration +@EnableAutoConfiguration +@EnableInfinispanRemoteHttpSession +@Import(WebConfig.class) +public class RemoteConfiguration { + + public static final int SERVER_PORT = SocketUtils.findAvailableTcpPort(); + + @Bean + public SpringRemoteCacheManagerFactoryBean springCacheManager() { + SpringRemoteCacheManagerFactoryBean factoryBean = new SpringRemoteCacheManagerFactoryBean(); + factoryBean.setServerList(Arrays.asList(new InetSocketAddress("localhost", SERVER_PORT))); + return factoryBean; + } + +} diff --git a/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/remote/RemoteSpringSessionTest.java b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/remote/RemoteSpringSessionTest.java new file mode 100644 index 000000000000..f01a12566bbc --- /dev/null +++ b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/remote/RemoteSpringSessionTest.java @@ -0,0 +1,45 @@ +package org.infinispan.integrationtests.spring.boot.session.remote; + +import org.infinispan.commons.equivalence.AnyServerEquivalence; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.integrationtests.spring.boot.session.AbstractSpringSessionTCK; +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.server.hotrod.HotRodServer; +import org.infinispan.server.hotrod.configuration.HotRodServerConfigurationBuilder; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = RemoteConfiguration.class, webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT) +public class RemoteSpringSessionTest extends AbstractSpringSessionTCK { + + private static EmbeddedCacheManager serverCache; + + private static HotRodServer server; + + @BeforeClass + public static void beforeclass() { + ConfigurationBuilder cacheConfiguration = new ConfigurationBuilder(); + cacheConfiguration.dataContainer().keyEquivalence(new AnyServerEquivalence()); + + serverCache = new DefaultCacheManager(); + serverCache.defineConfiguration("sessions", cacheConfiguration.build()); + + HotRodServerConfigurationBuilder hotRodServerConfigurationBuilder = new HotRodServerConfigurationBuilder(); + hotRodServerConfigurationBuilder.port(RemoteConfiguration.SERVER_PORT); + + server = new HotRodServer(); + server.start(hotRodServerConfigurationBuilder.build(), serverCache); + } + + @AfterClass + public static void afterClass() { + server.stop(); + serverCache.stop(); + } + +} diff --git a/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/web/TestRESTController.java b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/web/TestRESTController.java new file mode 100644 index 000000000000..bb100abbcd82 --- /dev/null +++ b/integrationtests/spring-boot-it/src/test/java/org/infinispan/integrationtests/spring/boot/session/web/TestRESTController.java @@ -0,0 +1,17 @@ +package org.infinispan.integrationtests.spring.boot.session.web; + +import org.apache.catalina.servlet4preview.http.HttpServletRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class TestRESTController { + + @RequestMapping("/test") + public ResponseEntity testRest(HttpServletRequest request){ + return new ResponseEntity(HttpStatus.OK); + } + +} diff --git a/parent/pom.xml b/parent/pom.xml index 7035f43c7986..d00a60c69e4c 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -165,7 +165,8 @@ 1.1.0.Final 1.0.5 3.1.0.Final - 3.2.9.RELEASE + 1.2.2.RELEASE + 1.4.0.RELEASE 4.1.0.RELEASE 2.12.1 6.8.8 diff --git a/pom.xml b/pom.xml index b394c5b4799d..cc6ffe23c1c4 100644 --- a/pom.xml +++ b/pom.xml @@ -88,6 +88,7 @@ integrationtests/all-embedded-it integrationtests/all-embedded-query-it integrationtests/all-remote-it + integrationtests/spring-boot-it diff --git a/spring/spring4/pom.xml b/spring/spring4/pom.xml index 0bbf21529e61..4c76c410a806 100644 --- a/spring/spring4/pom.xml +++ b/spring/spring4/pom.xml @@ -84,6 +84,16 @@ spring-context-support ${version.spring4} + + org.springframework.session + spring-session + ${version.spring-session} + + + org.springframework + spring-web + ${version.spring4} + diff --git a/spring/spring4/spring4-common/pom.xml b/spring/spring4/spring4-common/pom.xml index 6fe12ab94c13..39ca2c41baca 100644 --- a/spring/spring4/spring4-common/pom.xml +++ b/spring/spring4/spring4-common/pom.xml @@ -16,12 +16,34 @@ org.springframework spring-context + provided + + + org.springframework.session + spring-session + provided ${project.groupId} infinispan-commons provided + + ${project.groupId} + infinispan-core + test-jar + test + + + ${project.groupId} + infinispan-core + test + + + org.testng + testng + test + diff --git a/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/package-info.java b/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/package-info.java index 25085b4e2c71..b9178d1da153 100644 --- a/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/package-info.java +++ b/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/package-info.java @@ -13,6 +13,11 @@ * interface for easing usage of JBoss Infinispan within the Spring programming model.

* See package {@link org.infinispan.spring.support org.infinispan.spring.support}. * + *
  • + * Provide implementations of Spring's Session + * {@link org.springframework.session.SessionRepository SessionRepository} interface for session + * management with Spring and Spring Security. + *
  • * *

    */ diff --git a/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/provider/CacheDelegate.java b/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/provider/CacheDelegate.java index 08d52d0dd674..2e1ea872f28a 100644 --- a/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/provider/CacheDelegate.java +++ b/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/provider/CacheDelegate.java @@ -1,5 +1,7 @@ package org.infinispan.spring.provider; +import java.util.concurrent.TimeUnit; + import org.springframework.cache.Cache; import org.springframework.cache.support.SimpleValueWrapper; import org.springframework.util.Assert; @@ -64,6 +66,13 @@ public void put(final Object key, final Object value) { this.nativeCache.put(key, value != null ? value : NullValue.NULL); } + /** + * @see org.infinispan.commons.api.BasicCache#put(Object, Object, long, TimeUnit) + */ + public void put(Object key, Object value, long lifespan, TimeUnit unit) { + this.nativeCache.put(key, value != null ? value : NullValue.NULL, lifespan, unit); + } + @Override public ValueWrapper putIfAbsent(Object key, Object value) { return toValueWrapper(this.nativeCache.putIfAbsent(key, value)); @@ -103,5 +112,4 @@ private ValueWrapper toValueWrapper(Object value) { } return new SimpleValueWrapper(value); } - } diff --git a/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/provider/SpringCache.java b/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/provider/SpringCache.java index 7ffb41d2bfb7..39670caec525 100644 --- a/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/provider/SpringCache.java +++ b/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/provider/SpringCache.java @@ -1,5 +1,7 @@ package org.infinispan.spring.provider; +import java.util.concurrent.TimeUnit; + import org.springframework.cache.Cache; /** @@ -61,6 +63,13 @@ public void put(final Object key, final Object value) { this.cacheImplementation.put(key, value); } + /** + * @see CacheDelegate#put(Object, Object, long, TimeUnit). + */ + public void put(final Object key, final Object value, long lifespan, TimeUnit unit) { + this.cacheImplementation.put(key, value, lifespan, unit); + } + @Override public ValueWrapper putIfAbsent(Object key, Object value) { return cacheImplementation.putIfAbsent(key, value); diff --git a/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/session/AbstractApplicationPublisherBridge.java b/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/session/AbstractApplicationPublisherBridge.java new file mode 100644 index 000000000000..8d371aba6c76 --- /dev/null +++ b/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/session/AbstractApplicationPublisherBridge.java @@ -0,0 +1,61 @@ +package org.infinispan.spring.session; + +import java.util.Objects; +import java.util.Optional; + +import org.infinispan.commons.logging.Log; +import org.infinispan.commons.logging.LogFactory; +import org.infinispan.spring.provider.SpringCache; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.session.events.SessionCreatedEvent; +import org.springframework.session.events.SessionDeletedEvent; +import org.springframework.session.events.SessionDestroyedEvent; +import org.springframework.session.events.SessionExpiredEvent; + +/** + * A bridge for passing events between Infinispan (both embedded and remote) and Spring. + * + * @author Sebastian Łaskawiec + * @since 9.0 + */ +public abstract class AbstractApplicationPublisherBridge implements ApplicationEventPublisherAware { + + protected static final Log logger = LogFactory.getLog(AbstractApplicationPublisherBridge.class); + + protected final SpringCache eventSource; + protected Optional springEventsPublisher = Optional.empty(); + + protected AbstractApplicationPublisherBridge(SpringCache eventSource) { + Objects.requireNonNull(eventSource); + this.eventSource = eventSource; + } + + protected abstract void registerListener(); + public abstract void unregisterListener(); + + protected void emitSessionCreatedEvent(String sessionId) { + logger.debugf("Emitting session created %s", sessionId); + springEventsPublisher.ifPresent(p -> p.publishEvent(new SessionCreatedEvent(eventSource, sessionId))); + } + + protected void emitSessionExpiredEvent(String sessionId) { + logger.debugf("Emitting session expired %s", sessionId); + springEventsPublisher.ifPresent(p -> p.publishEvent(new SessionExpiredEvent(eventSource, sessionId))); + } + + protected void emitSessionDestroyedEvent(String sessionId) { + logger.debugf("Emitting session destroyed %s", sessionId); + springEventsPublisher.ifPresent(p -> p.publishEvent(new SessionDestroyedEvent(eventSource, sessionId))); + } + + protected void emitSessionDeletedEvent(String sessionId) { + logger.debugf("Emitting session deleted %s", sessionId); + springEventsPublisher.ifPresent(p -> p.publishEvent(new SessionDeletedEvent(eventSource, sessionId))); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + springEventsPublisher = Optional.ofNullable(applicationEventPublisher); + } +} diff --git a/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/session/AbstractInfinispanSessionRepository.java b/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/session/AbstractInfinispanSessionRepository.java new file mode 100644 index 000000000000..c6661ce681e1 --- /dev/null +++ b/spring/spring4/spring4-common/src/main/java/org/infinispan/spring/session/AbstractInfinispanSessionRepository.java @@ -0,0 +1,95 @@ +package org.infinispan.spring.session; + +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +import org.infinispan.spring.provider.SpringCache; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.session.MapSession; +import org.springframework.session.SessionRepository; + +/** + * Infinispan implementation for Spring Session with basic functionality. + * + * @author Sebastian Łaskawiec + * @see Spring Session Web Page + * @see SessionRepository + * @see ApplicationEventPublisherAware + * @since 9.0 + */ +public abstract class AbstractInfinispanSessionRepository implements SessionRepository, ApplicationEventPublisherAware, InitializingBean, DisposableBean { + + protected final AbstractApplicationPublisherBridge applicationEventPublisher; + protected final SpringCache cache; + + protected AbstractInfinispanSessionRepository(SpringCache cache, AbstractApplicationPublisherBridge eventsBridge) { + Objects.requireNonNull(cache, "SpringCache can not be null"); + Objects.requireNonNull(eventsBridge, "EventBridge can not be null"); + applicationEventPublisher = eventsBridge; + this.cache = cache; + } + + @Override + public void afterPropertiesSet() throws Exception { + applicationEventPublisher.registerListener(); + } + + @Override + public void destroy() throws Exception { + applicationEventPublisher.unregisterListener(); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.applicationEventPublisher.setApplicationEventPublisher(applicationEventPublisher); + } + + @Override + public MapSession createSession() { + MapSession result = new MapSession(); + result.setCreationTime(System.currentTimeMillis()); + return result; + } + + @Override + public void save(MapSession session) { + cache.put(session.getId(), session, session.getMaxInactiveIntervalInSeconds(), TimeUnit.SECONDS); + } + + @Override + public MapSession getSession(String id) { + return getSession(id, true); + } + + /** + * Returns session with optional parameter whether or not update time accessed. + * + * @param id Session ID. + * @param updateTTL true if time accessed needs to be updated. + * @return Session or null if it doesn't exist. + */ + public MapSession getSession(String id, boolean updateTTL) { + return Optional.ofNullable(cache.get(id)) + .map(v -> (MapSession) v.get()) + .map(v -> updateTTL(v, updateTTL)) + .orElse(null); + } + + protected MapSession updateTTL(MapSession session, boolean updateTTL) { + if (updateTTL) { + session.setLastAccessedTime(System.currentTimeMillis()); + cache.put(session.getId(), session, session.getMaxInactiveIntervalInSeconds(), TimeUnit.SECONDS); + } + return session; + } + + @Override + public void delete(String id) { + applicationEventPublisher.emitSessionDeletedEvent(id); + cache.evict(id); + } +} diff --git a/spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/InfinispanApplicationPublishedBridgeTCK.java b/spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/InfinispanApplicationPublishedBridgeTCK.java new file mode 100644 index 000000000000..9849b578254c --- /dev/null +++ b/spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/InfinispanApplicationPublishedBridgeTCK.java @@ -0,0 +1,95 @@ +package org.infinispan.spring.session; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.infinispan.spring.provider.SpringCache; +import org.infinispan.spring.session.util.EventsWaiter; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.session.MapSession; +import org.springframework.session.events.SessionCreatedEvent; +import org.springframework.session.events.SessionDeletedEvent; +import org.springframework.session.events.SessionExpiredEvent; +import org.testng.annotations.Test; + +public abstract class InfinispanApplicationPublishedBridgeTCK { + + protected SpringCache springCache; + + protected AbstractInfinispanSessionRepository sessionRepository; + + protected abstract SpringCache createSpringCache(); + + protected abstract void callEviction(); + + protected abstract AbstractInfinispanSessionRepository createRepository(SpringCache springCache) throws Exception; + + protected void init() throws Exception { + springCache = createSpringCache(); + sessionRepository = createRepository(springCache); + } + + @Test + public void testEventBridge() throws Exception { + //given + EventsCollector eventsCollector = new EventsCollector(); + + sessionRepository.setApplicationEventPublisher(eventsCollector); + + //when + MapSession sessionToBeDeleted = sessionRepository.createSession(); + MapSession sessionToBeExpired = sessionRepository.createSession(); + sessionToBeExpired.setMaxInactiveIntervalInSeconds(1); + + sessionRepository.save(sessionToBeExpired); + sessionRepository.save(sessionToBeDeleted); + sessionRepository.delete(sessionToBeDeleted.getId()); + + TimeUnit.SECONDS.sleep(1); + callEviction(); + + //then + assertNull(springCache.get(sessionToBeExpired.getId())); + assertNull(springCache.get(sessionToBeDeleted.getId())); + EventsWaiter.assertNumberOfEvents(() -> eventsCollector.getEvents(), SessionCreatedEvent.class, 2, 2, TimeUnit.SECONDS); + EventsWaiter.assertNumberOfEvents(() -> eventsCollector.getEvents(), SessionDeletedEvent.class, 1, 2, TimeUnit.SECONDS); + EventsWaiter.assertNumberOfEvents(() -> eventsCollector.getEvents(), SessionExpiredEvent.class, 1, 2, TimeUnit.SECONDS); + //FIXME: This doesn't work for remote... why? https://issues.jboss.org/browse/ISPN-7040 +// EventsWaiter.assertNumberOfEvents(() -> eventsCollector.getEvents(), SessionDestroyedEvent.class, 2, 10, TimeUnit.SECONDS); + } + + @Test + public void testUnregistration() throws Exception { + //given + EventsCollector eventsCollector = new EventsCollector(); + + sessionRepository.setApplicationEventPublisher(eventsCollector); + + //when + sessionRepository.destroy(); //simulate closing app context + MapSession sessionToBeExpired = sessionRepository.createSession(); + sessionRepository.save(sessionToBeExpired); + + //then + assertEquals(eventsCollector.getEvents().count(), 0); + } + + static class EventsCollector implements ApplicationEventPublisher { + private List events = new CopyOnWriteArrayList<>(); + + @Override + public void publishEvent(ApplicationEvent event) { + events.add(event); + } + + public Stream getEvents() { + return events.stream(); + } + } +} diff --git a/spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/InfinispanSessionRepositoryTCK.java b/spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/InfinispanSessionRepositoryTCK.java new file mode 100644 index 000000000000..a677ca720a0e --- /dev/null +++ b/spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/InfinispanSessionRepositoryTCK.java @@ -0,0 +1,135 @@ +package org.infinispan.spring.session; + +import static org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +import java.util.concurrent.TimeUnit; + +import org.infinispan.spring.provider.SpringCache; +import org.infinispan.test.AbstractInfinispanTest; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.MapSession; +import org.testng.annotations.Test; + +public abstract class InfinispanSessionRepositoryTCK extends AbstractInfinispanTest { + + protected SpringCache springCache; + + protected AbstractInfinispanSessionRepository sessionRepository; + + protected abstract SpringCache createSpringCache(); + + protected abstract AbstractInfinispanSessionRepository createRepository(SpringCache springCache) throws Exception; + + protected void init() throws Exception { + springCache = createSpringCache(); + sessionRepository = createRepository(springCache); + } + + @Test(expectedExceptions = NullPointerException.class) + public void testThrowingExceptionOnNullSpringCache() throws Exception { + createRepository(null); + } + + @Test + public void testCreatingSession() throws Exception { + //when + MapSession session = sessionRepository.createSession(); + + //then + assertTrue(session.getId() != null); + assertTrue(session.getCreationTime() != 0); + assertNull(sessionRepository.getSession(session.getId())); + } + + @Test + public void testSavingSession() throws Exception { + //when + MapSession session = sessionRepository.createSession(); + sessionRepository.save(session); + + //then + assertNotNull(sessionRepository.getSession(session.getId())); + } + + @Test + public void testUpdatingTTLOnAccessingData() throws Exception { + //when + MapSession session = sessionRepository.createSession(); + long accessTimeBeforeSaving = session.getLastAccessedTime(); + + sessionRepository.save(session); + long accessTimeAfterSaving = session.getLastAccessedTime(); + + long accessTimeAfterAccessing = sessionRepository.getSession(session.getId()).getLastAccessedTime(); + + //then + assertTrue(accessTimeBeforeSaving > 0); + assertTrue(accessTimeBeforeSaving <= System.currentTimeMillis()); + assertTrue(accessTimeAfterSaving > 0); + assertTrue(accessTimeAfterSaving <= System.currentTimeMillis()); + assertTrue(accessTimeAfterAccessing > 0); + assertTrue(accessTimeAfterAccessing >= accessTimeAfterSaving); + } + + @Test + public void testDeletingSession() throws Exception { + //when + MapSession session = sessionRepository.createSession(); + sessionRepository.save(session); + sessionRepository.delete(session.getId()); + + //then + assertNull(sessionRepository.getSession(session.getId())); + } + + @Test(timeOut = 5000) + public void testEvictingSession() throws Exception { + //when + MapSession session = sessionRepository.createSession(); + session.setMaxInactiveIntervalInSeconds(1); + sessionRepository.save(session); + + //then + while (sessionRepository.getSession(session.getId(), false) != null) { + TimeUnit.MILLISECONDS.sleep(500); + } + } + + @Test + public void testExtractingPrincipalWithWrongIndexName() throws Exception { + //when + int sizeWithWrongIndexName = ((FindByIndexNameSessionRepository) sessionRepository).findByIndexNameAndIndexValue("wrongIndexName", "").size(); + int sizeWithNullIndexName = ((FindByIndexNameSessionRepository) sessionRepository).findByIndexNameAndIndexValue(null, "").size(); + + //then + assertTrue(sizeWithNullIndexName == 0); + assertTrue(sizeWithWrongIndexName == 0); + } + + @Test + public void testExtractingPrincipal() throws Exception { + //given + addEmptySessionWithPrincipal(sessionRepository, "test1"); + addEmptySessionWithPrincipal(sessionRepository, "test2"); + addEmptySessionWithPrincipal(sessionRepository, "test3"); + + //when + int numberOfTest1Users = ((FindByIndexNameSessionRepository) sessionRepository) + .findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, "test1").size(); + int numberOfNonExistingUsers = ((FindByIndexNameSessionRepository) sessionRepository) + .findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, "notExisting").size(); + + //then + assertTrue(numberOfTest1Users == 1); + assertTrue(numberOfNonExistingUsers == 0); + } + + protected void addEmptySessionWithPrincipal(AbstractInfinispanSessionRepository sessionRepository, String principalName) { + MapSession session = sessionRepository.createSession(); + session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principalName); + sessionRepository.save(session); + } +} diff --git a/spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/util/EventsWaiter.java b/spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/util/EventsWaiter.java new file mode 100644 index 000000000000..afabc546ffac --- /dev/null +++ b/spring/spring4/spring4-common/src/test/java/org/infinispan/spring/session/util/EventsWaiter.java @@ -0,0 +1,32 @@ +package org.infinispan.spring.session.util; + +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.springframework.context.ApplicationEvent; +import org.springframework.session.events.AbstractSessionEvent; + +public class EventsWaiter { + + public static void assertNumberOfEvents(Supplier> eventCollector, + Class eventClass, + int expectedNumberOfEvents, + int timeout, + TimeUnit timeoutUnit) { + long stopTime = System.currentTimeMillis() + timeoutUnit.toMillis(timeout); + + long eventsCollected = -1; + while (System.currentTimeMillis() < stopTime) { + eventsCollected = eventCollector.get() + .filter(e -> e.getClass() == eventClass) + .count(); + if (expectedNumberOfEvents == eventsCollected) { + return; + } + } + throw new AssertionError("Expected " + expectedNumberOfEvents + " events of a class " + eventClass.getSimpleName() + " but found " + eventsCollected); + + } + +} diff --git a/spring/spring4/spring4-embedded/pom.xml b/spring/spring4/spring4-embedded/pom.xml index 1ce1c01477b9..b930d85bbe43 100644 --- a/spring/spring4/spring4-embedded/pom.xml +++ b/spring/spring4/spring4-embedded/pom.xml @@ -22,6 +22,12 @@ org.springframework spring-context + provided + + + org.springframework.session + spring-session + provided ${project.groupId} @@ -43,6 +49,12 @@ infinispan-commons-test test + + ${project.groupId} + infinispan-spring4-common + test-jar + test + ${project.groupId} infinispan-core @@ -76,6 +88,11 @@ spring-context-support test + + org.springframework + spring-web + test + com.h2database h2 diff --git a/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/EmbeddedApplicationPublishedBridge.java b/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/EmbeddedApplicationPublishedBridge.java new file mode 100644 index 000000000000..52519891fe2f --- /dev/null +++ b/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/EmbeddedApplicationPublishedBridge.java @@ -0,0 +1,51 @@ +package org.infinispan.spring.session; + +import org.infinispan.AdvancedCache; +import org.infinispan.notifications.Listener; +import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated; +import org.infinispan.notifications.cachelistener.annotation.CacheEntryExpired; +import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved; +import org.infinispan.notifications.cachelistener.event.CacheEntryCreatedEvent; +import org.infinispan.notifications.cachelistener.event.CacheEntryExpiredEvent; +import org.infinispan.notifications.cachelistener.event.CacheEntryRemovedEvent; +import org.infinispan.spring.provider.SpringCache; + +/** + * A bridge between Infinispan Embedded events and Spring. + * + * @author Sebastian Łaskawiec + * @since 9.0 + */ +@Listener(observation = Listener.Observation.POST) +public class EmbeddedApplicationPublishedBridge extends AbstractApplicationPublisherBridge { + + public EmbeddedApplicationPublishedBridge(SpringCache eventSource) { + super(eventSource); + } + + @Override + protected void registerListener() { + ((AdvancedCache) eventSource.getNativeCache()).addListener(this); + } + + @Override + public void unregisterListener() { + ((AdvancedCache) eventSource.getNativeCache()).removeListener(this); + } + + @CacheEntryCreated + public void processCacheEntryCreated(CacheEntryCreatedEvent event) { + emitSessionCreatedEvent((String) event.getKey()); + } + + @CacheEntryExpired + public void processCacheEntryExpired(CacheEntryExpiredEvent event) { + emitSessionExpiredEvent((String) event.getKey()); + emitSessionDestroyedEvent((String) event.getKey()); + } + + @CacheEntryRemoved + public void processCacheEntryDestroyed(CacheEntryRemovedEvent event) { + emitSessionDestroyedEvent((String) event.getKey()); + } +} diff --git a/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/InfinispanEmbeddedSessionRepository.java b/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/InfinispanEmbeddedSessionRepository.java new file mode 100644 index 000000000000..98c1d5cee379 --- /dev/null +++ b/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/InfinispanEmbeddedSessionRepository.java @@ -0,0 +1,43 @@ +package org.infinispan.spring.session; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.infinispan.spring.provider.SpringCache; +import org.springframework.session.FindByIndexNameSessionRepository; +import org.springframework.session.MapSession; + +/** + * Session Repository for Infinispan in Embedded mode. + * + * @author Sebastian Łaskawiec + * @since 9.0 + * @see FindByIndexNameSessionRepository + */ +public class InfinispanEmbeddedSessionRepository extends AbstractInfinispanSessionRepository implements FindByIndexNameSessionRepository { + + protected final PrincipalNameResolver principalNameResolver = new PrincipalNameResolver(); + + /** + * Creates new repository based on {@link SpringCache} + * + * @param cache Cache which shall be used for session repository. + */ + public InfinispanEmbeddedSessionRepository(SpringCache cache) { + super(cache, new EmbeddedApplicationPublishedBridge(cache)); + } + + @Override + public Map findByIndexNameAndIndexValue(String indexName, String indexValue) { + if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) { + return Collections.emptyMap(); + } + + return cache.getNativeCache().values().stream() + .map(cacheValue -> (MapSession) cacheValue) + .filter(session -> indexValue.equals(principalNameResolver.resolvePrincipal(session))) + .collect(Collectors.toMap(MapSession::getId, Function.identity())); + } +} diff --git a/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/PrincipalNameResolver.java b/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/PrincipalNameResolver.java new file mode 100644 index 000000000000..37fa54de12b4 --- /dev/null +++ b/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/PrincipalNameResolver.java @@ -0,0 +1,42 @@ +package org.infinispan.spring.session; + +import static org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.session.Session; + +/** + * Extracts Principal Name from Session. This needs to be done separately since Spring Session is not aware of any + * authentication mechanism (it is application developer's responsibility to implement it). + * + * @author Sebastian Łaskawiec + * @see org.springframework.session.FindByIndexNameSessionRepository + * @since 9.0 + */ +public class PrincipalNameResolver { + + private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT"; + + private SpelExpressionParser parser = new SpelExpressionParser(); + + /** + * Resolves Principal Name (e.g. user name) based on session. + * + * @param session Session to be checked. + * @return Extracted Principal Name + */ + public String resolvePrincipal(Session session) { + String principalName = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME); + if (principalName != null) { + return principalName; + } + Object authentication = session.getAttribute(SPRING_SECURITY_CONTEXT); + if (authentication != null) { + Expression expression = parser.parseExpression("authentication?.name"); + return expression.getValue(authentication, String.class); + } + return null; + } + +} diff --git a/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/configuration/EnableInfinispanEmbeddedHttpSession.java b/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/configuration/EnableInfinispanEmbeddedHttpSession.java new file mode 100644 index 000000000000..d3abb9b77fec --- /dev/null +++ b/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/configuration/EnableInfinispanEmbeddedHttpSession.java @@ -0,0 +1,61 @@ +package org.infinispan.spring.session.configuration; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.session.MapSession; +import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; + +/** + * Add this annotation to a {@code @Configuration} class to expose the SessionRepositoryFilter as a bean named + * "springSessionRepositoryFilter" and backed on Infinispan. + *

    + * The configuration requires creating a {@link org.infinispan.spring.provider.SpringCache} (for either remote or + * embedded configuration). Here's an example: + *

     
    + * {@literal @Configuration}
    + * {@literal @EnableInfinispanEmbeddednHttpSession}
    + * public class InfinispanConfiguration {
    + *
    + *     {@literal @Bean}
    + *     public SpringEmbeddedCacheManagerFactoryBean springCache() {
    + *         return new SpringEmbeddedCacheManagerFactoryBean();
    + *     }
    + * }
    + *  
    + * + * Configuring advanced features requires putting everything together manually or extending + * {@link InfinispanEmbeddedHttpSessionConfiguration}. + * + * @author Sebastian Łaskawiec + * @see EnableSpringHttpSession + * @since 9.0 + */ +@Retention(java.lang.annotation.RetentionPolicy.RUNTIME) +@Target({java.lang.annotation.ElementType.TYPE}) +@Documented +@Import(InfinispanEmbeddedHttpSessionConfiguration.class) +@Configuration +public @interface EnableInfinispanEmbeddedHttpSession { + + public static final String DEFAULT_CACHE_NAME = "sessions"; + + /** + * This is the session timeout in seconds. By default, it is set to 1800 seconds (30 minutes). This should be a + * non-negative integer. + * + * @return the seconds a session can be inactive before expiring + */ + int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; + + /** + * Cache name used for storing session data. + * + * @return the cache name for storing data. + */ + String cacheName() default DEFAULT_CACHE_NAME; + +} diff --git a/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/configuration/InfinispanEmbeddedHttpSessionConfiguration.java b/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/configuration/InfinispanEmbeddedHttpSessionConfiguration.java new file mode 100644 index 000000000000..8c41d1329e98 --- /dev/null +++ b/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/session/configuration/InfinispanEmbeddedHttpSessionConfiguration.java @@ -0,0 +1,54 @@ +package org.infinispan.spring.session.configuration; + +import java.util.Map; +import java.util.Objects; + +import org.infinispan.spring.provider.SpringCache; +import org.infinispan.spring.provider.SpringEmbeddedCacheManager; +import org.infinispan.spring.session.InfinispanEmbeddedSessionRepository; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.session.MapSession; +import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; + + +@Configuration +public class InfinispanEmbeddedHttpSessionConfiguration extends SpringHttpSessionConfiguration implements ImportAware { + + private String cacheName; + private int maxInactiveIntervalInSeconds; + + @Bean + public InfinispanEmbeddedSessionRepository sessionRepository(SpringEmbeddedCacheManager cacheManager, ApplicationEventPublisher eventPublisher) { + Objects.requireNonNull(cacheName, "Cache name can not be null"); + Objects.requireNonNull(cacheManager, "Cache Manager can not be null"); + Objects.requireNonNull(eventPublisher, "Event Publisher can not be null"); + + SpringCache cacheForSessions = cacheManager.getCache(cacheName); + + InfinispanEmbeddedSessionRepository sessionRepository = new InfinispanEmbeddedSessionRepository(cacheForSessions) { + @Override + public MapSession createSession() { + MapSession session = super.createSession(); + session.setMaxInactiveIntervalInSeconds(maxInactiveIntervalInSeconds); + return session; + } + }; + sessionRepository.setApplicationEventPublisher(eventPublisher); + + return sessionRepository; + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + Map enableAttrMap = importMetadata + .getAnnotationAttributes(EnableInfinispanEmbeddedHttpSession.class.getName()); + AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(enableAttrMap); + cacheName = annotationAttributes.getString("cacheName"); + maxInactiveIntervalInSeconds = annotationAttributes.getNumber("maxInactiveIntervalInSeconds").intValue(); + } +} diff --git a/spring/spring4/spring4-embedded/src/test/java/org/infinispan/spring/session/EmbeddedApplicationPublishedBridgeTest.java b/spring/spring4/spring4-embedded/src/test/java/org/infinispan/spring/session/EmbeddedApplicationPublishedBridgeTest.java new file mode 100644 index 000000000000..72f17ceaf3d2 --- /dev/null +++ b/spring/spring4/spring4-embedded/src/test/java/org/infinispan/spring/session/EmbeddedApplicationPublishedBridgeTest.java @@ -0,0 +1,63 @@ +package org.infinispan.spring.session; + +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.spring.provider.SpringCache; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +@Test(testName = "spring.session.EmbeddedApplicationPublishedBridgeTest", groups = "unit") +public class EmbeddedApplicationPublishedBridgeTest extends InfinispanApplicationPublishedBridgeTCK { + + private EmbeddedCacheManager embeddedCacheManager; + + @BeforeClass + public void beforeClass() { + embeddedCacheManager = new DefaultCacheManager(); + } + + @AfterMethod + public void afterMethod() { + embeddedCacheManager.getCache().clear(); + } + + @AfterClass + public void afterClass() { + embeddedCacheManager.stop(); + } + + @BeforeMethod + public void beforeMethod() throws Exception { + super.init(); + } + + @Override + protected SpringCache createSpringCache() { + return new SpringCache(embeddedCacheManager.getCache()); + } + + @Override + protected void callEviction() { + embeddedCacheManager.getCache().getAdvancedCache().getExpirationManager().processExpiration(); + } + + @Override + protected AbstractInfinispanSessionRepository createRepository(SpringCache springCache) throws Exception { + InfinispanEmbeddedSessionRepository sessionRepository = new InfinispanEmbeddedSessionRepository(springCache); + sessionRepository.afterPropertiesSet(); + return sessionRepository; + } + + @Override + public void testEventBridge() throws Exception { + super.testEventBridge(); + } + + @Override + public void testUnregistration() throws Exception { + super.testUnregistration(); + } +} diff --git a/spring/spring4/spring4-embedded/src/test/java/org/infinispan/spring/session/InfinispanEmbeddedSessionRepositoryTest.java b/spring/spring4/spring4-embedded/src/test/java/org/infinispan/spring/session/InfinispanEmbeddedSessionRepositoryTest.java new file mode 100644 index 000000000000..f933aa9283ab --- /dev/null +++ b/spring/spring4/spring4-embedded/src/test/java/org/infinispan/spring/session/InfinispanEmbeddedSessionRepositoryTest.java @@ -0,0 +1,89 @@ +package org.infinispan.spring.session; + +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.spring.provider.SpringCache; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +@Test(testName = "spring.session.InfinispanEmbeddedSessionRepositoryTest", groups = "unit") +public class InfinispanEmbeddedSessionRepositoryTest extends InfinispanSessionRepositoryTCK { + + private EmbeddedCacheManager embeddedCacheManager; + + @BeforeClass + public void beforeClass() { + embeddedCacheManager = new DefaultCacheManager(); + } + + @AfterMethod + public void afterMethod() { + embeddedCacheManager.getCache().clear(); + } + + @AfterClass + public void afterClass() { + embeddedCacheManager.stop(); + } + + @BeforeMethod + public void beforeMethod() throws Exception { + super.init(); + } + + @Override + protected SpringCache createSpringCache() { + return new SpringCache(embeddedCacheManager.getCache()); + } + + @Override + protected AbstractInfinispanSessionRepository createRepository(SpringCache springCache) throws Exception { + InfinispanEmbeddedSessionRepository sessionRepository = new InfinispanEmbeddedSessionRepository(springCache); + sessionRepository.afterPropertiesSet(); + return sessionRepository; + } + + @Test(expectedExceptions = NullPointerException.class) + @Override + public void testThrowingExceptionOnNullSpringCache() throws Exception { + super.testThrowingExceptionOnNullSpringCache(); + } + + @Override + public void testCreatingSession() throws Exception { + super.testCreatingSession(); + } + + @Override + public void testSavingSession() throws Exception { + super.testSavingSession(); + } + + @Override + public void testDeletingSession() throws Exception { + super.testDeletingSession(); + } + + @Override + public void testEvictingSession() throws Exception { + super.testEvictingSession(); + } + + @Override + public void testExtractingPrincipalWithWrongIndexName() throws Exception { + super.testExtractingPrincipalWithWrongIndexName(); + } + + @Override + public void testExtractingPrincipal() throws Exception { + super.testExtractingPrincipal(); + } + + @Override + public void testUpdatingTTLOnAccessingData() throws Exception { + super.testUpdatingTTLOnAccessingData(); + } +} diff --git a/spring/spring4/spring4-remote/pom.xml b/spring/spring4/spring4-remote/pom.xml index 2773c9811df3..8cee4347172e 100644 --- a/spring/spring4/spring4-remote/pom.xml +++ b/spring/spring4/spring4-remote/pom.xml @@ -12,7 +12,7 @@ infinispan-spring4-remote bundle - Infinispan Spring 4 Integration + Infinispan Spring 4 Remote support 1 @@ -22,6 +22,12 @@ org.springframework spring-context + provided + + + org.springframework.session + spring-session + provided ${project.groupId} @@ -38,6 +44,12 @@ spring-test test + + ${project.groupId} + infinispan-spring4-common + test-jar + test + ${project.groupId} infinispan-commons-test @@ -55,6 +67,11 @@ test-jar test + + org.infinispan + infinispan-query-dsl + test + ${project.groupId} infinispan-server-hotrod @@ -71,6 +88,11 @@ spring-jdbc test + + org.springframework + spring-web + test + org.springframework spring-context-support diff --git a/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/provider/SpringRemoteCache.java b/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/provider/SpringRemoteCache.java deleted file mode 100644 index 8a042f96d310..000000000000 --- a/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/provider/SpringRemoteCache.java +++ /dev/null @@ -1,94 +0,0 @@ -package org.infinispan.spring.provider; - -import org.infinispan.client.hotrod.RemoteCache; -import org.springframework.cache.Cache; - -/** - *

    - * A {@link org.springframework.cache.Cache Cache} implementation that delegates to a - * {@link org.infinispan.client.hotrod.RemoteCache org.infinispan.client.hotrod.RemoteCache} instance supplied at construction - * time. - *

    - * - * @author Olaf Bergner - * @author Marius Bogoevici - * - */ -public class SpringRemoteCache implements Cache { - - private final CacheDelegate cacheImplementation; - - /** - * @param nativeCache the underlying cache - */ - public SpringRemoteCache(final RemoteCache nativeCache) { - this.cacheImplementation = new CacheDelegate(nativeCache); - } - - /** - * @see org.springframework.cache.Cache#getName() - */ - @Override - public String getName() { - return this.cacheImplementation.getName(); - } - - /** - * @see org.springframework.cache.Cache#getNativeCache() - */ - @Override - public org.infinispan.commons.api.BasicCache getNativeCache() { - return this.cacheImplementation.getNativeCache(); - } - - /** - * @see org.springframework.cache.Cache#get(java.lang.Object) - */ - @Override - public ValueWrapper get(final Object key) { - return cacheImplementation.get(key); - } - - @Override - public T get(Object key, Class type) { - return cacheImplementation.get(key, type); - } - - /** - * @see org.springframework.cache.Cache#put(java.lang.Object, java.lang.Object) - */ - @Override - public void put(final Object key, final Object value) { - this.cacheImplementation.put(key, value); - } - - @Override - public ValueWrapper putIfAbsent(Object key, Object value) { - return cacheImplementation.putIfAbsent(key, value); - } - - /** - * @see org.springframework.cache.Cache#evict(java.lang.Object) - */ - @Override - public void evict(final Object key) { - this.cacheImplementation.evict(key); - } - - /** - * @see org.springframework.cache.Cache#clear() - */ - @Override - public void clear() { - this.cacheImplementation.clear(); - } - - - /** - * @see java.lang.Object#toString() - */ - @Override - public String toString() { - return "InfinispanCache [nativeCache = " + this.cacheImplementation.getNativeCache() + "]"; - } -} diff --git a/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/provider/SpringRemoteCacheManager.java b/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/provider/SpringRemoteCacheManager.java index 0b495cd7b6d1..f8f1cf22005d 100644 --- a/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/provider/SpringRemoteCacheManager.java +++ b/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/provider/SpringRemoteCacheManager.java @@ -3,7 +3,6 @@ import java.util.Collection; import org.infinispan.client.hotrod.RemoteCacheManager; -import org.springframework.cache.Cache; import org.springframework.util.Assert; /** @@ -34,8 +33,8 @@ public SpringRemoteCacheManager(final RemoteCacheManager nativeCacheManager) { * @see org.springframework.cache.CacheManager#getCache(java.lang.String) */ @Override - public Cache getCache(final String name) { - return new SpringRemoteCache(this.nativeCacheManager.getCache(name)); + public SpringCache getCache(final String name) { + return new SpringCache(this.nativeCacheManager.getCache(name)); } /** diff --git a/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/InfinispanRemoteSessionRepository.java b/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/InfinispanRemoteSessionRepository.java new file mode 100644 index 000000000000..6ed8b35a833a --- /dev/null +++ b/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/InfinispanRemoteSessionRepository.java @@ -0,0 +1,21 @@ +package org.infinispan.spring.session; + +import org.infinispan.spring.provider.SpringCache; + +/** + * Session Repository for Infinispan in client/server mode. + * + * @author Sebastian Łaskawiec + * @since 9.0 + */ +public class InfinispanRemoteSessionRepository extends AbstractInfinispanSessionRepository { + + /** + * Creates new repository based on {@link SpringCache} + * + * @param cache Cache which shall be used for session repository. + */ + public InfinispanRemoteSessionRepository(SpringCache cache) { + super(cache, new RemoteApplicationPublishedBridge(cache)); + } +} diff --git a/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/RemoteApplicationPublishedBridge.java b/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/RemoteApplicationPublishedBridge.java new file mode 100644 index 000000000000..0f610edb8f45 --- /dev/null +++ b/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/RemoteApplicationPublishedBridge.java @@ -0,0 +1,50 @@ +package org.infinispan.spring.session; + +import org.infinispan.client.hotrod.RemoteCache; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryExpired; +import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved; +import org.infinispan.client.hotrod.annotation.ClientListener; +import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent; +import org.infinispan.client.hotrod.event.ClientCacheEntryExpiredEvent; +import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent; +import org.infinispan.spring.provider.SpringCache; + +/** + * A bridge between Infinispan Remote events and Spring. + * + * @author Sebastian Łaskawiec + * @since 9.0 + */ +@ClientListener +public class RemoteApplicationPublishedBridge extends AbstractApplicationPublisherBridge { + + public RemoteApplicationPublishedBridge(SpringCache eventSource) { + super(eventSource); + } + + @Override + protected void registerListener() { + ((RemoteCache) eventSource.getNativeCache()).addClientListener(this); + } + + @Override + public void unregisterListener() { + ((RemoteCache) eventSource.getNativeCache()).removeClientListener(this); + } + + @ClientCacheEntryCreated + public void processCacheEntryCreated(ClientCacheEntryCreatedEvent event) { + emitSessionCreatedEvent((String) event.getKey()); + } + + @ClientCacheEntryExpired + public void processCacheEntryExpired(ClientCacheEntryExpiredEvent event) { + emitSessionExpiredEvent((String) event.getKey()); + } + + @ClientCacheEntryRemoved + public void processCacheEntryDestroyed(ClientCacheEntryRemovedEvent event) { + emitSessionDestroyedEvent((String) event.getKey()); + } +} diff --git a/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/configuration/EnableInfinispanRemoteHttpSession.java b/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/configuration/EnableInfinispanRemoteHttpSession.java new file mode 100644 index 000000000000..751aad5b3c56 --- /dev/null +++ b/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/configuration/EnableInfinispanRemoteHttpSession.java @@ -0,0 +1,61 @@ +package org.infinispan.spring.session.configuration; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.session.MapSession; +import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession; + +/** + * Add this annotation to a {@code @Configuration} class to expose the SessionRepositoryFilter as a bean named + * "springSessionRepositoryFilter" and backed on Infinispan. + *

    + * The configuration requires creating a {@link org.infinispan.spring.provider.SpringCache} (for either remote or + * embedded configuration). Here's an example: + *

     
    + * {@literal @Configuration}
    + * {@literal @EnableInfinispanRemoteHttpSession}
    + * public class InfinispanConfiguration {
    + *
    + *     {@literal @Bean}
    + *     public SpringRemoteCacheManagerFactoryBean springCache() {
    + *         return new SpringRemoteCacheManagerFactoryBean();
    + *     }
    + * }
    + *  
    + * + * Configuring advanced features requires putting everything together manually or extending + * {@link InfinispanRemoteHttpSessionConfiguration}. + * + * @author Sebastian Łaskawiec + * @see EnableSpringHttpSession + * @since 9.0 + */ +@Retention(java.lang.annotation.RetentionPolicy.RUNTIME) +@Target({java.lang.annotation.ElementType.TYPE}) +@Documented +@Import(InfinispanRemoteHttpSessionConfiguration.class) +@Configuration +public @interface EnableInfinispanRemoteHttpSession { + + public static final String DEFAULT_CACHE_NAME = "sessions"; + + /** + * This is the session timeout in seconds. By default, it is set to 1800 seconds (30 minutes). This should be a + * non-negative integer. + * + * @return the seconds a session can be inactive before expiring + */ + int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS; + + /** + * Cache name used for storing session data. + * + * @return the cache name for storing data. + */ + String cacheName() default DEFAULT_CACHE_NAME; + +} diff --git a/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/configuration/InfinispanRemoteHttpSessionConfiguration.java b/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/configuration/InfinispanRemoteHttpSessionConfiguration.java new file mode 100644 index 000000000000..0295f313b255 --- /dev/null +++ b/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/session/configuration/InfinispanRemoteHttpSessionConfiguration.java @@ -0,0 +1,53 @@ +package org.infinispan.spring.session.configuration; + +import java.util.Map; +import java.util.Objects; + +import org.infinispan.spring.provider.SpringCache; +import org.infinispan.spring.provider.SpringRemoteCacheManager; +import org.infinispan.spring.session.InfinispanRemoteSessionRepository; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.session.MapSession; +import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration; + +@Configuration +public class InfinispanRemoteHttpSessionConfiguration extends SpringHttpSessionConfiguration implements ImportAware { + + private String cacheName; + private int maxInactiveIntervalInSeconds; + + @Bean + public InfinispanRemoteSessionRepository sessionRepository(SpringRemoteCacheManager cacheManager, ApplicationEventPublisher eventPublisher) { + Objects.requireNonNull(cacheName, "Cache name can not be null"); + Objects.requireNonNull(cacheManager, "Cache Manager can not be null"); + Objects.requireNonNull(eventPublisher, "Event Publisher can not be null"); + + SpringCache cacheForSessions = cacheManager.getCache(cacheName); + + InfinispanRemoteSessionRepository sessionRepository = new InfinispanRemoteSessionRepository(cacheForSessions) { + @Override + public MapSession createSession() { + MapSession session = super.createSession(); + session.setMaxInactiveIntervalInSeconds(maxInactiveIntervalInSeconds); + return session; + } + }; + sessionRepository.setApplicationEventPublisher(eventPublisher); + + return sessionRepository; + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + Map enableAttrMap = importMetadata + .getAnnotationAttributes(EnableInfinispanRemoteHttpSession.class.getName()); + AnnotationAttributes annotationAttributes = AnnotationAttributes.fromMap(enableAttrMap); + cacheName = annotationAttributes.getString("cacheName"); + maxInactiveIntervalInSeconds = annotationAttributes.getNumber("maxInactiveIntervalInSeconds").intValue(); + } +} diff --git a/spring/spring4/spring4-remote/src/test/java/org/infinispan/spring/session/InfinispanRemoteSessionRepositoryTest.java b/spring/spring4/spring4-remote/src/test/java/org/infinispan/spring/session/InfinispanRemoteSessionRepositoryTest.java new file mode 100644 index 000000000000..ac10d87581ee --- /dev/null +++ b/spring/spring4/spring4-remote/src/test/java/org/infinispan/spring/session/InfinispanRemoteSessionRepositoryTest.java @@ -0,0 +1,95 @@ +package org.infinispan.spring.session; + +import org.infinispan.client.hotrod.RemoteCacheManager; +import org.infinispan.client.hotrod.configuration.ConfigurationBuilder; +import org.infinispan.commons.equivalence.AnyServerEquivalence; +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.server.hotrod.HotRodServer; +import org.infinispan.server.hotrod.test.HotRodTestingUtil; +import org.infinispan.spring.provider.SpringCache; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +@Test(testName = "spring.session.InfinispanRemoteSessionRepositoryTest", groups = "functional") +public class InfinispanRemoteSessionRepositoryTest extends InfinispanSessionRepositoryTCK { + + private EmbeddedCacheManager embeddedCacheManager; + private HotRodServer hotrodServer; + private RemoteCacheManager remoteCacheManager; + + @BeforeClass + public void beforeClass() { + org.infinispan.configuration.cache.ConfigurationBuilder cacheConfiguration = new org.infinispan.configuration.cache.ConfigurationBuilder(); + cacheConfiguration.dataContainer().keyEquivalence(new AnyServerEquivalence()); + embeddedCacheManager = new DefaultCacheManager(cacheConfiguration.build()); + hotrodServer = HotRodTestingUtil.startHotRodServer(embeddedCacheManager, 19723); + ConfigurationBuilder builder = new ConfigurationBuilder(); + builder.addServer().host("localhost").port(hotrodServer.getPort()); + remoteCacheManager = new RemoteCacheManager(builder.build()); + } + + @AfterMethod + public void afterMethod() { + remoteCacheManager.getCache().clear(); + } + + @AfterClass + public void afterClass() { + embeddedCacheManager.stop(); + remoteCacheManager.stop(); + hotrodServer.stop(); + } + + @BeforeMethod + public void beforeMethod() throws Exception { + super.init(); + } + + @Override + protected SpringCache createSpringCache() { + return new SpringCache(remoteCacheManager.getCache()); + } + + @Override + protected AbstractInfinispanSessionRepository createRepository(SpringCache springCache) throws Exception { + InfinispanRemoteSessionRepository sessionRepository = new InfinispanRemoteSessionRepository(springCache); + sessionRepository.afterPropertiesSet(); + return sessionRepository; + } + + @Test(expectedExceptions = NullPointerException.class) + @Override + public void testThrowingExceptionOnNullSpringCache() throws Exception { + super.testThrowingExceptionOnNullSpringCache(); + } + + @Override + public void testCreatingSession() throws Exception { + super.testCreatingSession(); + } + + @Override + public void testSavingSession() throws Exception { + super.testSavingSession(); + } + + @Override + public void testDeletingSession() throws Exception { + super.testDeletingSession(); + } + + @Override + public void testEvictingSession() throws Exception { + super.testEvictingSession(); + } + + @Override + public void testUpdatingTTLOnAccessingData() throws Exception { + super.testUpdatingTTLOnAccessingData(); + } + +} diff --git a/spring/spring4/spring4-remote/src/test/java/org/infinispan/spring/session/RemoteApplicationPublishedBridgeTest.java b/spring/spring4/spring4-remote/src/test/java/org/infinispan/spring/session/RemoteApplicationPublishedBridgeTest.java new file mode 100644 index 000000000000..340f5957b48e --- /dev/null +++ b/spring/spring4/spring4-remote/src/test/java/org/infinispan/spring/session/RemoteApplicationPublishedBridgeTest.java @@ -0,0 +1,75 @@ +package org.infinispan.spring.session; + +import org.infinispan.client.hotrod.RemoteCacheManager; +import org.infinispan.client.hotrod.configuration.ConfigurationBuilder; +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.server.hotrod.HotRodServer; +import org.infinispan.server.hotrod.test.HotRodTestingUtil; +import org.infinispan.spring.provider.SpringCache; +import org.testng.annotations.AfterClass; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +@Test(testName = "spring.session.RemoteApplicationPublishedBridgeTest", groups = "unit") +public class RemoteApplicationPublishedBridgeTest extends InfinispanApplicationPublishedBridgeTCK { + + private EmbeddedCacheManager embeddedCacheManager; + private HotRodServer hotrodServer; + private RemoteCacheManager remoteCacheManager; + + @BeforeClass + public void beforeClass() { + embeddedCacheManager = new DefaultCacheManager(); + hotrodServer = HotRodTestingUtil.startHotRodServer(embeddedCacheManager, 19723); + ConfigurationBuilder builder = new ConfigurationBuilder(); + builder.addServer().host("localhost").port(hotrodServer.getPort()); + remoteCacheManager = new RemoteCacheManager(builder.build()); + } + + @AfterMethod + public void afterMethod() { + remoteCacheManager.getCache().clear(); + } + + @AfterClass + public void afterClass() { + embeddedCacheManager.stop(); + remoteCacheManager.stop(); + hotrodServer.stop(); + } + + @BeforeMethod + public void beforeMethod() throws Exception { + super.init(); + } + + @Override + protected SpringCache createSpringCache() { + return new SpringCache(remoteCacheManager.getCache()); + } + + @Override + protected void callEviction() { + embeddedCacheManager.getCache().getAdvancedCache().getExpirationManager().processExpiration(); + } + + @Override + protected AbstractInfinispanSessionRepository createRepository(SpringCache springCache) throws Exception { + InfinispanRemoteSessionRepository sessionRepository = new InfinispanRemoteSessionRepository(springCache); + sessionRepository.afterPropertiesSet(); + return sessionRepository; + } + + @Override + public void testEventBridge() throws Exception { + super.testEventBridge(); + } + + @Override + public void testUnregistration() throws Exception { + super.testUnregistration(); + } +}