Java tutorial
// // ======================================================================== // Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.nosql.mongodb; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.UnknownHostException; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Set; import org.eclipse.jetty.nosql.NoSqlSession; import org.eclipse.jetty.nosql.NoSqlSessionManager; import org.eclipse.jetty.server.SessionIdManager; import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedObject; import org.eclipse.jetty.util.annotation.ManagedOperation; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; import com.mongodb.BasicDBObject; import com.mongodb.DBCollection; import com.mongodb.DBObject; import com.mongodb.MongoException; import com.mongodb.WriteConcern; /** * MongoSessionManager * <p> * Clustered session manager using MongoDB as the shared DB instance. * The document model is an outer object that contains the elements: * <ul> * <li>"id" : session_id </li> * <li>"created" : create_time </li> * <li>"accessed": last_access_time </li> * <li>"maxIdle" : max_idle_time setting as session was created </li> * <li>"expiry" : time at which session should expire </li> * <li>"valid" : session_valid </li> * <li>"context" : a nested object containing 1 nested object per context for which the session id is in use * </ul> * Each of the nested objects inside the "context" element contains: * <ul> * <li>unique_context_name : nested object containing name:value pairs of the session attributes for that context</li> * </ul> * <p> * One of the name:value attribute pairs will always be the special attribute "__metadata__". The value * is an object representing a version counter which is incremented every time the attributes change. * </p> * <p> * For example: * <pre> * { "_id" : ObjectId("52845534a40b66410f228f23"), * "accessed" : NumberLong("1384818548903"), * "maxIdle" : 1, * "context" : { "::/contextA" : { "A" : "A", * "__metadata__" : { "version" : NumberLong(2) } * }, * "::/contextB" : { "B" : "B", * "__metadata__" : { "version" : NumberLong(1) } * } * }, * "created" : NumberLong("1384818548903"), * "expiry" : NumberLong("1384818549903"), * "id" : "w01ijx2vnalgv1sqrpjwuirprp7", * "valid" : true * } * </pre> * <p> * In MongoDB, the nesting level is indicated by "." separators for the key name. Thus to * interact with a session attribute, the key is composed of: * <code>"context".unique_context_name.attribute_name</code> * Eg <code>"context"."::/contextA"."A"</code> */ @ManagedObject("Mongo Session Manager") public class MongoSessionManager extends NoSqlSessionManager { private static final Logger LOG = Log.getLogger(MongoSessionManager.class); private final static Logger __log = Log.getLogger("org.eclipse.jetty.server.session"); /* * strings used as keys or parts of keys in mongo */ /** * Special attribute for a session that is context-specific */ private final static String __METADATA = "__metadata__"; /** * Session id */ public final static String __ID = "id"; /** * Time of session creation */ private final static String __CREATED = "created"; /** * Whether or not session is valid */ public final static String __VALID = "valid"; /** * Time at which session was invalidated */ public final static String __INVALIDATED = "invalidated"; /** * Last access time of session */ public final static String __ACCESSED = "accessed"; /** * Time this session will expire, based on last access time and maxIdle */ public final static String __EXPIRY = "expiry"; /** * The max idle time of a session (smallest value across all contexts which has a session with the same id) */ public final static String __MAX_IDLE = "maxIdle"; /** * Name of nested document field containing 1 sub document per context for which the session id is in use */ private final static String __CONTEXT = "context"; /** * Special attribute per session per context, incremented each time attributes are modified */ public final static String __VERSION = __METADATA + ".version"; /** * the context id is only set when this class has been started */ private String _contextId = null; /** * Access to MongoDB */ private DBCollection _dbSessions; /** * Utility value of 1 for a session version for this context */ private DBObject _version_1; /* ------------------------------------------------------------ */ public MongoSessionManager() throws UnknownHostException, MongoException { } /*------------------------------------------------------------ */ @Override public void doStart() throws Exception { super.doStart(); String[] hosts = getContextHandler().getVirtualHosts(); if (hosts == null || hosts.length == 0) hosts = new String[] { "::" }; // IPv6 equiv of 0.0.0.0 String contextPath = getContext().getContextPath(); if (contextPath == null || "".equals(contextPath)) { contextPath = "*"; } _contextId = createContextId(hosts, contextPath); _version_1 = new BasicDBObject(getContextAttributeKey(__VERSION), 1); } /* ------------------------------------------------------------ */ /** * @see org.eclipse.jetty.server.session.AbstractSessionManager#setSessionIdManager(org.eclipse.jetty.server.SessionIdManager) */ @Override public void setSessionIdManager(SessionIdManager metaManager) { MongoSessionIdManager msim = (MongoSessionIdManager) metaManager; _dbSessions = msim.getSessions(); super.setSessionIdManager(metaManager); } /* ------------------------------------------------------------ */ @Override protected synchronized Object save(NoSqlSession session, Object version, boolean activateAfterSave) { try { __log.debug("MongoSessionManager:save session {}", session.getClusterId()); session.willPassivate(); // Form query for upsert BasicDBObject key = new BasicDBObject(__ID, session.getClusterId()); // Form updates BasicDBObject update = new BasicDBObject(); boolean upsert = false; BasicDBObject sets = new BasicDBObject(); BasicDBObject unsets = new BasicDBObject(); // handle valid or invalid if (session.isValid()) { long expiry = (session.getMaxInactiveInterval() > 0 ? (session.getAccessed() + (1000L * getMaxInactiveInterval())) : 0); __log.debug("MongoSessionManager: calculated expiry {} for session {}", expiry, session.getId()); // handle new or existing if (version == null) { // New session upsert = true; version = new Long(1); sets.put(__CREATED, session.getCreationTime()); sets.put(__VALID, true); sets.put(getContextAttributeKey(__VERSION), version); sets.put(__MAX_IDLE, getMaxInactiveInterval()); sets.put(__EXPIRY, expiry); } else { version = new Long(((Number) version).longValue() + 1); update.put("$inc", _version_1); //if max idle time and/or expiry is smaller for this context, then choose that for the whole session doc BasicDBObject fields = new BasicDBObject(); fields.append(__MAX_IDLE, true); fields.append(__EXPIRY, true); DBObject o = _dbSessions.findOne(new BasicDBObject("id", session.getClusterId()), fields); if (o != null) { Integer currentMaxIdle = (Integer) o.get(__MAX_IDLE); Long currentExpiry = (Long) o.get(__EXPIRY); if (currentMaxIdle != null && getMaxInactiveInterval() > 0 && getMaxInactiveInterval() < currentMaxIdle) sets.put(__MAX_IDLE, getMaxInactiveInterval()); if (currentExpiry != null && expiry > 0 && expiry != currentExpiry) sets.put(__EXPIRY, expiry); } } sets.put(__ACCESSED, session.getAccessed()); Set<String> names = session.takeDirty(); if (isSaveAllAttributes() || upsert) { names.addAll(session.getNames()); // note dirty may include removed names } for (String name : names) { Object value = session.getAttribute(name); if (value == null) unsets.put(getContextKey() + "." + encodeName(name), 1); else sets.put(getContextKey() + "." + encodeName(name), encodeName(value)); } } else { sets.put(__VALID, false); sets.put(__INVALIDATED, System.currentTimeMillis()); unsets.put(getContextKey(), 1); } // Do the upsert if (!sets.isEmpty()) update.put("$set", sets); if (!unsets.isEmpty()) update.put("$unset", unsets); _dbSessions.update(key, update, upsert, false, WriteConcern.SAFE); if (__log.isDebugEnabled()) __log.debug("MongoSessionManager:save:db.sessions.update( {}, {} )", key, update); if (activateAfterSave) session.didActivate(); return version; } catch (Exception e) { LOG.warn(e); } return null; } /*------------------------------------------------------------ */ @Override protected Object refresh(NoSqlSession session, Object version) { __log.debug("MongoSessionManager:refresh session {}", session.getId()); // check if our in memory version is the same as what is on the disk if (version != null) { DBObject o = _dbSessions.findOne(new BasicDBObject(__ID, session.getClusterId()), _version_1); if (o != null) { Object saved = getNestedValue(o, getContextAttributeKey(__VERSION)); if (saved != null && saved.equals(version)) { __log.debug("MongoSessionManager:refresh not needed session {}", session.getId()); return version; } version = saved; } } // If we are here, we have to load the object DBObject o = _dbSessions.findOne(new BasicDBObject(__ID, session.getClusterId())); // If it doesn't exist, invalidate if (o == null) { __log.debug("MongoSessionManager:refresh:marking session {} invalid, no object", session.getClusterId()); session.invalidate(); return null; } // If it has been flagged invalid, invalidate Boolean valid = (Boolean) o.get(__VALID); if (valid == null || !valid) { __log.debug("MongoSessionManager:refresh:marking session {} invalid, valid flag {}", session.getClusterId(), valid); session.invalidate(); return null; } // We need to update the attributes. We will model this as a passivate, // followed by bindings and then activation. session.willPassivate(); try { DBObject attrs = (DBObject) getNestedValue(o, getContextKey()); //if disk version now has no attributes, get rid of them if (attrs == null || attrs.keySet().size() == 0) { session.clearAttributes(); } else { //iterate over the names of the attributes on the disk version, updating the value for (String name : attrs.keySet()) { //skip special metadata field which is not one of the session attributes if (__METADATA.equals(name)) continue; String attr = decodeName(name); Object value = decodeValue(attrs.get(name)); //session does not already contain this attribute, so bind it if (session.getAttribute(attr) == null) { session.doPutOrRemove(attr, value); session.bindValue(attr, value); } else //session already contains this attribute, update its value { session.doPutOrRemove(attr, value); } } // cleanup, remove values from session, that don't exist in data anymore: for (String str : session.getNames()) { if (!attrs.keySet().contains(encodeName(str))) { session.doPutOrRemove(str, null); session.unbindValue(str, session.getAttribute(str)); } } } /* * We are refreshing so we should update the last accessed time. */ BasicDBObject key = new BasicDBObject(__ID, session.getClusterId()); BasicDBObject sets = new BasicDBObject(); // Form updates BasicDBObject update = new BasicDBObject(); sets.put(__ACCESSED, System.currentTimeMillis()); // Do the upsert if (!sets.isEmpty()) { update.put("$set", sets); } _dbSessions.update(key, update, false, false, WriteConcern.SAFE); session.didActivate(); return version; } catch (Exception e) { LOG.warn(e); } return null; } /*------------------------------------------------------------ */ @Override protected synchronized NoSqlSession loadSession(String clusterId) { DBObject o = _dbSessions.findOne(new BasicDBObject(__ID, clusterId)); __log.debug("MongoSessionManager:id={} loaded={}", clusterId, o); if (o == null) return null; Boolean valid = (Boolean) o.get(__VALID); __log.debug("MongoSessionManager:id={} valid={}", clusterId, valid); if (valid == null || !valid) return null; try { Object version = o.get(getContextAttributeKey(__VERSION)); Long created = (Long) o.get(__CREATED); Long accessed = (Long) o.get(__ACCESSED); NoSqlSession session = null; // get the session for the context DBObject attrs = (DBObject) getNestedValue(o, getContextKey()); __log.debug("MongoSessionManager:attrs {}", attrs); if (attrs != null) { __log.debug("MongoSessionManager: session {} present for context {}", clusterId, getContextKey()); //only load a session if it exists for this context session = new NoSqlSession(this, created, accessed, clusterId, version); for (String name : attrs.keySet()) { //skip special metadata attribute which is not one of the actual session attributes if (__METADATA.equals(name)) continue; String attr = decodeName(name); Object value = decodeValue(attrs.get(name)); session.doPutOrRemove(attr, value); session.bindValue(attr, value); } session.didActivate(); } else __log.debug("MongoSessionManager: session {} not present for context {}", clusterId, getContextKey()); return session; } catch (Exception e) { LOG.warn(e); } return null; } /*------------------------------------------------------------ */ /** * Remove the per-context sub document for this session id. * @see org.eclipse.jetty.nosql.NoSqlSessionManager#remove(org.eclipse.jetty.nosql.NoSqlSession) */ @Override protected boolean remove(NoSqlSession session) { __log.debug("MongoSessionManager:remove:session {} for context {}", session.getClusterId(), getContextKey()); /* * Check if the session exists and if it does remove the context * associated with this session */ BasicDBObject key = new BasicDBObject(__ID, session.getClusterId()); DBObject o = _dbSessions.findOne(key, _version_1); if (o != null) { BasicDBObject remove = new BasicDBObject(); BasicDBObject unsets = new BasicDBObject(); unsets.put(getContextKey(), 1); remove.put("$unset", unsets); _dbSessions.update(key, remove, false, false, WriteConcern.SAFE); return true; } else { return false; } } /** * @see org.eclipse.jetty.nosql.NoSqlSessionManager#expire(java.lang.String) */ @Override protected void expire(String idInCluster) { __log.debug("MongoSessionManager:expire session {} ", idInCluster); //Expire the session for this context super.expire(idInCluster); //If the outer session document has not already been marked invalid, do so. DBObject validKey = new BasicDBObject(__VALID, true); DBObject o = _dbSessions.findOne(new BasicDBObject(__ID, idInCluster), validKey); if (o != null && (Boolean) o.get(__VALID)) { BasicDBObject update = new BasicDBObject(); BasicDBObject sets = new BasicDBObject(); sets.put(__VALID, false); sets.put(__INVALIDATED, System.currentTimeMillis()); update.put("$set", sets); BasicDBObject key = new BasicDBObject(__ID, idInCluster); _dbSessions.update(key, update, false, false, WriteConcern.SAFE); } } /*------------------------------------------------------------ */ /** * Change the session id. Note that this will change the session id for all contexts for which the session id is in use. * @see org.eclipse.jetty.nosql.NoSqlSessionManager#update(org.eclipse.jetty.nosql.NoSqlSession, java.lang.String, java.lang.String) */ @Override protected void update(NoSqlSession session, String newClusterId, String newNodeId) throws Exception { BasicDBObject key = new BasicDBObject(__ID, session.getClusterId()); BasicDBObject sets = new BasicDBObject(); BasicDBObject update = new BasicDBObject(__ID, newClusterId); sets.put("$set", update); _dbSessions.update(key, sets, false, false, WriteConcern.SAFE); } /*------------------------------------------------------------ */ protected String encodeName(String name) { return name.replace("%", "%25").replace(".", "%2E"); } /*------------------------------------------------------------ */ protected String decodeName(String name) { return name.replace("%2E", ".").replace("%25", "%"); } /*------------------------------------------------------------ */ protected Object encodeName(Object value) throws IOException { if (value instanceof Number || value instanceof String || value instanceof Boolean || value instanceof Date) { return value; } else if (value.getClass().equals(HashMap.class)) { BasicDBObject o = new BasicDBObject(); for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) { if (!(entry.getKey() instanceof String)) { o = null; break; } o.append(encodeName(entry.getKey().toString()), encodeName(entry.getValue())); } if (o != null) return o; } ByteArrayOutputStream bout = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bout); out.reset(); out.writeUnshared(value); out.flush(); return bout.toByteArray(); } /*------------------------------------------------------------ */ protected Object decodeValue(final Object valueToDecode) throws IOException, ClassNotFoundException { if (valueToDecode == null || valueToDecode instanceof Number || valueToDecode instanceof String || valueToDecode instanceof Boolean || valueToDecode instanceof Date) { return valueToDecode; } else if (valueToDecode instanceof byte[]) { final byte[] decodeObject = (byte[]) valueToDecode; final ByteArrayInputStream bais = new ByteArrayInputStream(decodeObject); final ClassLoadingObjectInputStream objectInputStream = new ClassLoadingObjectInputStream(bais); return objectInputStream.readUnshared(); } else if (valueToDecode instanceof DBObject) { Map<String, Object> map = new HashMap<String, Object>(); for (String name : ((DBObject) valueToDecode).keySet()) { String attr = decodeName(name); map.put(attr, decodeValue(((DBObject) valueToDecode).get(name))); } return map; } else { throw new IllegalStateException(valueToDecode.getClass().toString()); } } /*------------------------------------------------------------ */ private String getContextKey() { return __CONTEXT + "." + _contextId; } /*------------------------------------------------------------ */ /** Get a dot separated key for * @param key * @return */ private String getContextAttributeKey(String attr) { return getContextKey() + "." + attr; } /*------------------------------------------------------------ */ @ManagedOperation(value = "purge invalid sessions in the session store based on normal criteria", impact = "ACTION") public void purge() { ((MongoSessionIdManager) _sessionIdManager).purge(); } /*------------------------------------------------------------ */ @ManagedOperation(value = "full purge of invalid sessions in the session store", impact = "ACTION") public void purgeFully() { ((MongoSessionIdManager) _sessionIdManager).purgeFully(); } /*------------------------------------------------------------ */ @ManagedOperation(value = "scavenge sessions known to this manager", impact = "ACTION") public void scavenge() { ((MongoSessionIdManager) _sessionIdManager).scavenge(); } /*------------------------------------------------------------ */ @ManagedOperation(value = "scanvenge all sessions", impact = "ACTION") public void scavengeFully() { ((MongoSessionIdManager) _sessionIdManager).scavengeFully(); } /*------------------------------------------------------------ */ /** * returns the total number of session objects in the session store * * the count() operation itself is optimized to perform on the server side * and avoid loading to client side. * @return the session store count */ @ManagedAttribute("total number of known sessions in the store") public long getSessionStoreCount() { return _dbSessions.find().count(); } /*------------------------------------------------------------ */ /** * MongoDB keys are . delimited for nesting so .'s are protected characters * * @param virtualHosts * @param contextPath * @return */ private String createContextId(String[] virtualHosts, String contextPath) { String contextId = virtualHosts[0] + contextPath; contextId.replace('/', '_'); contextId.replace('.', '_'); contextId.replace('\\', '_'); return contextId; } /*------------------------------------------------------------ */ /** * Dig through a given dbObject for the nested value */ private Object getNestedValue(DBObject dbObject, String nestedKey) { String[] keyChain = nestedKey.split("\\."); DBObject temp = dbObject; for (int i = 0; i < keyChain.length - 1; ++i) { temp = (DBObject) temp.get(keyChain[i]); if (temp == null) { return null; } } return temp.get(keyChain[keyChain.length - 1]); } /*------------------------------------------------------------ */ /** * ClassLoadingObjectInputStream * * */ protected class ClassLoadingObjectInputStream extends ObjectInputStream { public ClassLoadingObjectInputStream(java.io.InputStream in) throws IOException { super(in); } public ClassLoadingObjectInputStream() throws IOException { super(); } @Override public Class<?> resolveClass(java.io.ObjectStreamClass cl) throws IOException, ClassNotFoundException { try { return Class.forName(cl.getName(), false, Thread.currentThread().getContextClassLoader()); } catch (ClassNotFoundException e) { return super.resolveClass(cl); } } } }