1/* -*- Mode: Java; tab-width: 4 -*-
2 *
3 * Copyright (c) 2004 Apple Computer, Inc. All rights reserved.
4 *
5 * Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple Computer, Inc.
6 * ("Apple") in consideration of your agreement to the following terms, and your
7 * use, installation, modification or redistribution of this Apple software
8 * constitutes acceptance of these terms.  If you do not agree with these terms,
9 * please do not use, install, modify or redistribute this Apple software.
10 *
11 * In consideration of your agreement to abide by the following terms, and subject
12 * to these terms, Apple grants you a personal, non-exclusive license, under Apple's
13 * copyrights in this original Apple software (the "Apple Software"), to use,
14 * reproduce, modify and redistribute the Apple Software, with or without
15 * modifications, in source and/or binary forms; provided that if you redistribute
16 * the Apple Software in its entirety and without modifications, you must retain
17 * this notice and the following text and disclaimers in all such redistributions of
18 * the Apple Software.  Neither the name, trademarks, service marks or logos of
19 * Apple Computer, Inc. may be used to endorse or promote products derived from the
20 * Apple Software without specific prior written permission from Apple.  Except as
21 * expressly stated in this notice, no other rights or licenses, express or implied,
22 * are granted by Apple herein, including but not limited to any patent rights that
23 * may be infringed by your derivative works or by other works in which the Apple
24 * Software may be incorporated.
25 *
26 * The Apple Software is provided by Apple on an "AS IS" basis.  APPLE MAKES NO
27 * WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED
28 * WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR
29 * PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND OPERATION ALONE OR IN
30 * COMBINATION WITH YOUR PRODUCTS.
31 *
32 * IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL OR
33 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
34 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
35 * ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION
36 * OF THE APPLE SOFTWARE, HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT
37 * (INCLUDING NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
38 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39
40	SimpleChat is a simple peer-to-peer chat program that demonstrates
41	DNS-SD registration, browsing, resolving and record-querying.
42
43	To do:
44	- implement better coloring algorithm
45 */
46
47
48import java.awt.*;
49import java.awt.event.*;
50import java.text.*;
51import java.net.*;
52import javax.swing.*;
53import javax.swing.event.*;
54import javax.swing.text.*;
55
56import com.apple.dnssd.*;
57
58
59class	SimpleChat implements	ResolveListener, RegisterListener, QueryListener,
60								ActionListener, ItemListener, Runnable
61{
62	Document			textDoc;			// Holds all the chat text
63	JTextField			inputField;			// Holds a pending chat response
64	String				ourName;			// name used to identify this user in chat
65	DNSSDService		browser;			// object that actively browses for other chat clients
66	DNSSDService		resolver;			// object that resolves other chat clients
67	DNSSDRegistration	registration;		// object that maintains our connection advertisement
68	JComboBox			targetPicker;		// Indicates who we're talking to
69	TargetListModel		targetList;			// and its list model
70	JButton				sendButton;			// Will send text in inputField to target
71	InetAddress			buddyAddr;			// and address
72	int					buddyPort;			// and port
73	DatagramPacket		dataPacket;			// Inbound data packet
74	DatagramSocket		outSocket;			// Outbound data socket
75	SimpleAttributeSet	textAttribs;
76
77	static final String	kChatExampleRegType = "_p2pchat._udp";
78	static final String	kWireCharSet = "ISO-8859-1";
79
80	public		SimpleChat() throws Exception
81	{
82		JFrame frame = new JFrame("SimpleChat");
83		frame.addWindowListener(new WindowAdapter() {
84			public void windowClosing(WindowEvent e) {System.exit(0);}
85		});
86
87    	ourName = System.getProperty( "user.name");
88		targetList = new TargetListModel();
89		textAttribs = new SimpleAttributeSet();
90		DatagramSocket inSocket = new DatagramSocket();
91		dataPacket = new DatagramPacket( new byte[ 4096], 4096);
92		outSocket = new DatagramSocket();
93
94		this.setupSubPanes( frame.getContentPane(), frame.getRootPane());
95		frame.pack();
96		frame.setVisible(true);
97		inputField.requestFocusInWindow();
98
99		browser = DNSSD.browse( 0, 0, kChatExampleRegType, "", new SwingBrowseListener( targetList));
100
101		registration = DNSSD.register( 0, 0, ourName, kChatExampleRegType, "", "", inSocket.getLocalPort(), null, this);
102
103		new ListenerThread( this, inSocket, dataPacket).start();
104	}
105
106	protected void	setupSubPanes( Container parent, JRootPane rootPane)
107	{
108		parent.setLayout( new BoxLayout( parent, BoxLayout.Y_AXIS));
109
110		JPanel textRow = new JPanel();
111		textRow.setLayout( new BoxLayout( textRow, BoxLayout.X_AXIS));
112		textRow.add( Box.createRigidArea( new Dimension( 16, 0)));
113		JEditorPane textPane = new JEditorPane( "text/html", "<BR>");
114		textPane.setPreferredSize( new Dimension( 400, 300));
115		textPane.setEditable( false);
116		JScrollPane textScroller = new JScrollPane( textPane);
117		textRow.add( textScroller);
118		textRow.add( Box.createRigidArea( new Dimension( 16, 0)));
119		textDoc = textPane.getDocument();
120
121		JPanel addressRow = new JPanel();
122		addressRow.setLayout( new BoxLayout( addressRow, BoxLayout.X_AXIS));
123		targetPicker = new JComboBox( targetList);
124		targetPicker.addItemListener( this);
125		addressRow.add( Box.createRigidArea( new Dimension( 16, 0)));
126		addressRow.add( new JLabel( "Talk to: "));
127		addressRow.add( targetPicker);
128		addressRow.add( Box.createHorizontalGlue());
129
130		JPanel buttonRow = new JPanel();
131		buttonRow.setLayout( new BoxLayout( buttonRow, BoxLayout.X_AXIS));
132		buttonRow.add( Box.createRigidArea( new Dimension( 16, 0)));
133		inputField = new JTextField();
134		// prevent inputField from hijacking <Enter> key
135		inputField.getKeymap().removeKeyStrokeBinding( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0));
136		buttonRow.add( inputField);
137		sendButton = new JButton( "Send");
138		buttonRow.add( Box.createRigidArea( new Dimension( 8, 0)));
139		buttonRow.add( sendButton);
140		buttonRow.add( Box.createRigidArea( new Dimension( 16, 0)));
141		rootPane.setDefaultButton( sendButton);
142		sendButton.addActionListener( this);
143		sendButton.setEnabled( false);
144
145		parent.add( Box.createRigidArea( new Dimension( 0, 16)));
146		parent.add( textRow);
147		parent.add( Box.createRigidArea( new Dimension( 0, 8)));
148		parent.add( addressRow);
149		parent.add( Box.createRigidArea( new Dimension( 0, 8)));
150		parent.add( buttonRow);
151		parent.add( Box.createRigidArea( new Dimension( 0, 16)));
152	}
153
154	public void	serviceRegistered( DNSSDRegistration registration, int flags,
155									String serviceName, String regType, String domain)
156	{
157		ourName = serviceName;		// might have been renamed on collision
158	}
159
160	public void	operationFailed( DNSSDService service, int errorCode)
161	{
162		System.out.println( "Service reported error " + String.valueOf( errorCode));
163	}
164
165	public void	serviceResolved( DNSSDService resolver, int flags, int ifIndex, String fullName,
166										String hostName, int port, TXTRecord txtRecord)
167	{
168		buddyPort = port;
169		try {
170			// Start a record query to obtain IP address from hostname
171			DNSSD.queryRecord( 0, ifIndex, hostName, 1 /* ns_t_a */, 1 /* ns_c_in */,
172								new SwingQueryListener( this));
173		}
174		catch ( Exception e) { terminateWithException( e); }
175		resolver.stop();
176	}
177
178	public void	queryAnswered( DNSSDService query, int flags, int ifIndex, String fullName,
179									int rrtype, int rrclass, byte[] rdata, int ttl)
180	{
181		try {
182			buddyAddr = InetAddress.getByAddress( rdata);
183		}
184		catch ( Exception e) { terminateWithException( e); }
185		sendButton.setEnabled( true);
186	}
187
188	public void		actionPerformed( ActionEvent e) 	// invoked when Send button is hit
189	{
190		try
191		{
192			String	sendString = ourName + ": " + inputField.getText();
193			byte[] sendData = sendString.getBytes( kWireCharSet);
194			outSocket.send( new DatagramPacket( sendData, sendData.length, buddyAddr, buddyPort));
195			StyleConstants.setForeground( textAttribs, Color.black);
196			textDoc.insertString( textDoc.getLength(), inputField.getText() + "\n", textAttribs);
197			inputField.setText( "");
198		}
199		catch ( Exception exception) { terminateWithException( exception); }
200	}
201
202	public void		itemStateChanged( ItemEvent e) 	// invoked when Target selection changes
203	{
204		sendButton.setEnabled( false);
205		if ( e.getStateChange() == ItemEvent.SELECTED)
206		{
207			try {
208				TargetListElem	sel = (TargetListElem) targetList.getSelectedItem();
209				resolver = DNSSD.resolve( 0, sel.fInt, sel.fServiceName, sel.fType, sel.fDomain, this);
210			}
211			catch ( Exception exception) { terminateWithException( exception); }
212		}
213	}
214
215	public void		run()		// invoked on event thread when inbound packet arrives
216	{
217		try
218		{
219			String	inMessage = new String( dataPacket.getData(), 0, dataPacket.getLength(), kWireCharSet);
220			StyleConstants.setForeground( textAttribs, this.getColorFor( dataPacket.getData(), dataPacket.getLength()));
221			textDoc.insertString( textDoc.getLength(), inMessage + "\n", textAttribs);
222		}
223		catch ( Exception e) { terminateWithException( e); }
224	}
225
226	protected Color		getColorFor( byte[] chars, int length)
227	// Produce a mapping from a string to a color, suitable for text display
228	{
229		int		rgb = 0;
230		for ( int i=0; i < length && chars[i] != ':'; i++)
231			rgb = rgb ^ ( (int) chars[i] << (i%3+2) * 8);
232		return new Color( rgb & 0x007F7FFF);	// mask off high bits so it is a dark color
233
234//		for ( int i=0; i < length && chars[i] != ':'; i++)
235
236	}
237
238	protected static void	terminateWithException( Exception e)
239	{
240		e.printStackTrace();
241		System.exit( -1);
242	}
243
244    public static void main(String s[])
245    {
246    	try {
247			new SimpleChat();
248		}
249		catch ( Exception e) { terminateWithException( e); }
250    }
251}
252
253
254
255class	TargetListElem
256{
257	public	TargetListElem( String serviceName, String domain, String type, int ifIndex)
258	{ fServiceName = serviceName; fDomain = domain; fType = type; fInt = ifIndex; }
259
260	public String	toString() { return fServiceName; }
261
262	public String	fServiceName, fDomain, fType;
263	public int		fInt;
264}
265
266class		TargetListModel extends DefaultComboBoxModel implements BrowseListener
267{
268	/* The Browser invokes this callback when a service is discovered. */
269	public void	serviceFound( DNSSDService browser, int flags, int ifIndex,
270							String serviceName, String regType, String domain)
271	{
272		TargetListElem		match = this.findMatching( serviceName);	// probably doesn't handle near-duplicates well.
273
274		if ( match == null)
275			this.addElement( new TargetListElem( serviceName, domain, regType, ifIndex));
276	}
277
278	/* The Browser invokes this callback when a service disappears. */
279	public void	serviceLost( DNSSDService browser, int flags, int ifIndex,
280							String serviceName, String regType, String domain)
281	{
282		TargetListElem		match = this.findMatching( serviceName);	// probably doesn't handle near-duplicates well.
283
284		if ( match != null)
285			this.removeElement( match);
286	}
287
288	/* The Browser invokes this callback when a service disappears. */
289	public void	operationFailed( DNSSDService service, int errorCode)
290	{
291		System.out.println( "Service reported error " + String.valueOf( errorCode));
292	}
293
294	protected TargetListElem	findMatching( String match)
295	{
296		for ( int i = 0; i < this.getSize(); i++)
297			if ( match.equals( this.getElementAt( i).toString()))
298				return (TargetListElem) this.getElementAt( i);
299		return null;
300	}
301
302}
303
304
305// A ListenerThread runs its owner when datagram packet p appears on socket s.
306class	ListenerThread extends Thread
307{
308	public			ListenerThread( Runnable owner, DatagramSocket s, DatagramPacket p)
309	{ fOwner = owner; fSocket = s; fPacket = p; }
310
311	public void		run()
312	{
313		while ( true )
314		{
315			try
316			{
317				fSocket.receive( fPacket);
318				SwingUtilities.invokeAndWait( fOwner);	// process data on main thread
319			}
320			catch( Exception e)
321			{
322				break;	// terminate thread
323			}
324		}
325	}
326
327	protected Runnable			fOwner;
328	protected DatagramSocket	fSocket;
329	protected DatagramPacket	fPacket;
330}
331
332
333
334