/*
 netjconn.java - connection handling of the  VRML net java server

 Copyright (C) 2002    Peter Graf

 This file is part of VRMLNETJ - The VRML net java server.
 VRMLNETJ is free software.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

   For more information on the VRML net java server or Peter Graf,
   please see: http://www.mission-base.com/.

   $Log: vrmlnetj.java,v $

------------------------------------------------------------------------------
*/
//package vrmlnetj;

import java.io.*;
import java.util.*;
import java.net.*;

/**
 * A class containing connections handled
 */
public class Netjconn
{
    /**
     * All connections are kept in a static map
     */
    private static Map table;

    /**
     * The connection id, always unique
     */
    public String CID;

    /**
     * The client id, always unique
     */
    public String CLID;

    /**
     * The change ID of the connection, each attribute update increases this
     */
    public int    CHID;

    /**
     * NNM, UID, PWD, VER, special attributes stored for each client connection
     */
    public String NNM;
    public String UID;
    public String PWD;
    public String VER;

    /**
     * A flag showing that a client has left the scene
     */
    private boolean left;

    /**
     * All other attributes of the connection are kept in a map
     */
    private Map attributeMap;

    /**
     * The last request that was handled by the connection
     */
    private Netjrequest request;

    /**
     * The scene the connection belongs to
     */
    public Netjscene scene;

    /**
     * Time when the connection was created
     */
    private long creationTime;
    private long lastSentTime;
    private long lastRecvTime;

    /**
     * Internetadress and port used for connection
     */
    public InetAddress address;
    public int         port;

    /**
     * Attribute names that are reserved in the protocol
     */
    private static String[] reservedNames = { "NNM", "PWD", "SCN", "SCU", "VER",
                                              "CLID", "SCID", "NPP", "NSN",
                                              "POS", "ORI", "AVU", "UID", "TXT",
                                              "PEID", "CHID"
                                            };

    /**
     * test for reserved attribute names
     */
    private boolean isReservedName( String t )
    {
        for( int i = 0; i < reservedNames.length; i++ )
        {
            if( t.equals( reservedNames[ i ] ))
            {
                return( true );
            }
        }
        return( false );
    }

    /**
      * Set the request the connection is handling
      */
    public void setRequest( Netjrequest r )
    {
        request = r;
    }

    /**
     * Remember Internetadress and port used for connection
     */
    public void setAddress( InetAddress a, int p )
    {
        address = a;
        port    = p;
    }

    /**
     * Handle a PING request on the connection
     */
    public void ping( )
    {
        // create and send a reply
        //
        Netjsendpacket reply = new Netjsendpacket( this, "AN" );

        reply.setPid( vrmlnetj.strargs[ 1 ] );
        reply.setString( "PONG" );

        request.setReplyPacket( reply );
        reply.send( );
    }

    /**
     * send an error packet to a connection
     */
    public void sendBad( String arg, String val )
    {
        Netjsendpacket reply = new Netjsendpacket( this, "AN" );
        reply.setPid( vrmlnetj.strargs[ 1 ] );
        reply.setString( "BAD" );
        reply.setString( arg );
        reply.setString( val );

        request.setReplyPacket( reply );
        reply.send( );
    }

