org.debux.webmotion.server.call.ClientSession.java Source code

Java tutorial

Introduction

Here is the source code for org.debux.webmotion.server.call.ClientSession.java

Source

/*
 * #%L
 * Webmotion server
 * 
 * $Id$
 * $HeadURL$
 * %%
 * Copyright (C) 2011 - 2015 Debux
 * %%
 * This program 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.
 * 
 * This program 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 General Lesser Public License for more details.
 * 
 * You should have received a copy of the GNU General Lesser Public 
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/lgpl-3.0.html>.
 * #L%
 */
package org.debux.webmotion.server.call;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
import org.apache.commons.lang3.RandomStringUtils;
import org.debux.webmotion.server.call.CookieManager.CookieEntity;

/**
 * Manages a client session with the cookies. Thus you can easily scale your server. 
 * The class uses @see CookieManager to store secure data on client. The data is 
 * serialized to json object, to get the value use getAttribute(s) method with 
 * class as parameter. Currently the session is limited by cookie limitation, 
 * the session can't exceeded 4ko.
 * 
 * @author jruchaud
 */
public class ClientSession implements HttpSession {

    /** Default timeout for the session, you can modify with setMaxInactiveInterval method */
    public static final int DEFAULT_TIMEOUT = 2 * 60 * 60; // 2h

    /** Cookie name to store SessionContext */
    public static final String SESSION_CONTEXT_COOKIE_NAME = "wm_session_context";

    /* Cookie name to store attributes */
    public static final String ATTRIBUTES_COOKIE_NAME = "wm_attributes";

    /** Parser json */
    protected JsonParser parser = new JsonParser();

    /** Parser json */
    protected Gson gson = new Gson();

    /** Current http context */
    protected HttpContext context;

    /** Current session context */
    protected ClientSessionContext sessionContext;

    /** Current attributes in session */
    protected JsonObject attributes;

    /**
     * The class contains information on state of session.
     */
    public static class ClientSessionContext {

        /** Session id */
        protected String id;

        /** Creation date (millisecond) */
        protected long creationTime;

        /** Last access (millisecond) */
        protected long lastAccessedTime;

        /** Interval beetween access before to invalidate the session (second) */
        protected int maxInactiveInterval;

        /** Indicate if the session just created */
        protected transient boolean newly;

        /**
         * Default constructor, generate the id and indicate the session is new.
         */
        public ClientSessionContext() {
            this.id = RandomStringUtils.random(32, true, true);
            this.newly = true;
            this.creationTime = System.currentTimeMillis();
            this.lastAccessedTime = creationTime;
            this.maxInactiveInterval = DEFAULT_TIMEOUT;
        }

        public long getCreationTime() {
            return creationTime;
        }

        public void setCreationTime(long creationTime) {
            this.creationTime = creationTime;
        }

        public String getId() {
            return id;
        }

        public void setId(String id) {
            this.id = id;
        }

        public long getLastAccessedTime() {
            return lastAccessedTime;
        }

        public void setLastAccessedTime(long lastAccessedTime) {
            this.lastAccessedTime = lastAccessedTime;
        }

        public int getMaxInactiveInterval() {
            return maxInactiveInterval;
        }

        public void setMaxInactiveInterval(int maxInactiveInterval) {
            this.maxInactiveInterval = maxInactiveInterval;
        }

        public boolean isNewly() {
            return newly;
        }

        public void setNewly(boolean newly) {
            this.newly = newly;
        }
    }

    /**
     * Constructor use for the tests.
     */
    protected ClientSession() {
        sessionContext = new ClientSessionContext();
        attributes = new JsonObject();
    }

    /**
     * Read the session in cookie and validate if the session is valid.
     * 
     * @param context http context
     */
    public ClientSession(HttpContext context) {
        this.context = context;

        sessionContext = readSessionContext();

        long currentAccessedTime = System.currentTimeMillis();
        if (isExpired(currentAccessedTime)) {
            invalidate();
        } else {
            sessionContext.setLastAccessedTime(currentAccessedTime);
            attributes = readAttributes(sessionContext);
        }
    }

    /**
     * Check if the session is expired. The session is expired when the last 
     * access is too late betwwen inactive interval.
     * 
     * @param currentAccessedTime reference last access, generally equals <code>System.currentTimeMillis()</code>
     * @return true if the session is expired else false
     */
    protected boolean isExpired(long currentAccessedTime) {
        int maxInactiveInterval = sessionContext.getMaxInactiveInterval() * 1000;
        long lastAccessedTime = sessionContext.getLastAccessedTime();

        return maxInactiveInterval > 0 && maxInactiveInterval + lastAccessedTime < currentAccessedTime;
    }

    /**
     * Read the session context in cookie.
     * 
     * @return session context
     */
    protected ClientSessionContext readSessionContext() {
        CookieManager manager = context.getCookieManager();
        CookieEntity cookie = manager.get(SESSION_CONTEXT_COOKIE_NAME);

        ClientSessionContext result = null;
        if (cookie != null) {
            result = cookie.getValue(ClientSessionContext.class);
        } else {
            result = new ClientSessionContext();
        }
        return result;
    }

