Re-implement Session ID interest registration using Apache Geode/Pivotal GemFire Region.registerInterest(..) inside a registered, custom CacheListener.
Resolves gh-6.
This commit is contained in:
@@ -54,10 +54,10 @@ import org.springframework.test.context.web.WebAppConfiguration;
|
||||
* of the {@link GemFireOperationsSessionRepository} and GemFire's configuration.
|
||||
*
|
||||
* @author John Blum
|
||||
* @since 1.1.0
|
||||
* @see org.junit.Test
|
||||
* @see org.junit.runner.RunWith
|
||||
* @see org.apache.geode.cache.Region
|
||||
* @see org.springframework.data.gemfire.config.annotation.PeerCacheApplication
|
||||
* @see org.springframework.session.Session
|
||||
* @see org.springframework.session.data.gemfire.AbstractGemFireIntegrationTests
|
||||
* @see org.springframework.session.data.gemfire.GemFireOperationsSessionRepository
|
||||
@@ -68,6 +68,7 @@ import org.springframework.test.context.web.WebAppConfiguration;
|
||||
* @see org.springframework.test.context.ContextConfiguration
|
||||
* @see org.springframework.test.context.junit4.SpringRunner
|
||||
* @see org.springframework.test.context.web.WebAppConfiguration
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration
|
||||
@@ -164,6 +165,7 @@ public class EnableGemFireHttpSessionEventsIntegrationTests extends AbstractGemF
|
||||
Session expectedSession = save(touch(createSession()));
|
||||
Session savedSession = this.gemfireSessionRepository.findById(expectedSession.getId());
|
||||
|
||||
assertThat(savedSession).isNotNull();
|
||||
assertThat(savedSession).isEqualTo(expectedSession);
|
||||
assertThat(savedSession.isExpired()).isFalse();
|
||||
|
||||
|
||||
@@ -38,15 +38,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import org.apache.geode.DataSerializable;
|
||||
import org.apache.geode.DataSerializer;
|
||||
import org.apache.geode.Delta;
|
||||
import org.apache.geode.Instantiator;
|
||||
import org.apache.geode.InvalidDeltaException;
|
||||
import org.apache.geode.cache.AttributesMutator;
|
||||
import org.apache.geode.cache.EntryEvent;
|
||||
import org.apache.geode.cache.InterestResultPolicy;
|
||||
import org.apache.geode.cache.Operation;
|
||||
import org.apache.geode.cache.Region;
|
||||
import org.apache.geode.cache.util.CacheListenerAdapter;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
@@ -62,6 +60,8 @@ import org.springframework.session.SessionRepository;
|
||||
import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration;
|
||||
import org.springframework.session.data.gemfire.support.GemFireUtils;
|
||||
import org.springframework.session.data.gemfire.support.SessionIdHolder;
|
||||
import org.springframework.session.data.gemfire.support.SessionUtils;
|
||||
import org.springframework.session.events.AbstractSessionEvent;
|
||||
import org.springframework.session.events.SessionCreatedEvent;
|
||||
import org.springframework.session.events.SessionDeletedEvent;
|
||||
import org.springframework.session.events.SessionDestroyedEvent;
|
||||
@@ -78,35 +78,45 @@ import org.apache.commons.logging.LogFactory;
|
||||
* common to all implementations that support {@link SessionRepository} operations backed by Apache Geode.
|
||||
*
|
||||
* @author John Blum
|
||||
* @see java.time.Duration
|
||||
* @see java.time.Instant
|
||||
* @see org.apache.geode.DataSerializable
|
||||
* @see org.apache.geode.DataSerializer
|
||||
* @see org.apache.geode.Delta
|
||||
* @see org.apache.geode.Instantiator
|
||||
* @see org.apache.geode.cache.EntryEvent
|
||||
* @see org.apache.geode.cache.Operation
|
||||
* @see org.apache.geode.cache.Region
|
||||
* @see org.springframework.beans.factory.InitializingBean
|
||||
* @see org.apache.geode.cache.util.CacheListenerAdapter
|
||||
* @see org.springframework.context.ApplicationEvent
|
||||
* @see org.springframework.context.ApplicationEventPublisher
|
||||
* @see org.springframework.context.ApplicationEventPublisherAware
|
||||
* @see org.springframework.data.gemfire.GemfireOperations
|
||||
* @see org.springframework.expression.Expression
|
||||
* @see org.springframework.session.FindByIndexNameSessionRepository
|
||||
* @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.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession
|
||||
* @see org.springframework.session.data.gemfire.support.SessionIdHolder
|
||||
* @see org.springframework.session.events.AbstractSessionEvent
|
||||
* @see org.springframework.session.events.SessionCreatedEvent
|
||||
* @see org.springframework.session.events.SessionDeletedEvent
|
||||
* @see org.springframework.session.events.SessionDestroyedEvent
|
||||
* @see org.springframework.session.events.SessionExpiredEvent
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public abstract class AbstractGemFireOperationsSessionRepository extends CacheListenerAdapter<Object, Session>
|
||||
implements ApplicationEventPublisherAware, FindByIndexNameSessionRepository<Session>, InitializingBean {
|
||||
public abstract class AbstractGemFireOperationsSessionRepository
|
||||
implements ApplicationEventPublisherAware, FindByIndexNameSessionRepository<Session> {
|
||||
|
||||
private static final boolean DEFAULT_REGISTER_INTEREST_ENABLED = false;
|
||||
private static final boolean DEFAULT_REGISTER_INTEREST_DURABILITY = false;
|
||||
private static final boolean DEFAULT_REGISTER_INTEREST_RECEIVE_VALUES = false;
|
||||
|
||||
private static final AtomicBoolean usingDataSerialization = new AtomicBoolean(false);
|
||||
|
||||
private static final InterestResultPolicy DEFAULT_REGISTER_INTEREST_RESULT_POLICY = InterestResultPolicy.NONE;
|
||||
|
||||
private boolean registerInterestEnabled = DEFAULT_REGISTER_INTEREST_ENABLED;
|
||||
|
||||
private ApplicationEventPublisher applicationEventPublisher = event -> {};
|
||||
|
||||
private Duration maxInactiveInterval =
|
||||
@@ -116,32 +126,111 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi
|
||||
|
||||
private final Log logger = newLogger();
|
||||
|
||||
private final Set<Integer> cachedSessionIds = new ConcurrentSkipListSet<>();
|
||||
private final Region<Object, Session> sessions;
|
||||
|
||||
private String fullyQualifiedRegionName;
|
||||
private SessionEventHandlerCacheListenerAdapter sessionEventHandler;
|
||||
|
||||
private final Set<Integer> interestingSessionIds = new ConcurrentSkipListSet<>();
|
||||
|
||||
/**
|
||||
* Protected, default constructor used by extensions of {@link AbstractGemFireOperationsSessionRepository}
|
||||
* in order to affect and assess {@link SessionRepository} configuration and state.
|
||||
*/
|
||||
protected AbstractGemFireOperationsSessionRepository() {
|
||||
|
||||
this.sessions = null;
|
||||
this.template = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an instance of {@link AbstractGemFireOperationsSessionRepository}
|
||||
* with a required {@link GemfireOperations} instance used to perform Pivotal GemFire data access operations
|
||||
* and interactions supporting the SessionRepository operations.
|
||||
* Constructs a new instance of {@link AbstractGemFireOperationsSessionRepository} initialized with a required
|
||||
* {@link GemfireOperations} object, which is used to perform Apache Geode or Pivotal GemFire data access operations
|
||||
* on the cache {@link Region} storing and managing {@link Session} state to support this {@link SessionRepository}
|
||||
* and its operations.
|
||||
*
|
||||
* @param template {@link GemfireOperations} instance used to interact with GemFire; must not be {@literal null}.
|
||||
* @param template {@link GemfireOperations} object used to interact with the Apache Geode or Pivotal GemFire
|
||||
* cache {@link Region} storing and managing {@link Session} state; must not be {@literal null}.
|
||||
* @throws IllegalArgumentException if {@link GemfireOperations} is {@literal null}.
|
||||
* @see org.springframework.data.gemfire.GemfireOperations
|
||||
* @see #resolveSessionsRegion(GemfireOperations)
|
||||
* @see #initializeSessionsRegion(Region)
|
||||
* @see #newLogger()
|
||||
*/
|
||||
public AbstractGemFireOperationsSessionRepository(GemfireOperations template) {
|
||||
|
||||
Assert.notNull(template, "GemfireOperations is required");
|
||||
|
||||
this.template = template;
|
||||
this.sessions = initializeSessionsRegion(resolveSessionsRegion(template));
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the cache {@link Region} used to store and manage {@link Session} state
|
||||
* from the given {@link GemfireOperations} object.
|
||||
*
|
||||
* @param gemfireOperations {@link GemfireOperations} object used to resolve the {@link Session} {@link Region}.
|
||||
* @return the resolve cache {@link Region} used to store and manage {@link Session} state.
|
||||
* @throws IllegalStateException if the {@link Session Sessions} {@link Region} could not be resolved.
|
||||
* @see org.springframework.data.gemfire.GemfireOperations
|
||||
* @see org.springframework.session.Session
|
||||
* @see org.apache.geode.cache.Region
|
||||
*/
|
||||
private Region<Object, Session> resolveSessionsRegion(@Nullable GemfireOperations gemfireOperations) {
|
||||
|
||||
return Optional.ofNullable(gemfireOperations)
|
||||
.filter(GemfireAccessor.class::isInstance)
|
||||
.map(GemfireAccessor.class::cast)
|
||||
.<Region<Object, Session>>map(GemfireAccessor::getRegion)
|
||||
.orElseThrow(() -> newIllegalStateException("The ClusteredSpringSessions Region could not be resolved"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the cache {@link Region} used to store and manage {@link Session} state and register this
|
||||
* {@link SessionRepository} as an Apache Geode / Pivotal GemFire {@link org.apache.geode.cache.CacheListener}.
|
||||
*
|
||||
* @param sessionsRegion {@link Region} to initialize.
|
||||
* @return the given {@link Region}.
|
||||
* @see org.apache.geode.cache.Region
|
||||
* @see #newSessionEventHandler()
|
||||
* @see #newSessionIdInterestRegistrar()
|
||||
*/
|
||||
private Region<Object, Session> initializeSessionsRegion(@Nullable Region<Object, Session> sessionsRegion) {
|
||||
|
||||
Optional.ofNullable(sessionsRegion)
|
||||
.map(Region::getAttributesMutator)
|
||||
.ifPresent(sessionsRegionAttributesMutator -> {
|
||||
|
||||
this.sessionEventHandler = newSessionEventHandler();
|
||||
|
||||
sessionsRegionAttributesMutator.addCacheListener(this.sessionEventHandler);
|
||||
|
||||
if (GemFireUtils.isNonLocalClientRegion(sessionsRegion)) {
|
||||
this.registerInterestEnabled = true;
|
||||
sessionsRegionAttributesMutator.addCacheListener(newSessionIdInterestRegistrar());
|
||||
}
|
||||
});
|
||||
|
||||
return sessionsRegion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance of {@link SessionEventHandlerCacheListenerAdapter}.
|
||||
*
|
||||
* @return a new instance of {@link SessionEventHandlerCacheListenerAdapter}.
|
||||
* @see SessionEventHandlerCacheListenerAdapter
|
||||
*/
|
||||
protected SessionEventHandlerCacheListenerAdapter newSessionEventHandler() {
|
||||
return new SessionEventHandlerCacheListenerAdapter(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance of {@link SessionIdInterestRegisteringCacheListener}.
|
||||
*
|
||||
* @return a new instance of {@link SessionIdInterestRegisteringCacheListener}.
|
||||
* @see SessionIdInterestRegisteringCacheListener
|
||||
*/
|
||||
protected SessionIdInterestRegisteringCacheListener newSessionIdInterestRegistrar() {
|
||||
return new SessionIdInterestRegisteringCacheListener(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,15 +245,15 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the ApplicationEventPublisher used to publish Session events corresponding to
|
||||
* Pivotal GemFire cache events.
|
||||
* Sets the configured {@link ApplicationEventPublisher} used to publish {@link Session}
|
||||
* {@link AbstractSessionEvent events} corresponding to Apache Geode/Pivotal GemFire cache events.
|
||||
*
|
||||
* @param applicationEventPublisher the Spring ApplicationEventPublisher used to
|
||||
* publish Session-based events; must not be {@literal null}.
|
||||
* @param applicationEventPublisher {@link ApplicationEventPublisher} used to publish {@link Session}-based events;
|
||||
* must not be {@literal null}.
|
||||
* @throws IllegalArgumentException if {@link ApplicationEventPublisher} is {@literal null}.
|
||||
* @see org.springframework.context.ApplicationEventPublisher
|
||||
*/
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
public void setApplicationEventPublisher(@NonNull ApplicationEventPublisher applicationEventPublisher) {
|
||||
|
||||
Assert.notNull(applicationEventPublisher, "ApplicationEventPublisher is required");
|
||||
|
||||
@@ -172,24 +261,37 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the {@link ApplicationEventPublisher} used to publish {@link Session} events
|
||||
* corresponding to GemFire/Geode cache events.
|
||||
* Returns a reference to the configured {@link ApplicationEventPublisher} used to publish {@link Session}
|
||||
* {@link AbstractSessionEvent events} corresponding to Apache Geode/Pivotal GemFire cache events.
|
||||
*
|
||||
* @return the Spring {@link ApplicationEventPublisher} used to publish {@link Session} events.
|
||||
* @return the configured {@link ApplicationEventPublisher} used to publish {@link Session}
|
||||
* {@link AbstractSessionEvent events}.
|
||||
* @see org.springframework.context.ApplicationEventPublisher
|
||||
*/
|
||||
protected ApplicationEventPublisher getApplicationEventPublisher() {
|
||||
protected @NonNull ApplicationEventPublisher getApplicationEventPublisher() {
|
||||
return this.applicationEventPublisher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the fully-qualified name of the cache {@link Region} used to store and manage {@link Session} state.
|
||||
* Returns the {@link String fully-qualified name} of the cache {@link Region} used to store
|
||||
* and manage {@link Session} state.
|
||||
*
|
||||
* @return a {@link String} containing the fully qualified name of the cache {@link Region}
|
||||
* used to store and manage {@link Session} data.
|
||||
* @see #getSessionsRegion()
|
||||
*/
|
||||
protected String getFullyQualifiedRegionName() {
|
||||
return this.fullyQualifiedRegionName;
|
||||
return getSessionsRegion().getFullPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether {@link Region} {@literal register interest} is enabled
|
||||
* in the current Apache Geode / Pivotal GemFire configuration.
|
||||
*
|
||||
* @return a boolean value indicating whether interest registration is enabled.
|
||||
*/
|
||||
protected boolean isRegisterInterestEnabled() {
|
||||
return this.registerInterestEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,17 +356,43 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi
|
||||
.orElse(0);
|
||||
}
|
||||
|
||||
protected Optional<SessionEventHandlerCacheListenerAdapter> getSessionEventHandler() {
|
||||
return Optional.ofNullable(this.sessionEventHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a reference to the {@link GemfireOperations template} used to perform data access operations
|
||||
* and other interactions on the cache {@link Region} backing this {@link SessionRepository}.
|
||||
* Returns a reference to the configured Apache Geode / Pivotal GemFire cache {@link Region} used to
|
||||
* store and manage (HTTP) {@link Session} data.
|
||||
*
|
||||
* @return a reference to the {@link GemfireOperations template} used to interact with GemFire/Geode.
|
||||
* @return a reference to the configured {@link Session Sessions} {@link Region}.
|
||||
* @see org.springframework.session.Session
|
||||
* @see org.apache.geode.cache.Region
|
||||
*/
|
||||
protected @NonNull Region<Object, Session> getSessionsRegion() {
|
||||
return this.sessions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the {@link GemfireOperations template} used to perform data access operations
|
||||
* and other interactions on the cache {@link Region} storing and managing {@link Session} state
|
||||
* and backing this {@link SessionRepository}.
|
||||
*
|
||||
* @return a reference to the {@link GemfireOperations template} used to interact the {@link Region}
|
||||
* storing and managing {@link Session} state.
|
||||
* @see org.springframework.data.gemfire.GemfireOperations
|
||||
*/
|
||||
public GemfireOperations getTemplate() {
|
||||
public @NonNull GemfireOperations getSessionsTemplate() {
|
||||
return this.template;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #getSessionsTemplate()}.
|
||||
*/
|
||||
@Deprecated
|
||||
public @NonNull GemfireOperations getTemplate() {
|
||||
return getSessionsTemplate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a condition indicating whether the DataSerialization framework has been configured.
|
||||
*
|
||||
@@ -283,175 +411,10 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi
|
||||
return usingDataSerialization.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method during Spring bean initialization that will capture the fully-qualified name
|
||||
* of the cache {@link Region} used to manage {@link Session} state and register this {@link SessionRepository}
|
||||
* as a GemFire/Geode {@link org.apache.geode.cache.CacheListener}.
|
||||
*
|
||||
* Additionally, this method registers GemFire/Geode {@link Instantiator Instantiators}
|
||||
* for the {@link GemFireSession} and {@link GemFireSessionAttributes} types to optimize GemFire/Geode's
|
||||
* instantiation logic on deserialization using the data serialization framework when accessing the stored
|
||||
* {@link Session} state.
|
||||
*
|
||||
* @throws Exception if an error occurs during the initialization process.
|
||||
*/
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
|
||||
GemfireOperations template = getTemplate();
|
||||
|
||||
Assert.isInstanceOf(GemfireAccessor.class, template);
|
||||
|
||||
Region<Object, Session> region = ((GemfireAccessor) template).getRegion();
|
||||
|
||||
this.fullyQualifiedRegionName = region.getFullPath();
|
||||
|
||||
AttributesMutator<Object, Session> attributesMutator = region.getAttributesMutator();
|
||||
|
||||
attributesMutator.addCacheListener(this);
|
||||
}
|
||||
|
||||
boolean isCreate(EntryEvent<?, ?> event) {
|
||||
return isCreate(event.getOperation()) && isNotUpdate(event) && isSession(event.getNewValue());
|
||||
}
|
||||
|
||||
private boolean isCreate(Operation operation) {
|
||||
return operation.isCreate() && !Operation.LOCAL_LOAD_CREATE.equals(operation);
|
||||
}
|
||||
|
||||
private boolean isNotUpdate(EntryEvent event) {
|
||||
return isNotProxyRegion() || !this.cachedSessionIds.contains(ObjectUtils.nullSafeHashCode(event.getKey()));
|
||||
}
|
||||
|
||||
private boolean isNotProxyRegion() {
|
||||
return !isProxyRegion();
|
||||
}
|
||||
|
||||
private boolean isProxyRegion() {
|
||||
return GemFireUtils.isProxy(((GemfireAccessor) getTemplate()).getRegion());
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to determine whether the application developer is storing (HTTP) Sessions with other, arbitrary
|
||||
* application domain objects in the same Pivotal GemFire cache {@link Region}; crazier things have happened!
|
||||
*
|
||||
* @param obj {@link Object} to evaluate.
|
||||
* @return a boolean value indicating whether the old/new {@link Object} from the {@link Region}
|
||||
* {@link EntryEvent} is indeed a {@link Session}.
|
||||
* @see org.springframework.session.Session
|
||||
*/
|
||||
private boolean isSession(Object obj) {
|
||||
return obj instanceof Session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgets the given {@link Object session ID}.
|
||||
*
|
||||
* @param sessionId {@link Object} containing the session ID to forget.
|
||||
* @return a boolean value indicating whether the given session ID was even being remembered.
|
||||
* @see #remember(Object)
|
||||
*/
|
||||
boolean forget(Object sessionId) {
|
||||
return this.cachedSessionIds.remove(ObjectUtils.nullSafeHashCode(sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers the given {@link Object session ID}.
|
||||
*
|
||||
* @param sessionId {@link Object} containing the session ID to remember.
|
||||
* @return a boolean value whether Spring Session is interested in and will remember
|
||||
* this given session ID.
|
||||
* @see #forget(Object)
|
||||
*/
|
||||
@SuppressWarnings("all")
|
||||
boolean remember(Object sessionId) {
|
||||
return isProxyRegion() && this.cachedSessionIds.add(ObjectUtils.nullSafeHashCode(sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts the given {@link Object} into a {@link Session} iff the {@link Object} is a {@link Session}.
|
||||
*
|
||||
* Otherwise, this method attempts to use the supplied {@link String session ID} to create a {@link Session}
|
||||
* containing only the ID.
|
||||
*
|
||||
* @param obj {@link Object} to evaluate as a {@link Session}.
|
||||
* @param sessionId {@link String} containing the session ID.
|
||||
* @return a {@link Session} from the given {@link Object}
|
||||
* or a {@link Session} containing only the supplied {@link String session ID}.
|
||||
* @throws IllegalStateException if the given {@link Object} is not a {@link Session}
|
||||
* and {@link String session ID} was not supplied.
|
||||
*/
|
||||
Session toSession(Object obj, String sessionId) {
|
||||
|
||||
return obj instanceof Session
|
||||
? (Session) obj
|
||||
: Optional.ofNullable(sessionId)
|
||||
.filter(StringUtils::hasText)
|
||||
.map(SessionIdHolder::create)
|
||||
.orElseThrow(() -> newIllegalStateException(
|
||||
"Minimally, the session ID [%s] must be known to trigger a Session event", sessionId));
|
||||
}
|
||||
/**
|
||||
* Callback method triggered when an entry is created in the Pivotal GemFire cache {@link Region}.
|
||||
*
|
||||
* @param event {@link EntryEvent} containing the details of the cache operation.
|
||||
* @see org.apache.geode.cache.EntryEvent
|
||||
* @see #handleCreated(String, Session)
|
||||
*/
|
||||
@Override
|
||||
public void afterCreate(EntryEvent<Object, Session> event) {
|
||||
|
||||
Optional.ofNullable(event)
|
||||
.filter(this::isCreate)
|
||||
.ifPresent(it -> {
|
||||
|
||||
String sessionId = it.getKey().toString();
|
||||
|
||||
handleCreated(sessionId, toSession(it.getNewValue(), sessionId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method triggered when an entry is destroyed in the Pivotal GemFire cache {@link Region}.
|
||||
*
|
||||
* @param event {@link EntryEvent} containing the details of the cache operation.
|
||||
* @see org.apache.geode.cache.EntryEvent
|
||||
* @see #handleDestroyed(String, Session)
|
||||
*/
|
||||
@Override
|
||||
public void afterDestroy(EntryEvent<Object, Session> event) {
|
||||
|
||||
Optional.ofNullable(event)
|
||||
.ifPresent(it -> {
|
||||
|
||||
String sessionId = event.getKey().toString();
|
||||
|
||||
handleDestroyed(sessionId, toSession(event.getOldValue(), sessionId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method triggered when an entry is invalidated in the Pivotal GemFire cache {@link Region}.
|
||||
*
|
||||
* @param event {@link EntryEvent} containing the details of the cache operation.
|
||||
* @see org.apache.geode.cache.EntryEvent
|
||||
* @see #handleExpired(String, Session)
|
||||
*/
|
||||
@Override
|
||||
public void afterInvalidate(EntryEvent<Object, Session> event) {
|
||||
|
||||
Optional.ofNullable(event)
|
||||
.ifPresent(it -> {
|
||||
|
||||
String sessionId = event.getKey().toString();
|
||||
|
||||
handleExpired(sessionId, toSession(event.getOldValue(), sessionId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the given {@link Session}.
|
||||
*
|
||||
* @param session {@link Session} to commit, if committable.
|
||||
* @param session {@link Session} to commit, iff the {@link Session} is {@literal committable}.
|
||||
* @return the given {@link Session}
|
||||
* @see GemFireSession#commit()
|
||||
*/
|
||||
@@ -468,14 +431,15 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given {@link Session} from GemFire.
|
||||
* Deletes the given {@link Session} from Apache Geode / Pivotal GemFire.
|
||||
*
|
||||
* @param session {@link Session} to delete.
|
||||
* @return {@literal null}.
|
||||
* @see org.springframework.session.Session#getId()
|
||||
* @see org.springframework.session.Session
|
||||
* @see #deleteById(String)
|
||||
*/
|
||||
protected Session delete(Session session) {
|
||||
protected Session delete(@NonNull Session session) {
|
||||
|
||||
deleteById(session.getId());
|
||||
|
||||
@@ -483,88 +447,27 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes Session created events to be published to the Spring application context.
|
||||
* Handles the deletion of the given {@link Session}.
|
||||
*
|
||||
* @param sessionId a String indicating the ID of the Session.
|
||||
* @param session a reference to the Session triggering the event.
|
||||
* @see org.springframework.session.events.SessionCreatedEvent
|
||||
* @param sessionId {@link String} containing the {@link Session#getId()} of the given {@link Session}.
|
||||
* @param session deleted {@link Session}.
|
||||
* @see SessionEventHandlerCacheListenerAdapter#handleDeleted(String, Session)
|
||||
* @see org.springframework.session.Session
|
||||
* @see #newSessionCreatedEvent(Session)
|
||||
* @see #publishEvent(ApplicationEvent)
|
||||
*/
|
||||
protected void handleCreated(String sessionId, Session session) {
|
||||
remember(sessionId);
|
||||
publishEvent(newSessionCreatedEvent(session));
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes Session deleted events to be published to the Spring application context.
|
||||
*
|
||||
* @param sessionId a String indicating the ID of the Session.
|
||||
* @param session a reference to the Session triggering the event.
|
||||
* @see org.springframework.session.events.SessionDeletedEvent
|
||||
* @see org.springframework.session.Session
|
||||
* @see #newSessionDeletedEvent(Session)
|
||||
* @see #publishEvent(ApplicationEvent)
|
||||
* @see #forget(Object)
|
||||
* @see #unregisterInterest(Object)
|
||||
*/
|
||||
protected void handleDeleted(String sessionId, Session session) {
|
||||
forget(sessionId);
|
||||
publishEvent(newSessionDeletedEvent(session));
|
||||
|
||||
getSessionEventHandler()
|
||||
.ifPresent(it -> it.handleDeleted(sessionId, session));
|
||||
|
||||
unregisterInterest(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes Session destroyed events to be published to the Spring application context.
|
||||
* Publishes the specified {@link ApplicationEvent} to the Spring container thereby notifying other (potentially)
|
||||
* interested application components/beans.
|
||||
*
|
||||
* @param sessionId a String indicating the ID of the Session.
|
||||
* @param session a reference to the Session triggering the event.
|
||||
* @see org.springframework.session.events.SessionDestroyedEvent
|
||||
* @see org.springframework.session.Session
|
||||
* @see #newSessionDestroyedEvent(Session)
|
||||
* @see #publishEvent(ApplicationEvent)
|
||||
* @see #forget(Object)
|
||||
*/
|
||||
protected void handleDestroyed(String sessionId, Session session) {
|
||||
forget(sessionId);
|
||||
publishEvent(newSessionDestroyedEvent(session));
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes Session expired events to be published to the Spring application context.
|
||||
*
|
||||
* @param sessionId a String indicating the ID of the Session.
|
||||
* @param session a reference to the Session triggering the event.
|
||||
* @see org.springframework.session.events.SessionExpiredEvent
|
||||
* @see org.springframework.session.Session
|
||||
* @see #newSessionExpiredEvent(Session)
|
||||
* @see #publishEvent(ApplicationEvent)
|
||||
* @see #forget(Object)
|
||||
*/
|
||||
protected void handleExpired(String sessionId, Session session) {
|
||||
forget(sessionId);
|
||||
publishEvent(newSessionExpiredEvent(session));
|
||||
}
|
||||
|
||||
private SessionCreatedEvent newSessionCreatedEvent(Session session) {
|
||||
return new SessionCreatedEvent(this, session);
|
||||
}
|
||||
|
||||
private SessionDeletedEvent newSessionDeletedEvent(Session session) {
|
||||
return new SessionDeletedEvent(this, session);
|
||||
}
|
||||
|
||||
private SessionDestroyedEvent newSessionDestroyedEvent(Session session) {
|
||||
return new SessionDestroyedEvent(this, session);
|
||||
}
|
||||
|
||||
private SessionExpiredEvent newSessionExpiredEvent(Session session) {
|
||||
return new SessionExpiredEvent(this, session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes the specified ApplicationEvent to the Spring application context.
|
||||
*
|
||||
* @param event the ApplicationEvent to publish.
|
||||
* @param event {@link ApplicationEvent} to publish.
|
||||
* @see org.springframework.context.ApplicationEventPublisher#publishEvent(ApplicationEvent)
|
||||
* @see org.springframework.context.ApplicationEvent
|
||||
*/
|
||||
@@ -579,20 +482,99 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the {@link Session#setLastAccessedTime(Instant)} property of the {@link Session}.
|
||||
* Registers interest in the given {@link Session} in order to receive notifications and updates.
|
||||
*
|
||||
* @param session {@link Session} of interest to this application that will be registered.
|
||||
* @return the given {@link Session}.
|
||||
* @see org.springframework.session.Session#getId()
|
||||
* @see org.springframework.session.Session
|
||||
* @see #registerInterest(Object)
|
||||
*/
|
||||
protected Session registerInterest(@Nullable Session session) {
|
||||
|
||||
Optional.ofNullable(session)
|
||||
.map(Session::getId)
|
||||
.ifPresent(this::registerInterest);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers interest on the {@link Session#getId()} ID} of a {@link Session}.
|
||||
*
|
||||
* And, only registers interest in the given Session ID iff we have not already registered interest
|
||||
* in this Session ID before.
|
||||
*
|
||||
* @param sessionId {@link Session#getId() ID} of the {@link Session} of interest to this application.
|
||||
* @see org.apache.geode.cache.Region#registerInterest(Object, InterestResultPolicy, boolean, boolean)
|
||||
* @see #isRegisterInterestEnabled()
|
||||
*/
|
||||
protected void registerInterest(Object sessionId) {
|
||||
|
||||
Optional.ofNullable(sessionId)
|
||||
.filter(it -> this.isRegisterInterestEnabled())
|
||||
.filter(SessionUtils::isValidSessionId)
|
||||
.map(ObjectUtils::nullSafeHashCode)
|
||||
.filter(this.interestingSessionIds::add)
|
||||
.ifPresent(it ->
|
||||
getSessionsRegion().registerInterest(sessionId, DEFAULT_REGISTER_INTEREST_RESULT_POLICY,
|
||||
DEFAULT_REGISTER_INTEREST_DURABILITY, DEFAULT_REGISTER_INTEREST_RECEIVE_VALUES)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the {@link Session#setLastAccessedTime(Instant)} property of the {@link Session}
|
||||
* to the {@link Instant#now() current time}.
|
||||
*
|
||||
* @param session {@link Session} to touch.
|
||||
* @return the {@link Session}.
|
||||
* @see org.springframework.session.Session#setLastAccessedTime(Instant)
|
||||
* @see org.springframework.session.Session
|
||||
* @see java.time.Instant#now()
|
||||
*/
|
||||
protected Session touch(Session session) {
|
||||
protected @NonNull Session touch(@NonNull Session session) {
|
||||
|
||||
session.setLastAccessedTime(Instant.now());
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters interest in the given {@link Session} in order to stop notifications and updates.
|
||||
*
|
||||
* @param session {@link Session} no longer of any interest to this application that will be unregistered.
|
||||
* @return the given {@link Session}.
|
||||
* @see org.springframework.session.Session#getId()
|
||||
* @see org.springframework.session.Session
|
||||
* @see #unregisterInterest(Object)
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
protected Session unregisterInterest(@Nullable Session session) {
|
||||
|
||||
Optional.ofNullable(session)
|
||||
.map(Session::getId)
|
||||
.ifPresent(this::unregisterInterest);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters interest on the {@link Session#getId()} ID} of a {@link Session}.
|
||||
*
|
||||
* @param sessionId {@link Session#getId() ID} of the {@link Session} no longer of any interest
|
||||
* to this application.
|
||||
* @see org.apache.geode.cache.Region#unregisterInterest(Object)
|
||||
* @see #isRegisterInterestEnabled()
|
||||
*/
|
||||
protected void unregisterInterest(@Nullable Object sessionId) {
|
||||
|
||||
Optional.ofNullable(sessionId)
|
||||
.filter(it -> this.isRegisterInterestEnabled())
|
||||
.map(ObjectUtils::nullSafeHashCode)
|
||||
.filter(this.interestingSessionIds::remove)
|
||||
.ifPresent(it -> getSessionsRegion().unregisterInterest(sessionId));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static class DeltaCapableGemFireSession
|
||||
extends GemFireSession<DeltaCapableGemFireSessionAttributes> implements Delta {
|
||||
@@ -1273,4 +1255,275 @@ public abstract class AbstractGemFireOperationsSessionRepository extends CacheLi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static class SessionEventHandlerCacheListenerAdapter extends CacheListenerAdapter<Object, Session> {
|
||||
|
||||
private final AbstractGemFireOperationsSessionRepository sessionRepository;
|
||||
|
||||
private final Set<Integer> cachedSessionIds = new ConcurrentSkipListSet<>();
|
||||
|
||||
protected SessionEventHandlerCacheListenerAdapter(AbstractGemFireOperationsSessionRepository sessionRepository) {
|
||||
|
||||
Assert.notNull(sessionRepository, "SessionRepository is required");
|
||||
|
||||
this.sessionRepository = sessionRepository;
|
||||
}
|
||||
|
||||
protected AbstractGemFireOperationsSessionRepository getSessionRepository() {
|
||||
return this.sessionRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method triggered when an entry is created in the Pivotal GemFire cache {@link Region}.
|
||||
*
|
||||
* @param event {@link EntryEvent} containing the details of the cache operation.
|
||||
* @see org.apache.geode.cache.EntryEvent
|
||||
* @see #handleCreated(String, Session)
|
||||
*/
|
||||
@Override
|
||||
public void afterCreate(EntryEvent<Object, Session> event) {
|
||||
|
||||
Optional.ofNullable(event)
|
||||
.filter(this::isCreate)
|
||||
.ifPresent(it -> {
|
||||
|
||||
String sessionId = it.getKey().toString();
|
||||
|
||||
handleCreated(sessionId, toSession(it.getNewValue(), sessionId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes Session created events to be published to the Spring application context.
|
||||
*
|
||||
* @param sessionId a String indicating the ID of the Session.
|
||||
* @param session a reference to the Session triggering the event.
|
||||
* @see org.springframework.session.events.SessionCreatedEvent
|
||||
* @see org.springframework.session.Session
|
||||
* @see #newSessionCreatedEvent(Session)
|
||||
* @see #publishEvent(ApplicationEvent)
|
||||
*/
|
||||
protected void handleCreated(String sessionId, Session session) {
|
||||
|
||||
remember(sessionId);
|
||||
getSessionRepository().publishEvent(newSessionCreatedEvent(session));
|
||||
}
|
||||
|
||||
private SessionCreatedEvent newSessionCreatedEvent(Session session) {
|
||||
return new SessionCreatedEvent(getSessionRepository(), session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method triggered when an entry is destroyed in the Pivotal GemFire cache {@link Region}.
|
||||
*
|
||||
* @param event {@link EntryEvent} containing the details of the cache operation.
|
||||
* @see org.apache.geode.cache.EntryEvent
|
||||
* @see #handleDestroyed(String, Session)
|
||||
*/
|
||||
@Override
|
||||
public void afterDestroy(EntryEvent<Object, Session> event) {
|
||||
|
||||
Optional.ofNullable(event)
|
||||
.ifPresent(it -> {
|
||||
|
||||
String sessionId = event.getKey().toString();
|
||||
|
||||
handleDestroyed(sessionId, toSession(event.getOldValue(), sessionId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes Session destroyed events to be published to the Spring application context.
|
||||
*
|
||||
* @param sessionId a String indicating the ID of the Session.
|
||||
* @param session a reference to the Session triggering the event.
|
||||
* @see org.springframework.session.events.SessionDestroyedEvent
|
||||
* @see org.springframework.session.Session
|
||||
* @see #newSessionDestroyedEvent(Session)
|
||||
* @see #publishEvent(ApplicationEvent)
|
||||
* @see #forget(Object)
|
||||
*/
|
||||
protected void handleDestroyed(String sessionId, Session session) {
|
||||
|
||||
forget(sessionId);
|
||||
getSessionRepository().publishEvent(newSessionDestroyedEvent(session));
|
||||
}
|
||||
|
||||
private SessionDestroyedEvent newSessionDestroyedEvent(Session session) {
|
||||
return new SessionDestroyedEvent(getSessionRepository(), session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback method triggered when an entry is invalidated in the Pivotal GemFire cache {@link Region}.
|
||||
*
|
||||
* @param event {@link EntryEvent} containing the details of the cache operation.
|
||||
* @see org.apache.geode.cache.EntryEvent
|
||||
* @see #handleExpired(String, Session)
|
||||
*/
|
||||
@Override
|
||||
public void afterInvalidate(EntryEvent<Object, Session> event) {
|
||||
|
||||
Optional.ofNullable(event)
|
||||
.ifPresent(it -> {
|
||||
|
||||
String sessionId = event.getKey().toString();
|
||||
|
||||
handleExpired(sessionId, toSession(event.getOldValue(), sessionId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes Session expired events to be published to the Spring application context.
|
||||
*
|
||||
* @param sessionId a String indicating the ID of the Session.
|
||||
* @param session a reference to the Session triggering the event.
|
||||
* @see org.springframework.session.events.SessionExpiredEvent
|
||||
* @see org.springframework.session.Session
|
||||
* @see #newSessionExpiredEvent(Session)
|
||||
* @see #publishEvent(ApplicationEvent)
|
||||
* @see #forget(Object)
|
||||
*/
|
||||
protected void handleExpired(String sessionId, Session session) {
|
||||
|
||||
forget(sessionId);
|
||||
getSessionRepository().publishEvent(newSessionExpiredEvent(session));
|
||||
}
|
||||
|
||||
private SessionExpiredEvent newSessionExpiredEvent(Session session) {
|
||||
return new SessionExpiredEvent(getSessionRepository(), session);
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes Session deleted events to be published to the Spring application context.
|
||||
*
|
||||
* @param sessionId a String indicating the ID of the Session.
|
||||
* @param session a reference to the Session triggering the event.
|
||||
* @see org.springframework.session.events.SessionDeletedEvent
|
||||
* @see org.springframework.session.Session
|
||||
* @see #newSessionDeletedEvent(Session)
|
||||
* @see #publishEvent(ApplicationEvent)
|
||||
* @see #forget(Object)
|
||||
*/
|
||||
protected void handleDeleted(String sessionId, Session session) {
|
||||
|
||||
forget(sessionId);
|
||||
getSessionRepository().publishEvent(newSessionDeletedEvent(toSession(session, sessionId)));
|
||||
}
|
||||
|
||||
private SessionDeletedEvent newSessionDeletedEvent(Session session) {
|
||||
return new SessionDeletedEvent(getSessionRepository(), session);
|
||||
}
|
||||
|
||||
boolean isCreate(EntryEvent<?, ?> event) {
|
||||
return isCreate(event.getOperation()) && isNotUpdate(event) && isSession(event.getNewValue());
|
||||
}
|
||||
|
||||
private boolean isCreate(Operation operation) {
|
||||
return operation.isCreate() && !Operation.LOCAL_LOAD_CREATE.equals(operation);
|
||||
}
|
||||
|
||||
private boolean isNotUpdate(EntryEvent event) {
|
||||
return isNotProxyRegion() || !this.cachedSessionIds.contains(ObjectUtils.nullSafeHashCode(event.getKey()));
|
||||
}
|
||||
|
||||
private boolean isNotProxyRegion() {
|
||||
return !isProxyRegion();
|
||||
}
|
||||
|
||||
private boolean isProxyRegion() {
|
||||
return GemFireUtils.isProxy(getSessionRepository().getSessionsRegion());
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to determine whether the application developer is storing (HTTP) {@link Session Sessions} with other,
|
||||
* arbitrary application domain objects in the same Apache Geode / Pivotal GemFire cache {@link Region};
|
||||
* crazier things have happened!
|
||||
*
|
||||
* @param obj {@link Object} to evaluate.
|
||||
* @return a boolean value indicating whether the old/new {@link Object} from the {@link Region}
|
||||
* {@link EntryEvent} is indeed a {@link Session}.
|
||||
* @see org.springframework.session.Session
|
||||
*/
|
||||
private boolean isSession(Object obj) {
|
||||
return obj instanceof Session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgets the given {@link Object Session ID}.
|
||||
*
|
||||
* @param sessionId {@link Object} containing the Session ID to forget.
|
||||
* @return a boolean value indicating whether the given Session ID was even being remembered.
|
||||
* @see #remember(Object)
|
||||
*/
|
||||
boolean forget(Object sessionId) {
|
||||
return this.cachedSessionIds.remove(ObjectUtils.nullSafeHashCode(sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers the given {@link Object Session ID}.
|
||||
*
|
||||
* @param sessionId {@link Object} containing the Session ID to remember.
|
||||
* @return a boolean value indicating whether Spring Session is interested in
|
||||
* and will remember the given Session ID.
|
||||
* @see #forget(Object)
|
||||
*/
|
||||
boolean remember(Object sessionId) {
|
||||
return isProxyRegion() && this.cachedSessionIds.add(ObjectUtils.nullSafeHashCode(sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Casts the given {@link Object} into a {@link Session} iff the {@link Object} is a {@link Session}.
|
||||
*
|
||||
* Otherwise, this method attempts to use the supplied {@link String Session ID} to create a {@link Session}
|
||||
* representation containing only the ID.
|
||||
*
|
||||
* @param obj {@link Object} to evaluate as a {@link Session}.
|
||||
* @param sessionId {@link String} containing the Session ID.
|
||||
* @return a {@link Session} from the given {@link Object} or a {@link Session} representation
|
||||
* containing only the supplied {@link String Session ID}.
|
||||
* @throws IllegalStateException if the given {@link Object} is not a {@link Session}
|
||||
* and a {@link String Session ID} was not supplied.
|
||||
*/
|
||||
Session toSession(@Nullable Object obj, String sessionId) {
|
||||
|
||||
return obj instanceof Session
|
||||
? (Session) obj
|
||||
: Optional.ofNullable(sessionId)
|
||||
.filter(StringUtils::hasText)
|
||||
.map(SessionIdHolder::create)
|
||||
.orElseThrow(() -> newIllegalStateException(
|
||||
"Minimally, the Session ID [%s] must be known to trigger a Session event", sessionId));
|
||||
}
|
||||
}
|
||||
|
||||
protected static class SessionIdInterestRegisteringCacheListener extends CacheListenerAdapter<Object, Session> {
|
||||
|
||||
private final AbstractGemFireOperationsSessionRepository sessionRepository;
|
||||
|
||||
public SessionIdInterestRegisteringCacheListener(AbstractGemFireOperationsSessionRepository sessionRepository) {
|
||||
|
||||
Assert.notNull(sessionRepository, "SessionRepository is required");
|
||||
|
||||
this.sessionRepository = sessionRepository;
|
||||
}
|
||||
|
||||
protected AbstractGemFireOperationsSessionRepository getSessionRepository() {
|
||||
return this.sessionRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCreate(EntryEvent<Object, Session> event) {
|
||||
getSessionRepository().registerInterest(event.getKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterDestroy(EntryEvent<Object, Session> event) {
|
||||
getSessionRepository().unregisterInterest(event.getKey());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterInvalidate(EntryEvent<Object, Session> event) {
|
||||
getSessionRepository().unregisterInterest(event.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,52 +76,58 @@ public class GemFireOperationsSessionRepository extends AbstractGemFireOperation
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a copy of an existing, non-expired {@link Session} by ID.
|
||||
* Finds an existing, non-expired {@link Session} by ID.
|
||||
*
|
||||
* If the {@link Session} is expired, then the {@link Session }is deleted.
|
||||
* If the {@link Session} is expired, then the {@link Session} is deleted and {@literal null} is returned.
|
||||
*
|
||||
* @param sessionId a String indicating the ID of the Session to get.
|
||||
* @return an existing {@link Session} by ID or null if no {@link Session} exists.
|
||||
* @param sessionId {@link String} containing the {@link Session#getId() ID}} of the {@link Session} to get.
|
||||
* @return an existing {@link Session} by ID or {@literal null} if no {@link Session} exists
|
||||
* or the {@link Session} expired.
|
||||
* @see AbstractGemFireOperationsSessionRepository.GemFireSession#from(Session)
|
||||
* @see org.springframework.session.Session
|
||||
* @see #commit(Session)
|
||||
* @see #deleteById(String)
|
||||
* @see #registerInterest(Session)
|
||||
* @see #touch(Session)
|
||||
*/
|
||||
@Nullable
|
||||
public Session findById(String sessionId) {
|
||||
|
||||
Session storedSession = getTemplate().get(sessionId);
|
||||
Session storedSession = getSessionsTemplate().get(sessionId);
|
||||
|
||||
if (storedSession != null) {
|
||||
storedSession = storedSession.isExpired()
|
||||
? delete(storedSession)
|
||||
: touch(commit(GemFireSession.from(storedSession)));
|
||||
: registerInterest(touch(commit(GemFireSession.from(storedSession))));
|
||||
}
|
||||
|
||||
return storedSession;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up all available Sessions with the particular attribute indexed by name
|
||||
* having the given value.
|
||||
* Finds all available {@link Session Sessions} with the particular attribute indexed by {@link String name}
|
||||
* having the given {@link Object value}.
|
||||
*
|
||||
* @param indexName name of the indexed Session attribute. (e.g.
|
||||
* {@link org.springframework.session.FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME}
|
||||
* ).
|
||||
* @param indexValue value of the indexed Session attribute to search on (e.g.
|
||||
* username).
|
||||
* @return a mapping of Session ID to Session instances.
|
||||
* @param indexName {@link String name} of the indexed {@link Session} attribute.
|
||||
* (e.g. {@link org.springframework.session.FindByIndexNameSessionRepository#PRINCIPAL_NAME_INDEX_NAME}).
|
||||
* @param indexValue {@link Object value} of the indexed {@link Session} attribute to search on
|
||||
* (e.g. {@literal username}).
|
||||
* @return a mapping of {@link Session#getId()} Session IDs} to {@link Session} objects.
|
||||
* @see org.springframework.session.Session
|
||||
* @see java.util.Map
|
||||
* @see #prepareQuery(String)
|
||||
* @see java.util.Map
|
||||
* @see #commit(Session)
|
||||
* @see #registerInterest(Session)
|
||||
* @see #touch(Session)
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Session> findByIndexNameAndIndexValue(String indexName, String indexValue) {
|
||||
|
||||
SelectResults<Session> results = getTemplate().find(prepareQuery(indexName), indexValue);
|
||||
SelectResults<Session> results = getSessionsTemplate().find(prepareQuery(indexName), indexValue);
|
||||
|
||||
Map<String, Session> sessions = new HashMap<>(results.size());
|
||||
|
||||
results.asList().forEach(session -> sessions.put(session.getId(), session));
|
||||
results.asList().forEach(session -> sessions.put(session.getId(), registerInterest(touch(commit(session)))));
|
||||
|
||||
return sessions;
|
||||
}
|
||||
@@ -136,9 +142,11 @@ public class GemFireOperationsSessionRepository extends AbstractGemFireOperation
|
||||
*/
|
||||
protected String prepareQuery(String indexName) {
|
||||
|
||||
String fullyQualifiedRegionName = getFullyQualifiedRegionName();
|
||||
|
||||
return PRINCIPAL_NAME_INDEX_NAME.equals(indexName)
|
||||
? String.format(FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, getFullyQualifiedRegionName())
|
||||
: String.format(FIND_SESSIONS_BY_INDEX_NAME_AND_INDEX_VALUE_QUERY, getFullyQualifiedRegionName(), indexName);
|
||||
? String.format(FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, fullyQualifiedRegionName)
|
||||
: String.format(FIND_SESSIONS_BY_INDEX_NAME_AND_INDEX_VALUE_QUERY, fullyQualifiedRegionName, indexName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,7 +178,7 @@ public class GemFireOperationsSessionRepository extends AbstractGemFireOperation
|
||||
void doSave(@NonNull Session session) {
|
||||
|
||||
// Save Session As GemFireSession
|
||||
getTemplate().put(session.getId(), GemFireSession.from(session));
|
||||
getSessionsTemplate().put(session.getId(), GemFireSession.from(session));
|
||||
|
||||
// Commit Session
|
||||
commit(session);
|
||||
@@ -185,6 +193,6 @@ public class GemFireOperationsSessionRepository extends AbstractGemFireOperation
|
||||
* @see #handleDeleted(String, Session)
|
||||
*/
|
||||
public void deleteById(String sessionId) {
|
||||
handleDeleted(sessionId, toSession(getTemplate().<Object, Session>remove(sessionId), sessionId));
|
||||
handleDeleted(sessionId, getSessionsTemplate().<Object, Session>remove(sessionId));
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,17 +22,16 @@ import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isA;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
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.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.withSettings;
|
||||
import static org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME;
|
||||
import static org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.GemFireSession;
|
||||
|
||||
@@ -41,11 +40,9 @@ import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -61,8 +58,9 @@ import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.gemfire.GemfireAccessor;
|
||||
import org.springframework.data.gemfire.GemfireOperations;
|
||||
import org.springframework.data.gemfire.util.RegionUtils;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.data.gemfire.support.GemFireUtils;
|
||||
import org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.SessionEventHandlerCacheListenerAdapter;
|
||||
import org.springframework.session.events.AbstractSessionEvent;
|
||||
import org.springframework.session.events.SessionDeletedEvent;
|
||||
|
||||
@@ -97,102 +95,172 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
@Mock
|
||||
private ApplicationEventPublisher mockApplicationEventPublisher;
|
||||
|
||||
@Mock
|
||||
private AttributesMutator<Object, Session> mockAttributesMutator;
|
||||
|
||||
@Mock
|
||||
private GemfireOperationsAccessor mockTemplate;
|
||||
|
||||
// Subject Under Test (SUT)
|
||||
private GemFireOperationsSessionRepository sessionRepository;
|
||||
|
||||
@Mock
|
||||
private Region<Object, Session> mockRegion;
|
||||
|
||||
@Before
|
||||
@SuppressWarnings("unchecked")
|
||||
public void setup() throws Exception {
|
||||
|
||||
when(this.mockRegion.getAttributesMutator()).thenReturn(this.mockAttributesMutator);
|
||||
when(this.mockRegion.getFullPath()).thenReturn(GemFireUtils.toRegionPath("Example"));
|
||||
when(this.mockTemplate.<Object, Session>getRegion()).thenReturn(this.mockRegion);
|
||||
AttributesMutator<Object, Session> mockAttributesMutator = mock(AttributesMutator.class);
|
||||
|
||||
this.sessionRepository = spy(new GemFireOperationsSessionRepository(this.mockTemplate));
|
||||
Region<Object, Session> mockRegion = mock(Region.class);
|
||||
|
||||
when(mockRegion.getAttributesMutator()).thenReturn(mockAttributesMutator);
|
||||
when(mockRegion.getFullPath()).thenReturn(RegionUtils.toRegionPath("Example"));
|
||||
|
||||
doReturn(mockRegion).when(this.mockTemplate).<Object, Session>getRegion();
|
||||
|
||||
this.sessionRepository = new GemFireOperationsSessionRepository(this.mockTemplate);
|
||||
this.sessionRepository.setApplicationEventPublisher(this.mockApplicationEventPublisher);
|
||||
this.sessionRepository.setMaxInactiveIntervalInSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
this.sessionRepository.setUseDataSerialization(false);
|
||||
this.sessionRepository.afterPropertiesSet();
|
||||
|
||||
assertThat(this.sessionRepository.getApplicationEventPublisher()).isSameAs(this.mockApplicationEventPublisher);
|
||||
assertThat(this.sessionRepository.getFullyQualifiedRegionName()).isEqualTo(GemFireUtils.toRegionPath("Example"));
|
||||
assertThat(this.sessionRepository.getFullyQualifiedRegionName()).isEqualTo(RegionUtils.toRegionPath("Example"));
|
||||
assertThat(this.sessionRepository.getMaxInactiveIntervalInSeconds()).isEqualTo(MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
assertThat(this.sessionRepository.getTemplate()).isSameAs(this.mockTemplate);
|
||||
assertThat(this.sessionRepository.getSessionEventHandler().orElse(null)).isInstanceOf(SessionEventHandlerCacheListenerAdapter.class);
|
||||
assertThat(this.sessionRepository.getSessionsRegion()).isSameAs(mockRegion);
|
||||
assertThat(this.sessionRepository.getSessionsTemplate()).isSameAs(this.mockTemplate);
|
||||
assertThat(GemFireOperationsSessionRepository.isUsingDataSerialization()).isFalse();
|
||||
}
|
||||
|
||||
private Session mockSession() {
|
||||
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
|
||||
Instant now = Instant.now();
|
||||
|
||||
Duration maxInactiveInterval = Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS);
|
||||
|
||||
Session mockSession = mock(Session.class, withSettings().name(sessionId).lenient());
|
||||
|
||||
when(mockSession.getId()).thenReturn(sessionId);
|
||||
when(mockSession.getAttributeNames()).thenReturn(Collections.emptySet());
|
||||
when(mockSession.getCreationTime()).thenReturn(now);
|
||||
when(mockSession.getLastAccessedTime()).thenReturn(now);
|
||||
when(mockSession.getMaxInactiveInterval()).thenReturn(maxInactiveInterval);
|
||||
|
||||
return mockSession;
|
||||
verify(mockAttributesMutator).addCacheListener(isA(SessionEventHandlerCacheListenerAdapter.class));
|
||||
verify(mockRegion, times(1)).getAttributesMutator();
|
||||
verify(this.mockTemplate, times(1)).getRegion();
|
||||
}
|
||||
|
||||
private GemFireSession newNonDirtyGemFireSession() {
|
||||
|
||||
GemFireSession session = GemFireSession.create();
|
||||
|
||||
session.commit();;
|
||||
session.commit();
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void constructGemFireOperationSessionRepositoryWithTemplate() {
|
||||
|
||||
verify(this.mockAttributesMutator, times(1)).addCacheListener(same(this.sessionRepository));
|
||||
verify(this.mockRegion, times(1)).getFullPath();
|
||||
verify(this.mockTemplate, times(1)).getRegion();
|
||||
AttributesMutator<Object, Session> mockAttributesMutator = mock(AttributesMutator.class);
|
||||
|
||||
Region<Object, Session> mockRegion = mock(Region.class);
|
||||
|
||||
GemfireOperationsAccessor mockTemplate = mock(GemfireOperationsAccessor.class);
|
||||
|
||||
when(mockRegion.getAttributesMutator()).thenReturn(mockAttributesMutator);
|
||||
|
||||
doReturn(mockRegion).when(mockTemplate).getRegion();
|
||||
|
||||
GemFireOperationsSessionRepository sessionRepository =
|
||||
new GemFireOperationsSessionRepository(mockTemplate);
|
||||
|
||||
assertThat(sessionRepository).isNotNull();
|
||||
assertThat(sessionRepository.getSessionsRegion()).isSameAs(mockRegion);
|
||||
assertThat(sessionRepository.getSessionsTemplate()).isSameAs(mockTemplate);
|
||||
|
||||
verify(mockTemplate, times(1)).getRegion();
|
||||
verify(mockRegion, times(1)).getAttributesMutator();
|
||||
verify(mockAttributesMutator, times(1))
|
||||
.addCacheListener(isA(SessionEventHandlerCacheListenerAdapter.class));
|
||||
verifyNoMoreInteractions(mockAttributesMutator);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createProperlyInitializedSession() {
|
||||
|
||||
Instant beforeOrAtCreationTime = Instant.now();
|
||||
Instant beforeCreationTime = Instant.now();
|
||||
|
||||
Session session = this.sessionRepository.createSession();
|
||||
|
||||
assertThat(session).isInstanceOf(AbstractGemFireOperationsSessionRepository.GemFireSession.class);
|
||||
assertThat(session.getId()).isNotNull();
|
||||
assertThat(session.getId()).isNotEmpty();
|
||||
assertThat(session.getAttributeNames()).isEmpty();
|
||||
assertThat(session.getCreationTime().compareTo(beforeOrAtCreationTime)).isGreaterThanOrEqualTo(0);
|
||||
assertThat(session.getLastAccessedTime().compareTo(beforeOrAtCreationTime)).isGreaterThanOrEqualTo(0);
|
||||
assertThat(session.getCreationTime()).isAfterOrEqualTo(beforeCreationTime);
|
||||
assertThat(session.getCreationTime()).isBeforeOrEqualTo(Instant.now());
|
||||
assertThat(session.isExpired()).isFalse();
|
||||
assertThat(session.getLastAccessedTime()).isEqualTo(session.getCreationTime());
|
||||
assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createProperlyInitializedDeltaAwareSession() {
|
||||
|
||||
Instant beforeCreationTime = Instant.now();
|
||||
|
||||
this.sessionRepository.setUseDataSerialization(true);
|
||||
|
||||
Session session = this.sessionRepository.createSession();
|
||||
|
||||
assertThat(session).isInstanceOf(AbstractGemFireOperationsSessionRepository.DeltaCapableGemFireSession.class);
|
||||
assertThat(session.getId()).isNotEmpty();
|
||||
assertThat(session.getAttributeNames()).isEmpty();
|
||||
assertThat(session.getCreationTime()).isAfterOrEqualTo(beforeCreationTime);
|
||||
assertThat(session.getCreationTime()).isBeforeOrEqualTo(Instant.now());
|
||||
assertThat(session.isExpired()).isFalse();
|
||||
assertThat(session.getLastAccessedTime()).isEqualTo(session.getCreationTime());
|
||||
assertThat(session.getMaxInactiveInterval()).isEqualTo(Duration.ofSeconds(MAX_INACTIVE_INTERVAL_IN_SECONDS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByIdReturnsMatchingNonExpiredSession() {
|
||||
|
||||
Instant expectedCreationTime = Instant.now();
|
||||
Instant currentLastAccessedTime = expectedCreationTime.plusMillis(TimeUnit.MINUTES.toMillis(5L));
|
||||
|
||||
Session mockSession = mock(Session.class);
|
||||
|
||||
when(mockSession.isExpired()).thenReturn(false);
|
||||
when(mockSession.getId()).thenReturn("1");
|
||||
when(mockSession.getCreationTime()).thenReturn(expectedCreationTime);
|
||||
when(mockSession.getLastAccessedTime()).thenReturn(currentLastAccessedTime);
|
||||
when(mockSession.getAttributeNames()).thenReturn(Collections.singleton("attributeOne"));
|
||||
when(mockSession.getAttribute(eq("attributeOne"))).thenReturn("test");
|
||||
when(this.mockTemplate.get(eq("1"))).thenReturn(mockSession);
|
||||
|
||||
GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository);
|
||||
|
||||
Session actualSession = sessionRepositorySpy.findById("1");
|
||||
|
||||
assertThat(actualSession).isNotNull();
|
||||
assertThat(actualSession).isNotSameAs(mockSession);
|
||||
assertThat(actualSession.getId()).isEqualTo("1");
|
||||
assertThat(actualSession.getCreationTime()).isEqualTo(expectedCreationTime);
|
||||
assertThat(actualSession.getLastAccessedTime()).isNotEqualTo(currentLastAccessedTime);
|
||||
assertThat(actualSession.getLastAccessedTime()).isAfterOrEqualTo(expectedCreationTime);
|
||||
assertThat(actualSession.getLastAccessedTime()).isBeforeOrEqualTo(Instant.now());
|
||||
assertThat(actualSession.getAttributeNames()).containsExactly("attributeOne");
|
||||
assertThat(actualSession.<String>getAttribute("attributeOne")).isEqualTo("test");
|
||||
|
||||
verify(this.mockTemplate, times(1)).get(eq("1"));
|
||||
verify(mockSession, times(1)).isExpired();
|
||||
verify(mockSession, times(1)).getId();
|
||||
verify(mockSession, times(1)).getCreationTime();
|
||||
verify(mockSession, times(1)).getLastAccessedTime();
|
||||
verify(mockSession, times(1)).getAttributeNames();
|
||||
verify(mockSession, times(1)).getAttribute(eq("attributeOne"));
|
||||
|
||||
InOrder inOrder = inOrder(sessionRepositorySpy);
|
||||
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).commit(eq(actualSession));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).touch(eq(actualSession));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).registerInterest(eq(actualSession));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByIdDeletesMatchingExpiredSessionReturnsNull() {
|
||||
|
||||
String expectedSessionId = "1";
|
||||
|
||||
Session mockSession = mock(Session.class);
|
||||
|
||||
given(mockSession.isExpired()).willReturn(true);
|
||||
given(mockSession.getId()).willReturn(expectedSessionId);
|
||||
given(this.mockTemplate.get(eq(expectedSessionId))).willReturn(mockSession);
|
||||
given(this.mockTemplate.remove(eq(expectedSessionId))).willReturn(mockSession);
|
||||
when(mockSession.getId()).thenReturn("1");
|
||||
when(mockSession.isExpired()).thenReturn(true);
|
||||
when(this.mockTemplate.get(eq("1"))).thenReturn(mockSession);
|
||||
when(this.mockTemplate.remove(eq("1"))).thenReturn(mockSession);
|
||||
|
||||
willAnswer(invocation -> {
|
||||
doAnswer(invocation -> {
|
||||
|
||||
ApplicationEvent applicationEvent = invocation.getArgument(0);
|
||||
|
||||
@@ -201,83 +269,49 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent;
|
||||
|
||||
assertThat(sessionEvent.<Session>getSession()).isSameAs(mockSession);
|
||||
assertThat(sessionEvent.getSessionId()).isEqualTo(expectedSessionId);
|
||||
assertThat(sessionEvent.getSource())
|
||||
.isSameAs(GemFireOperationsSessionRepositoryTests.this.sessionRepository);
|
||||
assertThat(sessionEvent.getSessionId()).isEqualTo("1");
|
||||
assertThat(sessionEvent.getSource()).isSameAs(this.sessionRepository);
|
||||
|
||||
return null;
|
||||
|
||||
}).given(this.mockApplicationEventPublisher).publishEvent(any(ApplicationEvent.class));
|
||||
}).when(this.mockApplicationEventPublisher).publishEvent(any(ApplicationEvent.class));
|
||||
|
||||
assertThat(this.sessionRepository.findById(expectedSessionId)).isNull();
|
||||
assertThat(this.sessionRepository.findById("1")).isNull();
|
||||
|
||||
verify(this.mockTemplate, times(1)).get(eq(expectedSessionId));
|
||||
verify(this.mockTemplate, times(1)).remove(eq(expectedSessionId));
|
||||
verify(mockSession, times(1)).isExpired();
|
||||
verify(this.mockTemplate, times(1)).get(eq("1"));
|
||||
verify(this.mockTemplate, times(1)).remove(eq("1"));
|
||||
verify(mockSession, times(2)).getId();
|
||||
verify(mockSession, times(1)).isExpired();
|
||||
verify(this.mockApplicationEventPublisher, times(1))
|
||||
.publishEvent(isA(SessionDeletedEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByIdReturnsMatchingNonExpiredSession() {
|
||||
|
||||
String expectedId = "1";
|
||||
|
||||
Instant expectedCreationTime = Instant.now();
|
||||
Instant currentLastAccessedTime = expectedCreationTime.plusMillis(TimeUnit.MINUTES.toMillis(5));
|
||||
|
||||
Session mockSession = mock(Session.class);
|
||||
|
||||
given(mockSession.isExpired()).willReturn(false);
|
||||
given(mockSession.getId()).willReturn(expectedId);
|
||||
given(mockSession.getCreationTime()).willReturn(expectedCreationTime);
|
||||
given(mockSession.getLastAccessedTime()).willReturn(currentLastAccessedTime);
|
||||
given(mockSession.getAttributeNames()).willReturn(Collections.singleton("attrOne"));
|
||||
given(mockSession.getAttribute(eq("attrOne"))).willReturn("test");
|
||||
given(this.mockTemplate.get(eq(expectedId))).willReturn(mockSession);
|
||||
|
||||
Session actualSession = this.sessionRepository.findById(expectedId);
|
||||
|
||||
assertThat(actualSession).isNotNull();
|
||||
assertThat(actualSession).isNotSameAs(mockSession);
|
||||
assertThat(actualSession.getId()).isEqualTo(expectedId);
|
||||
assertThat(actualSession.getCreationTime()).isEqualTo(expectedCreationTime);
|
||||
assertThat(actualSession.getLastAccessedTime()).isNotEqualTo(currentLastAccessedTime);
|
||||
assertThat(actualSession.getLastAccessedTime().compareTo(expectedCreationTime)).isGreaterThanOrEqualTo(0);
|
||||
assertThat(actualSession.getAttributeNames()).isEqualTo(Collections.singleton("attrOne"));
|
||||
assertThat(String.valueOf(actualSession.<String>getAttribute("attrOne"))).isEqualTo("test");
|
||||
|
||||
verify(this.mockTemplate, times(1)).get(eq(expectedId));
|
||||
verify(mockSession, times(1)).isExpired();
|
||||
verify(mockSession, times(1)).getId();
|
||||
verify(mockSession, times(1)).getCreationTime();
|
||||
verify(mockSession, times(1)).getLastAccessedTime();
|
||||
verify(mockSession, times(1)).getAttributeNames();
|
||||
verify(mockSession, times(1)).getAttribute(eq("attrOne"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void findByIdReturnsNull() {
|
||||
|
||||
when(this.mockTemplate.get(anyString())).thenReturn(null);
|
||||
|
||||
assertThat(this.sessionRepository.findById("1")).isNull();
|
||||
GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository);
|
||||
|
||||
assertThat(sessionRepositorySpy.findById("1")).isNull();
|
||||
|
||||
verify(this.mockTemplate, times(1)).get(eq("1"));
|
||||
verify(sessionRepositorySpy, times(1)).findById(eq("1"));
|
||||
verify(sessionRepositorySpy, never()).delete(any());
|
||||
verify(sessionRepositorySpy, never()).commit(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void findByIndexNameAndIndexValueReturnsMatchingSession() {
|
||||
|
||||
Session mockSession = mock(Session.class, "MockSession");
|
||||
Session mockSession = mock(Session.class);
|
||||
|
||||
given(mockSession.getId()).willReturn("1");
|
||||
when(mockSession.getId()).thenReturn("1");
|
||||
|
||||
SelectResults<Object> mockSelectResults = mock(SelectResults.class);
|
||||
|
||||
given(mockSelectResults.asList()).willReturn(Collections.singletonList(mockSession));
|
||||
when(mockSelectResults.asList()).thenReturn(Collections.singletonList(mockSession));
|
||||
|
||||
String indexName = "vip";
|
||||
String indexValue = "rwinch";
|
||||
@@ -286,18 +320,26 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
String.format(GemFireOperationsSessionRepository.FIND_SESSIONS_BY_INDEX_NAME_AND_INDEX_VALUE_QUERY,
|
||||
this.sessionRepository.getFullyQualifiedRegionName(), indexName);
|
||||
|
||||
given(this.mockTemplate.find(eq(expectedQql), eq(indexValue))).willReturn(mockSelectResults);
|
||||
when(this.mockTemplate.find(eq(expectedQql), eq(indexValue))).thenReturn(mockSelectResults);
|
||||
|
||||
GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository);
|
||||
|
||||
Map<String, Session> sessions =
|
||||
this.sessionRepository.findByIndexNameAndIndexValue(indexName, indexValue);
|
||||
sessionRepositorySpy.findByIndexNameAndIndexValue(indexName, indexValue);
|
||||
|
||||
assertThat(sessions).isNotNull();
|
||||
assertThat(sessions.size()).isEqualTo(1);
|
||||
assertThat(sessions).hasSize(1);
|
||||
assertThat(sessions.get("1")).isEqualTo(mockSession);
|
||||
|
||||
verify(this.mockTemplate, times(1)).find(eq(expectedQql), eq(indexValue));
|
||||
verify(mockSelectResults, times(1)).asList();
|
||||
verify(mockSession, times(1)).getId();
|
||||
verify(mockSession, times(2)).getId();
|
||||
|
||||
InOrder inOrder = inOrder(sessionRepositorySpy);
|
||||
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).commit(eq(mockSession));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).touch(eq(mockSession));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).registerInterest(eq(mockSession));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -308,13 +350,13 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
Session mockSessionTwo = mock(Session.class, "MockSessionTwo");
|
||||
Session mockSessionThree = mock(Session.class, "MockSessionThree");
|
||||
|
||||
given(mockSessionOne.getId()).willReturn("1");
|
||||
given(mockSessionTwo.getId()).willReturn("2");
|
||||
given(mockSessionThree.getId()).willReturn("3");
|
||||
when(mockSessionOne.getId()).thenReturn("1");
|
||||
when(mockSessionTwo.getId()).thenReturn("2");
|
||||
when(mockSessionThree.getId()).thenReturn("3");
|
||||
|
||||
SelectResults<Object> mockSelectResults = mock(SelectResults.class);
|
||||
|
||||
given(mockSelectResults.asList()).willReturn(Arrays.asList(mockSessionOne, mockSessionTwo, mockSessionThree));
|
||||
when(mockSelectResults.asList()).thenReturn(Arrays.asList(mockSessionOne, mockSessionTwo, mockSessionThree));
|
||||
|
||||
String principalName = "jblum";
|
||||
|
||||
@@ -322,22 +364,36 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
String.format(GemFireOperationsSessionRepository.FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY,
|
||||
this.sessionRepository.getFullyQualifiedRegionName());
|
||||
|
||||
given(this.mockTemplate.find(eq(expectedOql), eq(principalName))).willReturn(mockSelectResults);
|
||||
when(this.mockTemplate.find(eq(expectedOql), eq(principalName))).thenReturn(mockSelectResults);
|
||||
|
||||
GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository);
|
||||
|
||||
Map<String, Session> sessions =
|
||||
this.sessionRepository.findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);
|
||||
sessionRepositorySpy.findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);
|
||||
|
||||
assertThat(sessions).isNotNull();
|
||||
assertThat(sessions.size()).isEqualTo(3);
|
||||
assertThat(sessions).hasSize(3);
|
||||
assertThat(sessions.get("1")).isEqualTo(mockSessionOne);
|
||||
assertThat(sessions.get("2")).isEqualTo(mockSessionTwo);
|
||||
assertThat(sessions.get("3")).isEqualTo(mockSessionThree);
|
||||
|
||||
verify(this.mockTemplate, times(1)).find(eq(expectedOql), eq(principalName));
|
||||
verify(mockSelectResults, times(1)).asList();
|
||||
verify(mockSessionOne, times(1)).getId();
|
||||
verify(mockSessionTwo, times(1)).getId();
|
||||
verify(mockSessionThree, times(1)).getId();
|
||||
verify(mockSessionOne, times(2)).getId();
|
||||
verify(mockSessionTwo, times(2)).getId();
|
||||
verify(mockSessionThree, times(2)).getId();
|
||||
|
||||
InOrder inOrder = inOrder(sessionRepositorySpy);
|
||||
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).commit(eq(mockSessionOne));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).touch(eq(mockSessionOne));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).registerInterest(eq(mockSessionOne));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).commit(eq(mockSessionTwo));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).touch(eq(mockSessionTwo));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).registerInterest(eq(mockSessionTwo));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).commit(eq(mockSessionThree));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).touch(eq(mockSessionThree));
|
||||
inOrder.verify(sessionRepositorySpy, times(1)).registerInterest(eq(mockSessionThree));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -346,7 +402,7 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
|
||||
SelectResults<Object> mockSelectResults = mock(SelectResults.class);
|
||||
|
||||
given(mockSelectResults.asList()).willReturn(Collections.emptyList());
|
||||
when(mockSelectResults.asList()).thenReturn(Collections.emptyList());
|
||||
|
||||
String principalName = "jblum";
|
||||
|
||||
@@ -354,20 +410,25 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
String.format(GemFireOperationsSessionRepository.FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY,
|
||||
this.sessionRepository.getFullyQualifiedRegionName());
|
||||
|
||||
given(this.mockTemplate.find(eq(expectedOql), eq(principalName))).willReturn(mockSelectResults);
|
||||
when(this.mockTemplate.find(eq(expectedOql), eq(principalName))).thenReturn(mockSelectResults);
|
||||
|
||||
GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository);
|
||||
|
||||
Map<String, Session> sessions =
|
||||
this.sessionRepository.findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);
|
||||
sessionRepositorySpy.findByIndexNameAndIndexValue(PRINCIPAL_NAME_INDEX_NAME, principalName);
|
||||
|
||||
assertThat(sessions).isNotNull();
|
||||
assertThat(sessions.isEmpty()).isTrue();
|
||||
assertThat(sessions).isEmpty();
|
||||
|
||||
verify(this.mockTemplate, times(1)).find(eq(expectedOql), eq(principalName));
|
||||
verify(mockSelectResults, times(1)).asList();
|
||||
verify(sessionRepositorySpy, times(1))
|
||||
.findByIndexNameAndIndexValue(eq(PRINCIPAL_NAME_INDEX_NAME), eq(principalName));
|
||||
verify(sessionRepositorySpy, never()).commit(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prepareQueryReturnsIndexNameValueOql() {
|
||||
public void prepareQueryReturnsIndexNameAndIndexValueOql() {
|
||||
|
||||
String attributeName = "testAttributeName";
|
||||
|
||||
@@ -403,8 +464,6 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
@Test
|
||||
public void saveStoresSession() {
|
||||
|
||||
String expectedSessionId = "1";
|
||||
|
||||
Instant expectedCreationTime = Instant.now();
|
||||
Instant expectedLastAccessTime = expectedCreationTime.plusMillis(TimeUnit.MINUTES.toMillis(5L));
|
||||
|
||||
@@ -412,19 +471,19 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
|
||||
Session mockSession = mock(Session.class);
|
||||
|
||||
when(mockSession.getId()).thenReturn(expectedSessionId);
|
||||
when(mockSession.getId()).thenReturn("1");
|
||||
when(mockSession.getCreationTime()).thenReturn(expectedCreationTime);
|
||||
when(mockSession.getLastAccessedTime()).thenReturn(expectedLastAccessTime);
|
||||
when(mockSession.getMaxInactiveInterval()).thenReturn(expectedMaxInactiveInterval);
|
||||
when(mockSession.getAttributeNames()).thenReturn(Collections.emptySet());
|
||||
|
||||
when(this.mockTemplate.put(eq(expectedSessionId), isA(GemFireSession.class)))
|
||||
when(this.mockTemplate.put(eq("1"), isA(GemFireSession.class)))
|
||||
.thenAnswer(invocation -> {
|
||||
|
||||
Session session = invocation.getArgument(1);
|
||||
|
||||
assertThat(session).isNotNull();
|
||||
assertThat(session.getId()).isEqualTo(expectedSessionId);
|
||||
assertThat(session.getId()).isEqualTo("1");
|
||||
assertThat(session.getCreationTime()).isEqualTo(expectedCreationTime);
|
||||
assertThat(session.getLastAccessedTime()).isEqualTo(expectedLastAccessTime);
|
||||
assertThat(session.getMaxInactiveInterval()).isEqualTo(expectedMaxInactiveInterval);
|
||||
@@ -433,30 +492,36 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
return null;
|
||||
});
|
||||
|
||||
this.sessionRepository.save(mockSession);
|
||||
GemFireOperationsSessionRepository sessionRepositorySpy = spy(this.sessionRepository);
|
||||
|
||||
sessionRepositorySpy.save(mockSession);
|
||||
|
||||
verify(mockSession, times(2)).getId();
|
||||
verify(mockSession, times(1)).getCreationTime();
|
||||
verify(mockSession, times(1)).getLastAccessedTime();
|
||||
verify(mockSession, times(1)).getMaxInactiveInterval();
|
||||
verify(mockSession, times(1)).getAttributeNames();
|
||||
verify(this.mockTemplate, times(1)).put(eq(expectedSessionId),
|
||||
verify(this.mockTemplate, times(1)).put(eq("1"),
|
||||
isA(GemFireSession.class));
|
||||
verify(sessionRepositorySpy, times(1)).save(eq(mockSession));
|
||||
verify(sessionRepositorySpy, times(1)).commit(eq(mockSession));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveStoresAndCommitsGemFireSession() {
|
||||
|
||||
GemFireSession<?> session = spy(GemFireSession.create());
|
||||
GemFireSession<?> session = GemFireSession.create();
|
||||
|
||||
assertThat(session).isNotNull();
|
||||
assertThat(session.hasDelta()).isTrue();
|
||||
|
||||
session = spy(session);
|
||||
|
||||
this.sessionRepository.save(session);
|
||||
|
||||
InOrder orderVerifier = inOrder(session);
|
||||
|
||||
orderVerifier.verify(session, times(2)).hasDelta();
|
||||
orderVerifier.verify(session, times(1)).hasDelta();
|
||||
orderVerifier.verify(session, times(1)).getId();
|
||||
orderVerifier.verify(session, times(1)).commit();
|
||||
|
||||
@@ -468,14 +533,16 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
@SuppressWarnings("unchecked")
|
||||
public void saveWillNotStoreNonDirtyGemFireSessions() {
|
||||
|
||||
GemFireSession session = spy(newNonDirtyGemFireSession());
|
||||
GemFireSession session = newNonDirtyGemFireSession();
|
||||
|
||||
assertThat(session).isNotNull();
|
||||
assertThat(session.hasDelta()).isFalse();
|
||||
|
||||
session = spy(session);
|
||||
|
||||
this.sessionRepository.save(session);
|
||||
|
||||
verify(session, times(2)).hasDelta();
|
||||
verify(session, times(1)).hasDelta();
|
||||
verify(session, never()).getId();
|
||||
verify(session, never()).commit();
|
||||
verify(this.mockTemplate, never()).put(any(), any(GemFireSession.class));
|
||||
@@ -484,14 +551,14 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
@Test
|
||||
public void deleteRemovesExistingSessionAndHandlesDelete() {
|
||||
|
||||
String expectedSessionId = "1";
|
||||
AtomicBoolean methodCalled = new AtomicBoolean(false);
|
||||
|
||||
Session mockSession = mock(Session.class);
|
||||
|
||||
given(mockSession.getId()).willReturn(expectedSessionId);
|
||||
given(this.mockTemplate.remove(eq(expectedSessionId))).willReturn(mockSession);
|
||||
when(mockSession.getId()).thenReturn("1");
|
||||
when(this.mockTemplate.remove(eq("1"))).thenReturn(mockSession);
|
||||
|
||||
willAnswer(invocation -> {
|
||||
doAnswer(invocation -> {
|
||||
|
||||
ApplicationEvent applicationEvent = invocation.getArgument(0);
|
||||
|
||||
@@ -499,18 +566,22 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
|
||||
AbstractSessionEvent sessionEvent = (AbstractSessionEvent) applicationEvent;
|
||||
|
||||
assertThat(sessionEvent.getSource()).isSameAs(GemFireOperationsSessionRepositoryTests.this.sessionRepository);
|
||||
assertThat(sessionEvent.<Session>getSession()).isSameAs(mockSession);
|
||||
assertThat(sessionEvent.getSessionId()).isEqualTo(expectedSessionId);
|
||||
assertThat(sessionEvent.getSessionId()).isEqualTo("1");
|
||||
assertThat(sessionEvent.getSource()).isSameAs(this.sessionRepository);
|
||||
|
||||
methodCalled.set(true);
|
||||
|
||||
return null;
|
||||
|
||||
}).given(this.mockApplicationEventPublisher).publishEvent(isA(SessionDeletedEvent.class));
|
||||
}).when(this.mockApplicationEventPublisher).publishEvent(isA(SessionDeletedEvent.class));
|
||||
|
||||
this.sessionRepository.deleteById(expectedSessionId);
|
||||
this.sessionRepository.deleteById("1");
|
||||
|
||||
assertThat(methodCalled.get()).isTrue();
|
||||
|
||||
verify(mockSession, times(1)).getId();
|
||||
verify(this.mockTemplate, times(1)).remove(eq(expectedSessionId));
|
||||
verify(this.mockTemplate, times(1)).remove(eq("1"));
|
||||
verify(this.mockApplicationEventPublisher, times(1))
|
||||
.publishEvent(isA(SessionDeletedEvent.class));
|
||||
}
|
||||
@@ -518,14 +589,9 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
@Test
|
||||
public void deleteRemovesNonExistingSessionAndHandlesDelete() {
|
||||
|
||||
AtomicBoolean called = new AtomicBoolean(false);
|
||||
AtomicBoolean methodCalled = new AtomicBoolean(false);
|
||||
|
||||
Session mockSession = mock(Session.class);
|
||||
|
||||
String expectedSessionId = "1";
|
||||
|
||||
when(mockSession.getId()).thenReturn(expectedSessionId);
|
||||
when(this.mockTemplate.remove(anyString())).thenReturn(mockSession);
|
||||
when(this.mockTemplate.remove(eq("1"))).thenReturn(null);
|
||||
|
||||
doAnswer(invocation -> {
|
||||
|
||||
@@ -538,23 +604,21 @@ public class GemFireOperationsSessionRepositoryTests {
|
||||
Session session = sessionEvent.getSession();
|
||||
|
||||
assertThat(session).isNotNull();
|
||||
assertThat(session.getId()).isEqualTo(expectedSessionId);
|
||||
assertThat(sessionEvent.getSessionId()).isEqualTo(expectedSessionId);
|
||||
assertThat(sessionEvent.getSource())
|
||||
.isSameAs(GemFireOperationsSessionRepositoryTests.this.sessionRepository);
|
||||
assertThat(session.getId()).isEqualTo("1");
|
||||
assertThat(sessionEvent.getSessionId()).isEqualTo("1");
|
||||
assertThat(sessionEvent.getSource()).isEqualTo(this.sessionRepository);
|
||||
|
||||
called.set(true);
|
||||
methodCalled.set(true);
|
||||
|
||||
return null;
|
||||
|
||||
}).when(this.mockApplicationEventPublisher).publishEvent(isA(SessionDeletedEvent.class));
|
||||
|
||||
this.sessionRepository.deleteById(expectedSessionId);
|
||||
this.sessionRepository.deleteById("1");
|
||||
|
||||
assertThat(called.get()).isTrue();
|
||||
assertThat(methodCalled.get()).isTrue();
|
||||
|
||||
verify(mockSession, times(2)).getId();
|
||||
verify(this.mockTemplate, times(1)).remove(eq(expectedSessionId));
|
||||
verify(this.mockTemplate, times(1)).remove(eq("1"));
|
||||
verify(this.mockApplicationEventPublisher, times(1))
|
||||
.publishEvent(isA(SessionDeletedEvent.class));
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.spy;
|
||||
@@ -58,7 +59,6 @@ import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.data.gemfire.GemfireOperations;
|
||||
import org.springframework.data.gemfire.GemfireTemplate;
|
||||
import org.springframework.data.gemfire.RegionAttributesFactoryBean;
|
||||
import org.springframework.data.gemfire.util.ArrayUtils;
|
||||
@@ -96,7 +96,7 @@ import org.springframework.util.ReflectionUtils;
|
||||
public class GemFireHttpSessionConfigurationTests {
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T getField(Object obj, String fieldName) {
|
||||
private static <T> T getField(Object obj, String fieldName) {
|
||||
|
||||
try {
|
||||
|
||||
@@ -678,9 +678,14 @@ public class GemFireHttpSessionConfigurationTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void createsAndInitializesSessionRepositoryBean() {
|
||||
|
||||
GemfireOperations mockGemfireOperations = mock(GemfireOperations.class);
|
||||
Region<Object, Session> mockRegion = mock(Region.class);
|
||||
|
||||
GemfireTemplate mockGemfireOperations = mock(GemfireTemplate.class);
|
||||
|
||||
doReturn(mockRegion).when(mockGemfireOperations).getRegion();
|
||||
|
||||
this.gemfireConfiguration.setMaxInactiveIntervalInSeconds(120);
|
||||
|
||||
@@ -688,7 +693,7 @@ public class GemFireHttpSessionConfigurationTests {
|
||||
this.gemfireConfiguration.sessionRepository(mockGemfireOperations);
|
||||
|
||||
assertThat(sessionRepository).isNotNull();
|
||||
assertThat(sessionRepository.getTemplate()).isSameAs(mockGemfireOperations);
|
||||
assertThat(sessionRepository.getSessionsTemplate()).isSameAs(mockGemfireOperations);
|
||||
assertThat(sessionRepository.getMaxInactiveIntervalInSeconds()).isEqualTo(120);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user