/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2000 Blacksmith, Inc. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.web; import java.io.Serializable; import java.util.LinkedList; import jakarta.servlet.http.HttpSession; import net.wotonomy.control.EOEditingContext; import net.wotonomy.control.KeyValueCodingUtilities; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSDate; import net.wotonomy.foundation.NSKeyValueCodingAdditions; import net.wotonomy.foundation.NSKeyValueCodingSupport; import net.wotonomy.foundation.NSMutableArray; import net.wotonomy.foundation.NSMutableDictionary; /** * A pure java implementation of WOSession. * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 905 $ */ public class WOSession implements Serializable, NSKeyValueCodingAdditions { private static final long serialVersionUID = -5060204676319332437L; // NOTE: need to set this when deserialized and on creation transient private HttpSession session; // the current context transient private WOContext context; // the last requested page: an optimization transient private WOComponent currentPage; // the last requested page's context id transient private String currentContextID; // FIXME: transient until ec's implement serializable private transient EOEditingContext defaultEditingContext; private NSMutableDictionary state; private NSMutableDictionary pages; private NSMutableDictionary permanentPages; private NSMutableArray pageStack; private NSMutableArray permanentPageStack; private boolean terminating; // used by WOResourceManager to cache dynamic resources transient NSMutableDictionary dynamicDataCache; public static final String WOSessionDidTimeOutNotification = "WOSessionDidTimeOutNotification"; public static final String WOSessionDidRestoreNotification = "WOSessionDidRestoreNotification"; public static final String WOSessionDidCreateNotification = "WOSessionDidCreateNotification"; /** * Default constructor. This is called implicitly by subclasses in all cases. */ public WOSession() { session = null; state = new NSMutableDictionary<>(); pages = new NSMutableDictionary<>(); permanentPages = new NSMutableDictionary<>(); NSMutableArray.mutableArrayBackedByList(new LinkedList<>()); pageStack = NSMutableArray.mutableArrayBackedByList(new LinkedList<>()); permanentPageStack = NSMutableArray.mutableArrayBackedByList(new LinkedList<>()); defaultEditingContext = null; terminating = false; } /** * Package method to initialize the backing session. */ void setServletSession(HttpSession aSession) { session = aSession; } /** * Package method to set the current context. */ void setContext(WOContext aContext) { context = aContext; } /** * Returns the id of the current session. If no session currently exists, return * null. */ public String sessionID() { if (session != null) { return session.getId(); } return null; } /** * Sets whether distribution is currently enabled. This method is not * implemented by this implementation as the servlet container manages * distribution. */ public void setDistributionEnabled(boolean enabled) { throw new RuntimeException("Not implemented yet."); } /** * Returns whether the session is part of a distributed application. This * implementation always returns false. */ public boolean isDistributionEnabled() { return false; } /** * Sets whether session ids should be stored in cookies. This method is not * implemented in this implementation as the servlet container manages sessions * with cookies. */ public void setStoresIDsInCookies(boolean cookies) { throw new RuntimeException("Not implemented yet."); } /** * Returns whether session ids are currently stored in cookies. This * implementation always returns true. */ public boolean storesIDsInCookies() { return true; } /** * Returns the current expiration date for cookies that store session ids. */ public NSDate expirationDateForIDCookies() { throw new RuntimeException("Not implemented yet."); } /** * Sets whether session ids should be stored in urls. This method is not * implemented in this implementation as the servlet container manages sessions * with cookies. */ public void setStoresIDsInURLs(boolean urls) { throw new RuntimeException("Not implemented yet."); } /** * Returns whether session ids are currently stored in urls. This implementation * always returns false. */ public boolean storesIDsInURLs() { return false; } /** * Returns the current domain for cookies containing session ids. */ public String domainForIDCookies() { throw new RuntimeException("Not implemented yet."); } /** * Terminates this session after the completion of the current response. */ public void terminate() { terminating = true; session.invalidate(); } /** * Returns whether the current session will terminate at the completion of the * current response. */ public boolean isTerminating() { return terminating; } /** * Sets the number of seconds after the last request before the session should * be terminated. */ public void setTimeOut(double timeout) { session.setMaxInactiveInterval((int) timeout); } /** * Returns the number of seconds after the last request before the session * should be terminated. */ public double timeOut() { return session.getMaxInactiveInterval(); } /** * Sets the languages for which this session has been localized, in order of * preference. The application will be responsible for localizing the content * based on the languages found in this array. */ public void setLanguages(NSArray anArray) { throw new RuntimeException("Not implemented yet."); } /** * Returns the languages for which this session has been localized, in order of * preference. The application will be responsible for localizing the content * based on the languages found in this array. */ public NSArray languages() { throw new RuntimeException("Not implemented yet."); } /** * Stores the specified key-value pair in the session. */ public void setObjectForKey(Object anObject, String aKey) { state.setObjectForKey(anObject, aKey); } /** * Returns the session value associated with the specified key. */ public Object objectForKey(String aKey) { return state.objectForKey(aKey); } /** * Removes the session value associated with the specified key. */ public void removeObjectForKey(String aKey) { state.removeObjectForKey(aKey); } /** * Returns the context for the current request. */ public WOContext context() { return context; } /** * Invoked at the beginning of the request-response cycle. Override to perform * any kind of initialization at the start of a request. This implementation * does nothing. */ public void awake() { } /** * Invoked by the Application to extract user-assigned balues and assign them to * attributes. This implementation calls takeValuesFromRequest on the top-level * component. */ public void takeValuesFromRequest(WORequest aRequest, WOContext aContext) { context().component().takeValuesFromRequest(aRequest, aContext); } /** * Invoked by the Application to determine which component is the intended * recipient of the user's action. This implementation calls invokeAction on the * top-level component. */ public WOActionResults invokeAction(WORequest aRequest, WOContext aContext) { return context().component().invokeAction(aRequest, aContext); } /** * Invoked by the Application to generate the content of the response. This * implementation calls appendToResponse on the top-level component. */ public void appendToResponse(WOResponse aResponse, WOContext aContext) { context().component().appendToResponse(aResponse, aContext); } /** * Invoked at the end of the request-response cycle. Override to perform any * kind of clean-up at the end of a request. This implementation does nothing. */ public void sleep() { } /** * Returns a list of pages accessed by this session in order of their access and * named by calling WOComponent.descriptionForResponse. */ public NSArray statistics() { throw new RuntimeException("Not implemented yet."); } /** * Puts this page in the session's page cache using the current context id as * the key. */ public void savePage(WOComponent aComponent) { currentPage = aComponent; currentContextID = context.contextID(); if (pages.objectForKey(currentContextID) == null) { byte[] data = KeyValueCodingUtilities.freeze(aComponent, defaultEditingContext(), aComponent, true); System.out.println("WOSession.savePage: " + currentContextID + " : " + data.length); pages.setObjectForKey(data, currentContextID); pageStack.addObject(currentContextID); if (pageStack.count() > context().application().pageCacheSize()) { String id = pageStack.remove(0).toString(); // removeObjectAtIndex System.out.println("WOSession.savePage: removing from cache: " + id); pages.removeObjectForKey(id); } } // System.out.println( "savePage: " + this + " : " + id + " : " + pages ); } /** * Returns the page in the session's page cache corresponding to the specified * context id. Any special permanent caches are searched before the standard * page cache. */ public WOComponent restorePageForContextID(String anID) { if (anID == null) return null; if (anID.equals(currentContextID)) return currentPage; WOComponent result = null; byte[] data = (byte[]) permanentPages.objectForKey(anID); if (data == null) data = (byte[]) pages.objectForKey(anID); if (data != null) { System.out.println("WOSession.restorePageForContextID: " + anID + " : " + data.length); result = (WOComponent) KeyValueCodingUtilities.thaw(data, defaultEditingContext(), WOApplication.application().getClass().getClassLoader(), true); } // System.out.println( "restorePageForContextID: " + this + " : " + anID + " : " // + result + " : " + pages ); return result; } /** * Puts this page in the special cache is will not get automatically flushed * like the session page cache. Use this if the page is likely to be around for * a while, specifically pages within frames. */ public void savePageInPermanentCache(WOComponent aComponent) { currentPage = aComponent; currentContextID = context.contextID(); if (permanentPages.objectForKey(currentContextID) == null) { byte[] data = KeyValueCodingUtilities.freeze(aComponent, defaultEditingContext(), aComponent, true); // System.out.println( "WOSession.savePageInPermanentCache: " // + currentContextID + " : " + data.length ); permanentPages.setObjectForKey(data, currentContextID); permanentPageStack.addObject(currentContextID); if (permanentPageStack.count() > context().application().pageCacheSize()) { String id = permanentPageStack.remove(0).toString(); // removeObjectAtIndex permanentPages.removeObjectForKey(id); } } } /** * Writes a message to the standard error stream. */ public static void logString(String aString) { System.err.println(aString); } /** * Writes a message to the standard error stream if debugging is activated. */ public static void debugString(String aString) { // TODO: Check to see if debugging is enabled. System.err.println(aString); } /** * Returns the default editing context used by this session. Defaults to null. */ public EOEditingContext defaultEditingContext() { return defaultEditingContext; } /** * Sets the default editing context used by this session. */ public void setDefaultEditingContext(EOEditingContext aContext) { defaultEditingContext = aContext; } // interface NSKeyValueCodingAdditions @Override public Object valueForKeyPath(String aPath) { // currently key value coding support also handles keypaths return valueForKey(aPath); } @Override public void takeValueForKeyPath(Object aValue, String aPath) { // currently key value coding support also handles keypaths takeValueForKey(aValue, aPath); } @Override public Object valueForKey(String aKey) { // System.out.println( "valueForKey: " + aKey + "->" + this ); Object result = objectForKey(aKey); if (result == null) result = NSKeyValueCodingSupport.valueForKey(this, aKey); return result; } @Override public void takeValueForKey(Object aValue, String aKey) { // System.out.println( "takeValueForKey: " + aKey + " : " // + aValue + "->" + this ); setObjectForKey(aValue, aKey); } @Override public Object storedValueForKey(String aKey) { Object result = objectForKey(aKey); if (result == null) NSKeyValueCodingSupport.storedValueForKey(this, aKey); return result; } @Override public void takeStoredValueForKey(Object aValue, String aKey) { setObjectForKey(aValue, aKey); } @Override public Object handleQueryWithUnboundKey(String aKey) { return NSKeyValueCodingSupport.handleQueryWithUnboundKey(this, aKey); } @Override public void handleTakeValueForUnboundKey(Object aValue, String aKey) { NSKeyValueCodingSupport.handleTakeValueForUnboundKey(this, aValue, aKey); } @Override public void unableToSetNullForKey(String aKey) { NSKeyValueCodingSupport.unableToSetNullForKey(this, aKey); } public Object validateTakeValueForKeyPath(Object aValue, String aKey) { throw new RuntimeException("Not implemented yet."); } }