    /**
     * Handle an attribute SET request on a connection or scene
     */
    public void set( )
    {
        boolean setSceneAttributes = false;

        if( this.scene == null )
        {
            // ignore the request, the connection is not in a scene
            //
            sendBad( "CID", CID );
            return;
        }

        // synchronize all set and get operations on the scene ID
        //
        synchronized( this.scene.SCID )
        {
            // create and send a reply
            //
            Netjsendpacket reply = new Netjsendpacket( this, "AN" );

            reply.setPid( vrmlnetj.strargs[ 1 ] );
            reply.setString( "OK" );

            request.setReplyPacket( reply );
            reply.send();

            // before we send an announcement we check the packet
            //
            boolean sendtopeers = false;
            boolean announce = false;

            // create an announcement packet
            //
            Netjsendpacket announcement = new Netjsendpacket( this, "RQ" );

            // set the announcement tag
            //
            announcement.setString( "SET" );

            boolean implicitCLID = false;

            // add a CLID value if no SCID value is given
            //
            if( 4 < vrmlnetj.nstrargs )
            {
                if( !vrmlnetj.strargs[ 4 ].equals( "SCID" ))
                {
                    if( announcement.setString( "CLID", CLID ) < 1 )
                    {
                        return;
                    }
                    implicitCLID = true;
                }
            }

            // look at the packet, copy the attributes known to the server
            //
            for( int i = 4; i < vrmlnetj.nstrargs; i++ )
            {
                String arg = vrmlnetj.strargs[ i ];
                String val = null;

                //vrmlnetj.log( "TRA ", "I " + i + " ARG " + arg );
                if( arg.equals( "" ) )
                {
                    // an empty attribute name is not allowed
                    //
                    i++;
                    continue;
                }
                else if( arg.equals( "NNM" ) && i < vrmlnetj.nstrargs - 1 )
                {
                    val = vrmlnetj.strargs[ ++i ];
                    if( !NNM.equals( val ))
                    {
                        // the user tried to use a different nickname
                        //
                        return;
                    }

                    if( announcement.setString( arg, val ) < 1 )
                    {
                        return;
                    }
                }
                else if( arg.equals( "UID" ) && i < vrmlnetj.nstrargs - 1 )
                {
                    val = vrmlnetj.strargs[ ++i ];
                    if( !UID.equals( val ))
                    {
                        // the user tried to use a different user id
                        //
                        return;
                    }

                    if( announcement.setString( arg, val ) < 1 )
                    {
                        return;
                    }
                }
                else if(( arg.equals( "AVU" ) || arg.equals( "POS" ) 
                       || arg.equals( "ORI" ) || arg.equals( "TXT" ))
                     && ( i < vrmlnetj.nstrargs - 1 ))
                {
                    // these names url are reserved, because we might
                    // restrict or optimize on the fields at some points
                    //
                    announce = true;
                    val = vrmlnetj.strargs[ ++i ];

                    //vrmlnetj.log( "TRA ", "SET " + arg + " TO " + val );
                    if( announcement.setString( arg, val ) < 1 )
                    {
                        return;
                    }

                    if( setSceneAttributes )
                    {
                        // change the attribute for the scene
                        //
                        scene.setAttribute( arg, val );
                    }
                    else
                    {
                        // change the attribute for the connection
                        //
                        setAttribute( arg, val );
                    }
                }
                else if( arg.equals( "SCID" ) && i < vrmlnetj.nstrargs - 1 )
                {
                    val = vrmlnetj.strargs[ ++i ];
                    if( i == 5 )
                    {
                        // if the argument list stars with a scene id
                        // this update is for the scene the client is in
                        //
                        setSceneAttributes = true;
                        //vrmlnetj.log( "TRA ", "SET " + arg + " TO " + val );
                    }


                    if( !scene.SCID.equals( val ))
                    {
                        // the user tried to use a different scene id
                        //
                        return;
                    }

                    if( announcement.setString( arg, val ) < 1 )
                    {
                        return;
                    }
                }
                else if( arg.equals( "CLID" ) && i < vrmlnetj.nstrargs - 1 )
                {
                    val = vrmlnetj.strargs[ ++i ];
                    if( !CLID.equals( val ))
                    {
                        // the user tried to use a different client id
                        //
                        return;
                    }

                    if( !implicitCLID )
                    {
                        if( announcement.setString( arg, val ) < 1 )
                        {
                            return;
                        }
                    }
                }
                else if( arg.equals( "PEID" ) && i < vrmlnetj.nstrargs - 1 )
                {
                    // remember that the packet has to be sent to specific peers
                    //
                    sendtopeers = true;

                    val = vrmlnetj.strargs[ ++i ];
                    if( announcement.setString( arg, val ) < 1 )
                    {
                        return;
                    }
                }
                else if( isReservedName( arg ))
                {
                    // ignore the attempt to set a reserved attribute
                    //
                    i++;
                }
                else if( i < vrmlnetj.nstrargs - 1 )
                {
                    val = vrmlnetj.strargs[ ++i ];

                    // announce attributes that have uppercase first letters
                    //
                    char f = arg.charAt( 0 );
                    if( f >= 'A' && f <= 'Z' )
                    {
                        announce = true;
                        if( announcement.setString( arg, val ) < 1 )
                        {
                            return;
                        }
                    }

                    if( setSceneAttributes )
                    {
                        // change the attribute for the scene
                        //
                        scene.setAttribute( arg, val );
                    }
                    else
                    {
                        // change the attribute for the connection
                        //
                        setAttribute( arg, val );
                    }
                }
            }

            // send also the change counter of the object
            //
            if( setSceneAttributes )
            {
                if( announcement.setString( "CHID", scene.getChid() ) < 1 )
                {
                    return;
                }
            }
            else
            {
                if( announcement.setString( "CHID", getChid() ) < 1 )
                {       
                    return; 
                }       
            }

            // if no attribute change has to be announced
            //
            if( !announce )
            {
                return;
            }

            // if there is no peers given explicitly
            //
            if( !sendtopeers )
            {
                // send to all peers in scene
                //
                scene.send( announcement, null );
                return;
            }

            // find the peers the packet has to be sent to
            //
            for( int i = 4; i < vrmlnetj.nstrargs; i++ )
            {
                String arg = vrmlnetj.strargs[ i ];

                if( arg.equals( "PEID" ) && i < vrmlnetj.nstrargs - 1 )
                {
                    Netjconn peerconn = find( vrmlnetj.strargs[ ++i ] );
                    if( peerconn != null )
                    {
                        announcement.send( peerconn );
                    }
                }
                else
                {
                    i++;
                }
            }
        }
    }

