/*
 * JORAM: Java(TM) Open Reliable Asynchronous Messaging
 * Copyright (C) 2010 - 2025 ScalAgent Distributed Technologies
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA.
 *
 * Initial developer(s): ScalAgent Distributed Technologies
 * Contributor(s): 
 */
package org.objectweb.joram.mom.dest.jms;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

import jakarta.jms.Connection;
import jakarta.jms.ConnectionFactory;
import jakarta.jms.ExceptionListener;
import jakarta.jms.JMSException;
import jakarta.jms.JMSSecurityException;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.StringRefAddr;

import org.objectweb.joram.mom.dest.jms.JMSAcquisition.JmsListener;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

import fr.dyade.aaa.agent.AgentServer;
import fr.dyade.aaa.common.Daemon;
import fr.dyade.aaa.common.Debug;
import fr.dyade.aaa.common.crypto.AESGCMTools;
import fr.dyade.aaa.common.crypto.Crypto;
import fr.dyade.aaa.common.crypto.VoidTool;
import fr.dyade.aaa.util.management.MXWrapper;

public class JMSModule implements ExceptionListener, Serializable, JMSModuleMBean {
  /**
   * define serialVersionUID for interoperability, fix with 5.15.0 value.
   * This value is identical in 5.15.0, 5.16.0, 5.16.3, 5.17 and next. It is different
   * in 5.16.1 and 5.16.2 (and 5.17.0RC).
   */
  private static final long serialVersionUID = -4654256406604729021L;

  private static final Logger logger = Debug.getLogger(JMSModule.class.getName());
  private static final boolean DEBUG = logger.isLoggable(BasicLevel.DEBUG);

  private final static String ENCRYPT_CONFIGURATION_PROPERTY = "org.ow2.joram.jmsbridge.ENCRYPT_CONFIGURATION";
  private final static boolean ENCRYPT_CONFIGURATION = AgentServer.getBoolean(ENCRYPT_CONFIGURATION_PROPERTY);
  
  private final static String CRYPTO_ALGO_PROPERTY = "org.ow2.joram.jmsbridge.CRYPTO_ALGO";
  private final static String CRYPTO_DFLT_ALGO = "AES/GCM/NoPadding";
  private final static String CRYPTO_ALGO = AgentServer.getProperty(CRYPTO_ALGO_PROPERTY, CRYPTO_DFLT_ALGO);
  
  private final static String CRYPTO_PASSWORD_PROPERTY = "org.ow2.joram.jmsbridge.CRYPTO_PASWWORD";
  private final static String CRYPTO_DFLT_PASSWORD = "password";
  private final static String CRYPTO_PASSWORD = AgentServer.getProperty(CRYPTO_PASSWORD_PROPERTY, CRYPTO_DFLT_PASSWORD);
  private static final int CRYPTO_PASSWORD_MIN_LENGTH = 8;
  
  private final static String CRYPTO_SALT_PROPERTY = "org.ow2.joram.jmsbridge.CRYPTO_SALT";
  private final static String CRYPTO_DFLT_SALT = "saltsalt";
  private final static String CRYPTO_SALT = AgentServer.getProperty(CRYPTO_SALT_PROPERTY);
  private static final int CRYPTO_SALT_MIN_LENGTH = 8;

  // Note: Be careful, nonce/iv length needs to be 16 for somes algorithms.
  private final static String CRYPTO_NONCE_PROPERTY = "org.ow2.joram.jmsbridge.CRYPTO_NONCE";
  private final static String CRYPTO_DFLT_NONCE = "0123456789012345";
  // Note: Currently unused, the user name is used as nonce.
  private final static String CRYPTO_NONCE = AgentServer.getProperty(CRYPTO_NONCE_PROPERTY);

  private final static String CRYPTO_ITERATION_COUNT_PROPERTY = "org.ow2.joram.jmsbridge.CRYPTO_ITERATION_COUNT";
  private final static int CRYPTO_DFLT_ITERATION_COUNT = 10000;
  private final static int CRYPTO_ITERATION_COUNT = AgentServer.getInteger(CRYPTO_ITERATION_COUNT_PROPERTY, CRYPTO_DFLT_ITERATION_COUNT);

