On Oct 16, 2012, at 4:07 PM, Juan José Gil <mat...@gmail.com> wrote:

> I would be glad to see that happen! :)


Here it goes:

1. We start modeling a session as a DB table that stores a bunch of common 
session attributes in columns, and all the custom attributes in a BLOB. E.g.:

CREATE TABLE `db_session` (
 `uuid` varchar(36) collate utf8_bin NOT NULL,
 `attributes` mediumblob,
 `created_on` datetime NOT NULL,
 `host` varchar(200) collate utf8_bin default NULL,
 `last_accessed_on` datetime NOT NULL,
 `stopped_on` datetime default NULL,
 `timeout_ms` bigint(20) NOT NULL,
 PRIMARY KEY  (`uuid`)
)

2. We map this in Cayenne as any other entity. So 'db_session' table would 
result in DbSession persistent object.

3. Customize Shiro runtime to stick 2 custom object to its SessionManager that 
I will show below: CayenneSessionDAO and CayenneSessionFactory. Both should be 
using a third custom object - CayenneSession. And there is a custom Shiro 
filter (not shown here). Shiro is a bit all over the place when you start 
overriding stuff. So from here the example becomes wordy and rather 
environment-specific. So you will have to wire that somehow in a way 
appropriate to your environment, so brace yourself for some Shiro hacking.

We are using Tapestry5 that provides an HttpServletRequest proxy that can be 
called from a singleton DAO. CayenneSessionDAO takes advantage of the request 
to store an uncommitted DbSession instance that can be updated multiple times 
during the request, and only serialized and committed once at the end of the 
request (via an explicit call from the custom ShiroFilter to 
'flushRequestChanges'). You can use some other mechanism, like ThreadLocal, 
instead of HttpServletRequest proxy. 

Another neat feature of CayenneSession is a 'touch' method that skips changing 
the session last access timestamp if the last update happened recently. This 
way we significantly reduce the DB traffic (very helpful when say images and 
CSS are served from the app, and fall under the same security domain as the 
main page that includes them).

IMO this is a bit too much code for what we are trying to achieve here. I 
considered offering some refactoring ideas to Shiro developers, but just don't 
have enough time to follow up. But one way or another Shiro does make this 
integration possible, so I am not complaining :)

Andrus

(lots of code follows…)


------------
package foo.shiro;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.cayenne.Cayenne;
import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.query.SelectQuery;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;

public class CayenneSessionDAO extends AbstractSessionDAO {


        private static final String REQUEST_CONTEXT_ATTRIBUTE = 
"CayenneSessionDAO.context";
        private static final String REQUEST_SESSIONS_ATTRIBUTE = 
"CayenneSessionDAO.sessions";

        private ICayenneService cayenneService;
        private HttpServletRequest request;

        CayenneSessionDAO(HttpServletRequest request, ICayenneService 
cayenneService) {
                this.request = request;
                this.cayenneService = cayenneService;
        }

        void flushRequestChanges() {
                Map<Serializable, CayenneSession> sessions = 
requestSessions(false);
                if (!sessions.isEmpty()) {

                        for (CayenneSession session : sessions.values()) {
                                session.save();
                        }

                        requestContext(false).commitChanges();
                }
        }

        ObjectContext requestContext(boolean create) {
                ObjectContext context = (ObjectContext) 
request.getAttribute(REQUEST_CONTEXT_ATTRIBUTE);

                if (context == null && create) {
                        context = cayenneService.newContext();
                        request.setAttribute(REQUEST_CONTEXT_ATTRIBUTE, 
context);
                }

                return context;
        }

        private Map<Serializable, CayenneSession> requestSessions(boolean 
create) {
                @SuppressWarnings("unchecked")
                Map<Serializable, CayenneSession> sessions = (Map<Serializable, 
CayenneSession>) request
                                .getAttribute(REQUEST_SESSIONS_ATTRIBUTE);

                if (sessions == null && create) {
                        sessions = new HashMap<Serializable, CayenneSession>();
                        request.setAttribute(REQUEST_SESSIONS_ATTRIBUTE, 
sessions);
                }

                if (sessions == null) {
                        return Collections.emptyMap();
                } else {
                        return sessions;
                }
        }

