com.activecq.experiments.redis.impl.RedisSessionUtilImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.activecq.experiments.redis.impl.RedisSessionUtilImpl.java

Source

/*
 * Copyright 2012 david gonzalez.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package com.activecq.experiments.redis.impl;

import com.activecq.experiments.datastores.SessionRequest;
import com.activecq.experiments.datastores.SessionUtil;
import com.activecq.experiments.redis.RedisConnectionPool;
import com.activecq.experiments.redis.RedisSession;
import org.apache.commons.lang.StringUtils;
import org.apache.felix.scr.annotations.*;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.adapter.AdapterFactory;
import org.apache.sling.commons.osgi.PropertiesUtil;
import org.osgi.framework.Constants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;

import javax.servlet.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;
import java.io.*;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * User: david
 */
@Component(label = "Experiments - Redis Session Util", description = "Redis session manager.")
@Properties({
        @Property(label = "Vendor", name = Constants.SERVICE_VENDOR, value = "ActiveCQ", propertyPrivate = true),
        @Property(label = "Adaptables", description = "Adaptables list the 'From' objects the adapter supports", name = AdapterFactory.ADAPTABLE_CLASSES, value = {
                "org.apache.sling.api.SlingHttpServletRequest" }, propertyPrivate = true),
        @Property(label = "Adapters", description = "Adapters list the 'To' objects the adapter supports", name = AdapterFactory.ADAPTER_CLASSES, value = {
                "com.activecq.experiments.redis.RedisSession" }, propertyPrivate = true),
        @Property(label = "Sling Filter Order", description = "Order in which this Sling Filter is invoked", name = "sling.filter.order", intValue = 0, propertyPrivate = true),
        @Property(label = "Sling Filter Scope", description = "Only invoke filter on requests entering CQ", name = "sling.filter.scope", value = "REQUEST", propertyPrivate = true) })
@Service(value = { SessionUtil.class, AdapterFactory.class, Filter.class })
public class RedisSessionUtilImpl implements SessionUtil, AdapterFactory, Filter {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    /* Session Cookie Name */
    private static final String DEFAULT_SESSION_COOKIE_NAME = "CQSESSION";
    private String sessionCookieName = DEFAULT_SESSION_COOKIE_NAME;
    @Property(label = "Session Cookie Name", description = "Name of HTTP Cookie that stores the Redis DB Session UID for the user. Defaults to: CQSESSION", value = DEFAULT_SESSION_COOKIE_NAME)
    private static final String PROP_SESSION_COOKIE_NAME = "prop.session.cookie.name";

    /* Session Expiry */
    private static final int DEFAULT_SESSION_EXPIRY = 60;
    private int sessionExpiry = DEFAULT_SESSION_EXPIRY;
    @Property(label = "Session Expiry", description = "In mins. Length of time a Redis-based session can persist after its last access.", intValue = DEFAULT_SESSION_EXPIRY)
    private static final String PROP_SESSION_EXPIRY = "prop.session.expiry";

    /* Session Cookie - Secure */
    private static final boolean DEFAULT_SECURE_COOKIE = false;
    private boolean secureCookie = DEFAULT_SECURE_COOKIE;
    @Property(label = "Secure Session Cookie", description = "Set session cookie to only be sent over Secure (HTTPS) connections.", boolValue = DEFAULT_SECURE_COOKIE)
    private static final String PROP_SECURE_COOKIE = "prop.session.cookie.secure";

    public static final String REDIS_KEY_PREFIX_SESSION = "cq:session:attr:";
    public static final String REDIS_KEY_PREFIX_INDEX = "cq:session:index:";
    public static final String REDIS_KEY_PREFIX_CREATED = "cq:session:created:";
    public static final String REDIS_KEY_PREFIX_LAST_ACCESSED = "cq:session:lastaccessed:";

    public static final String REDIS_KEY_PREFIX_DELIMITER = ":";

    @Reference(target = "(service.uid=session)")
    private RedisConnectionPool pool;

    @Override
    public boolean hasSessionCookie(final SlingHttpServletRequest request) {
        final Cookie cookie = request.getCookie(this.getSessionCookieName());

        final boolean hasCookie = cookie != null;

        if (!hasCookie) {
            log.debug("Redis Session Cookie cannot be found. Use of Redis-based sessions will silently fail.");
        }

        return hasCookie;
    }

    public String getSessionCookieName() {
        return sessionCookieName;
    }

    @Override
    public String getId(final SlingHttpServletRequest request) {
        final Cookie cookie = request.getCookie(this.getSessionCookieName());
        if (cookie != null) {
            return StringUtils.stripToNull(cookie.getValue());
        }
        return null;
    }