  private final static String CRYPTO_KEY_LENGTH_PROPERTY = "org.ow2.joram.jmsbridge.CRYPTO_KEY_LENGTH";
  private final static int CRYPTO_DFLT_KEY_LENGTH = 256;
  private final static int CRYPTO_KEY_LENGTH = AgentServer.getInteger(CRYPTO_KEY_LENGTH_PROPERTY, CRYPTO_DFLT_KEY_LENGTH);

  /** <code>true</code> if the module is fully usable. */
  protected transient boolean notUsable = false;

  /** Message explaining why the module is not usable. */
  protected transient String notUsableMessage;

  /** Daemon used for the reconnection process. */
  protected transient ReconnectionDaemon reconnectionDaemon;

  /** Connection to the foreign JMS server. */
  protected transient volatile Connection cnx;

  /**
   * @return the cnx
   */
  public Connection getCnx() {
    return cnx;
  }

  /** the name identifying the remote JMS provider */
  private String name;
  
  public String getName() {
    return name;
  }
  
  /** User identification for connecting to the foreign JMS server. */
  protected String userName = null;

  /**
   * @return the user name
   */
  public String getUserName() {
    return userName;
  }

  /**
   * User password for connecting to the foreign JMS server.
   * 
   * This field may be encrypted if JMSConnections.ENCRYPT_CONFIGURATION_PROPERTY is defined to true.
   */
  protected String password = null;
  
  /**
   * Contains decrypted value of password field, this field is lazily initialized.
   * Note (AF): Only used if ENCRYPT_CONFIGURATION is true.
   */
  private transient String password2 = null;
  
  private transient Crypto crypto = null;
  
  private Crypto getCrypto() {
    if (crypto == null) {
      try {
        String salt = (CRYPTO_SALT==null)?CRYPTO_DFLT_SALT:CRYPTO_SALT;
        String nonce = (CRYPTO_NONCE==null)?CRYPTO_DFLT_NONCE:CRYPTO_NONCE;
        
        if (CRYPTO_DFLT_PASSWORD.equals(CRYPTO_PASSWORD))
          logger.log(BasicLevel.WARN, "Do not use the default master password.");
        if (CRYPTO_PASSWORD.length() < CRYPTO_PASSWORD_MIN_LENGTH)
          logger.log(BasicLevel.WARN, "Master password length must be at least " + CRYPTO_PASSWORD_MIN_LENGTH);
        if (salt.length() < CRYPTO_SALT_MIN_LENGTH)
          logger.log(BasicLevel.WARN, "Salt length must be at least " + CRYPTO_SALT_MIN_LENGTH);
        
        crypto = AESGCMTools.createCrypto(CRYPTO_ALGO, CRYPTO_PASSWORD, salt, nonce, CRYPTO_ITERATION_COUNT, CRYPTO_KEY_LENGTH);
      } catch (NoSuchAlgorithmException|InvalidKeySpecException exc) {
        String errorMsg = "Cannot initialize cryptographic module: ";
        if (DEBUG)
          logger.log(BasicLevel.WARN, errorMsg, exc);
        else
          logger.log(BasicLevel.WARN, errorMsg + exc);
        crypto = new VoidTool();
      }
      if (DEBUG)
        logger.log(BasicLevel.DEBUG, crypto.getClass().getName());
    }
    return crypto;
  }
  
  private String encrypt(String clearText, String nonce) throws UnsupportedEncodingException, GeneralSecurityException {
    if ((nonce == null) || (clearText == null))
      throw new GeneralSecurityException("Cannot encrypt null (" + nonce + ").");
    
    getCrypto();
    return Base64.getEncoder().encodeToString(crypto.encrypt(clearText.getBytes("UTF-8"), nonce.getBytes("UTF-8")));
  }
  
  private String decrypt(String cypherText, String nonce) throws UnsupportedEncodingException, GeneralSecurityException {
    if ((nonce == null) || (cypherText == null))
      throw new GeneralSecurityException("Cannot decrypt null (" + nonce + ").");
    
    getCrypto();
    return new String(crypto.decrypt(Base64.getDecoder().decode(cypherText), nonce.getBytes("UTF-8")), "UTF-8");
  }
  
