/*
 * Copyright (C) 2003 - 2025 ScalAgent Distributed Technologies
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA.
 *
 * Initial developer(s): ScalAgent Distributed Technologies
 * Contributor(s): 
 */
package fr.dyade.aaa.agent;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

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

import fr.dyade.aaa.common.Daemon;

/**
 * <tt>HttpNetwork</tt> is a simple implementation of <tt>StreamNetwork</tt>
 * based on HTTP 1.1 protocol.
 */
public class HttpNetwork extends StreamNetwork implements HttpNetworkMBean {
  /**
   * Network address of proxy server.
   * 
   * @see #proxyhost
   * @see #proxyport
   */
  private InetAddress proxy = null;
  /**
   *  Hostname (or IP dotted address) of proxy host, if not defined there
   * is a direct connection between the client and the server.
   *  This value can be adjusted for all HttpNetwork components by setting
   * <code>proxyhost</code> global property or for a particular
   * network by setting <code>&lt;DomainName&gt;.proxyhost</code>
   * specific property.
   * <p>
   *  Theses properties can be fixed either from <code>java</code> launching
   * command, or in <code>a3servers.xml</code> configuration file.
   */
  String proxyhost = null;
  
  /**
   * Gets the proxyhost value.
   *
   * @return the proxyhost value
   */
  public String getProxyhost() {
    return proxyhost;
  }

  /**
   *  Port number of proxy if any.
   *  This value can be adjusted for all HttpNetwork components by setting
   * <code>proxyport</code> global property or for a particular
   * network by setting <code>&lt;DomainName&gt;.proxyport</code>
   * specific property.
   * <p>
   *  Theses properties can be fixed either from <code>java</code> launching
   * command, or in <code>a3servers.xml</code> configuration file.
   */
  int proxyport = 0;
  
  /**
   * Gets the proxyport value.
   *
   * @return the proxyport value
   */
  public long getProxyport() {
    return proxyport;
  }

  /**
   * Period of time between two activation of NetServerOut, it matches to the
   * time between two requests from the client to the server when there is no
   * message to transmit from client to server. This value can be adjusted for
   * all HttpNetwork components by setting <code>ActivationPeriod</code>
   * global property or for a particular network by setting
   * <code>&lt;DomainName&gt;.ActivationPeriod</code> specific property.
   * <p>
   * Theses properties can be fixed either from <code>java</code> launching
   * command, or in <code>a3servers.xml</code> configuration file. By default,
   * its value is 10000 (10s).
   */
  protected long activationPeriod = 10000L;

  /**
   * Gets the activationPeriod value.
   *
   * @return the activationPeriod value
   */
  public long getActivationPeriod() {
    return activationPeriod;
  }

  /**
   * Sets the activationPeriod value.
   *
   * @param activationPeriod	the activationPeriod value
   */
  public void setActivationPeriod(long activationPeriod) {
    this.activationPeriod = activationPeriod;
  }

  /**
   *  Number of listening daemon, this value is only valid for the server
   * part of the HttpNetwork.
   *  This value can be adjusted for all HttpNetwork components by setting
   * <code>NbDaemon</code> global property or for a particular network by
   * setting <code>&lt;DomainName&gt;.NbDaemon</code> specific property.
   * <p>
   *  Theses properties can be fixed either from <code>java</code> launching
   * command, or in <code>a3servers.xml</code> configuration file.
   */
  int NbDaemon = 1;

  /**
   * Gets the NbDaemon value.
   *
   * @return the NbDaemon value
   */
  public long getNbDaemon() {
    return NbDaemon;
  }

  /**
   * Creates a new network component.
   */
  public HttpNetwork() {
    super();
  }

  /**
   * Descriptor of the listen server, it is used only on the client side
   * (NetServerOut component).
   */
  ServerDesc server = null;

