diff --git a/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/serialization/data/support/DataSerializableSessionSerializerInitializerIntegrationTests.java b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/serialization/data/support/DataSerializableSessionSerializerInitializerIntegrationTests.java new file mode 100644 index 0000000..8669a70 --- /dev/null +++ b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/serialization/data/support/DataSerializableSessionSerializerInitializerIntegrationTests.java @@ -0,0 +1,165 @@ +/* + * Copyright 2018 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.serialization.data.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.data.gemfire.util.ArrayUtils.nullSafeArray; +import static org.springframework.session.data.gemfire.serialization.data.support.DataSerializableSessionSerializerInitializer.InitializingGemFireOperationsSessionRepository; + +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import java.util.stream.Collectors; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.apache.geode.DataSerializer; +import org.apache.geode.cache.Cache; +import org.apache.geode.cache.CacheClosedException; +import org.apache.geode.internal.InternalDataSerializer; + +import org.springframework.session.data.gemfire.AbstractGemFireIntegrationTests; +import org.springframework.session.data.gemfire.serialization.data.provider.DataSerializableSessionAttributesSerializer; +import org.springframework.session.data.gemfire.serialization.data.provider.DataSerializableSessionSerializer; + +/** + * Unit tests for {@link DataSerializableSessionSerializerInitializer}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.Spy + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.apache.geode.DataSerializer + * @see org.apache.geode.internal.InternalDataSerializer + * @see org.springframework.session.data.gemfire.AbstractGemFireIntegrationTests + * @see org.springframework.session.data.gemfire.serialization.data.provider.DataSerializableSessionAttributesSerializer + * @see org.springframework.session.data.gemfire.serialization.data.provider.DataSerializableSessionSerializer + * @see org.springframework.session.data.gemfire.serialization.data.support.DataSerializableSessionSerializerInitializer + * @since 1.0.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class DataSerializableSessionSerializerInitializerIntegrationTests extends AbstractGemFireIntegrationTests { + + @Mock + private Cache mockCache; + + @Test + public void constructDataSerializableSessionSerializerInitializerWithGemFireCache() { + + DataSerializableSessionSerializerInitializer initializer = + new DataSerializableSessionSerializerInitializer(this.mockCache); + + assertThat(initializer.getGemFireCache().orElse(null)).isSameAs(this.mockCache); + assertThat(initializer.getLogger()).isNotNull(); + } + + @Test + public void constructDataSerializableSessionSerializerInitializerWithNull() { + + DataSerializableSessionSerializerInitializer initializer = + new DataSerializableSessionSerializerInitializer(null); + + assertThat(initializer.getGemFireCache().orElse(null)).isNull(); + assertThat(initializer.getLogger()).isNotNull(); + } + + @Test + public void ofFactoryMethodInitializesDataSerializableSessionSerializerInitializer() { + + DataSerializableSessionSerializerInitializer initializer = + DataSerializableSessionSerializerInitializer.of(this.mockCache); + + assertThat(initializer).isNotNull(); + assertThat(initializer.getGemFireCache().orElse(null)).isSameAs(this.mockCache); + assertThat(initializer.getLogger()).isNotNull(); + } + + @Test + public void initializeWithCacheAndPropertiesParametersSetGemFireCacheReferenceAndCallsDoInitialize() { + + DataSerializableSessionSerializerInitializer initializer = + spy(DataSerializableSessionSerializerInitializer.of(null)); + + doNothing().when(initializer).doInitialization(); + + assertThat(initializer).isNotNull(); + assertThat(initializer.getGemFireCache().orElse(null)).isNull(); + + initializer.initialize(this.mockCache, new Properties()); + + assertThat(initializer.getGemFireCache().orElse(null)).isSameAs(this.mockCache); + + verify(initializer, times(1)).doInitialization(); + } + + @Test + public void doInitializationIsCorrect() { + + assertThat(InitializingGemFireOperationsSessionRepository.INSTANCE.isDataSerializationConfigured()).isFalse(); + + DataSerializableSessionSerializerInitializer initializer = + DataSerializableSessionSerializerInitializer.of(this.mockCache); + + assertThat(initializer).isNotNull(); + assertThat(initializer.getGemFireCache().orElse(null)).isSameAs(this.mockCache); + + initializer.doInitialization(); + + assertThat(InitializingGemFireOperationsSessionRepository.INSTANCE.isDataSerializationConfigured()).isTrue(); + + List registeredDataSerializerTypes = + Arrays.stream(nullSafeArray(InternalDataSerializer.getSerializers(), DataSerializer.class)) + .map(Object::getClass) + .collect(Collectors.toList()); + + assertThat(registeredDataSerializerTypes) + .containsExactly(DataSerializableSessionSerializer.class, DataSerializableSessionAttributesSerializer.class); + } + + @Test(expected = CacheClosedException.class) + public void doInitializationWhenGemFireCacheIsNotPresentThrowsCacheClosedException() { + + DataSerializableSessionSerializerInitializer initializer = + spy(DataSerializableSessionSerializerInitializer.of(null)); + + assertThat(initializer).isNotNull(); + assertThat(initializer.getGemFireCache().orElse(null)).isNull(); + + try { + initializer.doInitialization(); + } + finally { + + assertThat(initializer.getGemFireCache().orElse(null)).isNull(); + assertThat(InternalDataSerializer.getSerializers()).isEmpty(); + assertThat(InitializingGemFireOperationsSessionRepository.INSTANCE.isDataSerializationConfigured()).isFalse(); + + verify(initializer, times(1)).resolveGemFireCache(); + verify(initializer, never()).configureUseDataSerialization(); + } + } +} diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java index 31c768e..16b4c72 100644 --- a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepository.java @@ -121,6 +121,14 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi private String fullyQualifiedRegionName; + /** + * Protected, default constructor used by extensions of {@link AbstractGemFireOperationsSessionRepository} + * in order to affect and assess {@link SessionRepository} configuration and state. + */ + protected AbstractGemFireOperationsSessionRepository() { + this.template = null; + } + /** * Constructs an instance of {@link AbstractGemFireOperationsSessionRepository} * with a required {@link GemfireOperations} instance used to perform Pivotal GemFire data access operations diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/serialization/data/support/DataSerializableSessionSerializerInitializer.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/serialization/data/support/DataSerializableSessionSerializerInitializer.java new file mode 100644 index 0000000..889e953 --- /dev/null +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/serialization/data/support/DataSerializableSessionSerializerInitializer.java @@ -0,0 +1,196 @@ +/* + * Copyright 2018 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.serialization.data.support; + +import java.util.Optional; +import java.util.Properties; + +import org.apache.geode.DataSerializable; +import org.apache.geode.DataSerializer; +import org.apache.geode.cache.Cache; +import org.apache.geode.cache.CacheClosedException; +import org.apache.geode.cache.CacheFactory; +import org.apache.geode.cache.Declarable; +import org.apache.geode.cache.GemFireCache; + +import org.springframework.lang.Nullable; +import org.springframework.session.Session; +import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration; +import org.springframework.session.data.gemfire.serialization.data.provider.DataSerializableSessionSerializer; +import org.springframework.session.data.gemfire.support.GemFireOperationsSessionRepositorySupport; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Register the Spring Session for Apache Geode/Pivotal GemFire {@link DataSerializableSessionSerializer} + * with Apache Geode/Pivotal GemFire's DataSerialization framework as the {@link DataSerializer} used to handle + * de/serialization of the {@link Session}, the {@literal Session Attributes} and any application + * domain model objects contained in the {@link Session} (if necessary). + * + * @author John Blum + * @see java.util.Properties + * @see org.apache.geode.DataSerializable + * @see org.apache.geode.DataSerializer + * @see org.apache.geode.cache.Cache + * @see org.apache.geode.cache.CacheFactory + * @see org.apache.geode.cache.Declarable + * @see org.apache.geode.cache.GemFireCache + * @see org.springframework.session.Session + * @see org.springframework.session.data.gemfire.GemFireOperationsSessionRepository + * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration + * @see org.springframework.session.data.gemfire.serialization.data.provider.DataSerializableSessionSerializer + * @since 1.0.0 + */ +@SuppressWarnings("unused") +public class DataSerializableSessionSerializerInitializer implements Declarable { + + private static final String DEFAULT_SESSION_REGION_NAME = + GemFireHttpSessionConfiguration.DEFAULT_SESSION_REGION_NAME; + + private static final String SESSION_REGION_NAME_PROPERTY = "spring.session.data.gemfire.session.region.name"; + + private volatile GemFireCache gemfireCache; + + private Logger logger = LoggerFactory.getLogger(getClass()); + + private String sessionRegionName; + + /** + * Factory method used to construct a new instance of {@link DataSerializableSessionSerializerInitializer} + * initialized with the given, non-required {@link GemFireCache}. + * + * @param gemfireCache reference to the {@link GemFireCache} instance. + * @return a new {@link DataSerializableSessionSerializerInitializer} initialized with the given, non-required + * {@link GemFireCache}. + * @see org.apache.geode.cache.GemFireCache + * @see #DataSerializableSessionSerializerInitializer(GemFireCache) + */ + public static DataSerializableSessionSerializerInitializer of(@Nullable GemFireCache gemfireCache) { + return new DataSerializableSessionSerializerInitializer(gemfireCache); + } + + /** + * Default constructor used to construct a new, un-initialized instance of + * {@link DataSerializableSessionSerializerInitializer}. + * + * For use in Apache Geode/Pivotal GemFire {@literal cache.xml}. + */ + public DataSerializableSessionSerializerInitializer() { + this(null); + } + + /** + * Constructs a new instance of {@link DataSerializableSessionSerializerInitializer} initialized with the given, + * non-required {@link GemFireCache}. + * + * This constructor is meant to be used programmatically and users are encouraged to provide a reference to + * the configured and initialized {@link GemFireCache} instance if available. + * + * @param gemfireCache reference to the {@link GemFireCache} instance. {@link GemFireCache} may be {@literal null} + * in to order to "lazy" initialize or resolve the cache. + * @see org.apache.geode.cache.GemFireCache + */ + public DataSerializableSessionSerializerInitializer(@Nullable GemFireCache gemfireCache) { + this.gemfireCache = gemfireCache; + } + + /** + * Returns an {@link Optional} reference to the {@link GemFireCache}. + * + * The {@link GemFireCache} instance may be resolved lazily when the {@link #initialize(Cache, Properties)} method + * is called, such as when processing {@literal cache.xml}. + * + * @return an {@link Optional} reference to the {@link GemFireCache}. + * @see org.apache.geode.cache.GemFireCache + * @see java.util.Optional + */ + protected Optional getGemFireCache() { + return Optional.ofNullable(this.gemfireCache); + } + + /** + * Returns a reference to the configured {@link Logger} used to capture log events and messages. + * + * @return a reference to the configured {@link Logger} used for logging. + * @see org.slf4j.Logger + */ + protected Logger getLogger() { + return this.logger; + } + + /** + * @see #doInitialization() + */ + @Override + public void initialize(Cache cache, Properties parameters) { + + this.gemfireCache = cache; + + doInitialization(); + } + + /** + * Resolves the {@link GemFireCache} instance, configures Spring Session (for Apache Geode/Pivotal GemFire) to + * enable and use the DataSerialization framework and finally, registers the {@link DataSerializer DataSerializers} + * used by Spring Session and required by Apache Geode/Pivotal GemFire to de/serialize the {@link Session} objects + * as {@link DataSerializable} {@link Class types}. + * + * @see #resolveGemFireCache() + * @see #configureUseDataSerialization() + * @see org.springframework.session.data.gemfire.serialization.data.provider.DataSerializableSessionSerializer#register() + */ + public void doInitialization() { + + resolveGemFireCache(); + configureUseDataSerialization(); + DataSerializableSessionSerializer.register(); + } + + /** + * Resolves a reference to the {@link GemFireCache}. + * + * @return a resolved reference to the {@link GemFireCache}. + * @throws CacheClosedException if the {@link GemFireCache} cannot be resolved. + * @see org.apache.geode.cache.GemFireCache + */ + protected GemFireCache resolveGemFireCache() { + return getGemFireCache().orElseGet(CacheFactory::getAnyInstance); + } + + /** + * Configures Spring Session (for Apache Geode/Pivotal GemFire) to "use" Apache Geode/Pivotal GemFire's + * DataSerialization framework and Delta capable {@link DataSerializable} Session objects. + * + * @see org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.DeltaCapableGemFireSession + * @see org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.DeltaCapableGemFireSessionAttributes + */ + protected void configureUseDataSerialization() { + InitializingGemFireOperationsSessionRepository.INSTANCE.setUseDataSerialization(true); + } + + static final class InitializingGemFireOperationsSessionRepository + extends GemFireOperationsSessionRepositorySupport { + + static final InitializingGemFireOperationsSessionRepository INSTANCE = + new InitializingGemFireOperationsSessionRepository(); + + boolean isDataSerializationConfigured() { + return isUsingDataSerialization(); + } + } +} diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/support/GemFireOperationsSessionRepositorySupport.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/support/GemFireOperationsSessionRepositorySupport.java new file mode 100644 index 0000000..d21951a --- /dev/null +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/support/GemFireOperationsSessionRepositorySupport.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018 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.support; + +import java.util.Map; + +import org.springframework.data.gemfire.GemfireOperations; +import org.springframework.session.Session; +import org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository; + +/** + * The GemFireOperationsSessionRepositorySupport class... + * + * @author John Blum + * @since 1.0.0 + */ +public abstract class GemFireOperationsSessionRepositorySupport + extends AbstractGemFireOperationsSessionRepository { + + private static final String OPERATION_NOT_SUPPORTED = "Operation Not Supported"; + + protected GemFireOperationsSessionRepositorySupport() { } + + public GemFireOperationsSessionRepositorySupport(GemfireOperations gemfireOperations) { + super(gemfireOperations); + } + + @Override + public Map findByIndexNameAndIndexValue(String indexName, String indexValue) { + throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED); + } + + @Override + public Session createSession() { + throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED); + } + + @Override + public void deleteById(String id) { + throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED); + } + + @Override + public Session findById(String id) { + throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED); + } + + @Override + public void save(Session session) { + throw new UnsupportedOperationException(OPERATION_NOT_SUPPORTED); + } +}