    /**
     * Handle an attribute GET request on a connection or scene
     */
    public void get( )
    {
        if( this.scene == null )
        {
            // ignore the request, the connection is not in a scene
            //
            sendBad( "CID", CID );
            return;
        }

        // synchronize all set and get operations on the scene ID
        //
        synchronized( this.scene.SCID )
        {
            // create and send a reply
            //
            Netjsendpacket reply = new Netjsendpacket( this, "AN" );
            reply.setPid( vrmlnetj.strargs[ 1 ] );

            String scid = null;
            String peid = null;
            Netjconn peerconn = null;
            Netjscene peerscene = null;

            int startarg = 4;

            // look at the first two args
            //
            if( vrmlnetj.nstrargs > 5 )
            {
                String arg = vrmlnetj.strargs[ 4 ];

                if( arg.equals( "CLID" ))
                {
                    peid = vrmlnetj.strargs[ 5 ];
                    peerconn = find( peid );
                    startarg = 6;
                }
                else if( arg.equals( "SCID" ))
                {
                    scid = vrmlnetj.strargs[ 5 ];
                    peerscene = Netjscene.find( scid );
                    startarg = 6;
                }
                else
                {
                    // query of the attributes of the connection is the default
                    //
                    peid = CLID;
                    peerconn = this;
                    startarg = 4;
                }
            }
            else
            {
                // query of the attributes of the connection is the default
                //
                peid = CLID;
                peerconn = this;
                startarg = 4;
            }

            // if the get request regards a scene
            //
            if( scid != null )
            {
                if( peerscene == null )
                {
                    // we do not know about the scene
                    //
                    sendBad( "SCID", scid );
                    return;
                }
            }
            else if( peid == null )
            {
                // answer the bad request
                //
                sendBad( "?", "" );
                return;
            }
            else if( peerconn == null )
            {
                // we do not know about the peer
                //
                sendBad( "CLID", peid );

                request.setReplyPacket( reply );
                reply.send();
                return;
            }
        
            // if there is no other scene mentioned, use the connection scene
            //
            if( peerscene == null )
            {
                peerscene = this.scene;
            }

            // start the reply packet
            //
            reply.setString( "SET" );

            // give back the scene id or the peer id
            //
            if( scid != null )
            {
                reply.setString( "SCID", scid );
                reply.setString( "CHID", peerscene.getChid() );
            }
            else
            {
                reply.setString( "CLID", peid );
                reply.setString( "CHID", getChid() );
            }

            // loop through all attributes requested
            //
            for( int i = startarg; i < vrmlnetj.nstrargs; i++ )
            {
                String arg = vrmlnetj.strargs[ i ];
                String val = null;

                if( arg.equals( "" ))
                {
                    // empty argument names are not handled
                    //
                    continue;
                }
                else if( arg.equals( "NPP" ))
                {
                    val = "" + peerscene.nConnections;
                }
                else if( arg.equals( "NSN" ))
                {
                    val = "" + peerscene.getNScenes();
                }
                else if( arg.equals( "SCU" ))
                {
                    val = peerscene.SCU;
                }
                else if( arg.equals( "SCN" ))
                {
                    val = peerscene.SCN;
                }
                else if( scid != null )
                {
                    // get the attribute from the scene
                    //
                    val = peerscene.getAttribute( arg );
                }
                else
                {
                    // get the attribute from the peer connection
                    //
                    val = peerconn.getAttribute( arg );
                }

                // set the value to the result
                //
                if( reply.setString( arg, val ) < 1 )
                {
                    return;
                }
            }

            // send the reply to the client
            //
            request.setReplyPacket( reply );
            reply.send();
            return;
        }
    }

