diff --git a/pom.xml b/pom.xml index c5b3cac..dee08b2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,12 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 com.amazonaws aws-dynamodb-session-tomcat jar Amazon DynamoDB Session Manager for Tomcat - 1.2 + 1.0.5 The Amazon DynamoDB Session Manager for Tomcat provides a custom session manager for Tomcat 7 that stores session data in Amazon DynamoDB, Amazon's fully managed NoSQL database service. https://aws.amazon.com/java @@ -34,145 +34,173 @@ - - com.amazonaws - aws-java-sdk - 1.5.4 - - - org.apache.tomcat - tomcat-catalina - 7.0.47 - + + com.amazonaws + aws-java-sdk-dynamodb + 1.9.23 + + + org.apache.tomcat + tomcat-catalina + 7.0.42 + provided + + + org.mockito + mockito-core + 1.10.19 + test + + + junit + junit + 4.12 + test + - - - ${basedir} - - LICENSE.txt - NOTICE.txt - README.txt - - - + + + ${basedir} + + LICENSE.txt + NOTICE.txt + README.md + + + - - - org.apache.maven.plugins - maven-compiler-plugin - 2.3 - - 1.7 - 1.7 - UTF-8 - - + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3 + + 1.6 + 1.6 + UTF-8 + + - - org.apache.maven.plugins - maven-shade-plugin - 2.2 - - - package - - shade - - - true + + org.apache.maven.plugins + maven-shade-plugin + 2.2 + + + package + + shade + + + + + + false + + + true - - - com.amazonaws - com.amazonaws.tomcatsessionmanager.amazonaws - - - com.amazonaws.services.dynamodb.sessionmanager.DynamoDBSessionManager - - - - org.apache.http - com.amazonaws.tomcatsessionmanager.apache.http - - - org.apache.commons.logging - com.amazonaws.tomcatsessionmanager.apache.commons.logging - - - org.apache.commons.codec - com.amazonaws.tomcatsessionmanager.apache.commons.codec - - - - org.codehaus - com.amazonaws.tomcatsessionmanager.codehaus - - - - - - com.amazonaws:aws-java-sdk - commons-logging:* - org.apache.httpcomponents:* - commons-codec:* - org.codehaus.jackson:* - - + + + com.amazonaws + com.amazonaws.tomcatsessionmanager.amazonaws + + + com.amazonaws.services.dynamodb.sessionmanager.DynamoDBSessionManager + + + + org.apache.http + com.amazonaws.tomcatsessionmanager.apache.http + + + org.apache.commons.logging + com.amazonaws.tomcatsessionmanager.apache.commons.logging + + + org.apache.commons.codec + com.amazonaws.tomcatsessionmanager.apache.commons.codec + + + com.fasterxml + com.amazonaws.tomcatsessionmanager.fasterxml + + + org.joda.time + com.amazonaws.tomcatsessionmanager.joda.time + + - - - - commons-logging:commons-logging - - ** - - - + + + com.amazonaws:aws-java-sdk* + commons-logging:* + org.apache.httpcomponents:* + commons-codec:* + com.fasterxml.jackson.core:* + joda-time:* + + - - - - - + + + + commons-logging:commons-logging + + ** + + + + com.fasterxml.jackson.core:* + + ** + + + + + + + + - - publishing - - - - - org.apache.maven.plugins - maven-gpg-plugin - - - sign-artifacts - verify - - sign - - - - + + publishing + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.5.1 - true - - sonatype-nexus-staging - https://oss.sonatype.org - true - - - - - + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.5.1 + true + + sonatype-nexus-staging + https://oss.sonatype.org + true + + + + + diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoDBSessionManager.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoDBSessionManager.java index 0a098b1..444a792 100644 --- a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoDBSessionManager.java +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoDBSessionManager.java @@ -16,30 +16,42 @@ import java.io.File; +import org.apache.catalina.Context; import org.apache.catalina.LifecycleException; import org.apache.catalina.session.PersistentManagerBase; import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; import com.amazonaws.AmazonClientException; +import com.amazonaws.ClientConfiguration; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; import com.amazonaws.auth.PropertiesCredentials; import com.amazonaws.internal.StaticCredentialsProvider; import com.amazonaws.regions.RegionUtils; +import com.amazonaws.services.dynamodb.sessionmanager.converters.DefaultTomcatSessionConverter; +import com.amazonaws.services.dynamodb.sessionmanager.converters.LegacyDynamoDBSessionItemConverter; +import com.amazonaws.services.dynamodb.sessionmanager.converters.LegacyTomcatSessionConverter; +import com.amazonaws.services.dynamodb.sessionmanager.converters.SessionConverter; +import com.amazonaws.services.dynamodb.sessionmanager.converters.TomcatSessionConverterChain; import com.amazonaws.services.dynamodb.sessionmanager.util.DynamoUtils; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.util.Tables; +import com.amazonaws.util.StringUtils; /** - * Tomcat 7.0 persistent session manager implementation that uses Amazon - * DynamoDB to store HTTP session data. + * Tomcat persistent session manager implementation that uses Amazon DynamoDB to store HTTP session + * data. */ public class DynamoDBSessionManager extends PersistentManagerBase { - private static final String DEFAULT_TABLE_NAME = "Tomcat_SessionState"; + public static final String DEFAULT_TABLE_NAME = "Tomcat_SessionState"; + private static final String USER_AGENT = "DynamoSessionManager/1.1"; private static final String name = "AmazonDynamoDBSessionManager"; - private static final String info = name + "/2.0"; + private static final String info = name + "/1.1"; private String regionId = "us-east-1"; private String endpoint; @@ -50,15 +62,12 @@ public class DynamoDBSessionManager extends PersistentManagerBase { private long writeCapacityUnits = 5; private boolean createIfNotExist = true; private String tableName = DEFAULT_TABLE_NAME; + private String proxyHost; + private Integer proxyPort; - private final DynamoDBSessionStore dynamoSessionStore; - - private static Log logger; - + private static final Log logger = LogFactory.getLog(DynamoDBSessionManager.class); public DynamoDBSessionManager() { - dynamoSessionStore = new DynamoDBSessionStore(); - setStore(dynamoSessionStore); setSaveOnRestart(true); // MaxInactiveInterval controls when sessions are removed from the store @@ -118,6 +127,13 @@ public void setCreateIfNotExist(boolean createIfNotExist) { this.createIfNotExist = createIfNotExist; } + public void setProxyHost(String proxyHost) { + this.proxyHost = proxyHost; + } + + public void setProxyPort(Integer proxyPort) { + this.proxyPort = proxyPort; + } // // Private Interface @@ -127,78 +143,146 @@ public void setCreateIfNotExist(boolean createIfNotExist) { protected void initInternal() throws LifecycleException { this.setDistributable(true); - // Grab the container's logger - logger = getContainer().getLogger(); - - AWSCredentialsProvider credentialsProvider = initCredentials(); - AmazonDynamoDBClient dynamo = new AmazonDynamoDBClient(credentialsProvider); - if (this.regionId != null) dynamo.setRegion(RegionUtils.getRegion(this.regionId)); - if (this.endpoint != null) dynamo.setEndpoint(this.endpoint); - - initDynamoTable(dynamo); - - // init session store - dynamoSessionStore.setDynamoClient(dynamo); - dynamoSessionStore.setSessionTableName(this.tableName); - - } - - @Override - protected synchronized void stopInternal() throws LifecycleException { - super.stopInternal(); + AmazonDynamoDBClient dynamoClient = createDynamoClient(); + initDynamoTable(dynamoClient); + DynamoSessionStorage sessionStorage = createSessionStorage(dynamoClient); + setStore(new DynamoDBSessionStore(sessionStorage)); + new ExpiredSessionReaperExecutor(new ExpiredSessionReaper(sessionStorage)); } - private void initDynamoTable(AmazonDynamoDBClient dynamo) { - boolean tableExists = DynamoUtils.doesTableExist(dynamo, this.tableName); - - if (!tableExists && !createIfNotExist) { - throw new AmazonClientException("Session table '" + tableName + "' does not exist, " - + "and automatic table creation has been disabled in context.xml"); + private AmazonDynamoDBClient createDynamoClient() { + AWSCredentialsProvider credentialsProvider = initCredentials(); + ClientConfiguration clientConfiguration = initClientConfiguration(); + AmazonDynamoDBClient dynamoClient = new AmazonDynamoDBClient(credentialsProvider, clientConfiguration); + if (this.regionId != null) { + dynamoClient.setRegion(RegionUtils.getRegion(this.regionId)); } - - if (!tableExists) DynamoUtils.createSessionTable(dynamo, this.tableName, - this.readCapacityUnits, this.writeCapacityUnits); - - DynamoUtils.waitForTableToBecomeActive(dynamo, this.tableName); + if (this.endpoint != null) { + dynamoClient.setEndpoint(this.endpoint); + } + return dynamoClient; } private AWSCredentialsProvider initCredentials() { - // Attempt to use any explicitly specified credentials first - if (accessKey != null || secretKey != null) { - getContainer().getLogger().debug("Reading security credentials from context.xml"); - if (accessKey == null || secretKey == null) { + // Attempt to use any credentials specified in context.xml first + if (credentialsExistInContextConfig()) { + // Fail fast if credentials aren't valid as user has likely made a configuration mistake + if (credentialsInContextConfigAreValid()) { throw new AmazonClientException("Incomplete AWS security credentials specified in context.xml."); } - getContainer().getLogger().debug("Using AWS access key ID and secret key from context.xml"); + debug("Using AWS access key ID and secret key from context.xml"); return new StaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)); } // Use any explicitly specified credentials properties file next if (credentialsFile != null) { try { - getContainer().getLogger().debug("Reading security credentials from properties file: " + credentialsFile); + debug("Reading security credentials from properties file: " + credentialsFile); PropertiesCredentials credentials = new PropertiesCredentials(credentialsFile); - getContainer().getLogger().debug("Using AWS credentials from file: " + credentialsFile); + debug("Using AWS credentials from file: " + credentialsFile); return new StaticCredentialsProvider(credentials); } catch (Exception e) { throw new AmazonClientException( - "Unable to read AWS security credentials from file specified in context.xml: " + credentialsFile, e); + "Unable to read AWS security credentials from file specified in context.xml: " + + credentialsFile, e); } } // Fall back to the default credentials chain provider if credentials weren't explicitly set AWSCredentialsProvider defaultChainProvider = new DefaultAWSCredentialsProviderChain(); if (defaultChainProvider.getCredentials() == null) { - getContainer().getLogger().debug("Loading security credentials from default credentials provider chain."); + debug("Loading security credentials from default credentials provider chain."); throw new AmazonClientException( - "Unable find AWS security credentials. " + - "Searched JVM system properties, OS env vars, and EC2 instance roles. " + - "Specify credentials in Tomcat's context.xml file or put them in one of the places mentioned above."); + "Unable find AWS security credentials. " + + "Searched JVM system properties, OS env vars, and EC2 instance roles. " + + "Specify credentials in Tomcat's context.xml file or put them in one of the places mentioned above."); } - getContainer().getLogger().debug("Using default AWS credentials provider chain to load credentials"); + debug("Using default AWS credentials provider chain to load credentials"); return defaultChainProvider; } + /** + * @return True if the user has set their AWS credentials either partially or completely in + * context.xml. False otherwise + */ + private boolean credentialsExistInContextConfig() { + return accessKey != null || secretKey != null; + } + + /** + * @return True if both the access key and secret key were set in context.xml config. False + * otherwise + */ + private boolean credentialsInContextConfigAreValid() { + return StringUtils.isNullOrEmpty(accessKey) || StringUtils.isNullOrEmpty(secretKey); + } + + private ClientConfiguration initClientConfiguration() { + ClientConfiguration clientConfiguration = new ClientConfiguration(); + clientConfiguration.setUserAgent(USER_AGENT); + + // Attempt to use an explicit proxy configuration + if (proxyHost != null || proxyPort != null) { + debug("Reading proxy settings from context.xml"); + if (proxyHost == null || proxyPort == null) { + throw new AmazonClientException("Incomplete proxy settings specified in context.xml." + + " Both proxy hot and proxy port needs to be specified"); + } + debug("Using proxy host and port from context.xml"); + clientConfiguration.withProxyHost(proxyHost).withProxyPort(proxyPort); + } + + return clientConfiguration; + } + + private void initDynamoTable(AmazonDynamoDBClient dynamo) { + boolean tableExists = Tables.doesTableExist(dynamo, this.tableName); + + if (!tableExists && !createIfNotExist) { + throw new AmazonClientException("Session table '" + tableName + "' does not exist, " + + "and automatic table creation has been disabled in context.xml"); + } + + if (!tableExists) { + DynamoUtils.createSessionTable(dynamo, this.tableName, this.readCapacityUnits, this.writeCapacityUnits); + } + + Tables.waitForTableToBecomeActive(dynamo, this.tableName); + } + + private DynamoSessionStorage createSessionStorage(AmazonDynamoDBClient dynamoClient) { + DynamoDBMapper dynamoMapper = DynamoUtils.createDynamoMapper(dynamoClient, tableName); + return new DynamoSessionStorage(dynamoMapper, getSessionConverter()); + } + + private SessionConverter getSessionConverter() { + ClassLoader classLoader = getAssociatedContext().getLoader().getClassLoader(); + LegacyTomcatSessionConverter legacyConverter = new LegacyTomcatSessionConverter(this, classLoader, + maxInactiveInterval); + DefaultTomcatSessionConverter defaultConverter = new DefaultTomcatSessionConverter(this, classLoader); + // Converter compatible with the legacy schema but can understand new schema + return new SessionConverter(TomcatSessionConverterChain.wrap(legacyConverter, defaultConverter), + new LegacyDynamoDBSessionItemConverter()); + } + + /** + * To be compatible with Tomcat7 we have to call the getContainer method rather than getContext. + * The cast is safe as it only makes sense to use a session manager within the context of a + * webapp, the Tomcat 8 version of getContainer just delegates to getContext. When Tomcat7 is no + * longer supported this can be changed to getContext + * + * @return The context this manager is associated with + */ + // TODO Inline this method with getManager().getContext() when Tomcat7 is no longer supported + private Context getAssociatedContext() { + try { + return (Context) getContainer(); + } catch (ClassCastException e) { + logger.fatal("Unable to cast " + getClass().getName() + " to a Context." + + " DynamoDB SessionManager can only be used with a Context"); + throw new IllegalStateException(e); + } + } // // Logger Utility Functions diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoDBSessionStore.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoDBSessionStore.java index c9df083..3b97dc1 100644 --- a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoDBSessionStore.java +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoDBSessionStore.java @@ -14,204 +14,93 @@ */ package com.amazonaws.services.dynamodb.sessionmanager; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.nio.ByteBuffer; import java.util.Collections; import java.util.HashSet; -import java.util.List; import java.util.Set; -import org.apache.catalina.Container; -import org.apache.catalina.Loader; import org.apache.catalina.Session; -import org.apache.catalina.session.StandardSession; import org.apache.catalina.session.StoreBase; -import org.apache.catalina.util.CustomObjectInputStream; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; -import com.amazonaws.services.dynamodb.sessionmanager.util.DynamoUtils; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; -import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; -import com.amazonaws.services.dynamodbv2.model.TableDescription; +import com.amazonaws.services.dynamodb.sessionmanager.util.ValidatorUtils; /** - * Session store implementation that loads and stores HTTP sessions from Amazon - * DynamoDB. + * Session store implementation that loads and stores HTTP sessions from Amazon DynamoDB. */ public class DynamoDBSessionStore extends StoreBase { - private static final String name = "AmazonDynamoDBSessionStore"; - private static final String info = name + "/1.0"; - - private AmazonDynamoDBClient dynamo; - private String sessionTableName; - - private Set keys = Collections.synchronizedSet(new HashSet()); - private long keysTimestamp=0; - - @Override - public String getInfo() { - return info; - } - - @Override - public String getStoreName() { - return name; - } - - public void setDynamoClient(AmazonDynamoDBClient dynamo) { - this.dynamo = dynamo; - } - - public void setSessionTableName(String tableName) { - this.sessionTableName = tableName; - } - - @Override - public void clear() throws IOException { - final Set keysCopy = new HashSet(); - keysCopy.addAll(keys); - - new Thread("dynamodb-session-manager-clear") { - @Override - public void run() { - for (String sessionId : keysCopy) { - remove(sessionId); - } - } - }.start(); - - } - - @Override - public int getSize() throws IOException { - TableDescription table = dynamo.describeTable(new DescribeTableRequest().withTableName(sessionTableName)) - .getTable(); - long itemCount = table.getItemCount(); - - return (int) itemCount; - } - - @Override - public String[] keys() throws IOException { - // refresh the keys stored in memory in every hour. - if(keysTimestamp list=DynamoUtils.loadKeys(dynamo, sessionTableName); - keys.clear(); - keys.addAll(list); - keysTimestamp=System.currentTimeMillis(); - } - - return keys.toArray(new String[0]); - - } - - @Override - public Session load(String id) throws ClassNotFoundException, IOException { - - ByteBuffer byteBuffer = DynamoUtils.loadItemBySessionId(dynamo, sessionTableName, id); - if (byteBuffer == null) { - keys.remove(id); - return (null); - } - - if (manager.getContainer().getLogger().isDebugEnabled()) { - manager.getContainer().getLogger().debug(sm.getString(getStoreName() + ".loading", id, sessionTableName)); - } - - ByteArrayInputStream fis = null; - BufferedInputStream bis = null; - ObjectInputStream ois = null; - Loader loader = null; - ClassLoader classLoader = null; - try { - fis = new ByteArrayInputStream(byteBuffer.array()); - bis = new BufferedInputStream(fis); - Container container = manager.getContainer(); - if (container != null) { - loader = container.getLoader(); - } - if (loader != null) { - classLoader = loader.getClassLoader(); - } - if (classLoader != null) { - ois = new CustomObjectInputStream(bis, classLoader); - } else { - ois = new ObjectInputStream(bis); - } - } catch (Exception e) { - if (bis != null) { - try { - bis.close(); - } catch (IOException f) { - } - } - if (fis != null) { - try { - fis.close(); - } catch (IOException f) { - } - } - throw e; - } - - try { - StandardSession session = (StandardSession) manager.createEmptySession(); - session.readObjectData(ois); - session.setManager(manager); - keys.add(id); - return (session); - } finally { - try { - ois.close(); - } catch (IOException f) { - } - } - } - - @Override - public void save(Session session) throws IOException { - - String id = session.getIdInternal(); - - if (manager.getContainer().getLogger().isDebugEnabled()) { - manager.getContainer().getLogger() - .debug(sm.getString(getStoreName() + ".saving", id, sessionTableName)); - } - - ByteArrayOutputStream fos = new ByteArrayOutputStream(); - ObjectOutputStream oos = null; - try { - oos = new ObjectOutputStream(new BufferedOutputStream(fos)); - } catch (IOException e) { - try { - fos.close(); - } catch (IOException f) { - } - throw e; - } - - try { - ((StandardSession) session).writeObjectData(oos); - } finally { - oos.close(); - } - DynamoUtils.storeSession(dynamo, sessionTableName, id, ByteBuffer.wrap(fos.toByteArray())); - keys.add(id); - } - - @Override - public void remove(String id) { - if (manager.getContainer().getLogger().isDebugEnabled()) { - manager.getContainer().getLogger().debug(sm.getString(getStoreName() + ".removing", id, sessionTableName)); - } - DynamoUtils.deleteSession(dynamo, sessionTableName, id); - keys.remove(id); - } -} \ No newline at end of file + private static final Log logger = LogFactory.getLog(DynamoDBSessionStore.class); + private static final String name = "AmazonDynamoDBSessionStore"; + private static final String info = name + "/1.0"; + + private final Set sessionIds = Collections.synchronizedSet(new HashSet()); + private final DynamoSessionStorage sessionStorage; + + public DynamoDBSessionStore(DynamoSessionStorage sessionStorage) { + ValidatorUtils.nonNull(sessionStorage, "SessionStorage"); + this.sessionStorage = sessionStorage; + } + + @Override + public String getInfo() { + return info; + } + + @Override + public String getStoreName() { + return name; + } + + @Override + public void clear() throws IOException { + synchronized (sessionIds) { + final Set sessionsToDelete = new HashSet(sessionIds); + new Thread("dynamodb-session-manager-clear") { + @Override + public void run() { + for (String sessionId : sessionsToDelete) { + sessionStorage.deleteSession(sessionId); + } + } + }.start(); + sessionIds.clear(); + } + } + + @Override + public int getSize() throws IOException { + return sessionStorage.count(); + } + + @Override + public String[] keys() throws IOException { + return sessionIds.toArray(new String[0]); + } + + @Override + public Session load(String id) throws ClassNotFoundException, IOException { + Session session = sessionStorage.loadSession(id); + if (session == null) { + logger.warn("Unable to load session with id " + id); + return null; + } + + sessionIds.add(id); + return session; + } + + @Override + public void save(Session session) throws IOException { + sessionStorage.saveSession(session); + sessionIds.add(session.getId()); + } + + @Override + public void remove(String id) throws IOException { + sessionStorage.deleteSession(id); + sessionIds.remove(id); + } + +} diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoSessionItem.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoSessionItem.java new file mode 100644 index 0000000..23fd5c8 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoSessionItem.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager; + +import java.nio.ByteBuffer; + +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; + +@DynamoDBTable(tableName = DynamoDBSessionManager.DEFAULT_TABLE_NAME) +public class DynamoSessionItem { + + public static final String SESSION_ID_ATTRIBUTE_NAME = "sessionId"; + public static final String SESSION_DATA_ATTRIBUTE_NAME = "sessionData"; + public static final String CREATED_AT_ATTRIBUTE_NAME = "createdAt"; + public static final String LAST_UPDATED_AT_ATTRIBUTE_NAME = "lastUpdatedAt"; + + private String sessionId; + private ByteBuffer sessionData; + + // Legacy item attributes + private long lastUpdatedTime; + private long createdTime; + + public DynamoSessionItem() { + } + + public DynamoSessionItem(String id) { + this.sessionId = id; + } + + @DynamoDBHashKey(attributeName = SESSION_ID_ATTRIBUTE_NAME) + public String getSessionId() { + return sessionId; + } + + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + @DynamoDBAttribute(attributeName = SESSION_DATA_ATTRIBUTE_NAME) + public ByteBuffer getSessionData() { + return sessionData; + } + + public void setSessionData(ByteBuffer sessionData) { + this.sessionData = sessionData; + } + + /** + * @deprecated Part of the legacy item format. Will be removed in a later version + */ + @Deprecated + @DynamoDBAttribute(attributeName = LAST_UPDATED_AT_ATTRIBUTE_NAME) + public long getLastUpdatedTime() { + return lastUpdatedTime; + } + + /** + * @deprecated Part of the legacy item format. Will be removed in a later version + */ + @Deprecated + public void setLastUpdatedTime(long lastUpdatedTime) { + this.lastUpdatedTime = lastUpdatedTime; + } + + /** + * @deprecated Part of the legacy item format. Will be removed in a later version + */ + @Deprecated + @DynamoDBAttribute(attributeName = CREATED_AT_ATTRIBUTE_NAME) + public long getCreatedTime() { + return createdTime; + } + + /** + * @deprecated Part of the legacy item format. Will be removed in a later version + */ + @Deprecated + public void setCreatedTime(long createdTime) { + this.createdTime = createdTime; + } + +} diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoSessionStorage.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoSessionStorage.java new file mode 100644 index 0000000..2f4760f --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/DynamoSessionStorage.java @@ -0,0 +1,122 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager; + +import java.util.Collections; +import java.util.Iterator; + +import org.apache.catalina.Session; + +import com.amazonaws.services.dynamodb.sessionmanager.converters.SessionConverter; +import com.amazonaws.services.dynamodb.sessionmanager.util.ValidatorUtils; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBScanExpression; +import com.amazonaws.services.dynamodbv2.datamodeling.PaginatedScanList; + +public class DynamoSessionStorage { + + private final DynamoDBMapper mapper; + private final SessionConverter sessionConverter; + + public DynamoSessionStorage(DynamoDBMapper dynamoMapper, SessionConverter sessionConverter) { + ValidatorUtils.nonNull(dynamoMapper, "DynamoDBMapper"); + ValidatorUtils.nonNull(sessionConverter, "SessionConverter"); + this.mapper = dynamoMapper; + this.sessionConverter = sessionConverter; + } + + public int count() { + return mapper.count(DynamoSessionItem.class, new DynamoDBScanExpression()); + } + + public Session loadSession(String sessionId) { + DynamoSessionItem sessionItem = mapper.load(new DynamoSessionItem(sessionId)); + if (sessionItem != null) { + return sessionConverter.toSession(sessionItem); + } else { + return null; + } + } + + public void deleteSession(String sessionId) { + mapper.delete(new DynamoSessionItem(sessionId)); + } + + public void saveSession(Session session) { + mapper.save(sessionConverter.toSessionItem(session)); + } + + public Iterable listSessions() { + PaginatedScanList sessions = mapper.scan(DynamoSessionItem.class, + new DynamoDBScanExpression()); + return new SessionConverterIterable(sessions); + } + + private class SessionConverterIterable implements Iterable { + + private final Iterable sessionIterable; + + private SessionConverterIterable(Iterable sessionIterable) { + this.sessionIterable = sessionIterable; + } + + @Override + public Iterator iterator() { + return new SessionConverterIterator(getIteratorSafe(sessionIterable)); + } + + /** + * Returns either the Iterator for a given Iterable or an empty Iterator but not null. + */ + private Iterator getIteratorSafe(Iterable iterable) { + if (iterable != null) { + return iterable.iterator(); + } else { + return Collections. emptyList().iterator(); + } + } + + } + + /** + * Custom iterator to convert a {@link DynamoSessionItem} to a Tomcat {@link Session} before + * returning it + */ + private class SessionConverterIterator implements Iterator { + + private final Iterator sessionItemterator; + + private SessionConverterIterator(Iterator sessionItemIterator) { + this.sessionItemterator = sessionItemIterator; + } + + @Override + public boolean hasNext() { + return sessionItemterator.hasNext(); + } + + @Override + public Session next() { + return sessionConverter.toSession(sessionItemterator.next()); + } + + @Override + public void remove() { + sessionItemterator.remove(); + } + + } + +} diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/ExpiredSessionReaper.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/ExpiredSessionReaper.java new file mode 100644 index 0000000..98e157d --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/ExpiredSessionReaper.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager; + +import java.util.concurrent.TimeUnit; + +import org.apache.catalina.Session; + +import com.amazonaws.services.dynamodb.sessionmanager.util.ValidatorUtils; + +/** + * Scans Session table and deletes any sessions that have expired + */ +public class ExpiredSessionReaper implements Runnable { + + private final DynamoSessionStorage sessionStorage; + + public ExpiredSessionReaper(DynamoSessionStorage sessionStorage) { + ValidatorUtils.nonNull(sessionStorage, "SessionStorage"); + this.sessionStorage = sessionStorage; + } + + /** + * Scans the session table for expired sessions and deletes them. + */ + @Override + public void run() { + Iterable sessions = sessionStorage.listSessions(); + for (Session session : sessions) { + if (ExpiredSessionReaper.isExpired(session)) { + sessionStorage.deleteSession(session.getId()); + } + } + } + + public static boolean isExpired(Session session) { + if (canSessionExpire(session)) { + return session.getLastAccessedTimeInternal() < getInactiveCutoffTime(session); + } + return false; + } + + /** + * Sessions with a negative max inactive time never expire + */ + private static boolean canSessionExpire(Session session) { + return session.getMaxInactiveInterval() > 0; + } + + /** + * Any sessions whose access time is older than the cutoff time are considered inactive, those + * with access time after the cutoff time are still active + */ + private static long getInactiveCutoffTime(Session session) { + return System.currentTimeMillis() + - TimeUnit.MILLISECONDS.convert(session.getMaxInactiveInterval(), TimeUnit.SECONDS); + } + +} \ No newline at end of file diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/ExpiredSessionReaperExecutor.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/ExpiredSessionReaperExecutor.java new file mode 100644 index 0000000..8f880fc --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/ExpiredSessionReaperExecutor.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager; + +import java.util.Random; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * A background process to periodically scan and remove any expired session data from the session + * table in Amazon DynamoDB. + */ +public class ExpiredSessionReaperExecutor { + + private static final int REAP_FREQUENCY_HOURS = 12; + private static final int MAX_JITTER_HOURS = 5; + private static final String THREAD_NAME = "dynamo-session-manager-expired-sesion-reaper"; + + private final ScheduledThreadPoolExecutor executor; + + public ExpiredSessionReaperExecutor(Runnable expiredSessionRunnable) { + int initialDelay = new Random().nextInt(MAX_JITTER_HOURS) + 1; + executor = new ScheduledThreadPoolExecutor(1, new ExpiredSessionReaperThreadFactory()); + executor.scheduleAtFixedRate(expiredSessionRunnable, initialDelay, REAP_FREQUENCY_HOURS, TimeUnit.HOURS); + } + + /** + * Shuts down the expired session reaper. + */ + public void shutdown() { + executor.shutdown(); + } + + /** + * ThreadFactory for creating the daemon reaper thread. + */ + private final class ExpiredSessionReaperThreadFactory implements ThreadFactory { + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable); + thread.setDaemon(true); + thread.setName(THREAD_NAME); + return thread; + } + } +} diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DefaultDynamoSessionItemConverter.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DefaultDynamoSessionItemConverter.java new file mode 100644 index 0000000..913aea3 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DefaultDynamoSessionItemConverter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import java.io.ByteArrayOutputStream; +import java.io.ObjectOutputStream; +import java.nio.ByteBuffer; + +import org.apache.catalina.Session; +import org.apache.catalina.session.StandardSession; + +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; +import com.amazonaws.util.IOUtils; + +public class DefaultDynamoSessionItemConverter implements DynamoSessionItemConverter { + + @Override + public DynamoSessionItem toSessionItem(Session session) { + ObjectOutputStream oos = null; + try { + ByteArrayOutputStream fos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(fos); + ((StandardSession) session).writeObjectData(oos); + oos.close(); + DynamoSessionItem sessionItem = new DynamoSessionItem(session.getIdInternal()); + sessionItem.setSessionData(ByteBuffer.wrap(fos.toByteArray())); + return sessionItem; + } catch (Exception e) { + IOUtils.closeQuietly(oos, null); + throw new SessionConversionException("Unable to convert Tomcat Session into Dynamo storage representation", + e); + } + } +} diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DefaultTomcatSessionConverter.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DefaultTomcatSessionConverter.java new file mode 100644 index 0000000..b16e095 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DefaultTomcatSessionConverter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import java.io.ByteArrayInputStream; +import java.io.ObjectInputStream; + +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.session.StandardSession; +import org.apache.catalina.util.CustomObjectInputStream; + +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; +import com.amazonaws.services.dynamodb.sessionmanager.util.ValidatorUtils; +import com.amazonaws.util.IOUtils; + +public class DefaultTomcatSessionConverter implements TomcatSessionConverter { + + private final ClassLoader classLoader; + private final Manager manager; + + public DefaultTomcatSessionConverter(Manager manager, ClassLoader classLoader) { + ValidatorUtils.nonNull(manager, "Manager"); + ValidatorUtils.nonNull(classLoader, "ClassLoader"); + this.classLoader = classLoader; + this.manager = manager; + } + + @Override + public Session toSession(DynamoSessionItem sessionItem) { + ObjectInputStream ois = null; + try { + ByteArrayInputStream fis = new ByteArrayInputStream(sessionItem.getSessionData().array()); + ois = new CustomObjectInputStream(fis, classLoader); + + StandardSession session = new StandardSession(manager); + session.readObjectData(ois); + return session; + } catch (Exception e) { + throw new SessionConversionException("Unable to convert Dynamo storage representation to a Tomcat Session", + e); + } finally { + IOUtils.closeQuietly(ois, null); + } + } + +} diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DynamoSessionItemConverter.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DynamoSessionItemConverter.java new file mode 100644 index 0000000..28d4855 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DynamoSessionItemConverter.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import org.apache.catalina.Session; + +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; + +public interface DynamoSessionItemConverter { + + /** + * Converts the Tomcat {@link Session} into a {@link DynamoSessionItem} to be persisted to + * DynamoDB + */ + DynamoSessionItem toSessionItem(Session session); +} diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/LegacyDynamoDBSessionItemConverter.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/LegacyDynamoDBSessionItemConverter.java new file mode 100644 index 0000000..4e2f3f3 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/LegacyDynamoDBSessionItemConverter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.nio.ByteBuffer; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpSession; + +import org.apache.catalina.Session; + +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; + +public class LegacyDynamoDBSessionItemConverter implements DynamoSessionItemConverter { + + @Override + public DynamoSessionItem toSessionItem(Session session) { + try { + DynamoSessionItem sessionItem = new DynamoSessionItem(session.getIdInternal()); + sessionItem.setCreatedTime(session.getCreationTimeInternal()); + sessionItem.setLastUpdatedTime(session.getLastAccessedTimeInternal()); + sessionItem.setSessionData(sessionDataToByteBuffer(session)); + return sessionItem; + } catch (Exception e) { + throw new SessionConversionException("Unable to convert Tomcat Session into Dynamo storage representation", + e); + } + } + + private static ByteBuffer sessionDataToByteBuffer(Session session) throws IOException { + Map getterReturnResult = sessionDataToMap(session); + return objectToByteBuffer(getterReturnResult); + } + + private static Map sessionDataToMap(Session session) { + HttpSession httpSession = session.getSession(); + Map sessionAttributes = new HashMap(); + Enumeration attributeNames = httpSession.getAttributeNames(); + while (attributeNames.hasMoreElements()) { + String attributeName = attributeNames.nextElement(); + Object attributeValue = httpSession.getAttribute(attributeName); + sessionAttributes.put(attributeName, attributeValue); + } + return sessionAttributes; + } + + static ByteBuffer objectToByteBuffer(Object object) throws IOException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); + objectOutputStream.writeObject(object); + objectOutputStream.close(); + byte[] byteArray = byteArrayOutputStream.toByteArray(); + return ByteBuffer.wrap(byteArray); + } +} diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/LegacyTomcatSessionConverter.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/LegacyTomcatSessionConverter.java new file mode 100644 index 0000000..07787f0 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/LegacyTomcatSessionConverter.java @@ -0,0 +1,110 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.session.StandardSession; +import org.apache.catalina.util.CustomObjectInputStream; + +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; +import com.amazonaws.services.dynamodb.sessionmanager.util.ValidatorUtils; +import com.amazonaws.util.BinaryUtils; +import com.amazonaws.util.IOUtils; + +public class LegacyTomcatSessionConverter implements TomcatSessionConverter { + + private final Manager manager; + private final ClassLoader classLoader; + private final int maxInactiveInterval; + + public LegacyTomcatSessionConverter(Manager manager, ClassLoader classLoader, int maxInactiveInterval) { + ValidatorUtils.nonNull(manager, "Manager"); + ValidatorUtils.nonNull(classLoader, "ClassLoader"); + this.manager = manager; + this.classLoader = classLoader; + this.maxInactiveInterval = maxInactiveInterval; + } + + @Override + public Session toSession(DynamoSessionItem sessionItem) { + try { + LegacySession session = new LegacySession(null); + session.setValid(true); + session.setId(sessionItem.getSessionId(), false); + session.setCreationTime(sessionItem.getCreatedTime()); + session.setLastAccessedTime(sessionItem.getLastUpdatedTime()); + session.setMaxInactiveInterval(maxInactiveInterval); + session.setSessionAttributes(unmarshallSessionData(sessionItem)); + session.setManager(manager); + return session; + } catch (Exception e) { + throw new SessionConversionException("Unable to convert Dynamo storage representation to a Tomcat Session", + e); + } + } + + @SuppressWarnings("unchecked") + private Map unmarshallSessionData(DynamoSessionItem sessionItem) throws IOException, + ClassNotFoundException { + ByteBuffer rawSessionData = sessionItem.getSessionData(); + + Object marshalledSessionData; + ObjectInputStream objectInputStream = null; + try { + ByteArrayInputStream inputStream = new ByteArrayInputStream(BinaryUtils.copyAllBytesFrom(rawSessionData)); + objectInputStream = new CustomObjectInputStream(inputStream, classLoader); + marshalledSessionData = objectInputStream.readObject(); + } finally { + IOUtils.closeQuietly(objectInputStream, null); + } + if (!(marshalledSessionData instanceof Map)) { + throw new SessionConversionException("Unable to unmarshall session attributes from DynamoDB store"); + } + return (Map) marshalledSessionData; + } + + /** + * Subclassed standard session to allow setting lastAccessedTime through means other than + * readObject + */ + public static class LegacySession extends StandardSession { + + private static final long serialVersionUID = 9163946735192227235L; + + public LegacySession(Manager manager) { + super(manager); + } + + public void setLastAccessedTime(long lastAccessedTime) { + this.lastAccessedTime = lastAccessedTime; + this.thisAccessedTime = lastAccessedTime; + } + + public void setSessionAttributes(Map attributes) { + for (Entry attribute : attributes.entrySet()) { + this.setAttribute(attribute.getKey(), attribute.getValue(), false); + } + } + + } +} diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/SessionConversionException.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/SessionConversionException.java new file mode 100644 index 0000000..9545c64 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/SessionConversionException.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +public class SessionConversionException extends RuntimeException { + + private static final long serialVersionUID = -1123132762994997791L; + + public SessionConversionException(String message) { + super(message); + } + + public SessionConversionException(String message, Throwable t) { + super(message, t); + } +} \ No newline at end of file diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/SessionConverter.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/SessionConverter.java new file mode 100644 index 0000000..07ad012 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/SessionConverter.java @@ -0,0 +1,65 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import org.apache.catalina.Manager; +import org.apache.catalina.Session; + +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; +import com.amazonaws.services.dynamodb.sessionmanager.util.ValidatorUtils; + +public final class SessionConverter implements TomcatSessionConverter, DynamoSessionItemConverter { + + private final TomcatSessionConverter fromDynamo; + private final DynamoSessionItemConverter toDynamo; + + public SessionConverter(TomcatSessionConverter fromDynamo, DynamoSessionItemConverter toDynamo) { + ValidatorUtils.nonNull(fromDynamo, "TomcatSessionConverter"); + ValidatorUtils.nonNull(toDynamo, "DynamoSessionItemConverter"); + this.fromDynamo = fromDynamo; + this.toDynamo = toDynamo; + } + + @Override + public DynamoSessionItem toSessionItem(Session session) { + return toDynamo.toSessionItem(session); + } + + @Override + public Session toSession(DynamoSessionItem dynamoSessionItem) { + return fromDynamo.toSession(dynamoSessionItem); + } + + /** + * Factory method to create a SessionConverter with the default implementation of + * TomcatSessionConverter and DynamoSessionConverter + */ + public static SessionConverter createDefaultSessionConverter(Manager manager, ClassLoader classLoader) { + return new SessionConverter(new DefaultTomcatSessionConverter(manager, classLoader), + new DefaultDynamoSessionItemConverter()); + } + + /** + * Factory method to create a SessionConverter with the legacy implementation of + * TomcatSessionConverter and DynamoSessionConverter + */ + public static SessionConverter createLegacySessionConverter(Manager manager, + ClassLoader classLoader, + int maxInactiveInterval) { + return new SessionConverter(new LegacyTomcatSessionConverter(manager, classLoader, maxInactiveInterval), + new LegacyDynamoDBSessionItemConverter()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TomcatSessionConverter.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TomcatSessionConverter.java new file mode 100644 index 0000000..6d03b82 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TomcatSessionConverter.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import org.apache.catalina.Session; + +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; + +public interface TomcatSessionConverter { + + /** + * Converts a {@link DynamoSessionItem} into a Tomcat {@link Session} + */ + Session toSession(DynamoSessionItem sessionItem); +} diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TomcatSessionConverterChain.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TomcatSessionConverterChain.java new file mode 100644 index 0000000..8838710 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TomcatSessionConverterChain.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import java.util.Arrays; +import java.util.List; + +import org.apache.catalina.Session; + +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; + +public class TomcatSessionConverterChain implements TomcatSessionConverter { + + private final List tomcatSessionCoverters; + + private TomcatSessionConverterChain(List tomcatSessionConverters) { + this.tomcatSessionCoverters = tomcatSessionConverters; + } + + @Override + public Session toSession(DynamoSessionItem sessionItem) { + for (TomcatSessionConverter converter : tomcatSessionCoverters) { + try { + return converter.toSession(sessionItem); + } catch (SessionConversionException e) { + // Try next converter in chain + } + } + throw new SessionConversionException( + "Unable to convert Dynamo storage representation to a Tomcat Session with any converter provided"); + } + + public static TomcatSessionConverter wrap(TomcatSessionConverter... converters) { + return new TomcatSessionConverterChain(Arrays.asList(asArray(converters))); + } + + private static TomcatSessionConverter[] asArray(TomcatSessionConverter[] converters) { + return converters; + } +} diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/util/DynamoUtils.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/util/DynamoUtils.java index ae072c4..54b7423 100644 --- a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/util/DynamoUtils.java +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/util/DynamoUtils.java @@ -14,184 +14,43 @@ */ package com.amazonaws.services.dynamodb.sessionmanager.util; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import com.amazonaws.AmazonClientException; -import com.amazonaws.AmazonServiceException; -import com.amazonaws.AmazonWebServiceRequest; -import com.amazonaws.services.dynamodb.sessionmanager.DynamoDBSessionManager; -import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapperConfig.TableNameOverride; import com.amazonaws.services.dynamodbv2.model.AttributeDefinition; -import com.amazonaws.services.dynamodbv2.model.AttributeValue; import com.amazonaws.services.dynamodbv2.model.CreateTableRequest; -import com.amazonaws.services.dynamodbv2.model.DeleteItemRequest; -import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; -import com.amazonaws.services.dynamodbv2.model.GetItemRequest; import com.amazonaws.services.dynamodbv2.model.KeySchemaElement; import com.amazonaws.services.dynamodbv2.model.KeyType; import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput; -import com.amazonaws.services.dynamodbv2.model.PutItemRequest; import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType; -import com.amazonaws.services.dynamodbv2.model.ScanRequest; -import com.amazonaws.services.dynamodbv2.model.ScanResult; -import com.amazonaws.services.dynamodbv2.model.Select; -import com.amazonaws.services.dynamodbv2.model.TableDescription; -import com.amazonaws.services.dynamodbv2.model.TableStatus; -/** - * Utilities for working with Amazon DynamoDB for session management. - */ public class DynamoUtils { - public static final String SESSION_ID_KEY = "sessionId"; - public static final String SESSION_DATA_ATTRIBUTE = "sessionData"; - - public static List loadKeys(AmazonDynamoDB dynamo, String tableName) { - ScanRequest request = new ScanRequest(tableName); - request.setSelect(Select.SPECIFIC_ATTRIBUTES); - request.withAttributesToGet(SESSION_ID_KEY); - - ArrayList list = new ArrayList(); - ScanResult scanResult = null; - do { - if (scanResult != null) - request.setExclusiveStartKey(scanResult.getLastEvaluatedKey()); - - scanResult = dynamo.scan(request); - List> items = scanResult.getItems(); - for (Map item : items) { - list.add(item.get(SESSION_ID_KEY).getS()); - } - } while (scanResult.getLastEvaluatedKey() != null); - - return list; - } - - public static ByteBuffer loadItemBySessionId(AmazonDynamoDB dynamo, String tableName, String sessionId) { - Map key = newAttributeValueMap(); - key.put(SESSION_ID_KEY, new AttributeValue(sessionId)); - GetItemRequest request = new GetItemRequest(tableName, key); - addClientMarker(request); - try { - Map item = dynamo.getItem(request).getItem(); - if (item == null || !item.containsKey(SESSION_ID_KEY) || !item.containsKey(SESSION_DATA_ATTRIBUTE)) { - DynamoDBSessionManager.warn("Unable to load session attributes for session " + sessionId); - return null; - } - return item.get(SESSION_DATA_ATTRIBUTE).getB(); - } catch (Exception e) { - DynamoDBSessionManager.warn("Unable to load session " + sessionId, e); - } - return null; - } - - public static void deleteSession(AmazonDynamoDB dynamo, String tableName, String sessionId) { - Map key = newAttributeValueMap(); - key.put(SESSION_ID_KEY, new AttributeValue(sessionId)); - - DeleteItemRequest request = new DeleteItemRequest(tableName, key); - addClientMarker(request); - - try { - dynamo.deleteItem(request); - } catch (Exception e) { - DynamoDBSessionManager.warn("Unable to delete session " + sessionId, e); - } - } - - public static void storeSession(AmazonDynamoDB dynamo, String tableName, String sessionId, ByteBuffer byteBuffer) - throws IOException { - - Map attributes = newAttributeValueMap(); - attributes.put(SESSION_ID_KEY, new AttributeValue(sessionId)); - attributes.put(SESSION_DATA_ATTRIBUTE, new AttributeValue().withB(byteBuffer)); - - try { - PutItemRequest request = new PutItemRequest(tableName, attributes); - addClientMarker(request); - dynamo.putItem(request); - } catch (Exception e) { - DynamoDBSessionManager.error("Unable to save session " + sessionId, e); - } - } - - public static boolean doesTableExist(AmazonDynamoDBClient dynamo, String tableName) { - try { - DescribeTableRequest request = new DescribeTableRequest().withTableName(tableName); - addClientMarker(request); - - TableDescription table = dynamo.describeTable(request).getTable(); - if (table == null) - return false; - else - return true; - } catch (AmazonServiceException ase) { - if (ase.getErrorCode().equalsIgnoreCase("ResourceNotFoundException")) - return false; - else - throw ase; - } - } - - public static void waitForTableToBecomeActive(AmazonDynamoDBClient dynamo, String tableName) { - long startTime = System.currentTimeMillis(); - long endTime = startTime + (10 * 60 * 1000); - while (System.currentTimeMillis() < endTime) { - try { - DescribeTableRequest request = new DescribeTableRequest().withTableName(tableName); - addClientMarker(request); - - TableDescription tableDescription = dynamo.describeTable(request).getTable(); - if (tableDescription == null) - continue; - - String tableStatus = tableDescription.getTableStatus(); - if (tableStatus.equals(TableStatus.ACTIVE.toString())) - return; - } catch (AmazonServiceException ase) { - if (ase.getErrorCode().equalsIgnoreCase("ResourceNotFoundException") == false) - throw ase; - } - - try { - Thread.sleep(1000 * 5); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new AmazonClientException("Interrupted while waiting for table '" + tableName - + "' to become active.", e); - } - } - - throw new AmazonClientException("Table '" + tableName + "' never became active"); - } - - public static void createSessionTable(AmazonDynamoDBClient dynamo, String tableName, long readCapacityUnits, - long writeCapacityUnits) { - CreateTableRequest request = new CreateTableRequest().withTableName(tableName); - addClientMarker(request); + public static void createSessionTable(AmazonDynamoDBClient dynamo, + String tableName, + long readCapacityUnits, + long writeCapacityUnits) { + CreateTableRequest request = new CreateTableRequest().withTableName(tableName); - request.withKeySchema(new KeySchemaElement().withAttributeName(SESSION_ID_KEY).withKeyType(KeyType.HASH)); + request.withKeySchema(new KeySchemaElement().withAttributeName(DynamoSessionItem.SESSION_ID_ATTRIBUTE_NAME) + .withKeyType(KeyType.HASH)); - request.withAttributeDefinitions(new AttributeDefinition().withAttributeName(SESSION_ID_KEY).withAttributeType( - ScalarAttributeType.S)); + request.withAttributeDefinitions(new AttributeDefinition().withAttributeName( + DynamoSessionItem.SESSION_ID_ATTRIBUTE_NAME).withAttributeType(ScalarAttributeType.S)); - request.setProvisionedThroughput(new ProvisionedThroughput().withReadCapacityUnits(readCapacityUnits) - .withWriteCapacityUnits(writeCapacityUnits)); + request.setProvisionedThroughput(new ProvisionedThroughput().withReadCapacityUnits(readCapacityUnits) + .withWriteCapacityUnits(writeCapacityUnits)); - dynamo.createTable(request); - } + dynamo.createTable(request); + } - public static void addClientMarker(AmazonWebServiceRequest request) { - request.getRequestClientOptions().addClientMarker("DynamoSessionManager/2.0"); - } + /** + * Create a new DynamoDBMapper with table name override + */ + public static DynamoDBMapper createDynamoMapper(AmazonDynamoDBClient dynamoDbClient, String tableName) { + return new DynamoDBMapper(dynamoDbClient, new DynamoDBMapperConfig(new TableNameOverride(tableName))); + } - private static Map newAttributeValueMap() { - return new HashMap(); - } } diff --git a/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/util/ValidatorUtils.java b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/util/ValidatorUtils.java new file mode 100644 index 0000000..6523844 --- /dev/null +++ b/src/main/java/com/amazonaws/services/dynamodb/sessionmanager/util/ValidatorUtils.java @@ -0,0 +1,10 @@ +package com.amazonaws.services.dynamodb.sessionmanager.util; + +public class ValidatorUtils { + + public static void nonNull(Object obj, String argName) { + if (obj == null) { + throw new IllegalArgumentException(String.format("%s cannot be null", argName)); + } + } +} diff --git a/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/CustomAsserts.java b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/CustomAsserts.java new file mode 100644 index 0000000..c3bcca6 --- /dev/null +++ b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/CustomAsserts.java @@ -0,0 +1,53 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager; + +import static org.junit.Assert.assertEquals; + +import java.util.Enumeration; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.servlet.http.HttpSession; + +import org.apache.catalina.Session; + +public class CustomAsserts { + + public static void assertSessionEquals(Session expectedSession, Session actualSession) { + assertEquals(expectedSession.getIdInternal(), actualSession.getIdInternal()); + assertEquals(expectedSession.getCreationTimeInternal(), actualSession.getCreationTimeInternal()); + assertEquals(expectedSession.getLastAccessedTimeInternal(), actualSession.getLastAccessedTimeInternal()); + assertSessionDataEquals(expectedSession.getSession(), actualSession.getSession()); + } + + public static void assertSessionDataEquals(HttpSession expectedSession, HttpSession actualSession) { + SortedSet expectedAttributeNames = toSortedSet(expectedSession.getAttributeNames()); + SortedSet actualAttributeNames = toSortedSet(actualSession.getAttributeNames()); + assertEquals(expectedAttributeNames, actualAttributeNames); + for (String attributeName : expectedAttributeNames) { + assertEquals(expectedSession.getAttribute(attributeName), actualSession.getAttribute(attributeName)); + } + } + + private static SortedSet toSortedSet(Enumeration enumeration) { + SortedSet list = new TreeSet(); + while (enumeration.hasMoreElements()) { + list.add(enumeration.nextElement()); + } + return list; + } + +} diff --git a/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/CustomSessionClass.java b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/CustomSessionClass.java new file mode 100644 index 0000000..a77c496 --- /dev/null +++ b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/CustomSessionClass.java @@ -0,0 +1,55 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager; + +import java.io.Serializable; + +/** + * Class to test serialization/deserialization of a custom object class in the session data + */ +public class CustomSessionClass implements Serializable { + + private static final long serialVersionUID = -4704592898757232021L; + private final String data; + + public CustomSessionClass(String data) { + this.data = data; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((data == null) ? 0 : data.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + CustomSessionClass other = (CustomSessionClass) obj; + if (data == null) { + if (other.data != null) + return false; + } else if (!data.equals(other.data)) + return false; + return true; + } +} \ No newline at end of file diff --git a/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/ExpiredSessionReaperTest.java b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/ExpiredSessionReaperTest.java new file mode 100644 index 0000000..c6f1bea --- /dev/null +++ b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/ExpiredSessionReaperTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.amazonaws.services.dynamodb.sessionmanager.converters.LegacyTomcatSessionConverter.LegacySession; +import com.amazonaws.services.dynamodb.sessionmanager.converters.TestSessionFactory; + +public class ExpiredSessionReaperTest { + + @Test + public void isExpired_ActiveSession_ReturnsFalse() { + assertFalse(ExpiredSessionReaper.isExpired(createActiveSession())); + } + + @Test + public void isExpired_ExpiredSession_ReturnsTrue() { + assertTrue(ExpiredSessionReaper.isExpired(createExpiredSession())); + } + + @Test + public void isExpired_ImmortalSession_ReturnsFalse() { + assertFalse(ExpiredSessionReaper.isExpired(createImmortalSession())); + } + + public static LegacySession createActiveSession() { + LegacySession activeSession = new TestSessionFactory().withSessionId("active") + .withLastAccessedTime(System.currentTimeMillis()).createLegacySession(); + return activeSession; + } + + public static LegacySession createExpiredSession() { + LegacySession expiredSession = new TestSessionFactory().withSessionId("expired").withLastAccessedTime(0) + .createLegacySession(); + return expiredSession; + } + + /** + * A negative value for maxInactiveInterval means the session never expires. isExpired should + * always return false no matter what + */ + public static LegacySession createImmortalSession() { + LegacySession immortalSession = new TestSessionFactory().withSessionId("immortal").withMaxInactiveInterval(-1) + .withLastAccessedTime(0).createLegacySession(); + return immortalSession; + } +} diff --git a/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DefaultSessionConverterTest.java b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DefaultSessionConverterTest.java new file mode 100644 index 0000000..d6bb045 --- /dev/null +++ b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/DefaultSessionConverterTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import static com.amazonaws.services.dynamodb.sessionmanager.CustomAsserts.assertSessionEquals; +import static org.junit.Assert.assertNull; + +import org.apache.catalina.Session; +import org.apache.catalina.session.StandardSession; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests a SessionConverter with Default implementations of TomcatSessionConverter and + * DynamoSessionConverter + */ +public class DefaultSessionConverterTest { + + private static final TestSessionFactory SESSION_TEMPLATE = new TestSessionFactory(); + + private SessionConverter sessionConverter; + private StandardSession session; + + @Before + public void setup() { + sessionConverter = SessionConverter.createDefaultSessionConverter(SESSION_TEMPLATE.getManager(), getClass() + .getClassLoader()); + session = SESSION_TEMPLATE.createStandardSession(); + } + + @Test + public void roundTrip_ReturnsSameSession() throws Exception { + Session roundTripSession = sessionConverter.toSession(sessionConverter.toSessionItem(session)); + assertSessionEquals(session, roundTripSession); + } + + @Test + public void roundTrip_NoSessionData_ReturnsSameSession() throws Exception { + StandardSession session = new TestSessionFactory().withSessionAttributes(null).createStandardSession(); + Session roundTripSession = sessionConverter.toSession(sessionConverter.toSessionItem(session)); + assertSessionEquals(session, roundTripSession); + } + + @Test(expected = SessionConversionException.class) + public void toSessionItem_NullSession_ThrowsSessionConversionException() { + assertNull(sessionConverter.toSessionItem(null)); + } + + @Test(expected = SessionConversionException.class) + public void toSession_NullSessionItem_ThrowsSessionConversionException() { + assertNull(sessionConverter.toSession(null)); + } + + @Test(expected = SessionConversionException.class) + public void toSessionItem_NullManager_ThrowsSessionConversionException() { + session.setManager(null); + sessionConverter.toSessionItem(session); + } + +} diff --git a/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/LegacySessionConverterTest.java b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/LegacySessionConverterTest.java new file mode 100644 index 0000000..6146f55 --- /dev/null +++ b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/LegacySessionConverterTest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import static com.amazonaws.services.dynamodb.sessionmanager.CustomAsserts.assertSessionEquals; +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.apache.catalina.Session; +import org.apache.catalina.session.StandardSession; +import org.junit.Before; +import org.junit.Test; + +import com.amazonaws.services.dynamodb.sessionmanager.CustomSessionClass; +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; + +/** + * Tests a SessionConverter with Legacy implementations of TomcatSessionConverter and + * DynamoSessionConverter + */ +public class LegacySessionConverterTest { + + private static final int MAX_INACTIVE_INTERVAL = 60; + private static final TestSessionFactory SESSION_TEMPLATE = new TestSessionFactory(); + + private SessionConverter sessionConverter; + + @Before + public void setup() { + sessionConverter = SessionConverter.createLegacySessionConverter(SESSION_TEMPLATE.getManager(), getClass() + .getClassLoader(), MAX_INACTIVE_INTERVAL); + } + + /** + * Creates a StandardSession, converts it to a SessionItem and then back again to a + * StandardSession and assert we have the same thing + */ + @Test + public void roundTrip_ReturnsSameSession() throws Exception { + StandardSession session = SESSION_TEMPLATE.createStandardSession(); + Session roundTripSession = sessionConverter.toSession(sessionConverter.toSessionItem(session)); + assertSessionEquals(session, roundTripSession); + } + + @Test + public void roundTrip_NoSessionData_ReturnsSameSession() throws Exception { + StandardSession session = new TestSessionFactory().withSessionAttributes(null).createStandardSession(); + Session roundTripSession = sessionConverter.toSession(sessionConverter.toSessionItem(session)); + assertSessionEquals(session, roundTripSession); + } + + @Test(expected = SessionConversionException.class) + public void toSessionItem_StandardSessionNull_ThrowsSessionConversionException() { + assertNull(sessionConverter.toSessionItem(null)); + } + + @Test(expected = SessionConversionException.class) + public void toSession_SessionItemNull_ThrowsSessionConversionException() { + assertNull(sessionConverter.toSession(null)); + } + + @Test(expected = SessionConversionException.class) + public void toHttpSession_JunkSessionData_ThrowsSessionConversionException() throws IOException { + DynamoSessionItem sessionItem = SESSION_TEMPLATE.createLegacySessionItem(); + sessionItem.setSessionData(ByteBuffer.wrap(new byte[] { 1, 2, 3, 4 })); + sessionConverter.toSession(sessionItem); + } + + /** + * Session Data is expected to be a Map. This tests a session item whose session + * data is just a pojo throws an exception + */ + @Test(expected = SessionConversionException.class) + public void toHttpSession_SessionDataInvalidClass_ThrowsSessionConversionException() throws Exception { + DynamoSessionItem sessionItem = SESSION_TEMPLATE.createLegacySessionItem(); + sessionItem.setSessionData(LegacyDynamoDBSessionItemConverter + .objectToByteBuffer(new CustomSessionClass("data"))); + sessionConverter.toSession(sessionItem); + } + +} diff --git a/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TestSessionFactory.java b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TestSessionFactory.java new file mode 100644 index 0000000..18195ef --- /dev/null +++ b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TestSessionFactory.java @@ -0,0 +1,144 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.catalina.Manager; +import org.apache.catalina.Session; +import org.apache.catalina.session.StandardSession; + +import com.amazonaws.services.dynamodb.sessionmanager.CustomSessionClass; +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; +import com.amazonaws.services.dynamodb.sessionmanager.converters.LegacyTomcatSessionConverter.LegacySession; + +/** + * Utility class to create new {@link Session} and {@link DynamoSessionItem} object for tests. Has + * suitable defaults for fields that can be changed for individual test classes or cases + */ +public class TestSessionFactory { + + // Initialize fields with some defaults + private String sessionId = "1234"; + private int creationTime = 1234; + private long lastAccessedTime = creationTime; + private int maxInactiveInterval = 30; + private Manager manager = getDefaultManager(); + private Map sessionAttributes = getDefaultSessionAttributes(); + + public String getSessionId() { + return sessionId; + } + + public int getCreationTime() { + return creationTime; + } + + public long getLastAccessedTime() { + return lastAccessedTime; + } + + public int getMaxInactiveInterval() { + return maxInactiveInterval; + } + + public Map getSessionAttributes() { + return sessionAttributes; + } + + public Manager getManager() { + return manager; + } + + public TestSessionFactory withSessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + public TestSessionFactory withCreationTime(int creationTime) { + this.creationTime = creationTime; + return this; + } + + public TestSessionFactory withLastAccessedTime(long lastAccessedTime) { + this.lastAccessedTime = lastAccessedTime; + return this; + } + + public TestSessionFactory withMaxInactiveInterval(int maxInactiveInterval) { + this.maxInactiveInterval = maxInactiveInterval; + return this; + } + + public TestSessionFactory withSessionAttributes(Map sessionAttributes) { + this.sessionAttributes = sessionAttributes; + return this; + } + + public TestSessionFactory withManager(Manager manager) { + this.manager = manager; + return this; + } + + public final StandardSession createStandardSession() { + LegacySession session = new LegacySession(null); + session.setValid(true); + session.setId(getSessionId(), false); + session.setCreationTime(getCreationTime()); + session.setMaxInactiveInterval(maxInactiveInterval); + session.setLastAccessedTime(lastAccessedTime); + + Map sessionData = getSessionAttributes(); + if (sessionData != null) { + for (Entry attr : sessionData.entrySet()) { + session.setAttribute(attr.getKey(), attr.getValue(), false); + } + } + session.setManager(getManager()); + return session; + } + + public final LegacySession createLegacySession() { + return (LegacySession) createStandardSession(); + } + + public final DynamoSessionItem createLegacySessionItem() throws IOException { + DynamoSessionItem sessionItem = new DynamoSessionItem(getSessionId()); + sessionItem.setCreatedTime(getCreationTime()); + sessionItem.setLastUpdatedTime(getCreationTime()); + sessionItem.setSessionData(LegacyDynamoDBSessionItemConverter.objectToByteBuffer(getSessionAttributes())); + return sessionItem; + } + + private static Map getDefaultSessionAttributes() { + Map sessionData = new HashMap(); + sessionData.put("someAttribute", new CustomSessionClass("customData")); + return sessionData; + } + + private static Manager getDefaultManager() { + Manager mockManager = mock(Manager.class, RETURNS_DEEP_STUBS); + when(mockManager.getContainer().getLogger().isDebugEnabled()).thenReturn(false); + return mockManager; + } + +} \ No newline at end of file diff --git a/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TomcatSessionConverterChainTest.java b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TomcatSessionConverterChainTest.java new file mode 100644 index 0000000..b16fe03 --- /dev/null +++ b/src/test/java/com/amazonaws/services/dynamodb/sessionmanager/converters/TomcatSessionConverterChainTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.services.dynamodb.sessionmanager.converters; + +import static org.junit.Assert.assertEquals; + +import org.apache.catalina.Session; +import org.junit.Test; + +import com.amazonaws.services.dynamodb.sessionmanager.DynamoSessionItem; + +public class TomcatSessionConverterChainTest { + + private static final Session SESSION = new TestSessionFactory().createStandardSession(); + + private class FaultyTomcatSessionConverter implements TomcatSessionConverter { + @Override + public Session toSession(DynamoSessionItem sessionItem) { + throw new SessionConversionException("Unable to convert"); + } + } + + private class WorkingTomcatSessionConverter implements TomcatSessionConverter { + @Override + public Session toSession(DynamoSessionItem sessionItem) { + return SESSION; + } + } + + @Test + public void toSession_FaultyConverterFirst_FallsBackToWorkingConverter() throws Exception { + TomcatSessionConverter converterChain = TomcatSessionConverterChain.wrap(new FaultyTomcatSessionConverter(), + new WorkingTomcatSessionConverter()); + assertEquals(SESSION, converterChain.toSession(null)); + } + + @Test + public void toSession_WorkingConverterFirst_UsesWorkingConverter() throws Exception { + TomcatSessionConverter converterChain = TomcatSessionConverterChain.wrap(new WorkingTomcatSessionConverter(), + new FaultyTomcatSessionConverter()); + assertEquals(SESSION, converterChain.toSession(null)); + } + + @Test(expected = SessionConversionException.class) + public void toSession_AllConvertersFail_ThrowsDynamoSessionConversionException() { + TomcatSessionConverter converterChain = TomcatSessionConverterChain.wrap(new FaultyTomcatSessionConverter(), + new FaultyTomcatSessionConverter()); + converterChain.toSession(null); + } + + @Test(expected = SessionConversionException.class) + public void toSession_NoConverters_ThrowsDynamoSessionConversionException() { + TomcatSessionConverter converterChain = TomcatSessionConverterChain.wrap(); + converterChain.toSession(null); + } + +}