  /** Name of the JNDI factory class to use. */
  protected String jndiFactory = null;
  
  public String getNamingFactory() {
    return jndiFactory;
  }

  /** JNDI URL. */
  protected String jndiUrl = null;
  
  public String getNamingURL() {
    return jndiUrl;
  }

  /** ConnectionFactory JNDI name. */
  protected String cnxFactName;

  /**
   * @return the cnxFactName
   */
  public String getCnxFactName() {
    return cnxFactName;
  }

  /** JMS clientID field. */
  protected String clientID = null;
  
  public String getClientID() {
    return clientID;
  }

  /** Connection factory object for connecting to the foreign JMS server. */
  protected transient ConnectionFactory cnxFact = null;

  public JMSModule(String name, String cnxFactoryName, String jndiFactoryClass, String jndiUrl, String user, String password, String clientID) {
    if (DEBUG)
      logger.log(BasicLevel.DEBUG, "JMSModule.<init>: encryption=" + ENCRYPT_CONFIGURATION);

    this.name = name;
    
    this.jndiFactory = jndiFactoryClass;
    this.jndiUrl = jndiUrl;

    this.cnxFactName = cnxFactoryName;
    if (cnxFactName == null)
      throw new IllegalArgumentException("Missing ConnectionFactory JNDI name.");

    this.userName = user;
    if (ENCRYPT_CONFIGURATION && ((user != null) && (password != null))) {
      this.password2 = password;
      try {
        this.password = encrypt(password, user);
      } catch (Exception exc) {
        String errorMsg = "Error during encryption: ";
        if (DEBUG)
          logger.log(BasicLevel.WARN, errorMsg, exc);
        else
          logger.log(BasicLevel.WARN, errorMsg + exc);
        this.password = password;
      }
    } else {
      this.password = password;
    }

    this.clientID = clientID;
  }

  public void stopLiveConnection() {
    if (DEBUG) {
      logger.log(BasicLevel.DEBUG, "JMSModule.stopLiveConnection()");
    }

    // Waits for end of initialization if needed (JORAM-290)
    while (getStatus() <= STARTING) {
      logger.log(BasicLevel.WARN, "JMSModule.stopLiveConnection: Wait for StartupDaemon");
      try {
        Thread.sleep(1000L);
      } catch (InterruptedException exc) {}
    }
    
    if (cnx != null) {
      try {
        cnx.setExceptionListener(null);
      } catch (JMSException exc) {
        logger.log(BasicLevel.WARN, "JMSModule.stopLiveConnection", exc);
      }
    }

    if (cnx != null) {
      try {
        cnx.stop();
      } catch (JMSException exc) {
        logger.log(BasicLevel.WARN, "JMSModule.stopLiveConnection", exc);
      }
    }

    if (reconnectionDaemon != null) {
      try {
        reconnectionDaemon.stop();
      } catch (Exception exc) {
        logger.log(BasicLevel.WARN, "JMSModule.stopLiveConnection", exc);
      }
    }

    if (cnx != null) {
      try {
        cnx.close();
      } catch (JMSException exc) {
        logger.log(BasicLevel.WARN, "JMSModule.stopLiveConnection", exc);
      }
    }

    try {
      MXWrapper.unregisterMBean(getMBeanName());
    } catch (Exception exc) {
      if (DEBUG)
        logger.log(BasicLevel.DEBUG, "unregisterMBean", exc);
    }
  }

  /**
   * Launches the connection process to the foreign JMS server.
   * 
   * @exception javax.jms.IllegalStateException
   *              If the module can't access the foreign JMS server.
   * @exception javax.jms.JMSException
   *              If the needed JMS resources can't be created.
   */
  public void startLiveConnection() {
    if (DEBUG)
      logger.log(BasicLevel.DEBUG, "JMSModule.startLiveConnection()");
    
    // TODO (AF): is it correct to use the previous result to abort this new try.
    if (notUsable) {
      if (logger.isLoggable(BasicLevel.ERROR))
        logger.log(BasicLevel.ERROR, "JMSModule.startLiveConnection, not usable: " + notUsableMessage);
      return;
    }

    // Creating the module's daemons.
    reconnectionDaemon = new ReconnectionDaemon();

    StartupDaemon startup = new StartupDaemon();
    startup.start();

    try {
      MXWrapper.registerMBean(this, getMBeanName());
    } catch (Exception e) {
      if (DEBUG)
        logger.log(BasicLevel.DEBUG, "registerMBean", e);
    }

  }