    @Override
    public Object getAttribute(final SlingHttpServletRequest request, final String name)
            throws IOException, ClassNotFoundException {
        if (!this.hasSessionCookie(request)) {
            return null;
        }

        final String sessionId = this.getId(request);
        return this.getAttribute(sessionId, name);
    }

    protected Object getAttribute(final String sessionId, String name) throws IOException, ClassNotFoundException {
        if (StringUtils.isBlank(sessionId) || StringUtils.isBlank(name)) {
            return null;
        }

        final String attrKey = this.getAttrKey(sessionId, name);
        final String lastAccessedAtKey = this.getLastAccessedAtKey(sessionId);

        final Jedis jedis = this.getJedis();

        try {
            if (!jedis.exists(attrKey)) {
                return null;
            }

            final String timestamp = String.valueOf(System.currentTimeMillis());

            final Object val = this.deserialize(jedis.get(attrKey.getBytes()));
            jedis.set(lastAccessedAtKey, timestamp);

            this.updateExpiration(sessionId);

            return val;
        } finally {
            this.returnJedis(jedis);
        }
    }

    @Override
    public void setAttribute(final SlingHttpServletRequest request, final String name, final Object value)
            throws IOException {
        if (!this.hasSessionCookie(request)) {
            return;
        }

        final String sessionId = this.getId(request);
        this.setAttribute(sessionId, name, value);
    }

    protected void setAttribute(final String sessionId, String name, final Object value) throws IOException {
        final String attrKey = this.getAttrKey(sessionId, name);
        final String createdAtKey = this.getCreatedAtKey(sessionId);
        final String lastAccessedAtKey = this.getLastAccessedAtKey(sessionId);
        final String indexKey = this.getIndexKey(sessionId);

        final Jedis jedis = this.getJedis();

        try {
            final Pipeline pipeline = jedis.pipelined();

            final String timestamp = String.valueOf(System.currentTimeMillis());

            pipeline.set(attrKey.getBytes(), this.serialize(value));
            pipeline.sadd(indexKey, attrKey);

            pipeline.setnx(createdAtKey, timestamp);
            pipeline.set(lastAccessedAtKey, timestamp);

            pipeline.sync();

            this.updateExpiration(sessionId);
        } finally {
            this.returnJedis(jedis);
        }
    }

    @Override
    public void removeAttribute(final SlingHttpServletRequest request, final String name) {
        if (!this.hasSessionCookie(request)) {
            return;
        }

        final String sessionId = this.getId(request);
        this.removeAttribute(sessionId, name);
    }

    protected void removeAttribute(final String sessionId, final String name) {
        final String attrKey = this.getAttrKey(sessionId, name);
        final String lastAccessedAtKey = this.getLastAccessedAtKey(sessionId);
        final String indexKey = this.getIndexKey(sessionId);

        final Jedis jedis = this.getJedis();

        try {
            final Pipeline pipeline = jedis.pipelined();

            final String timestamp = String.valueOf(System.currentTimeMillis());

            pipeline.del(attrKey.getBytes());
            pipeline.srem(indexKey, attrKey);
            pipeline.set(lastAccessedAtKey, timestamp);

            pipeline.sync();

            this.updateExpiration(sessionId);
        } finally {
            this.returnJedis(jedis);
        }
    }

    @Override
    public long getLastAccessTime(final SlingHttpServletRequest request) {
        if (!this.hasSessionCookie(request)) {
            return 0;
        }

        final String sessionId = this.getId(request);
        return this.getLastAccessTime(sessionId);
    }

    protected long getLastAccessTime(final String sessionId) {
        final String lastAccessedAtKey = this.getLastAccessedAtKey(sessionId);

        final Jedis jedis = this.getJedis();

        try {
            final String lastAccessedAt = jedis.get(lastAccessedAtKey);
            if (lastAccessedAt == null) {
                return 0;
            }

            return Long.parseLong(lastAccessedAt);
        } finally {
            this.returnJedis(jedis);
        }
    }

    @Override
    public long getCreationTime(final SlingHttpServletRequest request) {
        if (!this.hasSessionCookie(request)) {
            return 0;
        }

        final String sessionId = this.getId(request);
        return this.getCreationTime(sessionId);
    }

    protected long getCreationTime(final String sessionId) {
        if (StringUtils.isBlank(sessionId)) {
            return 0;
        }

        final String createdAtKey = this.getCreatedAtKey(sessionId);

        final Jedis jedis = this.getJedis();

        try {
            final String createdAt = jedis.get(createdAtKey);
            if (createdAt == null) {
                return 0;
            }

            return Long.parseLong(createdAt);
        } finally {
            this.returnJedis(jedis);
        }
    }

