/*
 * Copyright (C) 2001 - 2025 ScalAgent Distributed Technologies
 * Copyright (C) 2004 France Telecom R&D
 * Copyright (C) 1996 - 2000 BULL
 * Copyright (C) 1996 - 2000 INRIA
 *
 * 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): Dyade
 * Contributor(s): ScalAgent Distributed Technologies
 */
package fr.dyade.aaa.agent;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Random;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.objectweb.util.monolog.api.LoggingFactory;

import fr.dyade.aaa.agent.conf.A3CML;
import fr.dyade.aaa.agent.conf.A3CMLConfig;
import fr.dyade.aaa.agent.conf.A3CMLDomain;
import fr.dyade.aaa.agent.conf.A3CMLNat;
import fr.dyade.aaa.agent.conf.A3CMLNetwork;
import fr.dyade.aaa.agent.conf.A3CMLProperty;
import fr.dyade.aaa.agent.conf.A3CMLServer;
import fr.dyade.aaa.agent.conf.A3CMLService;
import fr.dyade.aaa.agent.conf.Log;
import fr.dyade.aaa.agent.conf.ParseException;
import fr.dyade.aaa.common.Configuration;
import fr.dyade.aaa.common.Debug;
import fr.dyade.aaa.common.encoding.EncodableFactoryRepository;
import fr.dyade.aaa.util.Transaction;
import fr.dyade.aaa.util.backup.BackupRecord;
import fr.dyade.aaa.util.backup.RestoreFile;
import fr.dyade.aaa.util.management.MXWrapper;

/**
 * The <code>AgentServer</code> class manages the global configuration
 * of an agent server. It reads the configuration file, then it creates
 * and configure {@link Engine}, {@link Channel} and {@link Network}
 * components.
 * This class contains the main method for AgentServer, for example to
 * activate a server you have to run this class with two parameters: the
 * server id. and the path of the root of persistency. You can also use
 * a specialized main calling methods init and start.
 * <hr>
 * To start the agents server an XML configuration file describing the
 * architecture of the agent platform is needed. By default, this file is 
 * the a3servers.xml file and it should be located inside the running
 * directory where the server is launched. Each server must use the same
 * configuration file.<p>
 * The configuration file contains a <code>config</code> element, that
 * is essentially made up of <code>domain</code>s elements, and servers
 * (<code>server</code>s elements):
 * <ul>
 * <li>Each domain of the configuration is described by an XML element with
 * attributes giving the name and the classname for <code>Network</code>
 * implementation (class {@link SimpleNetwork} by default).
 * <li>Each server is described by an XML element with attributes giving
 * the id, the name (optional) and the node (the <code>hostname</code>
 * attribute describes the name or the IP address of this node)
 * <ul>
 * <li>Each persistent server must be part of a domain (at least one), to
 * do this you need to define a <code>network</code> element with attributes
 * giving the domain's name (<code>domain</code> attribute) and the
 * communication port (<code>port</code> attribute).
 * <li>A service can be declared on any of these servers by inserting a
 * <code>service</code> element describing it.
 * </ul>
 * <li>Additionally, you can define property for the global configuration
 * or for a particular server, it depends where you define it: in the
 * <code>config</code> element or in a server one.
 * </ul>
 * Each server that is part of two domains is named a "router", be careful,
 * it should have only one route between two domains. If it is not true the
 * configuration failed.
 * <hr>
 * A simple example of a3servers.xml follows:
 * <blockquote><pre>
 * &lt;?xml version="1.0"?&gt;
 * &lt;!DOCTYPE config SYSTEM "a3config.dtd"&gt;
 * 
 * &lt;config&gt;
 *   &lt;domain name="D1"/&gt;
 *   &lt;domain name="D2" class="fr.dyade.aaa.agent.PoolNetwork"/&gt;
 *
 *   &lt;property name="D2.nbMaxCnx" value="1"/&gt;
 *
 *   &lt;server id="0" name="S0" hostname="acores"&gt;
 *     &lt;network domain="D1" port="16300"/&gt;
 *     &lt;service class="fr.dyade.aaa.agent.AdminProxy" args="8090"/&gt;
 *     &lt;property name="A3DEBUG_PROXY" value="true"/&gt;
 *   &lt;/server&gt;
 *
 *   &lt;server id="2" name="S2" hostname="bermudes"&gt;
 *     &lt;network domain="D1" port="16310"/&gt;
 *     &lt;network domain="D2" port="16312"/&gt;
 *   &lt;/server&gt;
 * 
 *   &lt;server id="3" name="S3" hostname="baleares"&gt;
 *     &lt;network domain="D2" port="16320"/&gt;
 *   &lt;/server&gt;
 * &lt;/config&gt;
 * </pre></blockquote>
 * <p>
 * This file described a 2 domains configuration D1 and D2, D1 with default
 * network protocol and D2 with the <code>PoolNetwork</code> one, and 4
 * servers:
 * <ul>
 * <li>The first server (id 0 and name "S0") is hosted by acores, it is
 * in domain D1, and listen on port 16300. It defines a service and a
 * property.
 * <li>The second server (id 2) is hosted by bermudes and it is the router
 * between D1 and D2.
 * <li>The last server is a persistent one, it is hosted on baleares and it
 * runs in domain D2.
 * </ul>
 * At the beginning of the file, there is a global property that defines
 * the maximum number of connection handled by each server of domain D2.
 * <hr>
 * @see Engine
 * @see Channel
 * @see Network
 * @see MessageQueue
 * @see fr.dyade.aaa.util.Transaction
 */
public final class AgentServer {
  private static Logger logmon = Debug.getLogger(AgentServer.class.getName());
  
  public final static short NULL_ID = -1;
  
  public final static String ADMIN_DOMAIN = "D0";
  public final static String ADMIN_SERVER = "s0";

  public static final int ENCODABLE_CLASS_ID_AREA = 0x10000;
  public static final int MESSAGE_CLASS_ID = ENCODABLE_CLASS_ID_AREA + 0;
  
  static {
    registerFactories();
  }
  
  public final static void registerFactories() {
    EncodableFactoryRepository.putFactory(MESSAGE_CLASS_ID, new Message.Factory());
  }

  /**
   * Name of the transaction object indicating that the configuration object has been
   * updated in the transaction base. This makes it possible to initialize the server
   * in a slightly different way, for example reading anew the services list.
   * 
   * Currently we simply consider the fact that the configuration has been modified, in
   * the future this object could contain indications on the changes to be made.
   */
  public static final String UPDATE_CONF_TX_NAME = "updatecfg";
  
  /**
   * Name of the property defining the backup file to restore.
   * By default: "backup.tbck".
   */
  public static final String BACKUP_FILE = "BACKUP_FILE";
  
  /**
   * Default name of the backup file to restore if it exists.
   * The restore operation is only done if the transaction doesn't exist.
   */
  public static final String DEFAULT_BACKUP_FILE = "backup.tbck";
  
  /**
   * Object indicating that the configuration object has been updated in the transaction
   * base (see UpdateConfTxName).
   */
  public static ServerUpdate updatecfg = null;
  
  /**
   * Default configuration used if no other configuration is found, by default empty.
   */
  public static String defaultConfig = null;
  
  /**
   * Set default configuration for the specified server.
   * @param sid the server identifier.
   */
  public static void setDefaultConfig(int sid) {
    setDefaultConfig(sid, null, null, null, 0, 0);
  }

  /**
   * Set default configuration for the specified server.
   * 
   * @param sid       the server identifier.
   * @param host      the hostname (or IP adress) hosting the sever
   * @param adminuid  the administration login
   * @param adminpwd  the administration password
   * @param joram     the JMS TCP connector port
   * @param jndi      the JNDI TCP port
   */
  public static void setDefaultConfig(int sid, String host, String adminuid, String adminpwd, int joram, int jndi) {
    setDefaultConfig(sid, host, adminuid, adminpwd, joram, jndi, null);
  }
  
  /**
   *  Name of property allowing to configure the administrator user name when using the
   * default server configuration, by default "root". This Configuration is automatically
   * generated at first starting if no XML configuration file is found.
   * <p>
   *  Be careful, this configuration is normally used only for the initial starting of the
   * server, the configuration is then atomically maintained in the persistence directory.
   * <p>
   *  This property can only be fixed from <code>java</code> launching command.
   */
  public final static String CFG_ADMINUID_PROPERTY = "fr.dyade.aaa.agent.A3CONF_ADMINUID";
  
  /**
   *  Name of property allowing to configure the administrator password when using the
   * default server configuration, by default it is the same that the administrator user
   * name. This Configuration is automatically generated at first starting if no XML
   * configuration file is found.
   * <p>
   *  Be careful, this configuration is normally used only for the initial starting of the
   * server, the configuration is then atomically maintained in the persistence directory.
   * <p>
   *  This property can only be fixed from <code>java</code> launching command.
   */
  public final static String CFG_ADMINPWD_PROPERTY = "fr.dyade.aaa.agent.A3CONF_ADMINPWD";
  
  /**
   *  Name of property allowing to configure the listening port of the JMS server when
   * using the default server configuration, by default CFG_JMS_PORT_DFLT. This Configuration
   * is automatically generated at first starting if no XML configuration file is found.
   * <p>
   *  Be careful, this configuration is normally used only for the initial starting of the
   * server, the configuration is then atomically maintained in the persistence directory.
   * <p>
   *  This property can only be fixed from <code>java</code> launching command.
   */
  public final static String CFG_JMS_PORT_PROPERTY = "fr.dyade.aaa.agent.A3CONF_JMS_PORT";

  /**
   * Default value for JMS listening port: 16010.
   */
  public final static int CFG_JMS_PORT_DFLT = 16010;
  
  /**
   *  Name of property allowing to configure the minimum value for listening port of the JMS
   * server when using the default server configuration with random port choice (if listening
   * port is set to -3).
   *  By default this values is set to CFG_MIN_JORAM_PORT_DFLT.
   * <p>
   *  This property can only be fixed from <code>java</code> launching command.
   */
  public final static String CFG_MIN_JORAM_PORT_PROPERTY = "fr.dyade.aaa.agent.A3CONF_MIN_JMS_PORT";
  
  /**
   * Default value for minimum JMS listening port: 16000.
   */
  public final static int CFG_MIN_JORAM_PORT_DFLT = 16000;
  
  /**
   *  Name of property allowing to configure the maximum value for listening port of the JMS
   * server when using the default server configuration with random port choice (if listening
   * port is set to -3).
   *  By default this values is set to CFG_MAX_JORAM_PORT_DFLT.
   * <p>
   *  This property can only be fixed from <code>java</code> launching command.
   */
  public final static String CFG_MAX_JORAM_PORT_PROPERTY = "fr.dyade.aaa.agent.A3CONF_MAX_JMS_PORT";
  