  /**
   * Retrieves ConnectionFactory and Destination objects from JNDI.
   * This method converts 'old' javax.jms references to the new Jakarta JMS API.
   * 
   * TODO (AF): We should replace this code by a call to Helper.retrieveJakartaJMSObjectFromJNDI method (org.ow2.joram.jndi package).
   * 
   * @param jndiName
   * @return
   * @throws Exception
   */
  protected Object retrieveJndiObject(String jndiName) throws Exception {
    Context jndiCtx = null;
    ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader();

    try {
      jndiCtx = getInitialContext();
      Object obj = jndiCtx.lookup(jndiName);
      
      if (logger.isLoggable(BasicLevel.INFO))
        logger.log(BasicLevel.INFO, "JMSModule[" + name + "] retrieveJndiObject(" + jndiName + ") gets " + obj);
     
      if ((obj instanceof org.ow2.joram.jakarta.jms.ConnectionFactory) ||
          (obj instanceof org.ow2.joram.jakarta.jms.Destination)) {
        return obj;
      }
      
      if (obj instanceof org.objectweb.joram.client.jms.ConnectionFactory) {
        org.objectweb.joram.client.jms.ConnectionFactory cf1 = (org.objectweb.joram.client.jms.ConnectionFactory) obj;
        javax.naming.Reference ref1 = cf1.getReference();
        
        javax.naming.Reference ref2 = new Reference(this.getClass().getName(), org.ow2.joram.jakarta.jms.admin.ObjectFactory.class.getName(), null);

        Enumeration<RefAddr> refAddrs = ref1.getAll();
        while (refAddrs.hasMoreElements()) {
          RefAddr ra = refAddrs.nextElement();
          if ("cf.reliableClass".equals(ra.getType())) {
            if (ra.getContent() != null)
              ref2.add(new StringRefAddr(ra.getType(), ((String) ra.getContent()).replace("objectweb.joram.client", "ow2.joram.jakarta")));
            else
              ref2.add(new StringRefAddr(ra.getType(), null));
          } else {
            ref2.add(ra);
          }
        }
        
        String cn = ref1.getClassName().replace("objectweb.joram.client", "ow2.joram.jakarta");
        org.ow2.joram.jakarta.jms.ConnectionFactory cf2 = null;
        try {
          cf2 = (org.ow2.joram.jakarta.jms.ConnectionFactory) Class.forName(cn).newInstance();
        } catch (Exception exc) {
          logger.log(BasicLevel.WARN, "JMSModule[" + name + "] retrieveJndiObject(" + jndiName + ") Can't transform to Jakarta/JMS: " + obj);
          return obj;
        }
        cf2.fromReference(ref2);
        
        if (logger.isLoggable(BasicLevel.INFO))
          logger.log(BasicLevel.INFO, "JMSModule[" + name + "] retrieveJndiObject(" + jndiName + ") returns " + cf2);

        return cf2;
      } else if (obj instanceof org.objectweb.joram.client.jms.Destination) {
        org.objectweb.joram.client.jms.Destination dest1 = (org.objectweb.joram.client.jms.Destination) obj;
        javax.naming.Reference ref = dest1.getReference();
        String cn = ref.getClassName().replace("objectweb.joram.client", "ow2.joram.jakarta");
        org.ow2.joram.jakarta.jms.Destination dest2 = null;
        try {
          dest2 = (org.ow2.joram.jakarta.jms.Destination) Class.forName(cn).newInstance();
        } catch (Exception exc) {
          logger.log(BasicLevel.WARN, "JMSModule[" + name + "] retrieveJndiObject(" + jndiName + ") Can't transform to Jakarta/JMS: " + obj);
          return obj;
        }
        dest2.fromReference(ref);
        
        if (logger.isLoggable(BasicLevel.INFO))
          logger.log(BasicLevel.INFO, "JMSModule[" + name + "] retrieveJndiObject(" + jndiName + ") returns " + dest2);

        // Cannot transform, returns JNDI result.
        return dest2;
      }
      
      return obj;
    } finally {
      // Closing the JNDI context.
      if (jndiCtx != null) {
        jndiCtx.close();
      }
      Thread.currentThread().setContextClassLoader(oldClassLoader);
    }
  }