    @Override
    public Set<String> getAttributeNames(final SlingHttpServletRequest request) {
        if (!this.hasSessionCookie(request)) {
            return new HashSet<String>();
        }

        final String sessionId = this.getId(request);
        return this.getAttributeNames(sessionId);
    }

    protected Set<String> getAttributeNames(final String sessionId) {
        if (StringUtils.isBlank(sessionId)) {
            return new HashSet<String>();
        }
        final String indexKey = this.getIndexKey(sessionId);

        final Jedis jedis = this.getJedis();

        try {
            final Set<String> members = jedis.smembers(indexKey);
            this.updateExpiration(sessionId);

            final Set<String> attributes = new HashSet<String>();
            final String remove = this.getAttrKeyPrefix(sessionId);

            for (final String member : members) {
                final String tmp = StringUtils.removeStart(member, remove);
                attributes.add(tmp);
            }

            return attributes;
        } finally {
            this.returnJedis(jedis);
        }
    }

    @Override
    public void invalidate(final SlingHttpServletRequest request) {
        if (!this.hasSessionCookie(request)) {
            return;
        }

        final String sessionId = this.getId(request);
        this.invalidate(sessionId);

    }

    protected void invalidate(final String sessionId) {
        if (StringUtils.isBlank(sessionId)) {
            return;
        }

        final String indexKey = this.getIndexKey(sessionId);
        final String createdAtKey = this.getCreatedAtKey(sessionId);
        final String lastAccessedAtKey = this.getLastAccessedAtKey(sessionId);

        final Jedis jedis = this.getJedis();

        try {
            final Set<String> keysToDelete = jedis.smembers(indexKey);
            keysToDelete.add(indexKey);
            keysToDelete.add(createdAtKey);
            keysToDelete.add(lastAccessedAtKey);

            jedis.del(keysToDelete.toArray(new String[keysToDelete.size()]));
        } finally {
            this.returnJedis(jedis);
        }
    }

    @Override
    public Cookie createSessionCookie() {
        final Cookie cookie = new Cookie(this.getSessionCookieName(), java.util.UUID.randomUUID().toString());

        cookie.setPath("/");
        cookie.setSecure(this.secureCookie);

        // Expire with browser session
        cookie.setMaxAge(-1);

        return cookie;
    }

    @Override
    public String createSession(final SlingHttpServletRequest request, final SlingHttpServletResponse response) {
        if (this.hasValidSession(request)) {
            return this.getId(request);
        }

        final Cookie cookie = this.createSessionCookie();
        final String sessionId = cookie.getValue();

        this.initSession(sessionId);
        response.addCookie(cookie);

        return sessionId;
    }

    @Override
    public HttpSession getSession(final SlingHttpServletRequest request) {
        return new RedisSession(request, this);
    }

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

    @Override
    public boolean hasValidSession(final SlingHttpServletRequest request) {
        if (!this.hasSessionCookie(request)) {
            return false;
        }
        final String sessionId = this.getId(request);

        if (sessionId == null) {
            return false;
        }
        if (this.getCreationTime(sessionId) < 1) {
            return false;
        }
        if (this.getLastAccessTime(sessionId) < 1) {
            return false;
        }

        return true;
    }

    protected Jedis getJedis() {
        return this.pool.getJedis();
    }

    protected void returnJedis(final Jedis jedis) {
        this.pool.returnJedis(jedis);
    }

    protected long initSession(final String sessionId) {
        final String createdAtKey = this.getCreatedAtKey(sessionId);
        final String lastAccessedAtKey = this.getLastAccessedAtKey(sessionId);

        final Jedis jedis = this.getJedis();

        try {
            final Pipeline pipeline = jedis.pipelined();

            final long timeInMillis = System.currentTimeMillis();
            final String timestamp = String.valueOf(timeInMillis);

            pipeline.setnx(createdAtKey, timestamp);
            pipeline.set(lastAccessedAtKey, timestamp);

            pipeline.sync();

            this.updateExpiration(sessionId);

            return timeInMillis;
        } finally {
            this.returnJedis(jedis);
        }
    }

    protected void updateExpiration(String sessionId) {
        final String indexKey = this.getIndexKey(sessionId);
        final String createdAtKey = this.getCreatedAtKey(sessionId);
        final String lastAccessedAtKey = this.getLastAccessedAtKey(sessionId);

        final Jedis jedis = this.getJedis();

        try {
            final Set<String> members = jedis.smembers(indexKey);

            final Pipeline pipeline = jedis.pipelined();

            for (final String member : members) {
                pipeline.expire(member.getBytes(), sessionExpiry);
            }

            pipeline.expire(indexKey, sessionExpiry);
            pipeline.expire(createdAtKey, sessionExpiry);
            pipeline.expire(lastAccessedAtKey, sessionExpiry);

            pipeline.sync();
        } finally {
            this.returnJedis(jedis);
        }
    }