  /**
   * Default value for maximum JMS listening port: 16100.
   */
  public final static int CFG_MAX_JORAM_PORT_DFLT = 16100;
  
  /**
   *  Name of property allowing to configure the listening port of the JNDI server when
   * using the default server configuration, by default CFG_JNDI_PORT_PROPERTY. This Configuration
   * is automatically generated at first starting if no XML configuration file is found.
   * <p>
   *  Be careful, this configuration is normally used only for the initial starting of the
   * server, the configuration is then atomically maintained in the persistence directory.
   * <p>
   *  This property can only be fixed from <code>java</code> launching command.
   */
  public final static String CFG_JNDI_PORT_PROPERTY = "fr.dyade.aaa.agent.A3CONF_JNDI_PORT";

  /**
   * Default value for JNDI listening port: 16400.
   */
  public final static int CFG_JNDI_PORT_DFLT = 16400;
  
  private static Random rand = new Random();

  /**
   * Set default configuration for the specified server.
   * 
   * @param sid       Unique identifier of the server.
   * @param host      Host name or IP address of the physical machine.
   * @param adminuid  User name of the administrator.
   * @param adminpwd  Password of the administrator.
   * @param joram     Listen port for JMS connector. If set to 0 the listen port is first searched
   *                  through the CFG_JMS_PORT_PROPERTY property, then set to default value if not
   *                  defined. If less than 0..
   * @param jndi      Listen port for JNDI connector. If set to 0 the listen port is first searched
   *                  through the CFG_NDI_PORT_PROPERTY property, then set to default value if not
   *                  defined. If less than 0 the Joram/JNDI service is not started.
   * @param props     Set of properties to define in the built configuration.
   */
  public static void setDefaultConfig(int sid,
                                      String host,
                                      String adminuid, String adminpwd,
                                      int joram, int jndi, Properties props) {
    StringBuilder strbuf = new StringBuilder();
    
    if (host == null) {
      try {
        host = InetAddress.getLocalHost().getHostName();
      } catch (UnknownHostException e) {
        host = "localhost";
      }
    }
    if (adminuid == null) {
      adminuid = System.getProperty(AgentServer.CFG_ADMINUID_PROPERTY, "root");
    }
    if (adminpwd == null) {
      adminpwd = System.getProperty(AgentServer.CFG_ADMINPWD_PROPERTY, adminuid);
    }
    if (joram == 0) {
      joram = Integer.getInteger(AgentServer.CFG_JMS_PORT_PROPERTY, CFG_JMS_PORT_DFLT);
    }
    if (jndi == 0) {
      jndi = Integer.getInteger(AgentServer.CFG_JNDI_PORT_PROPERTY, CFG_JNDI_PORT_DFLT);
    }

    strbuf.append("<config>\n");
    if (props != null) {
      // Fixes the Transaction property if needed.
      if (props.getProperty("Transaction") == null)
        props.setProperty("Transaction", "fr.dyade.aaa.ext.NGTransaction");
      for (Enumeration e = props.keys(); e.hasMoreElements();) {
        String key = (String) e.nextElement();
        strbuf.append("<property name=\"").append(key).append("\" value=\"").append(props.getProperty(key)).append("\"/>\n");
      }
    } else {
      // There is no property defined, set the Transaction property.
      strbuf.append("<property name=\"Transaction\" value=\"fr.dyade.aaa.ext.NGTransaction\"/>\n" +
                    "<server id=\"").append(sid).append("\" name=\"S").append(sid).append("\" hostname=\"").append(host).append("\">\n");
    }
    
    if (joram > 0) { // Joram/JMS service and TCP connector
      strbuf.append(
          "<service class=\"org.objectweb.joram.mom.proxies.ConnectionManager\" args=\"").append(adminuid).append(' ').append(adminpwd).append("\"/>\n" +
          "<service class=\"org.objectweb.joram.mom.proxies.tcp.TcpProxyService\" args=\"").append(joram).append("\"/>\n");
    } else if (joram == -1) { // No Joram/JMS service
      strbuf.append("<!-- No Joram/JMS service -->\n");
    } else if (joram == -2) { // Joram/JMS service without TCP connector
      strbuf.append("<service class=\"org.objectweb.joram.mom.proxies.ConnectionManager\" args=\"").append(adminuid).append(' ').append(adminpwd).append("\"/>\n");
    } else if (joram == -3) { // Joram/JMS service with a TCP connector on random port
      int min = Integer.getInteger(AgentServer.CFG_MIN_JORAM_PORT_PROPERTY, CFG_MIN_JORAM_PORT_DFLT);
      int max = Integer.getInteger(AgentServer.CFG_MAX_JORAM_PORT_PROPERTY, CFG_MAX_JORAM_PORT_DFLT);
      joram = min + rand.nextInt(max-min);
      // TODO (AF): We should try if this listening port is free.
      strbuf.append(
          "<service class=\"org.objectweb.joram.mom.proxies.ConnectionManager\" args=\"").append(adminuid).append(' ').append(adminpwd).append("\"/>\n" +
          "<service class=\"org.objectweb.joram.mom.proxies.tcp.TcpProxyService\" args=\"").append(joram).append("\"/>\n");
    }
    
    if (jndi > 0) {
      strbuf.append("<service class=\"fr.dyade.aaa.jndi2.server.JndiServer\" args=\"").append(jndi).append("\"/>\n");
    }
    strbuf.append("</server>\n" + "</config>\n");
    
    defaultConfig = strbuf.toString();
  }
  
  public static void setDefaultConfig(String config) {
    defaultConfig = config;
  }

  private static short serverId = NULL_ID;

  /**
   * Reference to OSGi root activator (bundle #0) if any.
   * It allows to stop the OSGi container if needed using "context.getBundle(0).stop()".
   * 
   * Note (AF): This code creates a dependency to OSGi libraries, this can be annoying in
   * the use of methods by external tools.
   */
  public static AgentServerActivator activator;

  static void shutdown() {
    if (activator == null) return;

    try {
      activator.shutdown();
    } catch (Exception exc) {
      if (logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.WARN, getName() + ", shutdown error.", exc);
      else
        logmon.log(BasicLevel.WARN, getName() + ", shutdown error: " + exc);
    }
  }
  
  /**
   *  Name of property allowing to configure the directory to search the XML
   * server configuration.
   * <p>
   *  Be careful, the XML server configuration file is normally used only for the
   * initial starting of the server, the configuration is then atomically maintained
   * in the persistence directory.
   * <p>
   *  This property can only be fixed from <code>java</code> launching command.
   */
  public final static String CFG_DIR_PROPERTY = "fr.dyade.aaa.agent.A3CONF_DIR";
  /**
   *  Default value of the directory to search the XML server configuration,
   * value is null.
   */
  public final static String DEFAULT_CFG_DIR = null;

  /**
   *  Name of property allowing to configure the filename of the XML server
   * configuration.
   * <p>
   *  Be careful, the XML server configuration file is normally used only for the
   * initial starting of the server, the configuration is then atomically maintained
   * in the persistence directory.
   * <p>
   *  This property can only be fixed from <code>java</code> launching command.
   */
  public final static String CFG_FILE_PROPERTY = "fr.dyade.aaa.agent.A3CONF_FILE";
  /**
   *  Default value of the filename of the XML server configuration, value is
   * <code>a3servers.xml</code>.
   */
  public final static String DEFAULT_CFG_FILE = "a3servers.xml";
  
  /**
   *  Name of property allowing to use a default configuration when no configuration
   * file can be found.
   */
  public final static String USE_DEFAULT_CONFIG_PROPERTY = "fr.dyade.aaa.agent.useDefaultConfiguration";
  /**
   *  Default value for the USE_DEFAULT_CONFIG property.
   */
  public final static String DEFAULT_USE_DEFAULT_CONFIG = "true";
  
  /**
   *  Default value of the filename of the serialized server configuration in the
   * persistence directory, value is <code>a3cmlconfig</code>.
   * <p>
   *  Removing this file allows to load anew the XML configuration file at the next
   * starting of the server. Be careful, doing this can generate incoherence in the
   * global configuration.
   */
  public final static String DEFAULT_SER_CFG_FILE = "a3cmlconfig";
  
  public final static String CFG_NAME_PROPERTY = "fr.dyade.aaa.agent.A3CONF_NAME";
  public final static String DEFAULT_CFG_NAME = "default";
  
  /**
   *  Name of property allowing to configure the XML wrapper used to read the server
   * configuration.
   * <p>
   *  This property can only be fixed from <code>java</code> launching command.
   */
  public final static String A3CMLWRP_PROPERTY = "fr.dyade.aaa.agent.A3CMLWrapper";
  /**
   *  Default value of the XML wrapper used to read server configuration, this default
   * value implies the use of the default SaxWrapper.
   */
  public final static String DEFAULT_A3CMLWRP = "fr.dyade.aaa.agent.conf.A3CMLSaxWrapper";

  /**
   *  Name of property allowing to configure the checking period of the server (in ms).
   *  The corresponding value is the period of time beyond which an error is thrown and the
   *  registered listeners are called.
   *  The activation period is 5 times lower, this value cannot be less than 10s.
   *  <p>
   *  This property can be fixed either from XML configuration file or Java launching command.
   */
  public final static String CFG_CHECK_PERIOD_PROPERTY = "fr.dyade.aaa.agent.check.period";

  static CheckServerTask checkServerTask = null;
  
  public static void registerCheckServerListener(CheckServerListener listener) {
    if (checkServerTask == null)
      logmon.log(BasicLevel.ERROR, getName() + " cannot register CheckServerListener.");
    checkServerTask.registerListener(listener);
  }
  
  public static void unregisterCheckServerListener(CheckServerListener listener) {
    if (checkServerTask == null)
      logmon.log(BasicLevel.ERROR, getName() + " cannot unregister CheckServerListener.");
    checkServerTask.unregisterListener(listener);
  }

  static ThreadGroup tgroup = null;

  public static ThreadGroup getThreadGroup() {
    return tgroup;
  }

 /**
   * Static reference to the engine. Used in <code>Channel.sendTo</code> to
   * know if the method is called from a react or no.
   */
  static AgentEngine engine = null;    // TODO (AF): We must suppress this dependency in order to be able to run multiples engine.

  /**
   * Returns the agent server engine.
   * @return the agent server engine.
   */
  public static AgentEngine getEngine() {
    return engine;
  }

  public static boolean isEngineThread() {
    return engine.isEngineThread();
  }

  public static void resetEngineAverageLoad() {
    getEngine().resetAverageLoad();
  }
  
  /**
   * Returns the load averages for the last minute.
   * @return the load averages for the last minute.
   */
  public static float getEngineAverageLoad1() {
    return getEngine().getAverageLoad1();
  }

  /**
   * Returns the load averages for the past 5 minutes.
   * @return the load averages for the past 5 minutes.
   */
  public static float getEngineAverageLoad5() {
    return getEngine().getAverageLoad5();
  }
  
