com.runwaysdk.session.FileSessionCache.java Source code

Java tutorial

Introduction

Here is the source code for com.runwaysdk.session.FileSessionCache.java

Source

/**
 * Copyright (c) 2015 TerraFrame, Inc. All rights reserved.
 *
 * This file is part of Runway SDK(tm).
 *
 * Runway SDK(tm) is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * Runway SDK(tm) is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with Runway SDK(tm).  If not, see <http://www.gnu.org/licenses/>.
 */
package com.runwaysdk.session;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import org.apache.commons.io.filefilter.DirectoryFileFilter;

import com.google.inject.Inject;
import com.google.inject.name.Named;
import com.runwaysdk.business.Mutable;
import com.runwaysdk.business.rbac.UserDAO;
import com.runwaysdk.constants.CommonProperties;
import com.runwaysdk.dataaccess.ProgrammingErrorException;
import com.runwaysdk.dataaccess.io.FileReadException;
import com.runwaysdk.dataaccess.io.FileWriteException;
import com.runwaysdk.generation.LoaderDecoratorExceptionIF;
import com.runwaysdk.generation.loader.LoaderDecorator;
import com.runwaysdk.system.metadata.MdDimension;
import com.runwaysdk.util.FileIO;

/**
 * Concrete implementation of {@link FileSessionCache}. The {@link Session}s in this {@link SessionCache} are stored on the file system. In addition, this cache spawns a thread which automatically
 * checks and removes expired {@link Session}s from the cache.
 *
 * @author Justin Smethie
 */
public class FileSessionCache extends ManagedUserSessionCache {
    private static final int MAX_DPETH = 6;

    /**
     * The fully qualified path of the root directory for this {@link SessionCache}.
     */
    private String rootDirectory;

    /**
     * The session id of the public session
     */
    private String publicSessionId;

    /**
     * A cache of sessions, the mapping between the session id and the Session
     */
    private Map<String, Session> sessions;

    /**
     * The number of request open for an individual session
     */
    private Map<String, Integer> requestCount;

    /**
     * Creates a new {@link FileSessionCache}. This cache assumes that any file in the root directory or it's sub-directories is a serialized {@link Session} which belongs to the cache. Thus it is
     * important that the root directory does not overlap another {@link FileSessionCache} or have exteranious files in it.
     *
     * @param rootDirectory
     *          Fully qualified path of the root directory
     */
    @Inject
    public FileSessionCache(@Named("rootDirectory") String rootDirectory) {
        super();

        this.rootDirectory = rootDirectory;
        this.sessions = new HashMap<String, Session>();
        this.requestCount = new HashMap<String, Integer>();

        // Make the root directory
        File rootDirectoryFile = new File(rootDirectory);

        boolean directoryCreated = rootDirectoryFile.mkdirs();

        if (!directoryCreated) {
            String errMsg = "Error creating the directory for storing serialized sessions";
            new FileWriteException(errMsg, rootDirectoryFile);
        }

        // Make the public session
        Session publicSession = new Session(CommonProperties.getDefaultLocale());
        publicSession.setExpirationTime(-1);

        this.publicSessionId = publicSession.getId();
        this.addSession(publicSession);
    }

    @Override
    protected void addSession(Session session) {
        if (!sessions.containsKey(session.getId())) {
            putSessionOnFileSystem(session, true);
        }
    }

    /**
     * Serializes a {@link Session} to the file system. If the {@link Session} already exists on the file system then the content of the file is overwritten with the given {@link Session} object.
     * Optionally, updates the session count of the {@link UserDAO} of the {@link Session} if the {@link Session} does not already exist on the file system.
     *
     * @param session
     *          The {@link Session} to serialize to the file system
     * @param updateUserCount
     *          Flag indicating if the {@link UserDAO}'s session count should be checked and updated.
     */
    private void putSessionOnFileSystem(Session session, boolean updateUserCount) {
        sessionCacheLock.lock();

        try {
            String sessionId = session.getId();
            String directory = this.getDirectory(sessionId);

            // Create the directory structure and get the session file
            new File(directory).mkdirs();
            File file = new File(directory + sessionId);
            boolean exists = file.exists();

            try {
                // Serialize the session to it's file
                ObjectOutputStream stream = new ObjectOutputStream(new FileOutputStream(file));
                stream.writeObject(session);
                stream.close();

                // Do not update the session count until the Session has been
                // serialized in case of errors writing to the file system.
                if (updateUserCount && !exists) {
                    super.addSession(session);
                }
            } catch (FileNotFoundException e) {
                String error = "Session [" + sessionId + "] does not exist or has expired.";
                throw new InvalidSessionException(error, e);
            } catch (IOException e) {
                throw new FileWriteException(file, e);
            }
        } finally {
            sessionCacheLock.unlock();
        }
    }