    protected String getAttrKey(String sessionId, String name) {
        if (StringUtils.isBlank(sessionId) || StringUtils.isBlank(name)) {
            return null;
        }

        return REDIS_KEY_PREFIX_SESSION + sessionId + REDIS_KEY_PREFIX_DELIMITER + name;
    }

    protected String getAttrKeyPrefix(String sessionId) {
        if (StringUtils.isBlank(sessionId)) {
            return null;
        }

        return REDIS_KEY_PREFIX_SESSION + sessionId + REDIS_KEY_PREFIX_DELIMITER;
    }

    protected String getIndexKey(String sessionId) {
        if (sessionId == null) {
            return null;
        }

        return REDIS_KEY_PREFIX_INDEX + sessionId;
    }

    protected String getCreatedAtKey(String sessionId) {
        if (sessionId == null) {
            return null;
        }

        return REDIS_KEY_PREFIX_CREATED + sessionId;
    }

    protected String getLastAccessedAtKey(String sessionId) {
        if (sessionId == null) {
            return null;
        }

        return REDIS_KEY_PREFIX_LAST_ACCESSED + sessionId;
    }

    protected byte[] serialize(final Object obj) throws IOException {
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        final ObjectOutputStream os = new ObjectOutputStream(out);

        try {
            os.writeObject(obj);
            return out.toByteArray();
        } finally {
            out.close();
            os.close();
        }
    }

    protected Object deserialize(final byte[] bytes) throws IOException, ClassNotFoundException {
        if (bytes == null) {
            return null;
        }

        ByteArrayInputStream in = new ByteArrayInputStream(bytes);
        ObjectInputStream is = new ObjectInputStream(in);
        try {
            return is.readObject();
        } finally {
            in.close();
            is.close();
        }
    }

    /**************************************************************************
     *
     * Sling Adapter Factory
     *
     *************************************************************************/

    @Override
    public <AdapterType> AdapterType getAdapter(Object adaptable, Class<AdapterType> type) {
        // Ensure the adaptable object is of an appropriate type
        if (adaptable == null || !(adaptable instanceof SlingHttpServletRequest)) {
            log.warn("Object {} could not be adapted to {}", adaptable, type);
            return null;
        }

        final SlingHttpServletRequest request = ((SlingHttpServletRequest) adaptable);
        final RedisSession redisSession = new RedisSession(request, this);

        return (AdapterType) redisSession;
    }

    /**************************************************************************
     *
     * Sling Filter
     *
     *************************************************************************/

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        if (!(request instanceof SlingHttpServletRequest) || !(response instanceof SlingHttpServletResponse)) {
            // Not a SlingHttpServletRequest/Response, so ignore.
            chain.doFilter(request, response);
            return;
        }

        final SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;
        final SlingHttpServletResponse slingResponse = (SlingHttpServletResponse) response;

        if (this.hasValidSession(slingRequest)) {
            chain.doFilter(request, response);
        } else {
            // Add the Session Cookie to the current request so this request can access the SessionId
            // This SessionId is the same as the one added to the Response Cookie
            // This case should only happen once per session (the first time the session is instantiated)

            // Add Cookie to slingResponse so future requests can make use of it
            final String sessionId = this.createSession(slingRequest, slingResponse);

            final Cookie sessionCookie = new Cookie(this.getSessionCookieName(), sessionId);

            // Add sessionCookie to this request so it (this request) can make use of it
            final SessionRequest sessionRequest = new SessionRequest(slingRequest, sessionCookie);

            chain.doFilter(sessionRequest, slingResponse);
        }
    }

    @Override
    public void destroy() {
    }

    /**************************************************************************
     *
     * Sling Filter
     *
     *************************************************************************/

    @Activate
    protected void activate(final Map<String, Object> properties) throws Exception {
        this.sessionCookieName = PropertiesUtil.toString(properties.get(PROP_SESSION_COOKIE_NAME),
                DEFAULT_SESSION_COOKIE_NAME);
        this.secureCookie = PropertiesUtil.toBoolean(properties.get(PROP_SECURE_COOKIE), DEFAULT_SECURE_COOKIE);

        // Convert to Seconds (60 * N)
        this.sessionExpiry = 60 * PropertiesUtil.toInteger(PROP_SESSION_EXPIRY, DEFAULT_SESSION_EXPIRY);
    }

    @Deactivate
    protected void deactivate(final Map<String, Object> properties) throws Exception {
        this.sessionCookieName = "";
        this.sessionExpiry = 0;
    }
}