/*
* NodeWatcher.java
* JCollider
*
* Copyright (c) 2004-2010 Hanns Holger Rutz. All rights reserved.
*
* This software 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, june 1991 of the License, or (at your option) any later version.
*
* This software 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 (gpl.txt) along with this software; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de , or visit http://www.sciss.de/jcollider
*
*
* JCollider is closely modelled after SuperCollider Language,
* often exhibiting a direct translation from Smalltalk to Java.
* SCLang is a software originally developed by James McCartney,
* which has become an Open Source project.
* See http://supercollider.sourceforge.net/ for details.
*
*
* Changelog:
* 02-Oct-05 created
* 30-Jul-06 public constructor has been removed. Use newFrom instead!
* 17-Sep-06 fixed sync bug in listener notification; implemented queryAllNodes
*/
package de.sciss.jcollider;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.List;
import java.util.Set;
import javax.swing.Timer;
import de.sciss.app.BasicEvent;
import de.sciss.app.EventManager;
import de.sciss.net.OSCMessage;
import de.sciss.net.OSCPacket;
/**
* A node status managing class which has a similar concept
* as SCLang's NodeWatcher class, however the implementation is
* different and the feature set is different.
* <p>
* When a notification message arrives, it is deferred to the
* event dispatching thread. Interested objects can register
* a listener to receive <code>NodeEvent</code>s. Right before
* the listener's <code>nodeAction</code> method is called, the
* client side <code>Node</code> status is updated, i.e. the node
* linked to or unlinked from neighbouring nodes, the running
* playing flags are set etc.
* <p>
* Since these updates occur in the event thread, it is safe to
* use the <code>Node</code>'s <code>TreeNode</code> interface.
*
* @author Hanns Holger Rutz
* @version 0.33, 19-Mar-08
*
* @synchronization all methods are thread safe unless explicitely noted
* ; however you should avoid to register a node at more than
* one active node watcher at a time
*/
public class NodeWatcher
implements EventManager.Processor, OSCResponderNode.Action, Constants, Runnable
{
/**
* Set this to <code>true</code> for debugging
* all relevant actions of the watcher, such as starting,
* stopping, registering nodes, and updating node status.
*/
public boolean VERBOSE = false;
private EventManager em = null; // lazy
// no getter method now because we might
// allow to use more than one server
// in a future version
protected final Server server;
private int dumpMode = kDumpOff;
private boolean watching = false;
private boolean fireAllNodes = false;
private boolean autoRegister = false;
private final Map mapNodes = new HashMap(); // maps Integer( nodeID ) to Node ; synchronized through 'sync'
private final OSCResponderNode[] resps;
private final Object sync = new Object();
private final List collQueue = new ArrayList(); // element = (OSCMessage) ; synchronized through 'sync'
private static final Map allInstances = new HashMap(); // (String) Server.name to (NodeWatcher) instance
private NodeWatcher( Server s )
{
this.server = s;
// create responders for all known node notification messages.
final List collValidCmds = NodeEvent.getValidOSCCommands();
resps = new OSCResponderNode[ collValidCmds.size() ];
for( int i = 0; i < collValidCmds.size(); i++ ) {
resps[ i ] = new OSCResponderNode( server, (String) collValidCmds.get( i ), this );
}
}
/**
* Returns a <code>NodeWatcher</code> to
* monitor a given server. Note that the client must
* receive notifications, which is true by default when
* booting a server. By default, events are only fired
* for registered nodes. To change this behaviour, call
* <code>setFireAllNodes( true )</code>
*
* @param s the server to which the nodes to be watched belong
*
* @see Server#notify( boolean )
*/
public static NodeWatcher newFrom( Server s )
throws IOException
{
synchronized( allInstances ) {
NodeWatcher nw;
nw = (NodeWatcher) allInstances.get( s.getName() );
if( nw == null ) {
nw = new NodeWatcher( s );
nw.start();
allInstances.put( s.getName(), nw );
}
return nw;
}
}
/**
* Starts the OSC responders that trace incoming
* node notification events.
*/
public void start()
throws IOException
{
if( server.isRunning() && !server.isNotified() ) {
Server.getPrintStream().println( "NodeWatcher warning: server does not receive notifications" );
}
synchronized( sync ) {
try {
for( int i = 0; i < resps.length; i++ ) {
resps[ i ].add();
}
watching = true;
if( VERBOSE ) System.err.println( "NodeWatcher.start()" );
}
catch( IOException e1 ) {
stop();
throw e1;
}
}
}
/**
* Stops the OSC responders that trace incoming
* node notification events.
*/
public void stop()
throws IOException
{
synchronized( sync ) {
watching = false;
if( VERBOSE ) System.err.println( "NodeWatcher.stop()" );
for( int i = 0; i < resps.length; i++ ) {
resps[ i ].remove();
}
}
}
/**
* Queries the watching state
*
* @return <code>true</code> if we are watching for node changes
* (i.e. after calling <code>start</code>), <code>false</code> otherwise
* (i.e. after creating the watcher or after calling <code>stop</code>).
*/
public boolean isWatching()
{
synchronized( sync ) {
return watching;
}
}
//*unregister { arg node;
// var watcher;
// watcher = this.newFrom(node.server);
// watcher.unregister(node);
//}
/**
* Adds a node to the list of known nodes.
* The node will be automatically removed, when
* a corresponding <code>"/n_end"</code>
* message arrives. Note that there is a little chance
* that <code>"/n_go"</code> messages are
* missed if you register a node <strong>after</strong> it's new-message
* has been sent to the server. A safe way to register
* nodes is to call the basic-new-commands and send the
* new-message after the registration. for example:
* <PRE>
* Synth mySynth = Synth.basicNew( "mySynthDef", myServer );
* myNodeWatcher.register( mySynth );
* myServer.sendMsg( mySynth.newMsg( myTarget, myArgNames, myArgValues ));
* </PRE>
*
* @param node the node to register
*
* @see #setFireAllNodes( boolean )
*/
public void register( Node node )
{
register( node, false );
}
public void register( Node node, boolean assumePlaying )
{
synchronized( sync ) {
if( watching ) {
final Object key = new Integer( node.getNodeID() );
if( assumePlaying && mapNodes.containsKey( key )) {
node.setPlaying( true );
}
mapNodes.put( key, node );
if( VERBOSE ) System.err.println( "NodeWatcher.register( " + node + " )" );
}
}
}
/**
* Unregister a node, that is remove it from the list of
* known nodes. Note that you usually need not call this
* because when a node is automatically unregistered
* when a <code>"/n_end"</code> for that node arrives.
*
* @param node the node to unregister
*/
public void unregister( Node node )
{
synchronized( sync ) {
mapNodes.remove( new Integer( node.getNodeID() ));
if( VERBOSE ) System.err.println( "NodeWatcher.unregister( " + node + " )" );
}
}
/**
* Queries a list of all registered nodes.
*
* @return a list whose elements are of class <code>Node</code>. the list
* itself is a copy and maybe modified. It will not be affected by
* successive calls to <code>register</code> or <code>unregister</code>
*/
public List getAllNodes()
{
synchronized( sync ) {
return new ArrayList( mapNodes.values() );
}
}
/**
* Registers a listener to be informed about
* node status changes. Status changes occur
* as of nodes being created, destroyed, paused, resumed,
* moved, or as a result of sending a <code>/n_query</code>
* message to the server. The <code>fireAllNodes</code>
* flag determines whether all status changes are
* forwarded to listeners, or only those for previously
* registered nodes.
*
* @param l listener to be added
*
* @see #setFireAllNodes( boolean )
*/
public synchronized void addListener( NodeListener l )
{
if( em == null ) em = new EventManager( this );
em.addListener( l );
}
/**
* Unregisters a listener from being informed about
* node status changes.
*
* @param l listener to be removed
*/
public void removeListener( NodeListener l )
{
if( em != null ) em.removeListener( l );
}
/**
* @param timeout maximum time to wait for node info replies
* @param doneAction to be executed when the tree has been queried (can be <code>null</code>)
*/
public void queryAllNodes( float timeout, ActionListener doneAction )
{
setFireAllNodes( true );
setAutoRegister( true );
final Set setNodes = new HashSet();
final NodeListener nl;
final Timer stopQueryTimer;
stopQueryTimer = new Timer( (int) (timeout * 1000), null );
nl = new NodeListener() {
public void nodeAction( NodeEvent e )
{
if( e.getID() != NodeEvent.INFO ) return;
final Node n = e.getNode();
final List nextNodes;
if( setNodes.add( n )) {
register( n );
nextNodes = new ArrayList( 2 );
if( e.getHeadNodeID() != -1 ) nextNodes.add( new Integer( e.getHeadNodeID() ));
if( e.getSuccNodeID() != -1 ) nextNodes.add( new Integer( e.getSuccNodeID() ));
if( !nextNodes.isEmpty() ) {
try {
server.sendMsg( new OSCMessage( "/n_query", nextNodes.toArray() ));
}
catch( IOException e1 ) {
e1.printStackTrace();
}
}
}
stopQueryTimer.restart();
}
};
stopQueryTimer.addActionListener( new ActionListener() {
public void actionPerformed( ActionEvent e )
{
removeListener( nl );
}
});
if( doneAction != null ) stopQueryTimer.addActionListener( doneAction );
stopQueryTimer.setRepeats( false );
stopQueryTimer.restart();
addListener( nl );
try {
server.sendMsg( server.getDefaultGroup().queryMsg() );
}
catch( IOException e1 ) {
e1.printStackTrace();
}
}
/**
* Removes all nodes from the list of known nodes.
*/
public void clear()
{
synchronized( sync ) {
mapNodes.clear();
if( VERBOSE ) System.err.println( "NodeWatcher.clear()" );
}
}
/**
* Disposes any resources
* allocated by this representation.
* This shuts down OSC communication
* and server event dispatching.
* It clear the list of registered nodes.
* Do not use this object any more
* after calling this method.
*/
public void dispose()
{
synchronized( allInstances ) {
if( isWatching() ) {
try {
stop();
}
catch( IOException e1 ) {
System.err.println( "NodeWatcher.dispose : " +
e1.getClass().getName() + " : " + e1.getLocalizedMessage() );
}
}
if( em != null ) em.dispose();
clear();
allInstances.remove( server.getName() );
if( VERBOSE ) System.err.println( "NodeWatcher.dispose()" );
}
}
/**
* Changes the way incoming messages are dumped
* to the console. By default incoming messages are not
* dumped. The server's print stream is used to do
* the dumping
*
* @param dumpMode only <code>kDumpNone</code> and <code>kDumpText</code>
* are supported at the moment.
*
* @see Server#dumpOSC( int )
*/
public void dumpIncomingOSC( int dumpMode )
{
this.dumpMode = dumpMode;
}
/**
* Decides whether node changes for any node
* or only for registered nodes are delivered to
* event listeners.
*
* @param allNodes <code>true</code> to deliver events
* for all incoming node messages;
* <code>false</code> to deliver events only
* if the corresponding node was registered
*/
public void setFireAllNodes( boolean allNodes )
{
fireAllNodes = allNodes;
}
/**
* Queries the event dispatching mode. See <code>setFireAllNodes</code>
* for details
*
* @see #setFireAllNodes( boolean )
*/
public boolean getFireAllNodes()
{
return fireAllNodes;
}
/**
* Decides whether unknown nodes should be automatically
* added to the node watcher (usefull for debugging).
*
* @param onOff <code>true</code> to have nodes automatically created and
* registered upon incoming notification events. <code>false</code>
* to stop automatic registration.
*/
public void setAutoRegister( boolean onOff )
{
autoRegister = onOff;
}
/**
* Queries whether automatic node registration is enabled.
*
* @see #setAutoRegister( boolean )
*/
public boolean setAutoRegister()
{
return autoRegister;
}
// @synchronization has to be called with sync on sync
private void nodeGo( Node node, NodeEvent e )
{
final Group group = (Group) mapNodes.get( new Integer( e.getParentGroupID() ));
final Node pred = (Node) mapNodes.get( new Integer( e.getPredNodeID() ));
final Node succ = (Node) mapNodes.get( new Integer( e.getSuccNodeID() ));
node.setGroup( group );
node.setPredNode( pred );
node.setSuccNode( succ );
if( pred != null ) pred.setSuccNode( node );
if( succ != null ) succ.setPredNode( node );
if( group != null ) {
if( e.getPredNodeID() == -1 ) {
group.setHeadNode( node );
}
if( e.getSuccNodeID() == -1 ) {
group.setTailNode( node );
}
}
node.setRunning( true );
node.setPlaying( true );
if( VERBOSE ) System.err.println( "NodeWatcher.nodeGo( " + node + " )" );
}
// @synchronization has to be called with sync on mapNodes
private void nodeEnd( Node node, NodeEvent e )
{
final Group group = node.getGroup();
final Node pred = node.getPredNode();
final Node succ = node.getSuccNode();
//System.err.println( "Removing "+node );
//System.err.println( " ... pred "+pred );
//System.err.println( " ... succ "+succ );
node.setGroup( null );
node.setPredNode( null );
node.setSuccNode( null );
if( pred != null ) pred.setSuccNode( succ );
if( succ != null ) succ.setPredNode( pred );
if( group != null ) {
if( (group.getHeadNode() != null) && (group.getHeadNode().getNodeID() == node.getNodeID()) ) {
group.setHeadNode( succ );
}
if( (group.getTailNode() != null) && (group.getTailNode().getNodeID() == node.getNodeID()) ) {
group.setTailNode( pred );
}
}
node.setPlaying( false );
node.setRunning( false );
mapNodes.remove( new Integer( node.getNodeID() ));
if( VERBOSE ) System.err.println( "NodeWatcher.nodeEnd( " + node + " )" );
}
// @synchronization has to be called with sync on sync
private void nodeMove( Node node, NodeEvent e )
{
final Group oldGroup = node.getGroup();
final Node oldPred = node.getPredNode();
final Node oldSucc = node.getSuccNode();
final Group newGroup = (Group) mapNodes.get( new Integer( e.getParentGroupID() ));
final Node newPred = (Node) mapNodes.get( new Integer( e.getPredNodeID() ));
final Node newSucc = (Node) mapNodes.get( new Integer( e.getSuccNodeID() ));
node.setGroup( newGroup );
node.setPredNode( newPred );
node.setSuccNode( newSucc );
// needs to be done before setting new pred/succ
if( oldPred != null ) oldPred.setSuccNode( oldSucc );
if( oldSucc != null ) oldSucc.setPredNode( oldPred );
if( newPred != null ) newPred.setSuccNode( node );
if( newSucc != null ) newSucc.setPredNode( node );
// needs to be done before setting new group
if( oldGroup != null ) {
if( (oldGroup.getHeadNode() != null) && (oldGroup.getHeadNode().getNodeID() == node.getNodeID()) ) {
oldGroup.setHeadNode( oldSucc );
}
if( (oldGroup.getTailNode() != null) && (oldGroup.getTailNode().getNodeID() == node.getNodeID()) ) {
oldGroup.setTailNode( oldPred );
}
}
if( newGroup != null ) {
if( e.getPredNodeID() == -1 ) {
newGroup.setHeadNode( node );
}
if( e.getSuccNodeID() == -1 ) {
newGroup.setTailNode( node );
}
}
if( VERBOSE ) System.err.println( "NodeWatcher.nodeMove( " + node + " )" );
}
// ----------- OSCResponderNode.Action interface -----------
/**
* This method is part of the implementation of the
* OSCResponderNode.Action interface. Do not call this method.
*/
public void respond( OSCResponderNode r, OSCMessage msg, long time )
{
if( dumpMode == kDumpText ) {
OSCPacket.printTextOn( Server.getPrintStream(), msg );
}
if( autoRegister ) {
synchronized( sync ) {
final boolean invoke = collQueue.isEmpty();
collQueue.add( msg );
if( invoke ) EventQueue.invokeLater( this );
}
return;
}
final Integer nodeIDObj = (Integer) msg.getArg( 0 );
synchronized( sync ) {
if( mapNodes.containsKey( nodeIDObj )) {
final boolean invoke = collQueue.isEmpty();
collQueue.add( msg );
if( invoke ) EventQueue.invokeLater( this );
}
}
}
// ----------- Runnable interface -----------
/**
* Part of internal message queueing.
* Never call this method.
*/
public void run()
{
final long when;
Integer nodeIDObj;
OSCMessage msg;
NodeEvent nde;
NodeListener listener;
Node node;
synchronized( sync ) {
if( !watching ) return;
when = System.currentTimeMillis();
for( Iterator iter = collQueue.iterator(); iter.hasNext(); ) {
msg = (OSCMessage) iter.next();
nodeIDObj = (Integer) msg.getArg( 0 );
node = (Node) mapNodes.get( nodeIDObj );
if( node == null ) {
if( autoRegister ) {
node = ((Number) msg.getArg( 4 )).intValue() == NodeEvent.GROUP ? (Node) Group.basicNew( server, nodeIDObj.intValue() ) : (Node) Synth.basicNew( null, server, nodeIDObj.intValue() );
register( node );
} else if( !fireAllNodes ) return;
}
nde = NodeEvent.fromOSCMessage( msg, this, when, node );
if( node != null ) { // update the node's fields
switch( nde.getID() ) {
case NodeEvent.GO:
nodeGo( node, nde );
break;
case NodeEvent.END:
nodeEnd( node, nde );
break;
case NodeEvent.ON:
node.setPlaying( true );
break;
case NodeEvent.OFF:
node.setPlaying( false );
break;
case NodeEvent.MOVE:
nodeMove( node, nde );
break;
case NodeEvent.INFO:
nodeMove( node, nde );
break;
default:
assert false : nde.getID();
}
}
if( em != null ) {
// we are already in the event thread, so let's just call the listeners directly
for( int i = 0; i < em.countListeners(); i++ ) {
listener = (NodeListener) em.getListener( i );
try {
listener.nodeAction( nde );
}
catch( Exception e1 ) {
e1.printStackTrace();
}
}
}
} // for iter
collQueue.clear();
} // sync
} // run
// ----------- EventManager.Processor interface -----------
/**
* This is used to dispatch
* node events. Do not call this method.
*/
public void processEvent( BasicEvent e )
{
// NodeListener listener;
// final NodeEvent nde = (NodeEvent) e;
// final Node node = nde.getNode();
//
// synchronized( sync ) {
// if( !watching ) return;
//
// if( node != null ) { // update the node's fields
// switch( e.getID() ) {
// case NodeEvent.GO:
// nodeGo( node, nde );
// break;
//
// case NodeEvent.END:
// nodeEnd( node, nde );
// break;
//
// case NodeEvent.ON:
// node.setPlaying( true );
// break;
//
// case NodeEvent.OFF:
// node.setPlaying( false );
// break;
//
// case NodeEvent.MOVE:
// nodeMove( node, nde );
// break;
//
// case NodeEvent.INFO:
// nodeMove( node, nde );
// break;
//
// default:
// break;
// }
// }
//
// for( int i = 0; i < em.countListeners(); i++ ) {
// listener = (NodeListener) em.getListener( i );
//System.err.println(" --> "+listener );
// listener.nodeAction( nde );
// }
// } // synchronized( sync )
}
}