/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/presence/trunk/presence-impl/impl/src/java/org/sakaiproject/presence/impl/BasePresenceService.java $ * $Id: BasePresenceService.java 105079 2012-02-24 23:08:11Z ottenhoff@longsight.com $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008, 2009 The Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.presence.impl; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.Vector; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.api.privacy.PrivacyManager; import org.sakaiproject.component.cover.ComponentManager; import org.sakaiproject.courier.api.PresenceUpdater; import org.sakaiproject.entity.api.Entity; import org.sakaiproject.event.api.Event; import org.sakaiproject.event.api.EventTrackingService; import org.sakaiproject.event.api.Notification; import org.sakaiproject.event.api.NotificationAction; import org.sakaiproject.event.api.NotificationEdit; import org.sakaiproject.event.api.NotificationService; import org.sakaiproject.event.api.UsageSession; import org.sakaiproject.event.api.UsageSessionService; import org.sakaiproject.presence.api.PresenceService; import org.sakaiproject.tool.api.Session; import org.sakaiproject.tool.api.SessionBindingEvent; import org.sakaiproject.tool.api.SessionBindingListener; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.tool.api.ToolSession; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserDirectoryService; import org.w3c.dom.Element; /** * <p> * Implements the PresenceService, all but a Storage model. * </p> */ public abstract class BasePresenceService implements PresenceService, PresenceUpdater { /** Our log (commons). */ private static Log M_log = LogFactory.getLog(BasePresenceService.class); /** SessionState key. */ protected final static String SESSION_KEY = "sakai.presence.service"; /** Storage. */ protected Storage m_storage = null; /** * Allocate a new storage object. * * @return A new storage object. */ protected abstract Storage newStorage(); /** The maintenance. */ protected Maintenance m_maintenance = null; /********************************************************************************************************************************************************************************************************************************************************** * Constructors, Dependencies and their setter methods *********************************************************************************************************************************************************************************************************************************************************/ /** Dependency: SessionManager */ protected SessionManager m_sessionManager = null; /** * Dependency: SessionManager. * * @param service * The SessionManager. */ public void setSessionManager(SessionManager service) { m_sessionManager = service; } /** Dependency: UsageSessionService */ protected UsageSessionService m_usageSessionService = null; /** * Dependency: UsageSessionService. * * @param service * The UsageSessionService. */ public void setUsageSessionService(UsageSessionService service) { m_usageSessionService = service; } /** Dependency: UserDirectoryService */ protected UserDirectoryService m_userDirectoryService = null; /** * Dependency: UserDirectoryService. * * @param service * The UserDirectoryService. */ public void setUserDirectoryService(UserDirectoryService service) { m_userDirectoryService = service; } /** Dependency: EventTrackingService */ protected EventTrackingService m_eventTrackingService = null; /** * Dependency: EventTrackingService. * * @param service * The EventTrackingService. */ public void setEventTrackingService(EventTrackingService service) { m_eventTrackingService = service; } /** Dependency: PrivacyManager */ protected PrivacyManager m_privacyManager = null; /** * Dependency: PrivacyManager. * * @param service * The PrivacyManager. */ public void setPrivacyManager(PrivacyManager service) { m_privacyManager = service; } /** Configuration: default value in seconds till a non-refreshed presence entry times out. */ protected int m_timeout = 60; private NotificationEdit notification; private NotificationService notificationService; /** * Configuration: SECONDS till a non-refreshed presence entry times out. * * @param value * timeout seconds. */ public void setTimeoutSeconds(String value) { try { m_timeout = Integer.parseInt(value); } catch (Exception ignore) { } } /********************************************************************************************************************************************************************************************************************************************************** * Init and Destroy *********************************************************************************************************************************************************************************************************************************************************/ /** * Final initialization, once all dependencies are set. */ public void init() { try { // storage m_storage = newStorage(); // start the maintenance thread m_maintenance = new Maintenance(); m_maintenance.start(); M_log.info("init()"); // register a transient notification for resources notification = notificationService.addTransientNotification(); // add all the functions that are registered to trigger search index // modification notification.setFunction(UsageSessionService.EVENT_LOGOUT); // set the action notification.setAction(new NotificationAction(){ public NotificationAction getClone() { return null; } public void notify(Notification notification, Event event) { removeSessionPresence(event.getSessionId()); } public void set(Element el) { } public void set(NotificationAction other) { } public void toXml(Element el) { } }); } catch (Exception t) { M_log.warn("init(): ", t); } } /** * Returns to uninitialized state. */ public void destroy() { m_storage = null; M_log.info("destroy()"); } /********************************************************************************************************************************************************************************************************************************************************** * PresenceService implementation *********************************************************************************************************************************************************************************************************************************************************/ /** * {@inheritDoc} */ public String presenceReference(String id) { return REFERENCE_ROOT + Entity.SEPARATOR + id; } // presenceReference /** * {@inheritDoc} */ protected String presenceId(String ref) { String start = presenceReference(""); int i = ref.indexOf(start); if (i == -1) return ref; String id = ref.substring(i + start.length()); return id; } // presenceId /** * {@inheritDoc} */ public void setPresence(String locationId) { setPresence(locationId, m_timeout); } /** * {@inheritDoc} */ public void setPresence(String locationId, int timeout) { if (locationId == null) return; if (!checkPresence(locationId, true)) { // presence relates a usage session (the current one) with a location UsageSession curSession = m_usageSessionService.getSession(); if (curSession == null) return; // update the storage m_storage.setPresence(curSession.getId(), locationId); // generate the event Event event = m_eventTrackingService.newEvent(EVENT_PRESENCE, presenceReference(locationId), true); m_eventTrackingService.post(event, curSession); // create a presence for tracking // bind a presence tracking object to the sakai session for auto-cleanup when logout or inactivity invalidates the sakai session Session session = m_sessionManager.getCurrentSession(); ToolSession ts = session.getToolSession(SESSION_KEY); Presence p = new Presence(curSession, locationId, timeout); ts.setAttribute(locationId, p); } // retire any expired presence checkPresenceForExpiration(); } // setPresence /** * {@inheritDoc} */ public void removePresence(String locationId) { if (locationId == null) return; if (checkPresence(locationId, false)) { UsageSession curSession = m_usageSessionService.getSession(); // tell maintenance m_storage.removePresence(curSession.getId(), locationId); // generate the event Event event = m_eventTrackingService.newEvent(EVENT_ABSENCE, presenceReference(locationId), true); m_eventTrackingService.post(event, curSession); // remove from state Session session = m_sessionManager.getCurrentSession(); ToolSession ts = session.getToolSession(SESSION_KEY); Presence p = (Presence) ts.getAttribute(locationId); if (p != null) { p.deactivate(); ts.removeAttribute(locationId); } } } // removePresence /** * {@inheritDoc} */ public void removeSessionPresence(String sessionId) { List<String> presence = m_storage.removeSessionPresence(sessionId); // get the session UsageSession session = m_usageSessionService.getSession(sessionId); // send presence end events for these for (String locationId : presence) { Event event = m_eventTrackingService.newEvent(PresenceService.EVENT_ABSENCE, presenceReference(locationId), true); m_eventTrackingService.post(event, session); } } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") public List<UsageSession> getPresence(String locationId) { // get the sessions at this location List<UsageSession> sessions = m_storage.getSessions(locationId); // sort Collections.sort(sessions); return sessions; } // getPresence /** * {@inheritDoc} */ @SuppressWarnings("unchecked") public List<User> getPresentUsers(String locationId) { // get the sessions List<UsageSession> sessions = m_storage.getSessions(locationId); // form a list of user ids List<String> userIds = new Vector<String>(); for (UsageSession s : sessions) { if (!userIds.contains(s.getUserId())) { userIds.add(s.getUserId()); } } // get the users for these ids List<User> users = m_userDirectoryService.getUsers(userIds); // sort Collections.sort(users); return users; } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") public List<User> getPresentUsers(String locationId, String siteId) { // get the sessions List<UsageSession> sessions = m_storage.getSessions(locationId); // form a list of user ids List<String> userIds = new Vector<String>(); for (UsageSession s : sessions) { if (!userIds.contains(s.getUserId())) { userIds.add(s.getUserId()); } } Set<String> userIdsSet = m_privacyManager.findViewable("/site/" + siteId, new HashSet(userIds)); // get the users for these ids List<User> users = m_userDirectoryService.getUsers(userIdsSet); // sort Collections.sort(users); return users; } /** * {@inheritDoc} */ public List<String> getLocations() { List<String> locations = m_storage.getLocations(); Collections.sort(locations); return locations; } // getLocations /** * {@inheritDoc} */ public String locationId(String site, String page, String tool) { // TODO: remove return ""; } /** * {@inheritDoc} */ public String getLocationDescription(String location) { // TODO: get a description for a placement! return "location: " + location; } /** * {@inheritDoc} */ public int getTimeout() { return m_timeout; } /** * Check if the current session is present at the location - optionally refreshing it * * @param locationId * The location to check. * @param refresh * If true, refresh the timeout on the presence if found * @return True if the current session is present at that location, false if not. */ protected boolean checkPresence(String locationId, boolean refresh) { Session session = m_sessionManager.getCurrentSession(); ToolSession ts = session.getToolSession(SESSION_KEY); Presence p = (Presence) ts.getAttribute(locationId); if ((p != null) && refresh) { p.setActive(); } return (p != null); } /** * Check current session presences and remove any expired ones */ @SuppressWarnings("unchecked") protected void checkPresenceForExpiration() { Session session = m_sessionManager.getCurrentSession(); ToolSession ts = session.getToolSession(SESSION_KEY); Enumeration locations = ts.getAttributeNames(); while (locations.hasMoreElements()) { String location = (String) locations.nextElement(); Presence p = (Presence) ts.getAttribute(location); if (p != null && p.isExpired()) { ts.removeAttribute(location); } } } /** * Check all session presences and remove any expired ones */ @SuppressWarnings("unchecked") protected void checkAllPresenceForExpiration() { List<Session> sessions = m_sessionManager.getSessions(); for (Iterator<Session> i = sessions.iterator(); i.hasNext();) { Session session = (Session) i.next(); ToolSession ts = session.getToolSession(SESSION_KEY); Enumeration locations = ts.getAttributeNames(); while (locations.hasMoreElements()) { String location = (String) locations.nextElement(); Presence p = (Presence) ts.getAttribute(location); if (M_log.isDebugEnabled()) M_log.debug("checking expiry of session " + session.getId() + " in location " + location); if (p != null && p.isExpired()) { ts.removeAttribute(location); } } } } /********************************************************************************************************************************************************************************************************************************************************** * Storage *********************************************************************************************************************************************************************************************************************************************************/ protected interface Storage { /** * Add this session id's presence at this location, if not already there. * * @param sessionId * The session id. * @param locationId * The location id. */ void setPresence(String sessionId, String locationId); /** * Remove this sessions id's presence at this location. * * @param sessionId * The session id. * @param locationId * The location id. */ void removePresence(String sessionId, String locationId); /** * Remove presence for all locations for this session id. * * @param sessionId * The session id. * @param locationId * The location id. */ List<String> removeSessionPresence(String sessionId); /** * Access the List of UsageSessions present at this location. * * @param locationId * The location id. * @return The List of sessions (UsageSession) present at this location. */ List<UsageSession> getSessions(String locationId); /** * Access the List of all known location ids. * * @return The List (String) of all known locations. */ List<String> getLocations(); } /********************************************************************************************************************************************************************************************************************************************************** * Presence *********************************************************************************************************************************************************************************************************************************************************/ protected class Presence implements SessionBindingListener { /** The session. */ protected UsageSession m_session = null; /** The location id. */ protected String m_locationId = null; /** If true, process the unbound. */ protected boolean m_active = true; /** Time in seconds before expiry. */ protected long m_presence_timeout = 0; /** Timestamp in milliseconds to expire. */ protected long m_expireTime = 0; public Presence(UsageSession session, String locationId, int timeout) { m_session = session; m_locationId = locationId; m_presence_timeout = timeout; m_expireTime = System.currentTimeMillis() + m_presence_timeout * 1000; } public void deactivate() { m_active = false; } /** * Reset the timeout based on current activity */ public void setActive() { m_expireTime = System.currentTimeMillis() + m_presence_timeout * 1000; } /** * Has this presence timed out? * * @return true if expired, false if not. */ public boolean isExpired() { return System.currentTimeMillis() > m_expireTime; } /** * {@inheritDoc} */ public void valueBound(SessionBindingEvent event) { } /** * {@inheritDoc} */ public void valueUnbound(SessionBindingEvent evt) { if (m_active) { m_storage.removePresence(m_session.getId(), m_locationId); // generate the event Event event = m_eventTrackingService.newEvent(EVENT_ABSENCE, presenceReference(m_locationId), true); m_eventTrackingService.post(event, m_session); } } } /** * @return the notificationService */ public NotificationService getNotificationService() { return notificationService; } /** * @param notificationService the notificationService to set */ public void setNotificationService(NotificationService notificationService) { this.notificationService = notificationService; } // Maintenance thread protected class Maintenance implements Runnable { /** My thread running my timeout checker. */ protected Thread m_maintenanceChecker = null; /** Configuration: how often in seconds to check for expired presence */ protected long m_refresh = 15; /** Signal to the timeout checker to stop. */ protected boolean m_maintenanceCheckerStop = false; /** * Construct. */ public Maintenance() { } /** * Start the maintenance thread. */ public void start() { if (m_maintenanceChecker != null) return; m_maintenanceChecker = new Thread(this, "SakaiPresenceService.Maintenance"); m_maintenanceChecker.setDaemon(true); m_maintenanceCheckerStop = false; m_maintenanceChecker.start(); } /** * Stop the maintenance thread */ public void stop() { if (m_maintenanceChecker != null) { m_maintenanceCheckerStop = true; m_maintenanceChecker.interrupt(); try { // wait for it to die m_maintenanceChecker.join(); } catch (InterruptedException ignore) { } m_maintenanceChecker = null; } // Nothing to do } /** * Run the maintenance thread. Every REFRESH seconds, check for expired presence */ public void run() { // wait till things are rolling ComponentManager.waitTillConfigured(); if (M_log.isDebugEnabled()) M_log.debug("run()"); while (!m_maintenanceCheckerStop) { try { if (M_log.isDebugEnabled()) M_log.debug("checking for expired presence"); checkAllPresenceForExpiration(); } catch (Exception e) { //SAK-20847 don't print the stacktrace unless we are in debug mode M_log.warn("Exception checking for expired presence. If the app server is currently stopped you can safely ignore this warning. Under any other circumstances, enable debug logging to see the cause."); if (M_log.isDebugEnabled()) { M_log.debug("The exception is: ", e); } } // cycle every REFRESH seconds if (!m_maintenanceCheckerStop) { try { Thread.sleep(m_refresh * 1000L); } catch (Exception ignore) { } } } if (M_log.isDebugEnabled()) M_log.debug("done"); } } }