001: /*
002: * Copyright 2005 Joe Walker
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.directwebremoting.impl;
017:
018: import java.util.ArrayList;
019: import java.util.Collection;
020: import java.util.HashMap;
021: import java.util.HashSet;
022: import java.util.List;
023: import java.util.Map;
024: import java.util.Set;
025: import java.util.concurrent.ScheduledThreadPoolExecutor;
026: import java.util.concurrent.TimeUnit;
027:
028: import javax.swing.event.EventListenerList;
029:
030: import org.apache.commons.logging.Log;
031: import org.apache.commons.logging.LogFactory;
032: import org.directwebremoting.ScriptSession;
033: import org.directwebremoting.ServerContext;
034: import org.directwebremoting.event.ScriptSessionEvent;
035: import org.directwebremoting.event.ScriptSessionListener;
036: import org.directwebremoting.extend.PageNormalizer;
037: import org.directwebremoting.extend.RealScriptSession;
038: import org.directwebremoting.extend.ScriptSessionManager;
039: import org.directwebremoting.util.IdGenerator;
040: import org.directwebremoting.util.SharedObjects;
041:
042: /**
043: * A default implementation of ScriptSessionManager.
044: * <p>There are synchronization constraints on this class that could be broken
045: * by subclasses. Specifically anyone accessing either <code>sessionMap</code>
046: * or <code>pageSessionMap</code> must be holding the <code>sessionLock</code>.
047: * <p>In addition you should note that {@link DefaultScriptSession} and
048: * {@link DefaultScriptSessionManager} make calls to each other and you should
049: * take care not to break any constraints in inheriting from these classes.
050: * @author Joe Walker [joe at getahead dot ltd dot uk]
051: */
052: public class DefaultScriptSessionManager implements
053: ScriptSessionManager {
054: /**
055: * Setup a timer that will invalidate sessions
056: */
057: public DefaultScriptSessionManager() {
058: Runnable runnable = new Runnable() {
059: public void run() {
060: maybeCheckTimeouts();
061: }
062: };
063:
064: ScheduledThreadPoolExecutor executor = SharedObjects
065: .getScheduledThreadPoolExecutor();
066: executor.schedule(runnable, 60, TimeUnit.SECONDS);
067:
068: // Maybe we need be able to cancel the executor?
069: // ScheduledFuture<?> future = executor.schedule...
070: }
071:
072: /* (non-Javadoc)
073: * @see org.directwebremoting.ScriptSessionManager#getScriptSession(java.lang.String)
074: */
075: public RealScriptSession getScriptSession(String id, String page,
076: String httpSessionId) {
077: maybeCheckTimeouts();
078:
079: DefaultScriptSession scriptSession;
080:
081: synchronized (sessionLock) {
082: scriptSession = sessionMap.get(id);
083: if (scriptSession == null) {
084: return null;
085: }
086:
087: associateScriptSessionAndPage(scriptSession, page);
088: associateScriptSessionAndHttpSession(scriptSession,
089: httpSessionId);
090:
091: // Maybe we should update the access time of the ScriptSession?
092: // scriptSession.updateLastAccessedTime();
093: // Since this call could come from outside of a call from the
094: // browser, it's not really an indication that this session is still
095: // alive, so we don't.
096: }
097:
098: return scriptSession;
099: }
100:
101: /* (non-Javadoc)
102: * @see org.directwebremoting.extend.ScriptSessionManager#createScriptSession(org.directwebremoting.extend.RealWebContext)
103: */
104: public RealScriptSession createScriptSession(String page,
105: String httpSessionId) {
106: String id = generator.generateId(16);
107:
108: DefaultScriptSession scriptSession = new DefaultScriptSession(
109: id, this , page, httpSessionId);
110:
111: synchronized (sessionLock) {
112: sessionMap.put(id, scriptSession);
113:
114: associateScriptSessionAndPage(scriptSession, page);
115: associateScriptSessionAndHttpSession(scriptSession,
116: httpSessionId);
117: }
118:
119: // See notes on synchronization in invalidate()
120: fireScriptSessionCreatedEvent(scriptSession);
121:
122: return scriptSession;
123: }
124:
125: /**
126: * Link a script session and an http session in some way
127: * Exactly what we should do here is still something of a mystery. We don't
128: * really have much experience on the best way to handle this, so currently
129: * we're just setting a script session attribute that points at the
130: * http session id, and not exposing it.
131: * <p>This method is an ideal point to override and do something better.
132: * @param scriptSession The script session to be linked to an http session
133: * @param httpSessionId The http session from the browser with the given scriptSession
134: */
135: protected void associateScriptSessionAndHttpSession(
136: RealScriptSession scriptSession, String httpSessionId) {
137: scriptSession.setAttribute(ATTRIBUTE_HTTPSESSIONID,
138: httpSessionId);
139:
140: Set<String> scriptSessionIds = sessionXRef.get(httpSessionId);
141: if (scriptSessionIds == null) {
142: scriptSessionIds = new HashSet<String>();
143: sessionXRef.put(httpSessionId, scriptSessionIds);
144: }
145: scriptSessionIds.add(scriptSession.getId());
146: }
147:
148: /**
149: * Unlink any http sessions from this script session
150: * @see #associateScriptSessionAndHttpSession(RealScriptSession, String)
151: * @param scriptSession The script session to be unlinked
152: */
153: protected void disassociateScriptSessionAndHttpSession(
154: RealScriptSession scriptSession) {
155: Object httpSessionId = scriptSession
156: .getAttribute(ATTRIBUTE_HTTPSESSIONID);
157: if (httpSessionId == null) {
158: return;
159: }
160:
161: Set<String> scriptSessionIds = sessionXRef.get(httpSessionId);
162: if (scriptSessionIds == null) {
163: log
164: .debug("Warning: No script session ids for http session");
165: return;
166: }
167: scriptSessionIds.remove(scriptSession.getId());
168: if (scriptSessionIds.size() == 0) {
169: sessionXRef.remove(httpSessionId);
170: }
171:
172: scriptSession.setAttribute(ATTRIBUTE_HTTPSESSIONID, null);
173: }
174:
175: /**
176: * Link a script session to a web page.
177: * <p>This allows people to call {@link ServerContext#getScriptSessionsByPage(String)}
178: * <p>This method is an ideal point to override and do something better.
179: * @param scriptSession The script session to be linked to a page
180: * @param page The page (un-normalized) to be linked to
181: */
182: protected void associateScriptSessionAndPage(
183: RealScriptSession scriptSession, String page) {
184: if (page == null) {
185: return;
186: }
187:
188: String normalizedPage = pageNormalizer.normalizePage(page);
189:
190: Set<RealScriptSession> pageSessions = pageSessionMap
191: .get(normalizedPage);
192: if (pageSessions == null) {
193: pageSessions = new HashSet<RealScriptSession>();
194: pageSessionMap.put(normalizedPage, pageSessions);
195: }
196:
197: pageSessions.add(scriptSession);
198:
199: scriptSession.setAttribute(ATTRIBUTE_PAGE, normalizedPage);
200: }
201:
202: /**
203: * Unlink any pages from this script session
204: * @see #associateScriptSessionAndPage(RealScriptSession, String)
205: * @param scriptSession The script session to be unlinked
206: */
207: protected void disassociateScriptSessionAndPage(
208: RealScriptSession scriptSession) {
209: for (Set<RealScriptSession> pageSessions : pageSessionMap
210: .values()) {
211: pageSessions.remove(scriptSession);
212: }
213: }
214:
215: /* (non-Javadoc)
216: * @see org.directwebremoting.ScriptSessionManager#getScriptSessionsByPage(java.lang.String)
217: */
218: public Collection<ScriptSession> getScriptSessionsByPage(String url) {
219: String normalizedPage = pageNormalizer.normalizePage(url);
220: synchronized (sessionLock) {
221: Set<RealScriptSession> pageSessions = pageSessionMap
222: .get(normalizedPage);
223: if (pageSessions == null) {
224: pageSessions = new HashSet<RealScriptSession>();
225: }
226:
227: Set<ScriptSession> reply = new HashSet<ScriptSession>();
228: reply.addAll(pageSessions);
229: return reply;
230: }
231: }
232:
233: /**
234: * Lookup all the windows associated with a given browser
235: * @param httpSessionId The browser id to lookup
236: * @return A list of script sessions for each open window
237: */
238: public Collection<RealScriptSession> getScriptSessionsByHttpSessionId(
239: String httpSessionId) {
240: Collection<RealScriptSession> reply = new ArrayList<RealScriptSession>();
241:
242: Set<String> scriptSessionIds = sessionXRef.get(httpSessionId);
243: for (String scriptSessionId : scriptSessionIds) {
244: DefaultScriptSession scriptSession = sessionMap
245: .get(scriptSessionId);
246: if (scriptSession != null) {
247: reply.add(scriptSession);
248: }
249: }
250: return reply;
251: }
252:
253: /* (non-Javadoc)
254: * @see org.directwebremoting.ScriptSessionManager#getAllScriptSessions()
255: */
256: public Collection<ScriptSession> getAllScriptSessions() {
257: synchronized (sessionLock) {
258: Set<ScriptSession> reply = new HashSet<ScriptSession>();
259: reply.addAll(sessionMap.values());
260: return reply;
261: }
262: }
263:
264: /**
265: * Remove the given session from the list of sessions that we manage, and
266: * leave it for the GC vultures to pluck.
267: * @param scriptSession The session to get rid of
268: */
269: protected void invalidate(RealScriptSession scriptSession) {
270: // Can we think of a reason why we need to sync both together?
271: // It feels like a deadlock risk to do so
272: synchronized (sessionLock) {
273: // Due to the way systems get a number of script sessions for a page
274: // and the perform a number of actions on them, we may get a number
275: // of invalidation checks, and therefore calls to invalidate().
276: // We could protect ourselves from this by having a
277: // 'hasBeenInvalidated' flag, but we're taking the simple option
278: // here of just allowing multiple invalidations.
279: sessionMap.remove(scriptSession.getId());
280:
281: disassociateScriptSessionAndPage(scriptSession);
282: disassociateScriptSessionAndHttpSession(scriptSession);
283: }
284:
285: // Are there any risks from doing this outside the locks?
286: // The initial analysis is that 'Destroyed' is past tense so you would
287: // have expected it to have happened already.
288: fireScriptSessionDestroyedEvent(scriptSession);
289: }
290:
291: /**
292: * If we call {@link #checkTimeouts()} too often is could bog things down so
293: * we only check every one in a while (default 30 secs); this checks to see
294: * of we need to check, and checks if we do.
295: */
296: protected void maybeCheckTimeouts() {
297: long now = System.currentTimeMillis();
298: if (now - scriptSessionCheckTime > lastSessionCheckAt) {
299: checkTimeouts();
300: lastSessionCheckAt = now;
301: }
302: }
303:
304: /**
305: * Do a check on all the known sessions to see if and have timeout and need
306: * removing.
307: */
308: protected void checkTimeouts() {
309: long now = System.currentTimeMillis();
310: List<ScriptSession> timeouts = new ArrayList<ScriptSession>();
311:
312: synchronized (sessionLock) {
313: for (DefaultScriptSession session : sessionMap.values()) {
314: if (session.isInvalidated()) {
315: continue;
316: }
317:
318: long age = now - session.getLastAccessedTime();
319: if (age > scriptSessionTimeout) {
320: timeouts.add(session);
321: }
322: }
323:
324: for (ScriptSession scriptSession : timeouts) {
325: DefaultScriptSession session = (DefaultScriptSession) scriptSession;
326: session.invalidate();
327: }
328: }
329: }
330:
331: /* (non-Javadoc)
332: * @see org.directwebremoting.ScriptSessionManager#getScriptSessionTimeout()
333: */
334: public long getScriptSessionTimeout() {
335: return scriptSessionTimeout;
336: }
337:
338: /* (non-Javadoc)
339: * @see org.directwebremoting.ScriptSessionManager#setScriptSessionTimeout(long)
340: */
341: public void setScriptSessionTimeout(long scriptSessionTimeout) {
342: this .scriptSessionTimeout = scriptSessionTimeout;
343: }
344:
345: /* (non-Javadoc)
346: * @see org.directwebremoting.extend.ScriptSessionManager#addScriptSessionListener(org.directwebremoting.event.ScriptSessionListener)
347: */
348: public void addScriptSessionListener(ScriptSessionListener li) {
349: scriptSessionListeners.add(ScriptSessionListener.class, li);
350: }
351:
352: /* (non-Javadoc)
353: * @see org.directwebremoting.extend.ScriptSessionManager#removeScriptSessionListener(org.directwebremoting.event.ScriptSessionListener)
354: */
355: public void removeScriptSessionListener(ScriptSessionListener li) {
356: scriptSessionListeners.remove(ScriptSessionListener.class, li);
357: }
358:
359: /**
360: * Accessor for the PageNormalizer.
361: * @param pageNormalizer The new PageNormalizer
362: */
363: public void setPageNormalizer(PageNormalizer pageNormalizer) {
364: this .pageNormalizer = pageNormalizer;
365: }
366:
367: /**
368: * @param scriptSessionCheckTime the scriptSessionCheckTime to set
369: */
370: public void setScriptSessionCheckTime(long scriptSessionCheckTime) {
371: this .scriptSessionCheckTime = scriptSessionCheckTime;
372: }
373:
374: /**
375: * This should be called whenever a {@link ScriptSession} is created
376: * @param scriptSession The newly created ScriptSession
377: */
378: protected void fireScriptSessionCreatedEvent(
379: ScriptSession scriptSession) {
380: ScriptSessionEvent ev = new ScriptSessionEvent(scriptSession);
381: Object[] listeners = scriptSessionListeners.getListenerList();
382: for (int i = 0; i < listeners.length - 2; i += 2) {
383: if (listeners[i] == ScriptSessionListener.class) {
384: ((ScriptSessionListener) listeners[i + 1])
385: .sessionCreated(ev);
386: }
387: }
388: }
389:
390: /**
391: * This should be called whenever a {@link ScriptSession} is destroyed
392: * @param scriptSession The newly destroyed ScriptSession
393: */
394: protected void fireScriptSessionDestroyedEvent(
395: ScriptSession scriptSession) {
396: ScriptSessionEvent ev = new ScriptSessionEvent(scriptSession);
397: Object[] listeners = scriptSessionListeners.getListenerList();
398: for (int i = 0; i < listeners.length - 2; i += 2) {
399: if (listeners[i] == ScriptSessionListener.class) {
400: ((ScriptSessionListener) listeners[i + 1])
401: .sessionDestroyed(ev);
402: }
403: }
404: }
405:
406: /**
407: * Use of this attribute is currently discouraged, we may make this public
408: * in a later release. Until then, it may change or be removed without warning.
409: */
410: public static final String ATTRIBUTE_HTTPSESSIONID = "org.directwebremoting.ScriptSession.HttpSessionId";
411:
412: /**
413: * Use of this attribute is currently discouraged, we may make this public
414: * in a later release. Until then, it may change or be removed without warning.
415: */
416: public static final String ATTRIBUTE_PAGE = "org.directwebremoting.ScriptSession.Page";
417:
418: /**
419: * The list of current {@link ScriptSessionListener}s
420: */
421: protected EventListenerList scriptSessionListeners = new EventListenerList();
422:
423: /**
424: * How we create script session ids.
425: */
426: private static IdGenerator generator = new IdGenerator();
427:
428: /**
429: * By default we check for sessions that need expiring every 30 seconds
430: */
431: protected static final long DEFAULT_SESSION_CHECK_TIME = 30000;
432:
433: /**
434: * How we turn pages into the canonical form.
435: */
436: protected PageNormalizer pageNormalizer;
437:
438: /**
439: * How long do we wait before we timeout script sessions?
440: */
441: private long scriptSessionTimeout = DEFAULT_TIMEOUT_MILLIS;
442:
443: /**
444: * How often do we check for script sessions that need timing out
445: */
446: protected long scriptSessionCheckTime = DEFAULT_SESSION_CHECK_TIME;
447:
448: /**
449: * We check for sessions that need timing out every
450: * {@link #scriptSessionCheckTime}; this is when we last checked.
451: */
452: protected long lastSessionCheckAt = System.currentTimeMillis();
453:
454: /**
455: * What we synchronize against when we want to access either sessionMap or
456: * pageSessionMap
457: */
458: protected final Object sessionLock = new Object();
459:
460: /**
461: * Allows us to associate script sessions with http sessions.
462: * The key is an http session id, the
463: * <p>GuardedBy("sessionLock")
464: */
465: private Map<String, Set<String>> sessionXRef = new HashMap<String, Set<String>>();
466:
467: /**
468: * The map of all the known sessions.
469: * The key is the script session id, the value is the session data
470: * <p>GuardedBy("sessionLock")
471: */
472: private Map<String, DefaultScriptSession> sessionMap = new HashMap<String, DefaultScriptSession>();
473:
474: /**
475: * The map of pages that have sessions.
476: * The key is a normalized page, the value the script sessions that are
477: * known to be currently visiting the page
478: * <p>GuardedBy("sessionLock")
479: */
480: private Map<String, Set<RealScriptSession>> pageSessionMap = new HashMap<String, Set<RealScriptSession>>();
481:
482: /**
483: * The log stream
484: */
485: private static final Log log = LogFactory
486: .getLog(DefaultScriptSessionManager.class);
487: }
|