/*==========================================================================*\ | $Id: Session.java,v 1.10 2012/02/28 17:36:11 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2012 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT 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. See the | GNU General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.core; import org.apache.log4j.Logger; import org.webcat.core.messaging.UnexpectedExceptionMessage; import org.webcat.woextensions.WCEC; import org.webcat.woextensions.WCEC.PeerManager; import org.webcat.woextensions.WCEC.PeerManagerPool; import com.webobjects.appserver.WOComponent; import com.webobjects.eocontrol.EOEditingContext; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSBundle; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSTimeZone; import com.webobjects.foundation.NSTimestamp; // ------------------------------------------------------------------------- /** * The current user session. * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.10 $, $Date: 2012/02/28 17:36:11 $ */ public class Session extends er.extensions.appserver.ERXSession { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Creates a new Session object. */ public Session() { super(); initSession(); } // ---------------------------------------------------------- /** * Creates a new Session object. * @param sessionID The ID to use for this session */ public Session(String sessionID) { super(); _setSessionID(sessionID); initSession(); } // ---------------------------------------------------------- /** * Common initialization helper method used by all constructors. */ private final void initSession() { log.debug("creating " + sessionID()); setStoresIDsInCookies(true); setStoresIDsInURLs(false); defaultEditingContext().setUndoManager(null); // defaultEditingContext().setSharedEditingContext(null); tabs.mergeClonedChildren(subsystemTabTemplate); tabs.selectDefault(); childManagerPool = new WCEC.PeerManagerPool(); } //~ KVC Attributes (must be public) ....................................... public TabDescriptor tabs = new TabDescriptor("TBDPage", "root"); //~ Methods ............................................................... // ---------------------------------------------------------- /** * Determine whether the user is currently logged in. * * @return True if the user is logged in */ public boolean isLoggedIn() { return primeUser != null; } // ---------------------------------------------------------- /** * Set the user's identify when he or she first logs in. * * Returns the appropriate login <code>Session</code> object. * Note that this may return a <code>Session</code> other than the * recipient of this message, in which case the user has * another session open, which they must go to. * * @param u The user loggin in * @return The Session ID to use for this user */ public synchronized String setUser(User u) { log.debug("setUser(" + u.userName() + ")"); primeUser = u; localUser = u; log.debug("setUser: userPreferences = " + (primeUser == null ? null : primeUser.preferences())); Application.wcApplication().subsystemManager() .initializeSessionData(this); if (!properties().booleanForKey("core.suppressAccessControl")) { tabs.filterByAccessLevel(u.accessLevel()); } tabs.selectDefault(); if (!doNotUseLoginSession) { EOEditingContext ec = WCEC.newEditingContext(); loginSession = null; loginSessionId = null; try { ec.lock(); loginSession = LoginSession.getLoginSessionForUser(ec, user()); if (loginSession != null) { NSTimestamp now = new NSTimestamp(); if (loginSession.expirationTime().after(now)) { loginSessionId = loginSession.sessionId(); return loginSessionId; } // otherwise ... fall through to default case } } finally { ec.unlock(); } if (loginSession == null) { ec.dispose(); } updateLoginSession(); } if (loginSession != null) { loginSessionId = this.sessionID(); } return this.sessionID(); } // ---------------------------------------------------------- /** * Returns the current user, or null if one is not logged in. * This object lives in the session's local/child editing context. * @return The current user */ public User user() { return localUser; } // ---------------------------------------------------------- /** * Returns the current user, or null if one is not logged in. * This object lives in the session's local/child editing context. * @return The current user */ public User localUser() { return localUser; } // ---------------------------------------------------------- /** * Returns the current user, or null if one is not logged in. * This object lives in the session's default editing context. * @return The current user */ public User primeUser() { return primeUser; } // ---------------------------------------------------------- /** * Determine if we are operating as a different user (e.g., impersonating * a student). * @return True if the localUser is not the primeUser */ public boolean impersonatingAnotherUser() { return localUser != primeUser; } // ---------------------------------------------------------- /** * Find out if this session is memo-ized for later login reuse via * a LoginSession object. * @return True if this session will be shared among all logins */ public boolean useLoginSession() { return !doNotUseLoginSession; } // ---------------------------------------------------------- /** * Set whether this session is memo-ized for later login reuse via * a LoginSession object. * @param value If true, this session will be shared among all logins */ public void setUseLoginSession(boolean value) { doNotUseLoginSession = !value; } // ---------------------------------------------------------- /** * Refresh the stored information about the current login session * in the database. * * This updates the stored timeout for this session. */ private synchronized void updateLoginSession() { if (doNotUseLoginSession) { return; } log.debug("updateLoginSession()"); if (primeUser == null) { return; } if (loginSession != null && loginSession.editingContext() == null) { loginSession = null; } if (loginSession == null) { EOEditingContext ec = WCEC.newEditingContext(); try { ec.lock(); User loginUser = primeUser.localInstance(ec); loginSession = LoginSession.getLoginSessionForUser(ec, loginUser); if (loginSession == null) { loginSession = LoginSession.create(ec, loginUser, sessionID()); loginSessionId = sessionID(); } } finally { ec.unlock(); } if (loginSession == null) { ec.dispose(); log.error("updateLoginSession() cannot find login session for " + "user " + primeUser + " and sessionId = " + sessionID()); return; } } if (loginSession != null) { try { loginSession.editingContext().lock(); loginSession.setExpirationTime( (new NSTimestamp()).timestampByAddingGregorianUnits( 0, 0, 0, 0, 0, (int)timeOut())); try { loginSession.usagePeriod().updateEndTime(); } catch (Exception e) { log.warn("Exception updating usage period for " + primeUser + " (normally this is due to an external " + "change to\nusage periods, in which case the " + "problem will be auto-corrected now)", e); // Assume exception was due to a deleted/missing period // Attempt to create/retrieve new one loginSession.setUsagePeriod(UsagePeriod .currentUsagePeriodForUser( loginSession.editingContext(), loginSession.user())); } try { log.debug("attempting to save"); loginSession.editingContext().saveChanges(); log.debug("saving complete"); } catch (Exception e) { new UnexpectedExceptionMessage(e, context(), null, null) .send(); EOEditingContext ec = loginSession.editingContext(); loginSession = null; ec.revert(); ec.invalidateAllObjects(); ec.unlock(); } } finally { if (loginSession != null && loginSession.editingContext() != null) { loginSession.editingContext().unlock(); } } } } // ---------------------------------------------------------- /** * Called when request-response loop is done. Saves the current timeout * to the loginsession database. */ public void sleep() { log.debug("sleep()"); super.sleep(); updateLoginSession(); if (loginSession != null && loginSessionId != null && !loginSessionId.equals(sessionID())) { log.error("Error: sleep()'ing with multiple sessions active for " + "user: " + (user() == null ? "<null>" : user().name())); } } // ---------------------------------------------------------- /** * Returns true is access controls are currently disabled. * This is a convenience function that allows the * core.suppressAccessControl property to be accessible * from the DirectToWeb rule engine. The engine cannot access * it directly through the <code>properties</code> method, since * the property has a dot in its name. Regular web-cat client * code should use this expression instead: * <code>properties().getBoolean("core.suppressAccessControl")</code>. * * @return 1 if access controls are being suppressed, 0 otherwise */ public Number suppressAccessControl() { return properties().booleanForKey("core.suppressAccessControl") ? one : zero; } // ---------------------------------------------------------- /** * Returns the name of the page the session is currently viewing. * * @return The page name */ public String currentPageName() { return tabs.selectedPageName(); } // ---------------------------------------------------------- /** * Access the application's property settings. This is a * convenience function that allows properties to be accessible * from the DirectToWeb rule engine. * @return The application's property settings */ public WCProperties properties() { return Application.configurationProperties(); } // ---------------------------------------------------------- /** * Terminate this session. */ public void terminate() { if (log.isDebugEnabled()) { log.debug("terminating session " + sessionID()); log.debug("from here:", new Exception("terminate() called")); } if (primeUser != null) { log.info("session timeout: " + (primeUser == null ? "null" : primeUser.userName())); userLogout(); } else { try { super.terminate(); } catch (Exception e) { new UnexpectedExceptionMessage(e, context(), null, null) .send(); } } } // ---------------------------------------------------------- @Override public void savePageInPermanentCache(WOComponent page) { if (page instanceof WCComponent) { ((WCComponent)page).willCachePermanently(); } super.savePageInPermanentCache(page); } // ---------------------------------------------------------- /** * Set the user to null and erase the login session info from * the database. */ public synchronized void userLogout() { Application.userCount--; log.info("user logout: " + (primeUser == null ? "null" : primeUser.userName()) + " (now " + Application.userCount + " users)"); try { if (loginSession != null && loginSessionId != null && sessionID().equals(loginSessionId)) { EOEditingContext lockContext = loginSession.editingContext(); try { lockContext.lock(); log.debug("deleting login session " + loginSessionId); loginSession.usagePeriod().updateEndTime(); loginSession.usagePeriod().setIsLoggedOut(true); loginSession.editingContext().deleteObject(loginSession); loginSession.editingContext().saveChanges(); } catch (Exception e) { new UnexpectedExceptionMessage(e, context(), null, null) .send(); EOEditingContext ec = WCEC.newEditingContext(); try { ec.lock(); User u = primeUser.localInstance(ec); NSArray<LoginSession> items = LoginSession.objectsMatchingQualifier(ec, LoginSession.user.is(u)); if (items != null && items.count() >= 1) { LoginSession ls = items.objectAtIndex(0); ls.usagePeriod().updateEndTime(); ls.usagePeriod().setIsLoggedOut(true); ec.deleteObject(ls); } try { ec.saveChanges(); } catch (Exception e2) { new UnexpectedExceptionMessage( e2, context(), null, null).send(); } } finally { ec.unlock(); ec.dispose(); } } finally { lockContext.unlock(); } } } catch (Exception e) { new UnexpectedExceptionMessage(e, context(), null, null) .send(); } primeUser = null; localUser = null; if (transientState != null) { NSArray<Object> values = transientState.allValues(); for (int i = 0; i < values.count(); i++) { Object value = values.objectAtIndex(i); if (value instanceof EOManager.ECManager) { ((EOManager.ECManager)value).dispose(); } else if (value instanceof EOEditingContext) { ((EOEditingContext)value).dispose(); } else if (value instanceof WCEC.PeerManager) { ((PeerManager)value).dispose(); } else if (value instanceof WCEC.PeerManagerPool) { ((PeerManagerPool)value).dispose(); } } transientState = null; } terminate(); } // ---------------------------------------------------------- /** * Access this session's child editing context for storing multi-page * changes. * @return The child editing context */ public EOEditingContext sessionContext() { return defaultEditingContext(); // childContext; } // ---------------------------------------------------------- /** * Create a new child editing context within the session's default * context. * @return The child editing context, encapsulated in a manager wrapper */ public WCEC.PeerManager createManagedPeerEditingContext() { return new WCEC.PeerManager(childManagerPool); } // ---------------------------------------------------------- /** * Save all child context changes to the default editing context, then * commit them to the database. */ public void commitSessionChanges() { log.debug("commitLocalChanges()"); temporaryTheme = null; defaultEditingContext().saveChanges(); defaultEditingContext().revert(); defaultEditingContext().refaultAllObjects(); } // ---------------------------------------------------------- /** * Cancel all local changes and revert to the default editing context * state. */ public void cancelSessionChanges() { temporaryTheme = null; defaultEditingContext().revert(); defaultEditingContext().refaultAllObjects(); } // ---------------------------------------------------------- /** * Change the local user, to support impersonation of students by * administrators and instructors. * @param u the new user to impersonate */ public void setLocalUser(User u) { localUser = u; // (User)EOUtilities.localInstanceOfObject( childContext, u ); // setCoreSelectionsForLocalUser(); } // ---------------------------------------------------------- /** * Undo the effects of #setLocalUser(User) and revert back to * single-user mode. */ public void clearLocalUser() { localUser = primeUser; // (User)EOUtilities.localInstanceOfObject( // childContext, primeUser ); // setCoreSelectionsForLocalUser(); } // ---------------------------------------------------------- /** * Toggle the student view setting for this user, resetting tabs * as appropriate. This method uses {@link User#toggleStudentView()} * to toggle the user state, and then resets the session's tab navigation * as appropriate. It is intended to be called from within a page, * such as in {@link PageWithNavigation#toggleStudentView()}. */ public void toggleStudentView() { user().toggleStudentView(); if (user().restrictToStudentView()) { TabDescriptor td = tabs; while (td != null && td.selectedChild() != null) { if (td.selectedChild().accessLevel() > 0) { td.selectDefault(); break; } td = td.selectedChild(); } } } // ---------------------------------------------------------- /** * Get the user's preferred timestamp formatter. This method generates * and caches a formatter from the user's preferences on first access. * Later accesses use the cached value. If the user's time formatting * preferences change, use {@link #clearCachedTimeFormatter()}. * @return a formatter */ @SuppressWarnings("deprecation") public com.webobjects.foundation.NSTimestampFormatter timeFormatter() { if (timeFormatter == null) { String formatString = user().dateFormat() + " " + user().timeFormat(); timeFormatter = new com.webobjects.foundation.NSTimestampFormatter( formatString); NSTimeZone zone = NSTimeZone.timeZoneWithName( user().timeZoneName(), true); timeFormatter.setDefaultFormatTimeZone(zone); timeFormatter.setDefaultParseTimeZone(zone); if (log.isDebugEnabled()) { log.debug("timeFormatter(): format = " + formatString); log.debug("timeFormatter(): zone = " + zone); } } return timeFormatter; } // ---------------------------------------------------------- /** * Get the theme to use for this session. * @return The Theme to use */ public Theme theme() { if (temporaryTheme != null) { return temporaryTheme; } return (user() == null || user().theme() == null) ? Theme.defaultTheme() : user().theme(); } // ---------------------------------------------------------- public void setTemporaryTheme(Theme theme) { temporaryTheme = theme; } // ---------------------------------------------------------- /** * Clear the cached timestamp formatter in this session so that a fresh * one will be created from user preferences the next time one is needed. */ public void clearCachedTimeFormatter() { log.debug("clearCachedTimeFormatter()"); timeFormatter = null; } // ---------------------------------------------------------- /** * Retrieve an NSMutableDictionary used to hold transient settings for * this session (data that is not database-backed). * @return A map of transient settings */ public NSMutableDictionary<String, Object> transientState() { if (transientState == null) { transientState = new NSMutableDictionary<String, Object>(); } return transientState; } //~ Instance/static variables ............................................. private User primeUser = null; private User localUser = null; private LoginSession loginSession = null; private String loginSessionId = null; private NSMutableDictionary<String, Object> transientState; private WCEC.PeerManagerPool childManagerPool; private boolean doNotUseLoginSession = false; private Theme temporaryTheme; @SuppressWarnings("deprecation") private com.webobjects.foundation.NSTimestampFormatter timeFormatter = null; private static final Integer zero = new Integer( 0 ); private static final Integer one = new Integer( 1 ); private static NSArray<TabDescriptor> subsystemTabTemplate; { NSBundle myBundle = NSBundle.bundleForClass(Session.class); subsystemTabTemplate = TabDescriptor.tabsFromPropertyList( new NSData (myBundle.bytesForResourcePath( TabDescriptor.TAB_DEFINITIONS))); } static Logger log = Logger.getLogger(Session.class); }