org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.java

Source

/*
 * 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;

import static org.springframework.data.gemfire.util.RuntimeExceptionFactory.newIllegalStateException;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;

import org.apache.geode.DataSerializable;
import org.apache.geode.DataSerializer;
import org.apache.geode.Delta;
import org.apache.geode.InvalidDeltaException;
import org.apache.geode.cache.EntryEvent;
import org.apache.geode.cache.InterestResultPolicy;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.RegionAttributes;
import org.apache.geode.cache.client.Pool;
import org.apache.geode.cache.client.PoolManager;
import org.apache.geode.cache.util.CacheListenerAdapter;

import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.data.gemfire.GemfireAccessor;
import org.springframework.data.gemfire.GemfireOperations;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
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;
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;
import org.springframework.session.events.SessionExpiredEvent;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link AbstractGemFireOperationsSessionRepository} is an abstract base class encapsulating functionality
 * 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.cache.EntryEvent
 * @see org.apache.geode.cache.Region
 * @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.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.DeltaAwareDirtyPredicate
 * @see org.springframework.session.data.gemfire.support.IsDirtyPredicate
 * @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
        implements ApplicationEventPublisherAware, FindByIndexNameSessionRepository<Session> {

    private static final boolean DEFAULT_CLIENT_SUBSCRIPTIONS_ENABLED = false;
    private static final boolean DEFAULT_REGISTER_INTEREST_DURABILITY = false;
    private static final boolean DEFAULT_REGISTER_INTEREST_ENABLED = false;
    private static final boolean DEFAULT_REGISTER_INTEREST_RECEIVE_VALUES = true;

    // TODO - use non-static variable
    private static final AtomicBoolean usingDataSerialization = new AtomicBoolean(false);

    private static final Duration DEFAULT_MAX_INACTIVE_INTERVAL = Duration
            .ofSeconds(GemFireHttpSessionConfiguration.DEFAULT_MAX_INACTIVE_INTERVAL_IN_SECONDS);

    private static final InterestResultPolicy DEFAULT_REGISTER_INTEREST_RESULT_POLICY = InterestResultPolicy.NONE;

    private static final IsDirtyPredicate DEFAULT_IS_DIRTY_PREDICATE = GemFireHttpSessionConfiguration.DEFAULT_IS_DIRTY_PREDICATE;

    private boolean registerInterestEnabled = DEFAULT_REGISTER_INTEREST_ENABLED;

    private ApplicationEventPublisher applicationEventPublisher = event -> {
    };

    private Duration maxInactiveInterval = DEFAULT_MAX_INACTIVE_INTERVAL;

    private final GemfireOperations template;

    private IsDirtyPredicate dirtyPredicate = DEFAULT_IS_DIRTY_PREDICATE;

    private final Logger logger = newLogger();

    private final Region<Object, Session> sessions;

    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 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} 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()
     * @see #isRegionRegisterInterestAllowed(Region)
     */
    private @Nullable 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 (isRegionRegisterInterestAllowed(sessionsRegion)) {
                        this.registerInterestEnabled = true;
                        sessionsRegionAttributesMutator.addCacheListener(newSessionIdInterestRegistrar());
                    }
                });

        return sessionsRegion;
    }

    /**
     * Determines whether the given {@link Region} is a client, non-local {@link Region}.
     *
     * @param region {@link Region} to evaluate.
     * @return a boolean indicating whether the given {@link Region} is a client, non-local {@link Region}.
     * @see org.apache.geode.cache.Region
     */
    boolean isNonLocalClientRegion(@Nullable Region<?, ?> region) {
        return GemFireUtils.isNonLocalClientRegion(region);
    }

    /**
     * Determines whether the given client {@link Region Region's} configured {@link Pool} has subscription enabled.
     *
     * @param region {@link Region} to evaluate.
     * @return a boolean value indicating whether the client {@link Region Region's} configured {@link Pool}
     * has subscription enabled.
     * @see org.apache.geode.cache.client.Pool#getSubscriptionEnabled()
     * @see org.apache.geode.cache.Region
     */
    boolean isRegionPoolSubscriptionEnabled(@Nullable Region<?, ?> region) {

        return Boolean.TRUE.equals(Optional.ofNullable(region).map(Region::getAttributes)
                .map(RegionAttributes::getPoolName).map(this::resolvePool).map(Pool::getSubscriptionEnabled)
                .orElse(DEFAULT_CLIENT_SUBSCRIPTIONS_ENABLED));
    }

    /**
     * Determines whether the interest registration for the given {@link Region} is allowed.
     *
     * @param region {@link Region} to evaluate.
     * @return a boolean value indicating whether interest registration for the given {@link Region} is allowed.
     * @see org.apache.geode.cache.Region#registerInterest(Object)
     * @see org.apache.geode.cache.Region
     * @see #isNonLocalClientRegion(Region)
     * @see #isRegionPoolSubscriptionEnabled(Region)
     */
    boolean isRegionRegisterInterestAllowed(@Nullable Region<?, ?> region) {
        return isNonLocalClientRegion(region) && isRegionPoolSubscriptionEnabled(region);
    }

    /**
     * Resolves the {@link Pool} with the given {@link String name} from the {@link PoolManager}.
     *
     * @param name {@link String} containing the name of the {@link Pool} to resolve.
     * @return the resolved {@link Pool} for the given {@link String name}.
     * @see org.apache.geode.cache.client.PoolManager#find(String)
     * @see org.apache.geode.cache.client.Pool
     */
    protected @Nullable Pool resolvePool(String name) {
        return PoolManager.find(name);
    }

    /**
     * Constructs a new instance of {@link Logger} using Apache Commons {@link LoggerFactory}.
     *
     * @return a new instance of {@link Logger} constructed from Apache commons-logging {@link LoggerFactory}.
     * @see org.apache.commons.logging.LogFactory#getLog(Class)
     * @see org.apache.commons.logging.Log
     */
    private Logger newLogger() {
        return LoggerFactory.getLogger(getClass());
    }

    /**
     * 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);
    }

    /**
     * Sets the configured {@link ApplicationEventPublisher} used to publish {@link Session}
     * {@link AbstractSessionEvent events} corresponding to Apache Geode/Pivotal GemFire cache events.
     *
     * @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
     */
    @Override
    public void setApplicationEventPublisher(@NonNull ApplicationEventPublisher applicationEventPublisher) {

        Assert.notNull(applicationEventPublisher, "ApplicationEventPublisher is required");

        this.applicationEventPublisher = applicationEventPublisher;
    }

    /**
     * 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 configured {@link ApplicationEventPublisher} used to publish {@link Session}
     * {@link AbstractSessionEvent events}.
     * @see org.springframework.context.ApplicationEventPublisher
     */
    protected @NonNull ApplicationEventPublisher getApplicationEventPublisher() {
        return this.applicationEventPublisher;
    }

    /**
     * Configures the {@link IsDirtyPredicate} strategy interface used to determine whether the users' application
     * domain objects are dirty or not.
     *
     * @param dirtyPredicate {@link IsDirtyPredicate} strategy interface implementation used to determine whether
     * the users' application domain objects are dirty or not.
     * @see org.springframework.session.data.gemfire.support.IsDirtyPredicate
     */
    public void setIsDirtyPredicate(IsDirtyPredicate dirtyPredicate) {
        this.dirtyPredicate = dirtyPredicate;
    }

    /**
     * Returns the configured {@link IsDirtyPredicate} strategy interface implementation used to determine whether
     * the users' application domain objects are dirty or not.
     *
     * Defaults to {@link GemFireHttpSessionConfiguration#DEFAULT_IS_DIRTY_PREDICATE}.
     *
     * @return the configured {@link IsDirtyPredicate} strategy interface used to determine whether
     * the users' application domain objects are dirty or not.
     * @see org.springframework.session.data.gemfire.support.IsDirtyPredicate
     */
    public IsDirtyPredicate getIsDirtyPredicate() {

        return this.dirtyPredicate != null ? this.dirtyPredicate : DEFAULT_IS_DIRTY_PREDICATE;
    }

    /**
     * Return a reference to the {@link Logger} used to log messages.
     *
     * @return a reference to the {@link Logger} used to log messages.
     * @see org.apache.commons.logging.Log
     */
    protected Logger getLogger() {
        return this.logger;
    }

    /**
     * Sets the {@link Duration maximum interval} in which a {@link Session} can remain inactive
     * before the {@link Session} is considered expired.
     *
     * @param maxInactiveInterval {@link Duration} specifying the maximum interval that a {@link Session}
     * can remain inactive before the {@link Session} is considered expired.
     * @see java.time.Duration
     */
    public void setMaxInactiveInterval(Duration maxInactiveInterval) {
        this.maxInactiveInterval = maxInactiveInterval;
    }

    /**
     * Returns the {@link Duration maximum interval} in which a {@link Session} can remain inactive
     * before the {@link Session} is considered expired.
     *
     * @return a {@link Duration} specifying the maximum interval that a {@link Session} can remain inactive
     * before the {@link Session} is considered expired.
     * @see java.time.Duration
     */
    public Duration getMaxInactiveInterval() {
        return this.maxInactiveInterval;
    }

    /**
     * Sets the maximum interval in seconds in which a {@link Session} can remain inactive
     * before the {@link Session} is considered expired.
     *
     * @param maxInactiveIntervalInSeconds an integer value specifying the maximum interval in seconds
     * that a {@link Session} can remain inactive before the {@link Session }is considered expired.
     * @see #setMaxInactiveInterval(Duration)
     */
    public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
        setMaxInactiveInterval(Duration.ofSeconds(maxInactiveIntervalInSeconds));
    }

    /**
     * Returns the maximum interval in seconds in which a {@link Session} can remain inactive
     * before the {@link Session} is considered expired.
     *
     * @return an integer value specifying the maximum interval in seconds that a {@link Session} can remain inactive
     * before the {@link Session} is considered expired.
     * @see #getMaxInactiveInterval()
     */
    public int getMaxInactiveIntervalInSeconds() {

        return Optional.ofNullable(getMaxInactiveInterval()).map(Duration::getSeconds).map(Long::intValue)
                .orElse(0);
    }

    /**
     * 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;
    }

    protected Optional<SessionEventHandlerCacheListenerAdapter> getSessionEventHandler() {
        return Optional.ofNullable(this.sessionEventHandler);
    }

    /**
     * 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 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 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 getSessionsRegionName() {
        return getSessionsRegion().getFullPath();
    }

    /**
     * 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 @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.
     *
     * @param useDataSerialization boolean indicating whether the DataSerialization framework has been configured.
     */
    public void setUseDataSerialization(boolean useDataSerialization) {
        usingDataSerialization.set(useDataSerialization);
    }

    /**
     * Determines whether the DataSerialization framework has been configured.
     *
     * @return a boolean indicating whether the DataSerialization framework has been configured.
     */
    protected static boolean isUsingDataSerialization() {
        return usingDataSerialization.get();
    }

    /**
     * Commits the given {@link Session}.
     *
     * @param session {@link Session} to commit, iff the {@link Session} is {@literal committable}.
     * @return the given {@link Session}
     * @see GemFireSession#commit()
     */
    protected @Nullable Session commit(@Nullable Session session) {

        return Optional.ofNullable(session).filter(GemFireSession.class::isInstance).map(GemFireSession.class::cast)
                .<Session>map(gemfireSession -> {
                    gemfireSession.commit();
                    return gemfireSession;
                }).orElse(session);
    }

    protected @Nullable Session configure(@Nullable Session session) {

        return Optional.ofNullable(session).filter(GemFireSession.class::isInstance).map(GemFireSession.class::cast)
                .map(it -> it.configureWith(getMaxInactiveInterval()))
                .<Session>map(it -> it.configureWith(getIsDirtyPredicate())).orElse(session);
    }

    /**
     * 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 @Nullable Session delete(@NonNull Session session) {

        deleteById(session.getId());

        return null;
    }

    /**
     * Handles the deletion of the given {@link Session}.
     *
     * @param sessionId {@link String} containing the {@link Session#getId()} of the given {@link Session}.
     * @param session deleted {@link Session}.
     * @see SessionEventHandlerCacheListenerAdapter#afterDelete(String, Session)
     * @see org.springframework.session.Session
     * @see #unregisterInterest(Object)
     */
    protected void handleDeleted(String sessionId, Session session) {

        getSessionEventHandler().ifPresent(it -> it.afterDelete(sessionId, session));

        unregisterInterest(sessionId);
    }

    /**
     * Publishes the specified {@link ApplicationEvent} to the Spring container thereby notifying other (potentially)
     * interested application components/beans.
     *
     * @param event {@link ApplicationEvent} to publish.
     * @see org.springframework.context.ApplicationEventPublisher#publishEvent(ApplicationEvent)
     * @see org.springframework.context.ApplicationEvent
     */
    protected void publishEvent(ApplicationEvent event) {

        try {
            getApplicationEventPublisher().publishEvent(event);
        } catch (Throwable cause) {
            getLogger().error(String.format("Error occurred while publishing event [%s]", event), cause);
        }
    }

    /**
     * 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(@Nullable Object sessionId) {

        Optional.ofNullable(sessionId).filter(it -> 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 @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 {

        public DeltaCapableGemFireSession() {
        }

        public DeltaCapableGemFireSession(String id) {
            super(id);
        }

        public DeltaCapableGemFireSession(Session session) {
            super(session);
        }

        @Override
        protected DeltaCapableGemFireSessionAttributes newSessionAttributes(Object lock) {

            return new DeltaCapableGemFireSessionAttributes(lock).configureWith(getIsDirtyPredicate());
        }

        public synchronized void toDelta(DataOutput out) throws IOException {

            out.writeUTF(getId());
            out.writeLong(getLastAccessedTime().toEpochMilli());
            out.writeLong(getMaxInactiveInterval().getSeconds());
            getAttributes().toDelta(out);
        }

        public synchronized void fromDelta(DataInput in) throws IOException {

            setId(in.readUTF());
            setLastAccessedTime(Instant.ofEpochMilli(in.readLong()));
            setMaxInactiveInterval(Duration.ofSeconds(in.readLong()));
            getAttributes().fromDelta(in);
        }
    }

    /**
     * {@link GemFireSession} is a Abstract Data Type (ADT) for a Spring {@link Session} that stores and manages
     * {@link Session} state in Apache Geode or Pivotal GemFire.
     *
     * @see java.lang.Comparable
     * @see org.springframework.session.Session
     * @see org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.GemFireSessionAttributes
     */
    @SuppressWarnings("serial")
    public static class GemFireSession<T extends GemFireSessionAttributes> implements Comparable<Session>, Session {

        protected static final String GEMFIRE_SESSION_TO_STRING = "{ @type = %1$s, id = %2$s, creationTime = %3$s, lastAccessedTime = %4$s, maxInactiveInterval = %5$s, principalName = %6$s }";

        protected static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";

        /**
         * Factory method used to construct a new, default instance of {@link GemFireSession}.
         *
         * @param <T> {@link Class Sub-type} of {@link GemFireSessionAttributes}.
         * @return a new {@link GemFireSession}.
         * @see #isUsingDataSerialization()
         */
        @SuppressWarnings("unchecked")
        public static <T extends GemFireSessionAttributes> GemFireSession<T> create() {

            return isUsingDataSerialization() ? (GemFireSession<T>) new DeltaCapableGemFireSession()
                    : new GemFireSession();
        }

        /**
         * Copy (i.e. clone) the given {@link Session}.
         *
         * @param session {@link Session} to copy/clone.
         * @return a new instance of {@link GemFireSession} copied from the given {@link Session}.
         * @see org.springframework.session.Session
         * @see #isUsingDataSerialization()
         */
        public static GemFireSession copy(@NonNull Session session) {

            return isUsingDataSerialization() ? new DeltaCapableGemFireSession(session)
                    : new GemFireSession(session);
        }

        /**
         * Returns the given {@link Session} if the {@link Session} is a {@link GemFireSession}
         * or return a copy of the given {@link Session} as a {@link GemFireSession}.
         *
         * @param session {@link Session} to evaluate and possibly copy.
         * @return the given {@link Session} if the {@link Session} is a {@link GemFireSession}
         * or return a copy of the given {@link Session} as a {@link GemFireSession}.
         * @see #copy(Session)
         */
        public static GemFireSession from(@NonNull Session session) {
            return session instanceof GemFireSession ? (GemFireSession) session : copy(session);
        }

        private transient boolean delta = true;

        private Duration maxInactiveInterval;

        private final Instant creationTime;

        private Instant lastAccessedTime;

        private transient IsDirtyPredicate dirtyPredicate = DEFAULT_IS_DIRTY_PREDICATE;

        private transient final SpelExpressionParser parser = new SpelExpressionParser();

        private String id;

        private transient final T sessionAttributes = newSessionAttributes(this);

        /**
         * Constructs a new, default instance of {@link GemFireSession} initialized with
         * a generated {@link Session#getId() Session Identifier}.
         *
         * @see #GemFireSession(String)
         * @see #generateSessionId()
         */
        protected GemFireSession() {
            this(generateSessionId());
        }

        /**
         * Constructs a new instance of {@link GemFireSession} initialized with
         * the given {@link Session#getId() Session Identifier}.
         *
         * Additionally, the {@link #creationTime} is set to {@link Instant#now()}, {@link #lastAccessedTime}
         * is set to {@link #creationTime} and the {@link #maxInactiveInterval} is set to {@link Duration#ZERO}.
         *
         * @param id {@link String} containing the unique identifier for this {@link Session}.
         * @see #validateSessionId(String)
         */
        protected GemFireSession(String id) {

            this.id = validateSessionId(id);
            this.creationTime = Instant.now();
            this.lastAccessedTime = this.creationTime;
            this.maxInactiveInterval = Duration.ZERO;
        }

        /**
         * Constructs a new instance of {@link GemFireSession} copied from the given {@link Session}.
         *
         * @param session {@link Session} to copy.
         * @throws IllegalArgumentException if {@link Session} is {@literal null}.
         * @see org.springframework.session.Session
         */
        protected GemFireSession(Session session) {

            Assert.notNull(session, "Session is required");

            this.id = session.getId();
            this.creationTime = session.getCreationTime();
            this.lastAccessedTime = session.getLastAccessedTime();
            this.maxInactiveInterval = session.getMaxInactiveInterval();
            this.sessionAttributes.from(session);
        }

        /**
         * Constructs a new {@link GemFireSessionAttributes} object to store and manage Session attributes.
         *
         * @param lock {@link Object} used as the mutex for concurrent access and Thread-safety.
         * @return the new {@link GemFireSessionAttributes}.
         * @see GemFireSessionAttributes
         * @see #getIsDirtyPredicate()
         */
        @SuppressWarnings("unchecked")
        protected T newSessionAttributes(Object lock) {

            return (T) new GemFireSessionAttributes(lock).configureWith(getIsDirtyPredicate());
        }

        /**
         * Change the {@link String identifier} of this {@link Session}.
         *
         * @return the new {@link String identifier} of of this {@link Session}.
         * @see #generateSessionId()
         * @see #triggerDelta()
         * @see #getId()
         */
        @Override
        public synchronized String changeSessionId() {

            this.id = generateSessionId();

            triggerDelta();

            return getId();
        }

        /**
         * Randomly generates a {@link String unique identifier} (ID) from {@link UUID} to be used as
         * the {@link Session#getId() ID} for this {@link Session}.
         *
         * @return a new {@link String unique identifier (ID)}.
         * @see java.util.UUID#randomUUID()
         */
        private static String generateSessionId() {
            return UUID.randomUUID().toString();
        }

        /**
         * Validates the given {@link Session} {@link String identifier} (ID) is set and valid.
         *
         * @param id {@link String} containing the {@link Session} identifier.
         * @return the given {@link String ID}.
         * @throws IllegalArgumentException if {@link String} contains no value.
         */
        private static String validateSessionId(String id) {

            Assert.hasText(id, "ID is required");

            return id;
        }

        protected synchronized void commit() {
            this.delta = false;
            getAttributes().commit();
        }

        /**
         * Determines whether this {@link GemFireSession} has any changes (i.e. a delta).
         *
         * Changes exist if this {@link GemFireSession GemFireSession's} {@link #getId() ID},
         * {@link #getLastAccessedTime() last accessed time}, {@link #getMaxInactiveInterval() max inactive interval}
         * or any of these {@link #getAttributeNames() attributes} have changed.
         *
         * @return a boolean value indicating whether this {@link GemFireSession} has any changes.
         * @see GemFireSessionAttributes#hasDelta()
         * @see #getAttributes()
         */
        public synchronized boolean hasDelta() {
            return this.delta || getAttributes().hasDelta();
        }

        protected synchronized void triggerDelta() {
            triggerDelta(true);
        }

        protected synchronized void triggerDelta(boolean delta) {
            this.delta |= delta;
        }

        synchronized void setId(String id) {
            this.id = validateSessionId(id);
        }

        public synchronized String getId() {
            return this.id;
        }

        public void setAttribute(String attributeName, Object attributeValue) {
            getAttributes().setAttribute(attributeName, attributeValue);
        }

        public void removeAttribute(String attributeName) {
            getAttributes().removeAttribute(attributeName);
        }

        public <T> T getAttribute(String attributeName) {
            return getAttributes().getAttribute(attributeName);
        }

        public Set<String> getAttributeNames() {
            return getAttributes().getAttributeNames();
        }

        public T getAttributes() {
            return this.sessionAttributes;
        }

        public synchronized Instant getCreationTime() {
            return this.creationTime;
        }

        public synchronized boolean isExpired() {

            Instant lastAccessedTime = getLastAccessedTime();

            Duration maxInactiveInterval = getMaxInactiveInterval();

            return isExpirationEnabled(maxInactiveInterval)
                    && Instant.now().minus(maxInactiveInterval).isAfter(lastAccessedTime);
        }

        private boolean isExpirationDisabled(Duration duration) {
            return duration == null || duration.isNegative() || duration.isZero();
        }

        private boolean isExpirationEnabled(Duration duration) {
            return !isExpirationDisabled(duration);
        }

        protected synchronized void setIsDirtyPredicate(IsDirtyPredicate dirtyPredicate) {

            this.dirtyPredicate = dirtyPredicate;
            getAttributes().configureWith(dirtyPredicate);
        }

        protected synchronized IsDirtyPredicate getIsDirtyPredicate() {

            return this.dirtyPredicate != null ? this.dirtyPredicate : DEFAULT_IS_DIRTY_PREDICATE;
        }

        private boolean isLastAccessedTimeValid(Instant lastAccessedTime) {
            return lastAccessedTime != null;
        }

        public synchronized void setLastAccessedTime(Instant lastAccessedTime) {

            if (isLastAccessedTimeValid(lastAccessedTime)) {

                triggerDelta(!ObjectUtils.nullSafeEquals(this.lastAccessedTime, lastAccessedTime));

                this.lastAccessedTime = lastAccessedTime;
            }
        }

        public synchronized Instant getLastAccessedTime() {
            return this.lastAccessedTime;
        }

        public synchronized void setMaxInactiveInterval(Duration maxInactiveInterval) {

            triggerDelta(!ObjectUtils.nullSafeEquals(this.maxInactiveInterval, maxInactiveInterval));

            this.maxInactiveInterval = maxInactiveInterval;
        }

        public synchronized Duration getMaxInactiveInterval() {

            return this.maxInactiveInterval != null ? this.maxInactiveInterval : Duration.ZERO;
        }

        public synchronized void setPrincipalName(String principalName) {
            setAttribute(PRINCIPAL_NAME_INDEX_NAME, principalName);
        }

        public synchronized String getPrincipalName() {

            String principalName = getAttribute(PRINCIPAL_NAME_INDEX_NAME);

            if (principalName == null) {

                Object authentication = getAttribute(SPRING_SECURITY_CONTEXT);

                if (authentication != null) {

                    Expression expression = this.parser.parseExpression("authentication?.name");

                    principalName = expression.getValue(authentication, String.class);
                }
            }

            return principalName;
        }

        /**
         * Builder method to configure the {@link Duration max inactive interval} before this {@link GemFireSession}
         * will expire.
         *
         * @param maxInactiveInterval {@link Duration} specifying the maximum time this {@link GemFireSession}
         * can remain inactive before expiration.
         * @return this {@link GemFireSession}.
         * @see #setMaxInactiveInterval(Duration)
         * @see java.time.Duration
         */
        public GemFireSession<T> configureWith(Duration maxInactiveInterval) {
            setMaxInactiveInterval(maxInactiveInterval);
            return this;
        }

        /**
         * Builder method to configure the {@link IsDirtyPredicate} strategy interface implementation to determine
         * whether users' {@link Object application domain objects} stored in this {@link GemFireSession} are dirty.
         *
         * @param dirtyPredicate {@link IsDirtyPredicate} strategy interface implementation that determines whether
         * the users' {@link Object application domain objects} stored in this {@link GemFireSession} are dirty.
         * @return this {@link GemFireSession}.
         * @see org.springframework.session.data.gemfire.support.IsDirtyPredicate
         * @see #setIsDirtyPredicate(IsDirtyPredicate)
         */
        public GemFireSession<T> configureWith(IsDirtyPredicate dirtyPredicate) {
            setIsDirtyPredicate(dirtyPredicate);
            return this;
        }

        @SuppressWarnings("all")
        @Override
        public int compareTo(Session session) {
            return getCreationTime().compareTo(session.getCreationTime());
        }

        @Override
        public boolean equals(final Object obj) {

            if (this == obj) {
                return true;
            }

            if (!(obj instanceof Session)) {
                return false;
            }

            Session that = (Session) obj;

            return this.getId().equals(that.getId());
        }

        @Override
        public int hashCode() {

            int hashValue = 17;

            hashValue = 37 * hashValue + getId().hashCode();

            return hashValue;
        }

        @Override
        public synchronized String toString() {

            return String.format(GEMFIRE_SESSION_TO_STRING, getClass().getName(), getId(), getCreationTime(),
                    getLastAccessedTime(), getMaxInactiveInterval(), getPrincipalName());
        }
    }

    public static class DeltaCapableGemFireSessionAttributes extends GemFireSessionAttributes implements Delta {

        private transient final Set<String> sessionAttributeDeltas = new HashSet<>();

        public DeltaCapableGemFireSessionAttributes() {
        }

        public DeltaCapableGemFireSessionAttributes(Object lock) {
            super(lock);
        }

        Set<String> getSessionAttributeDeltas() {

            synchronized (getLock()) {
                return this.sessionAttributeDeltas;
            }
        }

        @Override
        protected BiFunction<String, Object, Boolean> sessionAttributesChangeInterceptor() {

            return (attributeName, attributeValue) -> {
                getSessionAttributeDeltas().add(attributeName);
                return true;
            };
        }

        public void toDelta(DataOutput out) throws IOException {

            synchronized (getLock()) {

                Set<String> sessionAttributeDeltas = getSessionAttributeDeltas();

                out.writeInt(sessionAttributeDeltas.size());

                for (String attributeName : sessionAttributeDeltas) {
                    out.writeUTF(attributeName);
                    writeObject(getAttribute(attributeName), out);
                }
            }
        }

        protected void writeObject(Object value, DataOutput out) throws IOException {
            DataSerializer.writeObject(value, out);
        }

        @Override
        public boolean hasDelta() {

            synchronized (getLock()) {
                return !getSessionAttributeDeltas().isEmpty();
            }
        }

        public void fromDelta(DataInput in) throws InvalidDeltaException, IOException {

            synchronized (getLock()) {
                try {

                    int count = in.readInt();

                    Map<String, Object> deltas = new HashMap<>(count);

                    while (count-- > 0) {
                        deltas.put(in.readUTF(), readObject(in));
                    }

                    Set<String> sessionAttributeDeltas = getSessionAttributeDeltas();

                    deltas.forEach((key, value) -> {
                        setAttribute(key, value);
                        sessionAttributeDeltas.remove(key);
                    });
                } catch (ClassNotFoundException cause) {
                    throw new InvalidDeltaException("Class type in data not found", cause);
                }
            }
        }

        protected <T> T readObject(DataInput in) throws ClassNotFoundException, IOException {
            return DataSerializer.readObject(in);
        }

        @Override
        protected void commit() {

            synchronized (getLock()) {
                getSessionAttributeDeltas().clear();
                super.commit();
            }
        }
    }

    /**
     * The {@link GemFireSessionAttributes} class is a container for Session attributes implementing
     * both the {@link DataSerializable} and {@link Delta} Pivotal GemFire interfaces for efficient
     * storage and distribution (replication) in GemFire. Additionally, GemFireSessionAttributes
     * extends {@link AbstractMap} providing {@link Map}-like behavior since attributes of a Session
     * are effectively a name to value mapping.
     *
     * @see java.util.AbstractMap
     * @see org.apache.geode.DataSerializable
     * @see org.apache.geode.DataSerializer
     * @see org.apache.geode.Delta
     */
    @SuppressWarnings("serial")
    public static class GemFireSessionAttributes extends AbstractMap<String, Object> {

        public static GemFireSessionAttributes create() {
            return new GemFireSessionAttributes();
        }

        public static GemFireSessionAttributes create(Object lock) {
            return new GemFireSessionAttributes(lock);
        }

        private transient boolean delta = false;

        private transient IsDirtyPredicate dirtyPredicate = DEFAULT_IS_DIRTY_PREDICATE;

        private transient final Map<String, Object> sessionAttributes = new HashMap<>();

        private transient final Object lock;

        /**
         * Constructs a new instance of {@link GemFireSessionAttributes}.
         */
        protected GemFireSessionAttributes() {
            this.lock = this;
        }

        /**
         * Constructs a new instance of {@link GemFireSessionAttributes} initialized with the given {@link Object lock}
         * to use to guard against concurrent access by multiple {@link Thread Threads}.
         *
         * @param lock {@link Object} used as the {@literal mutex} to guard the operations of this object
         * from concurrent access by multiple {@link Thread Threads}.
         */
        protected GemFireSessionAttributes(@Nullable Object lock) {
            this.lock = lock != null ? lock : this;
        }

        /**
         * Returns a reference to the internal, {@link Session} attributes data structure.
         *
         * @return a reference to the internal, {@link Session} attributes data structure.
         * @see java.util.Map
         */
        Map<String, Object> getMap() {
            return this.sessionAttributes;
        }

        /**
         * Returns the {@link Object} used as the {@literal lock} guarding the methods of this object
         * from concurrent access by multiple {@link Thread Threads}.
         *
         * @return the {@link Object lock} guarding the methods of this object from concurrent access
         * by multiple {@link Thread Threads}.
         */
        public Object getLock() {
            return this.lock;
        }

        protected void setIsDirtyPredicate(IsDirtyPredicate dirtyPredicate) {

            synchronized (getLock()) {
                this.dirtyPredicate = dirtyPredicate;
            }
        }

        protected IsDirtyPredicate getIsDirtyPredicate() {

            synchronized (getLock()) {
                return this.dirtyPredicate != null ? this.dirtyPredicate : DEFAULT_IS_DIRTY_PREDICATE;
            }
        }

        public Object setAttribute(String attributeName, Object attributeValue) {

            synchronized (getLock()) {
                return attributeValue != null ? doSetAttribute(attributeName, attributeValue)
                        : removeAttribute(attributeName);
            }
        }

        private Object doSetAttribute(String attributeName, Object attributeValue) {

            Map<String, Object> sessionAttributes = getMap();

            Object previousAttributeValue = sessionAttributes.put(attributeName, attributeValue);

            this.delta |= getIsDirtyPredicate().isDirty(previousAttributeValue, attributeValue)
                    && sessionAttributesChangeInterceptor().apply(attributeName, attributeValue);

            return previousAttributeValue;
        }

        public Object removeAttribute(String attributeName) {

            synchronized (getLock()) {

                Map<String, Object> sessionAttributes = getMap();

                this.delta |= sessionAttributes.containsKey(attributeName)
                        && sessionAttributesChangeInterceptor().apply(attributeName, null);

                return sessionAttributes.remove(attributeName);
            }
        }

        @SuppressWarnings("unchecked")
        public <T> T getAttribute(String attributeName) {

            synchronized (getLock()) {
                return (T) getMap().get(attributeName);
            }
        }

        public Set<String> getAttributeNames() {

            synchronized (getLock()) {
                return Collections.unmodifiableSet(getMap().keySet());
            }
        }

        @Override
        @SuppressWarnings("all")
        public Set<Entry<String, Object>> entrySet() {

            synchronized (getLock()) {

                return new AbstractSet<Entry<String, Object>>() {

                    @Override
                    public Iterator<Entry<String, Object>> iterator() {
                        return Collections.unmodifiableMap(GemFireSessionAttributes.this.getMap()).entrySet()
                                .iterator();
                    }

                    @Override
                    public int size() {
                        return GemFireSessionAttributes.this.getMap().size();
                    }
                };
            }
        }

        protected BiFunction<String, Object, Boolean> sessionAttributesChangeInterceptor() {
            return (attributeName, attributeValue) -> true;
        }

        protected void commit() {

            synchronized (getLock()) {
                this.delta = false;
            }
        }

        @SuppressWarnings("unchecked")
        public <T extends GemFireSessionAttributes> T configureWith(IsDirtyPredicate dirtyPredicate) {
            setIsDirtyPredicate(dirtyPredicate);
            return (T) this;
        }

        public void from(Session session) {

            synchronized (getLock()) {
                session.getAttributeNames()
                        .forEach(attributeName -> setAttribute(attributeName, session.getAttribute(attributeName)));
            }
        }

        public void from(Map<String, Object> map) {

            synchronized (getLock()) {
                map.forEach(this::setAttribute);
            }
        }

        public void from(GemFireSessionAttributes sessionAttributes) {

            synchronized (getLock()) {
                sessionAttributes.getAttributeNames().forEach(attributeName -> setAttribute(attributeName,
                        sessionAttributes.getAttribute(attributeName)));
            }
        }

        public boolean hasDelta() {

            synchronized (getLock()) {
                return this.delta;
            }

        }

        @Override
        public String toString() {

            synchronized (getLock()) {
                return getMap().toString();
            }
        }
    }

    protected static class SessionEventHandlerCacheListenerAdapter extends CacheListenerAdapter<Object, Session> {

        private final AbstractGemFireOperationsSessionRepository sessionRepository;

        private final Set<Integer> cachedSessionIds = new ConcurrentSkipListSet<>();

        /**
         * Constructs a new instance of the {@link SessionEventHandlerCacheListenerAdapter} initialized with
         * the given {@link AbstractGemFireOperationsSessionRepository}.
         *
         * @param sessionRepository {@link AbstractGemFireOperationsSessionRepository} used by this event handler
         * to manage {@link AbstractSessionEvent Session Events}.
         * @throws IllegalArgumentException if {@link AbstractGemFireOperationsSessionRepository} is {@literal null}.
         * @see org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository
         */
        protected SessionEventHandlerCacheListenerAdapter(
                AbstractGemFireOperationsSessionRepository sessionRepository) {

            Assert.notNull(sessionRepository, "SessionRepository is required");

            this.sessionRepository = sessionRepository;
        }

        /**
         * Returns a reference to the configured {@link SessionRepository}.
         *
         * @return a reference to the configured {@link SessionRepository}.
         * @see org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository
         */
        protected @NonNull AbstractGemFireOperationsSessionRepository getSessionRepository() {
            return this.sessionRepository;
        }

        /**
         * Callback method triggered when an entry is created (put) in the {@link Session} cache {@link Region}.
         *
         * @param event {@link EntryEvent} containing the details of the cache operation.
         * @see org.springframework.session.events.SessionCreatedEvent
         * @see org.springframework.session.Session
         * @see org.apache.geode.cache.EntryEvent
         * @see #newSessionCreatedEvent(Session)
         * @see #publishEvent(ApplicationEvent)
         * @see #toSession(Object, Object)
         * @see #forget(Object)
         */
        @Override
        public void afterCreate(EntryEvent<Object, Session> event) {

            Optional.ofNullable(event).filter(this::remember).ifPresent(it -> getSessionRepository()
                    .publishEvent(newSessionCreatedEvent(toSession(it.getNewValue(), it.getKey()))));
        }

        /**
         * 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 #toSession(Object, Object)
         * @see #forget(Object)
         */
        protected void afterDelete(String sessionId, Session session) {

            forget(sessionId);
            getSessionRepository().publishEvent(newSessionDeletedEvent(toSession(session, sessionId)));
        }

        /**
         * Callback method triggered when an entry is destroyed (removed) in the {@link Session} cache {@link Region}.
         *
         * @param event {@link EntryEvent} containing the details of the cache operation.
         * @see org.springframework.session.events.SessionDestroyedEvent
         * @see org.springframework.session.Session
         * @see org.apache.geode.cache.EntryEvent
         * @see #newSessionDestroyedEvent(Session)
         * @see #publishEvent(ApplicationEvent)
         * @see #toSession(Object, Object)
         * @see #forget(Object)
         */
        @Override
        public void afterDestroy(EntryEvent<Object, Session> event) {

            Optional.ofNullable(event).filter(this::forget).ifPresent(it -> getSessionRepository()
                    .publishEvent(newSessionDestroyedEvent(toSession(event.getOldValue(), it.getKey()))));
        }

        /**
         * Callback method triggered when an entry is invalidated (expired) in the {@link Session} cache {@link Region}.
         *
         * @param event {@link EntryEvent} containing the details of the cache operation.
         * @see org.springframework.session.events.SessionExpiredEvent
         * @see org.springframework.session.Session
         * @see org.apache.geode.cache.EntryEvent
         * @see #newSessionExpiredEvent(Session)
         * @see #publishEvent(ApplicationEvent)
         * @see #toSession(Object, Object)
         * @see #forget(Object)
         */
        @Override
        public void afterInvalidate(EntryEvent<Object, Session> event) {

            Optional.ofNullable(event).filter(this::forget).ifPresent(it -> getSessionRepository()
                    .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<Object, Session> 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.
         *
         * @param session {@link Session} that is the subject of the {@link AbstractSessionEvent event}.
         * @return a new {@link SessionCreatedEvent}.
         * @see org.springframework.session.events.SessionCreatedEvent
         * @see org.springframework.session.Session
         * @see #getSessionRepository()
         */
        protected SessionCreatedEvent newSessionCreatedEvent(Session session) {
            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.
         *
         * @param session {@link Session} that is the subject of the {@link AbstractSessionEvent event}.
         * @return a new {@link SessionDeletedEvent}.
         * @see org.springframework.session.events.SessionDeletedEvent
         * @see org.springframework.session.Session
         * @see #getSessionRepository()
         */
        protected SessionDeletedEvent newSessionDeletedEvent(Session session) {
            return new SessionDeletedEvent(getSessionRepository(), session);
        }

        /**
         * Constructs a new {@link SessionDestroyedEvent} 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 AbstractSessionEvent event}.
         * @return a new {@link SessionDestroyedEvent}.
         * @see org.springframework.session.events.SessionDestroyedEvent
         * @see org.springframework.session.Session
         * @see #getSessionRepository()
         */
        protected SessionDestroyedEvent newSessionDestroyedEvent(Session session) {
            return new SessionDestroyedEvent(getSessionRepository(), session);
        }

        /**
         * Constructs a new {@link SessionExpiredEvent} 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 AbstractSessionEvent event}.
         * @return a new {@link SessionExpiredEvent}.
         * @see org.springframework.session.events.SessionExpiredEvent
         * @see org.springframework.session.Session
         * @see #getSessionRepository()
         */
        protected SessionExpiredEvent newSessionExpiredEvent(Session session) {
            return new SessionExpiredEvent(getSessionRepository(), session);
        }

        Set<Integer> getCachedSessionIds() {
            return this.cachedSessionIds;
        }

        /**
         * Determines whether the given {@link Session#getId() Session ID} has been remembered.
         *
         * @param sessionId {@link Object Session ID} to evaluate.
         * @return return a boolean value determining whether the given {@link Session#getId() Session ID}
         * has been remembered.
         * @see #getCachedSessionIds()
         */
        protected boolean isRemembered(Object sessionId) {
            return getCachedSessionIds().contains(ObjectUtils.nullSafeHashCode(sessionId));
        }

        /**
         * Forgets the {@link EntryEvent#getKey() Key} contained in the given {@link EntryEvent}
         * as a {@link Session#getId() Session ID}.
         *
         * @param entryEvent {@link EntryEvent} to evaluate.
         * @return {@literal true} if the {@link EntryEvent#getKey() Key} contained in the given {@link EntryEvent}
         * was forgotten as a {@link Session#getId() Session ID}.
         * @see org.springframework.session.Session
         * @see org.apache.geode.cache.EntryEvent
         * @see #forget(Object)
         */
        protected boolean forget(EntryEvent<Object, Session> entryEvent) {

            return Optional.ofNullable(entryEvent).map(EntryEvent::getKey).map(this::forget).orElse(false);
        }

        /**
         * Forgets the given {@link Object Session ID}.
         *
         * @param sessionId {@link Object} containing the {@link Session#getId() Session ID} to forget.
         * @return a boolean value indicating whether the given {@link Session#getId() Session ID} was forgotten.
         * @see #getCachedSessionIds()
         * @see #remember(Object)
         */
        protected boolean forget(Object sessionId) {
            return getCachedSessionIds().remove(ObjectUtils.nullSafeHashCode(sessionId));
        }

        /**
         * Remembers the {@link EntryEvent#getKey() Key} contained by the given {@link EntryEvent}
         * iff the {@link EntryEvent#getKey() Key} is a valid {@link Session#getId() Session ID}
         * and the {@link EntryEvent#getNewValue() new value} is a {@link Session}.
         *
         * @param entryEvent {@link EntryEvent} to evaluate.
         * @return {@literal true} if the {@link EntryEvent#getKey() Key} of the given {@link EntryEvent}
         * is a valid {@link Session#getId() Session ID}.
         * @see SessionUtils#isValidSessionId(Object)
         * @see #isSession(EntryEvent)
         * @see #remember(Object)
         * @see org.springframework.session.Session
         * @see org.apache.geode.cache.EntryEvent
         */
        protected boolean remember(EntryEvent<Object, Session> entryEvent) {

            return Optional.ofNullable(entryEvent).filter(this::isSession).map(EntryEvent::getKey)
                    .filter(SessionUtils::isValidSessionId).map(this::remember).orElse(false);
        }

        /**
         * Remembers the given {@link Object Session ID}.
         *
         * @param sessionId {@link Object} containing the {@link Session#getId() Session ID} to remember.
         * @return a boolean value indicating whether Spring Session is interested in and will remember
         * the given {@link Session#getId() Session ID}.
         * @see #getCachedSessionIds()
         * @see #forget(Object)
         */
        protected boolean remember(Object sessionId) {
            return getCachedSessionIds().add(ObjectUtils.nullSafeHashCode(sessionId));
        }

        /**
         * Determines whether the {@link EntryEvent#getNewValue() new value} contained in the {@link EntryEvent}
         * is a {@link Session}.
         *
         * @param entryEvent {@link EntryEvent} to evaluate.
         * @return a boolean value indicating whether the {@link EntryEvent#getNewValue() new value}
         * contained in the {@link EntryEvent} is a {@link Session}.
         * @see org.springframework.session.Session
         * @see org.apache.geode.cache.EntryEvent
         */
        protected boolean isSession(EntryEvent<?, ?> entryEvent) {

            return Optional.ofNullable(entryEvent).map(EntryEvent::getNewValue).filter(Session.class::isInstance)
                    .isPresent();
        }

        /**
         * Determines whether the given {@link Object} is a {@link Session}.
         *
         * @param target {@link Object} to evaluate.
         * @return a boolean value determining whether the given {@link Object} is a {@link Session}.
         * @see org.springframework.session.Session
         */
        protected boolean isSession(Object target) {
            return target instanceof Session;
        }

        /**
         * 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 target {@link Object} to evaluate as a {@link Session}.
         * @param sessionId {@link String} containing the {@link Session#getId() 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.
         * @see org.springframework.session.Session
         * @see SessionUtils#isValidSessionId(Object)
         * @see #isSession(Object)
         */
        protected Session toSession(@Nullable Object target, Object sessionId) {

            return isSession(target) ? (Session) target
                    : Optional.ofNullable(sessionId).filter(SessionUtils::isValidSessionId).map(Object::toString)
                            .map(SessionIdHolder::create)
                            .orElseThrow(() -> newIllegalStateException(
                                    "Session or 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;

        /**
         * Constructs a new instance of the {@link SessionIdInterestRegisteringCacheListener} initialized with
         * the {@link AbstractGemFireOperationsSessionRepository}.
         *
         * @param sessionRepository {@link AbstractGemFireOperationsSessionRepository} used by this listener
         * to register and unregister interests in {@link Session Sessions}.
         * @throws IllegalArgumentException if {@link AbstractGemFireOperationsSessionRepository} is {@literal null}.
         * @see org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository
         */
        public SessionIdInterestRegisteringCacheListener(
                AbstractGemFireOperationsSessionRepository sessionRepository) {

            Assert.notNull(sessionRepository, "SessionRepository is required");

            this.sessionRepository = sessionRepository;
        }

        /**
         * Returns a reference to the configured {@link SessionRepository}.
         *
         * @return a reference to the configured {@link SessionRepository}.
         * @see org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository
         */
        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());
        }
    }
}