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    }