From ce73d4dbc0447a222fd78202c4edee551c29344b Mon Sep 17 00:00:00 2001 From: John Blum Date: Thu, 25 Mar 2021 01:26:46 -0700 Subject: [PATCH] Create the MappingPdxSerializerIncludedTypesRegistrar utility class and Spring BeanPostProcessor used to register additional class types to be 'included' in the accepted types de/serialized as PDX with the SDG MappingPdxSerializer. --- ...ngPdxSerializerIncludedTypesRegistrar.java | 169 ++++++++++++ ...alizerIncludedTypesRegistrarUnitTests.java | 247 ++++++++++++++++++ 2 files changed, 416 insertions(+) create mode 100644 spring-geode/src/main/java/org/springframework/geode/pdx/MappingPdxSerializerIncludedTypesRegistrar.java create mode 100644 spring-geode/src/test/java/org/springframework/geode/pdx/MappingPdxSerializerIncludedTypesRegistrarUnitTests.java diff --git a/spring-geode/src/main/java/org/springframework/geode/pdx/MappingPdxSerializerIncludedTypesRegistrar.java b/spring-geode/src/main/java/org/springframework/geode/pdx/MappingPdxSerializerIncludedTypesRegistrar.java new file mode 100644 index 00000000..bd16444a --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/pdx/MappingPdxSerializerIncludedTypesRegistrar.java @@ -0,0 +1,169 @@ +/* + * Copyright 2020 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 + * + * https://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.geode.pdx; + +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Predicate; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.pdx.PdxSerializer; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.data.gemfire.mapping.MappingPdxSerializer; +import org.springframework.data.gemfire.util.ArrayUtils; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; + +/** + * Spring {@link BeanPostProcessor} used to register additional {@link Class types} handled by + * the SDG {@link MappingPdxSerializer}. + * + * @author John Blum + * @see java.lang.Class + * @see java.util.function.Predicate + * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.pdx.PdxSerializer + * @see org.springframework.beans.factory.config.BeanPostProcessor + * @see org.springframework.data.gemfire.mapping.MappingPdxSerializer + * @since 1.5.0 + */ +@SuppressWarnings("unused") +public class MappingPdxSerializerIncludedTypesRegistrar implements BeanPostProcessor { + + /** + * Factory methods used to construct a new instance of {@link MappingPdxSerializerIncludedTypesRegistrar} + * initialized with given, required array of {@link Class} types that will be registered with + * SDG's {@link MappingPdxSerializer} in order to de/serialize the specified {@link Class} types as PDX. + * + * @param types array of {@link Class} types to be de/serialized as PDX using SDG's {@link MappingPdxSerializer}; + * must not be {@literal null}. + * @return a new instance of {@link MappingPdxSerializerIncludedTypesRegistrar} + * @see #MappingPdxSerializerIncludedTypesRegistrar(Class[]) + */ + public static @NonNull MappingPdxSerializerIncludedTypesRegistrar with(Class... types) { + return new MappingPdxSerializerIncludedTypesRegistrar(types); + } + + private final Class[] types; + + /** + * Constructs a new instance of {@link MappingPdxSerializerIncludedTypesRegistrar} initialized with given, + * required array of {@link Class} types that will be registered with SDG's {@link MappingPdxSerializer} + * in order to de/serialize the specified {@link Class} types as PDX. + * + * @param types array of {@link Class} types to be de/serialized as PDX using SDG's {@link MappingPdxSerializer}; + * must not be {@literal null}. + * @see java.lang.Class + */ + public MappingPdxSerializerIncludedTypesRegistrar(@NonNull Class[] types) { + + this.types = Arrays.stream(ArrayUtils.nullSafeArray(types, Class.class)) + .filter(Objects::nonNull) + .toArray(Class[]::new); + } + + /** + * Gets the array of {@link Class} types to register with SDG's {@link MappingPdxSerializer} in order to + * de/serialize the {@link Class} types as PDX. + * + * @return the configured array of {@link Class} types to be registered with SDG's {@link MappingPdxSerializer} + * in order to de/serialize the {@link Class} types as PDX; never {@literal null}. + * @see java.lang.Class + */ + protected @NonNull Class[] getTypes() { + return this.types; + } + + /** + * Composes an {@link Optional} {@link Predicate} consisting of the configured array {@link Class} types + * used to match possible types de/serialized as PDX using SDG's {@link MappingPdxSerializer}. + * + * @return an {@link Optional} composite {@link Predicate} consisting of the configured + * array of {@link Class} types. + * @see java.util.function.Predicate + * @see java.util.Optional + * @see #getTypes() + */ + protected Optional>> getCompositeIncludeTypeFilter() { + + Predicate> compositeIncludeTypeFilter = null; + + for (Class type : getTypes()) { + if (type != null) { + compositeIncludeTypeFilter = compositeIncludeTypeFilter != null + ? compositeIncludeTypeFilter.or(newIncludeTypeFilter(type)) + : newIncludeTypeFilter(type); + } + } + + return Optional.ofNullable(compositeIncludeTypeFilter); + } + + /** + * Null-safe method used to construct a new {@link Class type} include {@link Predicate filter} + * that can be registered with SDG's {@link MappingPdxSerializer}. + * + * The {@link Predicate} matches tested {@link Class types} that are + * {@link Class#isAssignableFrom(Class) assignable from} the given {@link Class type}. + * + * @param type {@link Class} used as the basis for matching in the {@link Predicate}. + * @return an (optional} {@link Predicate} from the given {@link Class type}; + * returns {@literal null} if the given {@link Class type} is {@literal null}. + * @see java.util.function.Predicate + * @see java.lang.Class + */ + protected @Nullable Predicate> newIncludeTypeFilter(@Nullable Class type) { + + return type != null + ? testType -> Objects.nonNull(testType) && type.isAssignableFrom(testType) + : null; + } + + /** + * Registers the configured {@link Class} types with SDG's {@link MappingPdxSerializer} providing the bean + * to post process after initialization is a {@link GemFireCache} instance and SDG's {@link MappingPdxSerializer} + * was configured as the cache's {@link PdxSerializer} used to de/serialize objects of the specified {@link Class} + * types. + * + * @param bean {@link Object bean} to evaluate. + * @param beanName {@link String} specifying the {@literal name} of the bean in the Spring container. + * @return the given {@link Object} bean. + * @throws BeansException if post processing of the bean fails. + */ + @Override + public @Nullable Object postProcessAfterInitialization(@Nullable Object bean, @Nullable String beanName) + throws BeansException { + + if (bean instanceof GemFireCache) { + + GemFireCache cache = (GemFireCache) bean; + + PdxSerializer pdxSerializer = cache.getPdxSerializer(); + + if (pdxSerializer instanceof MappingPdxSerializer) { + + MappingPdxSerializer mappingPdxSerializer = (MappingPdxSerializer) pdxSerializer; + + getCompositeIncludeTypeFilter().ifPresent(mappingPdxSerializer::setIncludeTypeFilters); + } + } + + return bean; + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/pdx/MappingPdxSerializerIncludedTypesRegistrarUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/pdx/MappingPdxSerializerIncludedTypesRegistrarUnitTests.java new file mode 100644 index 00000000..58622840 --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/pdx/MappingPdxSerializerIncludedTypesRegistrarUnitTests.java @@ -0,0 +1,247 @@ +/* + * Copyright 2020 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 + * + * https://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.geode.pdx; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +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.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.Optional; +import java.util.function.Predicate; + +import org.junit.Test; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.pdx.PdxSerializer; + +import org.springframework.data.gemfire.mapping.MappingPdxSerializer; + +/** + * Unit Tests for {@link MappingPdxSerializerIncludedTypesRegistrar}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.apache.geode.cache.GemFireCache + * @see org.apache.geode.pdx.PdxSerializer + * @see org.springframework.geode.pdx.MappingPdxSerializerIncludedTypesRegistrar + * @since 1.5.0 + */ +public class MappingPdxSerializerIncludedTypesRegistrarUnitTests { + + @Test + public void withClassTypesIsCorrect() { + + Class[] types = { TypeOne.class, TypeTwo.class }; + + MappingPdxSerializerIncludedTypesRegistrar registrar = MappingPdxSerializerIncludedTypesRegistrar.with(types); + + assertThat(registrar).isNotNull(); + assertThat(registrar.getTypes()).containsExactly(types); + + } + + @Test + public void withClassTypesContainingNullTypesIsCorrect() { + + Class[] types = { null, TypeOne.class, null, null, TypeTwo.class, null, null }; + + MappingPdxSerializerIncludedTypesRegistrar registrar = MappingPdxSerializerIncludedTypesRegistrar.with(types); + + assertThat(registrar).isNotNull(); + assertThat(registrar.getTypes()).containsExactly(TypeOne.class, TypeTwo.class); + } + + @Test + public void withNullClassTypesIsNullSafe() { + + MappingPdxSerializerIncludedTypesRegistrar registrar = + MappingPdxSerializerIncludedTypesRegistrar.with((Class[]) null); + + assertThat(registrar).isNotNull(); + assertThat(registrar.getTypes()).isNotNull(); + assertThat(registrar.getTypes()).isEmpty(); + } + + @Test + public void getCompositeIncludeTypeFilterIsCorrect() { + + MappingPdxSerializerIncludedTypesRegistrar registrar = + MappingPdxSerializerIncludedTypesRegistrar.with(TypeOne.class, TypeTwo.class); + + assertThat(registrar).isNotNull(); + assertThat(registrar.getTypes()).containsExactly(TypeOne.class, TypeTwo.class); + + Optional>> composite = registrar.getCompositeIncludeTypeFilter(); + + assertThat(composite).isNotNull(); + assertThat(composite.isPresent()).isTrue(); + + assertThat(composite.get().test(TypeOne.class)).isTrue(); + assertThat(composite.get().test(TypeTwo.class)).isTrue(); + assertThat(composite.get().test(SubtypeOne.class)).isTrue(); + assertThat(composite.get().test(TypeThree.class)).isFalse(); + assertThat(composite.get().test(null)).isFalse(); + } + + @Test + public void getCompositeIncludeTypeFilterIsNullSafe() { + + MappingPdxSerializerIncludedTypesRegistrar registrar = MappingPdxSerializerIncludedTypesRegistrar.with(); + + assertThat(registrar).isNotNull(); + assertThat(registrar.getTypes()).isEmpty(); + + Optional>> composite = registrar.getCompositeIncludeTypeFilter(); + + assertThat(composite).isNotNull(); + assertThat(composite.isPresent()).isFalse(); + } + + @Test + public void newIncludeTypeFilterWithNonNullType() { + + Predicate> predicate = + MappingPdxSerializerIncludedTypesRegistrar.with().newIncludeTypeFilter(SubtypeOne.class); + + assertThat(predicate).isNotNull(); + assertThat(predicate.test(SubtypeOne.class)).isTrue(); + assertThat(predicate.test(TypeOne.class)).isFalse(); + assertThat(predicate.test(TypeTwo.class)).isFalse(); + assertThat(predicate.test(null)).isFalse(); + } + + @Test + public void newIncludeTypeFilterWithNullType() { + assertThat(MappingPdxSerializerIncludedTypesRegistrar.with().newIncludeTypeFilter(null)).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + public void postProcessGemFireCacheAfterInitializationIsCorrect() { + + GemFireCache mockCache = mock(GemFireCache.class); + + MappingPdxSerializer mockPdxSerializer = mock(MappingPdxSerializer.class); + + doReturn(mockPdxSerializer).when(mockCache).getPdxSerializer(); + + MappingPdxSerializerIncludedTypesRegistrar registrar = + MappingPdxSerializerIncludedTypesRegistrar.with(TypeOne.class); + + assertThat(registrar).isNotNull(); + + assertThat(registrar.postProcessAfterInitialization(mockCache, "MockCache")).isEqualTo(mockCache); + + verify(mockCache, times(1)).getPdxSerializer(); + verify(mockPdxSerializer, times(1)).setIncludeTypeFilters(isA(Predicate.class)); + + verifyNoMoreInteractions(mockCache, mockPdxSerializer); + } + + @Test + public void postProcessGemFireCacheAfterInitializationWithNoRegisteredTypes() { + + GemFireCache mockCache = mock(GemFireCache.class); + + MappingPdxSerializer mockPdxSerializer = mock(MappingPdxSerializer.class); + + doReturn(mockPdxSerializer).when(mockCache).getPdxSerializer(); + + MappingPdxSerializerIncludedTypesRegistrar registrar = MappingPdxSerializerIncludedTypesRegistrar.with(); + + assertThat(registrar).isNotNull(); + assertThat(registrar.postProcessAfterInitialization(mockCache, "MockCache")).isEqualTo(mockCache); + + verify(mockCache, times(1)).getPdxSerializer(); + verifyNoMoreInteractions(mockCache); + verifyNoInteractions(mockPdxSerializer); + } + + @Test + public void postProcessGemFireCacheAfterInitializationWithNoPdxSerializer() { + + GemFireCache mockCache = mock(GemFireCache.class); + + doReturn(null).when(mockCache).getPdxSerializer(); + + MappingPdxSerializerIncludedTypesRegistrar registrar = spy(MappingPdxSerializerIncludedTypesRegistrar.with()); + + assertThat(registrar).isNotNull(); + assertThat(registrar.postProcessAfterInitialization(mockCache, "MockCache")).isEqualTo(mockCache); + + verify(mockCache, times(1)).getPdxSerializer(); + verify(registrar, never()).getCompositeIncludeTypeFilter(); + verifyNoMoreInteractions(mockCache); + } + + @Test + public void postProcessGemFireCacheAfterInitializationWithNonMappingPdxSerializer() { + + GemFireCache mockCache = mock(GemFireCache.class); + + PdxSerializer mockPdxSerializer = mock(PdxSerializer.class); + + doReturn(mockPdxSerializer).when(mockCache).getPdxSerializer(); + + MappingPdxSerializerIncludedTypesRegistrar registrar = spy(MappingPdxSerializerIncludedTypesRegistrar.with()); + + assertThat(registrar).isNotNull(); + assertThat(registrar.postProcessAfterInitialization(mockCache, "MockCache")).isEqualTo(mockCache); + + verify(mockCache, times(1)).getPdxSerializer(); + verify(registrar, never()).getCompositeIncludeTypeFilter(); + verifyNoMoreInteractions(mockCache); + verifyNoInteractions(mockPdxSerializer); + } + + @Test + public void postProcessNonGemFireCacheBeanAfterInitialization() { + + Object bean = new Object(); + + MappingPdxSerializerIncludedTypesRegistrar registrar = + spy(MappingPdxSerializerIncludedTypesRegistrar.with(TypeOne.class)); + + assertThat(registrar).isNotNull(); + assertThat(registrar.postProcessAfterInitialization(bean, "TestBean")).isEqualTo(bean); + + verify(registrar, never()).getCompositeIncludeTypeFilter(); + } + + @Test + public void postProcessAfterInitializationIsNullSafe() { + + assertThat(MappingPdxSerializerIncludedTypesRegistrar.with() + .postProcessAfterInitialization(null, "NullBean")).isNull(); + } + + static class TypeOne { } + + static class TypeTwo { } + + static class TypeThree { } + + static class SubtypeOne extends TypeOne { } + +}