  /**
   * Returns the load averages for the past 15 minutes.
   * @return the load averages for the past 15 minutes.
   */
  public static float getEngineAverageLoad15() {
    return getEngine().getAverageLoad15();
  }
  
  /**
   * Returns the immediate engine load.
   * @return the immediate engine load.
   */
  public static int getEngineLoad() {
    return getEngine().getNbWaitingMessages();
  }
  
  /**
   * Returns true if the agent profiling is on.
   * 
   * @return true if agent profiling is on.
   */
  public static boolean isAgentProfiling() {
    return engine.isAgentProfiling();
  }
  
  /**
   * Sets the agent profiling.
   * 
   * @param agentProfiling true to turn on agent profiling.
   */
  public static void setAgentProfiling(boolean agentProfiling) {
    engine.setAgentProfiling(agentProfiling);
  }
  
  /**
   * Return the total agent reaction time.
   * @return the reaction time
   */
  public static long getReactTime() {
    return engine.getReactTime();
  }

  /**
   * Return the total agent commit time.
   * @return the commit time
   */
  public static long getCommitTime() {
    return engine.getCommitTime();
  }

  /** Static reference to the AgentServer file lock */
  static ServerLock serverLock = null;
  
  /** Static reference to the transactional monitor. */
  static Transaction transaction = null;

  /**
   * Returns the agent server transaction context.
   * @return  agent server transaction context.
   */
  public static Transaction getTransaction() {
    return transaction;
  }

  /**
   * Static references to all messages consumers initialized in this
   * agent server (including <code>Engine</code>).
   */
  private static Hashtable<String, MessageConsumer> consumers = null;

  public static void addConsumer(String domain, MessageConsumer cons) throws Exception {
    if (consumers.containsKey(domain))
      throw new Exception("Consumer for domain " + domain + " already exist");

    consumers.put(domain, cons);

    try {
      MXWrapper.registerMBean(cons, "AgentServer", "server=" + getName() + ",cons=" + cons.getName());
    } catch (Exception exc) {
      if (logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.WARN, getName() + " jmx failed", exc);
    }
  }

  static Enumeration<MessageConsumer> getConsumers() {
    if (consumers == null)
      return null;
    return consumers.elements();
  }

  public static MessageConsumer getConsumer(String domain) throws Exception {
    if (! consumers.containsKey(domain))
      throw new Exception("Unknown consumer for domain " + domain);
    return (MessageConsumer) consumers.get(domain);
  }

