Initial Commit
diff --git a/modules/spring-sessions/pom.xml b/modules/spring-sessions/pom.xml
new file mode 100644
index 0000000..54f61f7
--- /dev/null
+++ b/modules/spring-sessions/pom.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>apache-ignite-extensions</artifactId>
+ <groupId>org.apache.ignite</groupId>
+ <version>1.0.0-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>spring-sessions</artifactId>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-core</artifactId>
+ <version>${ignite.version}</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-core</artifactId>
+ <version>${spring.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-expression</artifactId>
+ <version>${spring.version}</version>
+ </dependency>
+
+ <!-- https://mvnrepository.com/artifact/org.springframework.session/spring-session-core -->
+ <dependency>
+ <groupId>org.springframework.session</groupId>
+ <artifactId>spring-session-core</artifactId>
+ <version>2.5.0</version>
+ </dependency>
+
+ <!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
+ <dependency>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-context</artifactId>
+ <version>5.3.8</version>
+ </dependency>
+
+ <dependency>
+ <groupId>javax.annotation</groupId>
+ <artifactId>javax.annotation-api</artifactId>
+ <version>1.3.2</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.ignite</groupId>
+ <artifactId>ignite-core</artifactId>
+ <version>2.11.0-SNAPSHOT</version>
+ <scope>compile</scope>
+ </dependency>
+ </dependencies>
+
+
+</project>
\ No newline at end of file
diff --git a/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/EnableIgniteHttpSession.java b/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/EnableIgniteHttpSession.java
new file mode 100644
index 0000000..96fb752
--- /dev/null
+++ b/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/EnableIgniteHttpSession.java
@@ -0,0 +1,98 @@
+package org.apache.ignite.spring.sessions;
+
+/*
+ * Copyright 2014-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.ignite.Ignite;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.session.FlushMode;
+import org.springframework.session.MapSession;
+import org.springframework.session.SaveMode;
+import org.springframework.session.Session;
+import org.springframework.session.SessionRepository;
+import org.springframework.session.web.http.SessionRepositoryFilter;
+
+/**
+ * Add this annotation to an {@code @Configuration} class to expose the
+ * {@link SessionRepositoryFilter} as a bean named {@code springSessionRepositoryFilter}
+ * and backed by Ignite. In order to leverage the annotation, a single {@link Ignite} must
+ * be provided. For example:
+ *
+ * <pre class="code">
+ * @Configuration
+ * @EnableIgniteHttpSession
+ * public class IgniteHttpSessionConfig {
+ *
+ * @Bean
+ * public Ignite embeddedIgnite() {
+ * return IgniteEx.start();
+ * }
+ *
+ * }
+ * </pre>
+ *
+ * More advanced configurations can extend {@link IgniteHttpSessionConfiguration} instead.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Documented
+@Import(IgniteHttpSessionConfiguration.class)
+@Configuration(proxyBeanMethods = false)
+public @interface EnableIgniteHttpSession {
+
+ /**
+ * The session timeout in seconds. By default, it is set to 1800 seconds (30 minutes).
+ * This should be a non-negative integer.
+ * @return the seconds a session can be inactive before expiring
+ */
+ int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
+
+ /**
+ * This is the name of the Map that will be used in Ignite to store the session data.
+ * Default is "spring:session:sessions".
+ * @return the name of the Map to store the sessions in Ignite
+ */
+ String sessionMapName() default "spring:session:sessions";
+
+ /**
+ * Flush mode for the Ignite sessions. The default is {@code ON_SAVE} which only
+ * updates the backing Ignite when {@link SessionRepository#save(Session)} is invoked.
+ * In a web environment this happens just before the HTTP response is committed.
+ * <p>
+ * Setting the value to {@code IMMEDIATE} will ensure that the any updates to the
+ * Session are immediately written to the Ignite instance.
+ * @return the {@link FlushMode} to use
+ * @since 2.2.0
+ */
+ FlushMode flushMode() default FlushMode.ON_SAVE;
+
+ /**
+ * Save mode for the session. The default is {@link SaveMode#ON_SET_ATTRIBUTE}, which
+ * only saves changes made to session.
+ * @return the save mode
+ * @since 2.2.0
+ */
+ SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;
+
+}
\ No newline at end of file
diff --git a/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/IgniteHttpSessionConfiguration.java b/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/IgniteHttpSessionConfiguration.java
new file mode 100644
index 0000000..690bd8e
--- /dev/null
+++ b/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/IgniteHttpSessionConfiguration.java
@@ -0,0 +1,146 @@
+package org.apache.ignite.spring.sessions;
+
+/*
+ * Copyright 2014-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.ignite.Ignite;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.ImportAware;
+import org.springframework.core.annotation.AnnotationAttributes;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.session.FlushMode;
+import org.springframework.session.IndexResolver;
+import org.springframework.session.MapSession;
+import org.springframework.session.SaveMode;
+import org.springframework.session.Session;
+import org.springframework.session.SessionRepository;
+import org.springframework.session.config.SessionRepositoryCustomizer;
+import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
+import org.springframework.session.web.http.SessionRepositoryFilter;
+import org.springframework.util.StringUtils;
+
+/**
+ * Exposes the {@link SessionRepositoryFilter} as a bean named
+ * {@code springSessionRepositoryFilter}. In order to use this a single {@link Ignite}
+ * must be exposed as a Bean.
+ */
+@Configuration(proxyBeanMethods = false)
+public class IgniteHttpSessionConfiguration extends SpringHttpSessionConfiguration implements ImportAware {
+
+ private Integer maxInactiveIntervalInSeconds = MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS;
+
+ private String sessionMapName = IgniteIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME;
+
+ private FlushMode flushMode = FlushMode.ON_SAVE;
+
+ private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
+
+ private Ignite ignite;
+
+ private ApplicationEventPublisher applicationEventPublisher;
+
+ private IndexResolver<Session> indexResolver;
+
+ private List<SessionRepositoryCustomizer<IgniteIndexedSessionRepository>> sessionRepositoryCustomizers;
+
+ @Bean
+ public SessionRepository<?> sessionRepository() {
+ return createIgniteIndexedSessionRepository();
+ }
+
+ public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
+ this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
+ }
+
+ public void setSessionMapName(String sessionMapName) {
+ this.sessionMapName = sessionMapName;
+ }
+
+ public void setFlushMode(FlushMode flushMode) {
+ this.flushMode = flushMode;
+ }
+
+ public void setSaveMode(SaveMode saveMode) {
+ this.saveMode = saveMode;
+ }
+
+ @Autowired
+ public void setIgnite(@SpringSessionIgnite ObjectProvider<Ignite> springSessionIgnite,
+ ObjectProvider<Ignite> ignite) {
+ Ignite igniteToUse = springSessionIgnite.getIfAvailable();
+ if (igniteToUse == null) {
+ igniteToUse = ignite.getObject();
+ }
+ this.ignite = igniteToUse;
+ }
+
+ @Autowired
+ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
+ this.applicationEventPublisher = applicationEventPublisher;
+ }
+
+ @Autowired(required = false)
+ public void setIndexResolver(IndexResolver<Session> indexResolver) {
+ this.indexResolver = indexResolver;
+ }
+
+ @Autowired(required = false)
+ public void setSessionRepositoryCustomizer(
+ ObjectProvider<SessionRepositoryCustomizer<IgniteIndexedSessionRepository>> sessionRepositoryCustomizers) {
+ this.sessionRepositoryCustomizers = sessionRepositoryCustomizers.orderedStream().collect(Collectors.toList());
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void setImportMetadata(AnnotationMetadata importMetadata) {
+ Map<String, Object> attributeMap = importMetadata
+ .getAnnotationAttributes(EnableIgniteHttpSession.class.getName());
+ AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap);
+ this.maxInactiveIntervalInSeconds = attributes.getNumber("maxInactiveIntervalInSeconds");
+ String sessionMapNameValue = attributes.getString("sessionMapName");
+ if (StringUtils.hasText(sessionMapNameValue)) {
+ this.sessionMapName = sessionMapNameValue;
+ }
+ this.flushMode = attributes.getEnum("flushMode");
+ this.saveMode = attributes.getEnum("saveMode");
+ }
+
+ private IgniteIndexedSessionRepository createIgniteIndexedSessionRepository() {
+ IgniteIndexedSessionRepository sessionRepository = new IgniteIndexedSessionRepository(this.ignite);
+ sessionRepository.setApplicationEventPublisher(this.applicationEventPublisher);
+ if (this.indexResolver != null) {
+ sessionRepository.setIndexResolver(this.indexResolver);
+ }
+ if (StringUtils.hasText(this.sessionMapName)) {
+ sessionRepository.setSessionMapName(this.sessionMapName);
+ }
+ sessionRepository.setDefaultMaxInactiveInterval(this.maxInactiveIntervalInSeconds);
+ sessionRepository.setFlushMode(this.flushMode);
+ sessionRepository.setSaveMode(this.saveMode);
+ this.sessionRepositoryCustomizers
+ .forEach((sessionRepositoryCustomizer) -> sessionRepositoryCustomizer.customize(sessionRepository));
+ return sessionRepository;
+ }
+
+}
diff --git a/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/IgniteIndexedSessionRepository.java b/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/IgniteIndexedSessionRepository.java
new file mode 100644
index 0000000..8ddfb2f
--- /dev/null
+++ b/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/IgniteIndexedSessionRepository.java
@@ -0,0 +1,544 @@
+package org.apache.ignite.spring.sessions;
+
+/*
+ * Copyright 2014-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.cache.configuration.CacheEntryListenerConfiguration;
+import javax.cache.configuration.Factory;
+import javax.cache.event.CacheEntryCreatedListener;
+import javax.cache.event.CacheEntryEvent;
+import javax.cache.event.CacheEntryEventFilter;
+import javax.cache.event.CacheEntryExpiredListener;
+import javax.cache.event.CacheEntryListener;
+import javax.cache.event.CacheEntryListenerException;
+import javax.cache.event.CacheEntryRemovedListener;
+import javax.cache.expiry.TouchedExpiryPolicy;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgniteCache;
+import org.apache.ignite.cache.query.FieldsQueryCursor;
+import org.apache.ignite.cache.query.SqlFieldsQuery;
+import org.apache.ignite.cache.query.annotations.QuerySqlField;
+import org.apache.ignite.configuration.CacheConfiguration;
+import org.apache.ignite.internal.GridDirectTransient;
+
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.session.DelegatingIndexResolver;
+import org.springframework.session.FindByIndexNameSessionRepository;
+import org.springframework.session.FlushMode;
+import org.springframework.session.IndexResolver;
+import org.springframework.session.MapSession;
+import org.springframework.session.PrincipalNameIndexResolver;
+import org.springframework.session.SaveMode;
+import org.springframework.session.Session;
+import org.springframework.session.events.AbstractSessionEvent;
+import org.springframework.session.events.SessionCreatedEvent;
+import org.springframework.session.events.SessionDeletedEvent;
+import org.springframework.session.events.SessionExpiredEvent;
+import org.springframework.util.Assert;
+
+/**
+ * A {@link org.springframework.session.SessionRepository} implementation that stores
+ * sessions in Apache Ignite distributed {@link IgniteCache}.
+ *
+ * <p>
+ * An example of how to create a new instance can be seen below:
+ *
+ * <pre class="code">
+ * IgniteConfiguration config = new IgniteConfiguration();
+ *
+ * // ... configure Ignite ...
+ *
+ * Ignite ignite = IgnitionEx.start(config);
+ *
+ * IgniteIndexedSessionRepository sessionRepository =
+ * new IgniteIndexedSessionRepository(ignite);
+ * </pre>
+ *
+ * In order to support finding sessions by principal name using
+ * {@link #findByIndexNameAndIndexValue(String, String)} method, custom configuration of
+ * {@link IgniteCache} supplied to this implementation is required.
+ *
+ * This implementation listens for events on the Ignite-backed SessionRepository and
+ * translates those events into the corresponding Spring Session events. Publish the
+ * Spring Session events with the given {@link ApplicationEventPublisher}.
+ *
+ * <ul>
+ * <li>entryAdded - {@link SessionCreatedEvent}</li>
+ * <li>entryEvicted - {@link SessionExpiredEvent}</li>
+ * <li>entryRemoved - {@link SessionDeletedEvent}</li>
+ * </ul>
+ *
+ */
+public class IgniteIndexedSessionRepository
+ implements FindByIndexNameSessionRepository<IgniteIndexedSessionRepository.IgniteSession>,
+ CacheEntryCreatedListener<String, IgniteIndexedSessionRepository.IgniteSession>,
+ CacheEntryRemovedListener<String, IgniteIndexedSessionRepository.IgniteSession>,
+ CacheEntryExpiredListener<String, IgniteIndexedSessionRepository.IgniteSession> {
+
+ /**
+ * The default name of map used by Spring Session to store sessions.
+ */
+ public static final String DEFAULT_SESSION_MAP_NAME = "spring:session:sessions";
+
+ /**
+ * The principal name custom attribute name.
+ */
+ public static final String PRINCIPAL_NAME_ATTRIBUTE = "principalName";
+
+ private static final String SPRING_SECURITY_CONTEXT = "SPRING_SECURITY_CONTEXT";
+
+ private static final Log logger = LogFactory.getLog(IgniteIndexedSessionRepository.class);
+
+ private final Ignite ignite;
+
+ private ApplicationEventPublisher eventPublisher = (event) -> {
+ };
+
+ /**
+ * If non-null, this value is used to override
+ * {@link MapSession#setMaxInactiveInterval(Duration)}.
+ */
+ private Integer defaultMaxInactiveInterval;
+
+ private IndexResolver<Session> indexResolver = new DelegatingIndexResolver<>(new PrincipalNameIndexResolver<>());
+
+ private String sessionMapName = DEFAULT_SESSION_MAP_NAME;
+
+ private FlushMode flushMode = FlushMode.ON_SAVE;
+
+ private SaveMode saveMode = SaveMode.ON_SET_ATTRIBUTE;
+
+ private IgniteCache<String, IgniteSession> sessions;
+
+ private CacheEntryListenerConfiguration<String, IgniteSession> listenerConfiguration;
+
+ /**
+ * Create a new {@link IgniteIndexedSessionRepository} instance.
+ * @param ignite the {@link Ignite} instance to use for managing sessions
+ */
+ public IgniteIndexedSessionRepository(Ignite ignite) {
+ Assert.notNull(ignite, "Ignite must not be null");
+ this.ignite = ignite;
+ }
+
+ @PostConstruct
+ public void init() {
+ final CacheConfiguration<String, IgniteSession> configuration = new CacheConfiguration<String, IgniteSession>(
+ this.sessionMapName).setIndexedTypes(String.class, IgniteSession.class);
+
+ if (this.defaultMaxInactiveInterval != null) {
+ configuration.setExpiryPolicyFactory(TouchedExpiryPolicy
+ .factoryOf(new javax.cache.expiry.Duration(TimeUnit.SECONDS, this.defaultMaxInactiveInterval)));
+ }
+
+ this.sessions = this.ignite.getOrCreateCache(configuration);
+
+ this.listenerConfiguration = new CacheEntryListenerConfiguration<String, IgniteSession>() {
+ @Override
+ public Factory<CacheEntryListener<? super String, ? super IgniteSession>> getCacheEntryListenerFactory() {
+ return (Factory<CacheEntryListener<? super String, ? super IgniteSession>>) () -> IgniteIndexedSessionRepository.this;
+ }
+
+ @Override
+ public boolean isOldValueRequired() {
+ return true;
+ }
+
+ @Override
+ public Factory<CacheEntryEventFilter<? super String, ? super IgniteSession>> getCacheEntryEventFilterFactory() {
+ return null;
+ }
+
+ @Override
+ public boolean isSynchronous() {
+ return false;
+ }
+ };
+ this.sessions.registerCacheEntryListener(this.listenerConfiguration);
+ }
+
+ @PreDestroy
+ public void close() {
+ this.sessions.deregisterCacheEntryListener(this.listenerConfiguration);
+ }
+
+ /**
+ * Sets the {@link ApplicationEventPublisher} that is used to publish
+ * {@link AbstractSessionEvent session events}. The default is to not publish session
+ * events.
+ * @param applicationEventPublisher the {@link ApplicationEventPublisher} that is used
+ * to publish session events. Cannot be null.
+ */
+ public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
+ Assert.notNull(applicationEventPublisher, "ApplicationEventPublisher cannot be null");
+ this.eventPublisher = applicationEventPublisher;
+ }
+
+ /**
+ * Set the maximum inactive interval in seconds between requests before newly created
+ * sessions will be invalidated. A negative time indicates that the session will never
+ * timeout. The default is 1800 (30 minutes).
+ * @param defaultMaxInactiveInterval the maximum inactive interval in seconds
+ */
+ public void setDefaultMaxInactiveInterval(Integer defaultMaxInactiveInterval) {
+ this.defaultMaxInactiveInterval = defaultMaxInactiveInterval;
+ }
+
+ /**
+ * Set the {@link IndexResolver} to use.
+ * @param indexResolver the index resolver
+ */
+ public void setIndexResolver(IndexResolver<Session> indexResolver) {
+ Assert.notNull(indexResolver, "indexResolver cannot be null");
+ this.indexResolver = indexResolver;
+ }
+
+ /**
+ * Set the name of map used to store sessions.
+ * @param sessionMapName the session map name
+ */
+ public void setSessionMapName(String sessionMapName) {
+ Assert.hasText(sessionMapName, "Map name must not be empty");
+ this.sessionMapName = sessionMapName;
+ }
+
+ /**
+ * Sets the flush mode. Default flush mode is {@link FlushMode#ON_SAVE}.
+ * @param flushMode the new flush mode
+ */
+ public void setFlushMode(FlushMode flushMode) {
+ Assert.notNull(flushMode, "flushMode cannot be null");
+ this.flushMode = flushMode;
+ }
+
+ /**
+ * Set the save mode.
+ * @param saveMode the save mode
+ */
+ public void setSaveMode(SaveMode saveMode) {
+ Assert.notNull(saveMode, "saveMode must not be null");
+ this.saveMode = saveMode;
+ }
+
+ @Override
+ public IgniteSession createSession() {
+ MapSession cached = new MapSession();
+ if (this.defaultMaxInactiveInterval != null) {
+ cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
+ }
+ IgniteSession session = new IgniteSession(cached, true);
+ session.flushImmediateIfNecessary();
+ return session;
+ }
+
+ @Override
+ public void save(IgniteSession session) {
+ if (session.isNew) {
+ ttlSessions(session.getMaxInactiveInterval()).put(session.getId(), session);
+ }
+ else if (session.sessionIdChanged) {
+ this.sessions.remove(session.originalId);
+ session.originalId = session.getId();
+ ttlSessions(session.getMaxInactiveInterval()).put(session.getId(), session);
+ }
+ else if (session.hasChanges()) {
+ if (session.maxInactiveIntervalChanged) {
+ ttlSessions(session.getMaxInactiveInterval()).replace(session.getId(), session);
+ }
+ else {
+ this.sessions.replace(session.getId(), session);
+ }
+ }
+ session.clearChangeFlags();
+ }
+
+ @Override
+ public IgniteSession findById(String id) {
+ IgniteSession saved = this.sessions.get(id);
+ if (saved == null) {
+ return null;
+ }
+ if (saved.isExpired()) {
+ deleteById(saved.getId());
+ return null;
+ }
+ saved.isNew = false;
+ return saved;
+ }
+
+ @Override
+ public void deleteById(String id) {
+ this.sessions.remove(id);
+ }
+
+ @Override
+ public Map<String, IgniteSession> findByIndexNameAndIndexValue(String indexName, String indexValue) {
+ if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) {
+ return Collections.emptyMap();
+ }
+ final FieldsQueryCursor<List<?>> cursor = this.sessions
+ .query(new SqlFieldsQuery("SELECT * FROM IgniteSession WHERE principal='" + indexValue + "'"));
+
+ if (cursor == null) {
+ return Collections.emptyMap();
+ }
+
+ final List<List<?>> sessions = cursor.getAll();
+
+ Map<String, IgniteSession> sessionMap = new HashMap<>(sessions.size());
+ sessions.forEach((List<?> res) -> {
+ final MapSession session = (MapSession) res.get(0);
+ final IgniteSession value = new IgniteSession(session, false);
+ value.principal = (String) res.get(1);
+ sessionMap.put(session.getId(), value);
+ });
+
+ return sessionMap;
+ }
+
+ @Override
+ public void onCreated(Iterable<CacheEntryEvent<? extends String, ? extends IgniteSession>> events)
+ throws CacheEntryListenerException {
+ events.forEach((event) -> {
+ IgniteSession session = event.getValue();
+ if (session.getId().equals(session.getDelegate().getOriginalId())) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Session created with id: " + session.getId());
+ }
+ this.eventPublisher.publishEvent(new SessionCreatedEvent(this, session));
+ }
+ });
+ }
+
+ @Override
+ public void onExpired(Iterable<CacheEntryEvent<? extends String, ? extends IgniteSession>> events)
+ throws CacheEntryListenerException {
+ events.forEach((event) -> {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Session expired with id: " + event.getOldValue().getId());
+ }
+ this.eventPublisher.publishEvent(new SessionExpiredEvent(this, event.getOldValue()));
+ });
+ }
+
+ @Override
+ public void onRemoved(Iterable<CacheEntryEvent<? extends String, ? extends IgniteSession>> events)
+ throws CacheEntryListenerException {
+ events.forEach((event) -> {
+ IgniteSession session = event.getOldValue();
+ if (session != null) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Session deleted with id: " + session.getId());
+ }
+ this.eventPublisher.publishEvent(new SessionDeletedEvent(this, session));
+ }
+ });
+ }
+
+ /**
+ * Get cache view with custom duration expiry policy.
+ * @param duration expiry duration for IgniteSession.
+ * @return cache with custom duration expiry policy.
+ */
+ private IgniteCache<String, IgniteSession> ttlSessions(Duration duration) {
+ return this.sessions.withExpiryPolicy(createPolicy(duration));
+ }
+
+ /**
+ * Create expiry policy from {@link Duration}.
+ * @param duration expiry duration.
+ * @return expiry policy.
+ */
+ private static TouchedExpiryPolicy createPolicy(Duration duration) {
+ return new TouchedExpiryPolicy(new javax.cache.expiry.Duration(TimeUnit.SECONDS, duration.getSeconds()));
+ }
+
+ /**
+ * A custom implementation of {@link Session} that uses a {@link MapSession} as the
+ * basis for its mapping. It keeps track if changes have been made since last save.
+ */
+ final class IgniteSession implements Session {
+
+ @QuerySqlField
+ private final MapSession delegate;
+
+ @GridDirectTransient
+ private boolean isNew;
+
+ @GridDirectTransient
+ private boolean sessionIdChanged;
+
+ @GridDirectTransient
+ private boolean lastAccessedTimeChanged;
+
+ @GridDirectTransient
+ private boolean maxInactiveIntervalChanged;
+
+ @GridDirectTransient
+ private String originalId;
+
+ @GridDirectTransient
+ private Map<String, Object> delta = new HashMap<>();
+
+ @QuerySqlField(index = true)
+ private String principal;
+
+ IgniteSession(MapSession cached, boolean isNew) {
+ this.delegate = cached;
+ this.isNew = isNew;
+ this.originalId = cached.getId();
+ if (this.isNew || (IgniteIndexedSessionRepository.this.saveMode == SaveMode.ALWAYS)) {
+ getAttributeNames()
+ .forEach((attributeName) -> this.delta.put(attributeName, cached.getAttribute(attributeName)));
+ }
+ }
+
+ @Override
+ public void setLastAccessedTime(Instant lastAccessedTime) {
+ this.delegate.setLastAccessedTime(lastAccessedTime);
+ this.lastAccessedTimeChanged = true;
+ flushImmediateIfNecessary();
+ }
+
+ @Override
+ public boolean isExpired() {
+ return this.delegate.isExpired();
+ }
+
+ @Override
+ public Instant getCreationTime() {
+ return this.delegate.getCreationTime();
+ }
+
+ @Override
+ public String getId() {
+ return this.delegate.getId();
+ }
+
+ @Override
+ public String changeSessionId() {
+ String newSessionId = this.delegate.changeSessionId();
+ this.sessionIdChanged = true;
+ return newSessionId;
+ }
+
+ @Override
+ public Instant getLastAccessedTime() {
+ return this.delegate.getLastAccessedTime();
+ }
+
+ @Override
+ public void setMaxInactiveInterval(Duration interval) {
+ this.delegate.setMaxInactiveInterval(interval);
+ this.maxInactiveIntervalChanged = true;
+ flushImmediateIfNecessary();
+ }
+
+ @Override
+ public Duration getMaxInactiveInterval() {
+ return this.delegate.getMaxInactiveInterval();
+ }
+
+ @Override
+ public <T> T getAttribute(String attributeName) {
+ T attributeValue = this.delegate.getAttribute(attributeName);
+ if (attributeValue != null
+ && IgniteIndexedSessionRepository.this.saveMode.equals(SaveMode.ON_GET_ATTRIBUTE)) {
+ this.delta.put(attributeName, attributeValue);
+ }
+ return attributeValue;
+ }
+
+ @Override
+ public Set<String> getAttributeNames() {
+ return this.delegate.getAttributeNames();
+ }
+
+ @Override
+ public void setAttribute(String attributeName, Object attributeValue) {
+ this.delegate.setAttribute(attributeName, attributeValue);
+ this.delta.put(attributeName, attributeValue);
+ if (SPRING_SECURITY_CONTEXT.equals(attributeName)) {
+ Map<String, String> indexes = IgniteIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
+ String principal = (attributeValue != null) ? indexes.get(PRINCIPAL_NAME_INDEX_NAME) : null;
+ this.delegate.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal);
+ this.principal = principal;
+ }
+ flushImmediateIfNecessary();
+ }
+
+ @Override
+ public void removeAttribute(String attributeName) {
+ setAttribute(attributeName, null);
+ }
+
+ MapSession getDelegate() {
+ return this.delegate;
+ }
+
+ boolean hasChanges() {
+ return (this.lastAccessedTimeChanged || this.maxInactiveIntervalChanged || !this.delta.isEmpty());
+ }
+
+ void clearChangeFlags() {
+ this.isNew = false;
+ this.lastAccessedTimeChanged = false;
+ this.sessionIdChanged = false;
+ this.maxInactiveIntervalChanged = false;
+ this.delta.clear();
+ }
+
+ private void flushImmediateIfNecessary() {
+ if (IgniteIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
+ IgniteIndexedSessionRepository.this.save(this);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ IgniteSession session = (IgniteSession) o;
+ return this.delegate.equals(session.delegate);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.delegate);
+ }
+
+ }
+
+}
diff --git a/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/SessionEventRegistry.java b/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/SessionEventRegistry.java
new file mode 100644
index 0000000..c5c6479
--- /dev/null
+++ b/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/SessionEventRegistry.java
@@ -0,0 +1,74 @@
+package org.apache.ignite.spring.sessions;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.springframework.context.ApplicationListener;
+import org.springframework.session.events.AbstractSessionEvent;
+
+/**
+ *
+ */
+class SessionEventRegistry implements ApplicationListener<AbstractSessionEvent> {
+
+ private Map<String, AbstractSessionEvent> events = new HashMap<>();
+
+ private ConcurrentMap<String, Object> locks = new ConcurrentHashMap<>();
+
+ @Override
+ public void onApplicationEvent(AbstractSessionEvent event) {
+ String sessionId = event.getSessionId();
+ this.events.put(sessionId, event);
+ Object lock = getLock(sessionId);
+ synchronized (lock) {
+ lock.notifyAll();
+ }
+ }
+
+ void clear() {
+ this.events.clear();
+ this.locks.clear();
+ }
+
+ boolean receivedEvent(String sessionId) throws InterruptedException {
+ return waitForEvent(sessionId) != null;
+ }
+
+ <E extends AbstractSessionEvent> E getEvent(String sessionId) throws InterruptedException {
+ return (E) waitForEvent(sessionId);
+ }
+
+ private <E extends AbstractSessionEvent> E waitForEvent(String sessionId) throws InterruptedException {
+ Object lock = getLock(sessionId);
+ synchronized (lock) {
+ if (!this.events.containsKey(sessionId)) {
+ lock.wait(10000);
+ }
+ }
+ return (E) this.events.get(sessionId);
+ }
+
+ private Object getLock(String sessionId) {
+ return this.locks.computeIfAbsent(sessionId, (k) -> new Object());
+ }
+}
\ No newline at end of file
diff --git a/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/SpringSessionIgnite.java b/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/SpringSessionIgnite.java
new file mode 100644
index 0000000..aa1245f
--- /dev/null
+++ b/modules/spring-sessions/src/main/java/org/apache/ignite/spring/sessions/SpringSessionIgnite.java
@@ -0,0 +1,39 @@
+package org.apache.ignite.spring.sessions;
+
+/*
+ * Copyright 2014-2020 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.apache.ignite.Ignite;
+import org.springframework.beans.factory.annotation.Qualifier;
+
+/**
+ * Qualifier annotation for a {@link Ignite} to be injected in
+ * {@link IgniteIndexedSessionRepository}.
+ *
+ */
+@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Qualifier
+public @interface SpringSessionIgnite {
+
+}
diff --git a/pom.xml b/pom.xml
index 8682549..481d986 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,7 @@
<module>modules/performance-statistics-ext</module>
<module>modules/spring-tx-ext</module>
<module>modules/spring-cache-ext</module>
+ <module>modules/spring-sessions</module>
</modules>
<profiles>