  /**
   * Initializes a new network component. This method is used in order to
   * easily creates and configure a Network component from a class name.
   * So we can use the <code>Class.newInstance()</code> method for create
   * (whitout any parameter) the component, then we can initialize it with
   * this method.<br>
   * This method initializes the logical clock for the domain.
   *
   * @param name	The domain name.
   * @param port	The listen port.
   * @param servers	The list of servers directly accessible from this
   *			network interface.
   */
  public void init(String name, int port, short[] servers) throws Exception {
    super.init(name, port, servers);

    activationPeriod = AgentServer.getLong("ActivationPeriod", activationPeriod).longValue();
    activationPeriod = AgentServer.getLong(name + ".ActivationPeriod", activationPeriod).longValue();
    
    NbDaemon = AgentServer.getInteger("NbDaemon", NbDaemon).intValue();
    NbDaemon = AgentServer.getInteger(name + ".NbDaemon", NbDaemon).intValue();

    proxyhost = AgentServer.getProperty("proxyhost");
    proxyhost = AgentServer.getProperty(name + ".proxyhost", proxyhost);
    if (proxyhost != null) {
      proxyport = AgentServer.getInteger("proxyport", 8080).intValue();
      proxyport = AgentServer.getInteger(name + ".proxyport", proxyport).intValue();
      proxy = InetAddress.getByName(proxyhost);
    }
  }

  /** Daemon component */
  Daemon dmon[] = null;

  /**
   * Causes this network component to begin execution.
   */
  public void start() throws Exception {
    logger.log(BasicLevel.DEBUG, getName() + ", starting");
    try {
      if (isRunning()) return;

      if (port != 0) {
        dmon = new Daemon[NbDaemon];
        ServerSocket listen = createServerSocket();

        for (int i=0; i<NbDaemon; i++) {
          dmon[i] = new NetServerIn(getName() + '.' + i, listen, logger);
        }
      } else {
        // AF: May be, we have to verify that there is only one 'listen' network.
        for (int i=0; i<servers.length; i++) {
          server = AgentServer.getServerDesc(servers[i]);
          if ((server.getServerId() != AgentServer.getServerId()) && (server.getPort() > 0)) {
            logger.log(BasicLevel.DEBUG, getName() + ", server=" + server);
            break;
          }
          server = null;
        }
        dmon = new Daemon[1];
        dmon[0] = new NetServerOut(getName(), logger);
      }

      for (int i=0; i<dmon.length; i++) {
        dmon[i].start();
      }
    } catch (IOException exc) {
      logger.log(BasicLevel.ERROR, getName() + ", can't start", exc);
      throw exc;
    }
    logger.log(BasicLevel.DEBUG, getName() + ", started");
  }

  /**
   * Wakes up the watch-dog thread.
   */
  public void wakeup() {
//     if (dmon != null) dmon.wakeup();
  }

  /**
   * Forces the network component to stop executing.
   */
  public void stop() {
    if (dmon != null) {
      for (int i=0; i<dmon.length; i++) {
        dmon[i].stop();
      }
    }
    logger.log(BasicLevel.DEBUG, getName() + ", stopped");
  }

  /**
   * Tests if the network component is alive.
   *
   * @return	true if this <code>MessageConsumer</code> is alive; false
   * 		otherwise.
   */
  public boolean isRunning() {
    if (dmon != null) {
      for (int i=0; i<dmon.length; i++) {
        if (dmon[i].isRunning()) return true;
      }
    }
    return false;
  }

  /**
   * Returns a string representation of this consumer, including the
   * daemon's name and status.
   *
   * @return	A string representation of this consumer. 
   */
  public String toString() {
    StringBuffer strbuf = new StringBuffer();

    strbuf.append(super.toString()).append("\n\t");
    if (dmon != null) {
      for (int i=0; i<dmon.length; i++) {
        strbuf.append(dmon[i].toString()).append("\n\t");
      }
    }
    return strbuf.toString();
  }
  
  String readLine(InputStream is, byte[] buf) throws IOException {
    int i = 0;
    while ((buf[i++] = (byte) is.read()) != -1) {
      if ((buf[i-1] == '\n') && (buf[i-2] == '\r')) {
        i -= 2;
        break;
      }
    }
      
    if (i > 0) return new String(buf, 0, i);
    
    return null;
  }