  public static void removeConsumer(String domain) {
    MessageConsumer cons = (MessageConsumer) consumers.remove(domain);
    if (cons != null) {
      cons.stop();
      try {
        MXWrapper.unregisterMBean("AgentServer", "server=" + getName() + ",cons=" + cons.getName());
      } catch (Exception exc) {
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.WARN, getName() + " jmx failed", exc);
      }
    }
  }
  
  /**
   * Timer provided by the agent server.
   */
  private static Timer timer;

  /**
   * Returns a shared timer provided by the agent server.
   * @return a shared timer provided by the agent server.
   */
  public static synchronized final Timer getTimer() {
    if (timer == null) {
      timer = new Timer("a3rtTimer");
    }
    return timer;
  }
  
  /** Static reference to the configuration. */
  private static A3CMLConfig a3config = null;

  /**
   *  Set the agent server configuration. Be careful, this method cannot
   * be called after initialization.
   *
   * @param a3config  A3CMLConfig
   * @exception Exception	Server is already initialized.
   */
  public final static void setConfig(A3CMLConfig a3config) throws Exception {
    setConfig(a3config, false);
  }

  public final static void setConfig(A3CMLConfig a3config,
                              boolean force) throws Exception {
    if (! force) {
      synchronized(status) {
        if (status.value != Status.INSTALLED)
          throw new Exception("cannot set config, bad status: " + getStatusInfo());
      }
    }
    AgentServer.a3config = a3config;
  }
  
  /**
   * Returns the agent server configuration.
   *
   * @return  agent server configuration (A3CMLConfig)
   * @throws Exception an error occurs.
   */
  public final static A3CMLConfig getConfig() throws Exception {
    if (a3config == null) throw new Exception("Server not configured");
    return a3config;
  }

  /**
   *  Gets configuration of agent servers for a domain from the current
   * A3CMLConfig object. This method fills the object graph configuration
   * in a <code>A3CMLConfig</code> object.
   *
   * @param domains  	list of domain's names
   * @return a <code>A3CMLConfig</code> object.
   * @throws Exception an error occurs.
   */
  public static A3CMLConfig getAppConfig(String[] domains) throws Exception {
    return getConfig().getDomainConfig(domains);
  }

  public final static short getServerId() {
    return serverId;
  }

  private static String name = null;

  public final static String getName() {
    return name;
  }
  
  public final static String getServerName() {
    try {
      return getConfig().getServerNameById(getServerId());
    } catch (Exception e) {
      return getName();
    }
  }

  /**
   * Returns the identifier of the agent server which name is specified.
   *
   * @param name the name of the agent server
   * @return the identifier of the agent server
   * @exception Exception if the server name is unknown.
   */
  public static short getServerIdByName(String name) throws Exception {
    return getConfig().getServerIdByName(name);
  }
  
  /**
   * Searches for the property with the specified key in the server property
   * list.
   *
   * @param key	   the hashtable key.
   * @return	   the value with the specified key value.
   */
  public static String getProperty(String key) {
    return Configuration.getProperty(key);
  }

  /**
   * Searches for the property with the specified key in the server property
   * list.
   *
   * @param key	   the hashtable key.
   * @param value  a default value.
   * @return	   the value with the specified key value.
   */
  public static String getProperty(String key, String value) {
    return Configuration.getProperty(key, value);
  }

  /**
   * Determines the integer value of the server property with the
   * specified name.
   *
   * @param key property name.
   * @return 	the Integer value of the property.
   */
  public static Integer getInteger(String key) {
    return Configuration.getInteger(key);
  }

  /**
   * Determines the integer value of the server property with the
   * specified name.
   *
   * @param key property name.
   * @param value  a default value.
   * @return 	the Integer value of the property.
   */
  public static Integer getInteger(String key, int value) {
    return Configuration.getInteger(key, value);
  }

  /**
   * Determines the integer value of the server property with the specified
   * name.
   * 
   * @param key
   *          property name.
   * @return the Integer value of the property.
   */
  public static Long getLong(String key) {
    return Configuration.getLong(key);
  }

  /**
   * Determines the long value of the server property with the specified name.
   * 
   * @param key
   *          property name.
   * @param value
   *          a default value.
   * @return the Integer value of the property.
   */
  public static Long getLong(String key, long value) {
    return Configuration.getLong(key, value);
  }

  /**
   * Determines the boolean value of the server property with the specified name.
   * 
   * The returned value is true if the property is defined and is equal, ignoring case, to the string
   * "true". Otherwise, a false value is returned.
   * 
   * @param key property name.
   * @return the boolean value of the property.
   */
  public static boolean getBoolean(String key) {
    return Configuration.getBoolean(key);
  }

  /** Static description of all known agent servers in ascending order. */
  private static ServersHT servers = null;

  public static void addServerDesc(ServerDesc desc) throws Exception {
    if (desc == null) return;
    servers.put(desc);
  }

  public static ServerDesc removeServerDesc(short sid) throws Exception {
    return servers.remove(sid);
  }

  public static Enumeration<ServerDesc> elementsServerDesc() {
    return servers.elements();
  }

  public static Enumeration<Short> getServersIds() {
    return servers.keys();
  }

  /**
   * Gets the number of server known on the current server.
   *
   * @return	the number of server.
   */
  final static int getServerNb() {
    return servers.size();
  }

  /**
   * Gets the characteristics of the corresponding server.
   *
   * @param sid	agent server id.
   * @return	the server's descriptor.
   * @throws UnknownServerException the server is not defined.
   */
  public final static ServerDesc getServerDesc(short sid) throws UnknownServerException {
    ServerDesc serverDesc = servers.get(sid);
    if (serverDesc == null)
      throw new UnknownServerException("Unknow server id. #" + sid);
    return serverDesc;
  }

  /**
   * Gets the message consumer for the corresponding server.
   *
   * @param sid	agent server id.
   * @return	the corresponding message consumer.
   * @throws UnknownServerException the server is not defined.
   */
  final static MessageConsumer getConsumer(short sid) throws UnknownServerException {
    return getServerDesc(sid).getDomain();
  }

  /**
   * Get the host name of an agent server.
   *
   * @param sid		agent server id
   * @return		server host name as declared in configuration file
   * @throws UnknownServerException the server is not defined.
   */
  public final static String getHostname(short sid) throws UnknownServerException {
    return getServerDesc(sid).getHostname();
  }

  /**
   * Get the description of all services of the current agent server.
   *
   * @return		server host name as declared in configuration file
   * @throws UnknownServerException the server is not defined.
   */
  final static ServiceDesc[] getServices() throws UnknownServerException {
    return getServerDesc(getServerId()).services;
  }

  /**
   * Get the argument strings for a particular service.
   * The information provides from the A3 configuration file, so it's
   * only available if this file contains service's informations for all
   * nodes.
   *
   * @see A3CMLConfig#getServiceArgs(short,String)
   *
   * @param sid		agent server id
   * @param classname	the service class name
   * @return		the arguments as declared in configuration file
   * @exception	UnknownServerException The specified server does not exist.
   * @exception UnknownServiceException The specified service is not declared on this server. 
   * @exception Exception Probably there is no configuration defined.
   */
  public final static String getServiceArgs(short sid, String classname) throws Exception {
    return getConfig().getServiceArgs(sid, classname);
  }

  /**
   * Get the argument strings for a particular service running on a server
   * identified by its host.
   * The information provides from the A3 configuration file, so it's
   * only available if this file contains service's informations for all
   * nodes.
   *
   * @see A3CMLConfig#getServiceArgs(String, String)
   *
   * @param hostname	hostname
   * @param classname	the service class name
   * @return		the arguments as declared in configuration file
   * @exception UnknownServiceException The specified service is not declared on this server. 
   * @exception Exception Probably there is no configuration defined.
   */
  public final static String getServiceArgs(String hostname, String classname) throws Exception {
    return getConfig().getServiceArgsHost(hostname, classname);
  }

  /**
   *  The second step of initialization. It needs the Transaction component
   * be up, then it initializes all <code>AgentServer</code> structures from
   * the <code>A3CMLConfig</code> ones. In particular the servers array is
   * initialized.
   * 
   * @throws Exception an error occurs.
   */
  private static void configure() throws Exception {
    A3CMLServer root = getConfig().getServer(serverId);
    //Allocates the temporary descriptors hashtable for each server.
    servers = new ServersHT();
    // Initialized the descriptor of current server in order to permit
    // Channel and Engine initialization.
    ServerDesc local = new ServerDesc(root.sid, root.name, root.hostname, -1);
    servers.put(local);

    // Parse configuration in order to fix route related to the
    // current server
    getConfig().configure(root);

    // Creates all the local MessageConsumer.
    createConsumers(root);
    
    for (Enumeration<A3CMLServer> s = getConfig().servers.elements(); s.hasMoreElements();) {
      A3CMLServer server = s.nextElement();
      if (server.sid == root.sid) continue;

      ServerDesc desc = createServerDesc(server);
      addServerDesc(desc);
    }

    initServices(root, local);
    local.setDomain(engine);
    
    return;
  }

  private static void createConsumers(A3CMLServer root) throws Exception {
    consumers = new Hashtable<>();

    // Creates the local MessageConsumer: the Engine.
    
    String cname = "fr.dyade.aaa.agent.Engine";
    cname = AgentServer.getProperty("Engine", cname);

    Class<?> eclass = Class.forName(cname);
    engine = (AgentEngine) eclass.newInstance();
    
    addConsumer("local", engine);

    // Search all directly accessible domains.
    for (Enumeration<A3CMLNetwork> n = root.networks.elements(); n.hasMoreElements();) {
      A3CMLNetwork network = n.nextElement();

      A3CMLDomain domain = getConfig().getDomain(network.domain);
      // Creates the corresponding MessageConsumer.
      try {
        Network consumer = (Network) Class.forName(domain.network).newInstance();
        // Initializes it with domain description. Be careful, this array
        // is kept in consumer, don't reuse it!!
        consumer.init(domain.name, network.port, domain.getServersId());
//         domain.consumer = consumer;
        addConsumer(network.domain, consumer);
      } catch (ClassNotFoundException exc) {
        throw exc;
      } catch (InstantiationException exc) {
        throw exc;
      } catch (IllegalAccessException exc) {
        throw exc;
      }
    }
  }

  public static void initServerDesc(ServerDesc desc, A3CMLServer server) throws Exception {
    desc.gateway = server.gateway;
    // For each server set the gateway to the real next destination of
    // messages; if the server is directly accessible: itself.
    if ((desc.gateway == -1) || (desc.gateway == server.sid)) {
      desc.gateway = server.sid;
      desc.updateSockAddr(desc.getHostname(), server.port);   
      A3CMLServer current = getConfig().getServer(getServerId());
      if (current.containsNat(server.sid)) {
        A3CMLNat nat = current.getNat(server.sid);
        desc.updateSockAddr(nat.host, nat.port);
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.DEBUG, getName() + " : NAT sDesc = " + desc);
      }
    }
    desc.setDomain(getConsumer(server.domain));
  }   

  private static ServerDesc
      createServerDesc(A3CMLServer server) throws Exception {
    if (! server.visited)
      throw new Exception(server + " inaccessible");
    
    ServerDesc desc = new ServerDesc(server.sid, 
                                     server.name, 
                                     server.hostname,
                                     -1);

    initServerDesc(desc, server);
    initServices(server, desc);

    return desc;
  }

  private static void initServices(A3CMLServer server, ServerDesc desc) throws Exception {
    if (server.services != null) {
      ServiceDesc services[]  = new ServiceDesc[server.services.size()];
      int idx = 0;
      for (Enumeration<A3CMLService> x = server.services.elements(); x.hasMoreElements();) {
        A3CMLService service = x.nextElement();
        services[idx++] = new ServiceDesc(service.classname, service.args);
        
        // Need to load the service classes in order to register the Encodable factories
        Class.forName(service.classname);
      }
      desc.services = services;
    }
  }
  
  private static void setProperties(short sid) throws Exception {
    if (a3config == null) return;

    // add global properties
    if (a3config.properties != null) {
      for (Enumeration<A3CMLProperty> e = a3config.properties.elements(); e.hasMoreElements();) {
        A3CMLProperty p = e.nextElement();
        Configuration.putProperty(p.name, p.value);

        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.DEBUG,
                     getName() + " : Adds global property: " + p.name + " = " + p.value);
      }
    }

    A3CMLServer server = a3config.getServer(sid);

    // add server properties
    if (server != null && server.properties != null) {
      Enumeration<A3CMLProperty> e = server.properties.elements();
      do {
        A3CMLProperty p = e.nextElement();
        Configuration.putProperty(p.name, p.value);

        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.DEBUG,
                     getName() + " : Adds server property: " + p.name + " = " + p.value);
      } while (e.hasMoreElements());
    }
  }
  
  public static class Status {
    public static final int INSTALLED = 0;
    public static final int INITIALIZING = 0x1;
    public static final int INITIALIZED = 0x2;
    public static final int STARTING = 0x3;
    public static final int STARTED = 0x4;
    public static final int STOPPING = 0x5;
    public static final int STOPPED = 0x6;
    public static final int RESETING = 0x7;
    public static final int UPDATING = 0x8;

    int value = INSTALLED;

    static final String[] info = { "installed",
                                   "initializing", "initialized",
                                   "starting", "started",
                                   "stopping", "stopped",
                                   "reseting" };
  }

  private static Status status = new Status();

  public static int getStatus() {
    return status.value;
  }

  public static String getStatusInfo() {
    return Status.info[status.value];
  }

  /**
   * Parses agent server arguments, then initializes this agent server. The
   * <code>start</code> function is then called to start this agent server
   * execution. Between the <code>init</code> and <code>start</code> calls,
   * agents may be created and deployed, and notifications may be sent using
   * the <code>Channel</code> <code>sendTo</code> function.
   *
   * @param args	launching arguments, the first one is the server id
   *			and the second one the persistency directory.
   * @return		number of arguments consumed in args
   *
   * @exception Exception unspecialized exception
   */
  public static int init(String args[]) throws Exception {
    if (args.length < 2)
      throw new Exception("usage: java <main> sid storage");
    short sid = NULL_ID;
    try {
      sid = (short) Integer.parseInt(args[0]);
    } catch (NumberFormatException exc) {
      throw new Exception("usage: java <main> sid storage");
    }
    String path = args[1];

    init(sid, path, null);

    return 2;
  }

  public static void reset(boolean force) {
    if (force) {
      synchronized(status) {
        if (status.value != Status.STOPPED) {
          logmon.log(BasicLevel.WARN,
                     getName() + ", reset force status: " + getStatusInfo());
        }
        status.value = Status.STOPPED;
      }
    }
    reset();
  }

  /**
   *  Cleans an AgentServer configuration in order to restart it from
   * persistent storage.
   */
  public static void reset() {
    synchronized(status) {
      if (status.value == Status.INSTALLED) {
        // The server is already reseted, avoid a useless warning.
        logmon.log(BasicLevel.INFO, getName() + ", already reseted.");
        return;
      }
      if (status.value != Status.STOPPED) {
        logmon.log(BasicLevel.WARN,
                   getName() + ", cannot reset, bad status: " + getStatusInfo());
        return;
      }
      status.value = Status.RESETING;
    }

    // Remove all consumers Mbean
    Enumeration<MessageConsumer> e = getConsumers();
    if (e != null) {
      for (; e.hasMoreElements();) {
        MessageConsumer cons = e.nextElement();
        try {
          MXWrapper.unregisterMBean("AgentServer", "server=" + getName() + ",cons=" + cons.getName());
        } catch (Exception exc) {
          if (logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.WARN,
                       getName() + ", jmx failed: " + "server=" + getName() + ",cons=" + cons.getName(), exc);
        }
      }
      consumers = null;
    }

    try {
      MXWrapper.unregisterMBean("AgentServer", "server=" + getName() + ",cons=Transaction");
    } catch (Exception exc) {
      if (logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.WARN,
                   getName() + ", jmx failed: " + "server=" + getName() + ",cons=Transaction", exc);
    }

    if (transaction != null) transaction.close();
    transaction = null;
    serverLock.close();
    serverLock = null;

    try {
      MXWrapper.unregisterMBean("AgentServer", "server=" + getName());
    } catch (Exception exc) {
      if (logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.WARN,
                   getName() + " jmx failed: "+ "server=" + getName(), exc);
    }
    
    a3config = null;

    synchronized(status) {
      status.value = Status.INSTALLED;
    }
  }
  
  /* ================================================================================
   * Code allowing to update the stored configuration
   * ================================================================================ */
  
  /**
   * Updates the stored configuration of an external server from a file.
   * /!\ Be careful, the server must be stopped and reseted !!
   * 
   * It returns 0 if the configuration has been updated, -1 if there is no persistence to update.
   * This method locks the server lock file.
   *
   * @param serverConfigFilePath  pathname of file containing the XML configuration.
   * @param serverDataDirPath     pathname of the persistence repository to update.
   * @return 0 if the configuration is updated, -1 if there is no persistence to update.
   * @throws Exception an error occurred during update
   */
  public static void externalUpdateConfFromFile(String serverConfigFilePath, String serverDataDirPath) throws Exception {
    A3CMLConfig config = getConfFromFile(serverConfigFilePath);
    updateConf(config, serverDataDirPath);
  }
  
  /**
   * Updates the stored configuration of an external server from a String.
   * /!\ Be careful the server must be stopped and reseted !!
   * 
   * It returns 0 if the configuration has been updated, -1 if there is no persistence to update.
   * This method locks the server lock file.
   *  
   * @param serverConfig          the XML configuration.
   * @param serverDataDirPath     pathname of the persistence repository to update.
   * @return 0 if the configuration is updated, -1 if there is no persistence to update.
   * @throws Exception an error occurred during update
   */
  public static void externalUpdateConfFromString(String serverConfig, String serverDataDirPath) throws Exception {
    A3CMLConfig config = getConfFromString(serverConfig);
    updateConf(config, serverDataDirPath);
  }
  
  /**
   * Gets the AgentServer configuration from a file.
   *
   * @param serverConfigFilePath  pathname of file containing the server configuration.
   * @return The AgentServer configuration.
   * @throws Exception an error occurred during configuration parsing
   */
  public static A3CMLConfig getConfFromFile(String serverConfigFilePath) throws Exception {
    Reader reader = new FileReader(new File(serverConfigFilePath));
    A3CMLConfig a3cmlConfig = null;
    try {
      a3cmlConfig = A3CML.getConfig(reader);
    } catch (ParseException exc) {
      exc.setFileName(serverConfigFilePath);
      throw exc;
    } finally {
      reader.close();
    }
    
    return a3cmlConfig;
  }

  /**
   * Gets the AgentServer configuration from the String parameter.
   *  
   * @param serverConfig          the XML configuration.
   * @return The AgentServer configuration.
   * @throws Exception an error occurred during configuration parsing
   */
  public static A3CMLConfig getConfFromString(String serverConfig) throws Exception {
    return A3CML.getConfig(new StringReader(serverConfig));
  }
    
  /**
   * Updates the stored configuration.
   * /!\ Be careful the server must be stopped and reseted !!
   * /!\ Be careful, to update the configuration of an external server you need to ensures
   * that it is stopped acquiring the lock.
   *
   *  It returns 0 if the configuration has been updated, -1 if there is no persistence to update.
   *
   * @param a3cmlConfig          the configuration up-to-date.
   * @param serverDataDirPath     pathname of the persistence repository to update.
   * @return 0 if the configuration is updated, -1 if there is no persistence to update.
   * @throws Exception an error occurred during update
   */
  static void updateConf(A3CMLConfig a3cmlConfig, String serverDataDirPath) throws IOException {
    // Locks the AgentServer persistence.
    ServerLock lock = null;
    try {
      lock = lockAgentServer(serverDataDirPath);
      
      // Note (AF): This code seems useless, updateConf can only be called if the AgentServer
      // lock exists, so the persistence data necessarily exists.
      File dataDir = new File(serverDataDirPath);
      if (dataDir.exists() && dataDir.isDirectory()) {
        // Note (AF): This code seems useless, updateConf can only be called if the AgentServer
        // lock is free, so the server cannot run..
        synchronized(status) {
          if (status.value != Status.INSTALLED)
            throw new IOException("Cannot update configuration, bad status: " + getStatusInfo());
          // Status.Updating prevents from a starting of this server during the update
          status.value = Status.UPDATING;
        }

        try {
          // Updates transaction properties from new configuration id TPF file
          Properties props = new Properties();
          Hashtable<String, A3CMLProperty> properties = a3cmlConfig.properties;
          Set<Entry<String, A3CMLProperty>> set = properties.entrySet();
          for (Entry<String, A3CMLProperty> e : set) {
            if (e.getKey().startsWith("Transaction.")) {
              props.put(e.getValue().name, e.getValue().value);
            }
          }

          File tpf = new File(serverDataDirPath, "TPF");
          // TODO (AF): May be we should make a copy of old parameters!
          try (FileOutputStream fos = new FileOutputStream(tpf)) {
            props.storeToXML(fos, "Transaction properties");
          } catch (IOException exc) {
            throw new IOException("Cannot store Transaction properties: " + exc, exc);
          }

          // Initializes the Transaction component
          Transaction transaction = getTransaction(serverDataDirPath);
          if (transaction == null)
            throw new IOException("Cannot get transaction from directory: " + serverDataDirPath);
          transaction.init(serverDataDirPath);

          // ==================================================
          // Functional code of the upateConf.

          // Starts a transaction then updates the configuration stored
          transaction.begin();
          // JMQ-151: Indicates to the server that it needs to update the configuration during the
          // next reboot.
          ServerUpdate updatecfg = new ServerUpdate();
          transaction.save(updatecfg, AgentServer.UPDATE_CONF_TX_NAME);

          transaction.save(a3cmlConfig, AgentServer.DEFAULT_SER_CFG_FILE);
          transaction.commit(true);

          // End.
          // ==================================================

          transaction.stop();
          logmon.log(BasicLevel.INFO, "Configuration updated.");
        } finally {
          synchronized(status) {
            status.value = Status.INSTALLED;
          }
        }
      } else {
        throw new FileNotFoundException("Transactional persistence directory doesn't exist: " + serverDataDirPath);
      }
    } finally {
      if (lock != null) lock.close();
    }
  }
  
  /* ================================================================================
   * Code for managing backup and restore of transactional persistence.
   * ================================================================================ */

  /**
   * Backup the transactional persistence.
   * /!\ Be careful the server must be stopped and reseted !!
   *
   *  It returns 0 if the backup is done, -1 if there is an issue during backup.
   *
   * @param serverDataDirPath     pathname of the persistence repository to backup.
   * @param backupDir             pathname of the directory to contain the backup.
   * @throws Exception an error occurred during backup
   */
  public static void externalBackup(String serverDataDirPath, String backupDir) throws Exception {
    // Locks the AgentServer persistence.
    ServerLock lock = null;
    try {
      // Note (AF): lockAgentServer verifies that data directory already exists.
      lock = lockAgentServer(serverDataDirPath);

      // Initializes the Transaction component
      Transaction transaction = getTransaction(serverDataDirPath);
      if (transaction == null)
        throw new IOException("Cannot get transaction from directory: " + serverDataDirPath);
      transaction.init(serverDataDirPath);

      // ==================================================
      // Functional code of the backup.

      transaction.backup(backupDir);

      // End.
      // ==================================================

      transaction.stop();
      logmon.log(BasicLevel.INFO, "Backup done.");
    } finally {
      if (lock != null) lock.close();
    }
  }

  /**
   * Restores the server persistence from a backup file.
   * 
   * @param backupFile    The path of backup file.
   * @throws IOException  An error occurs.
   */
  public static void restoreServer(String fname) throws IOException {
    RestoreFile restoreFile = null;
    try {
      File backupFile = new File(fname);
      if (!backupFile.exists() || !backupFile.isFile() || !backupFile.canRead())
        throw new IOException(fname + " should be an existing file with read rigths.");
      
      restoreFile = new RestoreFile(backupFile);
      logmon.log(BasicLevel.WARN,
                 getName() + " restore from " + fname + ": " + restoreFile.getNbRecords() + " records.");
      
      getTransaction().begin();
      BackupRecord record = restoreFile.getNextRecord();
      while (record != null) {
        logmon.log(BasicLevel.INFO,
                   getName() + " restore dirName=" + record.getDirName() + ", name =" + record.getName());
        getTransaction().createByteArray(record.getValue(), record.getDirName(), record.getName());
        record = restoreFile.getNextRecord();
        // TODO (AF): May be we can do intermediate commit
      }
      getTransaction().commit(true);
      logmon.log(BasicLevel.WARN,
                 getName() + " restored from " + fname + ": " + restoreFile.getNbRecords() + " records.");
    } finally {
      if (restoreFile != null) restoreFile.close();
    }
  }
  
  /* ==================================================
   * 
   * ================================================== */

  public final static long startDate = System.currentTimeMillis();

  /**
   * Gets the Transaction component from an existing transactional persistence.
   * 
   * If the transactional persistence is not initialized (TFC file does not exist) then the method
   * simply return null.
   * 
   * If an A3CMLConfig configuration is provided, the transaction component is updated
   * prior to start the component.
   * 
   * @param serverDataDirPath pathname of the persistence repository
   * @return  The transaction component started, or null if the transaction persistence does not exist.
   * @throws IOException an error occurred during transaction initialization.
   */
  public static Transaction getTransaction(String serverDataDirPath) throws IOException {
    Transaction transaction = null;
    
    File tfc = new File(serverDataDirPath, "TFC");
    if (tfc.exists()) {
      // Reads classname of Transaction implementation
      try (DataInputStream dis = new DataInputStream(new FileInputStream(tfc))) {
        String tname = dis.readUTF();
        transaction = (Transaction) Class.forName(tname).newInstance();
      } catch (Exception exc) {
        throw new IOException("Failed to instantiate transaction manager: " + exc, exc);
      }
    }
    
    return transaction;
  }

  /**
   * Initializes this agent server.
   * <code>start</code> function is then called to start this agent server
   * execution. Between the <code>init</code> and <code>start</code> calls,
   * agents may be created and deployed, and notifications may be sent using
   * the <code>Channel</code> <code>sendTo</code> function.
   *
   * @param sid		        the server id
   * @param path          the persistency directory.
   * @param loggerFactory the monolog LoggerFactory;
   *
   * @exception Exception unspecialized exception
   */
  public static void init(short sid,
                          String path,
                          LoggingFactory loggerFactory) throws Exception {
    init(sid, path, false, loggerFactory);
  }
  
  /**
   * 
   * @param sid           the server id
   * @param serverDataDirPath          the persistency directory.
   * @param update        If true updates persistence from configuration
   * @param loggerFactory the monolog LoggerFactory;
   *
   * @exception Exception unspecialized exception
   */
  public static void init(short sid,
                          String serverDataDirPath,
                          boolean update,
                          LoggingFactory loggerFactory) throws Exception {
    name = new StringBuilder("AgentServer#").append(sid).toString();

    if (loggerFactory != null) {
      System.err.println("AgentServer.init: Could not set logger factory.");
//      Debug.setLoggerFactory(loggerFactory);
    }
    logmon = Debug.getLogger(AgentServer.class.getName() + ".#" + sid);

    if (logmon.isLoggable(BasicLevel.DEBUG))
      logmon.log(BasicLevel.INFO, getName() + ", init()", new Exception());
    else
      logmon.log(BasicLevel.INFO, getName() + ", init()");
    
    try {
      serverLock = lockAgentServer(serverDataDirPath , true);
    } catch (IOException exc) {
      if (logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.FATAL, "AgentServer.init(): ", exc);
      else
        logmon.log(BasicLevel.FATAL, "AgentServer.init(): " + exc.getMessage());
      throw new IOException(exc);
    }

    synchronized(status) {
      if (status.value == Status.STOPPED) {
        logmon.log(BasicLevel.DEBUG, getName() + ", reset configuration");
        reset();
      }
      if (status.value != Status.INSTALLED)
        throw new Exception("cannot initialize, bad status: " + getStatusInfo());
      status.value = Status.INITIALIZING;
    }

//    sdf = new PrintStream(new File("essai-" + sid + ".sdf"));
//    logsdf = Debug.getLogger(AgentServer.class.getName() + ".sdf");
    
    try {
      serverId = sid; 

      tgroup = new ThreadGroup(getName()) {
        public void uncaughtException(Thread t, Throwable e) {
          if (e instanceof VirtualMachineError) {
            if (logmon.isLoggable(BasicLevel.FATAL)) {
              logmon.log(BasicLevel.FATAL,
                         "Abnormal termination for " + t.getThreadGroup().getName() + "." + t.getName(), e);
              // AF: Should be AgentServer.stop() ?
              System.exit(-1);
            }
          } else {
            if (logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.WARN,
                         "Abnormal termination for " + t.getThreadGroup().getName() + "." + t.getName(), e);
              else
                logmon.log(BasicLevel.WARN,
                           "Abnormal termination for " + t.getThreadGroup().getName() + "." + t.getName() + ": " + e.getMessage());
          }
        }
      };
      
      // Note: We have acquired serverLock, so dataDir exists and is a Directory.
      try {
        // Try to start an existing transactional persistence.
        transaction = getTransaction(serverDataDirPath);
      } catch (IOException exc) {
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.DEBUG,
                     getName() + ", can't instantiate transaction manager", exc);
          throw exc;
      }
      // Note: if the transaction persistence is not initialized (TFC file does not exist) then the
      // getConfiguration simply return null.
      if (transaction != null) {
        try {
          transaction.init(serverDataDirPath);
        } catch (IOException exc) {
          if (logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.DEBUG, getName() + ", can't start transaction manager", exc);
          throw new Exception("Can't start transaction manager: " + exc.getMessage());
        }
      } else {
        logmon.log(BasicLevel.INFO, getName() + ", TFC file does not exist.");
      }

      // Gets static configuration of agent servers from a file. This method
      // fills the object graph configuration in the <code>A3CMLConfig</code>
      // object, then the configure method really initializes the server.
      // There are two steps because the configuration step needs the
      // transaction components to be initialized.
      if (transaction != null) {
        // Try to read the serialized configuration (through transaction)
        try {
          a3config = A3CMLConfig.load();
          logmon.log(BasicLevel.INFO, getName() + ", configuration retrieved from persistence");
        } catch (Exception exc) {
          if (logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.DEBUG, getName() + ", config not found in persistence");
        }
        // Verify if the configuration has been updated since last boot.
        updatecfg = (ServerUpdate) transaction.load(UPDATE_CONF_TX_NAME);
        if (updatecfg != null) {
          logmon.log(BasicLevel.WARN,
                     getName() + ", configuration update detected: " + updatecfg.getUpdateDate());
          // Removes the object before the next boot
          transaction.delete(UPDATE_CONF_TX_NAME);
        }
      } else {
        // First boot, forces the update
        updatecfg = new ServerUpdate();
      }
      
      if (a3config == null) {
        //  Try to load an initial configuration (serialized or XML), or
        // generates a default one in case of failure.
        try {
          // Try to load a serialized configuration file directly in the filesystem.
          // TODO (AF): Is it used ? During Mediation deployment ?
          a3config = A3CMLConfig.getConfig(DEFAULT_SER_CFG_FILE);
          logmon.log(BasicLevel.INFO, getName() + ", serialized a3cmlconfig file loaded");
        } catch (FileNotFoundException exc) {
          // no configuration found, try alternatives
          if (logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.DEBUG, getName() + ", serialized a3cmlconfig file not found");
        }
        // any other exception should stop the server
      }
        
      if (a3config == null) {
        // Try to find an existing XML configuration file, then parse it.
        try {
          a3config = A3CML.getXMLConfig();
          logmon.log(BasicLevel.INFO, getName() + ", XML configuration file loaded");
        } catch (FileNotFoundException exc) {
          // no configuration file found, allow a default configuration
          if (logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.DEBUG, getName() + ", XML configuration not found");
        }
        // any other exception should stop the server
      }

      if (a3config == null) {
        boolean useDefaultConfig = Boolean.parseBoolean(System.getProperty(USE_DEFAULT_CONFIG_PROPERTY, DEFAULT_USE_DEFAULT_CONFIG));
        if (! useDefaultConfig) {
          throw new FileNotFoundException(getName() + ", no configuration found");
        }
        
        if (defaultConfig == null) {
          // If needed creates a Joram configuration as default.
          logmon.log(BasicLevel.WARN,
                     getName() + ", no configuration found, generates a default one");
          setDefaultConfig(sid);
        }

        // Try to parse default XML configuration.
        try {
          if (Log.logger.isLoggable(BasicLevel.DEBUG))
            Log.logger.log(BasicLevel.DEBUG,
                           getName() + ", start AgentServer with default configuration: \n" + defaultConfig);
          a3config = A3CML.getConfig(new StringReader(defaultConfig));
          logmon.log(BasicLevel.INFO,
                     getName() + ", use default XML configuration");
        } catch (Exception exc) {
          if (logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.WARN,
                       getName() + ", bad default XML configuration", exc);
          else
            logmon.log(BasicLevel.WARN,
                       getName() + ", bad default XML configuration: " + exc.getMessage());
        }
      }

      if (a3config == null) {
        // Last, Generate a basic A3CMLConfig.
        logmon.log(BasicLevel.WARN,
                   getName() + ", generates a default configuration");
        A3CMLDomain d = new A3CMLDomain(ADMIN_DOMAIN, SimpleNetwork.class.getName());
        A3CMLServer s = new A3CMLServer((short) sid, ADMIN_SERVER, "localhost");
        s.networks.addElement(new A3CMLNetwork(ADMIN_DOMAIN, 27300));
        d.addServer(s);
        a3config = new A3CMLConfig();
        a3config.addDomain(d);
        a3config.addServer(s);
      }

      // Set properties from A3 configuration
      setProperties(serverId);
      
      if (getProperty("Transaction.UseLockFile") != null)
        logmon.log(BasicLevel.WARN, getName() + ", init(): property Transaction.UseLockFile is no longer used.");

      if (transaction == null) {
        // Initializes a new transactional persistence.
        try {
          String tname = getProperty("Transaction", "fr.dyade.aaa.ext.NGTransaction");
          transaction = (Transaction) Class.forName(tname).newInstance();
        } catch (Exception exc) {
          if (logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.FATAL, getName() + ", can't instantiate transaction manager", exc);
          else
            logmon.log(BasicLevel.FATAL, getName() + ", can't instantiate transaction manager: " + exc.getMessage());
          throw new Exception("Can't instantiate transaction manager");
        }

        try {
          transaction.init(serverDataDirPath);
        } catch (IOException exc) {
          if (logmon.isLoggable(BasicLevel.DEBUG))
            logmon.log(BasicLevel.FATAL, getName() + ", can't start transaction manager", exc);
          else
            logmon.log(BasicLevel.FATAL, getName() + ", can't start transaction manager: " + exc.getMessage());
          throw new Exception("Can't start transaction manager");
        }
        
        String restoreFile = getProperty(BACKUP_FILE, DEFAULT_BACKUP_FILE);
        if (new File(restoreFile).exists()) {
          try {
            logmon.log(BasicLevel.WARN, getName() + ", server restoring from " + restoreFile);
            restoreServer(restoreFile);
            logmon.log(BasicLevel.WARN, getName() + ", server restored.");
          } catch (IOException exc) {
            if (logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.ERROR,
                         getName() + ", cannot be restored from " + restoreFile, exc);
            else
              logmon.log(BasicLevel.ERROR,
                         getName() + ", cannot be restored from " + restoreFile + ": " + exc.getMessage());
          }
        }
      } else {
        String restoreFile = getProperty(BACKUP_FILE, DEFAULT_BACKUP_FILE);
        if (new File(restoreFile).exists())
          logmon.log(BasicLevel.WARN,
                     getName() + ", cannot be restored from " + restoreFile + ", transaction already initialized.");
      }

      try {
        MXWrapper.registerMBean(transaction,
                                "AgentServer", "server=" + getName() + ",cons=Transaction");
      } catch (Exception exc) {
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.WARN, getName() + " jmx failed", exc);
      }

      // save A3CMLConfig (Maybe we can omit it in some case).
      a3config.save();
      
      if (getProperty("Transaction.UseLockFile") != null)
        logmon.log(BasicLevel.WARN, getName() + ", init(): property Transaction.UseLockFile is no longer used.");

      try {
        // Initialize AgentId class's variables.
        AgentId.init();
      } catch (ClassNotFoundException exc) {
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.FATAL, getName() + ", can't initialize AgentId, bad classpath", exc);
        else
          logmon.log(BasicLevel.FATAL, getName() + ", can't initialize AgentId, bad classpath: " + exc.getMessage());
        throw new Exception("Can't initialize AgentId, bad classpath");
      } catch (IOException exc) {
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.FATAL, getName() + ", can't initialize AgentId", exc);
        else
          logmon.log(BasicLevel.FATAL, getName() + ", can't initialize AgentId: " + exc.getMessage());
        throw new Exception("Can't initialize AgentId, storage problems");
      }

      try {
        // Configure the agent server.
        configure();
      } catch (Exception exc) {
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.FATAL, getName() + ", can't configure", exc);
        else
          logmon.log(BasicLevel.FATAL, getName() + ", can't configure: " + exc.getMessage());
        throw new Exception("Can't configure server: " + exc.getMessage());
      }

      try {
        // then restores all messages.
        String[] list = transaction.getList("@");
        // BatchEngine and BatchNetwork need a global validate operation after insertion.
        Vector<MessageConsumer> toValidate = new Vector<>();
        for (int i=0; i<list.length; i++) {
          Message msg = Message.load(list[i]);

          if (msg.getSource() == serverId) {
            // The update has been locally generated, the message is ready to deliver to its
            // consumer (Engine or Network component). So we have to insert it in the queue
            // of this consumer.
            try {
              MessageConsumer cons = getConsumer(msg.getDest());
              cons.insert(msg);
              // BatchEngine and BatchNetwork need a global validate operation after insertion.
              if (! toValidate.contains(cons))
                toValidate.add(cons);
            } catch (UnknownServerException exc) {
              logmon.log(BasicLevel.ERROR,
                         getName() + ", discard message to unknown server id#" + msg.getDest());
              msg.delete();
              msg.free();
              continue;
            } catch (NullPointerException exc) {
              logmon.log(BasicLevel.ERROR,
                         getName() + ", discard message to unknown server id#" + msg.getDest());
              msg.delete();
              msg.free();
              continue;
            } catch (ArrayIndexOutOfBoundsException exc) {
              logmon.log(BasicLevel.ERROR,
                         getName() + ", discard message to unknown server id#" + msg.getDest());
              msg.delete();
              msg.free();
              continue;
            }
          } else {
            logmon.log(BasicLevel.ERROR,
                       getName() + ", discard undelivered message from server id#" +  msg.getDest());
            msg.delete();
            continue;
          }
        }
        // BatchEngine and BatchNetwork need a global validate operation after insertion.        
        for (MessageConsumer cons : toValidate)
          cons.validate();
      } catch (ClassNotFoundException exc) {
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.FATAL, getName() + ", can't restore messages", exc);
        else
          logmon.log(BasicLevel.FATAL, getName() + ", can't restore messages: " + exc.getMessage());
        throw new Exception("Can't restore messages, bad classpath");
      } catch (IOException exc) {
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.FATAL, getName() + ", can't restore messages", exc);
        else
          logmon.log(BasicLevel.FATAL, getName() + ", can't restore messages: " + exc.getMessage());
        throw new Exception("Can't restore messages, storage problems");
      }

      // initializes channel before initializing fixed agents
      Channel.newInstance();    

      try {
        //  Initialize services, loads the configuration from transaction.
        ServiceManager.init();

        logmon.log(BasicLevel.INFO,
                   getName() + ", ServiceManager initialized");

        // Actually get Services from A3CML configuration file.
        // Be careful to not erase the ServiceManager list in order to avoid 
        // a new initialization of existing services.
        ServiceDesc services[] = AgentServer.getServices();
        if (services != null) {
          if (updatecfg != null) {
            logmon.log(BasicLevel.INFO, getName() + ", Updates ServiceManager");
            
            Set<String> list = ServiceManager.listServices();
            for (int i = 0; i < services.length; i ++) {
              list.remove(services[i].getClassName());
              // Adds new services or validates existing ones.
              ServiceManager.register(services[i].getClassName(),
                                      services[i].getArguments());
            }
            for (String scname : list) {
              // Removes services no longer declared in configuration
              ServiceManager.unregister(scname);
            }
          }
        } else {
          logmon.log(BasicLevel.WARN, getName() + ", no services from configuration");
        }
        ServiceManager.save();
      } catch (Exception exc) {
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.FATAL, getName() + ", can't initialize services", exc);
        else
          logmon.log(BasicLevel.FATAL, getName() + ", can't initialize services: " + exc.getMessage());
        throw new Exception("Can't initialize services");
      }
      
      // Load the service classes before initializing the engine
      ServiceManager.loadServiceClasses();

      // initializes fixed agents
      engine.init(new AgentEngineContextImpl());

      logmon.log(BasicLevel.INFO,
                 getName() + ", initialized at " + new Date());

      // Commit all changes.
      transaction.begin();
      transaction.commit(true);

      try {
        SCServerMBean bean = new SCServer();
        MXWrapper.registerMBean(bean, "AgentServer", "server=" + getName());
      } catch (Exception exc) {
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.WARN, getName() + " jmx failed", exc);
      }
    } catch (Exception exc) {
      if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.ERROR, getName() + " cannot initialize", exc);
      else
        logmon.log(BasicLevel.ERROR, getName() + " cannot initialize: " + exc.getMessage());
      synchronized(status) {
        // AF: Will be replaced by a BAD_INITIALIZED status allowing the
        // re-initialization..
        status.value = Status.INSTALLED;
      }
      throw exc;
    } catch (Throwable t) {
      if (logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.ERROR, getName() + " cannot initialize", t);
      else
        logmon.log(BasicLevel.ERROR, getName() + " cannot initialize: " + t.getMessage());
      synchronized(status) {
        // AF: Will be replaced by a BAD_INITIALIZED status allowing the
        // re-initialization..
        status.value = Status.INSTALLED;
      }
      throw new Exception(t.getMessage());
    }

    long dterr = getLong(CFG_CHECK_PERIOD_PROPERTY, -1);
    if (dterr > 0) {
      checkServerTask = new CheckServerTask(dterr);
    }

    synchronized(status) {
      status.value = Status.INITIALIZED;
    }
  }
  
  /**
   *  Name of the property specifying that the server should stop if any of the services can
   *  not start correctly. By default false.
   *  <p>
   *  This property can be fixed either from XML configuration file or Java launching command.
   */
  public final static String CFG_EXIT_ON_SERVICE_FAILURE_PROPERTY = "fr.dyade.aaa.agent.exitOnServiceFailure";

  /**
   *  Causes this AgentServer to begin its execution. This method starts all
   * <code>MessageConsumer</code> (i.e. the engine and the network components).
   * 
   * @return status of the server.
   * @throws Exception an error occurs.
   */
  public static String start() throws Exception {
    if (logmon.isLoggable(BasicLevel.DEBUG))
      logmon.log(BasicLevel.DEBUG, getName() + ", start()", new Exception());
    else
      logmon.log(BasicLevel.INFO, getName() + ", start()");

    synchronized(status) {
      if ((status.value != Status.INITIALIZED) &&
          (status.value != Status.STOPPED))
        throw new Exception("cannot start, bad status: " + getStatusInfo());
      status.value = Status.STARTING;
    }

    StringBuilder errBuf = null;
    try {
      try {
        boolean exitOnServiceFailure =  getBoolean(CFG_EXIT_ON_SERVICE_FAILURE_PROPERTY);
        ServiceManager.start(exitOnServiceFailure);
        // with osgi, ServiceManager start asynchronously, we can't save here.
        // ServiceManager.save(); //NTA
        logmon.log(BasicLevel.INFO, getName() + ", ServiceManager started");
      } catch (Exception exc) {
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.FATAL, getName() + ", can't start services", exc);
        else
          logmon.log(BasicLevel.FATAL, getName() + ", can't start services: " + exc.getMessage());
        throw new Exception("Can't start services: " + exc.getMessage());
      }

      // Now we can start all message consumers.
      if (consumers != null) {
        for (Enumeration<MessageConsumer> c=AgentServer.getConsumers(); c.hasMoreElements(); ) {
          MessageConsumer cons = c.nextElement();
          if (cons != null) {
            try {
              cons.start();
            } catch (IOException exc) {
              if (errBuf == null) errBuf = new StringBuilder();
              errBuf.append(cons.getName()).append(": ");
              errBuf.append(exc.getMessage()).append('\n');
              if (logmon.isLoggable(BasicLevel.DEBUG))
                logmon.log(BasicLevel.FATAL, getName() + ", problem during " + cons.getName() + " starting", exc);
              else
                logmon.log(BasicLevel.FATAL, getName() + ", problem during " + cons.getName() + " starting: " + exc.getMessage());
            }
          }
        }
      }
      // The server is running.
      logmon.log(BasicLevel.INFO, getName() + ", started at " + new Date());

      // Commit all changes.
      transaction.begin();
      transaction.commit(true);

      if (checkServerTask != null) {
        logmon.log(BasicLevel.INFO, getName() + ", schedule checkServer for " + checkServerTask.dtwarn + "ms.");
        getTimer().scheduleAtFixedRate(checkServerTask, checkServerTask.dtwarn, checkServerTask.dtwarn);
      }
    } catch (Exception exc) {
      if (logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.ERROR, getName() + "Cannot start", exc);
      else
        logmon.log(BasicLevel.ERROR, getName() + "Cannot start: " + exc.getMessage());
      synchronized(status) {
        // TODO AF: Will be replaced by a BAD_STARTED status allowing the stop and reset..
        status.value = Status.STOPPED;
      }
      throw exc;
    } catch (Throwable t) {
      if (logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.ERROR, getName() + "Cannot start", t);
      else
        logmon.log(BasicLevel.ERROR, getName() + "Cannot start: " + t.getMessage());
      synchronized(status) {
        // TODO AF: Will be replaced by a BAD_STARTED status allowing the
        // stop and reset..
        status.value = Status.STOPPED;
      }
      throw new Exception(t.getMessage());
    }

    synchronized(status) {
      status.value = Status.STARTED;
    }
    
    if (errBuf == null) return null;
    return errBuf.toString();
  }

  /**
   *  Forces this AgentServer to stop executing. This method stops all
   * consumers and services. Be careful, if you specify a synchronous
   * process, this method wait for all server's thread to terminate; so
   * if this method is called from a server's thread it should result a
   * dead-lock.
   *
   * @param sync	If true the stop is processed synchronously, otherwise
   *			a thread is created and the method returns.
   */
  public static void stop(boolean sync) {
    stop(sync, 0, false);
  }

  /**
   *  Forces this AgentServer to stop executing. This method stops all
   * consumers and services. Be careful, if you specify a synchronous
   * process, this method wait for all server's thread to terminate; so
   * if this method is called from a server's thread it should result a
   * dead-lock.
   *
   * @param sync	If true the stop is processed synchronously, otherwise
   *			a thread is created and the method returns.
   * @param delay       if sync is false then the thread in charge of
   *                    stopping the server waits this delay before
   *                    initiating the stop.
   * @param reset	If true the server is stopped then reseted.
   */
  public static void stop(boolean sync, long delay, boolean reset) {
    ServerStopper stopper = new ServerStopper(delay, reset);
    if (sync == true) {
      stopper.run();
    } else {
      if (logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.DEBUG, getName() + ", stop()", new Exception());

      // Creates a thread to execute AgentServer.stop in order to avoid deadlock.
      Thread t = new Thread(stopper, "AgentServerStopper");
      t.setDaemon(false);
      t.start();
    }
  }
  
  /* ==================================================
   * Code handling the AgentServer lock file.
   * ================================================== */

  /**
   * Try to lock the AgentServer lock file.
   * 
   * @param serverDataDirPath pathname of the persistence repository to update.
   * @return The AgentServer lock (locked).
   * @throws IOException  Error preventing AgentServer locking.
   */
  public static ServerLock lockAgentServer(String serverDataDirPath) throws IOException {
    return lockAgentServer(serverDataDirPath, false);
  }
  
  /**
   * Try to lock the AgentServer lock file.
   * If the data directory does not exist and createDataDir is true, try to create data directory.
   * 
   * @param serverDataDirPath pathname of the persistence repository to update.
   * @param createDataDir     if true, try to create data directory.
   * @return The AgentServer lock (locked).
   * @throws IOException  Error preventing AgentServer locking.
   */
  private static ServerLock lockAgentServer(String serverDataDirPath, boolean createDataDir) throws IOException {
    ServerLock lock = null;
    File dataDir = new File(serverDataDirPath);
    if (! (dataDir.exists() && dataDir.isDirectory())) {
      if (createDataDir) {
        if (! dataDir.mkdirs()) {
          throw new IOException("Cannot creates storage directory: " + serverDataDirPath);
        }
      } else {
        throw new FileNotFoundException("It seems that the transactional persistence is not initialized: " + serverDataDirPath);
      }
    }

    File lockfile = new File(serverDataDirPath, "lock");
    try {
      lock = new ServerLock(lockfile);
    } catch (IOException exc) {
      throw new IOException("It seems that this server is running, try to remove the lock file: " + lockfile.getCanonicalPath(), exc);
    }
    return lock;
  }

  static class ServerStopper implements Runnable {
    private long delay;
    private boolean reset;

    ServerStopper(long delay, boolean reset) {
      this.delay = delay;
      this.reset = reset;
    }

    public void run() {
      if (delay > 0) {
        try {
          Thread.sleep(delay);
        } catch (InterruptedException exc) {}
      }
      AgentServer.stop();
      if (reset) {
        AgentServer.reset();
      }
    }
  }
  
  /**
   *  Name of property allowing to configure the maximum duration of the wait in seconds allowing the
   * child threads of the AgentServer to terminate during shutdown. Once this time has elapsed, the
   * transactional persistence module is stopped and the AgentServer terminates.
   * <p>
   *  By Default, 120, 2 minutes.
   * <p>
   *  This property can only be fixed from <code>java</code> launching command.
   */
  public final static String MAX_THREAD_WAIT_BEFFORE_EXIT_PROPERTY = "fr.dyade.aaa.agent.MAX_THREAD_WAIT_BEFFORE_EXIT";
  public final static String MAX_THREAD_WAIT_BEFORE_EXIT_PROPERTY = "fr.dyade.aaa.agent.MAX_THREAD_WAIT_BEFORE_EXIT";
  public final static int DFTLT_MAX_THREAD_WAIT_BEFORE_EXIT = 120;

  /**
   *  Forces this AgentServer to stop executing. This method stops all
   * consumers and services. Be careful, the stop process is now synchronous
   * and wait for all server's thread to terminate ; If this method is called
   * from a server's thread it should result a dead-lock.
   */
  public static void stop() {
    if (logmon.isLoggable(BasicLevel.DEBUG))
      logmon.log(BasicLevel.DEBUG, getName() + ", stop()", new Exception());
    else
      logmon.log(BasicLevel.INFO, getName() + ", stop()");

    synchronized(status) {
      if ((status.value == Status.STOPPED) || (status.value == Status.INSTALLED)) {
        // The server is already stopped.
        if (logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.DEBUG,
                     getName() + " not running, status: " + getStatusInfo(), new Exception());
        return;
      }
      if (status.value != Status.STARTED) {
        logmon.log(BasicLevel.WARN,
                   getName() + " cannot stop, bad status: " + getStatusInfo());
        return;
      }
      status.value = Status.STOPPING;
    }

    try {
      // Note (AF): First cancel timer, then set it later to null.
      if (timer != null)
        timer.cancel();
      
      // Stop all message consumers.
      if (consumers != null) {
        for (Enumeration<MessageConsumer> c=AgentServer.getConsumers(); c.hasMoreElements(); ) {
          MessageConsumer cons = c.nextElement();
          if (cons != null) {
            if (logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG, getName() + ", stop " + cons.getName());

            cons.stop();

            if (logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG, getName() + ", " + cons.getName() + " stopped");
          }
        }
      }
      
      // Stop all services.
      ServiceManager.stop();

      // Wait for all threads before stop the TM !!
      Integer value = AgentServer.getInteger(MAX_THREAD_WAIT_BEFFORE_EXIT_PROPERTY);
      if (value != null) {
        logmon.log(BasicLevel.WARN,
                   "Property " + MAX_THREAD_WAIT_BEFFORE_EXIT_PROPERTY + " is deprecated, please use " + MAX_THREAD_WAIT_BEFORE_EXIT_PROPERTY);
        value = AgentServer.getInteger(MAX_THREAD_WAIT_BEFORE_EXIT_PROPERTY, value.intValue());
        logmon.log(BasicLevel.WARN,
                   "Property MAX_THREAD_WAIT_BEFORE_EXIT_PROPERTY is initialized to " + value.intValue());
      } else {
        value = AgentServer.getInteger(MAX_THREAD_WAIT_BEFORE_EXIT_PROPERTY, DFTLT_MAX_THREAD_WAIT_BEFORE_EXIT);
      };
      int nbMaxTry = value.intValue();
      
      int nbtry = 0;
      while (nbtry < nbMaxTry) {
        int nbthread = getThreadGroup().activeCount();
        if (nbthread == 0) break;

        Thread[] tab = new Thread[nbthread];
        getThreadGroup().enumerate(tab);
        if ((nbthread == 1) && (tab[0] == Thread.currentThread())) break;

        for (int j=0; j<tab.length; j++) {
          logmon.log(BasicLevel.DEBUG,
                     "[" +  tab[j].getName() + ":" + (tab[j].isAlive()?"alive":"-") + "/" + (tab[j].isDaemon()?"daemon":"-") + "," + tab[j]);
        }
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {}
      }

      // Stop the transaction manager.
      if (transaction != null) transaction.stop();
      // Wait for the transaction manager stop

      Runtime.getRuntime().gc();

      logmon.log(BasicLevel.INFO, getName() + ", stopped at " + new Date());
    } catch (Throwable t) {
      if (logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.ERROR, getName() + "Cannot stop", t);
      else
        logmon.log(BasicLevel.ERROR, getName() + "Cannot stop: " + t.getMessage());
      synchronized(status) {
        // AF: Will be replaced by a BAD_STOPPED status allowing the
        // stop and reset..
        status.value = Status.STOPPED;
      }
    } finally {
      // Set timer to null allowing to allocate a new one if needed later.
      timer = null;
    }

    synchronized(status) {
      status.value = Status.STOPPED;
    }
  }

  public static final String OKSTRING = "OK";
  public static final String ERRORSTRING = "ERROR";
  public static final String ENDSTRING = "END";