    @Override
    protected void cleanUp() {
        sessionCacheLock.lock();
        try {
            cleanUpDirectory(new File(rootDirectory), System.currentTimeMillis());
        } finally {
            sessionCacheLock.unlock();
        }
    }

    @Override
    protected void closeSession(String sessionId) {
        sessionCacheLock.lock();

        try {
            // Do nothing if the sessionId is equal to the public session id
            if (!sessionId.equals(publicSessionId)) {
                Session session = this.getSession(sessionId);
                super.decrementUserLoginCount(session);

                // If session is in memory remove from memory
                if (sessions.containsKey(sessionId)) {
                    sessions.remove(sessionId);
                    requestCount.remove(sessionId);
                }

                // Delete the file from the cache
                String dir = this.getDirectory(sessionId);
                File file = new File(dir + sessionId);

                try {
                    FileIO.deleteFile(file);
                } catch (IOException e) {
                    throw new FileReadException(file, e);
                }
            }
        } finally {
            sessionCacheLock.unlock();
        }
    }

    @Override
    protected boolean containsSession(String sessionId) {
        String dir = this.getDirectory(sessionId);
        File file = new File(dir + sessionId);

        return file.exists();
    }

    @Override
    protected boolean full() {
        return false;
    }

    @Override
    protected Session makeRoom() {
        return null;
    }

    @Override
    protected Session getSession(String sessionId) {
        sessionCacheLock.lock();

        try {
            if (sessions.containsKey(sessionId)) {
                return sessions.get(sessionId);
            }

            String dir = this.getDirectory(sessionId);
            File file = new File(dir + sessionId);

            try {
                ObjectInputStream stream = new ResolvedObjectInputStream(new FileInputStream(file));
                Session session = (Session) stream.readObject();
                stream.close();

                // Check that the session has not expired
                long currentTime = System.currentTimeMillis();

                if (session.isExpired(currentTime)) {
                    // Session has expired: delete it from the file
                    // system and throw an exception
                    FileIO.deleteFile(file);

                    String error = "Session [" + sessionId + "] does not exist or has expired.";
                    throw new InvalidSessionException(error);
                }

                return session;
            } catch (FileNotFoundException e) {
                String error = "Session [" + sessionId + "] does not exist or has expired.";
                throw new InvalidSessionException(error);
            } catch (IOException e) {
                throw new FileReadException(file, e);
            } catch (ClassNotFoundException e) {
                throw new ProgrammingErrorException(e);
            }
        } finally {
            sessionCacheLock.unlock();
        }
    }

    @Override
    protected String logIn(String username, String password, Locale[] locales) {
        Session session = new Session(locales);
        // Heads up: Smethie: why is this supering up to a different method that does not add
        // the session to the session cache. Also, why are you adding it anyway below?
        // See if you can remove this method entirely and just have polymorphism call the super method.
        super.changeLogIn(username, password, session);

        // Update the session on the cache with the user logged in
        if (!sessions.containsKey(session.getId())) {
            this.putSessionOnFileSystem(session, false);
        }

        return session.getId();
    }

    @Override
    protected void changeLogin(String username, String password, String sessionId) {
        sessionCacheLock.lock();

        try {
            if (sessionId.equals(publicSessionId)) {
                String msg = "Users are not permitted to log into the public session [" + sessionId + "]";
                throw new InvalidLoginException(msg);
            }

            Session session = this.getSession(sessionId);

            super.changeLogIn(username, password, session);

            if (!sessions.containsKey(sessionId)) {
                this.putSessionOnFileSystem(session, false);
            }
        } finally {
            sessionCacheLock.unlock();
        }
    }