  protected void sendRequest(Message msg,
                             OutputStream os,
                             NetworkOutputStream nos,
                             int ack,
                             long currentTimeMillis) throws Exception {
    StringBuffer strbuf = new StringBuffer();

    strbuf.append("PUT ");
    if (proxy != null) {
      strbuf.append("http://").append(server.getHostname()).append(':').append(server.getPort());
    }
    strbuf.append("/msg?from=").append(AgentServer.getServerId());
    strbuf.append("&stamp=");
    if (msg != null) {
      strbuf.append(msg.getStamp());
    } else {
      strbuf.append("-1");
    }
    strbuf.append(" HTTP/1.1");
    
    nos.reset();
    nos.writeMessage(msg, ack, currentTimeMillis);

    if (proxy != null)
      strbuf.append("\r\nHost: ").append(server.getHostname());
    strbuf.append("\r\n" +
                  "User-Agent: ScalAgent 1.0\r\n" +
                  "Accept: image/jpeg;q=0.2\r\n" +
                  "Accept-Language: fr, en-us;q=0.50\r\n" +
                  "Accept-Encoding: gzip;q=0.9\r\n" +
                  "Accept-Charset: ISO-8859-1, utf-8;q=0.66\r\n" +
                  "Cache-Control: no-cache\r\n" +
                  "Cache-Control: no-store\r\n" +
                  "Keep-Alive: 300\r\n" +
                  "Connection: keep-alive\r\n" +
                  "Proxy-Connection: keep-alive\r\n" +
                  "Pragma: no-cache\r\n");
    strbuf.append("Content-Length: ").append(nos.size());
    strbuf.append("\r\n" +
                  "Content-Type: image/jpeg\r\n");
    strbuf.append("\r\n");

    os.write(strbuf.toString().getBytes());
    
    if (DEBUG)
      logger.log(BasicLevel.DEBUG, name + ", writes:" + nos.size());

    nos.writeTo(os);
    os.flush();
  }

  protected final short getRequest(InputStream is,
                             NetworkInputStream nis,
                             byte[] buf) throws Exception {
    String line = null;

    line = readLine(is, buf);
    if ((line == null) ||
        (! (line.startsWith("GET ") || line.startsWith("PUT ")))) {
      throw new Exception("Bad request: " + line);
    }

    int idx1 = line.indexOf("?from=");
    if (idx1 == -1) throw new Exception("Bad request: " + line);
    int idx2 = line.indexOf("&", idx1);
    if (idx2 == -1) throw new Exception("Bad request: " + line);
    short from = Short.parseShort(line.substring(idx1+6, idx2));

    // Skip all header lines, get length
    int length = 0;
    while (line != null) {
      line = readLine(is, buf);
      if ((line != null) && line.startsWith("Content-Length: ")) {
        // get content length
        length = Integer.parseInt(line.substring(16));
        if (DEBUG)
          logger.log(BasicLevel.DEBUG, name + ", length:" + length);
      }
    }

    nis.readFrom(is, length);
    return from;
  }

  protected final void sendReply(Message msg,
                                 OutputStream os,
                                 NetworkOutputStream nos,
                                 int ack,
                                 long currentTimeMillis) throws Exception {
    StringBuffer strbuf = new StringBuffer();

    strbuf.append("HTTP/1.1 200 OK\r\n");

    nos.reset();
    nos.writeMessage(msg, ack, currentTimeMillis);

    strbuf.append("Date: ").append("Fri, 21 Feb 2003 14:30:51 GMT");
    strbuf.append("\r\n" +
                  "Server: ScalAgent 1.0\r\n" +
                  "Last-Modified: ").append("Wed, 19 Apr 2000 08:16:28 GMT");
    strbuf.append("\r\n" +
                  "Cache-Control: no-cache\r\n" +
                  "Cache-Control: no-store\r\n" +
                  "Accept-Ranges: bytes\r\n" +
                  "Keep-Alive: timeout=15, max=100\r\n" +
                  "Connection: Keep-Alive\r\n" +
                  "Proxy-Connection: Keep-Alive\r\n" +
                  "Pragma: no-cache\r\n");
    strbuf.append("Content-Length: ").append(nos.size());
    strbuf.append("\r\n" +
                  "Content-Type: image/gif\r\n");
    strbuf.append("\r\n");

    os.write(strbuf.toString().getBytes());
    
    if (DEBUG)
      logger.log(BasicLevel.DEBUG, name + ", writes:" + nos.size());

    nos.writeTo(os);
    os.flush();
  }

