/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2000-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * @(#)HelloHandler.java	1.71 06/28/07
 */ 

package com.sun.messaging.jmq.jmsserver.data.handlers;

import java.util.*;
import java.net.*;
import com.sun.messaging.jmq.auth.api.FailedLoginException;
import com.sun.messaging.jmq.jmsserver.data.PacketHandler;
import com.sun.messaging.jmq.util.log.Logger;
import com.sun.messaging.jmq.util.GoodbyeReason;

import com.sun.messaging.jmq.io.*;
import com.sun.messaging.jmq.net.*;
import com.sun.messaging.jmq.util.ServiceType;
import com.sun.messaging.jmq.jmsserver.service.Connection;
import com.sun.messaging.jmq.jmsserver.service.ConnectionUID;
import com.sun.messaging.jmq.jmsserver.service.imq.IMQConnection;
import com.sun.messaging.jmq.jmsserver.service.imq.IMQBasicConnection;
import com.sun.messaging.jmq.jmsserver.service.imq.IMQService;
import com.sun.messaging.jmq.jmsserver.service.ConnectionManager;
import com.sun.messaging.jmq.jmsserver.service.HAMonitorService;
import com.sun.messaging.jmq.jmsserver.util.BrokerException;
import com.sun.messaging.jmq.jmsserver.Globals;
import com.sun.messaging.jmq.jmsserver.resources.BrokerResources;
import com.sun.messaging.jmq.jmsserver.auth.AccessController;
import com.sun.messaging.jmq.jmsserver.auth.AuthCacheData;
import com.sun.messaging.jmq.jmsserver.util.memory.MemoryManager;
import com.sun.messaging.jmq.jmsserver.license.*;
import com.sun.messaging.jmq.jmsserver.cluster.*;
import com.sun.messaging.jmq.util.UID;
import com.sun.messaging.jmq.util.GoodbyeReason;




/**
 * Handler class which deals with the Hello message which is created when
 * a client starts talking to the broker.
 * Hello provides a "ping" message and also sets up a connection if the
 * protocol used does not have a unique way of determining a connection
 * (e.g. tcp does not need the HELLO message to set up a connection, since
 * each socket corresponds to a new connection)
 */
public class HelloHandler extends PacketHandler 
{
    private ConnectionManager connectionList;

    private Logger logger = Globals.getLogger();
    private BrokerResources rb = Globals.getBrokerResources();
    private static boolean DEBUG = false;

    private static boolean ALLOW_C_CLIENTS = false;
    private static boolean CAN_RECONNECT = false;

    static {
        try {
            LicenseBase license = Globals.getCurrentLicense(null);
            ALLOW_C_CLIENTS = license.getBooleanProperty(
                                license.PROP_ENABLE_C_API, false);
        } catch (BrokerException ex) {
            ALLOW_C_CLIENTS = false;
        }
        try {
            LicenseBase license = Globals.getCurrentLicense(null);
            CAN_RECONNECT = license.getBooleanProperty(
                                license.PROP_ENABLE_RECONNECT, false);
        } catch (BrokerException ex) {
            CAN_RECONNECT = false;
        }

    }

    public static void DUMP(String title) {
        Globals.getLogger().log(Logger.DEBUG,title);
        Globals.getLogger().log(Logger.DEBUG,"------------------------");
        Globals.getLogger().log(Logger.DEBUG,"Number of connections is " + 
                          Globals.getConnectionManager().getNumConnections(null));
        List l = Globals.getConnectionManager().getConnectionList(null);
        for (int i=0; i < l.size(); i ++ ) {
            Connection c= (Connection)l.get(i);
            Globals.getLogger().log(Logger.DEBUG,"\t" + i + "\t" +
                 c.getConnectionUID() + " :" + c.getRemoteConnectionString());
        }
        Globals.getLogger().log(Logger.DEBUG,"------------------------");
    }



    public HelloHandler(ConnectionManager list)
    {
        connectionList = list;
    }

    /**
     * Method to handle HELLO messages
     */
    public boolean handle(IMQConnection con, Packet msg) 
        throws BrokerException 
    { 

         if (DEBUG) {
             logger.log(Logger.DEBUGHIGH, "HelloHandler: handle() [ Received Hello Message]");
          }

          String reason = null;
          Hashtable hello_props = null;


          try {
              hello_props = msg.getProperties();
          } catch (Exception ex) {
              
              logger.log(Logger.INFO,"Internal Error: error "
                   + " retrieving properties from hello message ", ex);
             hello_props = new Hashtable();

          }

          boolean alreadyStarted = con.isStarted();
          boolean alreadyAuthenticated = con.isAuthenticated();

          int requestedProtocol = 0;
          int highestProtocol = con.getHighestSupportedProtocol();
          int lowestProtocol = PacketType.VERSION1;

          String expectedClusterID = null;
          UID expectedSessionID = null;
          ConnectionUID oldCID = null;
          Integer bufsize = null;
          boolean badClientType = false;
          if (hello_props != null) {
              Integer level = (Integer)hello_props.get("JMQProtocolLevel");
              if (level == null) {
                  requestedProtocol=PacketType.VERSION1;
              } else {
                  requestedProtocol=level.intValue();
              }
               bufsize = (Integer)hello_props.get("JMQSize");
              if (bufsize == null) { //XXX try old protocol
                  bufsize = (Integer)hello_props.get("JMQRBufferSize");
              }

              // Retrieve HA related properties
              Long longUID = (Long)hello_props.get("JMQStoreSession");
              if (longUID != null) {
                  expectedSessionID = new UID(longUID.longValue());
              }

              expectedClusterID = (String)hello_props.get("JMQClusterID");

              Boolean reconnectable = (Boolean)hello_props.get("JMQReconnectable");
              Boolean haclient = (Boolean)hello_props.get("JMQHAClient");
              if (Globals.getHAEnabled() && haclient != null && haclient.booleanValue()) {
                  reconnectable = haclient;
              }

              String s = (String)hello_props.get("JMQUserAgent");
              if (!ALLOW_C_CLIENTS && s != null &&  s.indexOf("C;") != -1) {
                  badClientType = true;
              }
              if (s != null) {
                con.addClientData(IMQConnection.USER_AGENT, s);
              }

              longUID = (Long)hello_props.get("JMQConnectionID");

              if (longUID != null) {
                  logger.log(Logger.DEBUG,"Have old connectionUID");
                  oldCID = new ConnectionUID(longUID.longValue());
                  logger.log(Logger.INFO,
                         BrokerResources.I_RECONNECTING, oldCID);
                  logger.log(Logger.DEBUG,"Checking for active connection");

                  Connection oldcon = Globals.getConnectionManager().getConnection(oldCID);
                  DUMP("Before connection Destroy");
                  if (oldcon != null) {
                      logger.log(Logger.DEBUG,"Destroying old connection " + oldCID);
                      oldcon.destroyConnection(true,GoodbyeReason.ADMIN_KILLED_CON, "Destroying old connection with same connectionUID " + oldCID + " - reconnect is happening before connection was reaped");
                  }
/* LKS
                  DUMP();

                  logger.log(Logger.DEBUG,"Updating connection in id list " +
                           "["+oldcid + "," + uid + "]");
                  // old code
                  con.setConnectionUID(oldcid);
                  Globals.getConnectionManager().updateConnectionUID(
                         oldcid, uid);
                  //Globals.getConnectionManager().updateConnectionUID(
                  //       uid, oldcid);
*/
                  DUMP("After Connection Destroy");
              }

              con.getConnectionUID().setCanReconnect(reconnectable == null ? false :
                      reconnectable.booleanValue());

              Long interval = (Long)hello_props.get("JMQInterval");

              // LKS - XXX just override for testing
              long itime = interval == null ? 10000 : interval.intValue();
              con.setReconnectInterval(itime);
               
          } else {
              requestedProtocol=PacketType.VERSION1;
          }

          int supportedProtocol = 0;
          if (requestedProtocol > highestProtocol) {
              supportedProtocol = highestProtocol;
          } else if (requestedProtocol < lowestProtocol) {
              supportedProtocol = lowestProtocol;
          }  else {
              supportedProtocol = requestedProtocol;
          }
          con.setClientProtocolVersion(supportedProtocol);

           if (bufsize != null) {
              logger.log(Logger.DEBUG, "Received JMQRBufferSize -" + bufsize);
              con.setFlowCount(bufsize.intValue());
           }


          Packet pkt = new Packet(con.useDirectBuffers());
          pkt.setPacketType(PacketType.HELLO_REPLY);
          pkt.setConsumerID(msg.getConsumerID());
          Hashtable hash = new Hashtable();
          reason = "unavailable";
          int status = Status.UNAVAILABLE;

          // If the connection's remote IP address was not set by the
          // protocol, then use the IP in the message packet.
          if (con.getRemoteIP() == null) {
            con.setRemoteIP(msg.getIP());
          }


          if ((alreadyAuthenticated || alreadyStarted) 
                  && !msg.getIndempotent() ) { // handle ibit
              status = Status.ERROR;
              reason = "Connection reuse not allowed";
              if (alreadyAuthenticated) {
                  logger.log(Logger.WARNING,"Internal Error: " +
                    " received HELLO on already authenticated connection "
                    + con.getRemoteConnectionString() +
                    " " + con.getConnectionUID());
              } else {
                  logger.log(Logger.WARNING,"Internal Error: " +
                    " received HELLO on already started connection "
                    + con.getRemoteConnectionString() +
                    " " + con.getConnectionUID());
              }
          
          } else if (badClientType) {
              logger.log(Logger.ERROR, rb.E_FEATURE_UNAVAILABLE,
                   Globals.getBrokerResources().getString(
                        BrokerResources.M_C_API));
              reason = "C clients not allowed on this version";
	      status = Status.UNAVAILABLE;
          } else if (!CAN_RECONNECT && con.getConnectionUID().getCanReconnect()) {
              logger.log(Logger.ERROR, rb.E_FEATURE_UNAVAILABLE,
                   Globals.getBrokerResources().getString(
                        BrokerResources.M_CLIENT_FAILOVER));
              reason = "Client Failover not allowed on this version";
          } else if (requestedProtocol != supportedProtocol) {
              // Bad protocol level.
              logger.log(Logger.WARNING, rb.W_BAD_PROTO_VERSION,
                Integer.toString(requestedProtocol), 
                Integer.toString(supportedProtocol));

              reason = "bad version";
	      status = Status.BAD_VERSION;
          } else if (con.getConnectionState() != Connection.STATE_UNAVAILABLE) {
              /** 
               * connection may not be able to be created e.g: 
               * licensing, being destroyed (e.g due to timeout)
               */
              if (con.setConnectionState(Connection.STATE_INITIALIZED)) {
                  reason = null;
                  status = Status.OK;
              } else {
                  status = Status.UNAVAILABLE;
	      }
          } else {
	      status = Status.UNAVAILABLE;
          }
          
          UID brokerSessionID = Globals.getBrokerSessionID();
          if (brokerSessionID!=null){
        	  hash.put("JMQBrokerSessionID",new Long(brokerSessionID.longValue()));
          }
          
          // OK, handle the HA properties HERE

          String clusterID = null;
          UID sessionUID = null;
          Set supportedSessions = null;

          ClusterManager cfg = Globals.getClusterManager();
          if (cfg != null) {
              clusterID = cfg.getClusterId();
              sessionUID = cfg.getStoreSessionUID();

              hash.put("JMQHA",Boolean.valueOf(cfg.isHA()));

              if (clusterID != null) {
                  hash.put("JMQClusterID", clusterID);
              }
              if (sessionUID != null)  {
                  hash.put("JMQStoreSession",new Long(sessionUID.longValue()));
              }

              String list = null;
              Iterator itr = null;
              if (((IMQService)con.getService()).getServiceType() != ServiceType.ADMIN) {
                  itr = cfg.getKnownBrokers(false);
              } else {
                  itr = cfg.getKnownBrokers(true);
              }
              Set s = new HashSet();
              // ok get rid of dups
              while (itr.hasNext()) {
                  ClusteredBroker cb = (ClusteredBroker)itr.next();
                  s.add(cb.getBrokerURL().toString());
              }
              // OK .. now convert to a string

              itr = s.iterator();

              while (itr.hasNext()) {
                  if (list == null) {
                      list = itr.next().toString();
                  } else {
                      list += "," + itr.next().toString();
                  }
              }
              if (list != null) {
                  hash.put("JMQBrokerList", list);
              }   
          }

          HAMonitorService hamonitor = Globals.getHAMonitorService(); 
          if (hamonitor != null && hamonitor.inTakeover()) {
              if (((IMQService)con.getService()).getServiceType() != ServiceType.ADMIN) {
                  status = Status.TIMEOUT;
                  if (oldCID != null) {
                      logger.log(logger.INFO,
                          BrokerResources.W_IN_TAKEOVER_RECONNECT_LATER, oldCID);
                  } else {
                      logger.log(logger.INFO,
                          BrokerResources.W_IN_TAKEOVER_RECONNECT_LATER,
                          con.getConnectionUID());
                  }
              }
          }
          
          // first we want to deal with a bad clusterid
          if (clusterID != null && expectedClusterID != null
                 && !clusterID.equals(expectedClusterID)) {
              status = Status.BAD_REQUEST;

          } else if (expectedSessionID != null && sessionUID != null &&
                     expectedSessionID.equals(sessionUID)) {
               // cool we connected to the right broker
               // we already have the right owner
          } else if (expectedSessionID != null) {


              if (cfg == null) { // not running any cluster config
                      logger.log(Logger.WARNING, 
                            BrokerResources.E_INTERNAL_BROKER_ERROR, 
                              "Internal Error: Received session on"
                            + " non-clustered broker");

                      status = Status.NOT_FOUND;

              } else {
                  // OK, if we are here, we need to locate the right
                  // broker for the session
                  //
                  // Here are the steps we need to check:
                  //  1. does this broker support the sessionUID
                  //     if not
                  //  2. can we locate another broker with the sessionUID
                  //

                  ClusteredBroker owner = null;

                  //
                  // OK, see if this was a session UID we took over at some
                  // point in the past
                  Set s = cfg.getSupportedStoreSessionUIDs();
                  if (s.contains(expectedSessionID)) {
                      // yep, we took it over
                      owner = cfg.getLocalBroker();
                  }

                  // OK, we dont have it in our list
                  //
                  // We want to find out who did take it over (although that
                  // information may be lost, there are no guarentees at this
                  // point since there is a limit to what we persistently store
    
                  if (owner == null) { // this broker isnt supprting the session
    
                      // see if the database indicates someone else has it
    
                      String ownerString = cfg.lookupStoreSessionOwner(expectedSessionID);
                      if (ownerString != null) {
                          owner = cfg.getBroker(ownerString);
                      }
                  }
                  try {
    
                      // ok, we didnt find it (that doesnt mean someone didnt
                      // take it over, just that we cant find out who)
                      // makesure we return the right error message
     
                      if (owner != null) {
                          ClusteredBroker creator = null;
                          String creatorString = cfg.getStoreSessionCreator(expectedSessionID); 
                          if (creatorString != null) {
                              creator = cfg.getBroker(creatorString);
                          }
                          int stat = owner.getStatus();
                          if (BrokerStatus.getBrokerInDoubt(stat) ||
                              !BrokerStatus.getBrokerLinkIsUp(stat) ||
                              owner.getState() == BrokerState.FAILOVER_STARTED) {
                              status = Status.TIMEOUT;
                              logger.log(logger.INFO, Globals.getBrokerResources().getKString(
                              BrokerResources.I_RECONNECT_OWNER_INDOUBT, expectedSessionID, owner));
                          } else if (!owner.isLocalBroker()) {
                              status = Status.MOVED_PERMANENTLY;
                              hash.put("JMQStoreOwner", owner.getBrokerURL().toString());
                              logger.log(logger.INFO, Globals.getBrokerResources().getKString(
                                     BrokerResources.I_RECONNECT_OWNER_NOTME, expectedSessionID, owner));
                          } else if (creator == null) { //XXX
                              status = Status.NOT_FOUND;
                              logger.log(logger.INFO, Globals.getBrokerResources().getKString(
                                         BrokerResources.I_RECONNECT_NOCREATOR, expectedSessionID));
                          } else if (creator.getState() == BrokerState.FAILOVER_STARTED) {
                              status = Status.TIMEOUT;
                              logger.log(logger.INFO, Globals.getBrokerResources().getKString(
                                         BrokerResources.I_RECONNECT_INTAKEOVER, expectedSessionID));
                          } else { // local broker owns us - set owner for debugging only
                                   // not required for protocol
                              hash.put("JMQStoreOwner", owner.getBrokerURL().toString());
                          }
                      } else { // didnt find owner
                          status = Status.NOT_FOUND;
                          logger.log(logger.INFO, Globals.getBrokerResources().getKString(
                                  BrokerResources.I_RECONNECT_OWNER_NOTFOUND, expectedSessionID));
                      }
                  } catch (Exception ex) {
                      logger.log(Logger.WARNING, 
                            BrokerResources.W_RECONNECT_ERROR, expectedSessionID.toString(), ex);
                      status = Status.NOT_FOUND;
                  }
             }

          }


          if (!con.isAdminConnection() && Globals.getMemManager() != null) {

              hash.put("JMQSize", new Integer(Globals.getMemManager().getJMQSize()));
              hash.put("JMQBytes", new Long(Globals.getMemManager().getJMQBytes()));
              hash.put("JMQMaxMsgBytes", new Long(Globals.getMemManager().getJMQMaxMsgBytes()));
          }
          if (reason != null) {
              hash.put("JMQReason", reason);
          }
          hash.put("JMQService", con.getService().getName());
          hash.put("JMQStatus", new Integer(status));
          hash.put("JMQConnectionID", new Long(con.getConnectionUID().longValue()));
          hash.put("JMQProtocolLevel",
                       new Integer(supportedProtocol));
          hash.put("JMQVersion",
                       Globals.getVersion().getProductVersion());
          if (((IMQBasicConnection)con).getDumpPacket() ||
                 ((IMQBasicConnection)con).getDumpOutPacket())
              hash.put("JMQReqID", msg.getSysMessageID().toString());

          try {
              // Added licensing description properties
              LicenseBase license = Globals.getCurrentLicense(null);
              hash.put("JMQLicense",
                  license.getProperty(LicenseBase.PROP_LICENSE_TYPE));
              hash.put("JMQLicenseDesc",
                  license.getProperty(LicenseBase.PROP_DESCRIPTION));
          } catch (BrokerException ex) {
              // This should never happen, but go ahead and at least
              // capture exception here
              hash.put("JMQLicenseDesc", ex.toString());
          }

          pkt.setProperties(hash);

	  con.sendControlMessage(pkt);

         // OK .. valid status messages are
         if (status != Status.OK && status != Status.MOVED_PERMANENTLY
              && status != Status.NOT_FOUND && status != Status.TIMEOUT) {
             // destroy the connection !!! (should be ok if destroy twice)
             con.closeConnection(true, GoodbyeReason.CON_FATAL_ERROR,
                  Globals.getBrokerResources().getKString(
                  BrokerResources.M_INIT_FAIL_CLOSE));
             connectionList.removeConnection(con.getConnectionUID(),
                  false,GoodbyeReason.CON_FATAL_ERROR,
                  Globals.getBrokerResources().getKString(
                  BrokerResources.M_INIT_FAIL_CLOSE));
             return true;
         }

          status = Status.UNAVAILABLE;
          String authType = null;
          if (hello_props != null) {
              authType = (String)hello_props.get("JMQAuthType");
          }
          AccessController ac = con.getAccessController();
          pkt = new Packet(con.useDirectBuffers());
          pkt.setPacketType(PacketType.AUTHENTICATE_REQUEST);
          pkt.setConsumerID(msg.getConsumerID());

          hash = new Hashtable();
          hash.put("JMQSequence", new Integer(msg.getSequence()));
          hash.put("JMQChallenge", Boolean.valueOf(true));

          Properties props = new Properties();
          props.setProperty(Globals.IMQ + ".clientIP", msg.getIPString());
          props.setProperty(Globals.IMQ + ".connectionID", con.getConnectionUID().toString());
          byte[] req = null;
          try {
          AuthCacheData acd = ((IMQService)con.getService()).getAuthCacheData();
          req = ac.getChallenge(msg.getSequence(), props, 
                                acd.getCacheData(), authType);
          hash.put("JMQAuthType", ac.getAuthType());
          if (con.setConnectionState(Connection.STATE_AUTH_REQUESTED)) {
          status = Status.OK;
          }

          } catch (FailedLoginException e) {
          logger.log(Logger.WARNING, e.getMessage(), e);
          status = Status.FORBIDDEN;
          } catch (OutOfMemoryError err) {
              // throw error so that memory is freed and
              // packet is re-processed
              throw err;
          } catch (Throwable w) {
          logger.log(Logger.ERROR, Globals.getBrokerResources().getKString(
              BrokerResources.E_GET_CHALLENGE_FAILED)+" - "+w.getMessage(), w);
          status = Status.FORBIDDEN;
          }
          hash.put("JMQStatus", new Integer(status));
          if (((IMQBasicConnection)con).getDumpPacket() ||
                ((IMQBasicConnection)con).getDumpOutPacket()) 
              hash.put("JMQReqID", msg.getSysMessageID().toString());

          pkt.setProperties(hash);
          if (req != null) {
              pkt.setMessageBody(req);
          }
          
          con.sendControlMessage(pkt);
          if (DEBUG) {
          logger.log(Logger.DEBUG,  "HelloHandler: handle() [ sent challenge ]"
                                      + ":status="+ Status.getString(status));
          }

          if (status != Status.OK && status != Status.MOVED_PERMANENTLY
              && status != Status.NOT_FOUND && status != Status.TIMEOUT) {
             // destroy the connection !!! (should be ok if destroy twice)
              con.closeConnection(true, GoodbyeReason.CON_FATAL_ERROR, 
                  Globals.getBrokerResources().getKString(
                  BrokerResources.M_INIT_FAIL_CLOSE));
              connectionList.removeConnection(con.getConnectionUID(),
                  false,GoodbyeReason.CON_FATAL_ERROR,
                  Globals.getBrokerResources().getKString(
                  BrokerResources.M_INIT_FAIL_CLOSE));
          }
          return true;

    }

}
