/*
* Copyright (C) 2012 Jason Gedge <http://www.gedge.ca>
*
* This file is part of the OpGraph project.
*
* 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 3 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, see <http://www.gnu.org/licenses/>.
*/
package ca.gedge.opgraph;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import ca.gedge.opgraph.dag.CycleDetectedException;
import ca.gedge.opgraph.dag.DirectedAcyclicGraph;
import ca.gedge.opgraph.dag.VertexNotFoundException;
import ca.gedge.opgraph.exceptions.ItemMissingException;
import ca.gedge.opgraph.extensions.CompositeNode;
import ca.gedge.opgraph.extensions.Extendable;
import ca.gedge.opgraph.extensions.ExtendableSupport;
import ca.gedge.opgraph.util.Pair;
/**
* A DAG that supports a general flow of "operation". One implements various
* operation nodes. These nodes take input from incoming links, and produce
* output for outgoing links. The flow of operation obeys the topological
* ordering of the DAG.
*/
public final class OpGraph
extends DirectedAcyclicGraph<OpNode, OpLink>
implements Extendable
{
/** An id for the graph */
private String id;
/** A mapping from node id to node */
private Map<String, OpNode> nodeMap;
/**
* Default constructor.
*/
public OpGraph() {
this.nodeMap = new HashMap<String, OpNode>();
setId(null);
}
/**
* Gets the id for this graph.
*
* @return the id
*/
public String getId() {
return id;
}
/**
* Sets the id for this graph.
*
* @param id the id
*/
public void setId(String id) {
this.id = (id == null ? Integer.toHexString(super.hashCode()) : id);
}
//
// Helper methods
//
/**
* Gets a node by its id.
*
* @param id the id of the node
* @param deep if <code>true</code>, performs a deep search where composite
* nodes will also be searched for a node with the given id
*
* @return the node with the given id, or <code>null</code> if no such
* node exists in this graph
*/
public OpNode getNodeById(String id, boolean deep) {
if(deep) {
final Pair<OpGraph, OpNode> nodeGraphPair = findNodeById(id);
return (nodeGraphPair == null ? null : nodeGraphPair.getSecond());
} else {
return nodeMap.get(id);
}
}
/**
* Finds a node and its parent graph by id. This is a deep operation,
* and hence will recursively search through macro nodes to find the
* node with the given id.
*
* @param id the id of the node
*
* @return the graph/node pair, or <code>null</code> if no such node
* exists anywhere in this graph or its macros
*/
protected Pair<OpGraph, OpNode> findNodeById(String id) {
Pair<OpGraph, OpNode> ret = null;
if(nodeMap.containsKey(id)) {
final OpNode node = nodeMap.get(id);
if(node != null) {
ret = new Pair<OpGraph, OpNode>(this, node);
} else {
nodeMap.remove(id);
}
} else {
// Try searching through composite nodes
for(OpNode node : getVertices()) {
final CompositeNode composite = node.getExtension(CompositeNode.class);
if(composite != null) {
ret = composite.getGraph().findNodeById(id);
if(ret != null)
break;
}
}
}
return ret;
}
/**
* Connects the given source node/field pair to a given destination
* node/field pair. This is a convenience method which catches
* exceptions from the {@link OpLink} constructor, and from
* {@link #add(OpLink)}.
*
* @param source source node
* @param sourceFieldKey the key of the field connected at the source
* @param destination destination node
* @param destinationFieldKey the key of the field connected at the destination
*
* @return the link that was created, or <code>null</code> if the link
* could not be created
*/
public OpLink connect(OpNode source,
String sourceFieldKey,
OpNode destination,
String destinationFieldKey)
{
OpLink link = null;
try {
link = new OpLink(source, sourceFieldKey, destination, destinationFieldKey);
add(link);
} catch(ItemMissingException exc) {
link = null;
} catch(VertexNotFoundException exc) {
link = null;
} catch(CycleDetectedException exc) {
link = null;
}
return link;
}
/**
* Connects the given source node/field pair to a given destination node/field
* pair. This is a convenience method which catches exceptions from the
* {@link OpLink} constructor, and from {@link #add(OpLink)}.
*
* @param source source node
* @param sourceField the field connected at the source
* @param destination destination node
* @param destinationField the field connected at the destination
*
* @return the link that was created, or <code>null</code> if the link
* could not be created
*/
public OpLink connect(OpNode source,
OutputField sourceField,
OpNode destination,
InputField destinationField)
{
OpLink link = null;
try {
link = new OpLink(source, sourceField, destination, destinationField);
add(link);
} catch(ItemMissingException exc) {
link = null;
} catch(VertexNotFoundException exc) {
link = null;
} catch(CycleDetectedException exc) {
link = null;
}
return link;
}
//
// Overrides
//
@Override
public void add(OpNode node) {
if(node != null && node.getId() != null) {
if(nodeMap.containsKey(node)) {
// XXX What to do if node with that id already exists?
} else {
super.add(node);
node.addNodeListener(nodeListener);
nodeMap.put(node.getId(), node);
fireNodeAdded(node);
}
}
}
@Override
public boolean remove(OpNode node) {
final boolean removed = super.remove(node);
if(removed) {
node.removeNodeListener(nodeListener);
nodeMap.remove(node.getId());
fireNodeRemoved(node);
}
return removed;
}
@Override
public void add(OpLink link) throws VertexNotFoundException, CycleDetectedException {
super.add(link);
if(link != null)
fireLinkAdded(link);
}
@Override
public boolean remove(OpLink link) {
final boolean removed = super.remove(link);
if(removed)
fireLinkRemoved(link);
return removed;
}
//
// Extendable
//
private ExtendableSupport extendableSupport = new ExtendableSupport(OpLink.class);
@Override
public <T> T getExtension(Class<T> type) {
return extendableSupport.getExtension(type);
}
@Override
public Collection<Class<?>> getExtensionClasses() {
return extendableSupport.getExtensionClasses();
}
@Override
public <T> T putExtension(Class<T> type, T extension) {
return extendableSupport.putExtension(type, extension);
}
//
// OpNodeListener
//
final OpNodeListener nodeListener = new OpNodeListener() {
@Override
public void fieldRemoved(OpNode node, OutputField field) {
for(OpLink link : getOutgoingEdges(node)) {
if(link.getSourceField().equals(field)) {
remove(link);
break;
}
}
}
@Override
public void fieldRemoved(OpNode node, InputField field) {
for(OpLink link : getIncomingEdges(node)) {
if(link.getDestinationField().equals(field)) {
remove(link);
break;
}
}
}
@Override
public void nodePropertyChanged(String propertyName, Object oldValue, Object newValue) {}
@Override
public void fieldAdded(OpNode node, OutputField field) {}
@Override
public void fieldAdded(OpNode node, InputField field) {}
};
//
// Listeners
//
private final ArrayList<OpGraphListener> listeners = new ArrayList<OpGraphListener>();
/**
* Adds a listener to this graph.
*
* @param listener the listener to add
*/
public void addGraphListener(OpGraphListener listener) {
synchronized(listeners) {
listeners.add(listener);
}
}
/**
* Removes a listener from this graph.
*
* @param listener the listener to remove
*/
public void removeGraphListener(OpGraphListener listener) {
synchronized(listeners) {
listeners.remove(listener);
}
}
private void fireNodeAdded(OpNode node) {
synchronized(listeners) {
for(OpGraphListener listener : listeners)
listener.nodeAdded(this, node);
}
}
private void fireNodeRemoved(OpNode node) {
synchronized(listeners) {
for(OpGraphListener listener : listeners)
listener.nodeRemoved(this, node);
}
}
private void fireLinkAdded(OpLink link) {
synchronized(listeners) {
for(OpGraphListener listener : listeners)
listener.linkAdded(this, link);
}
}
private void fireLinkRemoved(OpLink link) {
synchronized(listeners) {
for(OpGraphListener listener : listeners)
listener.linkRemoved(this, link);
}
}
}