  protected void getReply(InputStream is,
                          NetworkInputStream nis,
                          byte[] buf) throws Exception {
    String line = null;

    line = readLine(is, buf);
    if ((line == null) ||
        ((! line.equals("HTTP/1.1 200 OK")) &&
         (! line.equals("HTTP/1.1 204 No Content")))) {
      throw new Exception("Bad reply: " + line);
    }

    // Skip all header lines, get length
    int length = 0;
    while (line != null) {
      line = readLine(is, buf);
      if ((line != null) && line.startsWith("Content-Length: ")) {
        // get content length
        length = Integer.parseInt(line.substring(16));
        if (DEBUG)
          logger.log(BasicLevel.DEBUG, name + ", length:" + length);
      }
    }

    nis.readFrom(is, length);
  }

  protected int handle(Message msgout,
                       NetworkInputStream nis) throws Exception {
    int ack = nis.getAckStamp();

    if (DEBUG)
      logger.log(BasicLevel.DEBUG,
                 this.getName() + ", handle: " + msgout + ", ack=" + ack);

    if ((msgout != null) && (msgout.stamp == ack)) {
      AgentServer.getTransaction().begin();
      //  Suppress the processed notification from message queue,
      // and deletes it.
      qout.removeMessage(msgout);
      msgout.delete();
      msgout.free();
      AgentServer.getTransaction().commit(true);
    }

    Message msg = nis.getMessage();
    if (DEBUG)
      logger.log(BasicLevel.DEBUG,
                 this.getName() + ", get: " + msg);

    if (msg != null) {
      ack = msg.stamp;
      testBootTS(msg.getSource(), nis.getBootTS());
      deliver(msg);
      return ack;
    }

    return -1;
  }

  final class NetServerOut extends Daemon {
    Socket socket = null;

    InputStream is = null;
    OutputStream os = null;

    NetworkInputStream nis = null;
    NetworkOutputStream nos = null;

    NetServerOut(String name, Logger logmon) throws IOException {
      super(name + ".NetServerOut", logmon);
      // Overload logmon definition in Daemon
      this.logmon = logmon;

      nis = new NetworkInputStream();
      nos = new NetworkOutputStream();
    }
    
    int nbCnxTry = 0;

    protected void open(long currentTimeMillis) throws IOException {
      if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.DEBUG, this.getName() + ", open: " + nbCnxTry);