    /**
     * Read the attributes in cookie. Requires session id to decrypt the cookie.
     * 
     * @param sessionContext session context
     * @return attributes
     */
    protected JsonObject readAttributes(ClientSessionContext sessionContext) {
        String id = sessionContext.getId();
        CookieManager manager = context.getCookieManager(id, true, true);
        CookieEntity cookie = manager.get(ATTRIBUTES_COOKIE_NAME);

        JsonObject result = new JsonObject();
        if (cookie != null) {
            String value = cookie.getValue();
            if (value != null) {
                result = parser.parse(value).getAsJsonObject();
            }
        }
        return result;
    }

    /**
     * Write the session context and attributes in cookie.
     */
    public void write() {
        CookieManager manager = context.getCookieManager();
        CookieEntity cookie = manager.create(SESSION_CONTEXT_COOKIE_NAME, sessionContext);
        cookie.setAbsolutePath("/");
        cookie.setMaxAge(sessionContext.getMaxInactiveInterval());
        manager.add(cookie);

        String id = sessionContext.getId();
        manager = context.getCookieManager(id, true, true);

        String toJson = gson.toJson(attributes);
        cookie = manager.create(ATTRIBUTES_COOKIE_NAME, toJson);
        cookie.setAbsolutePath("/");
        cookie.setMaxAge(sessionContext.getMaxInactiveInterval());
        manager.add(cookie);
    }

    @Override
    public long getCreationTime() {
        return sessionContext.getCreationTime();
    }

    @Override
    public String getId() {
        return sessionContext.getId();
    }

    @Override
    public long getLastAccessedTime() {
        return sessionContext.getLastAccessedTime();
    }

    @Override
    public ServletContext getServletContext() {
        return context.getServletContext();
    }

    @Override
    public void setMaxInactiveInterval(int interval) {
        sessionContext.setMaxInactiveInterval(interval);
    }

    @Override
    public int getMaxInactiveInterval() {
        return sessionContext.getMaxInactiveInterval();
    }

    /**
     * Return only JsonElement use getAttribute and getAttributes with class as 
     * parameter to get a value.
     */
    @Override
    public Object getAttribute(String name) {
        JsonElement value = attributes.get(name);
        if (value == null) {
            return value;

        } else if (value.isJsonPrimitive()) {
            JsonPrimitive primitive = value.getAsJsonPrimitive();
            if (primitive.isString()) {
                return primitive.getAsString();

            } else if (primitive.isBoolean()) {
                return primitive.getAsBoolean();

            } else if (primitive.isNumber()) {
                return primitive.getAsDouble();
            }

        } else if (value.isJsonArray()) {
            return value.getAsJsonArray();

        } else if (value.isJsonObject()) {
            return value.getAsJsonObject();

        } else if (value.isJsonNull()) {
            return value.getAsJsonNull();
        }
        return value;
    }

    /**
     * Get the attribute value as object representation. It is not possible to deserialize 
     * collection with objects of arbitrary types, in this case getAttribute 
     * as JsonElement.
     * For example get int array, pass <code>int[].class</code> as class.
     * @param name attribute name
     * @param clazz class to value
     * @return object
     */
    public <T> T getAttribute(String name, Class<T> clazz) {
        JsonElement value = attributes.get(name);
        T fromJson = gson.fromJson(value, clazz);
        return fromJson;
    }

    /**
     * Get the attribute value as collection. It is not possible to deserialize 
     * collection with objects of arbitrary types, in this case getAttribute 
     * as JsonElement.
     * @param name attribute name
     * @param clazz object class in collection
     * @return collection of values
     */
    public <T> Collection<T> getAttributes(String name, Class<T> clazz) {
        JsonElement value = attributes.get(name);
        Class arrayClass = Array.newInstance(clazz, 0).getClass();
        T[] fromJson = (T[]) gson.fromJson(value, arrayClass);
        if (fromJson != null) {
            return Arrays.asList(fromJson);
        }
        return null;
    }

    @Override
    public Object getValue(String name) {
        return getAttribute(name);
    }

    @Override
    public Enumeration<String> getAttributeNames() {
        String[] names = getValueNames();
        List<String> list = Arrays.asList(names);
        return Collections.enumeration(list);
    }

    @Override
    public String[] getValueNames() {
        Set<Entry<String, JsonElement>> entries = attributes.entrySet();
        String[] result = new String[entries.size()];
        int index = 0;

        for (Entry<String, JsonElement> entry : entries) {
            String name = entry.getKey();
            result[index++] = name;
        }
        return result;
    }

    @Override
    public void setAttribute(String name, Object value) {
        JsonElement element = gson.toJsonTree(value);
        attributes.add(name, element);
    }

    @Override
    public void putValue(String name, Object value) {
        setAttribute(name, value);
    }

    @Override
    public void removeAttribute(String name) {
        attributes.remove(name);
    }

    @Override
    public void removeValue(String name) {
        removeAttribute(name);
    }

    @Override
    public void invalidate() {
        attributes = new JsonObject();
        sessionContext = new ClientSessionContext();
    }

    @Override
    public boolean isNew() {
        return sessionContext.isNewly();
    }

    @Override
    public HttpSessionContext getSessionContext() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

}