    /**
     * Handle an attribute LST request on a connection
     */
    public void lst( )
    {
        if( this.scene == null )
        {
            // ignore the request, the connection is not in a scene
            //
            sendBad( "CID", CID );
            return;
        }

        // synchronize all set and get operations on the scene ID
        //
        synchronized( this.scene.SCID )
        {
            // create and send a reply
            //
            Netjsendpacket reply = new Netjsendpacket( this, "AN" );
            reply.setPid( vrmlnetj.strargs[ 1 ] );

            reply.setString( "SET" );
            reply.setString( "CLID", CLID );
            reply.setString( "CHID", getChid() );

            // loop through all attributes requested
            //
            for( int i = 4; i < vrmlnetj.nstrargs; i++ )
            {
                String arg = vrmlnetj.strargs[ i ];

                if( arg.equals( "" ))
                {
                    // empty argument names are not handled
                    //
                    continue;
                }

                if( reply.setString( arg, getAttribute( arg )) < 1 )
                {
                    return;
                }
            }

            // send the reply to the client
            //
            request.setReplyPacket( reply );
            reply.send();

            // request the send of the attributes for all connections
            // in the scene
            //
            scene.sendConnectionAttributes( this );
        }
    }

    /**
     * Handle an attribute SCL request on a scene
     */
    public void scl( )
    {
        if( this.scene == null )
        {
            // ignore the request, the connection is not in a scene
            //
            sendBad( "CID", CID );
            return;
        }

        // synchronize on the scene of the connection
        //
        synchronized( this.scene.SCID )
        {
            // create and send a reply
            //
            Netjsendpacket reply = new Netjsendpacket( this, "AN" );
            reply.setPid( vrmlnetj.strargs[ 1 ] );

            reply.setString( "SET" );
            reply.setString( "SCID", scene.SCID );
            reply.setString( "CHID", scene.getChid() );

            // loop through all attributes requested
            //
            for( int i = 4; i < vrmlnetj.nstrargs; i++ )
            {
                String arg = vrmlnetj.strargs[ i ];
                String val = null;

                if( arg.equals( "" ))
                {
                    // empty argument names are not handled
                    //
                    continue;
                }

                if( arg.equals( "SCID" ))
                {
                    continue;
                }
                else if( arg.equals( "NPP" ))
                {
                    val = "" + scene.nConnections;
                }
                else if( arg.equals( "NSN" ))
                {
                    val = "" + scene.getNScenes();
                }
                else if( arg.equals( "SCU" ))
                {
                    val = scene.SCU;
                }
                else if( arg.equals( "SCN" ))
                {
                    val = scene.SCN;
                }
                else
                {
                    // get the attribute from the scene
                    //
                    val = scene.getAttribute( arg );
                }

                if( reply.setString( arg, val ) < 1 )
                {
                    return;
                }
            }

            // send the reply to the client
            //
            request.setReplyPacket( reply );
            reply.send();
        }

        // request the send of the attributes for all connections
        // in the scene
        //
        scene.sendSceneAttributes( this );
    }

