/*
 *  SwingOSC.java
 *  SwingOSC
 *
 *  Copyright (c) 2005-2009 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
 *
 *
 *  Changelog:
 *		19-Aug-05	created
 *		05-Sep-05	lot's of changes
 *					- deferred messages to event thread
 *					- added /n_get ; added nested propertyNames (i.e. "size.width")
 *					- added special node names "GraphicsEnvironment", "Toolkit"
 *					- -U option is now -u for consistency with scsynth
 *		10-Nov-05	OSC command set completely reworked to make it as simple as possible
 *					and allow any kind of extensions not limited to the awt/swing sphere.
 *					special string<->object conversions are replaced by a nesting mechanism
 *					using message-blobs as automatically generated by supercollider when nesting arrays
 *		14-Feb-06	- fixed bug in /free
 *					- mostly exchanged decodeMessageArgs with step-by-step
 *					  decoding using decodeMessageArg such as to provide a consistent causality
 *					  when arguments include OSCMessages with side effects
 *					- added /field(r)
 *					- all commands are now subclasses of BasicCmd, more efficient lookup
 *					- removed pre-initialized object "toolkit" which would force the application
 *					  to become a swing app (in Mac OS X)
 *		25-Mar-06	- new String based message nesting format
 *					- uses new OSCClient class
 *		08-Aug-06	- uses custom class loader ; added /classes command
 *		01-Oct-06	uses new NetUtil version
 *		18-Jan-08	fixed checkMethodArgs to put lower priority to Object assignment
 */
 
package de.sciss.swingosc;

import java.awt.EventQueue;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
//import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import de.sciss.app.BasicEvent;
import de.sciss.app.EventManager;
import de.sciss.net.OSCBundle;
import de.sciss.net.OSCChannel;
import de.sciss.net.OSCListener;
import de.sciss.net.OSCMessage;
import de.sciss.net.OSCPacket;
import de.sciss.net.OSCPacketCodec;
import de.sciss.net.OSCServer;
import de.sciss.net.OSCTransmitter;
//import de.sciss.util.DynamicURLClassLoader;
import de.sciss.util.URLClassLoaderManager;

/**
 *	A one-in-all class launching an OSC widget server. Windows and
 *	gadgets can be created by sending OSC commands to the server.
 *	Action listeners can be registered and are notified about button
 *	state changes.
 *
 *  @author		Hanns Holger Rutz
 *  @version	0.64, 03-Feb-10
 *
 *	@todo		rendezvous option (jmDNS)
 *	@todo		[NOT?] /n_notify (sending things like /n_go, n_end)
 *	@todo		since we invoke runLater already, we could add timetag flavors easily
 *	@todo		/import !!!check ImportText.xcodeproj!!! XXX
 */
