001 // edu.isi.gamebots.clients.GamebotsClient 002 // Copyright 2000, University of Southern California, 003 // Information Science Institute 004 // 005 // Personal and Educational use is hereby granted. 006 // Permission required for commercial use and redistribution. 007 008 009 package edu.isi.gamebots.client; 010 011 import java.lang.*; 012 import java.io.*; 013 import java.net.*; 014 import java.util.*; 015 016 import us.ca.la.anm.util.io.*; 017 018 019 /** 020 * This class is a JavaBean client for the Gamebots server. It encapsulates 021 * the lowest level network connection and provides an EventListener interface 022 * {@link GamebotsClient.Listener} to handle incoming server messages. 023 * 024 * @author <a href="mailto:amarshal#gamebots@isi.edu">Andrew n marshall</a> 025 */ 026 027 public class GamebotsClient implements GamebotsConstants { 028 // Private Static Data 029 /////////////////////////////////////////////////////////////////////////// 030 /** 031 * Length of the socket read buffer. Used by {@link #parseMessage}. 032 */ 033 protected final static int BUFFER_LENGTH = 256; 034 protected final static int MAX_WAIT = 10000; // in milliseconds 035 036 // Private Data 037 /////////////////////////////////////////////////////////////////////////// 038 039 /** 040 * The client's logging mechanism. 041 */ 042 private DebugLog log = PrintWriterLog.DEFAULT; 043 /** 044 * Toggles whether sent messages are logged. 045 * Set by (@link #setLogOutput}. Used by {@link #sendMessage}. 046 */ 047 private boolean logOutput = false; 048 049 /** 050 * Gamebots server's internet address. 051 * Must be initialized via {@link #setServerAddress}. 052 */ 053 private InetAddress serverAddress = null; 054 /** 055 * Gamebots server's port number. 056 * Must be initialized via {@link #setServerPort}. 057 */ 058 private int serverPort = DEFAULT_BOT_PORT; 059 060 /** 061 * Synchronization lock for manipulating the connection. 062 */ 063 protected final Object connectionLock = new Object(); 064 /** 065 * The network connection socket. It remains null when not connected. 066 */ 067 protected Socket socket = null; 068 /** 069 * Thread used to handle incoming messages from the server. 070 */ 071 protected Thread inputThread = null; 072 073 /** 074 * Synchronization lock for manipulating the outgoing connection. 075 */ 076 protected final Object outputLock = new Object(); 077 /** 078 * The outgoing wruter . It remains null when not connected. 079 */ 080 protected PrintWriter output = null; 081 082 /** 083 * List of registered {@link GamebotsClient.Listener}s. 084 */ 085 protected List listeners = new ArrayList(); 086 087 /** 088 * Time of the last received message. 089 */ 090 protected long lastMessageTime = -1; 091 092 /** 093 * A timer used to detect dropped connections. 094 */ 095 protected Timer pingTimer = new Timer(); 096 097 /** 098 * The current PingTask on the {@link #pingTimer} detect dropped connections. 099 */ 100 protected PingTask pingTask = null; 101 102 103 // Public Methods 104 /////////////////////////////////////////////////////////////////////////// 105 /** 106 * Sets the {@link DebugLog} used to log events. If never set, events will 107 * be logged to {@link PrintWriterLog#DEFAULT}. 108 */ 109 public void setLog( DebugLog log ) { 110 this.log = log; 111 } 112 113 /** 114 * @return true if outgoing messages are being logged. 115 */ 116 public boolean getLogOutput() { 117 return logOutput; 118 } 119 120 /** 121 * Sets whether outgoing messages are logged. 122 * 123 * @see #setLog 124 */ 125 public void setLogOutput( boolean logOutput ) { 126 this.logOutput = logOutput; 127 } 128 129 /** 130 * Gets the timestamp of the last message received from the server. If the 131 * client is not connected or no message has been received, it returns -1. 132 */ 133 public long getLastMessageReceivedTimeStamp() { 134 return lastMessageTime; 135 } 136 137 /** 138 * Registers a {@link GamebotsClient.Listener} with this client. 139 * Does nothing if the listener is already registered. 140 */ 141 public void addListener( Listener listener ) { 142 if( listener == null ) 143 throw new IllegalArgumentException( "listener cannot be null" ); 144 if( !listeners.contains( listener ) ) 145 listeners.add( listener ); 146 } 147 148 /** 149 * Unregisters {@link GamebotsClient.Listener listener} with this client. 150 * Does nothing if the listener is not currently registered. 151 */ 152 public void removeListener( Listener listener ) { 153 listeners.remove( listener ); 154 } 155 156 /** 157 * @return the internet address of the Gamebots server. If unset, it returns null. 158 */ 159 public InetAddress getServerAddress() { 160 return serverAddress; 161 } 162 163 /** 164 * Sets the internet address of the Gamebots server. If the client is already 165 * connected, the connection is broken before setting the server. 166 * 167 * @throws IllegalArgumentException if serverAddress is null. 168 */ 169 public void setServerAddress( InetAddress serverAddress ) { 170 if( serverAddress == null ) 171 throw new IllegalArgumentException(); 172 if( serverAddress.equals( this.serverAddress ) ) 173 return; 174 synchronized( connectionLock ) { 175 if( isConnected() ) 176 disconnect(); 177 178 this.serverAddress = serverAddress; 179 } 180 } 181 182 /** 183 * @return the network port of the Gamebots server. 184 * Defaults to {@link GamebotsConstants#DEFAULT_BOT_PORT}. 185 */ 186 public int getServerPort() { 187 return serverPort; 188 } 189 190 /** 191 * Sets the network port of the Gamebots server. If the client is already 192 * connected, the connection is broken before setting the server. 193 * 194 * @throws IllegalStateException is client is currently connected. 195 * @throws IllegalArgumentException if port <= 0. 196 */ 197 public void setServerPort( int port ) { 198 if( port <= 0 ) 199 throw new IllegalArgumentException(); 200 if( port == this.serverPort ) 201 return; 202 synchronized( connectionLock ) { 203 if( isConnected() ) 204 disconnect(); 205 206 this.serverPort = port; 207 } 208 } 209 210 /** 211 * @return whether the client is currently connected. 212 */ 213 public boolean isConnected() { 214 return inputThread != null; 215 } 216 217 /** 218 * Initiates a connection to the server. If the client is already connected, 219 * it does nothing. 220 * 221 * @throws IOException if socket cannot be created. 222 * @throws IllegalStateException if the server address is not set. 223 */ 224 public void connect() throws IOException { 225 try { 226 synchronized( connectionLock ) { 227 if( !isConnected() ) { 228 if( serverAddress == null ) 229 throw new IllegalStateException( "Server address not set" ); 230 231 socket = new Socket( serverAddress, serverPort ); 232 inputThread = new Thread( new Runnable() { 233 public void run() { handleSocket( socket ); } } ); 234 inputThread.start(); 235 236 synchronized( outputLock ) { 237 output = new PrintWriter( socket.getOutputStream() ); 238 } 239 } 240 } // end sync 241 fireConnected(); 242 } catch( IOException error ) { 243 log.logError( "Cannot connect to server "+serverAddress.toString()+":"+serverPort, error ); 244 } 245 } 246 247 /** 248 * Sends a string message to the server 249 */ 250 public boolean sendMessage(String msg){ 251 synchronized(outputLock){ 252 if(output == null) return false; 253 output.println( msg ); 254 if( output.checkError() ) 255 disconnect(); 256 } 257 if( logOutput ) 258 log.logNote( "SENT: "+msg ); 259 return true; 260 } 261 262 /** 263 * Sends a message to the server with the given type and properties. May 264 * 265 * @throws IllegalArgumentException if type is null or empty. 266 * 267 * @see #setLogOutput 268 */ 269 public boolean sendMessage( String type, Properties properties ) { 270 if( type == null ) 271 throw new IllegalArgumentException( "Message type is null" ); 272 if( type.equals("") ) 273 throw new IllegalArgumentException( "Message type is empty" ); 274 275 StringBuffer buffer; 276 277 synchronized( outputLock ) { 278 if( output == null ) 279 return false; // Not connected anymore.... 280 281 buffer = new StringBuffer( type ); 282 if( properties != null ) { 283 java.util.Map.Entry entry; 284 Iterator i = properties.entrySet().iterator(); 285 while( i.hasNext() ) { 286 buffer.append( ' ' ); 287 entry = (java.util.Map.Entry) i.next(); 288 289 buffer.append( " {"+entry.getKey()+" "+entry.getValue()+"}" ); 290 } 291 } 292 293 output.write( buffer.toString() ); 294 output.write("\r\n"); 295 output.flush(); 296 if( output.checkError() ) 297 disconnect(); 298 299 } // end sync 300 if( logOutput ) 301 log.logNote( "SENT: "+buffer ); 302 303 return true; 304 } 305 306 public void ping() { 307 if( pingTask != null ) 308 return; // ignore 309 310 sendMessage( PING, null ); 311 pingTimer.schedule( pingTask = new PingTask(), MAX_WAIT ); 312 } 313 314 /** 315 * Disconnects the client from the server. If the client is not connected, 316 * it does nothing. Any errors thrown by socket.close() are caught and 317 * logged. 318 */ 319 public void disconnect() { 320 synchronized( connectionLock ) { 321 if( isConnected() ) { 322 Socket oldSocket = socket; 323 if( oldSocket != null ) { 324 synchronized( outputLock ) { 325 output = null; 326 } 327 Thread oldThread = inputThread; 328 socket = null; 329 long startWaitTime = System.currentTimeMillis(); 330 while( inputThread != null && 331 (System.currentTimeMillis() - startWaitTime < MAX_WAIT ) ) { // 10 sec wait 332 try { 333 oldThread.interrupt(); 334 connectionLock.wait( 500 ); 335 } catch( InterruptedException error ) { 336 // do nothing 337 } 338 } 339 try { 340 oldSocket.shutdownOutput(); 341 } catch( IOException error ) { 342 log.logError( "Cannot close socket output", error ); 343 } 344 try { 345 oldSocket.close(); 346 } catch( IOException error ) { 347 log.logError( "Cannot close socket", error ); 348 } 349 } 350 } 351 } // end sync 352 353 fireDisconnected(); 354 } 355 356 // Private Methods 357 /////////////////////////////////////////////////////////////////////////// 358 359 /** 360 * Manages incoming network stream, and sends incoming Messages to 361 * registered {@link Listener}s. 362 */ 363 protected void handleSocket( Socket socket ) { 364 Message message; 365 MessageBlock syncMessage; 366 Iterator i; 367 BufferedReader in = null; 368 try { 369 in = new BufferedReader( new InputStreamReader( socket.getInputStream() ) ); 370 371 MESSAGE_LOOP: while( socket == this.socket ) { 372 message = parseMessage( in ); 373 if( message == null ) 374 break MESSAGE_LOOP; 375 376 lastMessageTime = System.currentTimeMillis(); 377 if( pingTask != null ) { 378 pingTask.cancel(); 379 pingTask = null; 380 } 381 382 i = listeners.iterator(); 383 if( message instanceof MessageBlock ) { 384 syncMessage = (MessageBlock) message; 385 while( i.hasNext() ) 386 ((Listener) i.next()).receivedSyncMessage( syncMessage ); 387 } else { 388 while( i.hasNext() ) 389 ((Listener) i.next()).receivedAsyncMessage( message ); 390 391 // Check to see if this is the last message 392 if( message.getType().equalsIgnoreCase( FINISHED ) ) 393 break MESSAGE_LOOP; 394 } 395 Thread.currentThread().yield(); 396 } 397 } catch( IOException error ) { 398 fireReceivedSocketError( error ); 399 } catch( RuntimeException error ) { 400 fireReceivedSocketError( error ); 401 } finally { 402 try { 403 if( in != null ) 404 in.close(); 405 } catch ( IOException error ) { 406 // ignore... 407 } 408 try { 409 socket.close(); 410 } catch ( IOException error ) { 411 // ignore... 412 } 413 } 414 415 synchronized( connectionLock ) { 416 inputThread = null; 417 418 connectionLock.notifyAll(); 419 } 420 } 421 422 /** 423 * Builds {@link Message} objects out of the PushbackReader in. 424 * 425 * @throws IOException caused by in.read(...) 426 */ 427 protected Message parseMessage( Reader in ) throws IOException { 428 char[] buffer = new char[BUFFER_LENGTH]; 429 int length; 430 StringBuffer type = new StringBuffer(); 431 StringBuffer name = new StringBuffer(); 432 StringBuffer value = new StringBuffer(); 433 Properties props = new Properties(); 434 435 int c = in.read(); 436 if( c == -1 ) 437 return null; // End of stream 438 439 boolean complete = false; 440 READ_BLOCK: { 441 // Parse message type-name 442 while( c != ' ' ) { 443 if( c != '\r' && c != '\0' ) 444 type.append( (char) c ); 445 c = in.read(); 446 if( c == -1 ) 447 break READ_BLOCK; // End of stream, incomplete 448 } 449 450 // Parse name-value pairs in the message. 451 c = in.read(); 452 if( c == -1 ) 453 break READ_BLOCK; // End of stream, incomplete 454 while( c != '\n' ) { 455 if( c == '{' ) { 456 length = name.length(); 457 if( length > 0 ) 458 name.delete( 0, length ); 459 c = in.read(); 460 if( c == -1 ) 461 break READ_BLOCK; // End of stream, incomplete 462 while( c != ' ' && c != '\n' ) { 463 if( c != '\r' && c != '\0' ) 464 name.append( (char) c ); 465 c = in.read(); 466 if( c == -1 ) 467 break READ_BLOCK; // End of stream, incomplete 468 } 469 470 if( c != '\n' ) { 471 c = in.read(); 472 if( c == -1 ) 473 break READ_BLOCK; // End of stream, incomplete 474 } 475 476 length = value.length(); 477 if( length > 0 ) 478 value.delete( 0, length ); 479 while( c != '}' && c != '\n' ) { 480 if( c != '\r' && c != '\0' ) 481 value.append( (char) c ); 482 c = in.read(); 483 if( c == -1 ) 484 break READ_BLOCK; // End of stream, incomplete 485 } 486 487 props.setProperty( name.toString(), value.toString() ); 488 } 489 490 c = in.read(); 491 if( c == -1 ) 492 break READ_BLOCK; // End of stream, incomplete 493 } 494 complete = true; 495 } // end READ_BLOCK 496 497 String typeStr = type.toString(); 498 Message message; 499 if( typeStr.equals(BEG) ) { 500 List messages = new LinkedList(); 501 message = parseMessage( in ); 502 MESSAGE_LOOP: { 503 while( !message.getType().equals( END ) ) { 504 messages.add( message ); 505 if( !message.isComplete() ) { 506 complete = false; 507 break MESSAGE_LOOP; 508 } 509 message = parseMessage( in ); 510 if( message == null ) { 511 complete = false; 512 break MESSAGE_LOOP; 513 } 514 } 515 messages.add( message ); // END message 516 } 517 message = new MessageBlock( this, typeStr, props, messages, complete ); 518 } else { 519 message = new Message( this, typeStr, props, complete ); 520 } 521 522 return message; 523 } 524 525 protected void fireConnected() { 526 final String methodName = "fireConected()"; 527 528 Iterator i = listeners.iterator(); 529 while( i.hasNext() ) { 530 try { 531 ((Listener) i.next()).connected(); 532 } catch( RuntimeException error ) { 533 log.logError( "Error during "+methodName, error ); 534 } 535 } 536 } 537 538 /** 539 * Passes given network error to all registered {@link Listener}s. 540 */ 541 protected void fireReceivedSocketError( Throwable error ) { 542 final String methodName = "fireReceivedSocketError(Throwable)"; 543 544 Iterator i = listeners.iterator(); 545 while( i.hasNext() ) { 546 try { 547 ((Listener) i.next()).receivedError( error ); 548 } catch( RuntimeException newError ) { 549 log.logError( "Error during "+methodName, newError ); 550 } 551 } 552 } 553 554 protected void fireDisconnected() { 555 final String methodName = "fireDisconected()"; 556 557 Iterator i = listeners.iterator(); 558 while( i.hasNext() ) { 559 try { 560 ((Listener) i.next()).disconnected(); 561 } catch( RuntimeException error ) { 562 log.logError( "Error during "+methodName, error ); 563 } 564 } 565 } 566 567 // Inner Classes 568 /////////////////////////////////////////////////////////////////////////// 569 570 /** 571 * Interface for listening to a {@link GamebotsClient}. 572 */ 573 public static interface Listener extends EventListener { 574 /** 575 * Notifies the Listener that the {@link GamebotsClient client} has been 576 * connected to the server. 577 */ 578 public void connected(); 579 580 /** 581 * Notifies the Listener of a new asynchronous message. 582 */ 583 public void receivedAsyncMessage( Message message ); 584 585 /** 586 * Notifies the Listener of a new synchronous message, also know as a 587 * vision block. 588 */ 589 public void receivedSyncMessage( MessageBlock message ); 590 591 /** 592 * Notifies the Listener of any error that occured. 593 */ 594 public void receivedError( Throwable error ); 595 596 /** 597 * Notifies the Listener that the {@link GamebotsClient client} has been 598 * disconnected from the server. 599 */ 600 public void disconnected(); 601 } 602 603 /** 604 * Simple implementation of a {@link GamebotsClient.Listener}. 605 */ 606 public static class Adapter implements Listener { 607 /** 608 * Notifies the Listener that the {@link GamebotsClient client} has been 609 * connected to the server. 610 * 611 * @see GamebotsClient.Listener#connected 612 */ 613 public void connected() {} 614 615 /** 616 * Notifies the Listener of a new asynchronous message. 617 * 618 * @see GamebotsClient.Listener#receivedAsyncMessage 619 */ 620 public void receivedAsyncMessage( Message message ) {} 621 622 /** 623 * Notifies the Listener of a new synchronous message, also know as a 624 * vision block. 625 * 626 * @see GamebotsClient.Listener#receivedSyncMessage 627 */ 628 public void receivedSyncMessage( MessageBlock message ) {} 629 630 /** 631 * Notifies the Listener of any error that occured. 632 * 633 * @see GamebotsClient.Listener#receivedError 634 */ 635 public void receivedError( Throwable error ) {} 636 637 /** 638 * Notifies the Listener that the {@link GamebotsClient client} has been 639 * disconnected from the server. 640 * 641 * @see GamebotsClient.Listener#disconnected 642 */ 643 public void disconnected() {} 644 } 645 646 protected class PingTask extends TimerTask { 647 long sendTime = System.currentTimeMillis(); 648 649 public void run() { 650 if( pingTask != this ) 651 return; // ignore 652 pingTask = null; 653 fireReceivedSocketError( new IOException( "Server failed to respond to ping sent "+ 654 (((float) System.currentTimeMillis()-sendTime)/1000f )+ 655 " seconds ago" ) ); 656 } 657 } 658 }