//  public static PrintStream sdf = null;
//  public static Logger logsdf = null;

  /**
   * Main for a standard agent server.
   * The start arguments include in first position the identifier of the
   * agent server to start, and in second position the directory name where
   * the agent server stores its persistent data.
   *
   * @param args	start arguments
   *
   * @exception Exception
   *	unspecialized exception
   */
  public static void main(String args[]) throws Exception {
    try {
      init(args);
    } catch (Throwable exc) {
      System.out.println(getName() + "initialization failed: " + ERRORSTRING);
      System.out.println(exc.toString());
      System.out.println(ENDSTRING);
      logmon.log(BasicLevel.ERROR, getName() + " initialization failed", exc);
      System.exit(1);
    }

    try {
      String errStr = start();
      // Be careful, the output below is needed by some tools (AdminProxy for
      // example.
      if (errStr == null) {
        System.out.println(getName() + " started: " + OKSTRING);
      } else {
        System.out.println(getName() + " started: " + ERRORSTRING);
        System.out.print(errStr);
        System.out.println(ENDSTRING);
      }
    } catch (Throwable exc) {
      System.out.println(getName() + " start failed: " + ERRORSTRING);
      System.out.print(exc.toString());
      System.out.println(ENDSTRING);
      if (logmon == null)
        logmon = Debug.getLogger(AgentServer.class.getName());
      logmon.log(BasicLevel.ERROR, getName() + " failed", exc);
      System.exit(1);
    }
  }
}