    /**
     * handle an ENTER request on the connection
     */
    public void enter( )
    {
        if( this.scene != null )
        {
            // ignore the request, the connection is linked to a scene already
            //
            return;
        }

        // values expected in the packet
        //
        String sNNM = "";
        String sUID = "";
        String sPWD = "";
        String sSCN = "";
        String sSCU = "";
        String sVER = "";

        // parse the values expected from the packet
        //
        for( int i = 4; i < vrmlnetj.nstrargs; i++ )
        {
            String arg = vrmlnetj.strargs[ i ];

            if( arg.equals( "NNM" ) && i < vrmlnetj.nstrargs - 1 )
            {
                sNNM = vrmlnetj.strargs[ ++i ];
                setAttribute( arg, sNNM );
            }
            else if( arg.equals( "UID" ) && i < vrmlnetj.nstrargs - 1 )
            {
                sUID = vrmlnetj.strargs[ ++i ];
                setAttribute( arg, sUID );
            }
            else if( arg.equals( "PWD" ) && i < vrmlnetj.nstrargs - 1 )
            {
                sPWD = vrmlnetj.strargs[ ++i ];
            }
            else if( arg.equals( "VER" ) && i < vrmlnetj.nstrargs - 1 )
            {
                sVER = vrmlnetj.strargs[ ++i ];
                setAttribute( arg, sVER );
            }
            else if( arg.equals( "SCN" ) && i < vrmlnetj.nstrargs - 1 )
            {
                sSCN = vrmlnetj.strargs[ ++i ];
            }
            else if( arg.equals( "SCU" ) && i < vrmlnetj.nstrargs - 1 )
            {
                sSCU = vrmlnetj.strargs[ ++i ];
            }
            else if( isReservedName( arg ))
            {
                // attributes with reserved names cannot be set
                //
                i++;
            }
            else if( i < vrmlnetj.nstrargs - 1 )
            {
                // store all other attributes for the connection
                //
                setAttribute( arg, vrmlnetj.strargs[ ++i ] );
            }
        }

        // check the values given
        //
        if( sNNM.length() == 0 )
        {
            return;
        }

        char f = sNNM.charAt( 0 );
        if( ! (( f >= 'a' && f <= 'z' ) || ( f >= 'A' && f <= 'Z' )))
        {
            return;
        }

        if( sSCU.length() == 0 )
        {
            return;
        }

        f = sSCU.charAt( 0 );
        if( ! (( f >= 'a' && f <= 'z' ) || ( f >= 'A' && f <= 'Z' )))
        {
            return;
        }

        // remember the attributes of the connection
        //
        NNM = new String( sNNM );
        PWD = new String( sPWD );
        VER = new String( sVER );

        if( table == null )
        {
            return;
        }

        // synchronize the access to the scene table
        //
        synchronized( table )
        {
            // See what scene the connection belongs to
            //
            Netjscene pscene = Netjscene.find( sSCU, sSCN );
            if( pscene == null )
            {
                // create a new scene
                //
                pscene = new Netjscene( sSCU, sSCN );
                pscene = Netjscene.find( sSCU, sSCN );
            }

            // Add the connection to the scene, remember the scene it belongs to
            //
            pscene.addConnection( this );
            scene = pscene;
        }

        // if we could not enter the scene we give up
        //
        if( scene == null )
        {
            return;
        }

        Netjsendpacket announcement = new Netjsendpacket( this, "RQ" );

        // synchronize all set and get operations on the scene ID
        //
        synchronized( this.scene.SCID )
        {
            // send the answer back to the client
            //
            Netjsendpacket reply = new Netjsendpacket( this, "AN" );

            reply.setPid( vrmlnetj.strargs[ 1 ] );

            reply.setString( "HI" );
            reply.setString( "CLID", CLID );
            reply.setString( "NNM", NNM );
            reply.setString( "VER",  "1" );
            reply.setString( "SCID", scene.SCID );
            reply.setString( "SCN",  scene.SCN );
            reply.setString( "CHID", scene.getChid() );
            reply.setString( "NPP",  "" + scene.nConnections );
            reply.setString( "NSN",  "" + scene.getNScenes() );

            // create the reply by copying all other strings
            //
            for( int i = 4; i < vrmlnetj.nstrargs; i++ )
            {
                String arg = vrmlnetj.strargs[ i ];
                String val = null;

                // filter out the attributes that are not sent back
                //
                if( isReservedName( arg ))
                {
                    i++;
                    continue;
                }

                // there must be a value after the argument name
                //
                if( i >= vrmlnetj.nstrargs - 1 )
                {
                    continue;
                }
                val = vrmlnetj.strargs[ ++i ];

                // set the argument / value pair
                //
                if( reply.setString( arg, val ) < 1 )
                {
                    return;
                }
            }

            request.setReplyPacket( reply );
            reply.send( );

            // tell all clients in the scene that the new client arrived
            //
            announcement.setString( "ENTER" );
            announcement.setString( "CLID", CLID );
            announcement.setString( "NNM", NNM );
            announcement.setString( "UID", UID );
            announcement.setString( "VER",  VER );
            announcement.setString( "SCN",  scene.SCN );
            announcement.setString( "CHID", scene.getChid() );
            announcement.setString( "NPP",  "" + scene.nConnections );
            announcement.setString( "NSN",  "" + scene.getNScenes() );

            // create the announcement by copying all other strings
            //
            for( int i = 4; i < vrmlnetj.nstrargs; i++ )
            {
                String arg = vrmlnetj.strargs[ i ];
                String val = null;

                // filter out the attributes that are not sent back
                //
                if( isReservedName( arg ))
                {
                    i++;
                    continue;
                }

                // there must be a value after the argument name
                //
                if( i >= vrmlnetj.nstrargs - 1 )
                {
                    continue;
                }
                val = vrmlnetj.strargs[ ++i ];

                // set the argument / value pair
                //
                if( announcement.setString( arg, val ) < 1 )
                {
                    return;
                }
            }
        }

        // send the announcement to all other connections
        //
        scene.send( announcement, CID );
    }

