
// MultiChat.java
// Andrew Davison, April 2005, ad@fivedots.coe.psu.ac.th

/* Klient pro multicastovou diskusní skupinu, která pro jisté úkoly používá
   jmenný server (NameServer).

   Jmenný server provádí velmi zjednodušené ověřování uživatelů, než jim
   odhalí multicastovou adresu skupiny.

   Jmenný server dále odpovídá na dotazy "who".
   
   Existují tři možné typy zpráv:
       hi <name>   // přidá jméno name do seznamu skupiny (je-li unikátní)
                      a oznámí její multicastovou adresu

       bye <name>  // smaže jméno name ze seznamu skupiny

       who         // pošle zpět seznam všech aktuálních členů


   Typy zpráv zasílaných do multicastové skupiny

      (name): msg [ / toName ]

   Rozšíření "/ toName" znamená, že zpráva se objeví pouze v textovém
   okně uživatele "toName".

  ---- Změny: 30. dubna 2004 ---

  Upravena metoda showMsg() tak, aby posunula pozici kurzoru a využila
  metodu invokeLater, čímž zabrání problémům, které vyvstávají z knihovnou
  Swing a vlákny. Více informace najdete na adrese:  
  http://java.sun.com/products/jfc/tsc/articles/threads/threads1.html
    - poděkování patří Rachel Struthersové (rmstruthers@mn.rr.com)

*/

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;


