~vcs-imports/skycastle/trunk

« back to all changes in this revision

Viewing changes to modules/client/src/main/java/org/skycastle/client/hardcoded/ChatClient.java

  • Committer: zzorn
  • Date: 2008-11-10 14:55:40 UTC
  • Revision ID: vcs-imports@canonical.com-20081110145540-l1hvmkbhnd0612ws
Fixed compile: removed packages of unused code, updated to use new Darkstar API, moved external library versions to the master pom.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
/* =========================================================================
2
 
 *
3
 
 * This file is part of Skycastle.
4
 
 * 
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.
9
 
 * 
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 
13
 
 * for more details.
14
 
 * 
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
18
 
 * 
19
 
 * ========================================================================= */
20
 
 
21
 
package org.skycastle.client.hardcoded;
22
 
 
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;
28
 
 
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;
34
 
import java.util.Map;
35
 
import java.util.Properties;
36
 
import java.util.Random;
37
 
import java.util.logging.Level;
38
 
import java.util.logging.Logger;
39
 
 
40
 
/**
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.
43
 
 * <p/>
44
 
 * From server:
45
 
 * <p/>
46
 
 * JOIN [sessionId] [nickname] - User with nickname and sessionId has joined the chat.
47
 
 * <p/>
48
 
 * QUIT [sessionId] [nickname] - User with nickname and sessionId has left the chat.
49
 
 * <p/>
50
 
 * From client:
51
 
 * <p/>
52
 
 * NICK [newNickname] - User has changed nickname (TODO: this is very sloppy, with no protection whatsoever
53
 
 * against duplicates).
54
 
 * <p/>
55
 
 * TODO:
56
 
 * <p/>
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.
62
 
 */
63
 
public class ChatClient
64
 
        implements SimpleClientListener, ClientChannelListener
65
 