class CheckServerTask extends TimerTask {
  private static Logger logmon = Debug.getLogger(AgentServer.class.getName());

  long dtwarn, dterr;
  long lastThrow = -1L;

  AgentEngine engine = null;
  Transaction transaction = null;

  Vector<CheckServerListener> listeners = null;

  CheckServerTask(long dterr) {
    engine = AgentServer.getEngine();
//    waitingMessage = (engine.getNbWaitingMessages() > 0);

    transaction = AgentServer.getTransaction();

    if (dterr < 10000) {
      dterr = 10000;
      logmon.log(BasicLevel.WARN,
                 AgentServer.getName() + " checkServer period cannot be less than 10s.");
    }
    this.dtwarn = dterr / 5;
    this.dterr = dterr;
    
    listeners = new Vector<>();
  }

  public synchronized void registerListener(CheckServerListener listener) {
    logmon.log(BasicLevel.INFO,
               AgentServer.getName() + " registers listener for CheckServer.");
    listeners.addElement(listener);
  }

  public synchronized void unregisterListener(CheckServerListener listener) {
    logmon.log(BasicLevel.INFO,
               AgentServer.getName() + " unregisters listener for CheckServer.");
    listeners.removeElement(listener);
  }

  private synchronized void throwListeners() {
    for (CheckServerListener listener : listeners) {
      logmon.log(BasicLevel.DEBUG,
                 AgentServer.getName() + " calls listener for CheckServer.");
      listener.onServerCheckError();
    }
  }

