Add GemFire Support

Fixes GH PR #148 and PR #308 implementing a GemFire Adapter to support
clustered HttpSessions in Spring Session.

* Resolve SGF-373 - Implement a Spring Session Adapter for GemFire backing
a HttpSession similar to the Redis support.
* Add Spring Session annotation to enable GemFire support with
@EnableGemFireHttpSession.
* Add extesion of SpringHttpSessionConfiguration to configure GemFire using
GemFireHttpSessionConfiguration.
* Add implementation of SessionRepository to access clustered, replicated
HttpSession state in GemFire with GemFireOperationsSessionRepository.
* Utilize GemFire Data Serialization framework to both replicate
HttpSession state information as well as handle deltas.
* Utilize GemFire OQL query to lookup arbitrary Session attributes by name,
and in particular the user authenticated principal name.
* Implment unit and integration tests, and in particular, tests for both
peer-to-peer (p2p) and client/server topologies.
* Set initial Spring Data GemFire version to 1.7.2.RELEASE, which depends
on Pivotal GemFire 8.1.0.
* Add documentation, Javadoc and samples along with additional Integration
Tests.

Fixes gh-148
This commit is contained in:
John Blum
2016-01-28 13:19:18 -06:00
committed by Rob Winch
parent 1931da83a5
commit 019e0083b0
68 changed files with 8634 additions and 2 deletions

View File

