diff --git a/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/AbstractGemFireIntegrationTests.java b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/AbstractGemFireIntegrationTests.java index b673c1c..550beb9 100644 --- a/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/AbstractGemFireIntegrationTests.java +++ b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/AbstractGemFireIntegrationTests.java @@ -42,14 +42,15 @@ import org.apache.geode.cache.query.Index; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.data.gemfire.tests.integration.ForkingClientServerIntegrationTestsSupport; import org.springframework.data.gemfire.util.RegionUtils; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.session.Session; import org.springframework.session.SessionRepository; import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration; -import org.springframework.session.events.AbstractSessionEvent; import org.springframework.util.StringUtils; /** @@ -61,15 +62,16 @@ import org.springframework.util.StringUtils; * @see java.net.URL * @see java.time.Instant * @see org.junit.Test + * @see org.apache.geode.cache.DataPolicy * @see org.apache.geode.cache.ExpirationAttributes * @see org.apache.geode.cache.GemFireCache * @see org.apache.geode.cache.Region * @see org.apache.geode.cache.query.Index + * @see org.springframework.context.ApplicationEvent * @see org.springframework.context.ApplicationContext * @see org.springframework.context.ApplicationListener * @see org.springframework.data.gemfire.tests.integration.ForkingClientServerIntegrationTestsSupport * @see org.springframework.session.Session - * @see org.springframework.session.SessionRepository * @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration * @see org.springframework.session.events.AbstractSessionEvent * @since 1.1.0 @@ -422,12 +424,12 @@ public abstract class AbstractGemFireIntegrationTests extends ForkingClientServe * @see org.springframework.context.ApplicationListener * @see org.springframework.session.events.AbstractSessionEvent */ - public static class SessionEventListener implements ApplicationListener { + public static class SessionEventListener implements ApplicationListener { - private volatile AbstractSessionEvent sessionEvent; + private volatile ApplicationEvent sessionEvent; @SuppressWarnings("unchecked") - public T getSessionEvent() { + public T getSessionEvent() { T sessionEvent = (T) this.sessionEvent; @@ -436,11 +438,12 @@ public abstract class AbstractGemFireIntegrationTests extends ForkingClientServe return sessionEvent; } - public void onApplicationEvent(AbstractSessionEvent event) { + @Override + public void onApplicationEvent(@NonNull ApplicationEvent event) { this.sessionEvent = event; } - public T waitForSessionEvent(long duration) { + public T waitForSessionEvent(long duration) { waitOn(() -> SessionEventListener.this.sessionEvent != null, duration); diff --git a/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSessionEventsIntegrationTests.java b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSessionEventsIntegrationTests.java index 6abd59f..8b1c70c 100644 --- a/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSessionEventsIntegrationTests.java +++ b/spring-session-data-geode/src/integration-test/java/org/springframework/session/data/gemfire/config/annotation/web/http/EnableGemFireHttpSessionEventsIntegrationTests.java @@ -13,7 +13,6 @@ * 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; @@ -34,11 +33,13 @@ import org.apache.geode.cache.Region; import org.apache.geode.cache.RegionShortcut; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEvent; import org.springframework.context.annotation.Bean; import org.springframework.data.gemfire.config.annotation.PeerCacheApplication; import org.springframework.session.Session; import org.springframework.session.data.gemfire.AbstractGemFireIntegrationTests; import org.springframework.session.data.gemfire.GemFireOperationsSessionRepository; +import org.springframework.session.data.gemfire.events.SessionChangedEvent; import org.springframework.session.data.gemfire.support.GemFireUtils; import org.springframework.session.events.AbstractSessionEvent; import org.springframework.session.events.SessionCreatedEvent; @@ -125,6 +126,24 @@ public class EnableGemFireHttpSessionEventsIntegrationTests extends AbstractGemF assertThat(createdSession.isExpired()).isFalse(); } + @Test + public void sessionChangedEvent() { + + Session expectedSession = save(createSession()); + + ApplicationEvent sessionEvent = this.sessionEventListener.getSessionEvent(); + + assertThat(sessionEvent).isInstanceOf(SessionCreatedEvent.class); + + expectedSession.setAttribute("testKey", "testValue"); + save(touch(expectedSession)); + + sessionEvent = this.sessionEventListener.waitForSessionEvent(500L); + + assertThat(sessionEvent).isInstanceOf(SessionChangedEvent.class); + assertThat(((SessionChangedEvent) sessionEvent).getSession()).isEqualTo(expectedSession); + } + @Test public void getExistingNonExpiredSession() { @@ -212,7 +231,7 @@ public class EnableGemFireHttpSessionEventsIntegrationTests extends AbstractGemF assertThat(sessionEvent).isInstanceOf(SessionDeletedEvent.class); assertThat(sessionEvent.getSession()).isEqualTo(expectedSession); assertThat(sessionEvent.getSessionId()).isEqualTo(expiredSession.getId()); - assertThat(this.gemfireSessionRepository.findById(sessionEvent.getSessionId())).isNull(); + assertThat(this.gemfireSessionRepository.findById(sessionEvent.getSessionId())).isNull(); } @Test @@ -220,7 +239,7 @@ public class EnableGemFireHttpSessionEventsIntegrationTests extends AbstractGemF String expectedSessionId = UUID.randomUUID().toString(); - assertThat(this.gemfireSessionRepository.findById(expectedSessionId)).isNull(); + assertThat(this.gemfireSessionRepository.findById(expectedSessionId)).isNull(); this.gemfireSessionRepository.deleteById(expectedSessionId); 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 0aa3d8a..fbb4897 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 @@ -62,6 +62,7 @@ import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.Session; import org.springframework.session.SessionRepository; import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration; +import org.springframework.session.data.gemfire.events.SessionChangedEvent; import org.springframework.session.data.gemfire.support.GemFireUtils; import org.springframework.session.data.gemfire.support.IsDirtyPredicate; import org.springframework.session.data.gemfire.support.SessionIdHolder; @@ -1541,6 +1542,25 @@ public abstract class AbstractGemFireOperationsSessionRepository .publishEvent(newSessionExpiredEvent(toSession(event.getOldValue(), it.getKey())))); } + /** + * Callback method triggered when an entry is updated in the {@link Session} cache {@link Region}. + * + * @param event {@link EntryEvent} containing the details of the cache operation. + * @see org.springframework.session.data.gemfire.events.SessionChangedEvent + * @see org.springframework.session.Session + * @see org.apache.geode.cache.EntryEvent + * @see #newSessionChangedEvent(Session) + * @see #publishEvent(ApplicationEvent) + * @see #toSession(Object, Object) + */ + @Override + public void afterUpdate(EntryEvent event) { + + Optional.ofNullable(event) + .ifPresent(it -> getSessionRepository() + .publishEvent(newSessionChangedEvent(toSession(event.getNewValue(), it.getKey())))); + } + /** * Constructs a new {@link SessionCreatedEvent} initialized with the given {@link Session}, * using the {@link #getSessionRepository() SessionRepository} as the event source. @@ -1555,6 +1575,20 @@ public abstract class AbstractGemFireOperationsSessionRepository return new SessionCreatedEvent(getSessionRepository(), session); } + /** + * Constructs a new {@link SessionChangedEvent} initialized with the given {@link Session}, + * using the {@link #getSessionRepository() SessionRepository} as the event source. + * + * @param session {@link Session} that is the subject of the {@link ApplicationEvent change event}. + * @return a new {@link SessionChangedEvent}. + * @see org.springframework.session.data.gemfire.events.SessionChangedEvent + * @see org.springframework.session.Session + * @see #getSessionRepository() + */ + protected SessionChangedEvent newSessionChangedEvent(Session session) { + return new SessionChangedEvent(getSessionRepository(), session); + } + /** * Constructs a new {@link SessionDeletedEvent} initialized with the given {@link Session}, * using the {@link #getSessionRepository() SessionRepository} as the event source. diff --git a/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/events/SessionChangedEvent.java b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/events/SessionChangedEvent.java new file mode 100644 index 0000000..f8ea31e --- /dev/null +++ b/spring-session-data-geode/src/main/java/org/springframework/session/data/gemfire/events/SessionChangedEvent.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019 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.session.data.gemfire.events; + +import org.springframework.context.ApplicationEvent; +import org.springframework.session.Session; + +/** + * {@link SessionChangedEvent} is a Spring {@link ApplicationEvent} fire when the {@link Session} state changes. + * + * @author John Blum + * @see org.springframework.context.ApplicationEvent + * @see org.springframework.session.Session + * @since 2.2.0 + */ +public class SessionChangedEvent extends ApplicationEvent { + + private final Session session; + + /** + * Constructs a new instance of {@link SessionChangedEvent} initialized with the given {@link Object source} + * and {@link Session}. + * + * @param source {@link Object} referencing the source of the event. + * @param session {@link Session} that changed. + * @see org.springframework.session.Session + */ + public SessionChangedEvent(Object source, Session session) { + + super(source); + + this.session = session; + } + + /** + * Gets the {@link Session} that was changed. + * + * @param {@link Class type} of {@link Session}. + * @return a reference to the {@link Session} that is the subject of the change event. + * @see org.springframework.session.Session + */ + @SuppressWarnings("unchecked") + public S getSession() { + return (S) this.session; + } +} diff --git a/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java index 72e5073..77c3001 100644 --- a/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java +++ b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/AbstractGemFireOperationsSessionRepositoryTests.java @@ -88,6 +88,7 @@ import org.springframework.lang.Nullable; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.Session; import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration; +import org.springframework.session.data.gemfire.events.SessionChangedEvent; import org.springframework.session.data.gemfire.support.DeltaAwareDirtyPredicate; import org.springframework.session.data.gemfire.support.EqualsDirtyPredicate; import org.springframework.session.data.gemfire.support.GemFireOperationsSessionRepositorySupport; @@ -1401,6 +1402,41 @@ public class AbstractGemFireOperationsSessionRepositoryTests { verify(this.sessionRepository, never()).publishEvent(any(ApplicationEvent.class)); } + @Test + @SuppressWarnings("unchecked") + public void afterUpdateHandlesSessionPublishesSessionChangedEvent() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + EntryEvent mockEntryEvent = mock(EntryEvent.class); + + when(mockEntryEvent.getKey()).thenReturn("1"); + when(mockEntryEvent.getNewValue()).thenReturn(this.mockSession); + + sessionEventHandler.afterUpdate(mockEntryEvent); + + verify(mockEntryEvent, times(1)).getKey(); + verify(mockEntryEvent, times(1)).getNewValue(); + verify(mockEntryEvent, never()).getOldValue(); + verify(sessionEventHandler, times(1)).newSessionChangedEvent(eq(this.mockSession)); + verify(sessionEventHandler, times(1)).toSession(eq(this.mockSession), eq("1")); + verify(this.sessionRepository, times(1)).publishEvent(isA(SessionChangedEvent.class)); + } + + @Test + public void afterUpdateHandlesNullEntryEventDoesNotPublishSessionChangedEvent() { + + SessionEventHandlerCacheListenerAdapter sessionEventHandler = + spy(this.sessionRepository.newSessionEventHandler()); + + sessionEventHandler.afterUpdate(null); + + verify(sessionEventHandler, never()).newSessionChangedEvent(any(Session.class)); + verify(sessionEventHandler, never()).toSession(any(), any()); + verify(this.sessionRepository, never()).publishEvent(any(ApplicationEvent.class)); + } + @Test public void sessionCreateCreateExpireRecreatePublishesSessionEventsCreateExpireCreate() { diff --git a/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/events/SessionChangedEventUnitTests.java b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/events/SessionChangedEventUnitTests.java new file mode 100644 index 0000000..2cf5e30 --- /dev/null +++ b/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire/events/SessionChangedEventUnitTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019 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.session.data.gemfire.events; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import org.springframework.session.Session; + +/** + * Unit Tests for {@link SessionChangedEvent}. + * + * @author John Blum + * @see org.junit.Test + * @see org.mockito.Mock + * @see org.mockito.Mockito + * @see org.mockito.junit.MockitoJUnitRunner + * @see org.springframework.session.Session + * @since 2.2.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class SessionChangedEventUnitTests { + + @Mock + private Session mockSession; + + @Test + public void constructSessionChangedEvent() { + + Object source = new Object(); + + SessionChangedEvent event = new SessionChangedEvent(source, this.mockSession); + + assertThat(event).isNotNull(); + assertThat(event.getSource()).isEqualTo(source); + assertThat(event.getSession()).isEqualTo(this.mockSession); + } +}