/* * Copyright 2014-2017 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 * * 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. */ package org.springframework.session.hazelcast; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import com.hazelcast.core.EntryEvent; import com.hazelcast.core.IMap; import com.hazelcast.map.listener.EntryAddedListener; import com.hazelcast.map.listener.EntryEvictedListener; import com.hazelcast.map.listener.EntryRemovedListener; import com.hazelcast.query.Predicates; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.session.ExpiringSession; import org.springframework.session.FindByIndexNameSessionRepository; import org.springframework.session.MapSession; 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 Hazelcast's distributed {@link IMap}. * * <p> * An example of how to create a new instance can be seen below: * * <pre class="code"> * Config config = new Config(); * * // ... configure Hazelcast ... * * HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config); * * IMap{@code <String, MapSession>} sessions = hazelcastInstance * .getMap("spring:session:sessions"); * * HazelcastSessionRepository sessionRepository = * new HazelcastSessionRepository(sessions); * </pre> * * In order to support finding sessions by principal name using * {@link #findByIndexNameAndIndexValue(String, String)} method, custom configuration of * {@code IMap} supplied to this implementation is required. * * The following snippet demonstrates how to define required configuration using * programmatic Hazelcast Configuration: * * <pre class="code"> * MapAttributeConfig attributeConfig = new MapAttributeConfig() * .setName(HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE) * .setExtractor(PrincipalNameExtractor.class.getName()); * * Config config = new Config(); * * config.getMapConfig("spring:session:sessions") * .addMapAttributeConfig(attributeConfig) * .addMapIndexConfig(new MapIndexConfig( * HazelcastSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false)); * * Hazelcast.newHazelcastInstance(config); * </pre> * * This implementation listens for events on the Hazelcast-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> * * @author Vedran Pavic * @author Tommy Ludwig * @author Mark Anderson * @author Aleksandar Stojsavljevic * @since 1.3.0 */ public class HazelcastSessionRepository implements FindByIndexNameSessionRepository<HazelcastSessionRepository.HazelcastSession>, EntryAddedListener<String, MapSession>, EntryEvictedListener<String, MapSession>, EntryRemovedListener<String, MapSession> { /** * The principal name custom attribute name. */ public static final String PRINCIPAL_NAME_ATTRIBUTE = "principalName"; private static final Log logger = LogFactory.getLog(HazelcastSessionRepository.class); private final IMap<String, MapSession> sessions; private HazelcastFlushMode hazelcastFlushMode = HazelcastFlushMode.ON_SAVE; private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() { public void publishEvent(ApplicationEvent event) { } public void publishEvent(Object event) { } }; /** * If non-null, this value is used to override * {@link MapSession#setMaxInactiveIntervalInSeconds(int)}. */ private Integer defaultMaxInactiveInterval; private String sessionListenerId; public HazelcastSessionRepository(IMap<String, MapSession> sessions) { Assert.notNull(sessions, "Sessions IMap must not be null"); this.sessions = sessions; } @PostConstruct private void init() { this.sessionListenerId = this.sessions.addEntryListener(this, true); } @PreDestroy private void close() { this.sessions.removeEntryListener(this.sessionListenerId); } /** * 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; } /** * Sets the Hazelcast flush mode. Default flush mode is * {@link HazelcastFlushMode#ON_SAVE}. * @param hazelcastFlushMode the new Hazelcast flush mode */ public void setHazelcastFlushMode(HazelcastFlushMode hazelcastFlushMode) { Assert.notNull(hazelcastFlushMode, "HazelcastFlushMode cannot be null"); this.hazelcastFlushMode = hazelcastFlushMode; } public HazelcastSession createSession() { HazelcastSession result = new HazelcastSession(); if (this.defaultMaxInactiveInterval != null) { result.setMaxInactiveIntervalInSeconds(this.defaultMaxInactiveInterval); } return result; } public void save(HazelcastSession session) { if (session.isChanged()) { this.sessions.put(session.getId(), session.getDelegate(), session.getMaxInactiveIntervalInSeconds(), TimeUnit.SECONDS); session.markUnchanged(); } } public HazelcastSession getSession(String id) { MapSession saved = this.sessions.get(id); if (saved == null) { return null; } if (saved.isExpired()) { delete(saved.getId()); return null; } return new HazelcastSession(saved); } public void delete(String id) { this.sessions.remove(id); } public Map<String, HazelcastSession> findByIndexNameAndIndexValue( String indexName, String indexValue) { if (!PRINCIPAL_NAME_INDEX_NAME.equals(indexName)) { return Collections.emptyMap(); } Collection<MapSession> sessions = this.sessions.values( Predicates.equal(PRINCIPAL_NAME_ATTRIBUTE, indexValue)); Map<String, HazelcastSession> sessionMap = new HashMap<>( sessions.size()); for (MapSession session : sessions) { sessionMap.put(session.getId(), new HazelcastSession(session)); } return sessionMap; } public void entryAdded(EntryEvent<String, MapSession> event) { if (logger.isDebugEnabled()) { logger.debug("Session created with id: " + event.getValue().getId()); } this.eventPublisher.publishEvent(new SessionCreatedEvent(this, event.getValue())); } public void entryEvicted(EntryEvent<String, MapSession> event) { if (logger.isDebugEnabled()) { logger.debug("Session expired with id: " + event.getOldValue().getId()); } this.eventPublisher .publishEvent(new SessionExpiredEvent(this, event.getOldValue())); } public void entryRemoved(EntryEvent<String, MapSession> event) { if (logger.isDebugEnabled()) { logger.debug("Session deleted with id: " + event.getOldValue().getId()); } this.eventPublisher .publishEvent(new SessionDeletedEvent(this, event.getOldValue())); } /** * 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. * * @author Aleksandar Stojsavljevic */ final class HazelcastSession implements ExpiringSession { private final MapSession delegate; private boolean changed; /** * Creates a new instance ensuring to mark all of the new attributes to be * persisted in the next save operation. */ HazelcastSession() { this(new MapSession()); this.changed = true; flushImmediateIfNecessary(); } /** * Creates a new instance from the provided {@link MapSession}. * @param cached the {@link MapSession} that represents the persisted session that * was retrieved. Cannot be {@code null}. */ HazelcastSession(MapSession cached) { Assert.notNull(cached, "MapSession cannot be null"); this.delegate = cached; } public void setLastAccessedTime(long lastAccessedTime) { this.delegate.setLastAccessedTime(lastAccessedTime); this.changed = true; flushImmediateIfNecessary(); } public boolean isExpired() { return this.delegate.isExpired(); } public long getCreationTime() { return this.delegate.getCreationTime(); } public String getId() { return this.delegate.getId(); } public long getLastAccessedTime() { return this.delegate.getLastAccessedTime(); } public void setMaxInactiveIntervalInSeconds(int interval) { this.delegate.setMaxInactiveIntervalInSeconds(interval); this.changed = true; flushImmediateIfNecessary(); } public int getMaxInactiveIntervalInSeconds() { return this.delegate.getMaxInactiveIntervalInSeconds(); } public <T> T getAttribute(String attributeName) { return this.delegate.getAttribute(attributeName); } public Set<String> getAttributeNames() { return this.delegate.getAttributeNames(); } public void setAttribute(String attributeName, Object attributeValue) { this.delegate.setAttribute(attributeName, attributeValue); this.changed = true; flushImmediateIfNecessary(); } public void removeAttribute(String attributeName) { this.delegate.removeAttribute(attributeName); this.changed = true; flushImmediateIfNecessary(); } boolean isChanged() { return this.changed; } void markUnchanged() { this.changed = false; } MapSession getDelegate() { return this.delegate; } private void flushImmediateIfNecessary() { if (HazelcastSessionRepository.this.hazelcastFlushMode == HazelcastFlushMode.IMMEDIATE) { HazelcastSessionRepository.this.save(this); } } } }