public class MultiChat extends JFrame 
                             implements ActionListener
{
  private static final int TIME_OUT = 5000;   // 5 sekund
     // timeout při čekání na metodu receive()
  private static final int PACKET_SIZE = 1024;  // max. velikost zprávy

  // adresa a port jmenného serveru
  private static final String SERVER_HOST = "localhost";
  private static final int SERVER_PORT = 1234;

  /* Multicastový port. Adresu mutlicastové skupiny získáme 
     od objektu typu NameServer. */
  private static final int GROUP_PORT = 5555;

  // pro komunikaci se jmenným serverem
  private DatagramSocket clientSock;
  private InetAddress serverAddr;

  // pro komunikaci s multicastovou skupinou
  private MulticastSocket groupSock;
  private InetAddress groupAddr;

  private String userName;   // jméno tohoto klienta

  private JTextArea jtaMesgs;
  private JTextField jtfMsg;
  private JButton jbWho;


  public MultiChat(String nm)
  {
     super( "Multicastový klient pro uživatele: " + nm);
     userName = nm;
     initializeGUI();

     /* Pokos se zaregistrovat jméno a získat adresu multicastové skupiny
        od jmenného serveru */
     makeClientSock();
     sendServerMessage("hi " + userName);  // pozdrav jmenný server
     checkHiResponse();

     // Připoj se do multicastové skupiny a všechny pěkně pozdrav
     joinChatGroup();
     sendmacket("ahoj");

     addWindowListener( new WindowAdapter() {
       public void windowClosing(WindowEvent e)
       { sayBye(); }   // při ukončení odešli rozloučení
     });

     setSize(300,450);
     setVisible(true);

     waitForPackets();
  } // konec metody: MultiChat();


  // ----------- metody vztahující se k GUI -------------------------


  private void initializeGUI()
  /* Textová oblast ve středu a ovládaní pod ní.
     Ovládání:
         - textové pole pro zadávání zpráv
         - tlačítko "Kdo je přítomen"
  */
  { Container c = getContentPane();
    c.setLayout( new BorderLayout() );

    jtaMesgs = new JTextArea(7, 7);
    jtaMesgs.setEditable(false);
    JScrollPane jsp = new JScrollPane( jtaMesgs);
    c.add( jsp, "Center");

    JLabel jlMsg = new JLabel("Zpráva: ");
    jtfMsg = new JTextField(15);
    jtfMsg.addActionListener(this);    // stisknutí klávesy enter spustí odeslání zprávy

    jbWho = new JButton("Kdo je přítomen");
    jbWho.addActionListener(this);

    JPanel p1 = new JPanel( new FlowLayout() );
    p1.add(jlMsg); p1.add(jtfMsg);

    JPanel p2 = new JPanel( new FlowLayout() );
    p2.add(jbWho);

    JPanel p = new JPanel();
    p.setLayout( new BoxLayout(p, BoxLayout.Y_AXIS));
    p.add(p1); p.add(p2);

    c.add(p, "South");
  }  // konec metody: initializeGUI()



  public void actionPerformed(ActionEvent e)
  /* Buď se má odeslat zpráva nebo bylo stisknuto tlačítko "Kdo je přítomen" */
  {
    if (e.getSource() == jbWho)
      doWho();
    else if (e.getSource() == jtfMsg)
      sendMessage();
  }

/*
  synchronized private void showMsg(String msg)
  // Metodu je nunté synchronizovat, neboť může být volána jak
     tímto objektem, tak i vláknem URLChatWatchcer.
  { jtaMesgs.append(msg);  }
*/


  private void showMsg(final String msg)  
  /* Aktualizujeme textovou oblast se zprávami, takže kód by
     mělo provést vlákno obsluhující události knihovny Swing,
     čehož docílíme zavoláním metody invokeLater().

     Parametr msg musí být finalní, aby jej bylo možné použít ve vložené
     třídě pro rozhraní Runnable.
   
     Metoda showMsg() může být volána tímto objektem a vláken URLWatcher,
     ovšem aktualizace se serializují tím, že se umísťují do fronty 
     dispečera událostí. To znamená, že tuto metodu synchronizovat nemusíme.

     Poděkování náleží Rachle Struthersové (rmstruthers@mn.rr.com)
  */
  { 
    // System.out.println("showMsg(): " + msg);
    Runnable updateMsgsText = new Runnable() {
      public void run() 
      { jtaMesgs.append(msg);  // připoj zprávu do textové oblasti
        jtaMesgs.setCaretPosition( jtaMesgs.getText().length() );
            // přesuň vkládací pozici na konec textu
      }
    };
    SwingUtilities.invokeLater( updateMsgsText );
  } // konec metody: showMsg()


  // ----------- metody pro komunikaci se jmenným serverem --------------

  private void makeClientSock()
  {
    try {   // pokus se vytvořit klientův socket
      clientSock = new DatagramSocket();
      clientSock.setSoTimeout(TIME_OUT);  // zahrň time-out
    }
    catch( SocketException se ) {
      se.printStackTrace();
      System.exit(1);
    }

    try {  // řetězec s adresou jmenného serveru --> IP
      serverAddr = InetAddress.getByName(SERVER_HOST);
    }
    catch( UnknownHostException uhe) {
      uhe.printStackTrace();
      System.exit(1);
    }
  }  // konec metody: makeClientSock()


  private void checkHiResponse()
  /* Reakcí na zprávu "hi <name>" může být adresa multicastové skupiny
     (v textové formě) nebo "no".
  */
  {
    String hiResponse = readServerMessage();

    if (hiResponse.equals("no")) {
      System.out.println("Přihlášení odmítnuto; Končím...");
      System.exit(0);
    }
    else { 
      try {  // adresa multicastové skupiny --> IP
        groupAddr = InetAddress.getByName(hiResponse); 
      }       
      catch(Exception e)
      {  System.out.println(e);  
         System.exit(0);
      }
    }
  }  // konec metody: checkHiResponse()


  private void doWho()
  /* Zeptej se jmenného serveru kdo je právě přihlášen.
     Počkej na odpověď a zobraz sekvenci. */
  {
    sendServerMessage("who");
    String whoResponse = readServerMessage();
    if (whoResponse == null)
      showMsg("Problém se jmenným serverem: infomrace o přihlášených klientech není dostupná\n");
    else
      showMsg(whoResponse);
  }  // konec metody: doWho()


  private void sayBye()
  // Řekne "bye" mutlicastové skupina a jmennému serveru
  {
    try {
      sendmacket("bye");    // "bye" skupině
      sendServerMessage("bye " + userName);  // "bye" serveru
    }
    catch(Exception e)
    {  System.out.println( e );  }

    System.exit( 0 ); 
  } // konec metody: sayBye()


  private void sendServerMessage(String msg)
  // Pošle zprávu jmennému serveru
  {
    try {
      DatagramPacket sendmacket =
          new DatagramPacket( msg.getBytes(), msg.length(), 
   						serverAddr, SERVER_PORT);
      clientSock.send( sendmacket );
    }
    catch(IOException ioe)
    {  System.out.println(ioe);  }
  } // konec metody: sendServerMessage()


  private String readServerMessage()
  /* Přečte zprávu z jmenného serveru (jakmile dorazí). 
     S metodou recieve() je spojen jistý time-out(). */
  {
    String msg = null;
    try {
      byte[] data = new byte[PACKET_SIZE];
      DatagramPacket receivePacket = new DatagramPacket(data, data.length);

      clientSock.receive( receivePacket );  // počkej na paket
      msg = new String( receivePacket.getData(), 0,
                           receivePacket.getLength() );
    }
    catch(IOException ioe)
    {  System.out.println(ioe);  }

    return msg;
  }  // konec metody: readServerMessage()



  // ----------------------- metody multicastové skutpiny -------------
    
  private void joinChatGroup()
  {
    try {
      groupSock = new MulticastSocket(GROUP_PORT);
      groupSock.joinGroup(groupAddr);
    }
    catch(Exception e)
    {  System.out.println(e);  }
  }  // konec metody: joinChatGroup()


  private void sendMessage()
  /* Zkontrolujem, zadal-li uživatel zprávu, a pak
     ji pošli do multicastové skupiny. */
  {
    String msg = jtfMsg.getText().trim();
    // System.out.println("'"+msg+"'");

    if (msg.equals(""))
      JOptionPane.showMessageDialog( null, 
           "No message entered", "Send Message Error", 
			JOptionPane.ERROR_MESSAGE);
    else
      sendmacket(msg);
  }  // konec metody: sendMessage()



  private void sendmacket(String msg)
  /* Odešle paket do multicastové skupiny.
     Zpráva má následující tvar:
				(name): msg
  */
  { String labelledMsg = "(" + userName + "): " + msg;
    try {
      DatagramPacket sendmacket = 
        new DatagramPacket(labelledMsg.getBytes(), labelledMsg.length(), 
										groupAddr, GROUP_PORT);          
      groupSock.send(sendmacket); 
    }
    catch(IOException ioe)
    {  System.out.println(ioe);  }
  }  // konec metody: sendPacket()



  // --------------------------------------------------------
  // Zpracování zpráv pro skupinu aplikačním vláknem


  private void waitForPackets()
  /* Opakovaně přijímej paket ze skupiny a zpracuj jej.
     Odsud se žádné zprávy neposílají. Výstup provádí
     vlákno GUI voláním metody sendPacket(). */
  {
    DatagramPacket packet;
    byte data[];

    try {
      while (true) {
        data = new byte[PACKET_SIZE];    // připrav prázdný paket
        packet = new DatagramPacket(data, data.length);
        groupSock.receive(packet);  // počkej na paket
        processPacket(packet);
      }
    }
    catch(IOException ioe)
    {  System.out.println(ioe);  }
  }  // konec metody: waitForPackets()



   private void processPacket(DatagramPacket dm)
   /* Extrahuj údaje z příchozího paketu.
      Format:   (fromName): msg [ / toName ]

      Rozšíření "/ toName" znamená, že se zpráva objeví pouze v okně
      odesílatele a uživatele jménem "toName".
   */
   {
     String msg = new String( dm.getData(), 0, dm.getLength() );
     if (isVisibleMsg(msg, userName))
       showMsg(msg + "\n");
  }  // konec metody: processPacket()



  private boolean isVisibleMsg(String msg, String name)
  /* Zpráva je viditelná, pokud má část "/ name" nebo name
     je aktuální uživatel nebo je zpráva od aktuálního uživatele.
  */
  {
    int index = msg.indexOf("/");
    if (index == -1)  // nemá znak '/', takže je veřejná
      return true;

    // má část "/ name"
    String toName = msg.substring(index+1).trim();
    if (toName.equals(name))  // pro tohoto uživatele
      return true;
    else {   // pro jiného uživatele
      if (msg.startsWith("("+name))   // ale od tohoto uživatele
        return true;
      else     // od někoho jiného
        return false;
    }
  }  // konec metody: isVisibleMsg()



  // ------------------------------------

  public static void main(String args[]) 
  {  
     if (args.length != 1) {
       System.out.println("použití:  java MultiChat <your userName>");
       System.exit(0);
     }
     new MultiChat(args[0]);  
  } // konec metody: main()

} // konec třídy: MultiChat