  protected Context getInitialContext() throws IOException, NamingException {
    if (DEBUG)
      logger.log(BasicLevel.DEBUG, "getInitialContext() - Load jndi.properties file");
    
    Context jndiCtx;
    Properties props = new Properties();
    InputStream in = Class.class.getResourceAsStream("/jndi.properties");
    
    if (in == null) {
      if (DEBUG)
        logger.log(BasicLevel.DEBUG, "jndi.properties not found.");
    } else {
      props.load(in);
    }

    // Override jndi.properties with properties given at initialization if present
    if (jndiFactory != null) {
      props.setProperty(Context.INITIAL_CONTEXT_FACTORY, jndiFactory);
    }
    if (jndiUrl != null) {
      props.setProperty(Context.PROVIDER_URL, jndiUrl);
    }

    Thread.currentThread().setContextClassLoader(this.getClass().getClassLoader());
    jndiCtx = new InitialContext(props);
    return jndiCtx;
  }

  /**
   * Opens a connection with the foreign JMS server and creates the JMS
   * resources for interacting with the foreign JMS destination.
   * 
   * @exception JMSException
   *              If the needed JMS resources could not be created.
   */
  protected void doConnect() throws JMSException {
    if (DEBUG)
      logger.log(BasicLevel.DEBUG, "doConnect()");

    if (userName != null && password != null) {
      if (ENCRYPT_CONFIGURATION) {
        if (password2 == null) {
          try {
            password2 = decrypt(password, userName);
          } catch(Exception exc) {
            String errorMsg = "Error during decryption: ";
            if (DEBUG)
              logger.log(BasicLevel.WARN, errorMsg, exc);
            else
              logger.log(BasicLevel.WARN, errorMsg + exc);
            password2 = password;
          }
        }
        cnx = cnxFact.createConnection(userName, password2);
      } else {
        cnx = cnxFact.createConnection(userName, password);
      }
    } else {
      cnx = cnxFact.createConnection();
    }
    
    try {
      if (clientID == null) {
        // Be careful, durable subscriptions need a ClientID !!
        logger.log(BasicLevel.WARN,
                   "JMSModule.doConnect: Connection " + name + " does not define a ClientID");
//        cnx.setClientID("joramBridge_" + name + "_" + AgentServer.getServerId());
      } else {
        cnx.setClientID(clientID);
      }

      cnx.setExceptionListener(this);

      if (DEBUG)
        logger.log(BasicLevel.DEBUG, "doConnect: cnx=" + cnx);
    } catch (JMSException exc) {
      if (logger.isLoggable(BasicLevel.WARN)) {
        logger.log(BasicLevel.WARN, "Connection " + name + " failed", exc);
      }
      if (cnx != null) {
        try {
          cnx.close();
        } catch (JMSException exc2) {
          if (logger.isLoggable(BasicLevel.WARN)) {
            logger.log(BasicLevel.WARN, "Can't close failed connection", exc2);
          }
        } finally {
          cnx = null;
        }
      }
      throw exc;
    }
  }

