/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source 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, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.server.session; import java.io.IOException; import java.io.InputStream; import java.io.NotSerializableException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.ServletContext; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionActivationListener; import javax.servlet.http.HttpSessionAttributeListener; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionBindingListener; import javax.servlet.http.HttpSessionContext; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; import com.caucho.distcache.ByteStreamCache; import com.caucho.distcache.ExtCacheEntry; import com.caucho.security.Login; import com.caucho.server.webapp.WebApp; import com.caucho.util.Alarm; import com.caucho.util.CacheListener; import com.caucho.util.L10N; import com.caucho.vfs.IOExceptionWrapper; import com.caucho.vfs.TempOutputStream; /** * Implements a HTTP session. */ public class SessionImpl implements HttpSession, CacheListener { private static final Logger log = Logger.getLogger(SessionImpl.class.getName()); private static final L10N L = new L10N(SessionImpl.class); // the session's identifier private String _id; // the owning session manager protected SessionManager _manager; // the session objectStore // Map containing the actual values. protected Map<String,Object> _values; // time the session was created private long _creationTime; // time the session was last accessed private long _accessTime; // maximum time the session may stay alive. private long _lastUseTime; private long _idleTimeout; private boolean _isIdleSet; // true if the session is new private boolean _isNew = true; // true if the application has modified the data private boolean _isModified = false; // true if the session is still valid, i.e. not invalidated private boolean _isValid = true; // true if the session is closing private boolean _isClosing = false; // true if the session is being closed from an invalidation private boolean _isInvalidating = false; // the cache entry saved in the session private ExtCacheEntry _cacheEntry; // to protect for threading private final AtomicInteger _useCount = new AtomicInteger(); /** * Create a new session object. * * @param manager the owning session manager. * @param id the session identifier. * @param creationTime the time in milliseconds when the session was created. */ public SessionImpl(SessionManager manager, String id, long creationTime) { _manager = manager; // TCK requires exact time creationTime = Alarm.getExactTime(); _creationTime = creationTime; _accessTime = creationTime; _lastUseTime = _accessTime; _idleTimeout = manager.getSessionTimeout(); _id = id; _values = createValueMap(); if (log.isLoggable(Level.FINE)) log.fine(this + " new"); } /** * Create the map used to objectStore values. */ protected Map<String,Object> createValueMap() { return new TreeMap<String,Object>(); } /** * Returns the time the session was created. */ @Override public long getCreationTime() { // this test forced by TCK if (! _isValid) throw new IllegalStateException(L.l("{0}: can't call getCreationTime() when session is no longer valid.", this)); return _creationTime; } /** * Returns the session identifier. */ @Override public String getId() { return _id; } /** * Returns the last objectAccess time. */ @Override public long getLastAccessedTime() { // this test forced by TCK if (! _isValid) throw new IllegalStateException(L.l("{0}: can't call getLastAccessedTime() when session is no longer valid.", this)); return _accessTime; } /** * Returns the time the session is allowed to be alive. * * @return time allowed to live in seconds */ @Override public int getMaxInactiveInterval() { if (Long.MAX_VALUE / 2 <= _idleTimeout) return -1; else return (int) (_idleTimeout / 1000); } /** * Sets the maximum time a session is allowed to be alive. * * @param value time allowed to live in seconds */ @Override public void setMaxInactiveInterval(int value) { if (value < 0) _idleTimeout = Long.MAX_VALUE / 2; else _idleTimeout = ((long) value) * 1000; _isIdleSet = true; } /** * Returns the session context. * * @deprecated */ public HttpSessionContext getSessionContext() { return null; } /** * Returns the servlet context. */ public ServletContext getServletContext() { return _manager.getWebApp(); } /** * Returns the session manager. */ public SessionManager getManager() { return _manager; } /** * Returns true if the session is new. */ public boolean isNew() { if (! _isValid) throw new IllegalStateException(L.l("{0} can't call isNew() when session is no longer valid.", this)); return _isNew; } /** * Returns true if the session is valid. */ public boolean isValid() { return _isValid; } public boolean isTimeout() { return isTimeout(Alarm.getCurrentTime()); } boolean isTimeout(long now) { long maxIdleTime = _idleTimeout; if (inUse()) return false; else if (now < _lastUseTime + maxIdleTime) { return false; } else { long lastUseTime = getLastUseTime(); return lastUseTime + maxIdleTime < now; } } private long getLastUseTime() { ByteStreamCache cache = _manager.getCache(); if (cache == null) return _lastUseTime; ExtCacheEntry entry = cache.peekExtCacheEntry(_id); // server/01ke if (entry == null) return _lastUseTime; else return entry.getLastAccessedTime(); } boolean isClosing() { return _isClosing; } /** * Returns true if the session is empty. */ public boolean isEmpty() { return _values == null || _values.size() == 0; } /** * Returns true if the session is in use. */ public boolean inUse() { return _useCount.get() > 0; } // // Attribute API // /** * Returns the named attribute from the session. */ @Override public Object getAttribute(String name) { if (! _isValid) throw new IllegalStateException(L.l("{0}: can't call getAttribute() when session is no longer valid.", this)); synchronized (_values) { Object value = _values.get(name); return value; } } void setModified() { if (_values.size() > 0) _isModified = true; } /** * Sets a session attribute. If the value is a listener, notify it * of the objectModified. If the value has changed mark the session as changed * for persistent sessions. * * @param name the name of the attribute * @param value the value of the attribute */ @Override public void setAttribute(String name, Object value) { if (! _isValid) throw new IllegalStateException(L.l("{0}: can't call setAttribute(String, Object) when session is no longer valid.", this)); Object oldValue; if (value != null && ! (value instanceof Serializable) && log.isLoggable(Level.FINE)) { log.fine(L.l("{0} attribute '{1}' value is non-serializable type '{2}'", this, name, value.getClass().getName())); } synchronized (_values) { if (value != null) oldValue = _values.put(name, value); else oldValue = _values.remove(name); } // server/017p _isModified = true; if (oldValue instanceof HttpSessionBindingListener) { HttpSessionBindingListener listener; listener = (HttpSessionBindingListener) oldValue; listener.valueUnbound(new HttpSessionBindingEvent(SessionImpl.this, name, oldValue)); } if (value instanceof HttpSessionBindingListener) { HttpSessionBindingListener listener; listener = (HttpSessionBindingListener) value; listener.valueBound(new HttpSessionBindingEvent(SessionImpl.this, name, value)); } // Notify the attribute listeners ArrayList listeners = _manager.getAttributeListeners(); if (listeners != null && listeners.size() > 0) { HttpSessionBindingEvent event; if (oldValue != null) event = new HttpSessionBindingEvent(this, name, oldValue); else event = new HttpSessionBindingEvent(this, name, value); for (int i = 0; i < listeners.size(); i++) { HttpSessionAttributeListener listener; listener = (HttpSessionAttributeListener) listeners.get(i); if (oldValue != null) listener.attributeReplaced(event); else listener.attributeAdded(event); } } } /** * Remove a session attribute. If the value is a listener, notify it * of the objectModified. * * @param name the name of the attribute to objectRemove */ public void removeAttribute(String name) { if (! _isValid) throw new IllegalStateException(L.l("{0}: can't call removeAttribute(String) when session is no longer valid.", this)); Object oldValue; synchronized (_values) { oldValue = _values.remove(name); } if (oldValue != null) _isModified = true; notifyValueUnbound(name, oldValue); } /** * Return an enumeration of all the sessions' attribute names. * * @return enumeration of the attribute names. */ public Enumeration getAttributeNames() { synchronized (_values) { if (! _isValid) throw new IllegalStateException(L.l("{0} can't call getAttributeNames() when session is no longer valid.", this)); return Collections.enumeration(_values.keySet()); } } /** * @deprecated */ public Object getValue(String name) { return getAttribute(name); } /** * @deprecated */ public void putValue(String name, Object value) { setAttribute(name, value); } /** * @deprecated */ public void removeValue(String name) { removeAttribute(name); } /** * @deprecated */ public String []getValueNames() { synchronized (_values) { if (! _isValid) throw new IllegalStateException(L.l("{0} can't call getValueNames() when session is no longer valid.", this)); if (_values == null) return new String[0]; String []s = new String[_values.size()]; Enumeration e = getAttributeNames(); int count = 0; while (e.hasMoreElements()) s[count++] = (String) e.nextElement(); return s; } } // // lifecycle: creation // /** * Creates a new session. */ void create(long now, boolean isCreate) { if (log.isLoggable(Level.FINE)) { log.fine(this + " create session"); } // e.g. server 'C' when 'A' and 'B' have no record of session if (_isValid) unbind(); // TCK now cares about exact time now = Alarm.getCurrentTime(); _isValid = true; _isNew = true; _accessTime = now; _creationTime = now; /* if (_clusterObject != null && isCreate) _clusterObject.objectCreate(); */ } /** * Set true if the session is in use. */ boolean addUse() { _lastUseTime = Alarm.getCurrentTime(); _useCount.incrementAndGet(); /* synchronized (this) { if (_isClosing) return false; _useCount++; return true; } */ return true; } /** * Set true if the session is in use. */ void endUse() { _useCount.decrementAndGet(); /* synchronized (this) { _useCount--; } */ } /* * Set the current objectAccess time to now. */ void setAccess(long now) { // server/01k0 if (_useCount.get() > 1) return; _isNew = false; /* if (_clusterObject != null) _clusterObject.objectAccess(); */ // TCK now cares about exact time /*now = Alarm.getExactTime(); _accessTime = now;*/ } public void setAccessTime(long now) { // server/0123 (vs TCK?) _accessTime = now; } /** * Cleaning up session stuff at the end of a request. * * <p>If the session data has changed and we have persistent sessions, * save the session. However, if save-on-shutdown is true, only save * on a server shutdown. */ public void finishRequest() { // server/0122 _accessTime = Alarm.getCurrentTime(); _isNew = false; // update cache access? if (_useCount.decrementAndGet() > 0) return; saveAfterRequest(); } // // persistent sessions and passivation // /** * Loads the session. * * @return true if the session was found in the persistent store */ public boolean load(boolean isNew) { if (! _isValid) return false; else if (_isIdleSet && _accessTime + _idleTimeout < Alarm.getCurrentTime()) { // server/01o2 (tck) return false; } // server/01k0 if (_useCount.get() > 1) return true; try { ByteStreamCache cache = _manager.getCache(); if (cache == null) return ! isNew; // server/015m if (! isNew && _manager.isSaveOnShutdown()) return true; ExtCacheEntry entry = cache.getExtCacheEntry(_id); ExtCacheEntry cacheEntry = _cacheEntry; if (entry != null && ! entry.isValueNull()) { // server/01a1, #4419 _idleTimeout = entry.getAccessedExpireTimeout(); // _idleTimeout = entry.getIdleTimeout() * 4 / 5; //_isIdleSet = true; } if (entry != null && cacheEntry != null && cacheEntry.getValueHashKey() != null && cacheEntry.getValueHashKey().equals(entry.getValueHashKey())) { if (log.isLoggable(Level.FINE)) { log.fine(this + " session load-same valueHash=" + (entry != null ? entry.getValueHashKey() : null)); } return true; } TempOutputStream os = new TempOutputStream(); if (cache.get(_id, os)) { InputStream is = os.getInputStream(); SessionDeserializer in = _manager.createSessionDeserializer(is); if (log.isLoggable(Level.FINE)) { log.fine(this + " session load valueHash=" + (entry != null ? entry.getValueHashKey() : null)); } load(in); in.close(); is.close(); _cacheEntry = entry; return true; } else { _cacheEntry = null; if (log.isLoggable(Level.FINE)) { log.fine(this + " session remove"); } if (cacheEntry == null) return true; } } catch (IOException e) { log.log(Level.WARNING, e.toString(), e); } return false; } /** * Loads the object from the input stream. */ public void load(SessionDeserializer in) throws IOException { HttpSessionEvent event = null; ArrayList<HttpSessionActivationListener> listeners = null; synchronized (this) { synchronized (_values) { // server/017u _values.clear(); // unbind(); try { int size = in.readInt(); //System.out.println("LOAD: " + size + " " + this + " " + _clusterObject + System.identityHashCode(this)); for (int i = 0; i < size; i++) { String key = (String) in.readObject(); Object value = in.readObject(); if (value != null) { _values.put(key, value); if (value instanceof HttpSessionActivationListener) { HttpSessionActivationListener listener = (HttpSessionActivationListener) value; if (event == null) event = new HttpSessionEvent(this); if (listeners == null) listeners = new ArrayList<HttpSessionActivationListener>(); listeners.add(listener); } } } } catch (Exception e) { throw IOExceptionWrapper.create(e); } } } for (int i = 0; listeners != null && i < listeners.size(); i++) { HttpSessionActivationListener listener = listeners.get(i); if (event == null) event = new HttpSessionEvent(this); listener.sessionDidActivate(event); } listeners = _manager.getActivationListeners(); for (int i = 0; listeners != null && i < listeners.size(); i++) { HttpSessionActivationListener listener = listeners.get(i); if (event == null) event = new HttpSessionEvent(this); listener.sessionDidActivate(event); } } /** * Clears the session when reading a bad saved session. */ void reset(long now) { if (log.isLoggable(Level.FINER)) log.fine(this + " reset"); now = Alarm.getCurrentTime(); unbind(); _isValid = true; _isNew = true; _accessTime = now; _creationTime = now; } /** * Save changes before any flush. */ public final void saveBeforeFlush() { if (_manager == null || ! _manager.isSaveBeforeFlush()) return; save(); } /** * Flush changes before the headers. */ public final void saveBeforeHeaders() { if (_manager == null || ! _manager.isSaveBeforeHeaders()) return; save(); } /** * Flush changes after a request completes. */ public final void saveAfterRequest() { if (_manager == null || ! _manager.isSaveAfterRequest()) return; save(); } /** * Saves changes to the session. */ public final void save() { if (! isValid()) return; try { if (! _isModified && ! _manager.getAlwaysSaveSession()) return; if (! _manager.isPersistenceEnabled()) return; _isModified = false; TempOutputStream os = new TempOutputStream(); SessionSerializer out = _manager.createSessionSerializer(os); store(out); out.close(); _manager.addSessionSaveSample(os.getLength()); _cacheEntry = _manager.getCache().put(_id, os.getInputStream(), _idleTimeout, _idleTimeout); if (log.isLoggable(Level.FINE)) { log.fine(this + " session save valueHash=" + (_cacheEntry != null ? _cacheEntry.getValueHashKey() : null)); } os.close(); } catch (Exception e) { log.log(Level.WARNING, this + ": can't serialize session", e); } } /** * Store on shutdown. */ void saveOnShutdown() { /* if (session.isValid()) { synchronized (session) { // server/016i, server/018x if (! session.isEmpty()) session.saveOnShutdown(); } } */ save(); /* try { ClusterObject clusterObject = _clusterObject; if (clusterObject != null) { clusterObject.objectModified(); clusterObject.objectStore(this); } } catch (Throwable e) { log.log(Level.WARNING, this + ": can't serialize session", e); } */ } /** * Passivates the session. */ public void passivate() { unbind(); } /** * Saves the object to the input stream. */ public void store(SessionSerializer out) throws IOException { Set<Map.Entry<String,Object>> set = null; HttpSessionEvent event = null; ArrayList<HttpSessionActivationListener> listeners; synchronized (_values) { set = _values.entrySet(); int size = set == null ? 0 : set.size(); if (size == 0) { out.writeInt(0); return; } listeners = _manager.getActivationListeners(); if (listeners != null && listeners.size() > 0) { if (event == null) event = new HttpSessionEvent(this); for (int i = 0; i < listeners.size(); i++) { HttpSessionActivationListener listener = listeners.get(i); listener.sessionWillPassivate(event); } } for (Map.Entry entry : set) { Object value = entry.getValue(); if (value instanceof HttpSessionActivationListener) { HttpSessionActivationListener listener = (HttpSessionActivationListener) value; if (event == null) event = new HttpSessionEvent(this); listener.sessionWillPassivate(event); } } } synchronized (this) { synchronized (_values) { set = _values.entrySet(); int size = set == null ? 0 : set.size(); out.writeInt(size); if (size == 0) return; boolean ignoreNonSerializable = getManager().getIgnoreSerializationErrors(); for (Map.Entry entry : set) { Object value = entry.getValue(); out.writeObject(entry.getKey()); if (ignoreNonSerializable && ! (value instanceof Serializable)) { out.writeObject(null); continue; } try { out.writeObject(value); } catch (NotSerializableException e) { log.warning(L.l("{0}: failed storing persistent session attribute '{1}'. Persistent session values must extend java.io.Serializable.\n{2}", this, entry.getKey(), String.valueOf(e))); throw e; } } } } } // // invalidation, lru, timeout // /** * Invalidates the session, called by user code. * * This should never be called by Resin code (for logging purposes) */ public void invalidate() { if (log.isLoggable(Level.FINE)) log.fine(this + " invalidate"); _isInvalidating = true; invalidate(Logout.INVALIDATE); } /** * Called by the session manager for a session timeout */ public void timeout() { if (! isValid()) return; if (! _manager.isPersistenceEnabled()) { // if no persistent store then invalidate // XXX: server/12cg - single signon shouldn't logout invalidateTimeout(); } /* else if (session.getSrunIndex() != _selfIndex && _selfIndex >= 0) { if (log.isLoggable(Level.FINE)) log.fine(session + " timeout (backup)"); // if not the owner, then just remove _sessions.remove(session.getId()); } */ else { invalidateTimeout(); } } /** * Callback when the session is removed from the session cache, generally * because the session cache is full. */ @Override public void removeEvent() { synchronized (this) { if (_isInvalidating || _useCount.get() <= 0) _isClosing = true; } if (! _isClosing) { log.warning(L.l("{0} LRU while in use (use-count={1}). Consider increasing session-count.", this, _useCount)); } if (log.isLoggable(Level.FINE)) log.fine(this + " remove"); long now = Alarm.getCurrentTime(); // server/015k, server/10g2 if (_isInvalidating || _manager.isDestroyOnLru() || _accessTime + getMaxInactiveInterval() < now) { notifyDestroy(); } invalidateLocal(); } private void notifyDestroy() { // server/01ni /* XXX: if (_clusterObject != null && ! _clusterObject.isPrimary()) return; */ ArrayList<HttpSessionListener> listeners = _manager.getListeners(); if (listeners != null) { HttpSessionEvent event = new HttpSessionEvent(this); for (int i = listeners.size() - 1; i >= 0; i--) { HttpSessionListener listener; listener = (HttpSessionListener) listeners.get(i); listener.sessionDestroyed(event); } } } /** * Invalidates a session based on a logout. */ public void invalidateLogout() { if (log.isLoggable(Level.FINE)) log.fine(this + " logout"); _isInvalidating = true; invalidate(Logout.INVALIDATE); } /** * Invalidates a session based on a timeout */ void invalidateTimeout() { if (log.isLoggable(Level.FINE)) log.fine(this + " timeout"); _isInvalidating = _manager.isOwner(_id); invalidate(Logout.TIMEOUT); } /** * Invalidates a session based on a LRU */ void invalidateLru() { if (log.isLoggable(Level.FINE)) log.fine(this + " lru"); invalidateImpl(Logout.LRU); } /** * Invalidates the session, called by user code. * * This should never be called by Resin code (for logging purposes) */ public void invalidateRemote() { if (log.isLoggable(Level.FINE)) log.fine(this + " invalidate remote"); _isInvalidating = true; invalidate(Logout.INVALIDATE); } /** * Invalidates the session. */ private void invalidate(Logout logout) { if (! _isValid) throw new IllegalStateException(L.l("{0}: Can't call invalidate() when session is no longer valid.", this)); try { // server/017s // server/12i1, 12ch Login login = _manager.getWebApp().getLogin(); if (login != null) login.sessionInvalidate(this, logout == Logout.TIMEOUT); _manager.removeSession(this); invalidateImpl(logout); } finally { _isValid = false; } } /** * Invalidate the session, removing it from the manager, * unbinding the values, and removing it from the objectStore. */ private void invalidateImpl(Logout logout) { boolean invalidateAfterListener = _manager.isInvalidateAfterListener(); if (! invalidateAfterListener) _isValid = false; try { if (_isInvalidating && _manager.getSessionStore() != null) { boolean isRemove = false; /* if (logout == Logout.TIMEOUT) { // server/016r ExtCacheEntry entry = _manager.getSessionStore().peekExtCacheEntry(_id); if (entry == null || ! entry.isValid()) { isRemove = true; } } else isRemove = true; */ // server/018z isRemove = true; if (isRemove) _manager.getSessionStore().remove(_id); } } catch (Exception e) { log.log(Level.FINE, e.toString(), e); } invalidateLocal(); } /** * unbinds the session and saves if necessary. */ private void invalidateLocal() { if (_isValid && ! _isInvalidating) { if (_manager.isSaveOnlyOnShutdown()) { save(); } } unbind(); // we're invalidating, not passivating } /** * Cleans up the session. */ public void unbind() { if (_values.size() == 0) return; // ClusterObject clusterObject = _clusterObject; ArrayList<String> names = new ArrayList<String>(); ArrayList<Object> values = new ArrayList<Object>(); synchronized (_values) { /* if (_useCount > 0) Thread.dumpStack(); */ for (Map.Entry<String,Object> entry : _values.entrySet()) { names.add(entry.getKey()); values.add(entry.getValue()); } _values.clear(); } // server/015a for (int i = 0; i < names.size(); i++) { String name = names.get(i); Object value = values.get(i); notifyValueUnbound(name, value); } } /** * Notify any value unbound listeners. */ private void notifyValueUnbound(String name, Object oldValue) { if (oldValue == null) return; if (oldValue instanceof HttpSessionBindingListener) { HttpSessionBindingListener listener; listener = (HttpSessionBindingListener) oldValue; listener.valueUnbound(new HttpSessionBindingEvent(this, name, oldValue)); } // Notify the attributes listeners ArrayList listeners = _manager.getAttributeListeners(); if (listeners != null) { HttpSessionBindingEvent event; event = new HttpSessionBindingEvent(this, name, oldValue); for (int i = 0; i < listeners.size(); i++) { HttpSessionAttributeListener listener; listener = (HttpSessionAttributeListener) listeners.get(i); listener.attributeRemoved(event); } } } @Override public String toString() { String contextPath = ""; SessionManager manager = _manager; if (manager != null) { WebApp webApp = manager.getWebApp(); if (webApp != null) contextPath = "," + webApp.getContextPath(); } return getClass().getSimpleName() + "[" + getId() + contextPath + "]"; } enum Logout { INVALIDATE, LRU, TIMEOUT }; }