edu.cmu.tetradapp.session.Session Maven / Gradle / Ivy
///////////////////////////////////////////////////////////////////////////////
// For information as to what this class does, see the Javadoc, below. //
// Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, //
// 2007, 2008, 2009, 2010, 2014, 2015, 2022 by Peter Spirtes, Richard //
// Scheines, Joseph Ramsey, and Clark Glymour. //
// //
// This program is free software; you can redistribute it and/or modify //
// it under the terms of the GNU General Public License as published by //
// the Free Software Foundation; either version 2 of the License, or //
// (at your option) any later version. //
// //
// This program 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 General Public License //
// along with this program; if not, write to the Free Software //
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //
///////////////////////////////////////////////////////////////////////////////
package edu.cmu.tetradapp.session;
import edu.cmu.tetrad.util.TetradSerializable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serial;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* Stores a directed graph over models of specific types, where the possible
* parent relationships between the models are given in the constructors of the model classes themselves. For instance,
* if a class Model1 has only this constructor:> 0 public Model1(Model2 x, Model3 y)...
*
then if a SessionNode is constructed with Model.class as argument, it will
* configure itself as a SessionNode requiring two parents, one capable of implementing models of type Model2 and a
* second capable of implementing models of type Model3. If SessionNodes capable of implementing models of types Model2
* and Model3 are available as parents of Model1, a new object of type Model1 can be constructed using them. If Model1
* has more than one constructor, then there is more than one possible set of parents that can be used to construct it.
* SessionNodes can also support more than one possible type of model. A SessionNode, for instance, can support the
* construction of graphs in general, even if different graphs are implemented using different classes. The SessionNode
* can keep track of what its parents are and therefore which of its possible models it's capable of constructing.
*
The Session itself keeps track of which nodes are in the session and manages adding and removing nodes. Nodes
* that are added to the session must be freshly constructed. This constraint eliminates a number of problems that might
* otherwise exist if interconnected SessionNodes were permitted to participate in more than one Session. If the addNode
* method is called with a node that is not in the freshly constructed state (either because it was actually just
* constructed or because the reset
method was just called on the node), an IllegalArgumentException is
* thrown.
When a node is removed from a session, all of its connections to other objects are eliminated and its
* models destroyed. This has consequences for other objects, since destroying the model of a session node may result in
* the destruction of models downstream and the elimination of parent/child relationships between nodes is mutual.>
* 0
*
The Session organizes events coming from the nodes in the session so that a listener to the Session receives all
* events from the Session. This is convenience service so that listeners do not need to pay attention to all of the
* different nodes in the session individually. See SessionEvent
for the types of events that are sent.>
* 0
*
It is intended for the Session to be serializable. For the Session and
* SessionNode classes, this can be checked directly in unit tests. For the various models that the Session can
* construct, this has to be tested separately.> 0
*
* @author josephramsey
* @version $Id: $Id
* @see SessionNode
* @see SessionListener
* @see SessionAdapter
* @see SessionEvent
*/
public final class Session implements TetradSerializable {
private static final long serialVersionUID = 23L;
/**
* The session nodes, stored as a Set of nodes.
*
* @serial Can't be null.
*/
private final List nodes = new LinkedList<>();
/**
* The name of the session.
*
* @serial Can't be null.
*/
private String name;
/**
* Notes when the model has changed. Should be false at time of deserialization.
*/
private transient boolean sessionChanged = true;
/**
* True iff the session is new. Should be false at time of deserialization.
*/
private transient boolean newSession = true;
/**
* Convenience class for firing SessionEvents.
*/
private transient SessionSupport sessionSupport;
/**
* Handles incoming session events, basically by redirecting to any listeners of this session.
*/
private transient SessionHandler sessionHandler;
//===========================CONSTRUCTORS=============================//
/**
* Constructs a new session with the given name. (The name cannot be null.)
*
* @param name a {@link java.lang.String} object
*/
public Session(String name) {
setName(name);
}
/**
* Generates a simple exemplar of this class to test serialization.
*
* @return a {@link Session} object
*/
public static Session serializableInstance() {
return new Session("X");
}
//===========================PUBLIC METHODS===========================//
/**
* Gets the name.
*
* @return a {@link java.lang.String} object
*/
public String getName() {
return this.name;
}
/**
* Sets the name.
*
* @param name a {@link java.lang.String} object
*/
public void setName(String name) {
if (name == null) {
throw new NullPointerException("Name must not be null.");
}
this.name = name;
}
/**
* Adds the given node to the session, provided the node is in a freshly
* created state.> 0
*
* @param node a {@link SessionNode} object
* @throws java.lang.NullPointerException if the node is null.
* @throws java.lang.IllegalArgumentException if the node is not in a freshly created state. There are two ways to
* put a SessionNode into a freshly created state. One is to freshly
* create it, using one of the constructors. The other was is to call
* the
* reset
method on the SessionNode.
* @see SessionNode#isFreshlyCreated
* @see SessionNode#resetToFreshlyCreated
*/
public void addNode(SessionNode node) {
if (node == null) {
throw new NullPointerException("Node cannot be null.");
}
if (!node.isFreshlyCreated()) {
throw new IllegalArgumentException("Node must be freshly " +
"created in order to be added to a session: " + node);
}
// Causing templates not to work sometimes. Unnecessary. jdramsey 6/5/2015
this.nodes.add(node);
node.addSessionListener(getSessionHandler());
getSessionSupport().fireNodeAdded(node);
}
/**
* Adds a list of nodes to the session. Each item in the list must be a SessionNode, and none of them may have a
* name that already exists in the session. Upon being added to the session, if any node has a parent that is not in
* the list, the parent is removed, the node's model is destroyed, and any models downstream are destroyed as well.
* Any children not in the list are removed. Also, any listeners that are not SessionNodes are removed from each
* node.
*
* @param nodes a {@link java.util.List} object
*/
public void addNodeList(List nodes) {
if (nodes == null) {
throw new NullPointerException(
"The list of nodes must not be " + "null.");
}
// Verify that all of the nodes are SessionNodes.
for (int i = 0; i < nodes.size(); i++) {
if (nodes.get(i) == null) {
throw new IllegalArgumentException(
"The object at index " + i + " is null.");
}
}
// Check to make sure none of the nodes has a name already in the
// session.
for (SessionNode node : nodes) {
if (existsNodeByName(node.getDisplayName())) {
throw new IllegalArgumentException(
"Attempt to add node to the " +
"session with duplicate " + "name: " +
node.getDisplayName());
}
}
// Add the nodes, removing any parents or children not in the list.
for (int i = 0; i < nodes.size(); i++) {
SessionNode node = nodes.get(i);
node.restrictConnectionsToList(nodes);
node.restrictListenersToSessionNodes();
this.nodes.add(node);
node.addSessionListener(getSessionHandler());
getSessionSupport().fireNodeAdded(node);
}
}
/**
* Removes the given node from the session, removing any connectivity the
* node might have to other objects.> 0
*
* @param node the SessionNode to be removed.
* @throws java.lang.IllegalArgumentException if the specified node is not in the session.
* @see SessionNode#resetToFreshlyCreated
*/
public void removeNode(SessionNode node) {
if (this.nodes.contains(node)) {
node.resetToFreshlyCreated();
this.nodes.remove(node);
node.removeSessionListener(getSessionHandler());
getSessionSupport().fireNodeRemoved(node);
} else {
throw new IllegalArgumentException(
"Node doesn't exist in" + "graph: " + node);
}
}
/**
*
Getter for the field nodes
.
*
* @return the getModel set of session nodes.
*/
public Set getNodes() {
return new HashSet<>(this.nodes);
}
/**
* Removes all nodes.
*/
public void clearNodes() {
// Use the removeNode method to make sure events are fired.
Set _nodes = new HashSet<>(this.nodes);
for (SessionNode node : _nodes) {
removeNode(node);
}
}
/**
* contains.
*
* @param node a {@link SessionNode} object
* @return a boolean
*/
public boolean contains(SessionNode node) {
return this.nodes.contains(node);
}
/**
* Adds a session listener.
*
* @param l a {@link SessionListener} object
*/
public void addSessionListener(SessionListener l) {
getSessionSupport().addSessionListener(l);
}
/**
* Removes a session listener.
*
* @param l a {@link SessionListener} object
*/
public void removeSessionListener(SessionListener l) {
getSessionSupport().removeSessionListener(l);
}
//=====================PACKAGE PROTECTED METHODS=====================//
/**
* Indirect reference to session support to avoid saving out any listeners during serialization.
*/
SessionSupport getSessionSupport() {
if (this.sessionSupport == null) {
this.sessionSupport = new SessionSupport(this);
for (SessionNode node : this.nodes) {
node.addSessionListener(getSessionHandler());
}
}
return this.sessionSupport;
}
/**
* Indirect reference to session handler to avoid saving out listeners during serialization.
*/
SessionHandler getSessionHandler() {
if (this.sessionHandler == null) {
this.sessionHandler = new SessionHandler();
}
return this.sessionHandler;
}
//==========================PRIVATE METHODS===========================//
/**
* @return true iff a node exists in the session with the given name.
*/
private boolean existsNodeByName(String name) {
if (name == null) {
return false;
}
for (SessionNode node : getNodes()) {
if (name.equals(node.getDisplayName())) {
return true;
}
}
return false;
}
/**
* isSessionChanged.
*
* @return a boolean
*/
public boolean isSessionChanged() {
return this.sessionChanged;
}
/**
* Setter for the field sessionChanged
.
*
* @param sessionChanged a boolean
*/
public void setSessionChanged(boolean sessionChanged) {
this.sessionChanged = sessionChanged;
}
/**
* isNewSession.
*
* @return a boolean
*/
public boolean isNewSession() {
return this.newSession;
}
/**
* Setter for the field newSession
.
*
* @param newSession a boolean
*/
public void setNewSession(boolean newSession) {
this.newSession = newSession;
}
//=========================== MEMBER CLASSES =========================//
/**
* isEmpty.
*
* @return a boolean
*/
public boolean isEmpty() {
return this.nodes.isEmpty();
}
/**
* Adds semantic checks to the default deserialization method. This method must have the standard signature for a
* readObject method, and the body of the method must begin with "s.defaultReadObject();". Other than that, any
* semantic checks can be specified and do not need to stay the same from version to version. A readObject method of
* this form may be added to any class, even if Tetrad sessions were previously saved out using a version of the
* class that didn't include it. (That's what the "s.defaultReadObject();" is for. See J. Bloch, Effective Java, for
* help.
*
* @param s an {@link java.io.ObjectInputStream} object
* @throws IOException if any.
* @throws ClassNotFoundException if any.
*/
@Serial
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {
s.defaultReadObject();
this.sessionChanged = false;
this.newSession = false;
}
/**
* Handles SessionEvent
s. Hides the handling of these from the API.
*/
private class SessionHandler extends SessionAdapter {
/**
* This method is called when a node is added.
*/
public void nodeAdded(SessionEvent event) {
getSessionSupport().fireSessionEvent(event);
setSessionChanged(true);
}
/**
* This method is called when a node is removed.
*/
public void nodeRemoved(SessionEvent event) {
getSessionSupport().fireSessionEvent(event);
setSessionChanged(true);
}
/**
* This method is called when a parent is added.
*/
public void parentAdded(SessionEvent event) {
getSessionSupport().fireSessionEvent(event);
setSessionChanged(true);
}
/**
* This method is called when a parent is removed.
*/
public void parentRemoved(SessionEvent event) {
getSessionSupport().fireSessionEvent(event);
setSessionChanged(true);
}
/**
* This method is called when a model is created for a node.
*/
public void modelCreated(SessionEvent event) {
getSessionSupport().fireSessionEvent(event);
setSessionChanged(true);
}
/**
* This method is called when a model is destroyed for a node.
*/
public void modelDestroyed(SessionEvent event) {
getSessionSupport().fireSessionEvent(event);
setSessionChanged(true);
}
/**
* Relays addingEdge events up the chain, without changing their source.
*/
public void addingEdge(SessionEvent event) {
getSessionSupport().fireSessionEvent(event, false);
}
}
}