  /**
   * Implements the <code>javax.jms.ExceptionListener</code> interface for
   * catching the failures of the connection to the remote JMS server.
   * <p>
   * Reacts by launching a reconnection process.
   */
  public void onException(JMSException exc) {
    if (DEBUG)
      logger.log(BasicLevel.WARN, "JMSModule.onException()", exc);
    else
      logger.log(BasicLevel.WARN, "JMSModule.onException(): " + exc.getMessage());

    if (listeners != null) {
  		for (Iterator<JmsListener> listener = listeners.iterator(); listener.hasNext();) {
  			JmsListener type = listener.next();
  			type.onException(exc);
  		}
  		listeners.clear();
  	}
  	
  	// JORAM-306: Close the connection
    if (cnx != null) {
      try {
        cnx.close();
      } catch (JMSException exc2) {
        if (DEBUG)
          logger.log(BasicLevel.INFO, "JMSModule.onException(), error closing the connection", exc2);
        else
          logger.log(BasicLevel.INFO, "JMSModule.onException(), error closing the connection: " + exc2.getMessage());
      }
    }

  	reconnectionDaemon.start();
  }

  public boolean isConnectionOpen() {
    return cnx != null && !reconnectionDaemon.isRunning();
  }

  private int status = UNKNOWN;
  
  public final static int UNKNOWN = -1;
  public final static int STARTING = 0;
  public final static int OK = 1;
  public final static int FAILING = 2;

  public int getStatus() {
    if (status <= STARTING)
      return status;
    else if (isConnectionOpen())
      return OK;
    else
      return FAILING;
  }
  
  public String getState() {
    switch (getStatus()) {
    case STARTING:
      return "STARTING";
    case OK:
      return "OK";
    case FAILING:
      return "FAILING";
    }
    return "UNKNOWN";
  }

  private String getMBeanName() {
    StringBuilder strbuf = new StringBuilder();

    strbuf.append(JMSConnectionService.JMXBaseName).append(AgentServer.getServerId());
    strbuf.append(':');
    strbuf.append("type=Connections,name=").append(name);

    return strbuf.toString();
  }

  /**
   * The <code>StartupDaemon</code> thread is responsible for retrieving the
   * needed JMS administered objects from the JNDI server.
   */
  protected class StartupDaemon extends Daemon {
    /** Constructs a <code>StartupDaemon</code> thread. */
    protected StartupDaemon() {
      super("StartupDaemon", logger);
      setDaemon(false);
      if (DEBUG)
        logmon.log(BasicLevel.DEBUG, "StartupDaemon<init>");
    }

