content: /* -*- Mode: Java; tab-width: 4 -*-
   *
   * Copyright (c) 2004 Apple Computer, Inc. All rights reserved.
   *
   * Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple Computer, Inc.
   * ("Apple") in consideration of your agreement to the following terms, and your
   * use, installation, modification or redistribution of this Apple software
   * constitutes acceptance of these terms.  If you do not agree with these terms,
   * please do not use, install, modify or redistribute this Apple software.
   *
   * In consideration of your agreement to abide by the following terms, and subject
   * to these terms, Apple grants you a personal, non-exclusive license, under Apple's
   * copyrights in this original Apple software (the "Apple Software"), to use,
   * reproduce, modify and redistribute the Apple Software, with or without
   * modifications, in source and/or binary forms; provided that if you redistribute
   * the Apple Software in its entirety and without modifications, you must retain
   * this notice and the following text and disclaimers in all such redistributions of
   * the Apple Software.  Neither the name, trademarks, service marks or logos of
   * Apple Computer, Inc. may be used to endorse or promote products derived from the
   * Apple Software without specific prior written permission from Apple.  Except as
   * expressly stated in this notice, no other rights or licenses, express or implied,
   * are granted by Apple herein, including but not limited to any patent rights that
   * may be infringed by your derivative works or by other works in which the Apple
   * Software may be incorporated.
   *
   * The Apple Software is provided by Apple on an "AS IS" basis.  APPLE MAKES NO
   * WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
   * WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
   * PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
   * COMBINATION WITH YOUR PRODUCTS.
   *
   * IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
   * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
   * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
   * ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
   * OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT
   * (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  
  	SimpleChat is a simple peer-to-peer chat program that demonstrates 
  	DNS-SD registration, browsing, resolving and record-querying.
  
  	To do:
  	- implement better coloring algorithm
   */
  
  
  import java.awt.*;
  import java.awt.event.*;
  import java.text.*;
  import java.net.*;
  import javax.swing.*;
  import javax.swing.event.*;
  import javax.swing.text.*;
  
  import com.apple.dnssd.*;
  
  
  class	SimpleChat implements	ResolveListener, RegisterListener, QueryListener, 
  								ActionListener, ItemListener, Runnable
  {
  	Document			textDoc;			// Holds all the chat text
  	JTextField			inputField;			// Holds a pending chat response
  	String				ourName;			// name used to identify this user in chat
  	DNSSDService		browser;			// object that actively browses for other chat clients
  	DNSSDService		resolver;			// object that resolves other chat clients
  	DNSSDRegistration	registration;		// object that maintains our connection advertisement
  	JComboBox			targetPicker;		// Indicates who we're talking to
  	TargetListModel		targetList;			// and its list model
  	JButton				sendButton;			// Will send text in inputField to target
  	InetAddress			buddyAddr;			// and address
  	int					buddyPort;			// and port
  	DatagramPacket		dataPacket;			// Inbound data packet
  	DatagramSocket		outSocket;			// Outbound data socket
  	SimpleAttributeSet	textAttribs;
  
  	static final String	kChatExampleRegType = "_p2pchat._udp";
  	static final String	kWireCharSet = "ISO-8859-1";
  
  	public		SimpleChat() throws Exception
  	{
  		JFrame frame = new JFrame("SimpleChat");
  		frame.addWindowListener(new WindowAdapter() {
  			public void windowClosing(WindowEvent e) {System.exit(0);}
  		});
  
      	ourName = System.getProperty( "user.name");
  		targetList = new TargetListModel();
  		textAttribs = new SimpleAttributeSet();
  		DatagramSocket inSocket = new DatagramSocket();
  		dataPacket = new DatagramPacket( new byte[ 4096], 4096);
  		outSocket = new DatagramSocket();
  
  		this.setupSubPanes( frame.getContentPane(), frame.getRootPane());
  		frame.pack();
  		frame.setVisible(true);
  		inputField.requestFocusInWindow();
  
  		browser = DNSSD.browse( 0, 0, kChatExampleRegType, "", new SwingBrowseListener( targetList));
  
  		registration = DNSSD.register( 0, 0, ourName, kChatExampleRegType, "", "", inSocket.getLocalPort(), null, this);
  
  		new ListenerThread( this, inSocket, dataPacket).start();
  	}
  
  	protected void	setupSubPanes( Container parent, JRootPane rootPane)
  	{	
  		parent.setLayout( new BoxLayout( parent, BoxLayout.Y_AXIS));
  
  		JPanel textRow = new JPanel();
  		textRow.setLayout( new BoxLayout( textRow, BoxLayout.X_AXIS));
  		textRow.add( Box.createRigidArea( new Dimension( 16, 0)));
  		JEditorPane textPane = new JEditorPane( "text/html", "<BR>");
  		textPane.setPreferredSize( new Dimension( 400, 300));
  		textPane.setEditable( false);
  		JScrollPane textScroller = new JScrollPane( textPane);
  		textRow.add( textScroller);
  		textRow.add( Box.createRigidArea( new Dimension( 16, 0)));
  		textDoc = textPane.getDocument();
  
  		JPanel addressRow = new JPanel();
  		addressRow.setLayout( new BoxLayout( addressRow, BoxLayout.X_AXIS));
  		targetPicker = new JComboBox( targetList);
  		targetPicker.addItemListener( this);
  		addressRow.add( Box.createRigidArea( new Dimension( 16, 0)));
  		addressRow.add( new JLabel( "Talk to: "));
  		addressRow.add( targetPicker);
  		addressRow.add( Box.createHorizontalGlue());
  
  		JPanel buttonRow = new JPanel();
  		buttonRow.setLayout( new BoxLayout( buttonRow, BoxLayout.X_AXIS));
  		buttonRow.add( Box.createRigidArea( new Dimension( 16, 0)));
  		inputField = new JTextField();
  		// prevent inputField from hijacking <Enter> key
  		inputField.getKeymap().removeKeyStrokeBinding( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0));
  		buttonRow.add( inputField);
  		sendButton = new JButton( "Send");
  		buttonRow.add( Box.createRigidArea( new Dimension( 8, 0)));
  		buttonRow.add( sendButton);
  		buttonRow.add( Box.createRigidArea( new Dimension( 16, 0)));
  		rootPane.setDefaultButton( sendButton);
  		sendButton.addActionListener( this);
  		sendButton.setEnabled( false);
  
  		parent.add( Box.createRigidArea( new Dimension( 0, 16)));
  		parent.add( textRow);
  		parent.add( Box.createRigidArea( new Dimension( 0, 8)));
  		parent.add( addressRow);
  		parent.add( Box.createRigidArea( new Dimension( 0, 8)));
  		parent.add( buttonRow);
  		parent.add( Box.createRigidArea( new Dimension( 0, 16)));
  	}
  
  	public void	serviceRegistered( DNSSDRegistration registration, int flags, 
  									String serviceName, String regType, String domain)
  	{
  		ourName = serviceName;		// might have been renamed on collision
  	}
  
  	public void	operationFailed( DNSSDService service, int errorCode)
  	{
  		System.out.println( "Service reported error " + String.valueOf( errorCode));
  	}
  
  	public void	serviceResolved( DNSSDService resolver, int flags, int ifIndex, String fullName, 
  										String hostName, int port, TXTRecord txtRecord)
  	{
  		buddyPort = port;
  		try {
  			// Start a record query to obtain IP address from hostname
  			DNSSD.queryRecord( 0, ifIndex, hostName, 1 /* ns_t_a */, 1 /* ns_c_in */, 
  								new SwingQueryListener( this));
  		}
  		catch ( Exception e) { terminateWithException( e); }
  		resolver.stop();
  	}
  
  	public void	queryAnswered( DNSSDService query, int flags, int ifIndex, String fullName, 
  									int rrtype, int rrclass, byte[] rdata, int ttl)
  	{
  		try {
  			buddyAddr = InetAddress.getByAddress( rdata);
  		}
  		catch ( Exception e) { terminateWithException( e); }
  		sendButton.setEnabled( true);
  	}
  
  	public void		actionPerformed( ActionEvent e) 	// invoked when Send button is hit
  	{
  		try
  		{
  			String	sendString = ourName + ": " + inputField.getText();
  			byte[] sendData = sendString.getBytes( kWireCharSet);
  			outSocket.send( new DatagramPacket( sendData, sendData.length, buddyAddr, buddyPort));
  			StyleConstants.setForeground( textAttribs, Color.black);
  			textDoc.insertString( textDoc.getLength(), inputField.getText() + "\n", textAttribs);
  			inputField.setText( "");
  		}
  		catch ( Exception exception) { terminateWithException( exception); }
  	}
  
  	public void		itemStateChanged( ItemEvent e) 	// invoked when Target selection changes
  	{
  		sendButton.setEnabled( false);
  		if ( e.getStateChange() == ItemEvent.SELECTED)
  		{
  			try {
  				TargetListElem	sel = (TargetListElem) targetList.getSelectedItem();
  				resolver = DNSSD.resolve( 0, sel.fInt, sel.fServiceName, sel.fType, sel.fDomain, this);
  			}
  			catch ( Exception exception) { terminateWithException( exception); }
  		}
  	}
  
  	public void		run()		// invoked on event thread when inbound packet arrives
  	{
  		try
  		{
  			String	inMessage = new String( dataPacket.getData(), 0, dataPacket.getLength(), kWireCharSet);
  			StyleConstants.setForeground( textAttribs, this.getColorFor( dataPacket.getData(), dataPacket.getLength()));
  			textDoc.insertString( textDoc.getLength(), inMessage + "\n", textAttribs);
  		}
  		catch ( Exception e) { terminateWithException( e); }
  	}
  
  	protected Color		getColorFor( byte[] chars, int length)
  	// Produce a mapping from a string to a color, suitable for text display
  	{
  		int		rgb = 0;
  		for ( int i=0; i < length && chars[i] != ':'; i++)
  			rgb = rgb ^ ( (int) chars[i] << (i%3+2) * 8);
  		return new Color( rgb & 0x007F7FFF);	// mask off high bits so it is a dark color
  		
  //		for ( int i=0; i < length && chars[i] != ':'; i++)
  		
  	}
  
  	protected static void	terminateWithException( Exception e)
  	{
  		e.printStackTrace();
  		System.exit( -1);
  	}
  
      public static void main(String s[]) 
      {
      	try {
  			new SimpleChat();
  		}
  		catch ( Exception e) { terminateWithException( e); }
      }
  }
  
  
  
  class	TargetListElem
  {
  	public	TargetListElem( String serviceName, String domain, String type, int ifIndex)
  	{ fServiceName = serviceName; fDomain = domain; fType = type; fInt = ifIndex; }
  	
  	public String	toString() { return fServiceName; }
  	
  	public String	fServiceName, fDomain, fType;
  	public int		fInt;
  }
  
  class		TargetListModel extends DefaultComboBoxModel implements BrowseListener
  {
  	/* The Browser invokes this callback when a service is discovered. */
  	public void	serviceFound( DNSSDService browser, int flags, int ifIndex, 
  							String serviceName, String regType, String domain)
  	{
  		TargetListElem		match = this.findMatching( serviceName);	// probably doesn't handle near-duplicates well.
  
  		if ( match == null)
  			this.addElement( new TargetListElem( serviceName, domain, regType, ifIndex));
  	}
  
  	/* The Browser invokes this callback when a service disappears. */
  	public void	serviceLost( DNSSDService browser, int flags, int ifIndex, 
  							String serviceName, String regType, String domain)
  	{
  		TargetListElem		match = this.findMatching( serviceName);	// probably doesn't handle near-duplicates well.
  
  		if ( match != null)
  			this.removeElement( match);
  	}
  
  	/* The Browser invokes this callback when a service disappears. */
  	public void	operationFailed( DNSSDService service, int errorCode)
  	{
  		System.out.println( "Service reported error " + String.valueOf( errorCode));
  	}
  
  	protected TargetListElem	findMatching( String match)
  	{
  		for ( int i = 0; i < this.getSize(); i++)
  			if ( match.equals( this.getElementAt( i).toString()))
  				return (TargetListElem) this.getElementAt( i);
  		return null;
  	}
  
  }
  
  
  // A ListenerThread runs its owner when datagram packet p appears on socket s.
  class	ListenerThread extends Thread
  {
  	public			ListenerThread( Runnable owner, DatagramSocket s, DatagramPacket p) 
  	{ fOwner = owner; fSocket = s; fPacket = p; }
  
  	public void		run()
  	{		
  		while ( true )
  		{
  			try 
  			{
  				fSocket.receive( fPacket);
  				SwingUtilities.invokeAndWait( fOwner);	// process data on main thread
  			}
  			catch( Exception e)
  			{
  				break;	// terminate thread
  			}
  		}
  	}
  
  	protected Runnable			fOwner;
  	protected DatagramSocket	fSocket;
  	protected DatagramPacket	fPacket;
  }
  
  
  
  