@@ -0,0 +1,766 @@
/*
* Copyright 2002-2015 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.data.gemfire;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
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.session.ExpiringSession;
import org.springframework.session.FindByPrincipalNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration;
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.StringUtils;
import com.gemstone.gemfire.DataSerializable;
import com.gemstone.gemfire.DataSerializer;
import com.gemstone.gemfire.Delta;
import com.gemstone.gemfire.Instantiator;
import com.gemstone.gemfire.InvalidDeltaException;
import com.gemstone.gemfire.cache.EntryEvent;
import com.gemstone.gemfire.cache.Region;
import com.gemstone.gemfire.cache.util.CacheListenerAdapter;
/**
* AbstractGemFireOperationsSessionRepository is an abstract base class encapsulating functionality common
* to all implementations that support SessionRepository operations backed by GemFire.
*
* @author John Blum
* @see org.springframework.beans.factory.InitializingBean
* @see org.springframework.context.ApplicationEventPublisher
* @see org.springframework.context.ApplicationEventPublisherAware
* @see org.springframework.data.gemfire.GemfireAccessor
* @see org.springframework.data.gemfire.GemfireOperations
* @see org.springframework.session.ExpiringSession
* @see org.springframework.session.FindByPrincipalNameSessionRepository
* @see org.springframework.session.Session
* @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration
* @see com.gemstone.gemfire.cache.Region
* @see com.gemstone.gemfire.cache.util.CacheListenerAdapter
* @since 1.1.0
*/
public abstract class AbstractGemFireOperationsSessionRepository extends CacheListenerAdapter<Object, ExpiringSession>
implements InitializingBean, FindByPrincipalNameSessionRepository<ExpiringSession>,
ApplicationEventPublisherAware {
private int maxInactiveIntervalInSeconds = GemFireHttpSessionConfiguration.DEFAULT_MAX_INACTIVE_INTERVAL_IN_SECONDS;
private ApplicationEventPublisher applicationEventPublisher = new ApplicationEventPublisher() {
public void publishEvent(ApplicationEvent event) {
}
};
private final GemfireOperations template;
protected final Log logger = newLogger();
private String fullyQualifiedRegionName;
/**
* Constructs an instance of AbstractGemFireOperationsSessionRepository with a required GemfireOperations instance
* used to perform GemFire data access operations and interactions supporting the SessionRepository operations.
*
* @param template the GemfireOperations instance used to interact with GemFire.
* @see org.springframework.data.gemfire.GemfireOperations
*/
public AbstractGemFireOperationsSessionRepository(GemfireOperations template) {
Assert.notNull(template, "GemfireOperations must not be null");
this.template = template;
}
/**
* Used for testing purposes only to override the Log implementation with a mock.
*
* @return an instance of Log constructed from Apache commons-logging LogFactory.
* @see org.apache.commons.logging.LogFactory#getLog(Class)
*/
Log newLogger() {
return LogFactory.getLog(getClass());
}
/**
* Sets the ApplicationEventPublisher used to publish Session events corresponding to GemFire cache events.
*
* @param applicationEventPublisher the Spring ApplicationEventPublisher used to publish Session-based events.
* @see org.springframework.context.ApplicationEventPublisher
*/
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
Assert.notNull(applicationEventPublisher, "ApplicationEventPublisher must not be null");
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* Gets the ApplicationEventPublisher used to publish Session events corresponding to GemFire cache events.
*
* @return the Spring ApplicationEventPublisher used to publish Session-based events.
* @see org.springframework.context.ApplicationEventPublisher
*/
protected ApplicationEventPublisher getApplicationEventPublisher() {
return applicationEventPublisher;
}
/**
* Gets the fully-qualified name of the GemFire cache {@link Region} used to store and manage Session data.
*
* @return a String indicating the fully qualified name of the GemFire cache {@link Region} used to store
* and manage Session data.
*/
protected String getFullyQualifiedRegionName() {
return fullyQualifiedRegionName;
}
/**
* Sets the maximum interval in seconds in which a Session can remain inactive before it is considered expired.
*
* @param maxInactiveIntervalInSeconds an integer value specifying the maximum interval in seconds that a Session
* can remain inactive before it is considered expired.
*/
public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
/**
* Gets the maximum interval in seconds in which a Session can remain inactive before it is considered expired.
*
* @return an integer value specifying the maximum interval in seconds that a Session can remain inactive
* before it is considered expired.
*/
public int getMaxInactiveIntervalInSeconds() {
return maxInactiveIntervalInSeconds;
}
/**
* Gets a reference to the GemfireOperations (template) used to perform data access operations
* and other interactions on the GemFire cache {@link Region} backing this SessionRepository.
*
* @return a reference to the GemfireOperations used to interact with GemFire.
* @see org.springframework.data.gemfire.GemfireOperations
*/
public GemfireOperations getTemplate() {
return template;
}
/**
* Callback method during Spring bean initialization that will capture the fully-qualified name
* of the GemFire cache {@link Region} used to manage Session state and register this SessionRepository
* as a GemFire {@link com.gemstone.gemfire.cache.CacheListener}.
*
* @throws Exception if an error occurs during the initialization process.
*/
public void afterPropertiesSet() throws Exception {
GemfireOperations template = getTemplate();
Assert.isInstanceOf(GemfireAccessor.class, template);
Region<Object, ExpiringSession> region = ((GemfireAccessor) template).getRegion();
fullyQualifiedRegionName = region.getFullPath();
region.getAttributesMutator().addCacheListener(this);
}
/**
* Callback method triggered when an entry is created in the GemFire cache {@link Region}.
*
* @param event an EntryEvent containing the details of the cache operation.
* @see com.gemstone.gemfire.cache.EntryEvent
* @see #handleCreated(String, ExpiringSession)
*/
@Override
public void afterCreate(EntryEvent<Object, ExpiringSession> event) {
handleCreated(event.getKey().toString(), event.getNewValue());
}
/**
* Callback method triggered when an entry is destroyed in the GemFire cache {@link Region}.
*
* @param event an EntryEvent containing the details of the cache operation.
* @see com.gemstone.gemfire.cache.EntryEvent
* @see #handleDestroyed(String, ExpiringSession)
*/
@Override
public void afterDestroy(EntryEvent<Object, ExpiringSession> event) {
handleDestroyed(event.getKey().toString(), event.getOldValue());
}
/**
* Callback method triggered when an entry is invalidated in the GemFire cache {@link Region}.
*
* @param event an EntryEvent containing the details of the cache operation.
* @see com.gemstone.gemfire.cache.EntryEvent
* @see #handleExpired(String, ExpiringSession)
*/
@Override
public void afterInvalidate(EntryEvent<Object, ExpiringSession> event) {
handleExpired(event.getKey().toString(), event.getOldValue());
}
/**
* Causes Session created events to be published to the Spring application context.
*
* @param sessionId a String indicating the ID of the Session.
* @param session a reference to the Session triggering the event.
* @see org.springframework.session.events.SessionCreatedEvent
* @see org.springframework.session.ExpiringSession
* @see #publishEvent(ApplicationEvent)
*/
protected void handleCreated(String sessionId, ExpiringSession session) {
publishEvent(session != null ? new SessionCreatedEvent(this, session)
: new SessionCreatedEvent(this, sessionId));
}
/**
* 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.ExpiringSession
* @see #publishEvent(ApplicationEvent)
*/
protected void handleDeleted(String sessionId, ExpiringSession session) {
publishEvent(session != null ? new SessionDeletedEvent(this, session)
: new SessionDeletedEvent(this, sessionId));
}
/**
* Causes Session destroyed events to be published to the Spring application context.
*
* @param sessionId a String indicating the ID of the Session.
* @param session a reference to the Session triggering the event.
* @see org.springframework.session.events.SessionDestroyedEvent
* @see org.springframework.session.ExpiringSession
* @see #publishEvent(ApplicationEvent)
*/
protected void handleDestroyed(String sessionId, ExpiringSession session) {
publishEvent(session != null ? new SessionDestroyedEvent(this, session)
: new SessionDestroyedEvent(this, sessionId));
}
/**
* Causes Session expired events to be published to the Spring application context.
*
* @param sessionId a String indicating the ID of the Session.
* @param session a reference to the Session triggering the event.
* @see org.springframework.session.events.SessionExpiredEvent
* @see org.springframework.session.ExpiringSession
* @see #publishEvent(ApplicationEvent)
*/
protected void handleExpired(String sessionId, ExpiringSession session) {
publishEvent(session != null ? new SessionExpiredEvent(this, session)
: new SessionExpiredEvent(this, sessionId));
}
/**
* Publishes the specified ApplicationEvent to the Spring application context.
*
* @param event the 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 t) {
logger.error(String.format("error occurred publishing event (%1$s)", event), t);
}
}
/**
* GemFireSession is a GemFire representation model of a Spring {@link ExpiringSession} for storing and accessing
* Session state information in GemFire. This class implements GemFire's {@link DataSerializable} interface
* to better handle replication of Session information across the GemFire cluster.
*
* @see java.lang.Comparable
* @see org.springframework.session.ExpiringSession
* @see org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.GemFireSessionAttributes
* @see com.gemstone.gemfire.DataSerializable
* @see com.gemstone.gemfire.DataSerializer
* @see com.gemstone.gemfire.Delta
* @see com.gemstone.gemfire.Instantiator
*/
public static class GemFireSession implements Comparable<ExpiringSession>, DataSerializable, Delta, ExpiringSession {
protected static final boolean DEFAULT_ALLOW_JAVA_SERIALIZATION = true;
protected static final DateFormat TO_STRING_DATE_FORMAT = new SimpleDateFormat("YYYY-MM-dd-HH-mm-ss");
static {
Instantiator.register(new Instantiator(GemFireSession.class, 800813552) {
@Override public DataSerializable newInstance() {
return new GemFireSession();
}
});
}
private transient boolean delta = false;
private int maxInactiveIntervalInSeconds;
private long creationTime;
private long lastAccessedTime;
private transient final GemFireSessionAttributes sessionAttributes = new GemFireSessionAttributes(this);
private String id;
/* (non-Javadoc) */
protected GemFireSession() {
this(UUID.randomUUID().toString());
}
/* (non-Javadoc) */
protected GemFireSession(String id) {
this.id = validateId(id);
this.creationTime = System.currentTimeMillis();
this.lastAccessedTime = this.creationTime;
}
/* (non-Javadoc) */
protected GemFireSession(ExpiringSession session) {
Assert.notNull(session, "The ExpiringSession to copy cannot be null");
this.id = session.getId();
this.creationTime = session.getCreationTime();
this.lastAccessedTime = session.getLastAccessedTime();
this.maxInactiveIntervalInSeconds = session.getMaxInactiveIntervalInSeconds();
this.sessionAttributes.from(session);
}
/* (non-Javadoc) */
public static GemFireSession create(int maxInactiveIntervalInSeconds) {
GemFireSession session = new GemFireSession();
session.setMaxInactiveIntervalInSeconds(maxInactiveIntervalInSeconds);
return session;
}
/* (non-Javadoc) */
public static GemFireSession from(ExpiringSession expiringSession) {
GemFireSession session = new GemFireSession(expiringSession);
session.setLastAccessedTime(System.currentTimeMillis());
return session;
}
/* (non-Javadoc) */
private String validateId(String id) {
Assert.hasText(id, "ID must be specified");
return id;
}
/* (non-Javadoc) */
protected boolean allowJavaSerialization() {
return DEFAULT_ALLOW_JAVA_SERIALIZATION;
}
/* (non-Javadoc) */
public synchronized String getId() {
return id;
}
/* (non-Javadoc) */
public synchronized long getCreationTime() {
return creationTime;
}
/* (non-Javadoc) */
public void setAttribute(String attributeName, Object attributeValue) {
sessionAttributes.setAttribute(attributeName, attributeValue);
}
/* (non-Javadoc) */
public void removeAttribute(String attributeName) {
sessionAttributes.removeAttribute(attributeName);
}
/* (non-Javadoc) */
public <T> T getAttribute(String attributeName) {
return sessionAttributes.getAttribute(attributeName);
}
/* (non-Javadoc) */
public Set<String> getAttributeNames() {
return sessionAttributes.getAttributeNames();
}
/* (non-Javadoc) */
public synchronized boolean isExpired() {
long lastAccessedTime = getLastAccessedTime();
long maxInactiveIntervalInSeconds = getMaxInactiveIntervalInSeconds();
return (maxInactiveIntervalInSeconds >= 0
&& (idleTimeout(maxInactiveIntervalInSeconds) >= lastAccessedTime));
}
/* (non-Javadoc) */
private long idleTimeout(long maxInactiveIntervalInSeconds) {
return (System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(maxInactiveIntervalInSeconds));
}
/* (non-Javadoc) */
public synchronized void setLastAccessedTime(long lastAccessedTime) {
this.delta |= (this.lastAccessedTime != lastAccessedTime);
this.lastAccessedTime = lastAccessedTime;
}
/* (non-Javadoc) */
public synchronized long getLastAccessedTime() {
return lastAccessedTime;
}
/* (non-Javadoc) */
public synchronized void setMaxInactiveIntervalInSeconds(final int maxInactiveIntervalInSeconds) {
this.delta |= (this.maxInactiveIntervalInSeconds != maxInactiveIntervalInSeconds);
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
/* (non-Javadoc) */
public synchronized int getMaxInactiveIntervalInSeconds() {
return maxInactiveIntervalInSeconds;
}
/* (non-Javadoc) */
public synchronized void setPrincipalName(String principalName) {
setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
}
/* (non-Javadoc) */
public synchronized String getPrincipalName() {
return getAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME);
}
/* (non-Javadoc) */
public synchronized void toData(DataOutput out) throws IOException {
out.writeUTF(getId());
out.writeLong(getCreationTime());
out.writeLong(getLastAccessedTime());
out.writeInt(getMaxInactiveIntervalInSeconds());
String principalName = getPrincipalName();
int length = (StringUtils.hasText(principalName) ? principalName.length() : 0);
out.writeInt(length);
if (length > 0) {
out.writeUTF(principalName);
}
writeObject(sessionAttributes, out);
this.delta = false;
}
/* (non-Javadoc) */
void writeObject(Object obj, DataOutput out) throws IOException {
DataSerializer.writeObject(obj, out, allowJavaSerialization());
}
/* (non-Javadoc) */
public synchronized void fromData(DataInput in) throws ClassNotFoundException, IOException {
id = in.readUTF();
creationTime = in.readLong();
setLastAccessedTime(in.readLong());
setMaxInactiveIntervalInSeconds(in.readInt());
int principalNameLength = in.readInt();
if (principalNameLength > 0) {
setPrincipalName(in.readUTF());
}
sessionAttributes.from(this.<GemFireSessionAttributes>readObject(in));
this.delta = false;
}
/* (non-Javadoc) */
<T> T readObject(DataInput in) throws ClassNotFoundException, IOException {
return DataSerializer.readObject(in);
}
/* (non-Javadoc) */
public synchronized boolean hasDelta() {
return (delta || sessionAttributes.hasDelta());
}
/* (non-Javadoc) */
public synchronized void toDelta(DataOutput out) throws IOException {
out.writeLong(getLastAccessedTime());
out.writeInt(getMaxInactiveIntervalInSeconds());
sessionAttributes.toDelta(out);
this.delta = false;
}
/* (non-Javadoc) */
public synchronized void fromDelta(DataInput in) throws IOException {
setLastAccessedTime(in.readLong());
setMaxInactiveIntervalInSeconds(in.readInt());
sessionAttributes.fromDelta(in);
this.delta = false;
}
/* (non-Javadoc) */
@SuppressWarnings("all")
public int compareTo(ExpiringSession session) {
return (Long.valueOf(getCreationTime()).compareTo(session.getCreationTime()));
}
/* (non-Javadoc) */
@Override
public boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Session)) {
return false;
}
Session that = (Session) obj;
return this.getId().equals(that.getId());
}
/* (non-Javadoc) */
@Override
public int hashCode() {
int hashValue = 17;
hashValue = 37 * hashValue + getId().hashCode();
return hashValue;
}
/* (non-Javadoc) */
@Override
public synchronized String toString() {
return String.format("{ @type = %1$s, id = %2$s, creationTime = %3$s, lastAccessedTime = %4$s"
+ ", maxInactiveIntervalInSeconds = %5$s, principalName = %6$s }", getClass().getName(), getId(),
toString(getCreationTime()), toString(getLastAccessedTime()), getMaxInactiveIntervalInSeconds(),
getPrincipalName());
}
/* (non-Javadoc) */
private String toString(long timestamp) {
return TO_STRING_DATE_FORMAT.format(new Date(timestamp));
}
}
/**
* The GemFireSessionAttributes class is a container for a Session attributes that implements both
* the {@link DataSerializable} and {@link Delta} GemFire interfaces for efficient storage and distribution
* (replication) in GemFire.
*
* @see com.gemstone.gemfire.DataSerializable
* @see com.gemstone.gemfire.DataSerializer
* @see com.gemstone.gemfire.Delta
* @see com.gemstone.gemfire.Instantiator
*/
public static class GemFireSessionAttributes implements DataSerializable, Delta {
protected static final boolean DEFAULT_ALLOW_JAVA_SERIALIZATION = true;
static {
Instantiator.register(new Instantiator(GemFireSessionAttributes.class, 800828008) {
@Override public DataSerializable newInstance() {
return new GemFireSessionAttributes();
}
});
}
private transient final Map<String, Object> sessionAttributes = new HashMap<String, Object>();
private transient final Map<String, Object> sessionAttributeDeltas = new HashMap<String, Object>();
private transient final Object lock;
/* (non-Javadoc) */
protected GemFireSessionAttributes() {
this.lock = this;
}
/* (non-Javadoc) */
protected GemFireSessionAttributes(Object lock) {
this.lock = (lock != null ? lock : this);
}
/* (non-Javadoc) */
public void setAttribute(String attributeName, Object attributeValue) {
synchronized (lock) {
if (attributeValue != null) {
if (!attributeValue.equals(sessionAttributes.put(attributeName, attributeValue))) {
sessionAttributeDeltas.put(attributeName, attributeValue);
}
}
else {
removeAttribute(attributeName);
}
}
}
/* (non-Javadoc) */
public void removeAttribute(String attributeName) {
synchronized (lock) {
if (sessionAttributes.remove(attributeName) != null) {
sessionAttributeDeltas.put(attributeName, null);
}
}
}
/* (non-Javadoc) */
@SuppressWarnings("unchecked")
public <T> T getAttribute(String attributeName) {
synchronized (lock) {
return (T) sessionAttributes.get(attributeName);
}
}
/* (non-Javadoc) */
public Set<String> getAttributeNames() {
synchronized (lock) {
return Collections.unmodifiableSet(new HashSet<String>(sessionAttributes.keySet()));
}
}
/* (non-Javadoc) */
protected boolean allowJavaSerialization() {
return DEFAULT_ALLOW_JAVA_SERIALIZATION;
}
/* (non-Javadoc) */
public void from(Session session) {
synchronized (lock) {
for (String attributeName : session.getAttributeNames()) {
setAttribute(attributeName, session.getAttribute(attributeName));
}
}
}
/* (non-Javadoc) */
public void from(GemFireSessionAttributes sessionAttributes) {
synchronized (lock) {
for (String attributeName : sessionAttributes.getAttributeNames()) {
setAttribute(attributeName, sessionAttributes.getAttribute(attributeName));
}
}
}
/* (non-Javadoc) */
public void toData(DataOutput out) throws IOException {
synchronized (lock) {
Set<String> attributeNames = getAttributeNames();
out.writeInt(attributeNames.size());
for (String attributeName : attributeNames) {
out.writeUTF(attributeName);
writeObject(getAttribute(attributeName), out);
}
}
}
/* (non-Javadoc) */
void writeObject(Object obj, DataOutput out) throws IOException {
DataSerializer.writeObject(obj, out, allowJavaSerialization());
}
/* (non-Javadoc) */
public void fromData(DataInput in) throws IOException, ClassNotFoundException {
synchronized (lock) {
for (int count = in.readInt(); count > 0; count--) {
setAttribute(in.readUTF(), readObject(in));
}
sessionAttributeDeltas.clear();
}
}
/* (non-Javadoc) */
<T> T readObject(DataInput in) throws ClassNotFoundException , IOException {
return DataSerializer.readObject(in);
}
/* (non-Javadoc) */
public boolean hasDelta() {
synchronized (lock) {
return !sessionAttributeDeltas.isEmpty();
}
}
/* (non-Javadoc) */
public void toDelta(DataOutput out) throws IOException {
synchronized (lock) {
out.writeInt(sessionAttributeDeltas.size());
for (Map.Entry<String, Object> entry : sessionAttributeDeltas.entrySet()) {
out.writeUTF(entry.getKey());
writeObject(entry.getValue(), out);
}
sessionAttributeDeltas.clear();
}
}
/* (non-Javadoc) */
public void fromDelta(DataInput in) throws InvalidDeltaException, IOException {
synchronized (lock) {
try {
int count = in.readInt();
Map<String, Object> deltas = new HashMap<String, Object>(count);
while (count-- > 0) {
deltas.put(in.readUTF(), readObject(in));
}
for (Map.Entry<String, Object> entry : deltas.entrySet()) {
setAttribute(entry.getKey(), entry.getValue());
sessionAttributeDeltas.remove(entry.getKey());
}
}
catch (ClassNotFoundException e) {
throw new InvalidDeltaException("class type in data not found", e);
}
}
}
@Override
public String toString() {
return sessionAttributes.toString();
}
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2002-2015 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.data.gemfire;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.gemfire.GemfireOperations;
import org.springframework.session.ExpiringSession;
import com.gemstone.gemfire.cache.query.SelectResults;
/**
* The GemFireOperationsSessionRepository class is a Spring SessionRepository implementation that interfaces with
* and uses GemFire to back and store Spring Sessions.
*
* @author John Blum
* @see org.springframework.data.gemfire.GemfireOperations
* @see org.springframework.session.ExpiringSession
* @see org.springframework.session.Session
* @see org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository
* @since 1.1.0
*/
public class GemFireOperationsSessionRepository extends AbstractGemFireOperationsSessionRepository {
// GemFire OQL query used to look up Sessions by principal name.
protected static final String FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY =
"SELECT s FROM %1$s s WHERE s.principalName = $1";
/**
* Constructs an instance of GemFireOperationsSessionRepository initialized with the required GemfireOperations
* object used to perform data access operations to manage Session state.
*
* @param template the GemfireOperations object used to access and manage Session state in GemFire.
* @see org.springframework.data.gemfire.GemfireOperations
*/
public GemFireOperationsSessionRepository(GemfireOperations template) {
super(template);
}
/**
* Looks up all the available Sessions tied to the specific user identified by principal name.
*
* @param principalName the principal name (i.e. username) to search for all existing Spring Sessions.
* @return a mapping of Session ID to Session instances.
* @see org.springframework.session.ExpiringSession
*/
public Map<String, ExpiringSession> findByPrincipalName(String principalName) {
SelectResults<ExpiringSession> results = getTemplate().find(String.format(
FIND_SESSIONS_BY_PRINCIPAL_NAME_QUERY, getFullyQualifiedRegionName()), principalName);
Map<String, ExpiringSession> sessions = new HashMap<String, ExpiringSession>(results.size());
for (ExpiringSession session : results.asList()) {
sessions.put(session.getId(), session);
}
return sessions;
}
/**
* Constructs a new {@link ExpiringSession} instance backed by GemFire.
*
* @return an instance of {@link ExpiringSession} backed by GemFire.
* @see org.springframework.session.data.gemfire.GemFireOperationsSessionRepository.GemFireSession#create(int)
* @see org.springframework.session.ExpiringSession
* @see #getMaxInactiveIntervalInSeconds()
*/
public ExpiringSession createSession() {
return GemFireSession.create(getMaxInactiveIntervalInSeconds());
}
/**
* Gets a copy of an existing, non-expired {@link ExpiringSession} by ID. If the Session is expired,
* then it is deleted.
*
* @param sessionId a String indicating the ID of the Session to get.
* @return an existing {@link ExpiringSession} by ID or null if not Session exists.
* @see org.springframework.session.data.gemfire.GemFireOperationsSessionRepository.GemFireSession#from(ExpiringSession)
* @see org.springframework.session.ExpiringSession
* @see #delete(String)
*/
public ExpiringSession getSession(String sessionId) {
ExpiringSession storedSession = getTemplate().get(sessionId);
if (storedSession != null) {
if (storedSession.isExpired()) {
delete(storedSession.getId());
}
else {
return GemFireSession.from(storedSession);
}
}
return null;
}
/**
* Saves the specified {@link ExpiringSession} to GemFire.
*
* @param session the {@link ExpiringSession} to save.
* @see org.springframework.data.gemfire.GemfireOperations#put(Object, Object)
* @see org.springframework.session.ExpiringSession
*/
public void save(ExpiringSession session) {
getTemplate().put(session.getId(), new GemFireSession(session));
}
/**
* Deletes (removes) any existing {@link ExpiringSession} from GemFire. This operation also results in
* a SessionDeletedEvent.
*
* @param sessionId a String indicating the ID of the Session to remove from GemFire.
* @see org.springframework.data.gemfire.GemfireOperations#remove(Object)
* @see #handleDeleted(String, ExpiringSession)
*/
public void delete(String sessionId) {
handleDeleted(sessionId, getTemplate().<Object, ExpiringSession>remove(sessionId));
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2002-2015 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.data.gemfire.config.annotation.web.http;
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.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import com.gemstone.gemfire.cache.RegionShortcut;
import com.gemstone.gemfire.cache.client.ClientRegionShortcut;
/**
* Add this annotation to an {@code @Configuration} class to expose the SessionRepositoryFilter
* as a bean named "springSessionRepositoryFilter" and backed by Pivotal GemFire or Apache Geode.
*
* In order to leverage the annotation, a single Pivotal GemFire/Apache Geode {@link com.gemstone.gemfire.cache.Cache}
* or {@link com.gemstone.gemfire.cache.client.ClientCache} instance must be provided.
*
* For example:
*
* <pre>
* <code>
* {@literal @Configuration}
* {@literal @EnableGemFireHttpSession}
* public class GemFirePeerCacheHttpSessionConfiguration {
*
* {@literal @Bean}
* public Properties gemfireProperties() {
* Properties gemfireProperties = new Properties();
* gemfireProperties.setProperty("name", "ExamplePeer");
* gemfireProperties.setProperty("mcast-port", "0");
* gemfireProperties.setProperty("log-level", "warning");
* return gemfireProperties;
* }
*
* {@literal @Bean}
* public CacheFactoryBean gemfireCache() throws Exception {
* CacheFactoryBean clientCacheFactoryBean = new CacheFactoryBean();
* clientCacheFactoryBean.setLazyInitialize(false);
* clientCacheFactoryBean.setProperties(gemfireProperties());
* clientCacheFactoryBean.setUseBeanFactoryLocator(false);
* return clientCacheFactoryBean;
* }
* }
* </code>
* </pre>
*
* Alternatively, a Spring Session can be configured to use Pivotal GemFire (Apache Geode) as a client
* using a dedicated GemFire Server cluster and a {@link com.gemstone.gemfire.cache.client.ClientCache}.
* For example:
*
* <code>
* {@literal @Configuration}
* {@literal @EnableGemFireHttpSession}
* public class GemFireClientCacheHttpSessionConfiguration {
*
* {@literal @Bean}
* public Properties gemfireProperties() {
* Properties gemfireProperties = new Properties();
* gemfireProperties.setProperty("name", "ExampleClient");
* gemfireProperties.setProperty("log-level", "warning");
* return gemfireProperties;
* }
*
* {@literal @Bean}
* public ClientCacheFactoryBean gemfireCache() throws Exception {
* ClientCacheFactoryBean clientCacheFactoryBean = new ClientCacheFactoryBean();
* clientCacheFactoryBean.setLazyInitialize(false);
* clientCacheFactoryBean.setProperties(gemfireProperties());
* clientCacheFactoryBean.setUseBeanFactoryLocator(false);
* return clientCacheFactoryBean;
* }
* }
* </code>
*
* More advanced configurations can extend {@link GemFireHttpSessionConfiguration} instead.
*
* @author John Blum
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.Import
* @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration
* @see org.springframework.session.config.annotation.web.http.EnableSpringHttpSession
* @since 1.1.0
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@SuppressWarnings("unused")
@Target(ElementType.TYPE)
@Configuration
@Import(GemFireHttpSessionConfiguration.class)
public @interface EnableGemFireHttpSession {
/**
* Defines the GemFire ClientCache Region DataPolicy.
*
* @return a ClientRegionShortcut used to specify and configure the ClientCache Region DataPolicy.
* @see com.gemstone.gemfire.cache.client.ClientRegionShortcut
*/
ClientRegionShortcut clientRegionShortcut() default ClientRegionShortcut.PROXY;
/**
* Defines the maximum interval in seconds that a Session can remain inactive before it is considered expired.
* Defaults to 1800 seconds, or 30 minutes.
*
* @return an integer value defining the maximum inactive interval in seconds for declaring a Session expired.
*/
int maxInactiveIntervalInSeconds() default 1800;
/**
* Defines the name of the GemFire (Client)Cache Region used to store Sessions.
*
* @return a String specifying the name of the GemFire (Client)Cache Region used to store Sessions.
* @see com.gemstone.gemfire.cache.Region#getName()
*/
String regionName() default "ClusteredSpringSessions";
/**
* Defines the GemFire, Peer Cache Region DataPolicy.
*
* @return a RegionShortcut used to specify and configure the Peer Cache Region DataPolicy.
* @see com.gemstone.gemfire.cache.RegionShortcut
*/
RegionShortcut serverRegionShortcut() default RegionShortcut.PARTITION;
}

View File

@@ -0,0 +1,372 @@
/*
* Copyright 2002-2015 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.data.gemfire.config.annotation.web.http;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.ImportAware;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.data.gemfire.GemfireOperations;
import org.springframework.data.gemfire.GemfireTemplate;
import org.springframework.data.gemfire.IndexFactoryBean;
import org.springframework.data.gemfire.IndexType;
import org.springframework.data.gemfire.RegionAttributesFactoryBean;
import org.springframework.session.ExpiringSession;
import org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration;
import org.springframework.session.data.gemfire.AbstractGemFireOperationsSessionRepository.GemFireSession;
import org.springframework.session.data.gemfire.GemFireOperationsSessionRepository;
import org.springframework.session.data.gemfire.config.annotation.web.http.support.GemFireCacheTypeAwareRegionFactoryBean;
import org.springframework.session.data.gemfire.support.GemFireUtils;
import org.springframework.util.StringUtils;
import com.gemstone.gemfire.cache.ExpirationAction;
import com.gemstone.gemfire.cache.ExpirationAttributes;
import com.gemstone.gemfire.cache.GemFireCache;
import com.gemstone.gemfire.cache.Region;
import com.gemstone.gemfire.cache.RegionAttributes;
import com.gemstone.gemfire.cache.RegionShortcut;
import com.gemstone.gemfire.cache.client.ClientRegionShortcut;
/**
* The GemFireHttpSessionConfiguration class is a Spring @Configuration class used to configure and initialize
* Pivotal GemFire (or Apache Geode) as a clustered, replicated HttpSession provider implementation in Spring Session.
*
* @author John Blum
* @see org.springframework.beans.factory.BeanClassLoaderAware
* @see org.springframework.context.annotation.Bean
* @see org.springframework.context.annotation.Configuration
* @see org.springframework.context.annotation.ImportAware
* @see org.springframework.data.gemfire.GemfireOperations
* @see org.springframework.data.gemfire.GemfireTemplate
* @see org.springframework.data.gemfire.IndexFactoryBean
* @see org.springframework.data.gemfire.RegionAttributesFactoryBean
* @see org.springframework.session.ExpiringSession
* @see org.springframework.session.config.annotation.web.http.SpringHttpSessionConfiguration
* @see org.springframework.session.data.gemfire.GemFireOperationsSessionRepository
* @see org.springframework.session.data.gemfire.config.annotation.web.http.support.GemFireCacheTypeAwareRegionFactoryBean
* @see com.gemstone.gemfire.cache.ExpirationAttributes
* @see com.gemstone.gemfire.cache.GemFireCache
* @see com.gemstone.gemfire.cache.Region
* @see com.gemstone.gemfire.cache.RegionAttributes
* @since 1.1.0
*/
@Configuration
@SuppressWarnings("unused")
public class GemFireHttpSessionConfiguration extends SpringHttpSessionConfiguration
implements BeanClassLoaderAware, ImportAware {
public static final int DEFAULT_MAX_INACTIVE_INTERVAL_IN_SECONDS = (int) TimeUnit.MINUTES.toSeconds(30);
protected static final Class<Object> SPRING_SESSION_GEMFIRE_REGION_KEY_CONSTRAINT = Object.class;
protected static final Class<GemFireSession> SPRING_SESSION_GEMFIRE_REGION_VALUE_CONSTRAINT = GemFireSession.class;
public static final ClientRegionShortcut DEFAULT_CLIENT_REGION_SHORTCUT = ClientRegionShortcut.PROXY;
public static final RegionShortcut DEFAULT_SERVER_REGION_SHORTCUT = RegionShortcut.PARTITION;
public static final String DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME = "ClusteredSpringSessions";
private int maxInactiveIntervalInSeconds = DEFAULT_MAX_INACTIVE_INTERVAL_IN_SECONDS;
private ClassLoader beanClassLoader;
private ClientRegionShortcut clientRegionShortcut = DEFAULT_CLIENT_REGION_SHORTCUT;
private RegionShortcut serverRegionShortcut = DEFAULT_SERVER_REGION_SHORTCUT;
private String springSessionGemFireRegionName = DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME;
/**
* Sets a reference to the {@link ClassLoader} used to load bean definition class types in a Spring context.
*
* @param beanClassLoader the ClassLoader used by the Spring container to load bean class types.
* @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(ClassLoader)
* @see java.lang.ClassLoader
*/
public void setBeanClassLoader(ClassLoader beanClassLoader) {
this.beanClassLoader = beanClassLoader;
}
/**
* Gets a reference to the {@link ClassLoader} used to load bean definition class types in a Spring context.
*
* @return the ClassLoader used by the Spring container to load bean class types.
* @see java.lang.ClassLoader
*/
protected ClassLoader getBeanClassLoader() {
return beanClassLoader;
}
/**
* Sets the {@link ClientRegionShortcut} used to configure the GemFire ClientCache Region
* that will store Spring Sessions.
*
* @param shortcut the ClientRegionShortcut used to configure the GemFire ClientCache Region.
* @see com.gemstone.gemfire.cache.client.ClientRegionShortcut
*/
public void setClientRegionShortcut(ClientRegionShortcut shortcut) {
this.clientRegionShortcut = shortcut;
}
/**
* Gets the {@link ClientRegionShortcut} used to configure the GemFire ClientCache Region
* that will store Spring Sessions. Defaults to {@link ClientRegionShortcut#PROXY}.
*
* @return the ClientRegionShortcut used to configure the GemFire ClientCache Region.
* @see com.gemstone.gemfire.cache.client.ClientRegionShortcut
*/
protected ClientRegionShortcut getClientRegionShortcut() {
return (clientRegionShortcut != null ? clientRegionShortcut : DEFAULT_CLIENT_REGION_SHORTCUT);
}
/**
* Sets the maximum interval in seconds in which a Session can remain inactive before it is considered expired.
*
* @param maxInactiveIntervalInSeconds an integer value specifying the maximum interval in seconds that a Session
* can remain inactive before it is considered expired.
*/
public void setMaxInactiveIntervalInSeconds(int maxInactiveIntervalInSeconds) {
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
/**
* Gets the maximum interval in seconds in which a Session can remain inactive before it is considered expired.
*
* @return an integer value specifying the maximum interval in seconds that a Session can remain inactive
* before it is considered expired.
*/
protected int getMaxInactiveIntervalInSeconds() {
return maxInactiveIntervalInSeconds;
}
/**
* Sets the {@link RegionShortcut} used to configure the GemFire Cache Region that will store Spring Sessions.
*
* @param shortcut the RegionShortcut used to configure the GemFire Cache Region.
* @see com.gemstone.gemfire.cache.RegionShortcut
*/
public void setServerRegionShortcut(RegionShortcut shortcut) {
serverRegionShortcut = shortcut;
}
/**
* Gets the {@link RegionShortcut} used to configure the GemFire Cache Region that will store Spring Sessions.
* Defaults to {@link RegionShortcut#PARTITION}.
*
* @return the RegionShortcut used to configure the GemFire Cache Region.
* @see com.gemstone.gemfire.cache.RegionShortcut
*/
protected RegionShortcut getServerRegionShortcut() {
return (serverRegionShortcut != null ? serverRegionShortcut : DEFAULT_SERVER_REGION_SHORTCUT);
}
/**
* Sets the name of the Gemfire (Client)Cache Region used to store Sessions.
*
* @param springSessionGemFireRegionName a String specifying the name of the GemFire (Client)Cache Region
* used to store the Session.
*/
public void setSpringSessionGemFireRegionName(String springSessionGemFireRegionName) {
this.springSessionGemFireRegionName = springSessionGemFireRegionName;
}
/**
* Gets the name of the Gemfire (Client)Cache Region used to store Sessions. Defaults to 'ClusteredSpringSessions'.
*
* @return a String specifying the name of the GemFire (Client)Cache Region
* used to store the Session.
* @see com.gemstone.gemfire.cache.Region#getName()
*/
protected String getSpringSessionGemFireRegionName() {
return (StringUtils.hasText(springSessionGemFireRegionName) ? springSessionGemFireRegionName
: DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME);
}
/**
* Callback with the {@link AnnotationMetadata} of the class containing @Import annotation that imported
* this @Configuration class.
*
* @param importMetadata the AnnotationMetadata of the class importing this @Configuration class.
* @see org.springframework.session.data.gemfire.config.annotation.web.http.EnableGemFireHttpSession
* @see org.springframework.core.type.AnnotationMetadata
*/
public void setImportMetadata(AnnotationMetadata importMetadata) {
AnnotationAttributes enableGemFireHttpSessionAnnotationAttributes = AnnotationAttributes.fromMap(
importMetadata.getAnnotationAttributes(EnableGemFireHttpSession.class.getName()));
setClientRegionShortcut(ClientRegionShortcut.class.cast(enableGemFireHttpSessionAnnotationAttributes.getEnum(
"clientRegionShortcut")));
setMaxInactiveIntervalInSeconds(enableGemFireHttpSessionAnnotationAttributes.getNumber(
"maxInactiveIntervalInSeconds").intValue());
setServerRegionShortcut(RegionShortcut.class.cast(enableGemFireHttpSessionAnnotationAttributes.getEnum(
"serverRegionShortcut")));
setSpringSessionGemFireRegionName(enableGemFireHttpSessionAnnotationAttributes.getString("regionName"));
}
/**
* Defines the Spring SessionRepository bean used to interact with GemFire as a Spring Session provider.
*
* @param gemfireOperations an instance of {@link GemfireOperations} used to manage Spring Sessions in GemFire.
* @return a GemFireOperationsSessionRepository for managing (clustering/replicating) Sessions using GemFire.
* @see org.springframework.session.data.gemfire.GemFireOperationsSessionRepository
* @see org.springframework.data.gemfire.GemfireOperations
*/
@Bean
public GemFireOperationsSessionRepository sessionRepository(@Qualifier("sessionRegionTemplate")
GemfireOperations gemfireOperations) {
GemFireOperationsSessionRepository sessionRepository = new GemFireOperationsSessionRepository(gemfireOperations);
sessionRepository.setMaxInactiveIntervalInSeconds(getMaxInactiveIntervalInSeconds());
return sessionRepository;
}
/**
* Defines a Spring GemfireTemplate bean used to interact with GemFire's (Client)Cache {@link Region}
* storing Sessions.
*
* @param gemFireCache reference to the single GemFire cache instance used by the {@link GemfireTemplate}
* to perform GemFire cache data access operations.
* @return a {@link GemfireTemplate} used to interact with GemFire's (Client)Cache {@link Region} storing Sessions.
* @see org.springframework.data.gemfire.GemfireTemplate
* @see com.gemstone.gemfire.cache.Region
*/
@Bean
@DependsOn(DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME)
public GemfireTemplate sessionRegionTemplate(GemFireCache gemFireCache) {
return new GemfireTemplate(gemFireCache.getRegion(getSpringSessionGemFireRegionName()));
}
/**
* Defines a Spring GemFire {@link com.gemstone.gemfire.cache.Cache} {@link Region} bean used to store
* and manage Sessions using either a client-server or peer-to-peer (p2p) topology.
*
* @param gemfireCache a reference to the GemFire {@link com.gemstone.gemfire.cache.Cache}.
* @param sessionRegionAttributes the GemFire {@link RegionAttributes} used to configure the {@link Region}.
* @return a {@link GemFireCacheTypeAwareRegionFactoryBean} used to configure and initialize a GemFire Cache
* {@link Region} for storing and managing Sessions.
* @see org.springframework.session.data.gemfire.config.annotation.web.http.support.GemFireCacheTypeAwareRegionFactoryBean
* @see com.gemstone.gemfire.cache.GemFireCache
* @see com.gemstone.gemfire.cache.RegionAttributes
* @see #getClientRegionShortcut()
* @see #getSpringSessionGemFireRegionName()
* @see #getServerRegionShortcut()
*/
@Bean(name = DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME)
public GemFireCacheTypeAwareRegionFactoryBean<Object, ExpiringSession> sessionRegion(GemFireCache gemfireCache,
RegionAttributes<Object, ExpiringSession> sessionRegionAttributes) {
GemFireCacheTypeAwareRegionFactoryBean<Object, ExpiringSession> serverRegion =
new GemFireCacheTypeAwareRegionFactoryBean<Object, ExpiringSession>();
serverRegion.setGemfireCache(gemfireCache);
serverRegion.setClientRegionShortcut(getClientRegionShortcut());
serverRegion.setRegionAttributes(sessionRegionAttributes);
serverRegion.setRegionName(getSpringSessionGemFireRegionName());
serverRegion.setServerRegionShortcut(getServerRegionShortcut());
return serverRegion;
}
/**
* Defines a Spring GemFire {@link RegionAttributes} bean used to configure and initialize the GemFire cache
* {@link Region} storing Sessions. Expiration is also configured for the {@link Region} on the basis that the
* GemFire cache {@link Region} is a not a proxy, on either the client or server.
*
* @param gemfireCache a reference to the GemFire cache.
* @return an instance of {@link RegionAttributes} used to configure and initialize the GemFire cache {@link Region}
* for storing and managing Sessions.
* @see org.springframework.data.gemfire.RegionAttributesFactoryBean
* @see com.gemstone.gemfire.cache.GemFireCache
* @see com.gemstone.gemfire.cache.PartitionAttributes
* @see #isExpirationAllowed(GemFireCache)
*/
@Bean
@SuppressWarnings({ "unchecked", "deprecation" })
public RegionAttributesFactoryBean sessionRegionAttributes(GemFireCache gemfireCache) {
RegionAttributesFactoryBean regionAttributes = new RegionAttributesFactoryBean();
regionAttributes.setKeyConstraint(SPRING_SESSION_GEMFIRE_REGION_KEY_CONSTRAINT);
regionAttributes.setValueConstraint(SPRING_SESSION_GEMFIRE_REGION_VALUE_CONSTRAINT);
if (isExpirationAllowed(gemfireCache)) {
regionAttributes.setStatisticsEnabled(true);
regionAttributes.setEntryIdleTimeout(new ExpirationAttributes(
Math.max(getMaxInactiveIntervalInSeconds(), 0), ExpirationAction.INVALIDATE));
}
return regionAttributes;
}
/**
* Determines whether expiration configuration is allowed to be set on the GemFire cache {@link Region}
* used to store and manage Sessions.
*
* @param gemfireCache a reference to the GemFire cache.
* @return a boolean indicating if a {@link Region} can be configured for Region entry idle-timeout expiration.
* @see GemFireUtils#isClient(GemFireCache)
* @see GemFireUtils#isProxy(ClientRegionShortcut)
* @see GemFireUtils#isProxy(RegionShortcut)
*/
boolean isExpirationAllowed(GemFireCache gemfireCache) {
return !(GemFireUtils.isClient(gemfireCache) ? GemFireUtils.isProxy(getClientRegionShortcut())
: GemFireUtils.isProxy(getServerRegionShortcut()));
}
/**
* Defines a Spring GemFire Index bean on the GemFire cache {@link Region} storing and managing Sessions,
* specifically on the 'principalName' property for quick lookup and queries. This index will only be created
* on a server @{link Region}.
*
* @param gemfireCache a reference to the GemFire cache.
* @return a IndexFactoryBean creating an GemFire Index on the 'principalName' property of Sessions stored
* in the GemFire cache {@link Region}.
* @see org.springframework.data.gemfire.IndexFactoryBean
* @see com.gemstone.gemfire.cache.GemFireCache
*/
@Bean
@DependsOn(DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME)
public IndexFactoryBean principalNameIndex(final GemFireCache gemfireCache) {
IndexFactoryBean index = new IndexFactoryBean() {
@Override public void afterPropertiesSet() throws Exception {
if (GemFireUtils.isPeer(gemfireCache)) {
super.afterPropertiesSet();
}
}
};
index.setCache(gemfireCache);
index.setName("principalNameIdx");
index.setExpression("principalName");
index.setFrom(GemFireUtils.toRegionPath(getSpringSessionGemFireRegionName()));
index.setOverride(true);
index.setType(IndexType.HASH);
return index;
}
}

View File

@@ -0,0 +1,314 @@
/*
* Copyright 2002-2015 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.data.gemfire.config.annotation.web.http.support;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.gemfire.GenericRegionFactoryBean;
import org.springframework.data.gemfire.client.ClientRegionFactoryBean;
import org.springframework.data.gemfire.client.Interest;
import org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration;
import org.springframework.session.data.gemfire.support.GemFireUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.gemstone.gemfire.cache.GemFireCache;
import com.gemstone.gemfire.cache.InterestResultPolicy;
import com.gemstone.gemfire.cache.Region;
import com.gemstone.gemfire.cache.RegionAttributes;
import com.gemstone.gemfire.cache.RegionShortcut;
import com.gemstone.gemfire.cache.client.ClientRegionShortcut;
/**
* The GemFireCacheTypeAwareRegionFactoryBean class is a Spring {@link FactoryBean} used to construct, configure
* and initialize the GemFire cache {@link Region} used to store and manage Session state.
*
* @author John Blum
* @see org.springframework.beans.factory.FactoryBean
* @see org.springframework.beans.factory.InitializingBean
* @see org.springframework.data.gemfire.GenericRegionFactoryBean
* @see org.springframework.data.gemfire.client.ClientRegionFactoryBean
* @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration
* @see com.gemstone.gemfire.cache.GemFireCache
* @see com.gemstone.gemfire.cache.InterestResultPolicy
* @see com.gemstone.gemfire.cache.Region
* @see com.gemstone.gemfire.cache.RegionAttributes
* @see com.gemstone.gemfire.cache.RegionShortcut
* @see com.gemstone.gemfire.cache.client.ClientRegionShortcut
* @since 1.1.0
*/
public class GemFireCacheTypeAwareRegionFactoryBean<K, V> implements FactoryBean<Region<K, V>>, InitializingBean {
protected static final ClientRegionShortcut DEFAULT_CLIENT_REGION_SHORTCUT =
GemFireHttpSessionConfiguration.DEFAULT_CLIENT_REGION_SHORTCUT;
protected static final RegionShortcut DEFAULT_SERVER_REGION_SHORTCUT =
GemFireHttpSessionConfiguration.DEFAULT_SERVER_REGION_SHORTCUT;
protected static final String DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME =
GemFireHttpSessionConfiguration.DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME;
private ClientRegionShortcut clientRegionShortcut;
private GemFireCache gemfireCache;
private Region<K, V> region;
private RegionAttributes<K, V> regionAttributes;
private RegionShortcut serverRegionShortcut;
private String regionName;
/**
* Post-construction initialization callback to create, configure and initialize the GemFire cache {@link Region}
* used to store, replicate (distribute) and manage Session state. This method intelligently handles
* both client-server and peer-to-peer (p2p) GemFire supported distributed system topologies.
*
* @throws Exception if the initialization of the GemFire cache {@link Region} fails.
* @see org.springframework.session.data.gemfire.support.GemFireUtils#isClient(GemFireCache)
* @see #getGemfireCache()
* @see #newClientRegion(GemFireCache)
* @see #newServerRegion(GemFireCache)
*/
public void afterPropertiesSet() throws Exception {
GemFireCache gemfireCache = getGemfireCache();
region = (GemFireUtils.isClient(gemfireCache) ? newClientRegion(gemfireCache)
: newServerRegion(gemfireCache));
}
/**
* Constructs a GemFire cache {@link Region} using a peer-to-peer (p2p) GemFire topology to store
* and manage Session state in a GemFire server cluster accessible from a GemFire cache client.
*
* @param gemfireCache a reference to the GemFire {@link com.gemstone.gemfire.cache.Cache}.
* @return a peer-to-peer-based GemFire cache {@link Region} to store and manage Session state.
* @throws Exception if the instantiation, configuration and initialization
* of the GemFire cache {@link Region} fails.
* @see org.springframework.data.gemfire.GenericRegionFactoryBean
* @see com.gemstone.gemfire.cache.GemFireCache
* @see com.gemstone.gemfire.cache.Region
* @see #getRegionAttributes()
* @see #getRegionName()
* @see #getServerRegionShortcut()
*/
protected Region<K, V> newServerRegion(GemFireCache gemfireCache) throws Exception {
GenericRegionFactoryBean<K, V> serverRegion = new GenericRegionFactoryBean<K, V>();
serverRegion.setCache(gemfireCache);
serverRegion.setAttributes(getRegionAttributes());
serverRegion.setRegionName(getRegionName());
serverRegion.setShortcut(getServerRegionShortcut());
serverRegion.afterPropertiesSet();
return serverRegion.getObject();
}
/**
* Constructs a GemFire cache {@link Region} using the client-server GemFire topology to store
* and manage Session state in a GemFire server cluster accessible from a GemFire cache client.
*
* @param gemfireCache a reference to the GemFire {@link com.gemstone.gemfire.cache.Cache}.
* @return a client-server-based GemFire cache {@link Region} to store and manage Session state.
* @throws Exception if the instantiation, configuration and initialization
* of the GemFire cache {@link Region} fails.
* @see org.springframework.data.gemfire.client.ClientRegionFactoryBean
* @see com.gemstone.gemfire.cache.GemFireCache
* @see com.gemstone.gemfire.cache.Region
* @see #getClientRegionShortcut()
* @see #getRegionAttributes()
* @see #getRegionName()
* @see #registerInterests(boolean)
*/
protected Region<K, V> newClientRegion(GemFireCache gemfireCache) throws Exception {
ClientRegionFactoryBean<K, V> clientRegion = new ClientRegionFactoryBean<K, V>();
ClientRegionShortcut shortcut = getClientRegionShortcut();
clientRegion.setCache(gemfireCache);
clientRegion.setAttributes(getRegionAttributes());
clientRegion.setInterests(registerInterests(!GemFireUtils.isLocal(shortcut)));
clientRegion.setRegionName(getRegionName());
clientRegion.setShortcut(shortcut);
clientRegion.afterPropertiesSet();
return clientRegion.getObject();
}
/**
* Registers interests in all keys when the client {@link Region} is non-local.
*
* @return an array of Interests specifying the server notifications of interests to the client.
* @see org.springframework.data.gemfire.client.Interest
*/
/**
* Decides whether interests will be registered for all keys. Interests is only registered on a client
* and typically only when the client is a (CACHING) PROXY to the server (i.e. non-LOCAL only).
*
* @param register a boolean value indicating whether interests should be registered.
* @return an array of Interests KEY/VALUE registrations.
* @see org.springframework.data.gemfire.client.Interest
*/
@SuppressWarnings("unchecked")
protected Interest<K>[] registerInterests(boolean register) {
return (!register ? new Interest[0] : new Interest[] {
new Interest<String>("ALL_KEYS", InterestResultPolicy.KEYS)
});
}
/**
* Returns a reference to the constructed GemFire cache {@link Region} used to store and manage Session state.
*
* @return the {@link Region} used to store and manage Session state.
* @throws Exception if the {@link Region} reference cannot be obtained.
* @see com.gemstone.gemfire.cache.Region
*/
public Region<K, V> getObject() throws Exception {
return region;
}
/**
* Returns the specific type of GemFire cache {@link Region} this factory creates when initialized
* or Region.class when uninitialized.
*
* @return the GemFire cache {@link Region} class type constructed by this factory.
* @see com.gemstone.gemfire.cache.Region
* @see java.lang.Class
*/
public Class<?> getObjectType() {
return (region != null ? region.getClass() : Region.class);
}
/**
* Returns true indicating the GemFire cache {@link Region} created by this factory is the sole instance.
*
* @return true to indicate the GemFire cache {@link Region} storing and managing Sessions is a Singleton.
*/
public boolean isSingleton() {
return true;
}
/**
* Sets the {@link Region} data policy used by the GemFire cache client to manage Session state.
*
* @param clientRegionShortcut a {@link ClientRegionShortcut} to specify the client {@link Region}
* data management policy.
* @see com.gemstone.gemfire.cache.client.ClientRegionShortcut
*/
public void setClientRegionShortcut(ClientRegionShortcut clientRegionShortcut) {
this.clientRegionShortcut = clientRegionShortcut;
}
/**
* Returns the {@link Region} data policy used by the GemFire cache client to manage Session state. Defaults to
* {@link ClientRegionShortcut#PROXY}.
*
* @return a {@link ClientRegionShortcut} specifying the client {@link Region} data management policy.
* @see org.springframework.session.data.gemfire.config.annotation.web.http.GemFireHttpSessionConfiguration#DEFAULT_CLIENT_REGION_SHORTCUT
* @see com.gemstone.gemfire.cache.client.ClientRegionShortcut
*/
protected ClientRegionShortcut getClientRegionShortcut() {
return (clientRegionShortcut != null ? clientRegionShortcut : DEFAULT_CLIENT_REGION_SHORTCUT);
}
/**
* Sets a reference to the GemFire cache used to construct the appropriate {@link Region}.
*
* @param gemfireCache a reference to the GemFire cache.
* @throws IllegalArgumentException if the {@link GemFireCache} reference is null.
*/
public void setGemfireCache(GemFireCache gemfireCache) {
Assert.notNull(gemfireCache, "The GemFireCache reference must not be null");
this.gemfireCache = gemfireCache;
}
/**
* Returns a reference to the GemFire cache used to construct the appropriate {@link Region}.
*
* @return a reference to the GemFire cache.
* @throws IllegalStateException if the {@link GemFireCache} reference is null.
*/
protected GemFireCache getGemfireCache() {
Assert.state(gemfireCache != null, "A reference to a GemFireCache was not properly configured");
return gemfireCache;
}
/**
* Sets the GemFire {@link RegionAttributes} used to configure the GemFire cache {@link Region} used to
* store and manage Session state.
*
* @param regionAttributes the GemFire {@link RegionAttributes} used to configure the GemFire cache {@link Region}.
* @see com.gemstone.gemfire.cache.RegionAttributes
*/
public void setRegionAttributes(RegionAttributes<K, V> regionAttributes) {
this.regionAttributes = regionAttributes;
}
/**
* Returns the GemFire {@link RegionAttributes} used to configure the GemFire cache {@link Region} used to
* store and manage Session state.
*
* @return the GemFire {@link RegionAttributes} used to configure the GemFire cache {@link Region}.
* @see com.gemstone.gemfire.cache.RegionAttributes
*/
protected RegionAttributes<K, V> getRegionAttributes() {
return regionAttributes;
}
/**
* Sets the name of the GemFire cache {@link Region} use to store and manage Session state.
*
* @param regionName a String specifying the name of the GemFire cache {@link Region}.
*/
public void setRegionName(final String regionName) {
this.regionName = regionName;
}
/**
* Returns the configured name of the GemFire cache {@link Region} use to store and manage Session state.
* Defaults to "ClusteredSpringSessions"
*
* @return a String specifying the name of the GemFire cache {@link Region}.
* @see com.gemstone.gemfire.cache.Region#getName()
*/
protected String getRegionName() {
return (StringUtils.hasText(regionName) ? regionName : DEFAULT_SPRING_SESSION_GEMFIRE_REGION_NAME);
}
/**
* Sets the {@link Region} data policy used by the GemFire peer cache to manage Session state.
*
* @param serverRegionShortcut a {@link RegionShortcut} to specify the peer {@link Region} data management policy.
* @see com.gemstone.gemfire.cache.RegionShortcut
*/
public void setServerRegionShortcut(RegionShortcut serverRegionShortcut) {
this.serverRegionShortcut = serverRegionShortcut;
}
/**
* Returns the {@link Region} data policy used by the GemFire peer cache to manage Session state. Defaults to
* {@link RegionShortcut#PARTITION}.
*
* @return a {@link RegionShortcut} specifying the peer {@link Region} data management policy.
* @see com.gemstone.gemfire.cache.RegionShortcut
*/
protected RegionShortcut getServerRegionShortcut() {
return (serverRegionShortcut != null ? serverRegionShortcut : DEFAULT_SERVER_REGION_SHORTCUT);
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2002-2015 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.data.gemfire.support;
import java.io.Closeable;
import java.io.IOException;
import com.gemstone.gemfire.cache.Cache;
import com.gemstone.gemfire.cache.GemFireCache;
import com.gemstone.gemfire.cache.Region;
import com.gemstone.gemfire.cache.RegionShortcut;
import com.gemstone.gemfire.cache.client.ClientCache;
import com.gemstone.gemfire.cache.client.ClientRegionShortcut;
import com.gemstone.gemfire.internal.cache.GemFireCacheImpl;
/**
* GemFireUtils is an abstract, extensible utility class for working with GemFire types and functionality
* and is used by Spring Session's GemFire adapter support classes.
*
* @author John Blum
* @since 1.0.0
*/
public abstract class GemFireUtils {
/**
* Null-safe method to close the given {@link Closeable} object.
*
* @param obj the {@link Closeable} object to close.
* @return true if the {@link Closeable} object is not null and was successfully closed,
* otherwise return false.
* @see java.io.Closeable
*/
public static boolean close(Closeable obj) {
if (obj != null) {
try {
obj.close();
return true;
}
catch (IOException ignore) {
}
}
return false;
}
/**
* Determines whether the GemFire cache is a client.
*
* @param gemFireCache a reference to the GemFire cache.
* @return a boolean value indicating whether the GemFire cache is a client.
* @see com.gemstone.gemfire.cache.client.ClientCache
* @see com.gemstone.gemfire.cache.GemFireCache
*/
public static boolean isClient(GemFireCache gemFireCache) {
boolean client = (gemFireCache instanceof ClientCache);
client &= (!(gemFireCache instanceof GemFireCacheImpl) || ((GemFireCacheImpl) gemFireCache).isClient());
return client;
}
/**
* Determines whether the GemFire cache is a peer.
*
* @param gemFireCache a reference to the GemFire cache.
* @return a boolean value indicating whether the GemFire cache is a peer.
* @see com.gemstone.gemfire.cache.Cache
* @see com.gemstone.gemfire.cache.GemFireCache
*/
public static boolean isPeer(GemFireCache gemFireCache) {
return (gemFireCache instanceof Cache && !isClient(gemFireCache));
}
/**
* Determines whether the given {@link ClientRegionShortcut} is local only.
*
* @param shortcut the ClientRegionShortcut to evaluate.
* @return a boolean value indicating if the {@link ClientRegionShortcut} is local or not.
* @see com.gemstone.gemfire.cache.client.ClientRegionShortcut
*/
public static boolean isLocal(ClientRegionShortcut shortcut) {
switch (shortcut) {
case LOCAL:
case LOCAL_HEAP_LRU:
case LOCAL_OVERFLOW:
case LOCAL_PERSISTENT:
case LOCAL_PERSISTENT_OVERFLOW:
return true;
default:
return false;
}
}
/**
* Determines whether the client {@link ClientRegionShortcut} is a proxy-based shortcut.
* NOTE: "proxy"-based Regions keep no local state.
*
* @param shortcut the client {@link ClientRegionShortcut} to evaluate.
* @return a boolean value indicating whether the client {@link ClientRegionShortcut} refers to
* a proxy-based shortcut.
* @see com.gemstone.gemfire.cache.client.ClientRegionShortcut
*/
public static boolean isProxy(ClientRegionShortcut shortcut) {
switch (shortcut) {
case PROXY:
return true;
default:
return false;
}
}
/**
* Determines whether the peer {@link RegionShortcut} is a proxy-based shortcut. NOTE: "proxy"-based Regions
* keep no local state.
*
* @param shortcut the peer {@link RegionShortcut} to evaluate.
* @return a boolean value indicating whether the peer {@link RegionShortcut} refers to a proxy-based shortcut.
* @see com.gemstone.gemfire.cache.RegionShortcut
*/
public static boolean isProxy(RegionShortcut shortcut) {
switch (shortcut) {
case PARTITION_PROXY:
case PARTITION_PROXY_REDUNDANT:
case REPLICATE_PROXY:
return true;
default:
return false;
}
}
/**
* Converts a {@link Region} name to a {@link Region} path.
*
* @param regionName a String specifying the name of the {@link Region}.
* @return a String path for the given {@link Region} by name.
* @see com.gemstone.gemfire.cache.Region#getFullPath()
* @see com.gemstone.gemfire.cache.Region#getName()
*/
public static String toRegionPath(String regionName) {
return String.format("%1$s%2$s", Region.SEPARATOR, regionName);
}
}