package common;
import java.util.EventListener;
import java.util.LinkedList;
import javax.swing.event.EventListenerList;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import common.beans.AbstractBean;
import expressions.Expression;
/**
* Abstract implementation of the {@link common.ProofModel} interface,
* which is used by prover implementations. The user interface and
* other modules should never use this class (or a derived class)
* directly, as methods are subject to change. Instead the upper layer
* should restrict usage to the {@link common.ProofModel} interface.
*
* @author Benedikt Meurer
* @version $Id$
*
* @see common.AbstractProofNode
* @see common.ProofModel
* @see common.ProofNode
*/
public abstract class AbstractProofModel extends AbstractBean implements ProofModel {
//
// Attributes
//
/**
* Event listeners.
*
* @see #addTreeModelListener(TreeModelListener)
* @see #removeTreeModelListener(TreeModelListener)
*/
protected EventListenerList listenerList = new EventListenerList();
/**
* The root node of the proof tree.
*
* @see #getRoot()
*/
protected AbstractProofNode root;
//
// Constructor
//
/**
* Allocates a new <code>AbstractProofModel</code> using the
* given <code>root</code> item.
*
* @param root the new root item.
*
* @?hrows NullPointerException if <code>root</code> is <code>null</code>.
*/
protected AbstractProofModel(AbstractProofNode root) {
// validate the root node
if (root == null) {
throw new NullPointerException("root is null");
}
// use the root node
this.root = root;
}
//
// Primitives
//
/**
* {@inheritDoc}
*
* @see ProofModel#getRules()
*/
public abstract ProofRule[] getRules();
//
// Actions
//
/**
* {@inheritDoc}
*
* @see ProofModel#guess(ProofNode)
*/
public abstract void guess(ProofNode node) throws ProofGuessException;
/**
* {@inheritDoc}
*
* @see ProofModel#prove(ProofRule, ProofNode)
*/
public abstract void prove(ProofRule rule, ProofNode node) throws ProofRuleException;
/**
* {@inheritDoc}
*
* @see ProofModel#translateToCoreSyntax(ProofNode)
*/
public void translateToCoreSyntax(ProofNode node) {
// verify that the node is valid for the model
if (!getRoot().isNodeRelated(node)) {
throw new IllegalArgumentException("node is invalid");
}
// verify that no actions were performed on the node
if (node.getSteps().length > 0) {
throw new IllegalStateException("steps have been performed on node");
}
// verify that the expression actually contains syntactic sugar
final Expression expression = node.getExpression();
if (!expression.containsSyntacticSugar()) {
throw new IllegalArgumentException("node does not contain syntactic sugar");
}
// cast the proof node to the appropriate type
final AbstractProofNode abstractNode = (AbstractProofNode)node;
// create the undoable edit
UndoableTreeEdit edit = new UndoableTreeEdit() {
public void redo() {
// translate the expression of the node to core syntax
abstractNode.setExpression(expression.translateSyntacticSugar());
nodeChanged(abstractNode);
}
public void undo() {
// restore the previous expression
abstractNode.setExpression(expression);
nodeChanged(abstractNode);
}
};
// perform the redo operation
edit.redo();
// and record the edit
addUndoableTreeEdit(edit);
}
//
// Undo/Redo
//
/**
* The base interface for all undoable edit actions
* on the tree model. Derived classes will need to
* implement this interface whenever any action on
* the tree is to be performed.
*/
protected static interface UndoableTreeEdit {
/**
* Performs the action of this tree edit. This method
* is invoked initially, when the edit is added via
* {@link AbstractProofModel#addUndoableTreeEdit(UndoableTreeEdit)},
* and when a previously undone change is redone.
*/
public void redo();
/**
* Undoes the effect of a previous call to {@link #redo()}.
*/
public void undo();
}
/**
* The list of redoable edits.
*
* @see #isRedoable()
* @see #redo()
*/
private LinkedList<UndoableTreeEdit> redoEdits = new LinkedList<UndoableTreeEdit>();
/**
* The list of undoable edits.
*
* @see #isUndoable()
* @see #undo()
*/
private LinkedList<UndoableTreeEdit> undoEdits = new LinkedList<UndoableTreeEdit>();
/**
* Adds the specified <code>edit</code> to the undo history. The
* current redo history is cleared.
*
* @param edit the {@link UndoableTreeEdit} to add.
*
* @throws NullPointerException if <code>edit</code> is <code>null</code>.
*/
protected void addUndoableTreeEdit(UndoableTreeEdit edit) {
// remember the previous redoable/undoable properties
boolean oldRedoable = isRedoable();
boolean oldUndoable = isUndoable();
// clear the redo history
this.redoEdits.clear();
// add to the undo history
this.undoEdits.add(0, edit);
// notify the redoable/undoable properties
if (oldRedoable != isRedoable()) {
firePropertyChange("redoable", oldRedoable, isRedoable());
}
if (oldUndoable != isUndoable()) {
firePropertyChange("undoable", oldUndoable, isUndoable());
}
}
/**
* {@inheritDoc}
*
* @see ProofModel#isRedoable()
*/
public boolean isRedoable() {
return (!this.redoEdits.isEmpty());
}
/**
* {@inheritDoc}
*
* @see ProofModel#isUndoable()
*/
public boolean isUndoable() {
return (!this.undoEdits.isEmpty());
}
/**
* {@inheritDoc}
*
* @see common.ProofModel#redo()
*/
public void redo() throws CannotRedoException {
if (this.redoEdits.isEmpty()) {
throw new CannotRedoException("nothing to redo");
}
// remember the previous redoable/undoable properties
boolean oldRedoable = isRedoable();
boolean oldUndoable = isUndoable();
// redo the most recent tree edit
UndoableTreeEdit edit = this.redoEdits.poll();
this.undoEdits.add(0, edit);
edit.redo();
// notify the redoable/undoable properties
if (oldRedoable != isRedoable()) {
firePropertyChange("redoable", oldRedoable, isRedoable());
}
if (oldUndoable != isUndoable()) {
firePropertyChange("undoable", oldUndoable, isUndoable());
}
}
/**
* {@inheritDoc}
*
* @see common.ProofModel#undo()
*/
public void undo() throws CannotUndoException {
if (this.undoEdits.isEmpty()) {
throw new CannotUndoException("nothing to undo");
}
// remember the previous redoable/undoable properties
boolean oldRedoable = isRedoable();
boolean oldUndoable = isUndoable();
// undo the most recent tree edit
UndoableTreeEdit edit = this.undoEdits.poll();
this.redoEdits.add(0, edit);
edit.undo();
// notify the redoable/undoable properties
if (oldRedoable != isRedoable()) {
firePropertyChange("redoable", oldRedoable, isRedoable());
}
if (oldUndoable != isUndoable()) {
firePropertyChange("undoable", oldUndoable, isUndoable());
}
}
//
// Tree queries
//
/**
* Returns the root of the tree. Returns <code>null</code>
* only if the tree has no nodes.
*
* @return the root of the proof tree.
*
* @see javax.swing.tree.TreeModel#getRoot()
*/
public AbstractProofNode getRoot() {
return this.root;
}
/**
* Returns the child of <code>parent</code> at index <code>index</code> in
* the <code>parent</code>'s child array. <code>parent</code> must be a node
* previously obtained from this data source. This should not return <code>null</code>
* if <code>index</code> is a valid index for <code>parent</code> (that is
* <code>index >= 0 && index < getChildCount(parent)</code>).
*
* @param parent a node in the tree, obtained from this data source.
* @param index the child index of a child node of <code>parent</code>.
*
* @return the child of <code>parent</code> at index <code>index</code>.
*
* @see javax.swing.tree.TreeModel#getChild(java.lang.Object, int)
*/
public AbstractProofNode getChild(Object parent, int index) {
return ((AbstractProofNode)parent).getChildAt(index);
}
/**
* Returns the number of children of <code>parent</code>. Returns <code>0</code>
* if the node is a leaf or if it has no children. <code>parent</code> must be a
* node previously obtained from this data source.
*
* @param parent a node in the tree, obtained from this data source
*
* @return the number of children of the node <code>parent</code>.
*
* @see javax.swing.tree.TreeModel#getChildCount(java.lang.Object)
*/
public int getChildCount(Object parent) {
return ((ProofNode)parent).getChildCount();
}
/**
* Returns the index of child in parent. If either the parent or child is
* <code>null</code>, returns -1.
*
* @param parent a note in the tree, obtained from this data source.
* @param child the node we are interested in.
*
* @return the index of the <code>child</code> in the <code>parent</code>,
* or -1 if either the <code>parent</code> or the <code>child</code>
* is <code>null</code>.
*
* @see TreeNode#getIndex(javax.swing.tree.TreeNode)
* @see javax.swing.tree.TreeModel#getIndexOfChild(java.lang.Object, java.lang.Object)
*/
public int getIndexOfChild(Object parent, Object child) {
if (parent == null || child == null)
return -1;
return ((ProofNode)parent).getIndex((ProofNode)child);
}
/**
* {@inheritDoc}
*
* @see ProofModel#getPathToRoot(TreeNode)
*/
public TreeNode[] getPathToRoot(TreeNode aNode) {
return getPathToRoot(aNode, 0);
}
/**
* Builds the parents of node up to and including the root node,
* where the original node is the last element in the returned array.
* The length of the returned array gives the node's depth in the
* tree.
*
* @param aNode the {@link TreeNode} to get the path for
* @param depth an integer giving the number of steps already taken
* towards the root (on recursive calls), used to size the
* returned array.
*
* @return an array of {@link TreeNode}s giving the path from the root
* to the specified node.
*/
protected TreeNode[] getPathToRoot(TreeNode aNode, int depth) {
TreeNode[] nodes;
// This method recurses, traversing towards the root in order
// size the array. On the way back, it fills in the nodes,
// starting from the root and working back to the original node.
/* Check for null, in case someone passed in a null node, or
* they passed in an element that isn't rooted at root.
*/
if (aNode == null) {
if (depth == 0)
return null;
else
nodes = new TreeNode[depth];
}
else {
depth += 1;
if (aNode == this.root)
nodes = new TreeNode[depth];
else
nodes = getPathToRoot(aNode.getParent(), depth);
nodes[nodes.length - depth] = aNode;
}
return nodes;
}
/**
* Returns <code>true</code> if <code>node</code> is a leaf. It is possible for
* this method to return <code>false</code> even if node has no children. A
* directory in a filesystem, for example, may contain no files; the node
* representing the directory is not a leaf, but it also has no children.
*
* @param node a node in the tree, obtained from this data source.
*
* @return <code>true</code> if the <code>node</code> is a leaf.
*
* @see ProofNode#isLeaf()
* @see javax.swing.tree.TreeModel#isLeaf(java.lang.Object)
*/
public boolean isLeaf(Object node) {
return ((ProofNode)node).isLeaf();
}
/**
* This method is not implemented by the {@link ProofModel} class
* and will an {@link UnsupportedOperationException} on every
* invocation.
*
* @param path path to the node that the user has altered.
* @param newValue the new value from the {@link javax.swing.tree.TreeCellEditor}.
*
* @throws UnsupportedOperationException on every invocation.
*
* @see javax.swing.tree.TreeModel#valueForPathChanged(javax.swing.tree.TreePath, java.lang.Object)
*/
public void valueForPathChanged(TreePath path, Object newValue) {
throw new UnsupportedOperationException("Method not implemented");
}
//
// Events
//
/**
* Adds a listener for the {@link TreeModelEvent} posted after the tree changes.
*
* @param l the listener to be added.
*
* @see #getTreeModelListeners()
* @see #removeTreeModelListener(TreeModelListener)
* @see javax.swing.tree.TreeModel#addTreeModelListener(javax.swing.event.TreeModelListener)
*/
public void addTreeModelListener(TreeModelListener l) {
this.listenerList.add(TreeModelListener.class, l);
}
/**
* Removes a listener previously added with {@link #addTreeModelListener(TreeModelListener)}.
*
* @param l the listener to be removed.
*
* @see #addTreeModelListener(TreeModelListener)
* @see #getTreeModelListeners()
* @see javax.swing.tree.TreeModel#removeTreeModelListener(javax.swing.event.TreeModelListener)
*/
public void removeTreeModelListener(TreeModelListener l) {
this.listenerList.remove(TreeModelListener.class, l);
}
/**
* {@inheritDoc}
*
* @see ProofModel#getTreeModelListeners()
*/
public TreeModelListener[] getTreeModelListeners() {
return (TreeModelListener[])this.listenerList.getListeners(TreeModelListener.class);
}
/**
* {@inheritDoc}
*
* @see ProofModel#getListeners(Class)
*/
public <T extends EventListener> T[] getListeners(Class<T> listenerType) {
return listenerList.getListeners(listenerType);
}
/**
* Notifies all listeners that have registered interest for
* notification on this event type. The event instance
* is lazily created using the parameters passed into
* the fire method.
*
* @param source the node being changed
* @param path the path to the root node
* @param childIndices the indices of the changed elements
* @param children the changed elements
*
* @see EventListenerList
*/
protected void fireTreeNodesChanged(Object source, Object[] path, int[] childIndices, Object[] children) {
// Guaranteed to return a non-null array
Object[] listeners = this.listenerList.getListenerList();
TreeModelEvent e = null;
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == TreeModelListener.class) {
// Lazily create the event:
if (e == null)
e = new TreeModelEvent(source, path, childIndices, children);
((TreeModelListener)listeners[i + 1]).treeNodesChanged(e);
}
}
}
/**
* Notifies all listeners that have registered interest for
* notification on this event type. The event instance is
* lazily created using the parameters passed into the fire
* method.
*
* @param source the node where new elements are being inserted
* @param path the path to the root node
* @param childIndices the indices of the new elements
* @param children the new elements
*
* @see EventListenerList
*/
protected void fireTreeNodesInserted(Object source, Object[] path, int[] childIndices, Object[] children) {
// Guaranteed to return a non-null array
Object[] listeners = this.listenerList.getListenerList();
TreeModelEvent e = null;
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == TreeModelListener.class) {
// Lazily create the event:
if (e == null)
e = new TreeModelEvent(source, path, childIndices, children);
((TreeModelListener)listeners[i + 1]).treeNodesInserted(e);
}
}
}
/**
* Notifies all listeners that have registered interest for
* notification on this event type. The event instance is
* lazily created using the parameters passed into the fire
* method.
*
* @param source the node where elements are being removed
* @param path the path to the root node
* @param childIndices the indices of the removed elements
* @param children the removed elements
*
* @see EventListenerList
*/
protected void fireTreeNodesRemoved(Object source, Object[] path, int[] childIndices, Object[] children) {
// Guaranteed to return a non-null array
Object[] listeners = this.listenerList.getListenerList();
TreeModelEvent e = null;
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == TreeModelListener.class) {
// Lazily create the event:
if (e == null)
e = new TreeModelEvent(source, path, childIndices, children);
((TreeModelListener)listeners[i + 1]).treeNodesRemoved(e);
}
}
}
/**
* Notifies all listeners that have registered interest for
* notification on this event type. The event instance
* is lazily created using the parameters passed into
* the fire method.
*
* @param source the node where the tree model has changed
* @param path the path to the root node
* @param childIndices the indices of the affected elements
* @param children the affected elements
*
* @see EventListenerList
*/
protected void fireTreeStructureChanged(Object source, Object[] path, int[] childIndices, Object[] children) {
// Guaranteed to return a non-null array
Object[] listeners = this.listenerList.getListenerList();
TreeModelEvent e = null;
// Process the listeners last to first, notifying
// those that are interested in this event
for (int i = listeners.length - 2; i >= 0; i -= 2) {
if (listeners[i] == TreeModelListener.class) {
// Lazily create the event:
if (e == null)
e = new TreeModelEvent(source, path, childIndices, children);
((TreeModelListener)listeners[i + 1]).treeStructureChanged(e);
}
}
}
//
// Convenience methods for event handling
//
/**
* Invoke this method after you've changed how node is to be
* represented in the tree.
*
* @param node the {@link TreeNode} that was altered.
*/
protected void nodeChanged(TreeNode node) {
if (this.listenerList != null && node != null) {
// determine the parent node
TreeNode parent = node.getParent();
if (parent != null) {
// determine the index of the node
int anIndex = parent.getIndex(node);
if (anIndex != -1) {
int[] cIndexs = new int[1];
cIndexs[0] = anIndex;
nodesChanged(parent, cIndexs);
}
}
else if (node == getRoot()) {
nodesChanged(node, null);
}
}
}
/**
* Invoke this method after you've changed how the children identified by
* <code>childIndicies</code> are to be represented in the tree.
*
* @param node a {@link TreeNode} within this proof model.
* @param childIndices the indices of the children that changed.
*/
protected void nodesChanged(TreeNode node, int[] childIndices) {
if (node != null) {
if (childIndices != null) {
// check if any child indices were supplied
int cCount = childIndices.length;
if (cCount > 0) {
// collect the child nodes
Object[] cChildren = new Object[cCount];
for (int counter = 0; counter < cCount; ++counter)
cChildren[counter] = node.getChildAt(childIndices[counter]);
// notify the view
fireTreeNodesChanged(this, getPathToRoot(node), childIndices,
cChildren);
}
}
else if (node == getRoot()) {
fireTreeNodesChanged(this, getPathToRoot(node), null, null);
}
}
}
/**
* Invoke this method after you've inserted some TreeNodes into
* node. <code>childIndices</code> should be the index of the new
* elements and must be sorted in ascending order.
*
* @param node a node in the proof model.
* @param childIndices the indices of the children that were inserted.
*/
protected void nodesWereInserted(TreeNode node, int[] childIndices) {
if (this.listenerList != null && node != null && childIndices != null && childIndices.length > 0) {
int cCount = childIndices.length;
Object[] newChildren = new Object[cCount];
for (int counter = 0; counter < cCount; ++counter)
newChildren[counter] = node.getChildAt(childIndices[counter]);
fireTreeNodesInserted(this, getPathToRoot(node), childIndices, newChildren);
}
}
/**
* Invoke this method after you've removed some TreeNodes from
* node. <code>childIndices</code> should be the index of the
* removed elements and must be sorted in ascending order.
* And <code>removedChildren</code> should be the array of
* the children objects that were removed.
*
* @param node a node in the proof model.
* @param childIndices the indices of the children that were removed.
* @param removedChildren the removed children.
*/
protected void nodesWereRemoved(TreeNode node, int[] childIndices, Object[] removedChildren) {
if (node != null && childIndices != null) {
fireTreeNodesRemoved(this, getPathToRoot(node), childIndices, removedChildren);
}
}
}