    /**
     * handle an BYE request on the connection
     */
    public void bye( )
    {
        Netjscene currscene = this.scene;

        if( currscene == null )
        {
            // ignore the request, the connection is not in a scene
            //
            sendBad( "CID", CID );
            return;
        }

        // synchronize on the scene of the connection
        //
        synchronized( currscene.SCID )
        {
            // values expected in the packet
            //
            String sCLID = null;

            // parse the values expected from the packet
            //
            for( int i = 4; i < vrmlnetj.nstrargs; i++ )
            {
                String arg = vrmlnetj.strargs[ i ];

                if( arg.equals( "CLID" ) && i < vrmlnetj.nstrargs - 1 )
                {
                    sCLID = vrmlnetj.strargs[ ++i ];
                    break;
                }
            }

            // check the client ID given
            //
            if( sCLID == null || !sCLID.equals( CLID ))
            {
                return;
            }

            // send the answer back to the client
            //
            Netjsendpacket reply = new Netjsendpacket( this, "AN" );

            reply.setPid( vrmlnetj.strargs[ 1 ] );

            reply.setString( "BYE" );
            reply.setString( "CLID", CLID );

            request.setReplyPacket( reply );
            reply.send( );

            // tell all clients in the scene that the client is leaving
            //
            Netjsendpacket announcement = new Netjsendpacket( this, "RQ" );

            // delete the connection from the scene
            //
            delete();

            announcement.setString( "BYE" );
            announcement.setString( "CLID", CLID );
            announcement.setString( "CHID", currscene.getChid() );
            announcement.setString( "NPP",  "" + ( currscene.nConnections ) );
            announcement.setString( "NSN",  "" + currscene.getNScenes() );

            // send the announcement to all other connections
            //
            currscene.send( announcement, CID );
            left = true;
        }
    }

    /**
     * Create a new connection
     */
    public Netjconn( )
    {
        if( table == null )
        {
            newTable();
        }
        
        // set the creation time of the packet
        //
        creationTime = System.currentTimeMillis();
        lastSentTime = 0;
        lastRecvTime = 0;
        scene        = null;
        left         = false;

        // initialize the change count
        //
        CHID         = 0;

        // synchronize the creation access to the table
        //
        synchronized( table )
        {
            // create the connection ID of the connection
            //
            for(;;)
            {
                CID = vrmlnetj.randstr();
                if( table.containsKey( CID ))
                {
                    continue;
                }

                table.put( CID, this );
                break;
            }

            // create the client id of the connection
            //
            for(;;)
            {
                CLID = vrmlnetj.randstr();

                if( table.containsKey( CLID ))
                {
                    continue;
                }

                table.put( CLID, this );
                break;
            }

            vrmlnetj.log( "LOG", "NEW CONN " + CID + " CLID " + CLID );

            // create the attributes table of the connection
            //
            attributeMap = new HashMap();
        }
    }