{
66
 
 
67
 
    //======================================================================
68
 
    // Private Fields
69
 
 
70
 
    /**
71
 
     * The random number generator for login names.
72
 
     */
73
 
    private final Random random = new Random();
74
 
 
75
 
    //======================================================================
76
 
    // Non-Private Fields
77
 
 
78
 
    /**
79
 
     * The {@link SimpleClient} instance for this client.
80
 
     */
81
 
    protected final SimpleClient simpleClient;
82
 
 
83
 
    /**
84
 
     * Username. A placeholder until the system can handle actual user accounts.
85
 
     */
86
 
    protected String username = "";
87
 
 
88
 
    /**
89
 
     * Map that associates a channel name with a {@link ClientChannel}.
90
 
     */
91
 
    protected final Map<String, ClientChannel> channelsByName =
92
 
            new HashMap<String, ClientChannel>();
93
 
 
94
 
    /**
95
 
     * Map that associates a session ID with a nickname.
96
 
     */
97
 
    protected final Map<SessionId, String> nicknamesById =
98
 
            new HashMap<SessionId, String>();
99
 
 
100
 
    /**
101
 
     * Handler for chat messages that should be displayed.
102
 
     */
103
 
    protected ChatOutputHandler outputHandler = null;
104
 
 
105
 
    /**
106
 
     * Handler for chat user events.
107
 
     */
108
 
    protected ChatUserHandler userHandler;
109
 
 
110
 
    //======================================================================
111
 
    // Public Constants
112
 
 
113
 
    /**
114
 
     * The name of the general chat channel: '{@value #GENERAL_CHAT}'
115
 
     */
116
 
    public static final String GENERAL_CHAT = "General";
117
 
 
118
 
    /**
119
 
     * The name of the host property.
120
 
     */
121
 
    public static final String HOST_PROPERTY = "skycastle.host";
122
 
 
123
 
    /**
124
 
     * The default hostname.
125
 
     */
126
 
    public static final String DEFAULT_HOST = "localhost";
127
 
 
128
 
    /**
129
 
     * The name of the port property.
130
 
     */
131
 
    public static final String PORT_PROPERTY = "skycastle.port";
132
 
 
133
 
    /**
134
 
     * The default port.
135
 
     */
136
 
    public static final String DEFAULT_PORT = "1139";
137
 
 
138
 
    /**
139
 
     * The message encoding.
140
 
     */
141
 
    public static final String MESSAGE_CHARSET = "UTF-8";
142
 
 
143
 
    //======================================================================
144
 
    // Private Constants
145
 
 
146
 
    /**
147
 
     * The version of the serialized form of this class.
148
 
     */
149
 
    private static final long serialVersionUID = 1L;
150
 
 
151
 
    /**
152
 
     * The {@link Logger} for this class.
153
 
     */
154
 
    private static final Logger logger =
155
 
            Logger.getLogger( ChatClient.class.getName() );
156
 
 
157
 
    //======================================================================
158
 
    // Public Methods
159
 
 
160
 
    //----------------------------------------------------------------------
161
 
    // Constructors
162
 
 
163
 
    /**
164
 
     * Create a new chat client.
165
 
     */
166
 
    public ChatClient()
167
 
    {
168
 
        simpleClient = new SimpleClient( this );
169
 
        // TODO: Placeholder until we have something real.
170
 
        setUsername( "guest-" + random.nextInt( 1000 ) );
171
 
    }
172
 
 
173
 
 
174
 
    /**
175
 
     * Create a new chat client.
176
 
     *
177
 
     * @param initOutputHandler Handler for chat messages to be displayed.
178
 
     */
179
 
    public ChatClient( ChatOutputHandler initOutputHandler,
180
 
                       ChatUserHandler initUserHandler )
181
 
    {
182
 
        this();
183
 
        outputHandler = initOutputHandler;
184
 
        userHandler = initUserHandler;
185
 
    }
186
 
 
187
 
    //----------------------------------------------------------------------
188
 
    // ClientChannelListener Implementation
189
 
 
190
 
    /**
191
 
     * {@inheritDoc}
192
 
     * <p/>
193
 
     * This is called when a message arrives on a channel.
194
 
     */
195
 
    public void receivedMessage( ClientChannel channel,
196
 
                                 SessionId sender,
197
 
                                 byte[] message )
198
 
    {
199
 
        String msg = decodeString( message );
200
 
        logger.log( Level.INFO, "Channel message: [" + channel.getName()
201
 
                                + ", " + sender + "] " + msg );
202
 
        if ( outputHandler != null )
203
 
        {
204
 
            if ( sender != null )
205
 
            {
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 )
211
 
                {
212
 
                    /* Change nickname.
213
 
                       NICK <nickname> */
214
 
                    /* ----- This doesn't work well yet ----- //
215
 
                    if (msg.startsWith("NICK"))
216
 
                    {
217
 
                        final String[] parts = msg.split(" ");
218
 
                        if (parts.length >= 2)
219
 
                        {
220
 
                            String newNick = parts[1];
221
 
                            nicknamesById.put(sender, newNick);
222
 
                            getChatOutputHandler().appendChatOutput(null,
223
 
                                clientName + " changed nickname to "
224
 
                                + newNick + ".");
225
 
                            getChatUserHandler().removeChatUser(clientName);
226
 
                            getChatUserHandler().addChatUser(newNick);
227
 
                        }
228
 
                    } else
229
 
                    // ----- */
230
 
                    getChatOutputHandler().appendChatOutput( clientName, msg );
231
 
                }
232
 
                else
233
 
                {
234
 
                    getChatOutputHandler().appendChatOutput( null,
235
 
                                                             "UNNAMED-client: " + msg );
236
 
                }
237
 
            }
238
 
            else
239
 
            {
240
 
                // Message from server.
241
 
                receivedMessage( message );
242
 
            }
243
 
        }
244
 
    }
245
 
 
246
 
 
247
 
    /**
248
 
     * {@inheritDoc}
249
 
     * <p/>
250
 
     * This is called when the user leaves a channel.
251
 
     */
252
 
    public void leftChannel( ClientChannel channel )
253
 
    {
254
 
        channelsByName.remove( channel.getName() );
255
 
        logger.log( Level.INFO, "Left channel: " + channel.getName() );
256
 
        // TODO: Let the user know.
257
 
    }
258
 
 
259
 
    //----------------------------------------------------------------------
260
 
    // ServerSessionListener Implementation
261
 
 
262
 
    /**
263
 
     * {@inheritDoc}
264
 
     * <p/>
265
 
     * This is called when the user joins a channel.
266
 
     */
267
 
    public ClientChannelListener joinedChannel( ClientChannel channel )
268
 
    {
269
 
        channelsByName.put( channel.getName(), channel );
270
 
        logger.log( Level.INFO, "Joined channel: " + channel.getName() );
271
 
        // TODO: Let the user know.
272
 
        return this;
273
 
    }
274
 
 
275
 
 
276
 
    /**
277
 
     * {@inheritDoc}
278
 
     * <p/>
279
 
     * This is called when the client receives a message from the server.
280
 
     */
281
 
    public void receivedMessage( byte[] message )
