diff --git a/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/ConfigurerBasedGemFireHttpSessionConfigurationIntegrationTests.java b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/ConfigurerBasedGemFireHttpSessionConfigurationIntegrationTests.java new file mode 100644 index 0000000..a7bec3d --- /dev/null +++ b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/ConfigurerBasedGemFireHttpSessionConfigurationIntegrationTests.java @@ -0,0 +1,178 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.session.data.gemfire.config.annotation.web.http; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.Optional; + +import org.junit.After; +import org.junit.Test; + +import org.apache.geode.cache.RegionShortcut; +import org.apache.geode.cache.client.ClientRegionShortcut; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.PropertySource; +import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; +import org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects; +import org.springframework.mock.env.MockPropertySource; +import org.springframework.session.Session; +import org.springframework.session.data.gemfire.config.annotation.web.http.support.SpringSessionGemFireConfigurer; +import org.springframework.session.data.gemfire.serialization.SessionSerializer; + +/** + * Integration tests testing {@link SpringSessionGemFireConfigurer} based configuration of either Apache Geode + * or Pivotal GemFire * as the (HTTP) {@link Session} state management provider in Spring Session. + * + * @author John Blum + * @see org.junit.Test + * @see org.springframework.context.ConfigurableApplicationContext + * @see org.springframework.context.annotation.AnnotationConfigApplicationContext + * @see org.springframework.core.env.PropertySource + * @see org.springframework.data.gemfire.config.annotation.ClientCacheApplication + * @see org.springframework.data.gemfire.tests.mock.annotation.EnableGemFireMockObjects + * @see org.springframework.mock.env.MockPropertySource + * @see org.springframework.session.data.gemfire.config.annotation.web.http.support.SpringSessionGemFireConfigurer + * @since 2.0.4 + */ +@SuppressWarnings("unused") +public class ConfigurerBasedGemFireHttpSessionConfigurationIntegrationTests { + + private ConfigurableApplicationContext applicationContext; + + @After + public void tearDow() { + Optional.ofNullable(this.applicationContext).ifPresent(ConfigurableApplicationContext::close); + } + + private ConfigurableApplicationContext newApplicationContext(Class... annotatedClasses) { + return newApplicationContext(new MockPropertySource("TestProperties"), annotatedClasses); + } + + private ConfigurableApplicationContext newApplicationContext(PropertySource testPropertySource, + Class... annotatedClasses) { + + AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + + applicationContext.getEnvironment().getPropertySources().addFirst(testPropertySource); + applicationContext.register(annotatedClasses); + applicationContext.registerShutdownHook(); + applicationContext.refresh(); + + return applicationContext; + } + + @Test + public void springSessionGemFireConfigurerOverridesAnnotationAttributeAndPropertyConfiguration() { + + MockPropertySource testPropertySource = new MockPropertySource("TestProperties") + .withProperty("spring.session.data.gemfire.cache.client.pool.name", "Car") + .withProperty("spring.session.data.gemfire.cache.client.region.shortcut", ClientRegionShortcut.LOCAL_PERSISTENT.name()) + .withProperty("spring.session.data.gemfire.cache.server.region.shortcut", RegionShortcut.REPLICATE_PERSISTENT_OVERFLOW.name()) + .withProperty("spring.session.data.gemfire.session.attributes.indexable", "firstName, lastName") + .withProperty("spring.session.data.gemfire.session.expiration.max-inactive-interval-seconds", "600") + .withProperty("spring.session.data.gemfire.session.region.name", "PropertyRegionName") + .withProperty("spring.session.data.gemfire.session.serializer.bean-name", "MockSessionSerializer"); + + this.applicationContext = newApplicationContext(testPropertySource, TestConfiguration.class); + + GemFireHttpSessionConfiguration sessionConfiguration = + this.applicationContext.getBean(GemFireHttpSessionConfiguration.class); + + assertThat(sessionConfiguration).isNotNull(); + assertThat(sessionConfiguration.getClientRegionShortcut()).isEqualTo(ClientRegionShortcut.CACHING_PROXY); + assertThat(sessionConfiguration.getIndexableSessionAttributes()).containsExactly("two", "four"); + assertThat(sessionConfiguration.getMaxInactiveIntervalInSeconds()).isEqualTo(3600); + assertThat(sessionConfiguration.getPoolName()).isEqualTo("Dead"); + assertThat(sessionConfiguration.getServerRegionShortcut()).isEqualTo(RegionShortcut.PARTITION); + assertThat(sessionConfiguration.getSessionRegionName()).isEqualTo("ConfigurerRegionName"); + assertThat(sessionConfiguration.getSessionSerializerBeanName()).isEqualTo("SessionPdxSerializer"); + } + + @Test + public void usesSpringSessionGemFireConfigurerWhenPresent() { + + this.applicationContext = newApplicationContext(TestConfiguration.class); + + GemFireHttpSessionConfiguration sessionConfiguration = + this.applicationContext.getBean(GemFireHttpSessionConfiguration.class); + + assertThat(sessionConfiguration).isNotNull(); + assertThat(sessionConfiguration.getClientRegionShortcut()).isEqualTo(ClientRegionShortcut.CACHING_PROXY); + assertThat(sessionConfiguration.getIndexableSessionAttributes()).containsExactly("two", "four"); + assertThat(sessionConfiguration.getMaxInactiveIntervalInSeconds()).isEqualTo(3600); + assertThat(sessionConfiguration.getPoolName()).isEqualTo("Dead"); + assertThat(sessionConfiguration.getServerRegionShortcut()).isEqualTo(RegionShortcut.PARTITION); + assertThat(sessionConfiguration.getSessionRegionName()).isEqualTo("ConfigurerRegionName"); + assertThat(sessionConfiguration.getSessionSerializerBeanName()).isEqualTo("SessionPdxSerializer"); + } + + @ClientCacheApplication + @EnableGemFireMockObjects + @EnableGemFireHttpSession( + clientRegionShortcut = ClientRegionShortcut.LOCAL, + indexableSessionAttributes = { "one", "two" }, + maxInactiveIntervalInSeconds = 900, + poolName = "Swimming", + regionName = "AnnotationAttributeRegionName", + serverRegionShortcut = RegionShortcut.REPLICATE, + sessionSerializerBeanName = "TestSessionSerializer" + ) + static class TestConfiguration { + + @Bean("TestSessionSerializer") + Object testSessionSerializer() { + return mock(SessionSerializer.class); + } + + @Bean + SpringSessionGemFireConfigurer testSpringSessionGemFireConfigurer() { + + return new SpringSessionGemFireConfigurer() { + + @Override + public ClientRegionShortcut getClientRegionShortcut() { + return ClientRegionShortcut.CACHING_PROXY; + } + + @Override + public String[] getIndexableSessionAttributes() { + return new String[] { "two", "four" }; + } + + @Override + public int getMaxInactiveIntervalInSeconds() { + return 3600; + } + + @Override + public String getPoolName() { + return "Dead"; + } + + @Override + public String getRegionName() { + return "ConfigurerRegionName"; + } + }; + } + } +} diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java index 6349b0d..47cc764 100644 --- a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSession.java @@ -95,6 +95,8 @@ public @interface EnableGemFireHttpSession { /** * Defines the {@link ClientCache} {@link Region} data management policy. * + * Defaults to {@link ClientRegionShortcut#PROXY}. + * * @return a {@link ClientRegionShortcut} used to configure the {@link ClientCache} {@link Region} * data management policy. * @see org.apache.geode.cache.client.ClientRegionShortcut @@ -104,7 +106,10 @@ public @interface EnableGemFireHttpSession { /** * Identifies the {@link Session} attributes by name that will be indexed for query operations. * - * For instance, find all {@link Session Sessions} in Pivotal GemFire or Apache Geode having attribute A defined with value X. + * For instance, find all {@link Session Sessions} in Apache Geode or Pivotal GemFire having attribute A + * defined with value X. + * + * Defaults to empty {@link String} array. * * @return an array of {@link String Strings} identifying the names of {@link Session} attributes to index. */ @@ -113,36 +118,42 @@ public @interface EnableGemFireHttpSession { /** * Defines the maximum interval in seconds that a {@link Session} can remain inactive before it expires. * - * Defaults to 1800 seconds, or 30 minutes. + * Defaults to {@literal 1800} seconds, or {@literal 30} minutes. * * @return an integer value defining the maximum inactive interval in seconds before the {@link Session} expires. */ int maxInactiveIntervalInSeconds() default 1800; /** - * Specifies the name of the specific {@link Pool} used by the client cache {@link Region} + * Specifies the name of the specific {@link Pool} used by the {@link ClientCache} {@link Region} * (i.e. {@literal ClusteredSpringSessions}) when performing cache data access operations. * * This is attribute is only used in the client/server topology. * - * @return the name of the {@link Pool} to be used by the client cache Region to send {@link Session} state - * to the cluster of servers. - * @see GemFireHttpSessionConfiguration#DEFAULT_POOL_NAME + * Defaults to {@literal gemfirePool}. + * + * @return the name of the {@link Pool} used by the {@link ClientCache} {@link Region} + * to send {@link Session} state to the cluster of servers. + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration#DEFAULT_POOL_NAME */ String poolName() default GemFireHttpSessionConfiguration.DEFAULT_POOL_NAME; /** - * Defines the name of the (client)cache {@link Region} used to store {@link Session} state. + * Defines the {@link String name} of the (client)cache {@link Region} used to store {@link Session} state. * - * @return a {@link String} specifying the name of the (client)cace {@link Region} + * Defaults to {@literal ClusteredSpringSessions}. + * + * @return a {@link String} specifying the name of the (client)cache {@link Region} * used to store {@link Session} state. - * @see GemFireHttpSessionConfiguration#DEFAULT_SESSION_REGION_NAME + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration#DEFAULT_SESSION_REGION_NAME */ String regionName() default GemFireHttpSessionConfiguration.DEFAULT_SESSION_REGION_NAME; /** * Defines the {@link Cache} {@link Region} data management policy. * + * Defaults to {@link RegionShortcut#PARTITION}. + * * @return a {@link RegionShortcut} used to specify and configure the {@link Cache} {@link Region} * data management policy. * @see org.apache.geode.cache.RegionShortcut @@ -155,10 +166,10 @@ public @interface EnableGemFireHttpSession { * * The bean referred to by its name must be of type {@link SessionSerializer}. * - * Defaults to {@literal SessionDataSerializer}. + * Defaults to {@literal SessionPdxSerializer}. * * @return a {@link String} containing the bean name of the configured {@link SessionSerializer}. - * @see org.springframework.session.data.gemfire.serialization.data.provider.DataSerializableSessionSerializer + * @see org.springframework.session.data.gemfire.serialization.pdx.provider.PdxSerializableSessionSerializer * @see org.springframework.session.data.gemfire.serialization.SessionSerializer */ String sessionSerializerBeanName() default GemFireHttpSessionConfiguration.DEFAULT_SESSION_SERIALIZER_BEAN_NAME; diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java index 0a51259..6cc4f7a 100644 --- a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/GemFireHttpSessionConfiguration.java @@ -35,6 +35,7 @@ import org.apache.geode.cache.client.Pool; import org.apache.geode.pdx.PdxSerializer; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; @@ -58,6 +59,7 @@ import org.springframework.session.data.gemfire.AbstractGemFireOperationsSession import org.springframework.session.data.gemfire.GemFireOperationsSessionRepository; import org.springframework.session.data.gemfire.config.annotation.web.http.support.GemFireCacheTypeAwareRegionFactoryBean; import org.springframework.session.data.gemfire.config.annotation.web.http.support.SessionAttributesIndexFactoryBean; +import org.springframework.session.data.gemfire.config.annotation.web.http.support.SpringSessionGemFireConfigurer; import org.springframework.session.data.gemfire.serialization.SessionSerializer; import org.springframework.session.data.gemfire.serialization.data.provider.DataSerializableSessionSerializer; import org.springframework.session.data.gemfire.serialization.data.support.DataSerializerSessionSerializerAdapter; @@ -410,6 +412,36 @@ public class GemFireHttpSessionConfiguration extends AbstractGemFireHttpSessionC setSessionSerializerBeanName(resolveProperty(sessionSerializerBeanNamePropertyName(), defaultSessionSerializerBeanName)); + + applySpringSessionGemFireConfigurer(); + } + + private Optional resolveSpringSessionGemFireConfigurer() { + + try { + return Optional.of(getApplicationContext().getBean(SpringSessionGemFireConfigurer.class)); + } + catch (BeansException cause) { + + if (cause instanceof NoSuchBeanDefinitionException) { + return Optional.empty(); + } + + throw cause; + } + } + + private void applySpringSessionGemFireConfigurer() { + + resolveSpringSessionGemFireConfigurer().ifPresent(configurer -> { + setClientRegionShortcut(configurer.getClientRegionShortcut()); + setIndexableSessionAttributes(configurer.getIndexableSessionAttributes()); + setMaxInactiveIntervalInSeconds(configurer.getMaxInactiveIntervalInSeconds()); + setPoolName(configurer.getPoolName()); + setServerRegionShortcut(configurer.getServerRegionShortcut()); + setSessionRegionName(configurer.getRegionName()); + setSessionSerializerBeanName(configurer.getSessionSerializerBeanName()); + }); } @PostConstruct diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/support/SpringSessionGemFireConfigurer.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/support/SpringSessionGemFireConfigurer.java new file mode 100644 index 0000000..0bce6b0 --- /dev/null +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/config/annotation/web/http/support/SpringSessionGemFireConfigurer.java @@ -0,0 +1,143 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.session.data.gemfire.config.annotation.web.http.support; + +import org.apache.geode.cache.Cache; +import org.apache.geode.cache.Region; +import org.apache.geode.cache.RegionShortcut; +import org.apache.geode.cache.client.ClientCache; +import org.apache.geode.cache.client.ClientRegionShortcut; +import org.apache.geode.cache.client.Pool; + +import org.springframework.session.Session; +import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration; +import org.springframework.session.data.gemfire.serialization.SessionSerializer; + +/** + * The {@link SpringSessionGemFireConfigurer} interface defines a contract for programmatically controlling + * the configuration of either Apache Geode or Pivotal GemFire as a (HTTP) {@link Session} state management provider + * in Spring Session. + * + * @author John Blum + * @see org.springframework.session.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration + * @since 1.0.0 + */ +@SuppressWarnings("unused") +public interface SpringSessionGemFireConfigurer { + + /** + * Defines the {@link ClientCache} {@link Region} data management policy. + * + * Defaults to {@link ClientRegionShortcut#PROXY}. + * + * @return a {@link ClientRegionShortcut} used to configure the {@link ClientCache} {@link Region} + * data management policy. + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration#DEFAULT_CLIENT_REGION_SHORTCUT + * @see org.apache.geode.cache.client.ClientRegionShortcut + */ + default ClientRegionShortcut getClientRegionShortcut() { + return GemFireHttpSessionConfiguration.DEFAULT_CLIENT_REGION_SHORTCUT; + } + + /** + * Identifies the {@link Session} attributes by name that will be indexed for query operations. + * + * For instance, find all {@link Session Sessions} in Apache Geode or Pivotal GemFire having attribute A + * defined with value X. + * + * Defaults to empty {@link String} array. + * + * @return an array of {@link String Strings} identifying the names of {@link Session} attributes to index. + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration#DEFAULT_INDEXABLE_SESSION_ATTRIBUTES + */ + default String[] getIndexableSessionAttributes() { + return GemFireHttpSessionConfiguration.DEFAULT_INDEXABLE_SESSION_ATTRIBUTES; + } + + /** + * Defines the maximum interval in seconds that a {@link Session} can remain inactive before it expires. + * + * Defaults to {@literal 1800} seconds, or {@literal 30} minutes. + * + * @return an integer value defining the maximum inactive interval in seconds before the {@link Session} expires. + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration#DEFAULT_MAX_INACTIVE_INTERVAL_IN_SECONDS + */ + default int getMaxInactiveIntervalInSeconds() { + return GemFireHttpSessionConfiguration.DEFAULT_MAX_INACTIVE_INTERVAL_IN_SECONDS; + } + + /** + * Specifies the name of the specific {@link Pool} used by the {@link ClientCache} {@link Region} + * (i.e. {@literal ClusteredSpringSessions}) when performing cache data access operations. + * + * This is attribute is only used in the client/server topology. + * + * Defaults to {@literal gemfirePool}. + * + * @return the name of the {@link Pool} used by the {@link ClientCache} {@link Region} + * to send {@link Session} state to the cluster of servers. + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration#DEFAULT_POOL_NAME + */ + default String getPoolName() { + return GemFireHttpSessionConfiguration.DEFAULT_POOL_NAME; + } + + /** + * Defines the {@link String name} of the (client)cache {@link Region} used to store {@link Session} state. + * + * Defaults to {@literal ClusteredSpringSessions}. + * + * @return a {@link String} specifying the name of the (client)cache {@link Region} + * used to store {@link Session} state. + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration#DEFAULT_SESSION_REGION_NAME + */ + default String getRegionName() { + return GemFireHttpSessionConfiguration.DEFAULT_SESSION_REGION_NAME; + } + + /** + * Defines the {@link Cache} {@link Region} data management policy. + * + * Defaults to {@link RegionShortcut#PARTITION}. + * + * @return a {@link RegionShortcut} used to specify and configure the {@link Cache} {@link Region} + * data management policy. + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration#DEFAULT_SERVER_REGION_SHORTCUT + * @see org.apache.geode.cache.RegionShortcut + */ + default RegionShortcut getServerRegionShortcut() { + return GemFireHttpSessionConfiguration.DEFAULT_SERVER_REGION_SHORTCUT; + } + + /** + * Defines the bean name of the {@link SessionSerializer} used to serialize {@link Session} state + * between client and server or to disk when persisting or overflowing {@link Session} state. + * + * The bean referred to by its name must be of type {@link SessionSerializer}. + * + * Defaults to {@literal SessionPdxSerializer}. + * + * @return a {@link String} containing the bean name of the configured {@link SessionSerializer}. + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration#DEFAULT_SESSION_SERIALIZER_BEAN_NAME + * @see org.springframework.session.data.gemfire.serialization.pdx.provider.PdxSerializableSessionSerializer + * @see org.springframework.session.data.gemfire.serialization.SessionSerializer + */ + default String getSessionSerializerBeanName() { + return GemFireHttpSessionConfiguration.DEFAULT_SESSION_SERIALIZER_BEAN_NAME; + } +}