    /**
     * Set an attribute for a connection
     */
    private void setAttribute( String k, String v )
    {
        if( table == null )
        {
            return;
        }

        // synchronize the access to the table
        //
        synchronized( table )
        {
            // increase the change counter
            //
            CHID++;

            if( attributeMap == null )
            {
                return;
            }

            if( attributeMap.containsKey( k ))
            {
                attributeMap.remove( k );
            }
            attributeMap.put( k, v );
        }
    }

    /**
     * Get the change counter
     */
    public String getChid( )
    {
        return( vrmlnetj.hex8str( CHID ));
    }

    /**
     * Get an attribute of a connection
     */
    public String getAttribute( String k )
    {
        if( table == null )
        {
            return( "" );
        }

        // synchronize the access to the table
        //
        synchronized( table )
        {
            if( attributeMap == null )
            {
                return( "" );
            }

            String res = (String)attributeMap.get( k );
            if( res != null )
            {
                res = new String( res );
            }
            else
            {
                res = "";
            }
            return( res );
        }
    }

    /**
     * Find a connection in the table, synchronized
     */
    public static Netjconn find( String connectionId )
    {
        if( table == null )
        {
            return( null );
        }

        // synchronize the access to the table
        //
        synchronized( table )
        {
            if( table == null )
            {
                return( null );
            }

            Netjconn conn = (Netjconn)table.get( connectionId );
            if( conn == null )
            {
                return( null );
            }
            return( conn );
        }
    }

    /**
     * Announce the creation of a new scene to all connections
     */
    private static void newScene( Netjconn conn, String scid, int nsn )
    {
        // tell all clients that the scene was created
        //
        Netjsendpacket announcement = new Netjsendpacket( conn, "RQ" );

        announcement.setString( "SET" );
        announcement.setString( "SCID", scid );
        announcement.setString( "CHID", vrmlnetj.hex8str( 0 ) );
        announcement.setString( "NPP", "" + 1 );
        announcement.setString( "NSN", "" + nsn );

        // send the announcement to all other connections
        //
        Netjconn.send( announcement, scid );
    }

    /**
     * Announce the deletion of a new scene to all connections
     */
    private static void delScene( Netjconn conn, String scid, int nsn )
    {
        // tell all clients that the scene was deleted
        //
        Netjsendpacket announcement = new Netjsendpacket( conn, "RQ" );

        announcement.setString( "BAD" );
        announcement.setString( "SCID", scid );
        announcement.setString( "NSN", "" + nsn );

        // send the announcement to all other connections
        //                  
        Netjconn.send( announcement, scid );
    }

    /**
     * Send a packet to all connections that are NOT in a given scene 
     */
    private static void send(
        Netjsendpacket packet, /** packet to send                            */
        String exceptSCID      /** do not send to connections with this SCID */
    )
    {
        if( table == null )
        {
            return;
        }

        // synchronize the access to the table
        //
        synchronized( table )
        {
            Set s = table.keySet();

            for( Iterator i = s.iterator(); i.hasNext(); )
            {
                String currkey = (String)i.next();
                Netjconn conn = (Netjconn)table.get( currkey );
                Netjscene scn = conn.scene; 

                if( scn == null )
                {
                    // do not send to this connection
                    //
                    continue;
                }
                if( exceptSCID != null && exceptSCID.equals( scn.SCID ))
                {
                    // do not send to this connection
                    //
                    continue;
                }

                // send the packet to this connection
                //
                packet.send( conn );
            }
        }
    }

    /**
     * Delete a connection when a BYE packet arrives
     */
    public void delete( )
    {
        if( table == null )
        {
            return;
        }

        // synchronize the access to the table
        //
        synchronized( table )
        {
            // remove the attributes
            //
            attributeMap = null;

            // remove the connection from the scene
            //
            if( scene != null )
            {
                int n = scene.deleteConnection( this );
                if( n < 1 )
                {
                    // tell the world that the scene is empty now
                    //
                    delScene( this, scene.SCID, scene.getNScenes() );
                }
                scene = null;
            }

            // the connection is remembered twice in the connection table
            //
            table.remove( CID );
            table.remove( CLID );

            vrmlnetj.log( "LOG", "DEL CONN " + CID + " CLID " + CLID );
        }
    }