282
 
    {
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" ) )
288
 
        {
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 )
293
 
            {
294
 
                SessionId senderId =
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 ] );
300
 
            }
301
 
        }
302
 
        else if ( msg.startsWith( "QUIT" ) )
303
 
        {
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 )
308
 
            {
309
 
                SessionId senderId =
310
 
                        SessionId.fromBytes( decodeHexString( parts[ 1 ] ) );
311
 
                String victim = nicknamesById.get( senderId );
312
 
                if ( victim != null )
313
 
                {
314
 
                    nicknamesById.remove( senderId );
315
 
                    getChatOutputHandler().appendChatOutput( null,
316
 
                                                             victim + " left the chat." );
317
 
                    getChatUserHandler().removeChatUser( parts[ 2 ] );
318
 
                }
319
 
            }
320
 
        }
321
 
        else
322
 
        {
323
 
            getChatOutputHandler().appendChatOutput( null,
324
 
                                                     "Server: " + msg );
325
 
        }
326
 
    }
327
 
 
328
 
 
329
 
    /**
330
 
     * {@inheritDoc}
331
 
     * <p/>
332
 
     * This is called when reconnection is attempted.
333
 
     */
334
 
    public void reconnecting()
335
 
    {
336
 
        logger.log( Level.INFO, "Reconnecting." );
337
 
        // TODO: Let the user know.
338
 
    }
339
 
 
340
 
 
341
 
    /**
342
 
     * {@inheritDoc}
343
 
     * <p/>
344
 
     * This is called on a successful reconnect.
345
 
     */
346
 
    public void reconnected()
347
 
    {
348
 
        logger.log( Level.INFO, "Reconnected successfully." );
349
 
        // TODO: Let the user know.
350
 
    }
351
 
 
352
 
 
353
 
    /**
354
 
     * {@inheritDoc}
355
 
     * <p/>
356
 
     * This is called when the user is disconnected.
357
 
     */
358
 
    public void disconnected( boolean graceful, String reason )
359
 
    {
360
 
        logger.log( Level.INFO, "Disconnected (" + graceful + ", " + reason
361
 
                                + ")." );
362
 
        // TODO: Let the user know.
363
 
    }
364
 
 
365
 
    //----------------------------------------------------------------------
366
 
    // SimpleClientListener Implementation
367
 
 
368
 
    /**
369
 
     * {@inheritDoc}
370
 
     * <p/>
371
 
     * Returns dummy credentials where user is "guest-&lt;random&gt;" and the password is "guest."
372
 
     */
373
 
    public PasswordAuthentication getPasswordAuthentication()
374
 
    {
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() );
379
 
    }
380
 
 
381
 
 
382
 
    /**
383
 
     * {@inheritDoc}
384
 
     * <p/>
385
 
     * This is called on a successful login.
386
 
     */
387
 
    public void loggedIn()
388
 
    {
389
 
        logger.log( Level.INFO, "Logged in successfully." );
390
 
        // TODO: Let the user know.
391
 
    }
392
 
 
393
 
 
394
 
    /**
395
 
     * {@inheritDoc}
396
 
     * <p/>
397
 
     * This is called on a failed login.
398
 
     */
399
 
    public void loginFailed( String reason )
400
 
    {
401
 
        logger.log( Level.WARNING, "Login failed (" + reason + ")!" );
402
 
        // TODO: Let the user know.
403
 
    }
404
 
 
405
 
    //----------------------------------------------------------------------
406
 
    // Other Public Methods
407
 
 
408
 
    /**
409
 
     * Send a message to the general chat.
410
 
     *
411
 
     * @param message Message to be sent.
412
 
     */
413
 
    public void send( String message )
414
 
    {
415
 
        ClientChannel channel = channelsByName.get( GENERAL_CHAT );
416
 
        if ( channel != null )
417
 
        {
418
 
            try
419
 
            {
420
 
                channel.send( encodeString( message ) );
421
 
            }
422
 
            catch ( IOException e )
423
 
            {
424
 
                logger.log( Level.WARNING, "Could not send message on channel: "
425
 
                                           + GENERAL_CHAT );
426
 
            }
427
 
        }
428
 
        else
429
 
        {
430
 
            logger.log( Level.WARNING, "Client is not joined to channel: "
431
 
                                       + GENERAL_CHAT );
432
 
        }
433
 
    }
434
 
 
435
 
 
436
 
    /**
437
 
     * Set the handler for chat output.
438
 
     */
439
 
    public void setChatOutputHandler( ChatOutputHandler handler )
440
 
    {
441
 
        outputHandler = handler;
442
 
    }
