From 6c9b027b4f9ea83dd5375041fc4ca69bc8200f1e Mon Sep 17 00:00:00 2001 From: John Blum Date: Tue, 28 May 2019 15:54:18 -0700 Subject: [PATCH] DATAGEODE-198 - Define an abstract, lazy-resolving, composable Configurer implementation. --- ...ractLazyResolvingComposableConfigurer.java | 142 ++++++++++++ ...esolvingComposableConfigurerUnitTests.java | 209 ++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 src/main/java/org/springframework/data/gemfire/config/annotation/support/AbstractLazyResolvingComposableConfigurer.java create mode 100644 src/test/java/org/springframework/data/gemfire/config/annotation/support/AbstractLazyResolvingComposableConfigurerUnitTests.java diff --git a/src/main/java/org/springframework/data/gemfire/config/annotation/support/AbstractLazyResolvingComposableConfigurer.java b/src/main/java/org/springframework/data/gemfire/config/annotation/support/AbstractLazyResolvingComposableConfigurer.java new file mode 100644 index 00000000..50267f93 --- /dev/null +++ b/src/main/java/org/springframework/data/gemfire/config/annotation/support/AbstractLazyResolvingComposableConfigurer.java @@ -0,0 +1,142 @@ +/* + * 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.data.gemfire.config.annotation.support; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.core.OrderComparator; +import org.springframework.lang.Nullable; + +/** + * Abstract base class for {@link Configurer} interface implementations, encapsulating logic and functionality + * common to all {@link Configurer} implementations + * + * @author John Blum + * @see org.springframework.beans.factory.BeanFactory + * @see org.springframework.beans.factory.BeanFactoryAware + * @see org.springframework.beans.factory.FactoryBean + * @see org.springframework.data.gemfire.config.annotation.support.Configurer + * @since 2.2.0 + */ +public abstract class AbstractLazyResolvingComposableConfigurer, C extends Configurer> + implements BeanFactoryAware, Configurer { + + private BeanFactory beanFactory; + + private List configurers = Collections.emptyList(); + + /** + * Sets a reference to the configured Spring {@link BeanFactory}. + * + * @param beanFactory reference to the configured Spring {@link BeanFactory}. + * @see org.springframework.beans.factory.BeanFactory + */ + @Override + public void setBeanFactory(@Nullable BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + /** + * Returns a reference to the configured Spring {@link BeanFactory}. + * + * @return a reference to the configured Spring {@link BeanFactory}. + * @see org.springframework.beans.factory.BeanFactory + */ + protected Optional getBeanFactory() { + return Optional.ofNullable(this.beanFactory); + } + + /** + * Returns the primary {@link Class} type of the {@link Configurer} composed by this {@link Configurer}. + * + * @return the primary {@link Class} type of the {@link Configurer} composed by this {@link Configurer}. + * @see java.lang.Class + */ + protected abstract Class getConfigurerType(); + + /** + * Resolves the {@link Configurer Configurers} defined, declared and registered in the Spring application context. + * + * @return a {@link Stream} of {@link Configurer} objects defined, declared and registered in the Spring + * application context. + * @see org.springframework.data.gemfire.config.annotation.support.Configurer + * @see java.util.stream.Stream + */ + protected Stream resolveConfigurers() { + + return Optional.ofNullable(this.configurers) + .filter(it -> !it.isEmpty()) + .orElseGet(() -> getBeanFactory() + .filter(ListableBeanFactory.class::isInstance) + .map(ListableBeanFactory.class::cast) + .map(beanFactory -> { + + Map beansOfType = + beanFactory.getBeansOfType(getConfigurerType(), true, false); + + this.configurers = beansOfType.values().stream() + .sorted(new OrderComparator()) + .collect(Collectors.toList()); + + return this.configurers; + + }) + .orElseGet(Collections::emptyList)) + .stream(); + } + + /** + * Applies the configuration from the composition of {@link Configurer Configurers} composed by this + * {@link Configurer} to the given {@link FactoryBean}. + * + * @param beanName {@link String} containing the name of the Spring bean. + * @param factoryBean {@link FactoryBean} used to construct, configure and initialize the {@link Object} + * @see #resolveConfigurers() + */ + @Override + public synchronized void configure(String beanName, T factoryBean) { + + resolveConfigurers().forEach(configurer -> + configurer.configure(beanName, factoryBean)); + } + + /** + * Configures the Spring {@link BeanFactory} used to resolve {@link Configurer Configurers} from the Spring context. + * + * @param sub-class type of {@link Configurer}. + * @param beanFactory reference to the Spring {@link BeanFactory}. + * @return this {@link AbstractLazyResolvingComposableConfigurer}. + * @see org.springframework.beans.factory.BeanFactory + * @see #setBeanFactory(BeanFactory) + */ + @SuppressWarnings("unchecked") + public > S with(BeanFactory beanFactory) { + + setBeanFactory(beanFactory); + + return (S) this; + } +} diff --git a/src/test/java/org/springframework/data/gemfire/config/annotation/support/AbstractLazyResolvingComposableConfigurerUnitTests.java b/src/test/java/org/springframework/data/gemfire/config/annotation/support/AbstractLazyResolvingComposableConfigurerUnitTests.java new file mode 100644 index 00000000..9415ba1f --- /dev/null +++ b/src/test/java/org/springframework/data/gemfire/config/annotation/support/AbstractLazyResolvingComposableConfigurerUnitTests.java @@ -0,0 +1,209 @@ +/* + * 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.data.gemfire.config.annotation.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.core.Ordered; +import org.springframework.lang.Nullable; + +/** + * Unit Tests for {@link AbstractLazyResolvingComposableConfigurer}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.springframework.beans.factory.FactoryBean + * @see org.springframework.beans.factory.ListableBeanFactory + * @see org.springframework.data.gemfire.config.annotation.support.AbstractLazyResolvingComposableConfigurer + * @since 2.2.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class AbstractLazyResolvingComposableConfigurerUnitTests { + + @Mock + private ListableBeanFactory mockBeanFactory; + + private AbstractLazyResolvingComposableConfigurer configurer; + + @Before + public void setup() { + this.configurer = new TestLazyWiringResolvingComposableConfigurer(); + } + + @Test + public void withBeanFactoryReturnsThis() { + + assertThat(this.configurer.getBeanFactory().orElse(null)).isNull(); + assertThat(this.configurer.>with(this.mockBeanFactory)) + .isEqualTo(this.configurer); + assertThat(this.configurer.getBeanFactory().orElse(null)).isEqualTo(this.mockBeanFactory); + } + + @Test + public void setAndGetBeanFactoryIsCorrect() { + + assertThat(this.configurer.getBeanFactory().orElse(null)).isNull(); + + this.configurer.setBeanFactory(this.mockBeanFactory); + + assertThat(this.configurer.getBeanFactory().orElse(null)).isEqualTo(this.mockBeanFactory); + + this.configurer.setBeanFactory(null); + + assertThat(this.configurer.getBeanFactory().orElse(null)).isNull(); + } + + @Test + public void resolvesConfigurersIsCorrect() { + + TestConfigurer mockConfigurerOne = mock(TestConfigurer.class); + TestConfigurer mockConfigurerTwo = mock(TestConfigurer.class); + + Map beansOfType = new HashMap<>(); + + beansOfType.put("MockConfigurerOne", mockConfigurerOne); + beansOfType.put("MockConfigurerTwo", mockConfigurerTwo); + + when(this.mockBeanFactory.getBeansOfType(eq(TestConfigurer.class), anyBoolean(), anyBoolean())) + .thenReturn(beansOfType); + + this.configurer.setBeanFactory(this.mockBeanFactory); + + Stream streamOne = this.configurer.resolveConfigurers(); + + assertThat(streamOne).isNotNull(); + assertThat(streamOne).containsExactlyInAnyOrder(mockConfigurerOne, mockConfigurerTwo); + + Stream streamTwo = this.configurer.resolveConfigurers(); + + assertThat(streamTwo).isNotNull(); + assertThat(streamTwo).containsExactlyInAnyOrder(mockConfigurerOne, mockConfigurerTwo); + + verify(this.mockBeanFactory, times(1)) + .getBeansOfType(eq(TestConfigurer.class), eq(true), eq(false)); + verify(mockConfigurerOne, atLeastOnce()).getOrder(); + verify(mockConfigurerTwo, atLeastOnce()).getOrder(); + } + + @Test + public void resolvesConfigurersWithNonListableBeanFactoryReturnsEmptyStream() { + + BeanFactory mockBeanFactory = mock(BeanFactory.class); + + this.configurer.setBeanFactory(mockBeanFactory); + + assertThat(this.configurer.getBeanFactory().orElse(null)).isEqualTo(mockBeanFactory); + assertThat(this.configurer.getBeanFactory().orElse(null)).isNotInstanceOf(ListableBeanFactory.class); + + Stream stream = this.configurer.resolveConfigurers(); + + assertThat(stream).isNotNull(); + assertThat(stream).isEmpty(); + } + + @Test + public void resolvesConfigurersWithNullBeanFactoryReturnsEmptyStream() { + + assertThat(this.configurer.getBeanFactory().orElse(null)).isNull(); + + Stream stream = this.configurer.resolveConfigurers(); + + assertThat(stream).isNotNull(); + assertThat(stream).isEmpty(); + } + + @Test + public void configureConfiguresTestFactoryBeanWithComposedConfigurers() { + + TestConfigurer mockConfigurerOne = mock(TestConfigurer.class); + TestConfigurer mockConfigurerTwo = mock(TestConfigurer.class); + + when(mockConfigurerOne.getOrder()).thenReturn(2); + when(mockConfigurerTwo.getOrder()).thenReturn(1); + + Map beansOfType = new HashMap<>(); + + beansOfType.put("MockConfigurerOne", mockConfigurerOne); + beansOfType.put("MockConfigurerTwo", mockConfigurerTwo); + + when(this.mockBeanFactory.getBeansOfType(eq(TestConfigurer.class), anyBoolean(), anyBoolean())) + .thenReturn(beansOfType); + + TestFactoryBean testFactoryBean = new TestFactoryBean(); + + this.configurer.setBeanFactory(this.mockBeanFactory); + this.configurer.configure("TestBean", testFactoryBean); + + InOrder inOrder = Mockito.inOrder(this.mockBeanFactory, mockConfigurerOne, mockConfigurerTwo); + + inOrder.verify(this.mockBeanFactory, times(1)) + .getBeansOfType(eq(TestConfigurer.class), eq(true), eq(false)); + + inOrder.verify(mockConfigurerTwo, times(1)) + .configure(eq("TestBean"), eq(testFactoryBean)); + + inOrder.verify(mockConfigurerOne, times(1)) + .configure(eq("TestBean"), eq(testFactoryBean)); + } + + class TestFactoryBean implements FactoryBean { + + @Nullable @Override + public Object getObject() { + return null; + } + + @Nullable @Override + public Class getObjectType() { + return Object.class; + } + } + + interface TestConfigurer extends Configurer, Ordered { } + + class TestLazyWiringResolvingComposableConfigurer + extends AbstractLazyResolvingComposableConfigurer { + + @Override + protected Class getConfigurerType() { + return TestConfigurer.class; + } + } +}