  @Override
  public void run() {
    long now = System.currentTimeMillis();

    if ((engine == null) || (transaction == null)) {
      // Log an error and throw an alert
      logmon.log(BasicLevel.ERROR, AgentServer.getName() + " not initialized.");
      throwListeners();
    }

    if ((checkEngine(now) || checkTransaction(now)) && ((now - lastThrow) > dterr)) {
      throwListeners();
      lastThrow = now;
    }
  }
  
  long lastEngineChangeDate = -1L;
  long lastEngineWarnDate = -1L;
  long reactCount = 0;

  private boolean idleEngine() {
    return (engine.getRunningAgent() == null) && (engine.getNbWaitingMessages() == 0);
  }
  
  private boolean checkEngine(long now) {
    logmon.log(BasicLevel.DEBUG,
               AgentServer.getName() + ".checkEngine():" + engine.getNbReactions() + ", " + reactCount);

    if (engine.getNbReactions() > reactCount) {
      reactCount = engine.getNbReactions();
      lastEngineChangeDate = now;
      return false;
    }

    if (! engine.isRunning()) {
      // The engine does not run, just log a warn if needed
      if (((now - lastEngineChangeDate) > dtwarn) && ((now - lastEngineWarnDate) > dtwarn)) {
        logmon.log(BasicLevel.WARN,
                   AgentServer.getName() + ", engine is not running.");
        lastEngineWarnDate = now;
      }
      return false;
    }

    if (idleEngine()) {
      // The engine was idle, just log a warn if needed
      if (((now - lastEngineChangeDate) > dtwarn) && ((now - lastEngineWarnDate) > dtwarn)) {
        logmon.log(BasicLevel.WARN,
                   AgentServer.getName() + ", engine is idle.");
        lastEngineWarnDate = now;
      }
      return false;
    }


    if (((now - lastEngineChangeDate) > dterr) && ((now - lastEngineWarnDate) > dtwarn)) {
      // Log an error and throw an alert
      logmon.log(BasicLevel.ERROR,
                 AgentServer.getName() + ", the engine doesn't progress since " + new Date(lastEngineChangeDate));
      lastEngineWarnDate = now;
      return true;
    } else if (((now - lastEngineChangeDate) > dtwarn) && ((now - lastEngineWarnDate) > dtwarn)) {
      // Log a warning
      logmon.log(BasicLevel.WARN,
                 AgentServer.getName() + ", the engine doesn't progress since " + new Date(lastEngineChangeDate));
      lastEngineWarnDate = now;
      return false;
    }
    
    return false;
  }