    /** The daemon's loop. */
    public void run() {
      status = STARTING;
      if (DEBUG)
        logmon.log(BasicLevel.DEBUG, "run()");
      
      try {
        canStop = true;

        // TODO (AF): We should set notUsable=false and notUsableMessage=null, then we should log the error
        // in the final clause.
        // Be careful, notUsable is used in startLiveConnection but on the next call. Is this correct?
        if (cnxFact == null) {
          // Administered objects still to be retrieved: getting them from JNDI.
          cnxFact = (ConnectionFactory) retrieveJndiObject(cnxFactName);
        }
        try {
          doConnect();
          status = OK;
          if (logmon.isLoggable(BasicLevel.INFO)) {
            logmon.log(BasicLevel.INFO,
                       "StartupDaemon: " + name + " connected using " + cnxFactName + " ConnectionFactory.");
          }
        } catch (AbstractMethodError exc) {
          notUsable = true;
          notUsableMessage = "Retrieved administered objects types not compatible with the 'unified' communication mode: " + exc;
          if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.DEBUG, notUsableMessage, exc);
        } catch (ClassCastException exc) {
          notUsable = true;
          notUsableMessage = "Retrieved administered objects types not compatible with the chosen communication mode: " + exc;
          if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.DEBUG, notUsableMessage, exc);
        } catch (JMSSecurityException exc) {
          notUsable = true;
          notUsableMessage = "Provided user identification does not allow to connect to the foreign JMS server: " + exc;
          if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.DEBUG, notUsableMessage, exc);
        } catch (JMSException exc) {
          if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.DEBUG, "Exception:: ", exc);
          reconnectionDaemon.start();
        } catch (Throwable exc) {
          notUsable = true;
          notUsableMessage = exc.toString();
          if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.DEBUG, "Exception: ", exc);
        }
      } catch (NameNotFoundException exc) {
        if (cnxFact == null)
          notUsableMessage = "Could not retrieve ConnectionFactory [" + cnxFactName + "] from JNDI: " + exc;
        if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.DEBUG, notUsableMessage, exc);
        reconnectionDaemon.start();
      } catch (javax.naming.NoInitialContextException exc) {
        notUsable = true;
        notUsableMessage = "Initial context not available: " + exc;
        if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.DEBUG, notUsableMessage, exc);
      } catch (javax.naming.NamingException exc) {
        notUsableMessage = "Could not access JNDI: " + exc;
        if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.DEBUG, notUsableMessage, exc);
        reconnectionDaemon.start();
      } catch (ClassCastException exc) {
        notUsable = true;
        notUsableMessage = "Error retrieving administered objects via JNDI, possibly due to missing foreign JMS client libraries in the classpath: " + exc;
        if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.DEBUG, notUsableMessage, exc);
      } catch (Exception exc) {
        notUsable = true;
        notUsableMessage = "Error while retrieving administered objects " + "through JNDI: " + exc;
        if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.DEBUG, notUsableMessage, exc);
      } finally {
        // Removes the STARTING status if the connection is not established.
        if (status != OK)
          status = FAILING;
        finish();
      }
    }

    /** Shuts the daemon down. */
    public void shutdown() {
    }

    /** Releases the daemon's resources. */
    public void close() {
    }
  }

  /**
   * The <code>ReconnectionDaemon</code> thread is responsible for reconnecting
   * the bridge module with the foreign JMS server in case of disconnection.
   */
  protected class ReconnectionDaemon extends Daemon {
    /** Number of reconnection trials of the first step. */
    private int attempts1 = 30;

    /** Retry interval (in milliseconds) of the first step. */
    private long interval1 = 1000L;

    /** Number of reconnection trials of the second step. */
    private int attempts2 = 55;

    /** Retry interval (in milliseconds) of the second step. */
    private long interval2 = 5000L;

    /** Retry interval (in milliseconds) of the third step. */
    private long interval3 = 60000L;

    /** Constructs a <code>ReconnectionDaemon</code> thread. */
    protected ReconnectionDaemon() {
      super("ReconnectionDaemon", logger);
      setDaemon(false);
      if (DEBUG) {
        logmon.log(BasicLevel.DEBUG, "ReconnectionDaemon<init>");
      }
    }

    /** The daemon's loop. */
    public void run() {
      if (DEBUG)
        logmon.log(BasicLevel.DEBUG, "ReconnectionDaemon.run()");

      int attempts = 0;
      long interval;

      try {
        while (running) {
          attempts++;

          if (attempts <= attempts1) {
            interval = interval1;
          } else if (attempts <= attempts2) {
            interval = interval2;
          } else {
            interval = interval3;
          }

          canStop = true;
          try {
            if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG, "ReconnectionDaemon: attempt " + attempts + ", wait=" + interval);
            Thread.sleep(interval);

            if (DEBUG) {
              logmon.log(BasicLevel.DEBUG, "ReconnectionDaemon: connect...");
            }
            canStop = false;

            if (cnxFact == null) {
              // Administered objects still to be retrieved: getting them from JNDI.
              cnxFact = (ConnectionFactory) retrieveJndiObject(cnxFactName);
            }
            doConnect();
            if (logmon.isLoggable(BasicLevel.INFO)) {
              logmon.log(BasicLevel.INFO,
                         "ReconnectionDaemon: " + name + " connected using " + cnxFactName + " ConnectionFactory.");
            }
          } catch (Exception exc) {
            if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG, "ReconnectionDaemon: connection " + name + " failed, continue... " + exc.getMessage());
            // TODO (AF): May be we have to get anew the ConnectionFactory
            continue;
          }
          break;
        }
      } finally {
        finish();
      }
    }

    /** Shuts the daemon down. */
    public void shutdown() {
      interrupt();
    }

    /** Releases the daemon's resources. */
    public void close() {
    }
  }

  private transient List<JmsListener> listeners;

  void removeExceptionListener(JmsListener listener) {
    if ((listeners != null) && (listener != null)) {
      listeners.remove(listener);
    }
  }
  
  void addExceptionListener(JmsListener listener) {
    if (listeners == null) {
      listeners = new ArrayList<JmsListener>();
    }
    listeners.add(listener);
  }

}