    /**
     * set the last time a packet was sent on the connection
     */
    public void setLastSentTime()
    {
        lastSentTime = System.currentTimeMillis();
    }

    /**
     * set the last time a packet was received on the connection
     */
    public void setLastRecvTime()
    {
        lastRecvTime = System.currentTimeMillis();
    }

    /**
     * Periodic handling of connections
     */
    public static int periodic()
    {
        if( table == null )
        {
            newTable();
        }

        // synchronize the table access
        //
        synchronized( table )
        {
            long now = System.currentTimeMillis();

            // run through all connections stored
            //
            Set s = table.keySet();
            HashSet expired = new HashSet();

            for( Iterator i = s.iterator(); i.hasNext(); )
            {
                String currkey = (String)i.next();
                Netjconn current = (Netjconn)table.get( currkey );

                // we only handle the entry with the CID
                //
                if( current.CLID.equals( currkey ))
                {
                    continue;
                }

                // timeout idle connections
                //
                if( current.creationTime + 15000 < now )
                {
                    // timeout connections were nothing has happened at all
                    //
                    if( current.lastSentTime == 0
                     || current.lastRecvTime == 0
                     || current.scene == null )
                    {
                        // remove the attributes
                        //
                        current.attributeMap = null;

                        // remove the connection from the scene
                        //
                        if( current.scene != null )
                        {
                            int n = current.scene.deleteConnection( current );
                            if( n < 1 )
                            {
                                // tell the world that the scene is empty now
                                //
                                delScene( current, current.scene.SCID,
                                          current.scene.getNScenes() );
                            }
                        }
                        vrmlnetj.log( "LOG", "TOT CONN " + current.CID
                                              + " CLID " + current.CLID );

                        expired.add( current.CLID );
                        expired.add( currkey );
                        continue;
                    }

                    // if we have not sent anything in the last 10 seconds
                    //
                    if( current.lastSentTime + 10000 < now )
                    {
                        // send a PING packet on the connection
                        //
                        Netjsendpacket p = new Netjsendpacket( current, "RQ" );
                        p.setString( "PING" );
                        p.send( );
                        continue;
                    }

                    // timeout connections that have not sent anything
                    //
                    if( current.lastRecvTime + 30000 < now )
                    {
                        // remove the attributes
                        //
                        current.attributeMap = null;

                        // remove the connection from the scene
                        //
                        if( current.scene != null )
                        {
                            int n = current.scene.deleteConnection( current );
                            if( n < 1 )
                            {
                                // tell the world that the scene is empty now
                                //
                                delScene( current, current.scene.SCID,                                                    current.scene.getNScenes() );
                            }
                        }
                        vrmlnetj.log( "LOG", "TOT CONN " + current.CID
                                              + " CLID " + current.CLID );

                        expired.add( current.CLID );
                        expired.add( currkey );
                        continue;
                    }
                }
            }

            // delete expired elements from the table
            //
            for( Iterator i = expired.iterator(); i.hasNext(); )
            {
                String currkey = (String)i.next();
                Netjconn current = (Netjconn)table.get( currkey );

                if( current != null && !current.left 
                 && current.scene != null && currkey.equals( current.CID ))
                {
                    // tell all clients in the scene that the client left
                    //
                    Netjsendpacket announcement = new Netjsendpacket( current,
                                                                      "RQ" );
                    announcement.setString( "BYE" );
                    announcement.setString( "CLID", current.CLID );
                    announcement.setString( "CHID", current.scene.getChid() );
                    announcement.setString( "NPP", 
                                            "" + current.scene.nConnections );
                    announcement.setString( "NSN", 
                                            "" + current.scene.getNScenes() );

                    // send the announcement to all other connections
                    //                  
                    current.scene.send( announcement, current.CID );
                    current.left = true;
                }
                current.scene = null;

                table.remove( currkey );
            }
        }

        return( 0 );
    }

    /**
     * Create the connection table, synchronized
     */
    private synchronized static void newTable()
    {
        if( table == null )
        {
            table = new HashMap();
        }
    }
}