    /**
     * Sets the dimension of an existing {@link Session}.
     * 
     * @param sessionId
     *          The id of the {@link Session}.
     * @param dimensionKey
     *          key of a {@link MdDimension}.
     */
    @Override
    protected void setDimension(String sessionId, String dimensionKey) {
        sessionCacheLock.lock();

        try {
            if (sessionId.equals(publicSessionId)) {
                String msg = "Users are not permitted to change the dimension of the public session [" + sessionId
                        + "]";
                throw new InvalidLoginException(msg);
            }

            Session session = this.getSession(sessionId);
            session.setDimension(dimensionKey);

            if (!sessions.containsKey(sessionId)) {
                this.putSessionOnFileSystem(session, false);
            }
        } finally {
            sessionCacheLock.unlock();
        }
    }

    @Override
    protected void renewSession(String sessionId) {
        sessionCacheLock.lock();

        try {
            Session session = this.getSession(sessionId);
            session.renew();

            if (!sessions.containsKey(sessionId)) {
                this.putSessionOnFileSystem(session, false);
            }
        } finally {
            sessionCacheLock.unlock();
        }
    }

    @Override
    protected void clearSessions() {
        sessionCacheLock.lock();

        try {
            super.clearSessions();

            // Get the public session
            Session publicSession = this.getPublicSession();

            // Delete all of the files in the file cache
            File directory = new File(rootDirectory);
            FileIO.deleteDirectory(directory);

            // Recreate the root directory
            directory.mkdir();

            // Add the public session back into the cache
            this.addSession(publicSession);
        } catch (IOException e) {
            throw new FileReadException(new File(rootDirectory), e);
        } finally {
            sessionCacheLock.unlock();
        }
    }

    /**
     * @param sessionId
     *          The session id
     * @return Fully qualified directory location of a Session with the given session id.
     */
    private String getDirectory(String sessionId) {

        StringBuilder path = new StringBuilder();
        path.append(rootDirectory);

        for (int i = 0; i < MAX_DPETH; i++) {
            path.append(sessionId.charAt(i) + "/");
        }

        return path.toString();
    }

    /**
     * Crawls through a directory and all sub directories removing any serialized {@link Session} which has expired. This method assumes that any file it finds on the file system is either a directory
     * or a serialized {@link Session}. This method performs it crawl through the use of recursion. Do not use this method for a deep directory structure.
     *
     * @param directory
     *          The root directory.
     * @param time
     *          The time to use in expiration checks.
     */
    private void cleanUpDirectory(File directory, long time) {
        sessionCacheLock.lock();

        try {
            if (directory.isDirectory()) {
                File[] files = directory.listFiles();

                for (File file : files) {
                    cleanUpDirectory(file, time);
                }
            } else {
                // File is not directory: Get the session from the file and check if it
                // has expired. If it has expired removed it from the
                // file system.
                String sessionId = directory.getName();

                try {
                    this.getSession(sessionId);
                } catch (InvalidSessionException e) {
                    // If an expection is thrown then it means
                    // the session file has expired and has been removed.
                }
            }
        } finally {
            sessionCacheLock.unlock();
        }
    }

    @Override
    protected Session getPublicSession() {
        return this.getSession(publicSessionId);
    }

    @Override
    protected Session getSessionForRequest(String sessionId) {
        sessionCacheLock.lock();

        try {
            if (sessions.containsKey(sessionId)) {
                // Increment the request count by one
                int count = requestCount.get(sessionId) + 1;
                requestCount.put(sessionId, count);

                // return the session in memory
                return sessions.get(sessionId);
            }

            // Unserialize session from file system
            Session session = this.getSession(sessionId);

            // Put the session into memory
            sessions.put(sessionId, session);

            // Intialize the request count to 1
            requestCount.put(sessionId, new Integer(1));

            return session;
        } finally {
            sessionCacheLock.unlock();
        }
    }

    @Override
    protected void endOfRequest(String sessionId) {
        this.sessionCacheLock.lock();

        try {
            Session session = this.getSession(sessionId);

            if (session.closeOnEndOfRequest()) {
                this.closeSession(sessionId);
            } else if (sessions.containsKey(sessionId)) {
                // Decrement the request count
                int count = requestCount.get(sessionId) - 1;

                if (count < 1) {
                    requestCount.remove(sessionId);
                    sessions.remove(sessionId);

                    // Serialize the session back onto the file system
                    this.putSessionOnFileSystem(session, false);
                } else {
                    requestCount.put(sessionId, count);
                }
            }
        } finally {
            this.sessionCacheLock.unlock();
        }
    }