      // If there are errors wait to open the connection.
      if (nbCnxTry != 0) {
        try {
          if (nbCnxTry < WDNbRetryLevel1) {
            if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG, this.getName() + ", wait for " + WDRetryPeriod1);
            Thread.sleep(WDRetryPeriod1);
          } else if (nbCnxTry < WDNbRetryLevel2) {
            if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG, this.getName() + ", wait for " + WDRetryPeriod2);
            Thread.sleep(WDRetryPeriod2);
          } else {
            if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG, this.getName() + ", wait for " + WDRetryPeriod3);
            Thread.sleep(WDRetryPeriod3);
          }
        } catch (InterruptedException exc) {}
        
        if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
          logmon.log(BasicLevel.DEBUG, this.getName() + ", end of watchdog period");
      }
      
      // Open the connection.
      socket = null;

      nbCnxTry += 1;
      if (proxy == null) {
        try {
          socket = createSocket(server);
        } catch (IOException exc) {
          logmon.log(BasicLevel.WARN, this.getName() + ", connection refused", exc);
          throw exc;
        }
      } else {
        try {
          socket = createSocket(proxy, proxyport);
        } catch (IOException exc) {
          logmon.log(BasicLevel.WARN, this.getName() + ", connection refused, reset addr");
          proxy = InetAddress.getByName(proxyhost);
          throw exc;
        }
      }
      nbCnxTry = 0;
      setSocketOption(socket);

      os = socket.getOutputStream();
      is = socket.getInputStream();
    }

    protected void close() {
      try {
        os.close();
      } catch (Exception exc) {}
      os = null;
      try {
        is.close();
      } catch (Exception exc) {}
      is = null;
      try {
        socket.close();
      } catch (Exception exc) {}
      socket = null;
    }

    protected void shutdown() {
      thread.interrupt();
      close();
    }

    public void run() {
      Message msgout = null;
      int ack = -1;
      long currentTimeMillis = -1;
      byte[] buf = new byte[120];

      try {
        while (running) {
          canStop = true;

          try {
            currentTimeMillis = System.currentTimeMillis();
            do {
              // Removes all expired messages in qout.
              // AF: This task should be run regularly.
              msgout = qout.removeExpired(currentTimeMillis);
              if (msgout != null) {
                logmon.log(BasicLevel.DEBUG,
                           this.getName() + ", Handles expired message: " + msgout);

                if (msgout.not.deadNotificationAgentId != null) {
                  ExpiredNot expiredNot = new ExpiredNot(msgout.not,
                                                         msgout.from,
                                                         msgout.to);
                  AgentServer.getTransaction().begin();
                  Channel.post(Message.alloc(AgentId.localId, msgout.not.deadNotificationAgentId,
                                             expiredNot));
                  Channel.validate();
                  AgentServer.getTransaction().commit(true);
                }
                // Suppress the processed notification from message queue and deletes it.
                // It can be done outside of a transaction and committed later (on next handle).
                msgout.delete();
                msgout.free();
              }
            } while (msgout != null);

            try {
              if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
                logmon.log(BasicLevel.DEBUG,
                           this.getName() + ", waiting message: " + activationPeriod);

              msgout = qout.get(activationPeriod);
            } catch (InterruptedException exc) {
              if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
                logmon.log(BasicLevel.DEBUG,
                           this.getName() + ", interrupted");
            }
            open(currentTimeMillis);

            do {
              if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
                logmon.log(BasicLevel.DEBUG,
                           this.getName() + ", sendRequest: " + msgout + ", ack=" + ack);

              currentTimeMillis = System.currentTimeMillis();
              do {
                if ((msgout != null) &&
                    (msgout.not.expiration > 0) &&
                    (msgout.not.expiration < currentTimeMillis)) {
                  if (msgout.not.deadNotificationAgentId != null) {
                    if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG)) {
                      logmon.log(BasicLevel.DEBUG, getName() + ": forward expired notification "
                                 + msgout.from + ", " + msgout.not + " to " + msgout.not.deadNotificationAgentId);
                    }
                    ExpiredNot expiredNot = new ExpiredNot(msgout.not, msgout.from, msgout.to);
                    AgentServer.getTransaction().begin();
                    Channel.post(Message.alloc(AgentId.localId, msgout.not.deadNotificationAgentId,
                                               expiredNot));
                    Channel.validate();
                    AgentServer.getTransaction().commit(true);
                  } else {
                    if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG)) {
                      logmon.log(BasicLevel.DEBUG, getName() + ": removes expired notification "
                                 + msgout.from + ", " + msgout.not);
                    }
                  }
                  // Suppress the processed notification from message queue,
                  // and deletes it. It can be done outside of a transaction
                  // and committed later (on next handle).
                  qout.removeMessage(msgout);
                  msgout.delete();
                  msgout.free();

                  msgout = qout.get(0L);
                  continue;
                }
                break;
              } while (true);

              sendRequest(msgout, os, nos, ack, currentTimeMillis);
              getReply(is, nis, buf);

              canStop = false;
              ack = handle(msgout, nis);
              canStop = true;
              // Get next message to send if any
              msgout = qout.get(0);
            } while (running && ((msgout != null) || (ack != -1)));
          } catch (Exception exc) {
            if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG,
                         this.getName() + ", connection closed", exc);
          } finally {
            if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG,
                         this.getName() + ", connection ends");
            close();
          }
        }
      } finally {
        logmon.log(BasicLevel.WARN, ", exited");
        finish();
      }
    }
  }

  final class NetServerIn extends Daemon {
    ServerSocket listen = null;
    
    Socket socket = null;

    InputStream is = null;
    OutputStream os = null;

    NetworkInputStream nis = null;
    NetworkOutputStream nos = null;

    NetServerIn(String name, ServerSocket listen, Logger logmon) throws IOException {
      super(name + ".NetServerIn", logmon);
      this.listen = listen;
      // Overload logmon definition in Daemon
      this.logmon = logmon;

      nis = new NetworkInputStream();
      nos = new NetworkOutputStream();
    }

    protected void open(Socket socket) throws IOException {
      setSocketOption(socket);

      os = socket.getOutputStream();
      is = socket.getInputStream();

      if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
        logmon.log(BasicLevel.DEBUG, this.getName() + ", connected");
    }

    protected void close() {
      try {
        os.close();
      } catch (Exception exc) {}
      os = null;
      try {
        is.close();
      } catch (Exception exc) {}
      is = null;
      try {
        socket.close();
      } catch (Exception exc) {}
      socket = null;
    }

    protected void shutdown() {
      close();
      try {
        listen.close();
      } catch (Exception exc) {}
      listen = null;
    }

    public void run() {
      Message msgout= null;
      int ack = -1;

      byte[] buf = new byte[120];

      try {
        while (running) {
          canStop = true;

          // Get the connection
          try {
            if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG,
                         this.getName() + ", waiting connection");
            socket = listen.accept();
            open(socket);

            msgout = null;
            short from = getRequest(is, nis, buf);
            long currentTimeMillis = System.currentTimeMillis();
            do {
              canStop = false;
              ack = handle(msgout, nis);
              canStop = true;

              do {
                msgout = qout.getMessageTo(from);

                if ((msgout != null) && (msgout.not.expiration > 0L)
                    && (msgout.not.expiration < currentTimeMillis)) {

                  if (msgout.not.deadNotificationAgentId != null) {
                    if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG)) {
                      logmon.log(BasicLevel.DEBUG, getName() + ": forward expired notification "
                                 + msgout.from + ", " + msgout.not + " to " + msgout.not.deadNotificationAgentId);
                    }
                    ExpiredNot expiredNot = new ExpiredNot(msgout.not, msgout.from, msgout.to);
                    AgentServer.getTransaction().begin();
                    Channel.post(Message.alloc(AgentId.localId,
                                               msgout.not.deadNotificationAgentId,
                                               expiredNot));
                    Channel.validate();
                    AgentServer.getTransaction().commit(true);
                  } else {
                    if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG)) {
                      logmon.log(BasicLevel.DEBUG,
                                 getName() + ": removes expired notification " +
                                 msgout.from + ", " + msgout.not);
                    }
                  }
                  // Suppress the processed notification from message queue,
                  // and deletes it. It can be done outside of a transaction
                  // and committed later (on next handle).
                  qout.removeMessage(msgout);
                  msgout.delete();
                  msgout.free();

                  continue;
                }
                break;
              } while (true);

              if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
                logmon.log(BasicLevel.DEBUG,
                           this.getName() + ", sendReply: " + msgout);

              sendReply(msgout, os, nos, ack, currentTimeMillis);
              getRequest(is, nis, buf);
            } while (running);
          } catch (Exception exc) {
            if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG, ", connection closed", exc);
          } finally {
            if (DEBUG && logmon.isLoggable(BasicLevel.DEBUG))
              logmon.log(BasicLevel.DEBUG, ", connection ends");
            close();
          }
        }
      } finally {
        logmon.log(BasicLevel.WARN, ", exited");
        finish();
      }
    }
  }

  /**
   * Class used to read messages through a stream.
   */
  final class NetworkInputStream extends BufferedMessageInputStream {
    NetworkInputStream() {
      super();
    }

    // The boot timestamp of the incoming message.
    int boot;
    // The stamp of last acked outgoing message.
    int ack;

    /**
     * Reads the protocol header from this output stream.
     */
    protected void readHeader() throws IOException {
      readFully(8);
      // Reads boot timestamp of source server
      boot = readInt();
      ack = readInt();
    }

    Message msg = null;

    /**
     * Reads next header or message from inputstream.
     * /!\ Be careful, this method always return length or it throws an exception.
     * 
     * @param is
     * @param length
     * @return
     * @throws Exception
     */
    int readFrom(InputStream is, int length) throws Exception {
      this.in = is;
      if (length == 8) {
        readHeader();
        msg = null;
      } else {
        msg = readMessage();
      }

      clean();

      return length;
    }

    Message getMessage() {
      return msg;
    }

    int getBootTS() {
      return boot;
    }

    int getAckStamp() {
      return ack;
    }
  }

  /**
   * Class used to send messages through a stream.
   */
  final class NetworkOutputStream extends ByteArrayMessageOutputStream {
    /** Stamp of last acked message. */
    private int ack;

    NetworkOutputStream() throws IOException {
      super();
    }

    /**
     * Writes the protocol header to this output stream.
     */
    protected void writeHeader() {
      // Writes boot timestamp of source server
      writeInt(getBootTS());
      // Writes stamp of last received message
      writeInt(ack);
    }

    void writeMessage(Message msg, int ack,
                      long currentTimeMillis) throws IOException {
      this.ack = ack;
      super.writeMessage(msg, currentTimeMillis);
    }
  }
}
