diff --git a/spring-geode/src/main/java/org/springframework/geode/distributed/event/ApplicationContextMembershipListener.java b/spring-geode/src/main/java/org/springframework/geode/distributed/event/ApplicationContextMembershipListener.java new file mode 100644 index 00000000..0cca33b2 --- /dev/null +++ b/spring-geode/src/main/java/org/springframework/geode/distributed/event/ApplicationContextMembershipListener.java @@ -0,0 +1,98 @@ +/* + * 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.distributed.event; + +import org.apache.geode.distributed.DistributedMember; +import org.apache.geode.distributed.DistributedSystem; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.geode.distributed.event.support.MemberDepartedEvent; +import org.springframework.geode.distributed.event.support.MemberJoinedEvent; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +/** + * The {@link ApplicationContextMembershipListener} class is an extension of {@link MembershipListenerAdapter} used to + * adapt the {@link ConfigurableApplicationContext} to handle and process {@link MembershipEvent membership events}, + * and specifically {@link MemberDepartedEvent} and {@link MemberJoinedEvent}, by + * {@link ConfigurableApplicationContext#close() closing} and {@link ConfigurableApplicationContext#refresh() refreshing} + * the {@link ConfigurableApplicationContext} when the {@link DistributedMember peer member} departs and joins the + * {@link DistributedSystem cluster}. + * + * @author John Blum + * @see org.apache.geode.distributed.DistributedMember + * @see org.apache.geode.distributed.DistributedSystem + * @see org.springframework.context.ConfigurableApplicationContext + * @see org.springframework.geode.distributed.event.support.MemberDepartedEvent + * @see org.springframework.geode.distributed.event.support.MemberJoinedEvent + * @since 1.3.0 + */ +public class ApplicationContextMembershipListener + extends MembershipListenerAdapter { + + private final ConfigurableApplicationContext applicationContext; + + /** + * Constructs a new instance of {@link ConfigurableApplicationContext} initialized with + * the given {@link ConfigurableApplicationContext}. + * + * @param applicationContext configured {@link ConfigurableApplicationContext}; must not be {@literal null}. + * @throws IllegalArgumentException if {@link ConfigurableApplicationContext} is {@literal null}. + * @see org.springframework.context.ConfigurableApplicationContext + */ + public ApplicationContextMembershipListener(@NonNull ConfigurableApplicationContext applicationContext) { + + Assert.notNull(applicationContext, "ConfigurableApplicationContext must not be null"); + + this.applicationContext = applicationContext; + } + + /** + * Returns a reference to the configured {@link ConfigurableApplicationContext}. + * + * @return a reference to the configured {@link ConfigurableApplicationContext}. + * @see org.springframework.context.ConfigurableApplicationContext + */ + protected @NonNull ConfigurableApplicationContext getApplicationContext() { + return this.applicationContext; + } + + /** + * Handles the {@link MembershipEvent membership event} when a {@link DistributedMember peer member} + * departs from the {@link DistributedSystem cluster} by calling {@link ConfigurableApplicationContext#close()}. + * + * @param event {@link MemberDepartedEvent} to handle. + * @see org.springframework.geode.distributed.event.support.MemberDepartedEvent + * @see org.springframework.context.ConfigurableApplicationContext#close() + */ + @Override + public void handleMemberDeparted(MemberDepartedEvent event) { + getApplicationContext().close(); + } + + /** + * Handles the {@link MembershipEvent membership event} when a {@link DistributedMember peer member} + * joins the {@link DistributedSystem cluster} by calling {@link ConfigurableApplicationContext#refresh()}. + * + * @param event {@link MemberJoinedEvent} to handle. + * @see org.springframework.geode.distributed.event.support.MemberJoinedEvent + * @see org.springframework.context.ConfigurableApplicationContext#refresh() + */ + @Override + public void handleMemberJoined(MemberJoinedEvent event) { + getApplicationContext().refresh(); + } +} diff --git a/spring-geode/src/test/java/org/springframework/geode/distributed/event/ApplicationContextMembershipListenerUnitTests.java b/spring-geode/src/test/java/org/springframework/geode/distributed/event/ApplicationContextMembershipListenerUnitTests.java new file mode 100644 index 00000000..6829da63 --- /dev/null +++ b/spring-geode/src/test/java/org/springframework/geode/distributed/event/ApplicationContextMembershipListenerUnitTests.java @@ -0,0 +1,106 @@ +/* + * 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.distributed.event; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +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 org.junit.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.geode.distributed.event.support.MemberDepartedEvent; +import org.springframework.geode.distributed.event.support.MemberJoinedEvent; + +/** + * Unit Tests for {@link ApplicationContextMembershipListener}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mockito + * @see org.springframework.context.ConfigurableApplicationContext + * @see org.springframework.geode.distributed.event.ApplicationContextMembershipListener + * @see org.springframework.geode.distributed.event.support.MemberDepartedEvent + * @see org.springframework.geode.distributed.event.support.MemberJoinedEvent + * @since 1.3.0 + */ +public class ApplicationContextMembershipListenerUnitTests { + + @Test + public void constructsApplicationContextMembershipListener() { + + ConfigurableApplicationContext mockApplicationContext = mock(ConfigurableApplicationContext.class); + + ApplicationContextMembershipListener listener = + new ApplicationContextMembershipListener(mockApplicationContext); + + assertThat(listener).isNotNull(); + assertThat(listener.getApplicationContext()).isEqualTo(mockApplicationContext); + + verifyNoInteractions(mockApplicationContext); + } + + @Test(expected = IllegalArgumentException.class) + public void constructsApplicationContextMembershipListenerWithNullThrowsIllegalArgumentException() { + + try { + new ApplicationContextMembershipListener(null); + } + catch (IllegalArgumentException expected) { + + assertThat(expected).hasMessage("ConfigurableApplicationContext must not be null"); + assertThat(expected).hasNoCause(); + + throw expected; + } + } + + @Test + public void handleMemberDepartedCallsApplicationContextClose() { + + ConfigurableApplicationContext mockApplicationContext = mock(ConfigurableApplicationContext.class); + + MemberDepartedEvent mockEvent = mock(MemberDepartedEvent.class); + + ApplicationContextMembershipListener listener = + spy(new ApplicationContextMembershipListener(mockApplicationContext)); + + listener.handleMemberDeparted(mockEvent); + + verify(mockApplicationContext, times(1)).close(); + verifyNoMoreInteractions(mockApplicationContext); + } + + @Test + public void handleMemberJoinedCallApplicationContextRefresh() { + + ConfigurableApplicationContext mockApplicationContext = mock(ConfigurableApplicationContext.class); + + MemberJoinedEvent mockEvent = mock(MemberJoinedEvent.class); + + ApplicationContextMembershipListener listener = + spy(new ApplicationContextMembershipListener(mockApplicationContext)); + + listener.handleMemberJoined(mockEvent); + + verify(mockApplicationContext, times(1)).refresh(); + verifyNoMoreInteractions(mockApplicationContext); + } +}