443
 
 
444
 
 
445
 
    /**
446
 
     * Get the handler for local chat output.
447
 
     *
448
 
     * @return Handler for local chat output.
449
 
     */
450
 
    public ChatOutputHandler getChatOutputHandler()
451
 
    {
452
 
        return outputHandler;
453
 
    }
454
 
 
455
 
 
456
 
    /**
457
 
     * Set the handler for chat user events.
458
 
     */
459
 
    public void setChatUserHandler( ChatUserHandler handler )
460
 
    {
461
 
        userHandler = handler;
462
 
    }
463
 
 
464
 
 
465
 
    /**
466
 
     * Get the handler for chat user events.
467
 
     *
468
 
     * @return Handler for chat user events.
469
 
     */
470
 
    public ChatUserHandler getChatUserHandler()
471
 
    {
472
 
        return userHandler;
473
 
    }
474
 
 
475
 
 
476
 
    /**
477
 
     * Get username.
478
 
     */
479
 
    public String getUsername()
480
 
    {
481
 
        return username;
482
 
    }
483
 
 
484
 
 
485
 
    /**
486
 
     * Set username.
487
 
     *
488
 
     * @param newUsername Username.
489
 
     */
490
 
    public void setUsername( String newUsername )
491
 
    {
492
 
        username = newUsername;
493
 
    }
494
 
 
495
 
    //======================================================================
496
 
    // Protected Methods
497
 
 
498
 
    /**
499
 
     * Initiates asynchronous login to the SGS server specified by the host and port properties.
500
 
     */
501
 
    protected void login()
502
 
    {
503
 
        String host = System.getProperty( HOST_PROPERTY, DEFAULT_HOST );
504
 
        String port = System.getProperty( PORT_PROPERTY, DEFAULT_PORT );
505
 
 
506
 
        try
507
 
        {
508
 
            Properties connectProps = new Properties();
509
 
            connectProps.put( "host", host );
510
 
            connectProps.put( "port", port );
511
 
            simpleClient.login( connectProps );
512
 
        }
513
 
        catch ( Exception e )
514
 
        {
515
 
            e.printStackTrace();
516
 
            disconnected( false, e.getMessage() );
517
 
        }
518
 
    }
519
 
 
520
 
 
521
 
    /**
522
 
     * Encodes a {@code String} into an array of bytes.
523
 
     *
524
 
     * @param s the string to encode
525
 
     *
526
 
     * @return the byte array which encodes the given string
527
 
     */
528
 
    protected static byte[] encodeString( String s )
529
 
    {
530
 
        try
531
 
        {
532
 
            return s.getBytes( MESSAGE_CHARSET );
533
 
        }
534
 
        catch ( UnsupportedEncodingException e )
535
 
        {
536
 
            throw new Error( "Required character set " + MESSAGE_CHARSET
537
 
                             + " not found", e );
538
 
        }
539
 
    }
540
 
 
541
 
 
542
 
    /**
543
 
     * Decodes an array of bytes into a {@code String}.
544
 
     *
545
 
     * @param bytes the bytes to decode
546
 
     *
547
 
     * @return the decoded string
548
 
     */
549
 
    protected static String decodeString( byte[] bytes )
550
 
    {
551
 
        try
552
 
        {
553
 
            return new String( bytes, MESSAGE_CHARSET );
554
 
        }
555
 
        catch ( UnsupportedEncodingException e )
556
 
        {
557
 
            throw new Error( "Required character set " + MESSAGE_CHARSET
558
 
                             + " not found", e );
559
 
        }
560
 
    }
561
 
 
562
 
 
563
 
    /**
564
 
     * Encode an array of bytes into a hexadecimal string.
565
 
     *
566
 
     * @param bytes The bytes to be encoded.
567
 
     *
568
 
     * @return The encoded string.
569
 
     */
570
 
    protected static String encodeHexString( byte[] bytes )
571
 
    {
572
 
        BigInteger bi = new BigInteger( bytes );
573
 
        return bi.toString( 16 );
574
 
    }
575
 
 
576
 
 
577
 
    /**
578
 
     * Decode a hexadecimal string into an array of bytes.
579
 
     *
580
 
     * @param source The string to be decoded.
581
 
     *
582
 
     * @return The decoded string.
583
 
     */
584
 
    protected static byte[] decodeHexString( String source )
585
 
    {
586
 
        BigInteger bi = new BigInteger( source, 16 );
587
 
        return bi.toByteArray();
588
 
    }
589
 
 
590
 
}