/* * Copyright 2005 Joe Walker * * 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 org.directwebremoting.impl; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import javax.swing.event.EventListenerList; import org.directwebremoting.Container; import org.directwebremoting.ScriptBuffer; import org.directwebremoting.ScriptSession; import org.directwebremoting.event.ScriptSessionEvent; import org.directwebremoting.event.ScriptSessionListener; import org.directwebremoting.extend.EnginePrivate; import org.directwebremoting.extend.InitializingBean; import org.directwebremoting.extend.PageNormalizer; import org.directwebremoting.extend.RealScriptSession; import org.directwebremoting.extend.ScriptSessionManager; import org.directwebremoting.extend.UninitializingBean; import org.directwebremoting.util.IdGenerator; import org.directwebremoting.util.Loggers; /** * A default implementation of ScriptSessionManager. * <p>There are synchronization constraints on this class that could be broken * by subclasses. Specifically anyone accessing either <code>sessionMap</code> * or <code>pageSessionMap</code> must be holding the <code>sessionLock</code>. * <p>In addition you should note that {@link DefaultScriptSession} and * {@link DefaultScriptSessionManager} make calls to each other and you should * take care not to break any constraints in inheriting from these classes. * @author Joe Walker [joe at getahead dot ltd dot uk] */ public class DefaultScriptSessionManager implements ScriptSessionManager, InitializingBean, UninitializingBean { /* (non-Javadoc) * @see org.directwebremoting.extend.InitializingBean#afterContainerSetup(org.directwebremoting.Container) */ public void afterContainerSetup(Container container) { Runnable runnable = new Runnable() { public void run() { maybeCheckTimeouts(); } }; future = executor.scheduleWithFixedDelay(runnable, 60, 60, TimeUnit.SECONDS); } /* (non-Javadoc) * @see org.directwebremoting.extend.UninitializingBean#contextDestroyed() */ public void contextDestroyed() { } /* (non-Javadoc) * @see org.directwebremoting.extend.UninitializingBean#servletDestroyed() */ public void servletDestroyed() { future.cancel(true); } /* (non-Javadoc) * @see org.directwebremoting.ScriptSessionManager#getScriptSession(java.lang.String) */ public RealScriptSession getScriptSession(String sentScriptId, String page, String httpSessionId) { maybeCheckTimeouts(); DefaultScriptSession scriptSession; synchronized (sessionLock) { scriptSession = sessionMap.get(sentScriptId); if (scriptSession == null) { // Force creation of a new script session String newSessionId = generator.generateId(16); scriptSession = new DefaultScriptSession(newSessionId, this, page); Loggers.SESSION.debug("Creating " + scriptSession + " on " + scriptSession.getPage()); sessionMap.put(newSessionId, scriptSession); // See notes on synchronization in invalidate() fireScriptSessionCreatedEvent(scriptSession); // Inject a (new) script session id into the page ScriptBuffer script = EnginePrivate.getRemoteHandleNewScriptSessionScript(newSessionId); scriptSession.addScript(script); // Use the new script session id not the one passed in Loggers.SESSION.debug("ScriptSession re-sync: " + simplifyId(sentScriptId) + " has become " + simplifyId(newSessionId) + " on " + page); } else { // This could be called from a poll or an rpc call, so this is a // good place to update the session access time scriptSession.updateLastAccessedTime(); String storedPage = scriptSession.getPage(); if (!storedPage.equals(page)) { Loggers.SESSION.error("Invalid Page: Passed page=" + page + ", but page in script session=" + storedPage); throw new SecurityException("Invalid Page"); } } associateScriptSessionAndPage(scriptSession, page); associateScriptSessionAndHttpSession(scriptSession, httpSessionId); // Maybe we should update the access time of the ScriptSession? // scriptSession.updateLastAccessedTime(); // Since this call could come from outside of a call from the // browser, it's not really an indication that this session is still // alive, so we don't. } return scriptSession; } /** * Link a script session and an http session in some way * Exactly what we should do here is still something of a mystery. We don't * really have much experience on the best way to handle this, so currently * we're just setting a script session attribute that points at the * http session id, and not exposing it. * <p>This method is an ideal point to override and do something better. * @param scriptSession The script session to be linked to an http session * @param httpSessionId The http session from the browser with the given scriptSession */ protected void associateScriptSessionAndHttpSession(DefaultScriptSession scriptSession, String httpSessionId) { if (httpSessionId == null) { return; } scriptSession.setAttribute(ATTRIBUTE_HTTPSESSIONID, httpSessionId); Set<String> scriptSessionIds = sessionXRef.get(httpSessionId); if (scriptSessionIds == null) { scriptSessionIds = new HashSet<String>(); sessionXRef.put(httpSessionId, scriptSessionIds); } scriptSessionIds.add(scriptSession.getId()); } /** * Unlink any http sessions from this script session * @see #associateScriptSessionAndHttpSession(DefaultScriptSession, String) * @param scriptSession The script session to be unlinked */ protected void disassociateScriptSessionAndHttpSession(DefaultScriptSession scriptSession) { Object httpSessionId = scriptSession.getAttribute(ATTRIBUTE_HTTPSESSIONID); if (httpSessionId == null) { return; } Set<String> scriptSessionIds = sessionXRef.get(httpSessionId); if (scriptSessionIds == null) { Loggers.SESSION.debug("Warning: No script session ids for http session"); return; } scriptSessionIds.remove(scriptSession.getId()); if (scriptSessionIds.size() == 0) { sessionXRef.remove(httpSessionId); } scriptSession.setAttribute(ATTRIBUTE_HTTPSESSIONID, null); } /** * Link a script session to a web page. * <p>This allows people to call {@link org.directwebremoting.Browser#withPage} * <p>This method is an ideal point to override and do something better. * @param scriptSession The script session to be linked to a page * @param page The page (un-normalized) to be linked to */ protected void associateScriptSessionAndPage(DefaultScriptSession scriptSession, String page) { if (page == null) { return; } String normalizedPage = pageNormalizer.normalizePage(page); Set<DefaultScriptSession> pageSessions = pageSessionMap.get(normalizedPage); if (pageSessions == null) { pageSessions = new HashSet<DefaultScriptSession>(); pageSessionMap.put(normalizedPage, pageSessions); } pageSessions.add(scriptSession); scriptSession.setAttribute(ATTRIBUTE_PAGE, normalizedPage); } /** * Unlink any pages from this script session * @see #associateScriptSessionAndPage(DefaultScriptSession, String) * @param scriptSession The script session to be unlinked */ protected void disassociateScriptSessionAndPage(DefaultScriptSession scriptSession) { for (Set<DefaultScriptSession> pageSessions : pageSessionMap.values()) { pageSessions.remove(scriptSession); } } /* (non-Javadoc) * @see org.directwebremoting.extend.ScriptSessionManager#getScriptSessionsByHttpSessionId(java.lang.String) */ public Collection<RealScriptSession> getScriptSessionsByHttpSessionId(String httpSessionId) { Collection<RealScriptSession> reply = new ArrayList<RealScriptSession>(); synchronized (sessionLock) { Set<String> scriptSessionIds = sessionXRef.get(httpSessionId); if (scriptSessionIds != null) { for (String scriptSessionId : scriptSessionIds) { DefaultScriptSession scriptSession = sessionMap.get(scriptSessionId); if (scriptSession != null) { reply.add(scriptSession); } } } } return reply; } /* (non-Javadoc) * @see org.directwebremoting.ScriptSessionManager#getAllScriptSessions() */ public Collection<ScriptSession> getAllScriptSessions() { synchronized (sessionLock) { Set<ScriptSession> reply = new HashSet<ScriptSession>(); reply.addAll(sessionMap.values()); return reply; } } /** * Remove the given session from the list of sessions that we manage, and * leave it for the GC vultures to pluck. * @param scriptSession The session to get rid of */ protected void invalidate(DefaultScriptSession scriptSession) { Loggers.SESSION.debug("Invalidating " + scriptSession + " from " + scriptSession.getPage()); synchronized (sessionLock) { // Due to the way systems get a number of script sessions for a page // and the perform a number of actions on them, we may get a number // of invalidation checks, and therefore calls to invalidate(). // We could protect ourselves from this by having a // 'hasBeenInvalidated' flag, but we're taking the simple option // here of just allowing multiple invalidations. sessionMap.remove(scriptSession.getId()); disassociateScriptSessionAndPage(scriptSession); disassociateScriptSessionAndHttpSession(scriptSession); } // Are there any risks from doing this outside the locks? // The initial analysis is that 'Destroyed' is past tense so you would // have expected it to have happened already. fireScriptSessionDestroyedEvent(scriptSession); } /** * If we call {@link #checkTimeouts()} too often is could bog things down so * we only check every one in a while (default 30 secs); this checks to see * of we need to check, and checks if we do. */ protected void maybeCheckTimeouts() { long now = System.currentTimeMillis(); if (now - scriptSessionCheckTime > lastSessionCheckAt) { checkTimeouts(); lastSessionCheckAt = now; } } /** * Do a check on all the known sessions to see if and have timeout and need * removing. */ protected void checkTimeouts() { long now = System.currentTimeMillis(); List<ScriptSession> timeouts = new ArrayList<ScriptSession>(); synchronized (sessionLock) { for (DefaultScriptSession session : sessionMap.values()) { if (session.isInvalidated()) { continue; } long age = now - session.getLastAccessedTime(); if (age > scriptSessionTimeout) { timeouts.add(session); } } for (ScriptSession scriptSession : timeouts) { DefaultScriptSession session = (DefaultScriptSession) scriptSession; session.invalidate(); } } } /* (non-Javadoc) * @see org.directwebremoting.extend.ScriptSessionManager#addScriptSessionListener(org.directwebremoting.event.ScriptSessionListener) */ public void addScriptSessionListener(ScriptSessionListener li) { scriptSessionListeners.add(ScriptSessionListener.class, li); } /* (non-Javadoc) * @see org.directwebremoting.extend.ScriptSessionManager#removeScriptSessionListener(org.directwebremoting.event.ScriptSessionListener) */ public void removeScriptSessionListener(ScriptSessionListener li) { scriptSessionListeners.remove(ScriptSessionListener.class, li); } /** * This should be called whenever a {@link ScriptSession} is created * @param scriptSession The newly created ScriptSession */ protected void fireScriptSessionCreatedEvent(ScriptSession scriptSession) { ScriptSessionEvent ev = null; Object[] listeners = scriptSessionListeners.getListenerList(); for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == ScriptSessionListener.class) { if (ev == null) { ev = new ScriptSessionEvent(scriptSession); } ((ScriptSessionListener) listeners[i + 1]).sessionCreated(ev); } } } /** * This should be called whenever a {@link ScriptSession} is destroyed * @param scriptSession The newly destroyed ScriptSession */ protected void fireScriptSessionDestroyedEvent(ScriptSession scriptSession) { ScriptSessionEvent ev = null; Object[] listeners = scriptSessionListeners.getListenerList(); for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == ScriptSessionListener.class) { if (ev == null) { ev = new ScriptSessionEvent(scriptSession); } ((ScriptSessionListener) listeners[i + 1]).sessionDestroyed(ev); } } } /* (non-Javadoc) * @see org.directwebremoting.extend.ScriptSessionManager#getInitCode() */ public String getInitCode() { return "dwr.engine._execute(dwr.engine._pathToDwrServlet, '__System', 'pageLoaded', [ function() { dwr.engine._ordered = false; }]);"; } /** * ScriptSession IDs are too long to be useful to humans. We shorten them * to the first 4 characters. */ private String simplifyId(String id) { if (id == null) { return "[null]"; } if (id.length() == 0) { return "[blank]"; } if (id.length() < 4) { return id; } return id.substring(0, 4); } /* (non-Javadoc) * @see org.directwebremoting.ScriptSessionManager#getScriptSessionTimeout() */ public long getScriptSessionTimeout() { return scriptSessionTimeout; } /* (non-Javadoc) * @see org.directwebremoting.ScriptSessionManager#setScriptSessionTimeout(long) */ public void setScriptSessionTimeout(long scriptSessionTimeout) { this.scriptSessionTimeout = scriptSessionTimeout; } /** * How long do we wait before we timeout script sessions? */ private long scriptSessionTimeout = DEFAULT_TIMEOUT_MILLIS; /** * How we turn pages into the canonical form. * @param pageNormalizer The new PageNormalizer */ public void setPageNormalizer(PageNormalizer pageNormalizer) { this.pageNormalizer = pageNormalizer; } /** * @see #setPageNormalizer(PageNormalizer) */ protected PageNormalizer pageNormalizer; /** * How often do we check for script sessions that need timing out */ public void setScriptSessionCheckTime(long scriptSessionCheckTime) { this.scriptSessionCheckTime = scriptSessionCheckTime; } /** * @see #setScriptSessionCheckTime(long) */ protected long scriptSessionCheckTime = DEFAULT_SESSION_CHECK_TIME; /** * How often do we check for script sessions that need timing out */ public void setScheduledThreadPoolExecutor(ScheduledThreadPoolExecutor executor) { this.executor = executor; } /** * @see #setScheduledThreadPoolExecutor(ScheduledThreadPoolExecutor) */ protected ScheduledThreadPoolExecutor executor; /** * Use of this attribute is currently discouraged, we may make this public * in a later release. Until then, it may change or be removed without warning. */ public static final String ATTRIBUTE_HTTPSESSIONID = "org.directwebremoting.ScriptSession.HttpSessionId"; /** * Use of this attribute is currently discouraged, we may make this public * in a later release. Until then, it may change or be removed without warning. */ public static final String ATTRIBUTE_PAGE = "org.directwebremoting.ScriptSession.Page"; /** * By default we check for sessions that need expiring every 30 seconds */ protected static final long DEFAULT_SESSION_CHECK_TIME = 30000; /** * The list of current {@link ScriptSessionListener}s */ protected EventListenerList scriptSessionListeners = new EventListenerList(); /** * How we create script session ids. */ private static IdGenerator generator = new IdGenerator(); /** * The session timeout checker function so we can shutdown cleanly */ private ScheduledFuture<?> future; /** * We check for sessions that need timing out every * {@link #scriptSessionCheckTime}; this is when we last checked. */ protected long lastSessionCheckAt = System.currentTimeMillis(); /** * What we synchronize against when we want to access either sessionMap or * pageSessionMap */ protected final Object sessionLock = new Object(); /** * Allows us to associate script sessions with http sessions. * The key is an http session id, the * <p>GuardedBy("sessionLock") */ protected final Map<String, Set<String>> sessionXRef = new HashMap<String, Set<String>>(); /** * The map of all the known sessions. * The key is the script session id, the value is the session data * <p>GuardedBy("sessionLock") */ protected final Map<String, DefaultScriptSession> sessionMap = new HashMap<String, DefaultScriptSession>(); /** * The map of pages that have sessions. * The key is a normalized page, the value the script sessions that are * known to be currently visiting the page * <p>GuardedBy("sessionLock") */ protected final Map<String, Set<DefaultScriptSession>> pageSessionMap = new HashMap<String, Set<DefaultScriptSession>>(); }