1
/* =========================================================================
3
* This file is part of Skycastle.
5
* Skycastle is free software; you can redistribute it and/or modify it
6
* under the terms of the GNU General Public License as published by the
7
* Free Software Foundation; either version 2 of the License, or (at your
8
* option) any later version.
10
* Skycastle is distributed in the hope that it will be useful, but WITHOUT
11
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15
* You should have received a copy of the GNU General Public License along
16
* with Skycastle; if not, write to the Free Software Foundation,
17
* Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
* ========================================================================= */
21
package org.skycastle.client.hardcoded;
23
import com.sun.sgs.client.ClientChannel;
24
import com.sun.sgs.client.ClientChannelListener;
25
import com.sun.sgs.client.SessionId;
26
import com.sun.sgs.client.simple.SimpleClient;
27
import com.sun.sgs.client.simple.SimpleClientListener;
29
import java.io.IOException;
30
import java.io.UnsupportedEncodingException;
31
import java.math.BigInteger;
32
import java.net.PasswordAuthentication;
33
import java.util.HashMap;
35
import java.util.Properties;
36
import java.util.Random;
37
import java.util.logging.Level;
38
import java.util.logging.Logger;
41
* Chat client. This client connects to the SGS Skycastle server and processes chat messages on a channel.
42
* Until we have implemented a real chat protocol, the client understands the following special messages.
46
* JOIN [sessionId] [nickname] - User with nickname and sessionId has joined the chat.
48
* QUIT [sessionId] [nickname] - User with nickname and sessionId has left the chat.
52
* NICK [newNickname] - User has changed nickname (TODO: this is very sloppy, with no protection whatsoever
53
* against duplicates).
57
* The channel-based chat has some issues since the communication is effectively client-to-client, even though
58
* the server acts as the hub for dispatching messages.. It is difficult to enforce any rules and protect
59
* clients from invalid messages. The state of the network may be difficult to keep consistent across clients
60
* (for example, when mapping sessions to nicknames. Each client has to do its own validity checking and
61
* enforce unique nicknames.). See also SkycastleServerListener.
63
public class ChatClient
64
implements SimpleClientListener, ClientChannelListener
67
//======================================================================
71
* The random number generator for login names.
73
private final Random random = new Random();
75
//======================================================================
79
* The {@link SimpleClient} instance for this client.
81
protected final SimpleClient simpleClient;
84
* Username. A placeholder until the system can handle actual user accounts.
86
protected String username = "";
89
* Map that associates a channel name with a {@link ClientChannel}.
91
protected final Map<String, ClientChannel> channelsByName =
92
new HashMap<String, ClientChannel>();
95
* Map that associates a session ID with a nickname.
97
protected final Map<SessionId, String> nicknamesById =
98
new HashMap<SessionId, String>();
101
* Handler for chat messages that should be displayed.
103
protected ChatOutputHandler outputHandler = null;
106
* Handler for chat user events.
108
protected ChatUserHandler userHandler;
110
//======================================================================
114
* The name of the general chat channel: '{@value #GENERAL_CHAT}'
116
public static final String GENERAL_CHAT = "General";
119
* The name of the host property.
121
public static final String HOST_PROPERTY = "skycastle.host";
124
* The default hostname.
126
public static final String DEFAULT_HOST = "localhost";
129
* The name of the port property.
131
public static final String PORT_PROPERTY = "skycastle.port";
136
public static final String DEFAULT_PORT = "1139";
139
* The message encoding.
141
public static final String MESSAGE_CHARSET = "UTF-8";
143
//======================================================================
147
* The version of the serialized form of this class.
149
private static final long serialVersionUID = 1L;
152
* The {@link Logger} for this class.
154
private static final Logger logger =
155
Logger.getLogger( ChatClient.class.getName() );
157
//======================================================================
160
//----------------------------------------------------------------------
164
* Create a new chat client.
168
simpleClient = new SimpleClient( this );
169
// TODO: Placeholder until we have something real.
170
setUsername( "guest-" + random.nextInt( 1000 ) );
175
* Create a new chat client.
177
* @param initOutputHandler Handler for chat messages to be displayed.
179
public ChatClient( ChatOutputHandler initOutputHandler,
180
ChatUserHandler initUserHandler )
183
outputHandler = initOutputHandler;
184
userHandler = initUserHandler;
187
//----------------------------------------------------------------------
188
// ClientChannelListener Implementation
193
* This is called when a message arrives on a channel.
195
public void receivedMessage( ClientChannel channel,
199
String msg = decodeString( message );
200
logger.log( Level.INFO, "Channel message: [" + channel.getName()
201
+ ", " + sender + "] " + msg );
202
if ( outputHandler != null )
204
if ( sender != null )
206
// Message from client.
207
/* Do some quick and dirty server message handling until we
208
have implemented a real chat protocol. */
209
String clientName = nicknamesById.get( sender );
210
if ( clientName != null )
214
/* ----- This doesn't work well yet ----- //
215
if (msg.startsWith("NICK"))
217
final String[] parts = msg.split(" ");
218
if (parts.length >= 2)
220
String newNick = parts[1];
221
nicknamesById.put(sender, newNick);
222
getChatOutputHandler().appendChatOutput(null,
223
clientName + " changed nickname to "
225
getChatUserHandler().removeChatUser(clientName);
226
getChatUserHandler().addChatUser(newNick);
230
getChatOutputHandler().appendChatOutput( clientName, msg );
234
getChatOutputHandler().appendChatOutput( null,
235
"UNNAMED-client: " + msg );
240
// Message from server.
241
receivedMessage( message );
250
* This is called when the user leaves a channel.
252
public void leftChannel( ClientChannel channel )
254
channelsByName.remove( channel.getName() );
255
logger.log( Level.INFO, "Left channel: " + channel.getName() );
256
// TODO: Let the user know.
259
//----------------------------------------------------------------------
260
// ServerSessionListener Implementation
265
* This is called when the user joins a channel.
267
public ClientChannelListener joinedChannel( ClientChannel channel )
269
channelsByName.put( channel.getName(), channel );
270
logger.log( Level.INFO, "Joined channel: " + channel.getName() );
271
// TODO: Let the user know.
279
* This is called when the client receives a message from the server.
281
public void receivedMessage( byte[] message )
283
String msg = decodeString( message );
284
logger.log( Level.INFO, "Message from server: " + msg );
285
/* Do some quick and dirty server message handling until we
286
have implemented a real chat protocol. */
287
if ( msg.startsWith( "JOIN" ) )
289
/* Let users know that a client has joined the chat.
290
JOIN <sessionId> <nickname> */
291
final String[] parts = msg.split( " " );
292
if ( parts.length >= 3 )
295
SessionId.fromBytes( decodeHexString( parts[ 1 ] ) );
296
nicknamesById.put( senderId, parts[ 2 ] );
297
getChatOutputHandler().appendChatOutput( null, parts[ 2 ]
298
+ " joined the chat." );
299
getChatUserHandler().addChatUser( parts[ 2 ] );
302
else if ( msg.startsWith( "QUIT" ) )
304
/* Let users know that a client has left the chat.
305
QUIT <sessionId> <nickname> */
306
final String[] parts = msg.split( " " );
307
if ( parts.length >= 3 )
310
SessionId.fromBytes( decodeHexString( parts[ 1 ] ) );
311
String victim = nicknamesById.get( senderId );
312
if ( victim != null )
314
nicknamesById.remove( senderId );
315
getChatOutputHandler().appendChatOutput( null,
316
victim + " left the chat." );
317
getChatUserHandler().removeChatUser( parts[ 2 ] );
323
getChatOutputHandler().appendChatOutput( null,
332
* This is called when reconnection is attempted.
334
public void reconnecting()
336
logger.log( Level.INFO, "Reconnecting." );
337
// TODO: Let the user know.
344
* This is called on a successful reconnect.
346
public void reconnected()
348
logger.log( Level.INFO, "Reconnected successfully." );
349
// TODO: Let the user know.
356
* This is called when the user is disconnected.
358
public void disconnected( boolean graceful, String reason )
360
logger.log( Level.INFO, "Disconnected (" + graceful + ", " + reason
362
// TODO: Let the user know.
365
//----------------------------------------------------------------------
366
// SimpleClientListener Implementation
371
* Returns dummy credentials where user is "guest-<random>" and the password is "guest."
373
public PasswordAuthentication getPasswordAuthentication()
375
logger.log( Level.INFO, "Logging in as " + username );
376
// TODO: Let the user know.
377
String password = "guest";
378
return new PasswordAuthentication( username, password.toCharArray() );
385
* This is called on a successful login.
387
public void loggedIn()
389
logger.log( Level.INFO, "Logged in successfully." );
390
// TODO: Let the user know.
397
* This is called on a failed login.
399
public void loginFailed( String reason )
401
logger.log( Level.WARNING, "Login failed (" + reason + ")!" );
402
// TODO: Let the user know.
405
//----------------------------------------------------------------------
406
// Other Public Methods
409
* Send a message to the general chat.
411
* @param message Message to be sent.
413
public void send( String message )
415
ClientChannel channel = channelsByName.get( GENERAL_CHAT );
416
if ( channel != null )
420
channel.send( encodeString( message ) );
422
catch ( IOException e )
424
logger.log( Level.WARNING, "Could not send message on channel: "
430
logger.log( Level.WARNING, "Client is not joined to channel: "
437
* Set the handler for chat output.
439
public void setChatOutputHandler( ChatOutputHandler handler )
441
outputHandler = handler;
446
* Get the handler for local chat output.
448
* @return Handler for local chat output.
450
public ChatOutputHandler getChatOutputHandler()
452
return outputHandler;
457
* Set the handler for chat user events.
459
public void setChatUserHandler( ChatUserHandler handler )
461
userHandler = handler;
466
* Get the handler for chat user events.
468
* @return Handler for chat user events.
470
public ChatUserHandler getChatUserHandler()
479
public String getUsername()
488
* @param newUsername Username.
490
public void setUsername( String newUsername )
492
username = newUsername;
495
//======================================================================
499
* Initiates asynchronous login to the SGS server specified by the host and port properties.
501
protected void login()
503
String host = System.getProperty( HOST_PROPERTY, DEFAULT_HOST );
504
String port = System.getProperty( PORT_PROPERTY, DEFAULT_PORT );
508
Properties connectProps = new Properties();
509
connectProps.put( "host", host );
510
connectProps.put( "port", port );
511
simpleClient.login( connectProps );
513
catch ( Exception e )
516
disconnected( false, e.getMessage() );
522
* Encodes a {@code String} into an array of bytes.
524
* @param s the string to encode
526
* @return the byte array which encodes the given string
528
protected static byte[] encodeString( String s )
532
return s.getBytes( MESSAGE_CHARSET );
534
catch ( UnsupportedEncodingException e )
536
throw new Error( "Required character set " + MESSAGE_CHARSET
543
* Decodes an array of bytes into a {@code String}.
545
* @param bytes the bytes to decode
547
* @return the decoded string
549
protected static String decodeString( byte[] bytes )
553
return new String( bytes, MESSAGE_CHARSET );
555
catch ( UnsupportedEncodingException e )
557
throw new Error( "Required character set " + MESSAGE_CHARSET
564
* Encode an array of bytes into a hexadecimal string.
566
* @param bytes The bytes to be encoded.
568
* @return The encoded string.
570
protected static String encodeHexString( byte[] bytes )
572
BigInteger bi = new BigInteger( bytes );
573
return bi.toString( 16 );
578
* Decode a hexadecimal string into an array of bytes.
580
* @param source The string to be decoded.
582
* @return The decoded string.
584
protected static byte[] decodeHexString( String source )
586
BigInteger bi = new BigInteger( source, 16 );
587
return bi.toByteArray();