  int commitCount = 0;
  long lastTransactionChangeDate = -1L;
  long lastTransactionWarnDate = -1L;

  private boolean checkTransaction(long now) {
    logmon.log(BasicLevel.DEBUG,
               AgentServer.getName() + ".checkTransaction()" + transaction.getCommitCount() + ", " + commitCount + "," + transaction.getPhase());

    if (transaction.getCommitCount() > commitCount) {
      commitCount = transaction.getCommitCount();
      lastTransactionChangeDate = now;
      return false;
    }

    if (transaction.getPhase() == Transaction.FREE) {
      lastTransactionChangeDate = now;
      return false;
    }
    
    if (((now - lastTransactionChangeDate) > dterr) && ((now - lastTransactionWarnDate) > dtwarn)) {
      // Log an error and throw an alert
      logmon.log(BasicLevel.ERROR,
                 AgentServer.getName() + ", Transaction doesn't progress since " + new Date(lastTransactionChangeDate));
      lastTransactionWarnDate = now;
    } else if (((now - lastTransactionChangeDate) > dtwarn) && ((now - lastTransactionWarnDate) > dtwarn)) {
      // Log a warning
      logmon.log(BasicLevel.WARN,
                 AgentServer.getName() + ", Transaction doesn't progress since " + new Date(lastTransactionChangeDate));
      lastTransactionWarnDate = now;
    }

    return true;
  }
}