public class SwingOSC
implements OSCListener, OSCProcessor, EventManager.Processor
{
	public static final double		VERSION			= 0.64;

	protected OSCServer				serv			= null;
	
//	private final List				collMessages	= Collections.synchronizedList( new ArrayList() );	// elements = IncomingMessage instances

	private final Map				mapClients		= new HashMap();	// SocketAddr to Client
	protected final Map				globals			= new HashMap();	// objectID to value
	private final Map				constants		= new HashMap();	// objectID to value
	
	private static SwingOSC			instance;
	private SwingClient				currentClient	= null;
	
	protected final Map				oscCmds			= new HashMap( 20 );	// OSC-command-name to OSCProcessor
	private final EventManager		elm;
	
	private static int				numTraceLines	= 3;

//	private final DynamicURLClassLoader classLoaderMgr;
	protected final URLClassLoaderManager classLoaderMgr;
	
    public static void main( String[] args )
	{
    	System.exit( launch( args ));
	}
    
    /**
     * 	Launches the server with given command line options.
     * 
     *	@param	args	the array of command line options, e.g.
     * 					<code>-t 57111</code>.
     *	@return			the return code (0 for success, 1 for failure)
     */
    public static int launch( String[] args )
    {
		String					arg;
		int						port		= 0;
		boolean					loopBack	= false;
		boolean					initSwing	= false;
		InetSocketAddress		hello		= null;
		int						bufSize		= 65536;
		String					protocol	= OSCChannel.UDP;
		int						i, j;
	
		for( i = 0; i < args.length; i++ ) {
			arg = args[ i ];
			if( arg.equals( "-u" )) {
				i++;
				if( i < args.length ) {
					try {
						port = Integer.parseInt( args[ i ]);
					}
					catch( NumberFormatException e1 ) {
						printException( e1, "-u" );
					}
				} else {
					System.out.println( "-u option requires port specification! (-u <portNum>)" );
				}
				
			} else if( arg.equals( "-t" )) {
					i++;
					if( i < args.length ) {
						try {
							port 		= Integer.parseInt( args[ i ]);
							protocol	= OSCChannel.TCP;
						}
						catch( NumberFormatException e1 ) {
							printException( e1, "-t" );
						}
					} else {
						System.out.println( "-t option requires port specification! (-t <portNum>)" );
					}
					
			} else if( arg.equals( "-L" )) {
				loopBack	= true;
			
			} else if( arg.equals( "-b" )) {
				i++;
				if( i < args.length ) {
					try {
						bufSize 	= Integer.parseInt( args[ i ]);
					}
					catch( NumberFormatException e1 ) {
						printException( e1, "-b" );
					}
				} else {
					System.out.println( "-b option requires size specification! (-b <size>)" );
				}
				
			} else if( arg.equals( "-i" )) {
				initSwing	= true;
			
			} else if( arg.equals( "-h" )) {
				i++;
				if( i < args.length ) {
					j = args[ i ].indexOf( ':' );
					try {
						hello = new InetSocketAddress( args[ i ].substring( 0, j ),
								Integer.parseInt( args[ i ].substring( j + 1 )));
					}
					catch( IndexOutOfBoundsException e1 ) {
						printException( e1, "-h" );
					}
					catch( NumberFormatException e1 ) {
						printException( e1, "-h" );
					}
					catch( IllegalArgumentException e1 ) {
						printException( e1, "-h" );
					}
				} else {
					System.out.println( "-h option requires address specification! (-h <hostName:port>)" );
				}
			
			} else if( arg.equals( "--help" )) {
				System.out.println( "SwingOSC command line options:\n\n" +
									"   -u <portNum>    at which UDP port the server shall receive.\n"+
									"                   may be omitted (a number is automatically\n"+
									"                   generated by the system).\n\n"+
									"   -t <portNum>    at which TCP port the server shall receive.\n"+
									"   -L              use the loopback address for reception.\n"+
									"                   this restricts access to the local machine.\n\n"+
									"   -b <size>       maximum size of OSC messages (default 65536).\n"+
									"   -i              initialize GUI environment upon startup,\n"+
									"                   i.e. create menubar and dock icon on mac os\n"+
									"   -h <host:port>  send /swing hello message to specified address" );
				
				return 0;
				
			} else {
				System.out.println( "Ignoring unknown option " + args[ i ] +
									"\nUse --help for a list of options." );
			}
		}
	
		new SwingOSC();
		if (!loopBack) {
			System.out.println("\n WARNING : your system is very vulnerable\n"
					+ "           when connected to a network!\n"
					+ "           using SwingOSC, it is easy to\n"
					+ "           hijack the machine or even erase\n"
					+ "           the hardisk.\n");
		}

		try {
			synchronized( instance ) {
				instance.start( protocol, port, loopBack, bufSize, initSwing, hello );
				instance.wait();
				return instance.quit();
			}
		}
		catch( InterruptedException e2 ) {
			System.out.println( e2.getClass().getName() + " : " + e2.getLocalizedMessage() );
		}
		catch( IOException e1 ) {
			printException( e1, instance.getClass().getName() );
		}
		return 1;
	}

	/*
	 * @synchronization needn't be called in event thread
	 */
    public SwingOSC()
	{
    	instance	= this;
//    	classLoaderMgr = new DynamicURLClassLoader( this.getClass().getClassLoader() );
    	classLoaderMgr = new URLClassLoaderManager( this.getClass().getClassLoader() );
    	
    	 // helper symbols
		constants.put( "null", null );
		constants.put( "brko", "[" );
		constants.put( "brkc", "]" );
		constants.put( "swing", this );
//		constants.put( "toolkit", Toolkit.getDefaultToolkit() );
		
		// OSC commands (they register themselves)
		new CmdQuit( this );
		new CmdNew();
		new CmdRef();
		new CmdLocal();
		new CmdGlobal();
		new CmdSet();
		new CmdGet();
		new CmdMethod();
		new CmdMethodR();
		new CmdField();
		new CmdFieldR();
		new CmdFree();
		new CmdArray();
		new CmdQuery();
		new CmdPrint();
		new CmdDumpOSC();
		new CmdClasses();
		
//		try {
//			final Class[] classes = AudioFileFormat.class.getClasses();
//			for( int i = 0; i < classes.length; i++ ) {
//				System.out.println( "i = "+i+"; name = "+classes[i].getName() );
//			}
//		}
//		catch( Exception e ) {
//			System.out.println( e );
//		}
		
		elm = new EventManager( this );
	}
    
    public static void setNumTraceLines( int num )
    {
    	numTraceLines = num;
    }
	
	public static int notNull( Object o )
	{
		return o == null ? 0 : 1;
	}
	
	public static SwingOSC getInstance()
	{
		return instance;
	}
	
	public SwingClient getCurrentClient()
	{
		return currentClient;
	}
	
	public OSCPacketCodec getDefaultCodec()
	{
		return OSCPacketCodec.getDefaultCodec();
	}
	
	public void setReplyAddress( SocketAddress addr )
	{
		currentClient.setReplyAddress( addr );
	}
	
	public void setReplyPort( int port )
	{
		currentClient.setReplyPort( port );
	}
	
	public double getVersion()
	{
		return VERSION;
	}
	
	public InetSocketAddress getLocalAddress()
	throws IOException
	{
		return serv.getLocalAddress();
	}
	
	public String getProtocol()
	{
		return serv.getProtocol();
	}
	
	public void send( OSCPacket p, SocketAddress addr )
	throws IOException
	{
		serv.send( p, addr );
	}
	
	public void start( String protocol, int port, boolean loopBack, int bufSize,
					   boolean initSwing, InetSocketAddress helloAddr )
	throws IOException
	{
		final InetSocketAddress	ourAddress;
		final String		 	ourHost;
		final int				ourPort;

		serv		= OSCServer.newUsing( protocol, port, loopBack );
		serv.setBufferSize( bufSize );
		ourAddress	= serv.getLocalAddress();
//		ourHost		= ourAddress.getHostName();
		ourHost		= ourAddress.getAddress().getHostAddress();
		ourPort		= ourAddress.getPort();
		serv.addOSCListener( this );
	
		final String vStr = String.valueOf( VERSION + 0.001 ); // use double prec!
		System.out.println( "SwingOSC v" + vStr.substring( 0, vStr.length() - 1 ) +". receiving " + protocol.toUpperCase() +
							" at address " + ourHost + ":" + ourPort );
//		serv.dumpIncomingOSC( 3, System.out );
		serv.getCodec().setSupportMode( (OSCPacketCodec.MODE_GRACEFUL & ~OSCPacketCodec.MODE_WRITE_DOUBLE_AS_FLOAT) | OSCPacketCodec.MODE_WRITE_DOUBLE );
		serv.start();
		if( initSwing ) {
			// calling one of AWT's method will make Mac OS X recognize we're
			// a GUI app and hence launch the screen menu bar and put an icon in the dock
			// ; since this takes a moment it can be useful to do it at startup
			// and not lazily when the first OSC message comes in
			EventQueue.invokeLater( new Runnable() { public void run() { /* empty */ }});
		}
		if( helloAddr != null ) {
//			serv.send( new OSCMessage( "/swing", new Object[] {
//					"hello", ourHost, new Integer( ourPort )}),
//					hello );
			final OSCMessage helloMsg = new OSCMessage( "/swing", new Object[] {
				"hello", ourHost, new Integer( ourPort ), protocol });
//			final OSCTransmitter helloTrns = OSCTransmitter.newUsing( OSCChannel.UDP );
			final OSCTransmitter helloTrns = OSCTransmitter.newUsing( OSCChannel.UDP, 0, helloAddr.getAddress().isLoopbackAddress() );
			helloTrns.connect();
			helloTrns.send( helloMsg, helloAddr );
			helloTrns.dispose();
		}
	}
	
	private int quit()
	{
		if( serv != null ) serv.dispose();
		return 0;
	}

	public static void printException( Throwable e, String opName )
	{
		System.out.println( opName + " : " + e.getClass().getName() + " : " + e.getLocalizedMessage() );
        final StackTraceElement[] trace = e.getStackTrace();
        for( int i = 0; i < Math.min( numTraceLines, trace.length ); i++ ) {
            System.out.println( "\tat " + trace[ i ]);
        }
        if( trace.length > 3 ) {
            System.out.println( "\t..." );
        }
	}

	public static void printException( Throwable e, OSCMessage msg )
	{
		printException( e, msg.getName() );
	}

	private static void printWarning( OSCMessage msg, String text )
	{
		System.out.println( "WARNING " + msg.getName() + " " + text );
	}

	protected static void printFailed( OSCMessage msg, String text )
	{
		System.out.println( "FAILURE " + msg.getName() + " " + text );
	}

	protected static void printArgMismatch( OSCMessage msg )
	{
		printFailed( msg, "Argument mismatch" );
	}

	protected static void printWrongArgCount( OSCMessage msg )
	{
		printWarning( msg, "Called with illegal arg count" );
	}

	protected static void printNotFound( OSCMessage msg, Object id )
	{
		printFailed( msg, "Object not found : " + id );
	}

	public void messageReceived( OSCMessage msg, SocketAddress addr, long when )
	{
//		collMessages.add( new IncomingMessage( msg, addr, when ));
//		EventQueue.invokeLater( this );
		elm.dispatchEvent( new IncomingMessage( msg, addr, when ));
	}

	// called from the event thread
	// when new messages have been queued
	public void processEvent( BasicEvent e )
	{
		final IncomingMessage	imsg = (IncomingMessage) e;
		SwingClient				c;
	
		c		= (SwingClient) mapClients.get( imsg.addr );
		if( c == null ) {
			c = new SwingClient( this, imsg.addr );
			mapClients.put( imsg.addr, c );
		}
		processMessage( imsg.msg, c );
	}

	protected Object[] decodeMessageArgs( OSCMessage msg, SwingClient c )
	throws IOException
	{
		final Object[] result = new Object[ msg.getArgCount() ];
		
		for( int idx = 0; idx < result.length; idx++ ) {
			result[ idx ] = decodeMessageArg( msg, c, idx );
//			o = msg.getArg( i );
//			if( o instanceof byte[] ) {
//				o = OSCPacket.decode( ByteBuffer.wrap( (byte[]) o), null );
//			}
//			if( o instanceof OSCMessage ) {
//				o = processMessage( (OSCMessage) o, addr );
//			} else if( o instanceof OSCBundle ) {
//				o = processBundle( (OSCBundle) o, addr );
//			}
//			result[ i ] = o;
//System.out.println( "result[ "+i+" ] = "+o );
		}
		
		return result;
	}

	protected Object decodeMessageArg( OSCMessage msg, SwingClient c, int idx )
	throws IOException
	{
		Object			o;
//		OSCPacketCodec	codec;
		
		o = msg.getArg( idx );
		if( o instanceof byte[] ) {
//			o = c.getCodec().decode( ByteBuffer.wrap( (byte[]) o) );
			o  = serv.getCodec( c.getReplyAddress() ).decode( ByteBuffer.wrap( (byte[]) o) );
		}
		if( o instanceof OSCMessage ) {
			o = processMessage( (OSCMessage) o, c );
		} else if( o instanceof OSCBundle ) {
			o = processBundle( (OSCBundle) o, c );
		}
		return o;
	}

	// best possible match : numArgs * 4
	protected static int checkMethodArgs( Class[] signature, Object[] msgArgs, int msgArgOff, Object[] result )
	{
		Class	type;
		Object	msgArg;
		int		match	= 0;
	
		for( int j = 0, k = msgArgOff; j < signature.length; j++, k++ ) {
			type		= signature[ j ];
			msgArg	= msgArgs[ k ];
			if( msgArg == null ) {
				result[ j ] = msgArg;
				match	   += 4;
			} else if( type.isPrimitive() ) {
				if( msgArg instanceof Number ) {
					if( type.equals( Boolean.TYPE )) {
						result[ j ] = new Boolean( ((Number) msgArg).intValue() != 0 );
						match += (msgArg instanceof Byte) ? 3 : ((msgArg instanceof Integer) ? 2 : 1);
					} else if( type.equals( Integer.TYPE )) {
						if( msgArg instanceof Integer ) {
							result[ j ] = msgArg;
							match += 4;
						} else {
							result[ j ] = new Integer( ((Number) msgArg).intValue() );
							match += ((msgArg instanceof Long) || (msgArg instanceof Byte) ||
									  (msgArg instanceof Short)) ? 3 : 1;
						}
					} else if( type.equals( Long.TYPE )) {
						if( msgArg instanceof Long ) {
							result[ j ] = msgArg;
							match += 4;
						} else {
							result[ j ] = new Long( ((Number) msgArg).longValue() );
							match += ((msgArg instanceof Integer) || (msgArg instanceof Byte) ||
									  (msgArg instanceof Short)) ? 3 : 1;
						}
					} else if( type.equals( Float.TYPE )) {
						if( msgArg instanceof Float ) {
							result[ j ] = msgArg;
							match += 4;
						} else {
							result[ j ] = new Float( ((Number) msgArg).floatValue() );
							match += (msgArg instanceof Double) ? 3 : 1;
						}
					} else if( type.equals( Double.TYPE )) {
						if( msgArg instanceof Double ) {
							result[ j ] = msgArg;
							match += 4;
						} else {
							result[ j ] = new Double( ((Number) msgArg).doubleValue() );
							match += (msgArg instanceof Float) ? 3 : 1;
						}
					} else if( type.equals( Byte.TYPE )) {
						if( msgArg instanceof Byte ) {
							result[ j ] = msgArg;
							match += 4;
						} else {
							result[ j ] = new Byte( ((Number) msgArg).byteValue() );
							match += ((msgArg instanceof Long) || (msgArg instanceof Integer) ||
									  (msgArg instanceof Short)) ? 3 : 1;
						}
					} else if( type.equals( Short.TYPE )) {
						if( msgArg instanceof Short ) {
							result[ j ] = msgArg;
							match += 4;
						} else {
							result[ j ] = new Short( ((Number) msgArg).shortValue() );
							match += ((msgArg instanceof Long) || (msgArg instanceof Integer) ||
									  (msgArg instanceof Byte)) ? 3 : 1;
						}
					} else {
						return -1;
					}
				} else if( type.equals( Boolean.TYPE )) {
					if( msgArg instanceof Boolean ) {
						result[ j ] = msgArg;
						match	   += 4;
					} else {
						return -1;
					}
				} else if( type.equals( Character.TYPE )) {
					if( (msgArg instanceof String) && ((String) msgArg).length() == 1 ) {
						result[ j ] = new Character( ((String) msgArg).charAt( 0 ));
						match += 3;
					} else {
						return -1;
					}
				}
			} else if( type.equals( String.class )) {
				if( msgArg instanceof String ) {
					result[ j ]	= msgArg;
					match      += 4;
				} else {
					result[ j ]	= msgArg.toString();
				}
			// direct assignment always preferred
			} else if( type.isAssignableFrom( msgArg.getClass() )) {
				result[ j ] = msgArg;
				match	   += type.equals( Object.class ) ? 2 : 4;
			} else { // not assignable
				return -1;
			}
		} // for signature types
		
		return match;
	}		

	protected Object getObject( Object id )
	{
		Object result	= null;
		
		result		= globals.get( id );
		if( result != null ) return result;
		result		= constants.get( id );
		if( result == null ) {
			final String className = id.toString();
			if( (className.length() > 0) &&
				(Character.isUpperCase( className.charAt( 0 )) || (className.indexOf( '.' ) >= 0))) {

				// ok, maybe it's a class reference
				try {
					result	= Class.forName( className, true, classLoaderMgr.getCurrentLoader() );
				}
				catch( LinkageError e ) {
					printException( e, "getObject" );
				}
				catch( ClassNotFoundException e ) {
					printException( e, "getObject" );
				}
			}
		}
		
		return result;
	}

	protected Object setProperty( OSCMessage msg, String propName, Object propValue, Object root )
	throws NoSuchFieldException, NoSuchMethodException, IllegalAccessException, InvocationTargetException
	{
		final int		subIdx			= propName.indexOf( '.' );
		final boolean	hasSub			= subIdx > 0;
		final String		setterName, getterSuffix;
		final Object[]	methodArgs;
		final Method[]	methods;
		final Object		o;
		final Class		c				=  root instanceof Class ? (Class) root : root.getClass();
		Method			m;
		Object			methodArg;
		Class[]			paramTypes;
		Class			paramClass;

		// ----------------- call getter first to find the target object -----------------
		if( hasSub ) {
			getterSuffix	= Character.toUpperCase( propName.charAt( 0 )) + propName.substring( 1, subIdx );
			try {
				m	= c.getMethod( "get" + getterSuffix, (Class[]) null );
			}
			catch( NoSuchMethodException e11 ) {	// ok, retry with "isXY"
				m	= c.getMethod( "is" + getterSuffix, (Class[]) null );
			}
			o = m.invoke( root, (Object[]) null );
			return setProperty( msg, propName.substring( subIdx + 1 ), propValue, o );

		// ----------------- find setter method and invoke it -----------------
		} else {
			if( propName.length() > 0 ) {
				setterName		= "set" + Character.toUpperCase( propName.charAt( 0 )) + propName.substring( 1 );
			} else {
				setterName		= "set";
			}
			methods				= c.getMethods();
			methodArgs			= new Object[ 1 ];
			for( int i = 0; i < methods.length; i++ ) {
				if( methods[ i ].getName().equals( setterName )) {
					paramTypes	= methods[ i ].getParameterTypes();
					methodArg	= null;
					if( (paramTypes.length == 1) && (propValue != null) ) {
						paramClass	= paramTypes[ 0 ];
						if( paramClass.isAssignableFrom( propValue.getClass() )) {
							methodArg = propValue;
						} else if( paramClass.equals( String.class )) {
							methodArg = propValue.toString();
						} else if( paramClass.isPrimitive() ) {
							if( paramClass.equals( Boolean.TYPE )) {
								if( propValue instanceof Boolean ) {
									methodArg = propValue;
								} else if( propValue instanceof Number ) {
									methodArg = new Boolean( ((Number) propValue).intValue() != 0 );
								}
							} else if( paramClass.equals( Integer.TYPE )) {
								if( propValue instanceof Number ) {
									methodArg = new Integer( ((Number) propValue).intValue() );
								}
							} else if( paramClass.equals( Long.TYPE )) {
								if( propValue instanceof Number ) {
									methodArg = new Long( ((Number) propValue).longValue() );
								}
							} else if( paramClass.equals( Float.TYPE )) {
								if( propValue instanceof Number ) {
									methodArg = new Float( ((Number) propValue).floatValue() );
								}
							} else if( paramClass.equals( Double.TYPE )) {
								if( propValue instanceof Number ) {
									methodArg = new Double( ((Number) propValue).doubleValue() );
								}
							} else if( paramClass.equals( Byte.TYPE )) {
								if( propValue instanceof Number ) {
									methodArg = new Byte( ((Number) propValue).byteValue() );
								}
							} else if( paramClass.equals( Short.TYPE )) {
								if( propValue instanceof Number ) {
									methodArg = new Short( ((Number) propValue).shortValue() );
								}
							} else if( paramClass.equals( Character.TYPE )) {
								if( propValue instanceof Character ) {
									methodArg = propValue;
								} else if( (propValue instanceof String) && ((String) propValue).length() == 1 ) {
									methodArg = new Character( ((String) propValue).charAt( 0 ));
								}
							}
						}
					}
					if( (methodArg != null) || (propValue == null) ) {
						methodArgs[ 0 ] = methodArg;
						return methods[ i ].invoke( root, methodArgs );
					}						
				}
			}
			printFailed( msg, "No matching setter method for " + propName );
			return null;
		}
	}

	protected Object getProperty( String propName, Object root )
	throws NoSuchMethodException, IllegalAccessException, InvocationTargetException
	{
		final int		subIdx			= propName.indexOf( '.' );
		final boolean	hasSub			= subIdx > 0;
		final String	getterSuffix	= Character.toUpperCase( propName.charAt( 0 )) + propName.substring( 1, hasSub ? subIdx : propName.length() );
		final Object	o;
		final Class		c				=  root instanceof Class ? (Class) root : root.getClass();
		Method			m;

		try {
			m	= c.getMethod( "get" + getterSuffix, (Class[]) null );
		}
		catch( NoSuchMethodException e11 ) {	// ok, retry with "isXY"
			m	= c.getMethod( "is" + getterSuffix, (Class[]) null );
		}
		
		o = m.invoke( root, (Object[]) null );
		
		if( hasSub ) {
			return getProperty( propName.substring( subIdx + 1 ), o );
		} else {
			if( o instanceof Boolean ) {
				return new Integer( ((Boolean) o).booleanValue() ? 1 : 0 );
			} else {
				return o;
			}
		}
	}

	protected Object getProperties( Object o, Object[] replyArgs, int replyOff,
									Object[] propNames, int propOff, int numArgs )
	throws NoSuchMethodException, IllegalAccessException, InvocationTargetException
	{
		Object	result		= null;
		String	propName;
	
		for( int i = 0; i < numArgs; i++ ) {
			propName					= propNames[ propOff++ ].toString();
			replyArgs[ replyOff++ ]	= propName;
			result					= getProperty( propName, o );
			replyArgs[ replyOff++ ]	= result;
		}
		
		return result;
	}

	protected Object invokeField( OSCMessage msg, Object o, String name )
	{
		final Class c = o instanceof Class ? (Class) o : o.getClass();

		try {
			return( c.getField( name ).get( o ));
		} catch( IllegalAccessException e ) {
			printException( e, msg );
		} catch( IllegalArgumentException e ) {
			printException( e, msg );
		} catch( NullPointerException e) {
			printException( e, msg );
		} catch( ExceptionInInitializerError e ) {
			printException( e, msg );
		} catch( NoSuchFieldException e ) {
			printException( e, msg );
		} catch( SecurityException e ) {
			printException( e, msg );
		}
		return null;
	}
	
	/**
	 * 	Fines a best matching method for a given argument set.
	 * 
	 *	@param 	o			the instance which will be searched for the method.
	 *						if o is a Class, a static method
	 *						will be looked up.
	 *	@param 	methodName	the name of the method to invoke.
	 *	@param 	methodArgs	the arguments to parse to the method invocation. according to
	 *						the heuristics used, a best matching method is tried to be found
	 *						by checking all methods with methodName and the given number
	 *						of arguments. For example, an Integer in methodArgs might be translated
	 *						into a primitive int, or even a float.
	 *	@return	the result of the invoked method.
	 *
	 *	@throws NoSuchMethodException	if no method could be found for the given set of arguments
	 */
	protected Method findBestMethod( Object o, String methodName, Object[] methodArgs, Object[] methodCArgs )
	throws NoSuchMethodException
	{
		final Class			c			= o instanceof Class ? (Class) o : o.getClass();
		final int			numArgs		= methodArgs.length;

		final Method[]		methods;
		Object[]			methodTArgs;
		Class[]				types;
		Method				method, bestMethod;
		int					match, bestMatch;
		final int			bestPossible;

		if( numArgs == 0 ) {	// the easy way
			return c.getMethod( methodName, (Class[]) null );
		} else {
			methods			= c.getMethods();
			methodTArgs		= new Object[ numArgs ];	// test converted args
			bestMatch		= -1;
			bestMethod		= null;
			bestPossible	= numArgs << 2;
			for( int i = 0; i < methods.length; i++ ) {
				method	= methods[ i ];
				if( !method.getName().equals( methodName )) continue;
				types	= method.getParameterTypes();
				if( types.length != numArgs ) continue;

				match	= checkMethodArgs( types, methodArgs, 0, methodTArgs );
				if( match > bestMatch ) {
					bestMatch	= match;
					bestMethod	= method;
					System.arraycopy( methodTArgs, 0, methodCArgs, 0, numArgs );
					if( bestMatch == bestPossible ) {
						break;
					}
//					if( i + 1 < methods.length ) methodCArgs = new Object[ numArgs ];
				}
			} // for methods

			if( bestMethod != null ) return bestMethod;
			else throw new NoSuchMethodException( "No matching method signature for " + methodName );
		}
	}
	
	/**
	 * 	Invokes a method by finding the best match for a given argument set.
	 * 
	 *	@param 	o			the object on which to call the method. the method must be
	 *						an instance method of o, or if o is a Class, a static method
	 *						will be looked up.
	 *	@param 	methodName	the name of the method to invoke.
	 *	@param 	methodArgs	the arguments to parse to the method invocation. according to
	 *						the heuristics used, a best matching method is tried to be found
	 *						by checking all methods with methodName and the given number
	 *						of arguments. For example, an Integer in methodArgs might be translated
	 *						into a primitive int, or even a float.
	 *	@return	the result of the invoked method.
	 *
	 *	@throws NoSuchMethodException	if no method could be found for the given set of arguments
	 *	@throws LinkageError
	 *	@throws SecurityException
	 *	@throws IllegalAccessException
	 *	@throws IllegalArgumentException
	 *	@throws InvocationTargetException
	 *	@throws ClassCastException
	 */
	protected Object invokeMethod( Object o, String methodName, Object[] methodArgs )
	throws	LinkageError, SecurityException, NoSuchMethodException, IllegalAccessException,
			IllegalArgumentException, InvocationTargetException, ClassCastException
	{
		final Object[] 		methodCArgs	= new Object[ methodArgs.length ];
		final Method 		method		= findBestMethod( o, methodName, methodArgs, methodCArgs );
		return method.invoke( o, methodCArgs );
	}
	
	protected Object invokeMethod( OSCMessage msg, Object o, String methodName, Object[] msgArgs )
	{
		try {
			return invokeMethod( o, methodName, msgArgs );
		}
		catch( LinkageError e ) {
			printException( e, msg );
		}
		catch( SecurityException e ) {
			printException( e, msg );
		}		
		catch( NoSuchMethodException e ) {
			printException( e, msg );
		}
		catch( IllegalAccessException e ) {
			printException( e, msg );
		}
		catch( IllegalArgumentException e ) {
			printException( e, msg );
		}
		catch( InvocationTargetException e ) {
			printException( e.getTargetException(), msg );
		}
		catch( ClassCastException e ) {
			printException( e, msg );
		}
		
		return null;
	}
	
	private Object processBundle( OSCBundle bndl, SwingClient c )
	{
		OSCPacket	p;
		Object		result = null;
		
		for( int j = 0; j < bndl.getPacketCount(); j++ ) {
			p = bndl.getPacket( j );
			if( p instanceof OSCMessage ) {
				result = processMessage( (OSCMessage) p, c );
			} else {
				result = processBundle( (OSCBundle) p, c );
			}
		}
		return result;
	}

//	 ------------------- OSCProcessor interface  -------------------

	// all messages are deferred to the swing event thread
	// for obvious reasons. also frees us from thread concurrency issues
	public Object processMessage( OSCMessage msg, SwingClient c )
	{
		try {	// catch everything because if a runtime exception occurs, OSC receiver thread dies
			final OSCProcessor cmd = (OSCProcessor) oscCmds.get( msg.getName() );

			currentClient = c;
			
			if( cmd != null ) {
				return cmd.processMessage( msg, c );
			} else {
				printFailed( msg, "Command not found" );
			}
		}
		catch( Exception e1 ) {
			printException( e1, "" );
		}
		return null;
	}

// ------------------- internal classes  -------------------

	private static class IncomingMessage
	extends BasicEvent
	{
		protected final OSCMessage		msg;
		protected final SocketAddress	addr;
		
		protected IncomingMessage( OSCMessage msg, SocketAddress addr, long when )
		{
			super( addr, 0, when );
			this.msg 	= resolveMessage( msg );
			this.addr	= addr;
		}
		
		public boolean incorporate( BasicEvent e ) { return false; }

		private static OSCMessage resolveMessage( OSCMessage msg )
		{
			Object o;
			
			for( int idx = 0; idx < msg.getArgCount(); idx++ ) {
				o = msg.getArg( idx );
				if( o.equals( "[" )) {
					final List newArgs = new ArrayList();
					for( int j = 0; j < idx; j++ ) {
						newArgs.add( msg.getArg( j ));
					}
					idx = resolveNested( msg, newArgs, idx );
					while( idx < msg.getArgCount() ) {
						o = msg.getArg( idx );
						if( o.equals( "[" )) {
							idx = resolveNested( msg, newArgs, idx );
						} else {
							newArgs.add( o );
							idx++;
						}
					}
					return new OSCMessage( msg.getName(), newArgs.toArray() );
				}
			}
			return msg;
		}
		
		private static int resolveNested( OSCMessage msg, List parentArgs, int idx )
		{
			idx++;	// skip opening bracket
			final List 		subArgs	= new ArrayList();
			final String	subName	= msg.getArg( idx++ ).toString();
			Object			o;
			do {
				o = msg.getArg( idx );
				if( o.equals( "[" )) {
					idx = resolveNested( msg, subArgs, idx );
				} else if( o.equals( "]" )) {
					parentArgs.add( new OSCMessage( subName, subArgs.toArray() ));
					idx++;
					return idx;
				} else {
					subArgs.add( o );
					idx++;
				}
			} while( true );
//			throw new ArrayIndexOutOfBoundsException( idx );
		}
	}

	private abstract class BasicCmd
	implements OSCProcessor
	{
		protected BasicCmd( String name )
		{
			oscCmds.put( name, this );
		}
	}
	
	/**
	 *	Command: /quit
	 */
	private class CmdQuit
	extends BasicCmd
	{
		private final Object sync;
		
		protected CmdQuit( Object sync )
		{
			super( "/quit" );
			this.sync = sync;
		}
		
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
			synchronized( sync ) {
				sync.notifyAll();
				return null;
			}
		}
	} // class CmdQuit
	
	/**
	 *	Command: /new, String <className>, [ Object <arg1> ... ]
	 */
	private class CmdNew
	extends BasicCmd
	{
		protected CmdNew()
		{
			super( "/new" );
		}

		/**
		 *	Note that message args are processed stricly after another
		 *	which means for example that the className statement, if it is an OSCMessage
		 *	and not a constant string, will not be able to access
		 *	object bindings made by later arguments.
		 *
		 *	@param	msg		the /new message
		 *	@param	c		the client
		 *	@return			the newly created object
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
			try {
	//			final Object[]		msgArgs		= decodeMessageArgs( msg, addr );
				final Object[]		msgArgs;
				final String			className;
				final Class			theClass;
				final int			numArgs		= msg.getArgCount() - 1;
				final Constructor[]	cons;
				Object[]				consArgs, bestArgs;
				Class[]				types;
				Constructor			bestCons;
				int					match, bestMatch;
				final int			bestPossible;
	
	//			if( msgArgs.length >= 1 ) {
				if( numArgs >= 0 ) {
					className	= decodeMessageArg( msg, c, 0 ).toString();
					theClass	= Class.forName( className, true, classLoaderMgr.getCurrentLoader() );
					if( numArgs == 0 ) {	// the easy way
						return theClass.newInstance();
					} else {
						cons			= theClass.getConstructors();
						consArgs		= new Object[ numArgs ];
						bestMatch	= -1;
						bestCons		= null;
						bestArgs		= null;
						bestPossible	= numArgs << 2;
						msgArgs		= decodeMessageArgs( msg, c );
						for( int i = 0; i < cons.length; i++ ) {
							types	= cons[ i ].getParameterTypes();
							if( types.length != numArgs ) continue;
							match	= checkMethodArgs( types, msgArgs, 1, consArgs );
	//System.out.print( "checking cons with " );
	//for( int jjj = 0; jjj < types.length; jjj++ ) {
	//	System.out.print( types[ jjj ].getName() + "; " );
	//}
	//System.out.println( " -> match "+ match );
							if( match > bestMatch ) {
								bestMatch	= match;
								bestCons	= cons[ i ];
								bestArgs	= consArgs;
								if( bestMatch == bestPossible ) {
	//								System.out.println( "... best possible" );
									break;
								}
								if( i + 1 < cons.length ) consArgs = new Object[ numArgs ];
							}
						} // for constructors
	
						if( bestCons != null ) {
							return bestCons.newInstance( bestArgs );
						}
					}
					
					printFailed( msg, "No matching constructor for " + className );
				} else {
					printArgMismatch( msg );
				}
			}
			catch( LinkageError e ) {
				printException( e, msg );
			}
			catch( ClassNotFoundException e ) {
				printException( e, msg );
			}
			catch( SecurityException e ) {
				printException( e, msg );
			}		
			catch( IllegalAccessException e ) {
				printException( e, msg );
			}
			catch( InstantiationException e ) {
				printException( e, msg );
			}
			catch( IllegalArgumentException e ) {
				printException( e, msg );
			}
			catch( InvocationTargetException e ) {
				printException( e.getTargetException(), msg );
			}
			catch( ClassCastException e ) {
				printException( e, msg );
			}
			
			return null;
		}
	} // class CmdNew

	/**
	 *	Command: /ref, String <objectID>
	 */
	private class CmdRef
	extends BasicCmd
	{
		protected CmdRef()
		{
			super( "/ref" );
		}
	
		/**
		 *	@param	msg		the /ref message
		 *	@param	c		the client
		 *	@return			the referred object
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
			if( msg.getArgCount() == 1 ) {
				return c.getObject( decodeMessageArg( msg, c, 0 ));
			} else {
				printArgMismatch( msg );
				return null;
			}
		}
	} // class CmdRef
	
	/**
	 *	Command: /local, [ String <objectID1>, Object <value1> ... ]
	 */
	private class CmdLocal
	extends BasicCmd
	{
		protected CmdLocal()
		{
			super( "/local" );
		}
		
		/**
		 *	Note that each message argument is parsed strictly after another
		 *	so that later arguments can make use of assignments made by earlier arguments.
		 *
		 *	@param	msg		the /local message
		 *	@param	c		the client
		 *	@return			the last objectID (for convenience)
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
	//		final Object[]	msgArgs	= decodeMessageArgs( msg, addr );
			final int		numArgs	= msg.getArgCount(); // msgArgs.length;
			Object			id		= null;
			Object			val;
	
			if( (numArgs & 1) != 0 ) {
				printWrongArgCount( msg );
			}
			
			for( int i = 0; i < numArgs; ) {
				id	= decodeMessageArg( msg, c, i++ );
				val	= decodeMessageArg( msg, c, i++ );
				c.locals.put( id, val );
			}
	
			return id;
	//		return numArgs < 2 ? null : msgArgs[ numArgs - 2 ];
		}
	} // class CmdLocal
	
	/**
	 *	Command: /global, [ String <objectID1>, Object <value1> ... ]
	 */
	private class CmdGlobal
	extends BasicCmd
	{
		protected CmdGlobal()
		{
			super( "/global" );
		}
		
		/**
		 *	Note that each message argument is parsed strictly after another
		 *	so that later arguments can make use of assignments made by earlier arguments.
		 *
		 *	@param	msg		the /global message
		 *	@param	c		the client
		 *	@return			the last objectID (for convenience)
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
	//		final Object[]	msgArgs	= decodeMessageArgs( msg, addr );
			final int		numArgs	= msg.getArgCount(); // msgArgs.length;
			Object			id		= null;
			Object			val;
	
			if( (numArgs & 1) != 0 ) {
				printWrongArgCount( msg );
			}
			
			for( int i = 0; i < numArgs; ) {
				id	= decodeMessageArg( msg, c, i++ );
				val	= decodeMessageArg( msg, c, i++ );
				globals.put( id, val );
			}
			
			return id;
	//		return numArgs < 2 ? null : msgArgs[ numArgs - 2 ];
		}
	} // class CmdGlobal

	/**
	 *	Command: /set String <objectID>, [ String <propertyName1>, Object <propertyValue1> ... ]
	 */
	private class CmdSet
	extends BasicCmd
	{
		protected CmdSet()
		{
			super( "/set" );
		}

		/**
		 *	Note that each message argument is parsed strictly after another
		 *	so that side effect statements in later arguments may assume that earlier properties 
		 *	have already been set.
		 *
		 *	@param	msg		the /set message
		 *	@param	c		the client
		 *	@return			the last value
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
	//		final Object[]		msgArgs		= decodeMessageArgs( msg, addr );
			final Object			id;
			final Object			o;
			int					numArgs		= msg.getArgCount(); // msgArgs.length;
			Object				result		= null;
			String				propName;
			Object				propValue;
	
			if( numArgs >= 1 ) {
				if( (numArgs & 1) == 0 ) {
					printWrongArgCount( msg );
					numArgs--;	// avoid array index exception
				}
	
				id	= decodeMessageArg( msg, c, 0 );
				o 	= c.getObject( id );
				if( o != null ) {
					for( int i = 1; i < numArgs; ) {
						propName		= decodeMessageArg( msg, c, i++ ).toString();
						propValue	= decodeMessageArg( msg, c, i++ );
						try {
							result	= setProperty( msg, propName, propValue, o );
						}
						catch( LinkageError e ) {
							printException( e, msg );
						}
						catch( SecurityException e ) {
							printException( e, msg );
						}		
						catch( NoSuchMethodException e ) {
							printException( e, msg );
						}
						catch( IllegalAccessException e ) {
							printException( e, msg );
						}
						catch( IllegalArgumentException e ) {
							printException( e, msg );
						}
						catch( InvocationTargetException e ) {
							printException( e.getTargetException(), msg );
						}
						catch( ClassCastException e ) {
							printException( e, msg );
						}
						catch( NoSuchFieldException e ) {
							printException( e, msg );
						}
					}
				} else {
					printNotFound( msg, id );
				}
			} else {
				printArgMismatch( msg );
			}
			return result;
		}
	} // class CmdSet

	/**
	 *	Command: /get String <objectID>, [ String <propertyName1>, ... ]
	 */
	private class CmdGet
	extends BasicCmd
	{
		protected CmdGet()
		{
			super( "/get" );
		}

		/**
		 *	The message args are processed strictly after another.
		 *
		 *	@param	msg		the /get message
		 *	@param	c		the client
		 *	@return			the last value
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
	//		final Object[]		msgArgs		= decodeMessageArgs( msg, addr );
			final Object			o;
			final Object			id;
			final Object[]		replyArgs;
			int					numArgs		= msg.getArgCount(); // msgArgs.length;
			Object				result		= null;
			String				propName;
	
			if( numArgs >= 1 ) {
				id	= decodeMessageArg( msg, c, 0 );
				o 	= c.getObject( id );
				if( o != null ) {
					replyArgs		= new Object[ (numArgs << 1) - 1 ];
					replyArgs[ 0 ]	= id;
					try {
						for( int replyOff = 1, propOff = 1; propOff < numArgs; propOff++ ) {
							propName	= decodeMessageArg( msg, c, propOff ).toString();
							replyArgs[ replyOff++ ]	= propName;
							result					= getProperty( propName, o );
							replyArgs[ replyOff++ ]	= result;
						}
						c.reply( new OSCMessage( "/set", replyArgs ));
					}
					catch( LinkageError e ) {
						printException( e, msg );
					}
					catch( SecurityException e ) {
						printException( e, msg );
					}		
					catch( NoSuchMethodException e ) {
						printException( e, msg );
					}
					catch( IllegalAccessException e ) {
						printException( e, msg );
					}
					catch( IllegalArgumentException e ) {
						printException( e, msg );
					}
					catch( InvocationTargetException e ) {
						printException( e.getTargetException(), msg );
					}
					catch( ClassCastException e ) {
						printException( e, msg );
					}
					
				} else {
					printNotFound( msg, id );
				}
			} else {
				printArgMismatch( msg );
			}
			return result;
		}
	} // class CmdGet

	/**
	 *	Command: /method, String <objectID>, String <methodName>, [ Object <arg1> ... ]
	 */
	private class CmdMethod
	extends BasicCmd
	{
		protected CmdMethod()
		{
			super( "/method" );
		}

		/**
		 *	@param	msg		the /method message
		 *	@param	c		the client
		 *	@return			the return value of the method
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
	//		final Object[] msgArgs = decodeMessageArgs( msg, addr );
			final Object[]	msgArgs;
			final int		numArgs = msg.getArgCount() - 2;
			final Object		id;
			final Object		o;
			final Object		name;
	
			if( numArgs >= 0 ) {
				id 	= decodeMessageArg( msg, c, 0 );
				o	= c.getObject( id );	// de-reference
				if( o != null ) {
					name = decodeMessageArg( msg, c, 1 );
					if( name != null ) {
						msgArgs = new Object[ numArgs ];
						for( int i = 0, j = 2; i < numArgs; i++, j++ ) {
							msgArgs[ i ] = decodeMessageArg( msg, c, j );
						}
						return invokeMethod( msg, o, name.toString(), msgArgs );
					} else {
						printFailed( msg, "Method name is null" );				
					}
				} else {
					printNotFound( msg, id );
				}
			} else {
				printArgMismatch( msg );
			}
			
			return null;
		}
	} // class CmdMethod

	/**
	 *	Command: /methodr, Object <object>, String <methodName>, [ Object <arg1> ... ]
	 */
	private class CmdMethodR
	extends BasicCmd
	{
		protected CmdMethodR()
		{
			super( "/methodr" );
		}

		/**
		 *	@param	msg		the /methodr message
		 *	@param	c		the client
		 *	@return			the return value of the method
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
	//		final Object[] msgArgs = decodeMessageArgs( msg, addr );
			final Object[]	msgArgs;
			final int		numArgs = msg.getArgCount() - 2;
			final Object		o;
			final Object		name;
	
			if( numArgs >= 0 ) {
				o = decodeMessageArg( msg, c, 0 );
				if( o != null ) {
					name = decodeMessageArg( msg, c, 1 );
					if( name != null ) {
						msgArgs = new Object[ numArgs ];
						for( int i = 0, j = 2; i < numArgs; i++, j++ ) {
							msgArgs[ i ] = decodeMessageArg( msg, c, j );
						}
						return invokeMethod( msg, o, name.toString(), msgArgs );
					} else {
						printFailed( msg, "Method name is null" );				
					}
				} else {
					printFailed( msg, "Cannot operate on null result" );
				}
			} else {
				printArgMismatch( msg );
			}
			
			return null;
		}
	} // class CmdMethodR

	/**
	 *	Command: /field, String <objectID>, String <fieldName>
	 */
	private class CmdField
	extends BasicCmd
	{
		protected CmdField()
		{
			super( "/field" );
		}

		/**
		 *	@param	msg		the /field message
		 *	@param	c		the client
		 *	@return			the value of the field
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
	//		final Object[]	msgArgs = decodeMessageArgs( msg, addr );
			final Object		o;
			final Object		id;
			final Object		name;
	
			if( msg.getArgCount() >= 2 ) {
				id	= decodeMessageArg( msg, c, 0 );
				o	= c.getObject( id );	// de-reference
				if( o != null ) {
					name = decodeMessageArg( msg, c, 1 );
					if( name != null ) {
						return invokeField( msg, o, name.toString() );
					} else {
						printFailed( msg, "Field name is null" );				
					}
				} else {
					printNotFound( msg, id );
				}
			} else {
				printArgMismatch( msg );
			}
			
			return null;
		}
	} // class CmdField
	
	/**
	 *	Command: /fieldr, Object <object>, String <fieldName>
	 */
	private class CmdFieldR
	extends BasicCmd
	{
		protected CmdFieldR()
		{
			super( "/fieldr" );
		}

		/**
		 *	@param	msg		the /fieldr message
		 *	@param	c		the client
		 *	@return			the value of the field
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
	//		final Object[]	msgArgs = decodeMessageArgs( msg, addr );
			final Object		o;
			final Object		name;
	
			if( msg.getArgCount() >= 2 ) {
				o	= decodeMessageArg( msg, c, 0 );
				if( o != null ) {
					name = decodeMessageArg( msg, c, 1 );
					if( name != null ) {
						return invokeField( msg, o, name.toString() );
					} else {
						printFailed( msg, "Field name is null" );				
					}
				} else {
					printFailed( msg, "Cannot operate on null result" );
				}
			} else {
				printArgMismatch( msg );
			}
			
			return null;
		}
	} // class CmdFieldR

	/**
	 *	Command: /free, [ String <objectID1>, ... ]
	 */
	private class CmdFree
	extends BasicCmd
	{
		protected CmdFree()
		{
			super( "/free" );
		}

		/**
		 *	Note that each argument is strictly parsed after another which
		 *	means that later statements cannot access earlier (now freed)
		 *	object bindings. That is:
		 *	<pre>
		 *	[ "/free", "schoko", [ "/method", "schoko", "getAnotherObjectID" ]]
		 *	</pre>
		 *	will fail.
		 *
		 *	@param	msg		the /free message
		 *	@param	c		the client
		 *	@return			the value of the last object in the list
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
			final int		numArgs	= msg.getArgCount(); // msgArgs.length;
			Object			id;
			Object			result	= null;
			
			for( int i = 0; i < numArgs; i++ ) {
				id		= decodeMessageArg( msg, c, i );
				result	= c.locals.remove( id );
				if( result == null ) {
					result	= globals.remove( id );
				}
				if( result == null ) {
					printNotFound( msg, id );
				}
			}
			return result;
		}
	} // class CmdFree

	/**
	 *	Command: /array, [ Object <object1>, ... ]
	 */
	private class CmdArray
	extends BasicCmd
	{
		protected CmdArray()
		{
			super( "/array" );
		}

		/**
		 *	@param	msg		the /array message
		 *	@param	c		the client
		 *	@return			the object array
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
			return decodeMessageArgs( msg, c );
		}
	} // class CmdArray

	/**
	 *	Command: /query, [ String <returnID1>, Object <anObject1> ... ]
	 */
	private class CmdQuery
	extends BasicCmd
	{
		protected CmdQuery()
		{
			super( "/query" );
		}

		/**
		 *	@param	msg		the /query message
		 *	@param	c		the client
		 *	@return			the last object's return value
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
			final Object[]	msgArgs		= decodeMessageArgs( msg, c );
			final int		numArgs		= msgArgs.length;
	
			if( (numArgs & 1) != 0 ) {
				printWrongArgCount( msg );
			}
	
			c.reply( new OSCMessage( "/info", msgArgs ));
			
			return numArgs == 0 ? null : msgArgs[ numArgs - 1 ];
		}
	} // class CmdQuery
	
	/**
	 *	Command: /print, [ String <objectID1>, ... ]
	 */
	private class CmdPrint
	extends BasicCmd
	{
		protected CmdPrint()
		{
			super( "/print" );
		}

		/**
		 *	Note that arguments are processed strictly after another.
		 *
		 *	@param	msg		the /print message
		 *	@param	c		the client
		 *	@return			the last object's value
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
	//		final Object[]	msgArgs	= decodeMessageArgs( msg, addr );
			final int		numArgs	= msg.getArgCount(); // msgArgs.length;
			Object			id;
			Object			o		= null;
			
			for( int i = 0; i < numArgs; i++ ) {
				id	= decodeMessageArg( msg, c, i );
				o	= c.getObject( id );
				System.out.println( id + " : " + o );
			}
			return o;
		}
	} // class CmdPrint
	
	/**
	 *	Command: /dumpOSC, int <incomingMode> [, int <outgoingMode> ]
	 */
	private class CmdDumpOSC
	extends BasicCmd
	{
		protected CmdDumpOSC()
		{
			super( "/dumpOSC" );
		}

		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
			final int numArgs	= msg.getArgCount();
		
			if( (numArgs >= 1) && (numArgs <= 2) && (msg.getArg( 0 ) instanceof Number) ) {
				serv.dumpIncomingOSC( ((Number) msg.getArg( 0 )).intValue(), System.out );
				if( numArgs == 2 ) {
					if( msg.getArg( 1 ) instanceof Number ) {
						serv.dumpOutgoingOSC( ((Number) msg.getArg( 1 )).intValue(), System.out );
					} else {
						printArgMismatch( msg );
					}
				}
			} else {
				printArgMismatch( msg );
			}
			
			return null;
		}
	} // class CmdDumpOSC

	/**
	 *	Command: /classes, String <cmd>, [ String <classPathURL1>, ... ]
	 *
	 *	where cmd is one of "add", "remove", "update"
	 */
	private class CmdClasses
	extends BasicCmd
	{
		protected CmdClasses()
		{
			super( "/classes" );
		}

		/**
		 *	@param	msg		the /classes message
		 *	@param	c		the client
		 *	@return			the last path
		 */
		public Object processMessage( OSCMessage msg, SwingClient c )
		throws IOException
		{
			final int		numArgs	= msg.getArgCount();
			final int		numURLs	= numArgs - 1;
			final URL[]		paths;
			final String	cmd;
			String			path	= null;
			
			if( numArgs > 0 ) {
				cmd	= decodeMessageArg( msg, c, 0 ).toString();
				paths = new URL[ numURLs ];
				for( int i = 0; i < numURLs; i++ ) {
					path		= decodeMessageArg( msg, c, i + 1 ).toString();
					paths[ i ]	= new URL( path );
				}
				if( cmd.equals( "add" )) {
					classLoaderMgr.addURLs( paths );
					return path;
				} else if( cmd.equals( "remove" )) {
					classLoaderMgr.removeURLs( paths );
					return path;
				} else if( cmd.equals( "update" )) {
					classLoaderMgr.removeURLs( paths );
					classLoaderMgr.addURLs( paths );
					return path;
				} else {
					printArgMismatch( msg );
				}
			} else {
				printArgMismatch( msg );
			}
			return null;
		}
	} // class CmdClasses
} // class SwingOSC