    @Override
    protected void put(String sessionId, Mutable mutable) {
        this.sessionCacheLock.lock();

        try {
            Session session = this.getSession(sessionId);
            session.put(mutable);

            if (!sessions.containsKey(sessionId)) {
                this.putSessionOnFileSystem(session, false);
            }
        } finally {

        }
    }

    /**
     * ObjectInputStream which delegates to LoaderDecorator when loading classes from the JVM.
     *
     * @author Justin Smethie
     */
    class ResolvedObjectInputStream extends ObjectInputStream {
        public ResolvedObjectInputStream(InputStream in) throws IOException {
            super(in);
        }

        /*
         * (non-Javadoc)
         * 
         * @see java.io.ObjectInputStream#resolveClass(java.io.ObjectStreamClass)
         */
        @Override
        protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
            String name = desc.getName();
            try {
                return LoaderDecorator.load(name);
            } catch (RuntimeException ex) {
                if (ex instanceof LoaderDecoratorExceptionIF) {
                    return super.resolveClass(desc);
                } else {
                    throw ex;
                }
            }
        }
    }

    /**
     * @return The number of sessions in memory
     */
    int getMemoryCount() {
        return sessions.size();
    }

    /**
     * @see com.runwaysdk.session.SessionCache#getIterator()
     */
    public SessionIterator getIterator() {
        return new FileSessionIterator();
    }

    private class FileSessionIterator implements SessionIterator {
        private Iterator<String> iterator;

        private boolean initialized;

        private String sessionId;

        private FileSessionIterator() {
            sessionCacheLock.lock();

            this.initialized = false;
        }

        /**
         * Loads into memmory a list of all the possible session ids
         */
        private synchronized void initialize() {
            if (!this.initialized) {
                Set<String> sessionIds = new TreeSet<String>();
                sessionIds.addAll(sessions.keySet());

                File root = new File(rootDirectory);

                this.loadSetFromDirectory(root, sessionIds, 0);

                // Remove the public user from the list of sessionIds
                sessionIds.remove(FileSessionCache.this.publicSessionId);

                this.iterator = sessionIds.iterator();

                this.initialized = true;
            }
        }

        private void loadSetFromDirectory(File directory, Set<String> sessionIds, int depth) {
            if (depth < MAX_DPETH) {
                FileFilter filter = (FileFilter) DirectoryFileFilter.DIRECTORY;

                File[] subdirectories = directory.listFiles(filter);

                for (File subdirectory : subdirectories) {
                    this.loadSetFromDirectory(subdirectory, sessionIds, (depth + 1));
                }
            } else {
                File[] sessions = directory.listFiles();

                for (File session : sessions) {
                    if (session.isFile()) {
                        String sessionId = session.getName();

                        sessionIds.add(sessionId);
                    } else {
                        throw new ProgrammingErrorException("Expecting only session");
                    }
                }
            }
        }

        /**
         * @see com.runwaysdk.session.SessionIterator#next()
         */
        @Override
        public SessionIF next() {
            this.initialize();

            this.sessionId = this.iterator.next();

            return FileSessionCache.this.getSession(sessionId);
        }

        /**
         * @see com.runwaysdk.session.SessionIterator#remove()
         */
        @Override
        public void remove() {
            this.initialize();

            this.iterator.remove();

            FileSessionCache.this.closeSession(this.sessionId);
        }

        /**
         * @see com.runwaysdk.session.SessionIterator#hasNext()
         */
        @Override
        public boolean hasNext() {
            this.initialize();

            return this.iterator.hasNext();
        }

        /**
         * @see com.runwaysdk.session.SessionIterator#close()
         */
        @Override
        public synchronized void close() {
            this.iterator = null;
            this.initialized = false;

            sessionCacheLock.unlock();
        }

        /**
         * @see com.runwaysdk.session.SessionIterator#getAll()
         */
        @Override
        public Collection<SessionIF> getAll() {
            Collection<SessionIF> sesses = new LinkedList<SessionIF>();

            while (this.hasNext()) {
                SessionIF session = this.next();

                sesses.add(session);
            }

            return sesses;
        }
    }
}