        private void storeSession(Serializable id, Session session) {
                if (id == null) {
                        throw new NullPointerException("id argument cannot be 
null.");
                }

                // postponing DB updates till the end of request... hoping that
                // doesn't cause in any inconsistency in Shiro...
                requestSessions(true).put(id, (CayenneSession) session);
        }

        @Override
        protected Session doReadSession(Serializable sessionId) {

                CayenneSession existing = (CayenneSession) 
requestSessions(false).get(sessionId);
                if (existing != null) {
                        return existing;
                }

                SelectQuery query = new SelectQuery(DbSession.class);
                
query.andQualifier(ExpressionFactory.matchExp(DbSession.UUID_PROPERTY, 
sessionId));

                DbSession shiroSession = (DbSession) 
Cayenne.objectForQuery(requestContext(true), query);

                if (shiroSession != null) {

                        // manually cache in request scope
                        CayenneSession session = new 
CayenneSession(shiroSession, CayenneSessionFactory.DEFAULT_TOUCH_DRIFT_MS);
                        requestSessions(true).put(sessionId, session);

                        return session;
                }

                return null;
        }

        @Override
        public void update(Session session) throws UnknownSessionException {
                storeSession(session.getId(), session);
        }

        @Override
        public void delete(Session session) {
                if (session == null) {
                        throw new NullPointerException("session argument cannot 
be null.");
                }

                Serializable id = session.getId();
                if (id != null) {

                        requestSessions(false).remove(id);

                        // unlike updates, process DB deletes immediately...
                        ObjectContext context = cayenneService.newContext();
                        ((CayenneSession) session).delete(context);
                        context.commitChanges();
                }
        }

        @Override
        public Collection<Session> getActiveSessions() {
                SelectQuery query = new SelectQuery(DbSession.class);
                
query.andQualifier(ExpressionFactory.matchExp(DbSession.STOPPED_ON_PROPERTY, 
null));
                @SuppressWarnings("unchecked")
                List<DbSession> savedSessions = 
cayenneService.sharedContext().performQuery(query);
                List<Session> sessions = new 
ArrayList<Session>(savedSessions.size());

                for (DbSession s : savedSessions) {
                        sessions.add(new CayenneSession(s, 
CayenneSessionFactory.DEFAULT_TOUCH_DRIFT_MS));
                }

                return sessions;
        }

        @Override
        protected Serializable doCreate(Session session) {
                Serializable sessionId = generateSessionId(session);
                assignSessionId(session, sessionId);
                storeSession(sessionId, session);
                return sessionId;
        }

        @Override
        protected void assignSessionId(Session session, Serializable sessionId) 
{
                ((CayenneSession) session).setId(sessionId.toString());
        }

}


--------------
package foo.shiro;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.text.DateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.apache.cayenne.ObjectContext;
import org.apache.shiro.session.ExpiredSessionException;
import org.apache.shiro.session.InvalidSessionException;
import org.apache.shiro.session.StoppedSessionException;
import org.apache.shiro.session.mgt.ValidatingSession;

public class CayenneSession implements ValidatingSession {

        private DbSession session;

        private Map<Object, Object> savedAttributes;
        private Map<Object, Object> attributes;
        private long touchDriftMs;

        public CayenneSession(DbSession session, long touchDriftMs) {
                this.session = session;
                this.touchDriftMs = touchDriftMs;

                // clone session attributes to be able to save against the 
baseline
                this.savedAttributes = toMap(session.getAttributes());
                this.attributes = new HashMap<Object, Object>(savedAttributes);
        }

        @SuppressWarnings("unchecked")
        private Map<Object, Object> toMap(byte[] bytes) {

                if (bytes == null || bytes.length == 0) {
                        return new HashMap<Object, Object>();
                }

                try {
                        return (Map<Object, Object>) new ObjectInputStream(new 
ByteArrayInputStream(bytes)).readObject();
                } catch (Exception e) {
                        throw new RuntimeException("Error deserializing 
attributes", e);
                }
        }

        void delete(ObjectContext context) {
                DbSession localSession = context.localObject(session);
                context.deleteObjects(localSession);
        }

        void save() {

                // serialize and update 'attributes' property only when there 
are
                // differences... All other properties are already 
Cayenne-managed
                if (!savedAttributes.equals(attributes)) {

                        ByteArrayOutputStream bytes = new 
ByteArrayOutputStream() {

                                // avoid unneeded array copy...
                                @Override
                                public synchronized byte[] toByteArray() {
                                        return buf;
                                }
                        };

                        try {
                                ObjectOutputStream out = new 
ObjectOutputStream(bytes);
                                out.writeObject(attributes);
                                out.close();
                        } catch (Exception e) {
                                throw new RuntimeException("Error serializing 
attributes", e);
                        }

                        session.setAttributes(bytes.toByteArray());
                }
        }

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

        void setId(String id) {
                session.setUuid(id);
        }

        @Override
        public Date getStartTimestamp() {
                return session.getCreatedOn();
        }

        @Override
        public Date getLastAccessTime() {
                return session.getLastAccessedOn();
        }

        @Override
        public long getTimeout() throws InvalidSessionException {
                return session.getTimeoutMs();
        }

        @Override
        public void setTimeout(long maxIdleTimeInMillis) throws 
InvalidSessionException {
                session.setTimeoutMs(maxIdleTimeInMillis);
        }

        @Override
        public String getHost() {
                return session.getHost();
        }

        @Override
        public void touch() throws InvalidSessionException {

                Date now = new Date();

                // do not update last access timestamp very often to prevent 
large
                // amount of updates when a page loads multiple secure resources
                if (touchDriftMs <= 0 || session.getLastAccessedOn().getTime() 
+ touchDriftMs < now.getTime()) {
                        session.setLastAccessedOn(now);
                }
        }

        @Override
        public void stop() throws InvalidSessionException {
                if (session.getStoppedOn() == null) {
                        session.setStoppedOn(new Date());
                }
        }

        @Override
        public Collection<Object> getAttributeKeys() throws 
InvalidSessionException {
                return Collections.unmodifiableCollection(attributes.keySet());
        }

        @Override
        public Object getAttribute(Object key) throws InvalidSessionException {
                return attributes.get(key);
        }

        @Override
        public void setAttribute(Object key, Object value) throws 
InvalidSessionException {
                attributes.put(key, value);
        }

        @Override
        public Object removeAttribute(Object key) throws 
InvalidSessionException {
                return attributes.remove(key);
        }

        @Override
        public boolean isValid() {
                return session.getStoppedOn() == null && !isTimedOut();
        }

        @Override
        public void validate() throws InvalidSessionException {

                if (session.getStoppedOn() != null) {
                        String msg = "Session with id [" + getId() + "] has 
been "
                                        + "explicitly stopped.  No further 
interaction under this session is " + "allowed.";
                        throw new StoppedSessionException(msg);
                }

                if (isTimedOut()) {
                        stop();

                        // throw an exception explaining details of why it 
expired:
                        Date lastAccessTime = getLastAccessTime();
                        long timeout = getTimeout();

                        Serializable sessionId = getId();

                        DateFormat df = DateFormat.getInstance();
                        String msg = "Session with id [" + sessionId + "] has 
expired. " + "Last access time: "
                                        + df.format(lastAccessTime) + ".  
Current time: " + df.format(new Date())
                                        + ".  Session timeout is set to " + 
timeout / 1000 + " seconds (" + timeout / 60000 + " minutes)";
                        throw new ExpiredSessionException(msg);
                }

        }

        private boolean isTimedOut() {

                long timeout = getTimeout();

                if (timeout >= 0l) {

                        Date lastAccessTime = getLastAccessTime();

                        if (lastAccessTime == null) {
                                String msg = "session.lastAccessTime for 
session with id [" + getId()
                                                + "] is null.  This value must 
be set at "
                                                + "least once, preferably at 
least upon instantiation.  Please check the "
                                                + getClass().getName() + " 
implementation and ensure "
                                                + "this value will be set 
(perhaps in the constructor?)";
                                throw new IllegalStateException(msg);
                        }

                        long expireTimeMillis = System.currentTimeMillis() - 
timeout;
                        Date expireTime = new Date(expireTimeMillis);
                        return lastAccessTime.before(expireTime);
                }

                return false;
        }

}

-----
package foo.shiro;

import java.util.Date;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionContext;
import org.apache.shiro.session.mgt.SessionFactory;


public class CayenneSessionFactory implements SessionFactory {

        static final long DEFAULT_TOUCH_DRIFT_MS = 20000;

        private CayenneSessionDAO sessionDAO;

        public CayenneSessionFactory(CayenneSessionDAO sessionDAO) {
                this.sessionDAO = sessionDAO;
        }

        @Override
        public Session createSession(SessionContext initData) {

                String host = null;
                if (initData != null) {
                        host = initData.getHost();
                }

                DbSession session = 
sessionDAO.requestContext(true).newObject(DbSession.class);
                
session.setTimeoutMs(DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT);
                session.setHost(host);
                session.setCreatedOn(new Date());
                session.setLastAccessedOn(session.getCreatedOn());

                return new CayenneSession(session, DEFAULT_TOUCH_DRIFT_MS);
        }
}

Reply via email to