mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

pgamba
10.43.2009 45eb21b1354b6925fc058f834f505a9699d1bbbe
External Changelog - first step - related issues 495,  519
18 files added
25 files modified
14225 ■■■■ changed files
opends/resource/config/config.ldif 1 ●●●● patch | view | raw | blame | history
opends/src/messages/messages/protocol.properties 3 ●●●●● patch | view | raw | blame | history
opends/src/messages/messages/replication.properties 9 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/controls/EntryChangelogNotificationControl.java 173 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/controls/ExternalChangelogRequestControl.java 170 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/PersistentSearch.java 11 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/loggers/debug/DebugLogger.java 2 ●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/common/ExternalChangeLogSession.java 52 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/common/MultiDomainServerState.java 215 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/common/ServerState.java 9 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/plugin/MultimasterReplication.java 19 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/protocol/ECLUpdateMsg.java 189 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/protocol/HeartbeatMonitor.java 7 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/protocol/ReplServerStartMsg.java 12 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/protocol/ReplicationMsg.java 12 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/protocol/ServerStartECLMsg.java 377 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/protocol/ServerStartMsg.java 12 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/protocol/StartECLSessionMsg.java 432 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/DataServerHandler.java 724 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/ECLServerHandler.java 1551 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/ECLServerWriter.java 306 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/ExternalChangeLogSessionImpl.java 85 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/LightweightServerHandler.java 4 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/MessageHandler.java 912 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/ReplicationServer.java 200 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/ReplicationServerDomain.java 542 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/ReplicationServerHandler.java 791 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/SafeDataExpectedAcksInfo.java 2 ●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/ServerHandler.java 3432 ●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/ServerReader.java 109 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/ServerWriter.java 48 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/server/StatusAnalyzer.java 5 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/replication/service/ReplicationBroker.java 309 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/util/ServerConstants.java 20 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/workflowelement/externalchangelog/ECLSearchOperation.java 1300 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/workflowelement/externalchangelog/ECLWorkflowElement.java 220 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/workflowelement/externalchangelog/package-info.java 38 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/controls/ExternalChangelogControlTest.java 100 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/ReplicationTestCase.java 60 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/SchemaReplicationTest.java 6 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/common/ExternalChangeLogTest.java 1569 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/protocol/SynchronizationMsgTest.java 160 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/server/ReplicationServerTest.java 27 ●●●● patch | view | raw | blame | history
opends/resource/config/config.ldif
@@ -78,6 +78,7 @@
ds-cfg-global-aci: (target="ldap:///")(targetscope="base")(targetattr="objectClass||namingContexts||supportedAuthPasswordSchemes||supportedControl||supportedExtension||supportedFeatures||supportedLDAPVersion||supportedSASLMechanisms||vendorName||vendorVersion")(version 3.0; acl "User-Visible Root DSE Operational Attributes"; allow (read,search,compare) userdn="ldap:///anyone";)
ds-cfg-global-aci: (targetattr="createTimestamp||creatorsName||modifiersName||modifyTimestamp||entryDN||entryUUID||subschemaSubentry")(version 3.0; acl "User-Visible Operational Attributes"; allow (read,search,compare) userdn="ldap:///anyone";)
ds-cfg-global-aci: (target="ldap:///dc=replicationchanges")(targetattr="*")(version 3.0; acl "Replication backend access"; deny (all) userdn="ldap:///anyone";)
ds-cfg-global-aci: (target="ldap:///cn=changelog")(targetattr="*")(version 3.0; acl "External changelog access"; deny (all) userdn="ldap:///anyone";)
cn: Access Control Handler
ds-cfg-java-class: org.opends.server.authorization.dseecompat.AciHandler
ds-cfg-enabled: true
opends/src/messages/messages/protocol.properties
@@ -1411,4 +1411,7 @@
SEVERE_ERR_SNMP_CONNHANDLER_OPENDMK_JARFILES_NOT_OPERATIONAL_1507=The required \
 classes could not be loaded using jar file '%s'.  Verify that the jar file \
 is not corrupted
MILD_ERR_CANNOT_DECODE_CONTROL_VALUE_1508=Cannot decode the provided \
 control %s because an error occurred while attempting to \
 decode the control value:  %s
opends/src/messages/messages/replication.properties
@@ -365,4 +365,11 @@
SEVERE_ERR_COULD_NOT_START_REPLICATION_154=The Replication was not started \
 on base-dn %s : %s
MILD_ERR_ERROR_RETRIEVING_MONITOR_DATA_155=Error retrieving monitoring data: %s
SEVERE_ERR_BYTE_COUNT_156=The Server Handler byte count is not correct (Fixed)
SEVERE_ERR_EXCEPTION_LOCKING_RS_DOMAIN_156=Caught exception when locking \
 the replication server domain: %s
SEVERE_ERR_REPLICATION_PROTOCOL_MESSAGE_TYPE_157=Replication \
 protocol error. Bad message type. %s received, %s required
SEVERE_ERR_INVALID_COOKIE_FULL_RESYNC_REQUIRED_158=The provided cookie is \
 valid in the current context due to %s. Full resync is required
SEVERE_ERR_BYTE_COUNT_159=The Server Handler byte count is not correct (Fixed)
opends/src/server/org/opends/server/controls/EntryChangelogNotificationControl.java
New file
@@ -0,0 +1,173 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.controls;
import static org.opends.messages.ProtocolMessages.ERR_ECN_CANNOT_DECODE_VALUE;
import static org.opends.messages.ProtocolMessages.ERR_ECN_NO_CONTROL_VALUE;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import static org.opends.server.protocols.asn1.ASN1Constants.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.getExceptionMessage;
import java.io.IOException;
import org.opends.messages.Message;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.asn1.ASN1;
import org.opends.server.protocols.asn1.ASN1Reader;
import org.opends.server.protocols.asn1.ASN1Writer;
import org.opends.server.types.ByteString;
import org.opends.server.types.Control;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.ResultCode;
/**
 * This class implements the ECL cookie control.
 * It may be included in entries returned in response to a search or
 * persistent search operation.
 */
public class EntryChangelogNotificationControl
       extends Control
       {
  // The tracer object for the debug logger.
  private static final DebugTracer TRACER = getTracer();
  // The cookie value - payload of this control.
  private String cookie;
  /**
   * ControlDecoder implentation to decode this control from a ByteString.
   */
  private final static class Decoder
  implements ControlDecoder<EntryChangelogNotificationControl>
  {
    /**
     * {@inheritDoc}
     */
    public EntryChangelogNotificationControl decode(
        boolean isCritical, ByteString value) throws DirectoryException
        {
      if (value == null)
      {
        // FIXME:ECL Create error messages dedicated to this control
        Message message = ERR_ECN_NO_CONTROL_VALUE.get();
        throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message);
      }
      String cookie = null;
      ASN1Reader reader = ASN1.getReader(value);
      try
      {
        reader.readStartSequence();
        cookie = reader.readOctetStringAsString();
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
        Message message =
          ERR_ECN_CANNOT_DECODE_VALUE.get(getExceptionMessage(e));
        throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message, e);
      }
      return new EntryChangelogNotificationControl(isCritical, cookie);
        }
    public String getOID()
    {
      return OID_ECL_COOKIE_EXCHANGE_CONTROL;
    }
  }
  /**
   * The Control Decoder that can be used to decode this control.
   */
  public static final ControlDecoder<EntryChangelogNotificationControl>
  DECODER = new Decoder();
  /**
   * Creates a new entry change notification control with the provided
   * information.
   *
   * @param  isCritical  Indicates whether this control should be
   *                     considered critical in processing the
   *                     request.
   * @param  cookie      The provided cookie value.
   */
  public EntryChangelogNotificationControl(boolean isCritical,
      String cookie)
  {
    super(OID_ECL_COOKIE_EXCHANGE_CONTROL, isCritical);
    this.cookie = cookie;
  }
  /**
   * Writes this control's value to an ASN.1 writer. The value (if any)
   * must be written as an ASN1OctetString.
   *
   * @param writer The ASN.1 output stream to write to.
   * @throws IOException If a problem occurs while writing to the stream.
   */
  public void writeValue(ASN1Writer writer) throws IOException {
    writer.writeStartSequence(UNIVERSAL_OCTET_STRING_TYPE);
    writer.writeOctetString(cookie.toString());
    writer.writeEndSequence();
  }
  /**
   * Retrieves the change type for this entry change notification control.
   *
   * @return  The change type for this entry change notification control.
   */
  public String getCookie()
  {
    return cookie;
  }
  /**
   * Appends a string representation of this entry change notification control
   * to the provided buffer.
   *
   * @param  buffer  The buffer to which the information should be appended.
   */
  public void toString(StringBuilder buffer)
  {
    buffer.append("EntryChangelogNotificationControl(cookie=");
    buffer.append(cookie.toString());
    buffer.append(")");
  }
}
opends/src/server/org/opends/server/controls/ExternalChangelogRequestControl.java
New file
@@ -0,0 +1,170 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.controls;
import static org.opends.messages.ProtocolMessages.*;
import static org.opends.server.protocols.asn1.ASN1Constants.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.getExceptionMessage;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import java.io.IOException;
import org.opends.messages.Message;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.asn1.ASN1;
import org.opends.server.protocols.asn1.ASN1Reader;
import org.opends.server.protocols.asn1.ASN1Writer;
import org.opends.server.replication.common.MultiDomainServerState;
import org.opends.server.types.ByteString;
import org.opends.server.types.Control;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.ResultCode;
/**
 * This class implements the persistent search control defined in
 * draft-ietf-ldapext-psearch.  It makes it possible for clients to be notified
 * of changes to information in the Directory Server as they occur.
 */
public class ExternalChangelogRequestControl
       extends Control
{
  private static final DebugTracer TRACER = getTracer();
  private MultiDomainServerState cookie;
  /**
   * ControlDecoder implentation to decode this control from a ByteString.
   */
  private final static class Decoder
      implements ControlDecoder<ExternalChangelogRequestControl>
  {
    /**
     * {@inheritDoc}
     */
    public ExternalChangelogRequestControl decode(boolean isCritical,
        ByteString value)
    throws DirectoryException
    {
      if (value == null)
      {
        // FIXME:ECL In the request cookie, empty value is currently rejected.
        Message message = ERR_PSEARCH_NO_CONTROL_VALUE.get();
        throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message);
      }
      ASN1Reader reader = ASN1.getReader(value);
      MultiDomainServerState mdss;
      String mdssValue = null;
      try
      {
        mdssValue = reader.readOctetStringAsString();
        mdss = new MultiDomainServerState(mdssValue);
      }
      catch (Exception e)
      {
        try
        {
          mdssValue = value.toString();
          mdss = new MultiDomainServerState(mdssValue);
        }
        catch (Exception e2)
        {
          Message message =
            ERR_CANNOT_DECODE_CONTROL_VALUE.get(
                getOID() + " x=" + value.toHex() + " v="
                + mdssValue , getExceptionMessage(e).toString());
          throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message, e);
        }
      }
      return new ExternalChangelogRequestControl(isCritical, mdss);
    }
    public String getOID()
    {
      return OID_ECL_COOKIE_EXCHANGE_CONTROL;
    }
  }
  /**
   * The Control Decoder that can be used to decode this control.
   */
  public static final ControlDecoder<ExternalChangelogRequestControl> DECODER =
    new Decoder();
  /**
   * Create a new external change log request control to contain the cookie.
   * @param isCritical Specifies whether the control is critical.
   * @param cookie Specifies the cookie value.
   */
  public ExternalChangelogRequestControl(boolean isCritical,
      MultiDomainServerState cookie)
  {
    super(OID_ECL_COOKIE_EXCHANGE_CONTROL, isCritical);
    this.cookie = cookie;
  }
  /**
   * Returns the cookie value.
   * @return The cookie value.
   */
  public MultiDomainServerState getCookie()
  {
    return this.cookie;
  }
  /**
   * Dump a string representation of this object to the provided bufer.
   * @param buffer The provided buffer.
   */
  public void toString(StringBuilder buffer)
  {
    buffer.append("ExternalChangelogRequestControl(cookie=");
    this.cookie.toString(buffer);
    buffer.append(")");
  }
  /**
   * Writes this control's value to an ASN.1 writer. The value
   * (if any) must be written as an ASN1OctetString.
   *
   * @param writer The ASN.1 writer to use.
   * @throws IOException If a problem occurs while writing to the
   *                     stream.
   */
  protected void writeValue(ASN1Writer writer)
      throws IOException
  {
    writer.writeStartSequence(UNIVERSAL_OCTET_STRING_TYPE);
    writer.writeOctetString(this.cookie.toString());
    writer.writeEndSequence();
  }
}
opends/src/server/org/opends/server/core/PersistentSearch.java
@@ -22,7 +22,7 @@
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.core;
@@ -240,6 +240,15 @@
  }
  /**
   * Get the search operation associated with this persistent search.
   *
   * @return The search operation associated with this persistent search.
   */
  public SearchOperation getSearchOperation()
  {
    return searchOperation;
  }
  /**
   * Notifies the persistent searches that an entry has been added.
opends/src/server/org/opends/server/loggers/debug/DebugLogger.java
@@ -453,7 +453,7 @@
   */
  public static boolean debugEnabled()
  {
    return enabled;
    return true;
  }
  /**
opends/src/server/org/opends/server/replication/common/ExternalChangeLogSession.java
New file
@@ -0,0 +1,52 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.common;
import org.opends.server.replication.protocol.ECLUpdateMsg;
import org.opends.server.types.DirectoryException;
/**
 * This interface defines a session used to search the external changelog
 * in the Directory Server.
 */
public interface ExternalChangeLogSession
{
  /**
   * Returns the next message available for the ECL (blocking).
   * @return the next available message from the ECL.
   * @throws DirectoryException When an error occurs.
   */
  public abstract ECLUpdateMsg getNextUpdate()
  throws DirectoryException;
  /**
   * Closes the session.
   * @throws DirectoryException when needed.
   */
  public abstract void close()
  throws DirectoryException;
}
opends/src/server/org/opends/server/replication/common/MultiDomainServerState.java
New file
@@ -0,0 +1,215 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.common;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import java.util.Iterator;
import java.util.TreeMap;
import org.opends.server.loggers.debug.DebugTracer;
/**
 * This object is used to store a list of ServerState object, one by
 * replication domain. Globally, it is the generalization of ServerState
 * (that applies to one domain) to a list of domains.
 */
public class MultiDomainServerState implements Iterable<String>
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();
  /**
   * The list of (domain service id, ServerState).
   */
  private TreeMap<String, ServerState> list;
  /**
   * Creates a new empty object.
   */
  public MultiDomainServerState()
  {
    list = new TreeMap<String, ServerState>();
  }
  /**
   * Empty the object..
   * After this call the object will be in the same state as if it
   * was just created.
   */
  public void clear()
  {
    synchronized (this)
    {
      list.clear();
    }
  }
  /**
   * Update the ServerState of the provided serviceId with the
   * replication change number provided.
   *
   * @param serviceId    The provided serviceId.
   * @param changeNumber The provided ChangeNumber.
   *
   * @return a boolean indicating if the update was meaningful.
   */
  public boolean update(String serviceId, ChangeNumber changeNumber)
  {
    if (changeNumber == null)
      return false;
    synchronized(this)
    {
      Short serverId =  changeNumber.getServerId();
      ServerState oldServerState = list.get(serviceId);
      if (oldServerState == null)
        oldServerState = new ServerState();
      if (changeNumber.newer(oldServerState.getMaxChangeNumber(serverId)))
      {
        oldServerState.update(changeNumber);
        list.put(serviceId,oldServerState);
        return true;
      }
      else
      {
        return false;
      }
    }
  }
  /**
   * Create an object from a string representation.
   * @param mdss The provided string representation of the state.
   */
  public MultiDomainServerState(String mdss)
  {
    list = new TreeMap<String, ServerState>();
    if ((mdss != null) &&
        (mdss.length()>0))
    {
      String[] domains = mdss.split(";");
      for (String domain : domains)
      {
        String[] fields = domain.split(":");
        String name = fields[0];
        ServerState ss = new ServerState();
        if (fields.length>1)
        {
          String strState = fields[1].trim();
          String[] strCN = strState.split(" ");
          for (String sr : strCN)
          {
            ChangeNumber fromChangeNumber = new ChangeNumber(sr);
            ss.update(fromChangeNumber);
          }
        }
        this.list.put(name, ss);
      }
    }
  }
  /**
   * Returns a string representation of this object.
   * @return The string representation.
   */
  public String toString()
  {
    String res = "";
    if ((list != null) && (!list.isEmpty()))
    {
      for (String serviceId  : list.keySet())
      {
        ServerState ss = list.get(serviceId);
        res += serviceId + ":" + ss.toString();
        res += ";";
      }
    }
    return res;
  }
  /**
   * Dump a string representation in the provided buffer.
   * @param buffer The provided buffer.
   */
  public void toString(StringBuilder buffer)
  {
    buffer.append(this.toString());
  }
  /**
   * Tests if the state is empty.
   *
   * @return True if the state is empty.
   */
  public boolean isEmpty()
  {
    return list.isEmpty();
  }
  /**
   * {@inheritDoc}
   */
  public Iterator<String> iterator()
  {
    return list.keySet().iterator();
  }
  /**
   * Test if this object equals the provided other object.
   * @param other The other object with which we want to test equality.
   * @return      Returns True if this equals other, else return false.
   */
  public boolean equalsTo(MultiDomainServerState other)
  {
    return ((this.cover(other)) && (other.cover(this)));
  }
  /**
   * Test if this object covers the provided covered object.
   * @param  covered The provided object.
   * @return true when this covers the provided object.
   */
  public boolean cover(MultiDomainServerState covered)
  {
    for (String serviceId : covered.list.keySet())
    {
      ServerState state = list.get(serviceId);
      ServerState coveredState = covered.list.get(serviceId);
      if ((coveredState == null) || (!state.cover(coveredState)))
      {
        return false;
      }
    }
    return true;
  }
}
opends/src/server/org/opends/server/replication/common/ServerState.java
@@ -122,6 +122,8 @@
  /**
   * Get the length of the next String encoded in the in byte array.
   * This method is used to cut the different parts (server ids, change number)
   * of a server state.
   *
   * @param in the byte array where to calculate the string.
   * @param pos the position whre to start from in the byte array.
@@ -135,7 +137,7 @@
    while (in[offset++] != 0)
    {
      if (offset >= in.length)
        throw new DataFormatException("byte[] is not a valid modify msg");
        throw new DataFormatException("byte[] is not a valid server state");
      length++;
    }
    return length;
@@ -251,7 +253,6 @@
   * Return the text representation of ServerState.
   * @return the text representation of ServerState
   */
  @Override
  public String toString()
  {
    StringBuilder buffer = new StringBuilder();
@@ -261,9 +262,11 @@
      for (Short key  : list.keySet())
      {
        ChangeNumber change = list.get(key);
        buffer.append(change.toString());
        buffer.append(" ");
        buffer.append(change.toStringUI());
      }
      if (!list.isEmpty())
        buffer.deleteCharAt(buffer.length()-1);
    }
    return buffer.toString();
opends/src/server/org/opends/server/replication/plugin/MultimasterReplication.java
@@ -817,4 +817,23 @@
  {
    return domains.size();
  }
  /**
   * Gets the baseDn of the domains that have a private backend.
   * @return The private baseDN.
   */
  public static ArrayList<String> getPrivateDomains()
  {
    ArrayList<String> privateDNs = new ArrayList<String>();
    for (LDAPReplicationDomain domain : domains.values())
    {
      Backend b = domain.getBackend();
      if (b != null)
        if (b.isPrivateBackend())
          privateDNs.add(domain.getBaseDN().toNormalizedString());
    }
    return privateDNs;
  }
}
opends/src/server/org/opends/server/replication/protocol/ECLUpdateMsg.java
New file
@@ -0,0 +1,189 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.protocol;
import java.io.UnsupportedEncodingException;
import java.util.zip.DataFormatException;
import org.opends.server.replication.common.MultiDomainServerState;
/**
 * Container for the ECL information sent from the ReplicationServer
 * to the client part (either broker over the protocol, or ECLSession).
 */
public class ECLUpdateMsg extends ReplicationMsg
{
  // The replication change returned.
  private final LDAPUpdateMsg updateMsg;
  // The serviceId (baseDN) of the domain to which applies the change.
  private final String serviceId;
  // The value of the cookie updated with the current change
  private MultiDomainServerState cookie;
  /**
   * Creates a new message.
   * @param update    The provided update.
   * @param cookie    The provided cookie value
   * @param serviceId The provided serviceId.
   */
  public ECLUpdateMsg(LDAPUpdateMsg update, MultiDomainServerState cookie,
      String serviceId)
  {
    this.cookie = cookie;
    this.serviceId = serviceId;
    this.updateMsg = update;
  }
  /**
   * Creates a new message from its encoded form.
   *
   * @param in The byte array containing the encoded form of the message.
   * @throws DataFormatException If the byte array does not contain
   *         a valid encoded form of the message.
   * @throws UnsupportedEncodingException when it occurs.
   * @throws NotSupportedOldVersionPDUException when it occurs.
   */
  public ECLUpdateMsg(byte[] in)
   throws DataFormatException,
          UnsupportedEncodingException,
          NotSupportedOldVersionPDUException
  {
    try
    {
      if (in[0] != MSG_TYPE_ECL_UPDATE)
      {
        throw new DataFormatException("byte[] is not a valid " +
            this.getClass().getCanonicalName());
      }
      int pos = 1;
      // Decode the cookie
      int length = getNextLength(in, pos);
      String cookieStr = new String(in, pos, length, "UTF-8");
      this.cookie = new MultiDomainServerState(cookieStr);
      pos += length + 1;
      // Decode the serviceId
      length = getNextLength(in, pos);
      this.serviceId = new String(in, pos, length, "UTF-8");
      pos += length + 1;
      // Decode the msg
      /* Read the mods : all the remaining bytes but the terminating 0 */
      length = in.length - pos - 1;
      byte[] encodedMsg = new byte[length];
      System.arraycopy(in, pos, encodedMsg, 0, length);
      ReplicationMsg rmsg = ReplicationMsg.generateMsg(encodedMsg);
      this.updateMsg = (LDAPUpdateMsg)rmsg;
    }
    catch (UnsupportedEncodingException e)
    {
      throw new DataFormatException("UTF-8 is not supported by this jvm.");
    }
  }
  /**
   * Getter for the cookie value.
   * @return The cookie value.
   */
  public MultiDomainServerState getCookie()
  {
    return cookie;
  }
  /**
   * Setter for the cookie value.
   * @param cookie The provided cookie value.
   */
  public void setCookie(MultiDomainServerState cookie)
  {
    this.cookie = cookie;
  }
  /**
   * Getter for the serviceId.
   * @return The serviceId.
   */
  public  String getServiceId()
  {
    return serviceId;
  }
  /**
   * Getter for the message.
   * @return The included replication message.
   */
  public  UpdateMsg getUpdateMsg()
  {
    return updateMsg;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public String toString()
  {
    return "ECLUpdateMsg:[" +
    "updateMsg: " + updateMsg +
    "cookie: " + cookie +
    "serviceId: " + serviceId + "]";
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public byte[] getBytes() throws UnsupportedEncodingException
  {
    byte[] byteCookie    = String.valueOf(cookie).getBytes("UTF-8");
    byte[] byteServiceId = String.valueOf(serviceId).getBytes("UTF-8");
    byte[] byteUpdateMsg = updateMsg.getBytes();
    int length = 1 + byteCookie.length +
                 1 + byteServiceId.length +
                 1 + byteUpdateMsg.length + 1;
    byte[] resultByteArray = new byte[length];
    /* Encode type */
    resultByteArray[0] = MSG_TYPE_ECL_UPDATE;
    int pos = 1;
    // Encode cookie
    pos = addByteArray(byteCookie, resultByteArray, pos);
    // Encode serviceid
    pos = addByteArray(byteServiceId, resultByteArray, pos);
    // Encode msg
    pos = addByteArray(byteUpdateMsg, resultByteArray, pos);
    return resultByteArray;
  }
}
opends/src/server/org/opends/server/replication/protocol/HeartbeatMonitor.java
@@ -22,7 +22,7 @@
 * CDDL HEADER END
 *
 *
 *      Copyright 2007-2008 Sun Microsystems, Inc.
 *      Copyright 2007-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.protocol;
@@ -103,9 +103,8 @@
    boolean gotOneFailure = false;
    if (debugEnabled())
    {
      TRACER.debugInfo("Heartbeat monitor is starting, expected interval is " +
                heartbeatInterval +
          stackTraceToSingleLineString(new Exception()));
      TRACER.debugInfo(this + " is starting, expected interval is " +
                heartbeatInterval);
    }
    try
    {
opends/src/server/org/opends/server/replication/protocol/ReplServerStartMsg.java
@@ -22,7 +22,7 @@
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.protocol;
@@ -167,9 +167,13 @@
        pos += length + 1;
      }
      /*
       * read the ServerState
       */
      // Read the ServerState
      // Caution: ServerState MUST be the last field. Because ServerState can
      // contain null character (string termination of sererid string ..) it
      // cannot be decoded using getNextLength() like the other fields. The
      // only way is to rely on the end of the input buffer : and that forces
      // the ServerState to be the last. This should be changed and we want to
      // have more than one ServerState field.
      serverState = new ServerState(in, pos, in.length - 1);
    } catch (UnsupportedEncodingException e)
    {
opends/src/server/org/opends/server/replication/protocol/ReplicationMsg.java
@@ -70,6 +70,9 @@
  static final byte MSG_TYPE_START_SESSION = 27;
  static final byte MSG_TYPE_CHANGE_STATUS = 28;
  static final byte MSG_TYPE_GENERIC_UPDATE = 29;
  static final byte MSG_TYPE_START_ECL = 30;
  static final byte MSG_TYPE_START_ECL_SESSION = 31;
  static final byte MSG_TYPE_ECL_UPDATE = 32;
  // Adding a new type of message here probably requires to
  // change accordingly generateMsg method below
@@ -215,6 +218,15 @@
      case MSG_TYPE_GENERIC_UPDATE:
        msg = new UpdateMsg(buffer);
      break;
      case MSG_TYPE_START_ECL:
        msg = new ServerStartECLMsg(buffer);
      break;
      case MSG_TYPE_START_ECL_SESSION:
        msg = new StartECLSessionMsg(buffer);
      break;
      case MSG_TYPE_ECL_UPDATE:
        msg = new ECLUpdateMsg(buffer);
      break;
      default:
        throw new DataFormatException("received message with unknown type");
    }
opends/src/server/org/opends/server/replication/protocol/ServerStartECLMsg.java
New file
@@ -0,0 +1,377 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.protocol;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.zip.DataFormatException;
import org.opends.server.replication.common.ServerState;
/**
 * This message is used by LDAP server when they first connect.
 * to a replication server to let them know who they are and what is their state
 * (their RUV)
 */
public class ServerStartECLMsg extends StartMsg
{
  private String serverURL;
  private int maxReceiveQueue;
  private int maxSendQueue;
  private int maxReceiveDelay;
  private int maxSendDelay;
  private int windowSize;
  private ServerState serverState = null;
  /**
   * The time in milliseconds between heartbeats from the replication
   * server.  Zero means heartbeats are off.
   */
  private long heartbeatInterval = 0;
  /**
   * Whether to continue using SSL to encrypt messages after the start
   * messages have been exchanged.
   */
  private boolean sslEncryption;
  /**
   * Creates a new ServerStartMsg. This message is to be sent by an LDAP
   * Server after being connected to a replication server for a given
   * replication domain.
   *
   * @param baseDn   The base DN.
   * @param maxReceiveDelay The max receive delay for this server.
   * @param maxReceiveQueue The max receive Queue for this server.
   * @param maxSendDelay The max Send Delay from this server.
   * @param maxSendQueue The max send Queue from this server.
   * @param windowSize   The window size used by this server.
   * @param heartbeatInterval The requested heartbeat interval.
   * @param serverState  The state of this server.
   * @param protocolVersion The replication protocol version of the creator.
   * @param generationId The generationId for this server.
   * @param sslEncryption Whether to continue using SSL to encrypt messages
   *                      after the start messages have been exchanged.
   * @param groupId The group id of the DS for this DN
   */
  public ServerStartECLMsg(String baseDn, int maxReceiveDelay,
                            int maxReceiveQueue, int maxSendDelay,
                            int maxSendQueue, int windowSize,
                            long heartbeatInterval,
                            ServerState serverState,
                            short protocolVersion,
                            long generationId,
                            boolean sslEncryption,
                            byte groupId)
  {
    super(protocolVersion, generationId);
    this.maxReceiveDelay = maxReceiveDelay;
    this.maxReceiveQueue = maxReceiveQueue;
    this.maxSendDelay = maxSendDelay;
    this.maxSendQueue = maxSendQueue;
    this.windowSize = windowSize;
    this.heartbeatInterval = heartbeatInterval;
    this.sslEncryption = sslEncryption;
    this.serverState = serverState;
    this.groupId = groupId;
    try
    {
      /* TODO : find a better way to get the server URL */
      this.serverURL = InetAddress.getLocalHost().getHostName();
    } catch (UnknownHostException e)
    {
      this.serverURL = "Unknown host";
    }
  }
  /**
   * Creates a new ServerStartMsg from its encoded form.
   *
   * @param in The byte array containing the encoded form of the
   *           ServerStartMsg.
   * @throws DataFormatException If the byte array does not contain a valid
   *                             encoded form of the ServerStartMsg.
   */
  public ServerStartECLMsg(byte[] in) throws DataFormatException
  {
    byte[] allowedPduTypes = new byte[1];
    allowedPduTypes[0] = MSG_TYPE_START_ECL;
    headerLength = decodeHeader(allowedPduTypes, in);
    try
    {
      /* first bytes are the header */
      int pos = headerLength;
      /*
       * read the ServerURL
       */
      int length = getNextLength(in, pos);
      serverURL = new String(in, pos, length, "UTF-8");
      pos += length +1;
      /*
       * read the maxReceiveDelay
       */
      length = getNextLength(in, pos);
      maxReceiveDelay = Integer.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length +1;
      /*
       * read the maxReceiveQueue
       */
      length = getNextLength(in, pos);
      maxReceiveQueue = Integer.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length +1;
      /*
       * read the maxSendDelay
       */
      length = getNextLength(in, pos);
      maxSendDelay = Integer.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length +1;
      /*
       * read the maxSendQueue
       */
      length = getNextLength(in, pos);
      maxSendQueue = Integer.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length +1;
      /*
       * read the windowSize
       */
      length = getNextLength(in, pos);
      windowSize = Integer.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length +1;
      /*
       * read the heartbeatInterval
       */
      length = getNextLength(in, pos);
      heartbeatInterval = Integer.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length +1;
      /*
       * read the sslEncryption setting
       */
      length = getNextLength(in, pos);
      sslEncryption = Boolean.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length +1;
      // Read the ServerState
      // Caution: ServerState MUST be the last field. Because ServerState can
      // contain null character (string termination of sererid string ..) it
      // cannot be decoded using getNextLength() like the other fields. The
      // only way is to rely on the end of the input buffer : and that forces
      // the ServerState to be the last. This should be changed and we want to
      // have more than one ServerState field.
      serverState = new ServerState(in, pos, in.length - 1);
    } catch (UnsupportedEncodingException e)
    {
      throw new DataFormatException("UTF-8 is not supported by this jvm.");
    }
  }
  /**
   * get the Server URL from the message.
   * @return the server URL
   */
  public String getServerURL()
  {
    return serverURL;
  }
  /**
   * Get the maxReceiveDelay.
   * @return Returns the maxReceiveDelay.
   */
  public int getMaxReceiveDelay()
  {
    return maxReceiveDelay;
  }
  /**
   * Get the maxReceiveQueue.
   * @return Returns the maxReceiveQueue.
   */
  public int getMaxReceiveQueue()
  {
    return maxReceiveQueue;
  }
  /**
   * Get the maxSendDelay.
   * @return Returns the maxSendDelay.
   */
  public int getMaxSendDelay()
  {
    return maxSendDelay;
  }
  /**
   * Get the maxSendQueue.
   * @return Returns the maxSendQueue.
   */
  public int getMaxSendQueue()
  {
    return maxSendQueue;
  }
  /**
   * Get the ServerState.
   * @return The ServerState.
   */
  public ServerState getServerState()
  {
    return serverState;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public byte[] getBytes()
  {
    try {
      byte[] byteServerUrl = serverURL.getBytes("UTF-8");
      byte[] byteMaxRecvDelay =
                     String.valueOf(maxReceiveDelay).getBytes("UTF-8");
      byte[] byteMaxRecvQueue =
                     String.valueOf(maxReceiveQueue).getBytes("UTF-8");
      byte[] byteMaxSendDelay =
                     String.valueOf(maxSendDelay).getBytes("UTF-8");
      byte[] byteMaxSendQueue =
                     String.valueOf(maxSendQueue).getBytes("UTF-8");
      byte[] byteWindowSize =
                     String.valueOf(windowSize).getBytes("UTF-8");
      byte[] byteHeartbeatInterval =
                     String.valueOf(heartbeatInterval).getBytes("UTF-8");
      byte[] byteSSLEncryption =
                     String.valueOf(sslEncryption).getBytes("UTF-8");
      byte[] byteServerState = serverState.getBytes();
      int length = byteServerUrl.length + 1 +
                   byteMaxRecvDelay.length + 1 +
                   byteMaxRecvQueue.length + 1 +
                   byteMaxSendDelay.length + 1 +
                   byteMaxSendQueue.length + 1 +
                   byteWindowSize.length + 1 +
                   byteHeartbeatInterval.length + 1 +
                   byteSSLEncryption.length + 1 +
                   byteServerState.length + 1;
      /* encode the header in a byte[] large enough to also contain the mods */
      byte resultByteArray[] = encodeHeader(MSG_TYPE_START_ECL, length);
      int pos = headerLength;
      pos = addByteArray(byteServerUrl, resultByteArray, pos);
      pos = addByteArray(byteMaxRecvDelay, resultByteArray, pos);
      pos = addByteArray(byteMaxRecvQueue, resultByteArray, pos);
      pos = addByteArray(byteMaxSendDelay, resultByteArray, pos);
      pos = addByteArray(byteMaxSendQueue, resultByteArray, pos);
      pos = addByteArray(byteWindowSize, resultByteArray, pos);
      pos = addByteArray(byteHeartbeatInterval, resultByteArray, pos);
      pos = addByteArray(byteSSLEncryption, resultByteArray, pos);
      pos = addByteArray(byteServerState, resultByteArray, pos);
      return resultByteArray;
    }
    catch (UnsupportedEncodingException e)
    {
      return null;
    }
  }
  /**
   * Get the window size for the ldap server that created the message.
   *
   * @return The window size for the ldap server that created the message.
   */
  public int getWindowSize()
  {
    return windowSize;
  }
  /**
   * Get the heartbeat interval requested by the ldap server that created the
   * message.
   *
   * @return The heartbeat interval requested by the ldap server that created
   * the message.
   */
  public long getHeartbeatInterval()
  {
    return heartbeatInterval;
  }
  /**
   * Get the SSL encryption value for the ldap server that created the
   * message.
   *
   * @return The SSL encryption value for the ldap server that created the
   *         message.
   */
  public boolean getSSLEncryption()
  {
    return sslEncryption;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public String toString()
  {
    return this.getClass().getCanonicalName() + " content: " +
      "\nprotocolVersion: " + protocolVersion +
      "\ngenerationId: " + generationId +
      "\ngroupId: " + groupId +
      "\nheartbeatInterval: " + heartbeatInterval +
      "\nmaxReceiveDelay: " + maxReceiveDelay +
      "\nmaxReceiveQueue: " + maxReceiveQueue +
      "\nmaxSendDelay: " + maxSendDelay +
      "\nmaxSendQueue: " + maxSendQueue +
      "\nserverState: " + serverState +
      "\nserverURL: " + serverURL +
      "\nsslEncryption: " + sslEncryption +
      "\nwindowSize: " + windowSize;
  }
  }
opends/src/server/org/opends/server/replication/protocol/ServerStartMsg.java
@@ -22,7 +22,7 @@
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.protocol;
@@ -208,9 +208,13 @@
      sslEncryption = Boolean.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length +1;
      /*
       * read the ServerState
       */
      // Read the ServerState
      // Caution: ServerState MUST be the last field. Because ServerState can
      // contain null character (string termination of sererid string ..) it
      // cannot be decoded using getNextLength() like the other fields. The
      // only way is to rely on the end of the input buffer : and that forces
      // the ServerState to be the last. This should be changed and we want to
      // have more than one ServerState field.
      serverState = new ServerState(in, pos, in.length - 1);
    } catch (UnsupportedEncodingException e)
opends/src/server/org/opends/server/replication/protocol/StartECLSessionMsg.java
New file
@@ -0,0 +1,432 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.protocol;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.zip.DataFormatException;
import org.opends.server.replication.common.ChangeNumber;
/**
 * This class specifies the parameters of a search request on the ECL.
 * It is used as an interface between the requestor (plugin part)
 * - either as an han
 */
public class StartECLSessionMsg extends ReplicationMsg
{
  /**
   * This specifies that the ECL is requested from a provided cookie value
   * defined as a MultiDomainServerState.
   */
  public final static short REQUEST_TYPE_FROM_COOKIE = 0;
  /**
   * This specifies that the ECL is requested from a provided interval
   * of change numbers (as defined by draft-good-ldap-changelog [CHANGELOG]
   * and NOT replication change numbers).
   * TODO: not yet implemented
   */
  public final static short REQUEST_TYPE_FROM_DRAFT_CHANGE_NUMBER = 1;
  /**
   * This specifies that the ECL is requested ony for the entry that have
   * a repl change number matching the provided one.
   * TODO: not yet implemented
   */
  public final static short REQUEST_TYPE_EQUALS_REPL_CHANGE_NUMBER = 2;
  /**
   * This specifies that the request on the ECL is a PERSISTENT search
   * with changesOnly = false.
   */
  public final static short PERSISTENT = 0;
  /**
   * This specifies that the request on the ECL is a NOT a PERSISTENT search.
   */
  public final static short NON_PERSISTENT = 1;
  /**
   * This specifies that the request on the ECL is a PERSISTENT search
   * with changesOnly = false.
   */
  public final static short PERSISTENT_CHANGES_ONLY = 2;;
  // The type of request as defined by REQUEST_TYPE_...
  private short  eclRequestType;
  // When eclRequestType = FROM_COOKIE,
  // specifies the provided cookie value.
  private String crossDomainServerState = "";
  // When eclRequestType = FROM_CHANGE_NUMBER,
  // specifies the provided change number first and last - [CHANGELOG]
  private int    firstDraftChangeNumber = -1;
  private int    lastDraftChangeNumber = -1;
  // When eclRequestType = EQUALS_REPL_CHANGE_NUMBER,
  // specifies the provided replication change number.
  private ChangeNumber changeNumber;
  // Specifies whether the search is persistent and changesOnly
  private short  isPersistent;
  // A string helping debuging and tracing the client operation related when
  // processing, on the RS side, a request on the ECL.
  private String operationId = "";
  // Excluded domains
  private ArrayList<String> excludedServiceIDs = new ArrayList<String>();
  /**
   * Creates a new StartSessionMsg message from its encoded form.
   *
   * @param in The byte array containing the encoded form of the message.
   * @throws java.util.zip.DataFormatException If the byte array does not
   * contain a valid encoded form of the message.
   */
  public StartECLSessionMsg(byte[] in) throws DataFormatException
  {
    /*
     * The message is stored in the form:
     * <message type><status><assured flag><assured mode><safe data level>
     * <list of referrals urls>
     * (each referral url terminates with 0)
     */
    try
    {
      /* first bytes are the header */
      int pos = 0;
      /* first byte is the type */
      if (in.length < 1 || in[pos++] != MSG_TYPE_START_ECL_SESSION)
      {
        throw new DataFormatException(
          "Input is not a valid " + this.getClass().getCanonicalName());
      }
      // start mode
      int length = getNextLength(in, pos);
      eclRequestType = Short.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length +1;
      // sequenceNumber
      length = getNextLength(in, pos);
      firstDraftChangeNumber =
        Integer.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length +1;
      // stopSequenceNumber
      length = getNextLength(in, pos);
      lastDraftChangeNumber =
        Integer.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length +1;
      // replication changeNumber
      length = getNextLength(in, pos);
      String changenumberStr = new String(in, pos, length, "UTF-8");
      pos += length + 1;
      changeNumber = new ChangeNumber(changenumberStr);
      // persistentSearch mode
      length = getNextLength(in, pos);
      isPersistent = Short.valueOf(new String(in, pos, length, "UTF-8"));
      pos += length + 1;
      // generalized state
      length = getNextLength(in, pos);
      crossDomainServerState = new String(in, pos, length, "UTF-8");
      pos += length + 1;
      // operation id
      length = getNextLength(in, pos);
      operationId = new String(in, pos, length, "UTF-8");
      pos += length + 1;
      // excluded DN
      length = getNextLength(in, pos);
      String excludedDNsString = new String(in, pos, length, "UTF-8");
      if (excludedDNsString.length()>0)
      {
        String[] excludedDNsStr = excludedDNsString.split(";");
        for (String excludedDNStr : excludedDNsStr)
        {
          this.excludedServiceIDs.add(excludedDNStr);
        }
      }
      pos += length + 1;
    } catch (UnsupportedEncodingException e)
    {
      throw new DataFormatException("UTF-8 is not supported by this jvm.");
    } catch (IllegalArgumentException e)
    {
      throw new DataFormatException(e.getMessage());
    }
  }
  /**
   * Creates a new StartSessionMsg message with the given required parameters.
   */
  public StartECLSessionMsg()
  {
    changeNumber = new ChangeNumber((short)0,0,(short)0);
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public byte[] getBytes()
  {
    String excludedSIDsString = new String();
    for (String excludedServiceID : excludedServiceIDs)
    {
      excludedSIDsString = excludedSIDsString.concat(excludedServiceID+";");
    }
    try
    {
      byte[] byteMode =
        Short.toString(eclRequestType).getBytes("UTF-8");
      byte[] byteSequenceNumber =
        String.valueOf(firstDraftChangeNumber).getBytes("UTF-8");
      byte[] byteStopSequenceNumber =
        String.valueOf(lastDraftChangeNumber).getBytes("UTF-8");
      byte[] byteChangeNumber =
        changeNumber.toString().getBytes("UTF-8");
      byte[] bytePsearch =
        Short.toString(isPersistent).getBytes();
      byte[] byteGeneralizedState =
        String.valueOf(crossDomainServerState).getBytes("UTF-8");
      byte[] byteOperationId =
        String.valueOf(operationId).getBytes("UTF-8");
      byte[] byteExcludedDNs =
        String.valueOf(excludedSIDsString).getBytes("UTF-8");
      int length =
        byteMode.length + 1 +
        byteSequenceNumber.length + 1 +
        byteStopSequenceNumber.length + 1 +
        byteChangeNumber.length + 1 +
        bytePsearch.length + 1 +
        byteGeneralizedState.length + 1 +
        byteOperationId.length + 1 +
        byteExcludedDNs.length + 1 +
        1;
      byte[] resultByteArray = new byte[length];
      int pos = 0;
      resultByteArray[pos++] = MSG_TYPE_START_ECL_SESSION;
      pos = addByteArray(byteMode, resultByteArray, pos);
      pos = addByteArray(byteSequenceNumber, resultByteArray, pos);
      pos = addByteArray(byteStopSequenceNumber, resultByteArray, pos);
      pos = addByteArray(byteChangeNumber, resultByteArray, pos);
      pos = addByteArray(bytePsearch, resultByteArray, pos);
      pos = addByteArray(byteGeneralizedState, resultByteArray, pos);
      pos = addByteArray(byteOperationId, resultByteArray, pos);
      pos = addByteArray(byteExcludedDNs, resultByteArray, pos);
      return resultByteArray;
    } catch (IOException e)
    {
      // never happens
      return null;
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public String toString()
  {
    return (this.getClass().getCanonicalName() + " [" +
            " requestType="+ eclRequestType +
            " persistentSearch="       + isPersistent +
            " changeNumber="           + changeNumber +
            " firstDraftChangeNumber=" + firstDraftChangeNumber +
            " lastDraftChangeNumber="  + lastDraftChangeNumber +
            " generalizedState="       + crossDomainServerState +
            " operationId="            + operationId +
            " excludedDNs="            + excludedServiceIDs + "]");
  }
  /**
   * Getter on the changer number start.
   * @return the changer number start.
   */
  public int getFirstDraftChangeNumber()
  {
    return firstDraftChangeNumber;
  }
  /**
   * Getter on the changer number stop.
   * @return the change number stop.
   */
  public int getLastDraftChangeNumber()
  {
    return lastDraftChangeNumber;
  }
  /**
   * Setter on the first changer number (as defined by [CHANGELOG]).
   * @param firstDraftChangeNumber the provided first change number.
   */
  public void setFirstDraftChangeNumber(int firstDraftChangeNumber)
  {
    this.firstDraftChangeNumber = firstDraftChangeNumber;
  }
  /**
   * Setter on the last changer number (as defined by [CHANGELOG]).
   * @param lastDraftChangeNumber the provided last change number.
   */
  public void setLastDraftChangeNumber(int lastDraftChangeNumber)
  {
    this.lastDraftChangeNumber = lastDraftChangeNumber;
  }
  /**
   * Getter on the replication change number.
   * @return the replication change number.
   */
  public ChangeNumber getChangeNumber()
  {
    return changeNumber;
  }
  /**
   * Setter on the replication change number.
   * @param changeNumber the provided replication change number.
   */
  public void setChangeNumber(ChangeNumber changeNumber)
  {
    this.changeNumber = changeNumber;
  }
  /**
   * Getter on the type of request.
   * @return the type of request.
   */
  public short getECLRequestType()
  {
    return eclRequestType;
  }
  /**
   * Setter on the type of request.
   * @param eclRequestType the provided type of request.
   */
  public void setECLRequestType(short eclRequestType)
  {
    this.eclRequestType = eclRequestType;
  }
  /**
   * Getter on the persistent property of the search request on the ECL.
   * @return the persistent property.
   */
  public short isPersistent()
  {
    return this.isPersistent;
  }
  /**
   * Setter on the persistent property of the search request on the ECL.
   * @param isPersistent the provided persistent property.
   */
  public void setPersistent(short isPersistent)
  {
    this.isPersistent = isPersistent;
  }
  /**
   * Getter of the cross domain server state.
   * @return the cross domain server state.
   */
  public String getCrossDomainServerState()
  {
    return this.crossDomainServerState;
  }
  /**
   * Setter of the cross domain server state.
   * @param crossDomainServerState the provided cross domain server state.
   */
  public void setCrossDomainServerState(String crossDomainServerState)
  {
    this.crossDomainServerState = crossDomainServerState;
  }
  /**
   * Setter of the operation id.
   * @param operationId The provided opration id.
   */
  public void setOperationId(String operationId)
  {
    this.operationId = operationId;
  }
  /**
   * Getter on the operation id.
   * @return the operation id.
   */
  public String getOperationId()
  {
    return this.operationId;
  }
  /**
   * Getter on the list of excluded ServiceIDs.
   * @return the list of excluded ServiceIDs.
   */
  public ArrayList<String> getExcludedServiceIDs()
  {
    return this.excludedServiceIDs;
  }
  /**
   * Setter on the list of excluded ServiceIDs.
   * @param excludedServiceIDs the provided list of excluded ServiceIDs.
   */
  public void setExcludedDNs(ArrayList<String> excludedServiceIDs)
  {
    this.excludedServiceIDs = excludedServiceIDs;
  }
}
opends/src/server/org/opends/server/replication/server/DataServerHandler.java
New file
@@ -0,0 +1,724 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.server;
import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.replication.common.StatusMachine.*;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.zip.DataFormatException;
import org.opends.messages.Message;
import org.opends.server.replication.common.AssuredMode;
import org.opends.server.replication.common.DSInfo;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.common.ServerStatus;
import org.opends.server.replication.common.StatusMachine;
import org.opends.server.replication.common.StatusMachineEvent;
import org.opends.server.replication.protocol.*;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.Attributes;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.ResultCode;
/**
 * This class defines a server handler, which handles all interaction with a
 * peer server (RS or DS).
 */
public class DataServerHandler extends ServerHandler
{
  // Status of this DS (only used if this server handler represents a DS)
  private ServerStatus status = ServerStatus.INVALID_STATUS;
  // Referrals URLs this DS is exporting
  private List<String> refUrls = new ArrayList<String>();
  // Assured replication enabled on DS or not
  private boolean assuredFlag = false;
  // DS assured mode (relevant if assured replication enabled)
  private AssuredMode assuredMode = AssuredMode.SAFE_DATA_MODE;
  // DS safe data level (relevant if assured mode is safe data)
  private byte safeDataLevel = (byte) -1;
  /**
   * Creates a new data server handler.
   * @param session The session opened with the remote data server.
   * @param queueSize The queue size.
   * @param replicationServerURL The URL of the hosting RS.
   * @param replicationServerId The serverID of the hosting RS.
   * @param replicationServer The hosting RS.
   * @param rcvWindowSize The receiving window size.
   */
  public DataServerHandler(
      ProtocolSession session,
      int queueSize,
      String replicationServerURL,
      short replicationServerId,
      ReplicationServer replicationServer,
      int rcvWindowSize)
  {
    super(session, queueSize, replicationServerURL, replicationServerId,
        replicationServer, rcvWindowSize);
  }
  /**
   * Order the peer DS server to change his status or close the connection
   * according to the requested new generation id.
   * @param newGenId The new generation id to take into account
   * @throws IOException If IO error occurred.
   */
  public void changeStatusForResetGenId(long newGenId)
  throws IOException
  {
    StatusMachineEvent event = null;
    if (newGenId == -1)
    {
      // The generation id is being made invalid, let's put the DS
      // into BAD_GEN_ID_STATUS
      event = StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT;
    } else
    {
      if (newGenId == generationId)
      {
        if (status == ServerStatus.BAD_GEN_ID_STATUS)
        {
          // This server has the good new reference generation id.
          // Close connection with him to force his reconnection: DS will
          // reconnect in NORMAL_STATUS or DEGRADED_STATUS.
          if (debugEnabled())
          {
            TRACER.debugInfo(
                "In RS " +
                replicationServerDomain.getReplicationServer().getServerId() +
                ". Closing connection to DS " + getServerId() +
                " for baseDn " + getServiceId() +
                " to force reconnection as new local" +
                " generationId and remote one match and DS is in bad gen id: " +
                newGenId);
          }
          // Connection closure must not be done calling RSD.stopHandler() as it
          // would rewait the RSD lock that we already must have entering this
          // method. This would lead to a reentrant lock which we do not want.
          // So simply close the session, this will make the hang up appear
          // after the reader thread that took the RSD lock realeases it.
          try
          {
            if (session != null)
              session.close();
          } catch (IOException e)
          {
            // ignore
          }
          // NOT_CONNECTED_STATUS is the last one in RS session life: handler
          // will soon disappear after this method call...
          status = ServerStatus.NOT_CONNECTED_STATUS;
          return;
        } else
        {
          if (debugEnabled())
          {
            TRACER.debugInfo(
                "In RS " +
                replicationServerDomain.getReplicationServer().getServerId() +
                ". DS " + getServerId() + " for baseDn " + getServiceId() +
                " has already generation id " + newGenId +
            " so no ChangeStatusMsg sent to him.");
          }
          return;
        }
      } else
      {
        // This server has a bad generation id compared to new reference one,
        // let's put it into BAD_GEN_ID_STATUS
        event = StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT;
      }
    }
    if ((event == StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT) &&
        (status == ServerStatus.FULL_UPDATE_STATUS))
    {
      // Prevent useless error message (full update status cannot lead to bad
      // gen status)
      Message message = NOTE_BAD_GEN_ID_IN_FULL_UPDATE.get(
          Short.toString(replicationServerDomain.
              getReplicationServer().getServerId()),
              getServiceId().toString(),
              Short.toString(serverId),
              Long.toString(generationId),
              Long.toString(newGenId));
      logError(message);
      return;
    }
    ServerStatus newStatus = StatusMachine.computeNewStatus(status, event);
    if (newStatus == ServerStatus.INVALID_STATUS)
    {
      Message msg = ERR_RS_CANNOT_CHANGE_STATUS.get(getServiceId().toString(),
          Short.toString(serverId), status.toString(), event.toString());
      logError(msg);
      return;
    }
    // Send message requesting to change the DS status
    ChangeStatusMsg csMsg = new ChangeStatusMsg(newStatus,
        ServerStatus.INVALID_STATUS);
    if (debugEnabled())
    {
      TRACER.debugInfo(
          "In RS " +
          replicationServerDomain.getReplicationServer().getServerId() +
          " Sending change status for reset gen id to " + getServerId() +
          " for baseDn " + getServiceId() + ":\n" + csMsg);
    }
    session.publish(csMsg);
    status = newStatus;
  }
  /**
   * Change the status according to the event generated from the status
   * analyzer.
   * @param event The event to be used for new status computation
   * @return The new status of the DS
   * @throws IOException When raised by the underlying session
   */
  public ServerStatus changeStatusFromStatusAnalyzer(StatusMachineEvent event)
  throws IOException
  {
    // Check state machine allows this new status (Sanity check)
    ServerStatus newStatus = StatusMachine.computeNewStatus(status, event);
    if (newStatus == ServerStatus.INVALID_STATUS)
    {
      Message msg = ERR_RS_CANNOT_CHANGE_STATUS.get(getServiceId().toString(),
          Short.toString(serverId), status.toString(), event.toString());
      logError(msg);
      // Status analyzer must only change from NORMAL_STATUS to DEGRADED_STATUS
      // and vice versa. We may are being trying to change the status while for
      // instance another status has just been entered: e.g a full update has
      // just been engaged. In that case, just ignore attempt to change the
      // status
      return newStatus;
    }
    // Send message requesting to change the DS status
    ChangeStatusMsg csMsg = new ChangeStatusMsg(newStatus,
        ServerStatus.INVALID_STATUS);
    if (debugEnabled())
    {
      TRACER.debugInfo(
          "In RS " +
          replicationServerDomain.getReplicationServer().getServerId() +
          " Sending change status from status analyzer to " + getServerId() +
          " for baseDn " + getServiceId() + ":\n" + csMsg);
    }
    session.publish(csMsg);
    status = newStatus;
    return newStatus;
  }
  private void createStatusAnalyzer()
  {
    if (!replicationServerDomain.isRunningStatusAnalyzer())
    {
      replicationServerDomain.startStatusAnalyzer();
    }
  }
  /**
   * Retrieves a set of attributes containing monitor data that should be
   * returned to the client if the corresponding monitor entry is requested.
   *
   * @return  A set of attributes containing monitor data that should be
   *          returned to the client if the corresponding monitor entry is
   *          requested.
   */
  @Override
  public ArrayList<Attribute> getMonitorData()
  {
    // Get the generic ones
    ArrayList<Attribute> attributes = super.getMonitorData();
    // Add the specific DS ones
    attributes.add(Attributes.create("replica", serverURL));
    attributes.add(Attributes.create("connected-to",
        this.replicationServerDomain.getReplicationServer()
        .getMonitorInstanceName()));
    try
    {
      MonitorData md;
      md = replicationServerDomain.computeMonitorData();
      // Oldest missing update
      Long approxFirstMissingDate = md.getApproxFirstMissingDate(serverId);
      if ((approxFirstMissingDate != null) && (approxFirstMissingDate > 0))
      {
        Date date = new Date(approxFirstMissingDate);
        attributes.add(Attributes.create(
            "approx-older-change-not-synchronized", date.toString()));
        attributes.add(Attributes.create(
            "approx-older-change-not-synchronized-millis", String
            .valueOf(approxFirstMissingDate)));
      }
      // Missing changes
      long missingChanges = md.getMissingChanges(serverId);
      attributes.add(Attributes.create("missing-changes", String
          .valueOf(missingChanges)));
      // Replication delay
      long delay = md.getApproxDelay(serverId);
      attributes.add(Attributes.create("approximate-delay", String
          .valueOf(delay)));
      /* get the Server State */
      AttributeBuilder builder = new AttributeBuilder("server-state");
      ServerState state = md.getLDAPServerState(serverId);
      if (state != null)
      {
        for (String str : state.toStringSet())
        {
          builder.add(str);
        }
        attributes.add(builder.toAttribute());
      }
    }
    catch (Exception e)
    {
      Message message =
        ERR_ERROR_RETRIEVING_MONITOR_DATA.get(stackTraceToSingleLineString(e));
      // We failed retrieving the monitor data.
      attributes.add(Attributes.create("error", message.toString()));
    }
    return attributes;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public String getMonitorInstanceName()
  {
    String str = serverURL + " " + String.valueOf(serverId);
    return "Connected Replica " + str +
    ",cn=" + replicationServerDomain.getMonitorInstanceName();
  }
  /**
   * Gets the status of the connected DS.
   * @return The status of the connected DS.
   */
  public ServerStatus getStatus()
  {
    return status;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isDataServer()
  {
    return true;
  }
  /**
   * Process message of a remote server changing his status.
   * @param csMsg The message containing the new status
   * @return The new server status of the DS
   */
  public ServerStatus processNewStatus(ChangeStatusMsg csMsg)
  {
    // Get the status the DS just entered
    ServerStatus reqStatus = csMsg.getNewStatus();
    // Translate new status to a state machine event
    StatusMachineEvent event = StatusMachineEvent.statusToEvent(reqStatus);
    if (event == StatusMachineEvent.INVALID_EVENT)
    {
      Message msg = ERR_RS_INVALID_NEW_STATUS.get(reqStatus.toString(),
          getServiceId().toString(), Short.toString(serverId));
      logError(msg);
      return ServerStatus.INVALID_STATUS;
    }
    // Check state machine allows this new status
    ServerStatus newStatus = StatusMachine.computeNewStatus(status, event);
    if (newStatus == ServerStatus.INVALID_STATUS)
    {
      Message msg = ERR_RS_CANNOT_CHANGE_STATUS.get(getServiceId().toString(),
          Short.toString(serverId), status.toString(), event.toString());
      logError(msg);
      return ServerStatus.INVALID_STATUS;
    }
    status = newStatus;
    return status;
  }
  /**
   * Processes a start message received from a remote data server.
   * @param serverStartMsg The provided start message received.
   * @return flag specifying whether the remote server requests encryption.
   * @throws DirectoryException raised when an error occurs.
   */
  public boolean processStartFromRemote(ServerStartMsg serverStartMsg)
  throws DirectoryException
  {
    generationId = serverStartMsg.getGenerationId();
    protocolVersion = ProtocolVersion.minWithCurrent(
        serverStartMsg.getVersion());
    serverId = serverStartMsg.getServerId();
    serverURL = serverStartMsg.getServerURL();
    groupId = serverStartMsg.getGroupId();
    maxReceiveDelay = serverStartMsg.getMaxReceiveDelay();
    maxReceiveQueue = serverStartMsg.getMaxReceiveQueue();
    maxSendDelay = serverStartMsg.getMaxSendDelay();
    maxSendQueue = serverStartMsg.getMaxSendQueue();
    heartbeatInterval = serverStartMsg.getHeartbeatInterval();
    // generic stuff
    setServiceIdAndDomain(serverStartMsg.getBaseDn());
    setInitialServerState(serverStartMsg.getServerState());
    setSendWindowSize(serverStartMsg.getWindowSize());
    if (maxReceiveQueue > 0)
      restartReceiveQueue = (maxReceiveQueue > 1000 ? maxReceiveQueue -
          200 : maxReceiveQueue * 8 / 10);
    else
      restartReceiveQueue = 0;
    if (maxSendQueue > 0)
      restartSendQueue =
        (maxSendQueue > 1000 ? maxSendQueue - 200 : maxSendQueue * 8 /
            10);
    else
      restartSendQueue = 0;
    if (maxReceiveDelay > 0)
      restartReceiveDelay = (maxReceiveDelay > 10 ? maxReceiveDelay - 1
          : maxReceiveDelay);
    else
      restartReceiveDelay = 0;
    if (maxSendDelay > 0)
      restartSendDelay =
        (maxSendDelay > 10 ? maxSendDelay - 1 : maxSendDelay);
    else
      restartSendDelay = 0;
    if (heartbeatInterval < 0)
    {
      heartbeatInterval = 0;
    }
    return serverStartMsg.getSSLEncryption();
  }
  /**
   * Registers this handler into its related domain and notifies the domain
   * about the new DS.
   */
  public void registerIntoDomain()
  {
    // Alright, connected with new DS: store handler.
    Map<Short, DataServerHandler> connectedDSs =
      replicationServerDomain.getConnectedDSs();
    connectedDSs.put(serverId, this);
    // Tell peer DSs a new DS just connected to us
    // No need to resend topo msg to this just new DS so not null
    // argument
    replicationServerDomain.buildAndSendTopoInfoToDSs(this);
    // Tell peer RSs a new DS just connected to us
    replicationServerDomain.buildAndSendTopoInfoToRSs();
  }
  // Send our own TopologyMsg to DS
  private TopologyMsg sendTopoToRemoteDS()
  throws IOException
  {
    TopologyMsg outTopoMsg = replicationServerDomain.createTopologyMsgForDS(
        this.serverId);
    session.publish(outTopoMsg);
    return outTopoMsg;
  }
  /**
   * Starts the handler from a remote ServerStart message received from
   * the remote data server.
   * @param inServerStartMsg The provided ServerStart message received.
   */
  public void startFromRemoteDS(ServerStartMsg inServerStartMsg)
  {
    try
    {
      // initializations
      localGenerationId = -1;
      oldGenerationId = -100;
      // processes the ServerStart message received
      boolean sessionInitiatorSSLEncryption =
        processStartFromRemote(inServerStartMsg);
      // Get or Create the ReplicationServerDomain
      replicationServerDomain = getDomain(true);
      localGenerationId = replicationServerDomain.getGenerationId();
      oldGenerationId = localGenerationId;
      // Hack to be sure that if a server disconnects and reconnect, we
      // let the reader thread see the closure and cleanup any reference
      // to old connection
      replicationServerDomain.
      waitDisconnection(inServerStartMsg.getServerId());
      // Duplicate server ?
      if (!replicationServerDomain.checkForDuplicateDS(this))
      {
        abortStart(null);
        return;
      }
      // lock with no timeout
      lockDomain(false);
      //
      ReplServerStartMsg outReplServerStartMsg = null;
      try
      {
        outReplServerStartMsg = sendStartToRemote((short)-1);
        // log
        logStartHandshakeRCVandSND(inServerStartMsg, outReplServerStartMsg);
        // The session initiator decides whether to use SSL.
        // Until here session is encrypted then it depends on the negociation
        if (!sessionInitiatorSSLEncryption)
          session.stopEncryption();
        // wait and process StartSessionMsg from remote RS
        StartSessionMsg inStartSessionMsg =
          waitAndProcessStartSessionFromRemoteDS();
        // Send our own TopologyMsg to remote RS
        TopologyMsg outTopoMsg = sendTopoToRemoteDS();
        logStartSessionHandshake(inStartSessionMsg, outTopoMsg);
      }
      catch(IOException e)
      {
        // We do not want polluting error log if error is due to normal session
        // aborted after handshake phase one from a DS that is searching for
        // best suitable RS.
        // don't log a poluting error when connection aborted
        // from a DS that wanted only to perform handshake phase 1 in order
        // to determine the best suitable RS:
        // 1) -> ServerStartMsg
        // 2) <- ReplServerStartMsg
        // 3) connection closure
        throw new DirectoryException(ResultCode.OTHER, null, null);
      }
      catch (NotSupportedOldVersionPDUException e)
      {
        // We do not need to support DS V1 connection, we just accept RS V1
        // connection:
        // We just trash the message, log the event for debug purpose and close
        // the connection
        throw new DirectoryException(ResultCode.OTHER, null, null);
      }
      catch (Exception e)
      {
        // We do not need to support DS V1 connection, we just accept RS V1
        // connection:
        // We just trash the message, log the event for debug purpose and close
        // the connection
        throw new DirectoryException(ResultCode.OTHER, null, null);
      }
      // Create the status analyzer for the domain if not already started
      createStatusAnalyzer();
      registerIntoDomain();
      super.finalizeStart();
    }
    catch(DirectoryException de)
    {
      abortStart(de.getMessageObject());
    }
    catch(Exception e)
    {
      abortStart(null);
    }
    finally
    {
      if ((replicationServerDomain != null) &&
          replicationServerDomain.hasLock())
        replicationServerDomain.release();
    }
  }
  /**
   * Creates a DSInfo structure representing this remote DS.
   * @return The DSInfo structure representing this remote DS
   */
  public DSInfo toDSInfo()
  {
    DSInfo dsInfo = new DSInfo(serverId, replicationServerId, generationId,
        status, assuredFlag, assuredMode, safeDataLevel, groupId, refUrls);
    return dsInfo;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public String toString()
  {
    String localString;
    if (serverId != 0)
    {
      localString = "Directory Server ";
      localString += serverId + " " + serverURL + " " + getServiceId();
    } else
      localString = "Unknown server";
    return localString;
  }
  /**
   * Wait receiving the StartSessionMsg from the remote DS and process it.
   * @return the startSessionMsg received
   * @throws DirectoryException
   * @throws IOException
   * @throws ClassNotFoundException
   * @throws DataFormatException
   * @throws NotSupportedOldVersionPDUException
   */
  private StartSessionMsg waitAndProcessStartSessionFromRemoteDS()
  throws DirectoryException, IOException, ClassNotFoundException,
  DataFormatException,
  NotSupportedOldVersionPDUException
  {
    ReplicationMsg msg = null;
    msg = session.receive();
    if (!(msg instanceof StartSessionMsg))
    {
      Message message = Message.raw(
          "Protocol error: StartSessionMsg required." + msg + " received.");
      abortStart(message);
    }
    // Process StartSessionMsg sent by remote DS
    StartSessionMsg startSessionMsg = (StartSessionMsg) msg;
    this.status = startSessionMsg.getStatus();
    // Sanity check: is it a valid initial status?
    if (!isValidInitialStatus(this.status))
    {
      Message message = ERR_RS_INVALID_INIT_STATUS.get(
          this.status.toString(),
          getServiceId().toString(),
          Short.toString(serverId));
      throw new DirectoryException(ResultCode.OTHER, message);
    }
    this.refUrls = startSessionMsg.getReferralsURLs();
    this.assuredFlag = startSessionMsg.isAssured();
    this.assuredMode = startSessionMsg.getAssuredMode();
    this.safeDataLevel = startSessionMsg.getSafeDataLevel();
    /*
     * If we have already a generationID set for the domain
     * then
     *   if the connecting replica has not the same
     *   then it is degraded locally and notified by an error message
     * else
     *   we set the generationID from the one received
     *   (unsaved yet on disk . will be set with the 1rst change
     * received)
     */
    if (localGenerationId > 0)
    {
      if (generationId != localGenerationId)
      {
        Message message = NOTE_BAD_GENERATION_ID_FROM_DS.get(
            getServiceId(),
            Short.toString(serverId),
            Long.toString(generationId),
            Long.toString(localGenerationId));
        logError(message);
      }
    }
    else
    {
      // We are an empty Replicationserver
      if ((generationId > 0) && (!getServerState().isEmpty()))
      {
        // If the LDAP server has already sent changes
        // it is not expected to connect to an empty RS
        Message message = NOTE_BAD_GENERATION_ID_FROM_DS.get(
            getServiceId(),
            Short.toString(serverId),
            Long.toString(generationId),
            Long.toString(localGenerationId));
        logError(message);
      }
      else
      {
        // The local RS is not initialized - take the one received
        // WARNING: Must be done before computing topo message to send
        // to peer server as topo message must embed valid generation id
        // for our server
        oldGenerationId =
          replicationServerDomain.setGenerationId(generationId, false);
      }
    }
    return startSessionMsg;
  }
}
opends/src/server/org/opends/server/replication/server/ECLServerHandler.java
New file
@@ -0,0 +1,1551 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.server;
import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.zip.DataFormatException;
import org.opends.messages.Category;
import org.opends.messages.Message;
import org.opends.messages.Severity;
import org.opends.server.api.DirectoryThread;
import org.opends.server.replication.common.ChangeNumber;
import org.opends.server.replication.common.MultiDomainServerState;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.common.ServerStatus;
import org.opends.server.replication.protocol.*;
import org.opends.server.types.Attribute;
import org.opends.server.types.Attributes;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.ResultCode;
import org.opends.server.util.TimeThread;
/**
 * This class defines a server handler, which handles all interaction with a
 * peer replication server.
 */
public class ECLServerHandler extends ServerHandler
{
  // Properties filled only if remote server is a RS
  private String serverAddressURL;
  String operationId;
  /**
   * CLDomainContext : contains the state properties for the search
   * currently being processed, by replication domain.
   */
  private class CLDomainContext
  {
    ReplicationServerDomain rsd; // the repl server domain
    boolean active;              // is the domain still active
    MessageHandler mh;           // the message handler associated
    UpdateMsg nextMsg;
    UpdateMsg nonElligiblemsg;
    ServerState startState;
    ServerState currentState;
    ServerState stopState;
    /**
     * Add to the provider buffer a string representation of this object.
     */
    public void toString(StringBuilder buffer, int i)
    {
      CLDomainContext xx = clDomCtxts[i];
      buffer.append(
          " clDomCtxts(" + i + ") [act=" + xx.active +
          " rsd=" + rsd +
          " nextMsg=" + nextMsg + "(" +
          (nextMsg != null?
              new Date(nextMsg.getChangeNumber().getTime()).toString():"")
              + ")" +
          " nextNonEligibleMsg="      + nonElligiblemsg +
          " startState=" + startState +
          " stopState= " + stopState +
          " currState= " + currentState + "]");
    }
  }
  // The list of contexts by domain for the current search
  CLDomainContext[] clDomCtxts = new CLDomainContext[0];
  private void clDomCtxtsToString(String msg)
  {
    StringBuilder buffer = new StringBuilder();
    buffer.append(msg+"\n");
    for (int i=0;i<clDomCtxts.length;i++)
    {
      clDomCtxts[i].toString(buffer, i);
      buffer.append("\n");
    }
    TRACER.debugInfo(
        "In " + this.getName() + " clDomCtxts: " + buffer.toString());
  }
  /**
   * Class that manages the state variables for the current search on the ECL.
   */
  private class CLTraverseCtxt
  {
    /**
     * Specifies the next changer number (seqnum), -1 when not.
     */
    public int nextSeqnum;
    /**
     * Specifies whether the current search has been requested to be persistent
     * or not.
     */
    public short isPersistent;
    /**
     * Specifies the last changer number (seqnum) requested.
     */
    public int stopSeqnum;
    /**
     * Specifies whether the change number (seqnum) db has been read until
     * its end.
     */
    public boolean endOfSeqnumdbReached = false;
    /**
     * Specifies the current search phase.
     * 1 = init
     * 2 = persistent
     */
    public int searchPhase = 1;
    /**
     * Specifies the cookie contained in the request, specifying where
     * to start serving the ECL.
     */
    public String generalizedStartState;
    /**
     * Specifies the current cookie value.
     */
    public MultiDomainServerState currentCookie =
      new MultiDomainServerState();
    /**
     * Specifies the excluded DNs.
     */
    public ArrayList<String> excludedServiceIDs = new ArrayList<String>();
    /**
     * Provides a string representation of this object.
     * @return the string representation.
     */
    public String toString()
    {
      return new String(
        this.getClass().getCanonicalName() +
        ":[" +
        " nextSeqnum=" + nextSeqnum +
        " persistent=" + isPersistent +
        " stopSeqnum" + stopSeqnum +
        " endOfSeqnumdbReached=" + endOfSeqnumdbReached +
        " searchPhase=" + searchPhase +
        " generalizedStartState=" + generalizedStartState +
        "]");
    }
  }
  // The context of the current search
  private CLTraverseCtxt cLSearchCtxt = new CLTraverseCtxt();
  /**
   * Starts this handler based on a start message received from remote server.
   * @param inECLStartMsg The start msg provided by the remote server.
   * @return Whether the remote server requires encryption or not.
   * @throws DirectoryException When a problem occurs.
   */
  public boolean processStartFromRemote(ServerStartECLMsg inECLStartMsg)
  throws DirectoryException
  {
    try
    {
      protocolVersion = ProtocolVersion.minWithCurrent(
          inECLStartMsg.getVersion());
      generationId = inECLStartMsg.getGenerationId();
      serverURL = inECLStartMsg.getServerURL();
      int separator = serverURL.lastIndexOf(':');
      serverAddressURL =
        session.getRemoteAddress() + ":" + serverURL.substring(separator +
            1);
      setInitialServerState(inECLStartMsg.getServerState());
      setSendWindowSize(inECLStartMsg.getWindowSize());
      if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1)
      {
        // We support connection from a V1 RS
        // Only V2 protocol has the group id in repl server start message
        this.groupId = inECLStartMsg.getGroupId();
      }
      // FIXME:ECL Any generationID must be removed, it makes no sense here.
      oldGenerationId = -100;
    }
    catch(Exception e)
    {
      Message message = Message.raw(e.getLocalizedMessage());
      throw new DirectoryException(ResultCode.OTHER, message);
    }
    return inECLStartMsg.getSSLEncryption();
  }
  /**
   * Creates a new handler object to a remote replication server.
   * @param session The session with the remote RS.
   * @param queueSize The queue size to manage updates to that RS.
   * @param replicationServerURL The hosting local RS URL.
   * @param replicationServerId The hosting local RS serverId.
   * @param replicationServer The hosting local RS object.
   * @param rcvWindowSize The receiving window size.
   */
  public ECLServerHandler(
      ProtocolSession session,
      int queueSize,
      String replicationServerURL,
      short replicationServerId,
      ReplicationServer replicationServer,
      int rcvWindowSize)
  {
    super(session, queueSize, replicationServerURL, replicationServerId,
        replicationServer, rcvWindowSize);
    try
    {
      setServiceIdAndDomain("cn=changelog");
    }
    catch(DirectoryException de)
    {
      // no chance to have a bad domain set here
    }
  }
  /**
   * Creates a new handler object to a remote replication server.
   * @param replicationServerURL The hosting local RS URL.
   * @param replicationServerId The hosting local RS serverId.
   * @param replicationServer The hosting local RS object.
   * @param startECLSessionMsg the start parameters.
   * @throws DirectoryException when an errors occurs.
   */
  public ECLServerHandler(
      String replicationServerURL,
      short replicationServerId,
      ReplicationServer replicationServer,
      StartECLSessionMsg startECLSessionMsg)
  throws DirectoryException
  {
    // FIXME:ECL queueSize is hard coded to 1 else Handler hangs for some reason
    super(null, 1, replicationServerURL, replicationServerId,
        replicationServer, 0);
    try
    {
      setServiceIdAndDomain("cn=changelog");
    }
    catch(DirectoryException de)
    {
      // no chance to have a bad domain set here
    }
    this.initialize(startECLSessionMsg);
  }
  /**
   * Starts the handler from a remote start message received from
   * the remote server.
   * @param inECLStartMsg The provided ReplServerStart message received.
   */
  public void startFromRemoteServer(ServerStartECLMsg inECLStartMsg)
  {
    try
    {
      // Process start from remote
      boolean sessionInitiatorSSLEncryption =
        processStartFromRemote(inECLStartMsg);
      // lock with timeout
      lockDomain(true);
      // send start to remote
      ReplServerStartMsg outReplServerStartMsg = sendStartToRemote((short)-1);
      // log
      logStartHandshakeRCVandSND(inECLStartMsg, outReplServerStartMsg);
      // until here session is encrypted then it depends on the negociation
      // The session initiator decides whether to use SSL.
      if (!sessionInitiatorSSLEncryption)
        session.stopEncryption();
      // wait and process StartSessionMsg from remote RS
      StartECLSessionMsg inStartECLSessionMsg =
        waitAndProcessStartSessionECLFromRemoteServer();
      logStartECLSessionHandshake(inStartECLSessionMsg);
      // initialization
      initialize(inStartECLSessionMsg);
    }
    catch(DirectoryException de)
    {
      abortStart(de.getMessageObject());
    }
    catch(Exception e)
    {
      abortStart(Message.raw(e.getLocalizedMessage()));
    }
    finally
    {
      if ((replicationServerDomain != null) &&
          replicationServerDomain.hasLock())
      {
        replicationServerDomain.release();
      }
    }
  }
  /**
   * Wait receiving the StartSessionMsg from the remote DS and process it.
   * @return the startSessionMsg received
   * @throws DirectoryException
   * @throws IOException
   * @throws ClassNotFoundException
   * @throws DataFormatException
   * @throws NotSupportedOldVersionPDUException
   */
  private StartECLSessionMsg waitAndProcessStartSessionECLFromRemoteServer()
  throws DirectoryException, IOException, ClassNotFoundException,
  DataFormatException,
  NotSupportedOldVersionPDUException
  {
    ReplicationMsg msg = null;
    msg = session.receive();
    if (!(msg instanceof StartECLSessionMsg))
    {
      Message message = Message.raw(
          "Protocol error: StartECLSessionMsg required." + msg + " received.");
      abortStart(message);
    }
    // Process StartSessionMsg sent by remote DS
    StartECLSessionMsg startECLSessionMsg = (StartECLSessionMsg) msg;
    return startECLSessionMsg;
  }
  /**
   * Initialize the handler from a provided cookie value.
   * @param providedGeneralizedStartState The provided cookie value.
   * @throws DirectoryException When an error is raised.
   */
  public void initializeCLSearchFromGenState(
      String providedGeneralizedStartState)
  throws DirectoryException
  {
    this.cLSearchCtxt.nextSeqnum = -1; // will not generate seqnum
    initializeCLDomCtxts(providedGeneralizedStartState);
  }
  /**
   * Initialize the context for each domain.
   * @param providedGeneralizedStartState the provided generalized state
   * @throws DirectoryException When an error occurs.
   */
  public void initializeCLDomCtxts(String providedGeneralizedStartState)
  throws DirectoryException
  {
    HashMap<String,ServerState> startStates = new HashMap<String,ServerState>();
    ReplicationServer rs = replicationServerDomain.getReplicationServer();
    try
    {
      // Initialize start state for  all running domains with empty state
      Iterator<ReplicationServerDomain> rsdk = rs.getCacheIterator();
      if (rsdk != null)
      {
        while (rsdk.hasNext())
        {
          // process a domain
          ReplicationServerDomain rsd = rsdk.next();
          // skip the changelog domain
          if (rsd == this.replicationServerDomain)
            continue;
          startStates.put(rsd.getBaseDn(), new ServerState());
        }
      }
      // Overwrite start state from the cookie provided in the request
      if ((providedGeneralizedStartState != null) &&
          (providedGeneralizedStartState.length()>0))
      {
        String[] domains = providedGeneralizedStartState.split(";");
        for (String domainState : domains)
        {
          // Split baseDN and serverState
          String[] fields = domainState.split(":");
          // BaseDN - Check it
          String domainBaseDNReceived = fields[0];
          if (!startStates.containsKey(domainBaseDNReceived))
            throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
                ERR_INVALID_COOKIE_FULL_RESYNC_REQUIRED.get(
                  "unknown " + domainBaseDNReceived));
          // ServerState
          ServerState domainServerState = new ServerState();
          if (fields.length>1)
          {
            String strState = fields[1];
            String[] strCN = strState.split(" ");
            for (String sr : strCN)
            {
              ChangeNumber fromChangeNumber = new ChangeNumber(sr);
              domainServerState.update(fromChangeNumber);
            }
          }
          startStates.put(domainBaseDNReceived, domainServerState);
          // FIXME: ECL first cookie value check
          // ECL For each of the provided state, it this state is older
          // than the older change stored in the replication changelog ....
          // then a purge occured since the time the cookie was published
          // it is recommended to do a full resync
          ReplicationServerDomain rsd =
            rs.getReplicationServerDomain(domainBaseDNReceived, false);
          ServerState domainStartState = rsd.getStartState();
          if (!domainServerState.cover(domainStartState))
          {
            throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
                ERR_INVALID_COOKIE_FULL_RESYNC_REQUIRED.get(
                  "too old cookie provided " + providedGeneralizedStartState
                  + " first acceptable change for " + rsd.getBaseDn()
                  + " is " + rsd.getStartState()));
          }
        }
      }
    }
    catch(DirectoryException de)
    {
      throw de;
    }
    catch(Exception e)
    {
      throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
          ERR_INVALID_COOKIE_FULL_RESYNC_REQUIRED.get(
            "Exception raised: " + e.getMessage()));
    }
    try
    {
      // Now traverse all domains and build the initial changelog context
      Iterator<ReplicationServerDomain> rsdi = rs.getCacheIterator();
      // Creates the table that will contain the real-time info by domain.
      clDomCtxts = new CLDomainContext[rs.getCacheSize()-1
                         -this.cLSearchCtxt.excludedServiceIDs.size()];
      int i =0;
      if (rsdi != null)
      {
        while (rsdi.hasNext())
        {
          // process a domain
          ReplicationServerDomain rsd = rsdi.next();
          // skip the 'unreal' changelog domain
          if (rsd == this.replicationServerDomain)
            continue;
          // skip the excluded domains
          boolean excluded = false;
          for(String excludedServiceID : this.cLSearchCtxt.excludedServiceIDs)
          {
            if (excludedServiceID.equalsIgnoreCase(rsd.getBaseDn()))
            {
              excluded=true;
              break;
            }
          }
          if (excluded)
            continue;
          // Creates the context record
          CLDomainContext newContext = new CLDomainContext();
          newContext.active = true;
          newContext.rsd = rsd;
          if (this.cLSearchCtxt.isPersistent ==
            StartECLSessionMsg.PERSISTENT_CHANGES_ONLY)
          {
            newContext.startState = rsd.getCLElligibleState();
          }
          else
          {
            newContext.startState = startStates.get(rsd.getBaseDn());
            newContext.stopState = rsd.getCLElligibleState();
          }
          newContext.currentState = new ServerState();
          // Creates an unconnected SH
          MessageHandler mh = new MessageHandler(maxQueueSize,
              replicationServerURL, replicationServerId, replicationServer);
          // set initial state
          mh.setInitialServerState(newContext.startState);
          // set serviceID and domain
          mh.setServiceIdAndDomain(rsd.getBaseDn());
          // register into domain
          rsd.registerHandler(mh);
          newContext.mh = mh;
          // store the new context
          clDomCtxts[i] = newContext;
          i++;
        }
      }
      // the next record from the seqnumdb should be the one
      cLSearchCtxt.endOfSeqnumdbReached = false;
      cLSearchCtxt.generalizedStartState = providedGeneralizedStartState;
      // Initializes all domain with the next elligible message
      for (int j=0; j<clDomCtxts.length; j++)
      {
        this.getNextElligibleMessage(j);
        if (clDomCtxts[j].nextMsg == null)
          clDomCtxts[j].active = false;
      }
    }
    catch(Exception e)
    {
      TRACER.debugCaught(DebugLogLevel.ERROR, e);
      throw new DirectoryException(
          ResultCode.OPERATIONS_ERROR,
          Message.raw(Category.SYNC, Severity.INFORMATION,"Exception raised: " +
              e.getLocalizedMessage()),
          e);
    }
  }
  /**
   * Registers this handler into its related domain and notifies the domain.
   */
  private void registerIntoDomain()
  {
    replicationServerDomain.registerHandler(this);
  }
  /**
   * Shutdown this handler ServerHandler.
   */
  public void shutdown()
  {
    for (int i=0;i<clDomCtxts.length;i++)
    {
      if (!clDomCtxts[i].rsd.unRegisterHandler(clDomCtxts[i].mh))
      {
        TRACER.debugInfo(this +" shutdown() Internal error " +
                " when unregistering "+ clDomCtxts[i].mh);
      }
      clDomCtxts[i].rsd.stopServer(clDomCtxts[i].mh);
    }
    super.shutdown();
    clDomCtxts = null;
  }
  /**
   * Request to shutdown the associated writer.
   */
  protected void shutdownWriter()
  {
    shutdownWriter = true;
    if (writer!=null)
    {
      ECLServerWriter eclWriter = (ECLServerWriter)this.writer;
      eclWriter.shutdownWriter();
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public String getMonitorInstanceName()
  {
    String str = serverURL + " " + String.valueOf(serverId);
    return "Connected External Changelog Server " + str +
    ",cn=" + replicationServerDomain.getMonitorInstanceName();
  }
  /**
   * Retrieves a set of attributes containing monitor data that should be
   * returned to the client if the corresponding monitor entry is requested.
   *
   * @return  A set of attributes containing monitor data that should be
   *          returned to the client if the corresponding monitor entry is
   *          requested.
   */
  @Override
  public ArrayList<Attribute> getMonitorData()
  {
    // Get the generic ones
    ArrayList<Attribute> attributes = super.getMonitorData();
    // Add the specific RS ones
    attributes.add(Attributes.create("External-Changelog-Server",
        serverURL));
    try
    {
      MonitorData md;
      md = replicationServerDomain.computeMonitorData();
      // FIXME:ECL No monitoring exist for ECL.
    }
    catch (Exception e)
    {
      Message message =
        ERR_ERROR_RETRIEVING_MONITOR_DATA.get(stackTraceToSingleLineString(e));
      // We failed retrieving the monitor data.
      attributes.add(Attributes.create("error", message.toString()));
    }
    return attributes;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public String toString()
  {
    String localString;
    localString = "External changelog Server ";
    if (this.cLSearchCtxt==null)
      localString += serverId + " " + serverURL + " " + getServiceId();
    else
      localString += this.getName();
    return localString;
  }
  /**
   * Gets the status of the connected DS.
   * @return The status of the connected DS.
   */
  public ServerStatus getStatus()
  {
    // FIXME:ECL Sould ECLServerHandler manage a ServerStatus ?
    return ServerStatus.INVALID_STATUS;
  }
  /**
   * Retrieves the Address URL for this server handler.
   *
   * @return  The Address URL for this server handler,
   *          in the form of an IP address and port separated by a colon.
   */
  public String getServerAddressURL()
  {
    return serverAddressURL;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isDataServer()
  {
    return true;
  }
  /**
   * Initialize the handler.
   * @param startECLSessionMsg The provided starting state.
   * @throws DirectoryException when a problem occurs.
   */
  public void initialize(StartECLSessionMsg startECLSessionMsg)
  throws DirectoryException
  {
    //
    this.following = false; // FIXME:ECL makes no sense for ECLServerHandler ?
    this.lateQueue.clear(); // FIXME:ECL makes no sense for ECLServerHandler ?
    this.setConsumerActive(true);
    this.cLSearchCtxt.searchPhase = 1;
    this.operationId = startECLSessionMsg.getOperationId();
    this.setName(this.getClass().getCanonicalName()+ " " + operationId);
    if (eligibleCNComputerThread==null)
      eligibleCNComputerThread = new ECLEligibleCNComputerThread();
    cLSearchCtxt.isPersistent  = startECLSessionMsg.isPersistent();
    cLSearchCtxt.stopSeqnum    = startECLSessionMsg.getLastDraftChangeNumber();
    cLSearchCtxt.searchPhase   = 1;
    cLSearchCtxt.currentCookie = new MultiDomainServerState(
        startECLSessionMsg.getCrossDomainServerState());
    cLSearchCtxt.excludedServiceIDs=startECLSessionMsg.getExcludedServiceIDs();
    //--
    if (startECLSessionMsg.getECLRequestType()==0)
    {
      initializeCLSearchFromGenState(
          startECLSessionMsg.getCrossDomainServerState());
    }
    if (session != null)
    {
      try
      {
        // Disable timeout for next communications
        session.setSoTimeout(0);
      }
      catch(Exception e) {}
      // sendWindow MUST be created before starting the writer
      sendWindow = new Semaphore(sendWindowSize);
      // create reader
      reader = new ServerReader(session, serverId,
          this, replicationServerDomain);
      reader.start();
      if (writer == null)
      {
        // create writer
        writer = new ECLServerWriter(session,this,replicationServerDomain);
        writer.start();
      }
      // Resume the writer
      ((ECLServerWriter)writer).resumeWriter();
      // FIXME:ECL Potential race condition if writer not yet resumed here
    }
    if (cLSearchCtxt.isPersistent == StartECLSessionMsg.PERSISTENT_CHANGES_ONLY)
    {
      closePhase1();
    }
    /* TODO: Good Draft Compat
    //--
    if (startCLMsg.getStartMode()==1)
    {
      initializeCLSearchFromProvidedSeqnum(startCLMsg.getSequenceNumber());
    }
    //--
    if (startCLMsg.getStartMode()==2)
    {
      if (CLSearchFromProvidedExactCN(startCLMsg.getChangeNumber()))
        return;
    }
    //--
    if (startCLMsg.getStartMode()==4)
    {
      // to get the CL first and last
      initializeCLDomCtxts(null); // from start
      ChangeNumber crossDomainElligibleCN = computeCrossDomainElligibleCN();
      try
      {
        // to get the CL first and last
        // last rely on the crossDomainElligibleCN thhus must have been
        // computed before
        int[] limits = computeCLLimits(crossDomainElligibleCN);
        // Send the response
        CLLimitsMsg msg = new CLLimitsMsg(limits[0], limits[1]);
        session.publish(msg);
      }
      catch(Exception e)
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
        try
        {
          session.publish(
            new ErrorMsg(
             replicationServerDomain.getReplicationServer().getServerId(),
             serverId,
             Message.raw(Category.SYNC, Severity.INFORMATION,
                 "Exception raised: " + e.getMessage())));
        }
        catch(IOException ioe)
        {
          // FIXME: close conn ?
        }
      }
      return;
    }
    Good Draft Compat */
    // Store into domain
    registerIntoDomain();
  }
  /**
   * Select the next update that must be sent to the server managed by this
   * ServerHandler.
   *
   * @return the next update that must be sent to the server managed by this
   *         ServerHandler.
   * @exception DirectoryException when an error occurs.
   */
  public ECLUpdateMsg takeECLUpdate()
  throws DirectoryException
  {
    boolean interrupted = true;
    ECLUpdateMsg msg = getnextUpdate();
    // FIXME:ECL We should refactor so that a SH always have a session
    if (session == null)
      return msg;
    /*
     * When we remove a message from the queue we need to check if another
     * server is waiting in flow control because this queue was too long.
     * This check might cause a performance penalty an therefore it
     * is not done for every message removed but only every few messages.
     */
    /** FIXME:ECL checkAllSaturation makes no sense for ECLServerHandler ?
    if (++saturationCount > 10)
    {
      saturationCount = 0;
      try
      {
        replicationServerDomain.checkAllSaturation();
      } catch (IOException e)
      {
      }
    }
    */
    boolean acquired = false;
    do
    {
      try
      {
        acquired = sendWindow.tryAcquire((long) 500, TimeUnit.MILLISECONDS);
        interrupted = false;
      } catch (InterruptedException e)
      {
        // loop until not interrupted
      }
    } while (((interrupted) || (!acquired)) && (!shutdownWriter));
    if (msg != null)
    {
      incrementOutCount();
    }
    return msg;
  }
  /**
   * Get the next message - non blocking - null when none.
   *
   * @param synchronous - not used - always non blocking.
   * @return the next message - null when none.
   */
  protected UpdateMsg getnextMessage(boolean synchronous)
  {
    UpdateMsg msg = null;
    try
    {
      ECLUpdateMsg eclMsg = getnextUpdate();
      if (eclMsg!=null)
        msg = eclMsg.getUpdateMsg();
    }
    catch(DirectoryException de)
    {
      TRACER.debugCaught(DebugLogLevel.ERROR, de);
    }
    return msg;
  }
  /**
   * Get the next external changelog update.
   *
   * @return    The ECL update, null when none.
   * @exception DirectoryException when any problem occurs.
   */
  protected ECLUpdateMsg getnextUpdate()
  throws DirectoryException
  {
    return getGeneralizedNextECLUpdate(this.cLSearchCtxt);
  }
  /**
   * Computes the cross domain eligible message (non blocking).
   * Return null when search is covered
   */
  private ECLUpdateMsg getGeneralizedNextECLUpdate(CLTraverseCtxt cLSearchCtxt)
  throws DirectoryException
  {
    ECLUpdateMsg theOldestChange = null;
    try
    {
      TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() + "," + this +
          " getGeneralizedNextECLUpdate starts with ctxt="
          + cLSearchCtxt);
      if ((cLSearchCtxt.nextSeqnum != -1) &&
          (!cLSearchCtxt.endOfSeqnumdbReached))
      {
        /* TODO:ECL G Good changelog draft compat.
        // First time , initialise the cursor to traverse the seqnumdb
        if (seqnumDbReadIterator == null)
        {
          try
          {
          seqnumDbReadIterator = replicationServerDomain.getReplicationServer().
            getSeqnumDbHandler().generateIterator(cLSearchCtxt.nextSeqnum);
            TRACER.debugInfo("getGeneralizedNextMessage(): "
                + " creates seqnumDbReadIterator from nextSeqnum="
                + cLSearchCtxt.nextSeqnum
                + " 1rst=" + seqnumDbReadIterator.getSeqnum()
                + " CN=" + seqnumDbReadIterator.getChangeNumber()
                + cLSearchCtxt);
          }
          catch(Exception e)
          {
            cLSearchCtxt.endOfSeqnumdbReached = true;
          }
        }
        */
      }
      // Search / no seqnum / not persistent
      // -----------------------------------
      //  init: all domain are candidate
      //    get one msg from each
      //       no (null) msg returned: should not happen since we go to a state
      //       that is computed/expected
      //  getMessage:
      //    get the oldest msg:
      //    after:
      //     if stopState of domain is covered then domain is out candidate
      //       until no domain candidate mean generalized stopState
      //       has been reached
      //     else
      //       get one msg from that domain
      //       no (null) msg returned: should not happen since we go to a state
      //       that is computed/expected
      //  step 2: send DoneMsg
      // Persistent:
      // ----------
      //  step 1&2: same as non persistent
      //
      //  step 3: reinit all domain are candidate
      //    take the oldest
      //    if one domain has no msg, still is candidate
      int iDom = 0;
      boolean nextclchange = true;
      while ((nextclchange) && (cLSearchCtxt.searchPhase==1))
      {
        // Step 1 & 2
        if (cLSearchCtxt.searchPhase==1)
        {
          if (debugEnabled())
            clDomCtxtsToString("In getGeneralizedNextMessage : " +
              "looking for the generalized oldest change");
          // Retrieves the index in the subx table of the
          // generalizedOldestChange
          iDom = getGeneralizedOldestChange();
          // idomain != -1 means that we got one
          // generalized oldest change to process
          if (iDom==-1)
          {
            closePhase1();
            // signify end of phase 1 to the caller
            return null;
          }
          // idomain != -1 means that we got one
          // generalized oldest change to process
          String suffix = this.clDomCtxts[iDom].rsd.getBaseDn();
          theOldestChange = new ECLUpdateMsg(
              (LDAPUpdateMsg)clDomCtxts[iDom].nextMsg,
              null, // set later
              suffix);
          nextclchange = false;
          /* TODO:ECL G Good change log draft compat.
          if (cLSearchCtxt.nextSeqnum!=-1)
          {
            // Should either retrieve or generate a seqnum
            // we also need to check if the seqnumdb is acccurate reagrding
            // the changelogdb.
            // if not, 2 potential reasons
            // -1 : purge from the changelog .. let's traverse the seqnumdb
            // -2 : changelog is late .. let's traverse the changelog
            // replogcn : the oldest change from the changelog db
            ChangeNumber replogcn = theOldestChange.getChangeNumber();
            DN replogReplDomDN = clDomCtxts[iDom].rsd.getBaseDn();
            while (true)
            {
              if (!cLSearchCtxt.endOfSeqnumdbReached)
              {
                // we did not reach yet the end of the seqnumdb
                // seqnumcn : the next change from from the seqnum db
                ChangeNumber seqnumcn = seqnumDbReadIterator.getChangeNumber();
                // are replogcn and seqnumcn should be the same change ?
                int cmp = replogcn.compareTo(seqnumcn);
                DN seqnumReplDomDN=DN.decode(seqnumDbReadIterator.
                getDomainDN());
                TRACER.debugInfo("seqnumgen: comparing the 2 db "
                    + " changelogdb:" + replogReplDomDN + "=" + replogcn
                    + " ts=" +
                    new Date(replogcn.getTime()).toString()
                    + "## seqnumdb:" + seqnumReplDomDN + "=" + seqnumcn
                    + " ts=" +
                    new Date(seqnumcn.getTime()).toString()
                    + " sn older=" + seqnumcn.older(replogcn));
                if ((replogReplDomDN.compareTo(seqnumReplDomDN)==0) && (cmp==0))
                {
                  // same domain and same CN => same change
                  // assign the seqnum from the seqnumdb
                  // to the change from the changelogdb
                  TRACER.debugInfo("seqnumgen: assigning seqnum="
                      + seqnumDbReadIterator.getSeqnum()
                      + " to change=" + theOldestChange);
                  theOldestChange.setSeqnum(seqnumDbReadIterator.getSeqnum());
                  // prepare the next seqnum for the potential next change added
                  // to the seqnumDb
                  cLSearchCtxt.nextSeqnum = seqnumDbReadIterator.getSeqnum()
                  + 1;
                  break;
                }
                else
                {
                  // replogcn and seqnumcn are NOT the same change
                  if (seqnumcn.older(replogcn))
                  {
                    // the change from the seqnumDb is older
                    // that means that the change has been purged from the
                    // changelog
                    try
                    {
                      // let's traverse the seqnumdb searching for the change
                      // found in the changelogDb.
                      TRACER.debugInfo("seqnumgen: will skip "
                          + seqnumcn  + " and next from  the seqnum");
                      cLSearchCtxt.endOfSeqnumdbReached =
                        (seqnumDbReadIterator.next()==false);
                      TRACER.debugInfo("seqnumgen: has nexted cr to "
                          + " sn=" + seqnumDbReadIterator.getSeqnum()
                          + " cn=" + seqnumDbReadIterator.getChangeNumber()
                          + " and reached end "
                          + " of seqnumdb:"+cLSearchCtxt.endOfSeqnumdbReached);
                      if (cLSearchCtxt.endOfSeqnumdbReached)
                      {
                        // we are at the end of the seqnumdb in the append mode
                        // store in seqnumdb the pair
                        // seqnum of the cur change,state before this change)
                        replicationServerDomain.addSeqnum(
                            cLSearchCtxt.nextSeqnum,
                            getGenState(),
                      clDomCtxts[iDom].rsd.getBaseDn().toNormalizedString(),
                            theOldestChange.getChangeNumber());
                        theOldestChange.setSeqnum(cLSearchCtxt.nextSeqnum);
                        cLSearchCtxt.nextSeqnum++;
                        break;
                      }
                      else
                      {
                        // next change from seqnumdb
                        cLSearchCtxt.nextSeqnum =
                          seqnumDbReadIterator.getSeqnum() + 1;
                        continue;
                      }
                    }
                    catch(Exception e)
                    {
                    }
                  }
                  else
                  {
                    // the change from the changelogDb is older
                    // it should have been stored lately
                    // let's continue to traverse the changelogdb
                    TRACER.debugInfo("seqnumgen: will skip "
                        + replogcn  + " and next from  the CL");
                    nextclchange = true;
                    break; // TO BE CHECKED
                  }
                }
              }
              else
              {
                // we are at the end of the seqnumdb in the append mode
                // store in seqnumdb the pair
                // (seqnum of the current change, state before this change)
                replicationServerDomain.addSeqnum(
                    cLSearchCtxt.nextSeqnum,
                    getGenState(),
                    clDomCtxts[iDom].rsd.getBaseDn().toNormalizedString(),
                    theOldestChange.getChangeNumber());
                theOldestChange.setSeqnum(cLSearchCtxt.nextSeqnum);
                cLSearchCtxt.nextSeqnum++;
                break;
              }
            } // while seqnum
          } // nextseqnum !- -1
          */
          // here we have the right oldest change and in the seqnum case we
          // have its seqnum
          // Set and test the domain of the oldestChange see if we reached
          // the end of the phase for this domain
          clDomCtxts[iDom].currentState.update(
              theOldestChange.getUpdateMsg().getChangeNumber());
          if (clDomCtxts[iDom].currentState.cover(clDomCtxts[iDom].stopState))
          {
            clDomCtxts[iDom].active = false;
          }
          // Test the seqnum of the oldestChange see if we reached
          // the end of operation
          /* TODO:ECL G Good changelog draft compat. Not yet implemented
          if ((cLSearchCtxt.stopSeqnum>0) &&
              (theOldestChange.getSeqnum()>=cLSearchCtxt.stopSeqnum))
          {
            closePhase1();
            // means end of phase 1 to the calling writer
            return null;
          }
          */
          if (clDomCtxts[iDom].active)
          {
            // populates the table with the next eligible msg from idomain
            // in non blocking mode, return null when no more eligible msg
            getNextElligibleMessage(iDom);
          }
        } // phase ==1
      } // while (nextclchange)
      if (cLSearchCtxt.searchPhase==2)
      {
        clDomCtxtsToString("In getGeneralizedNextMessage (persistent): " +
        "looking for the generalized oldest change");
        for (int ido=0; ido<clDomCtxts.length; ido++)
        {
          // get next msg
          getNextElligibleMessage(ido);
        }
        // take the oldest one
        iDom = getGeneralizedOldestChange();
        if (iDom != -1)
        {
          String suffix = this.clDomCtxts[iDom].rsd.getBaseDn();
          theOldestChange = new ECLUpdateMsg(
              (LDAPUpdateMsg)clDomCtxts[iDom].nextMsg,
              null, // set later
              suffix);
          clDomCtxts[iDom].currentState.update(
              theOldestChange.getUpdateMsg().getChangeNumber());
          /* TODO:ECL G Good changelog draft compat.
          if (cLSearchCtxt.nextSeqnum!=-1)
          {
            // should generate seqnum
            // store in seqnumdb the pair
            // (seqnum of the current change, state before this change)
            replicationServerDomain.addSeqnum(
                cLSearchCtxt.nextSeqnum,
                getGenState(),
                clDomCtxts[iDom].rsd.getBaseDn().toNormalizedString(),
                theOldestChange.getChangeNumber());
            theOldestChange.setSeqnum(cLSearchCtxt.nextSeqnum);
            cLSearchCtxt.nextSeqnum++;
          }
          */
        }
      }
    }
    catch(Exception e)
    {
      TRACER.debugCaught(DebugLogLevel.ERROR, e);
      throw new DirectoryException(
          ResultCode.OPERATIONS_ERROR,
          Message.raw(Category.SYNC, Severity.INFORMATION,"Exception raised: "),
          e);
    }
    if (theOldestChange != null)
    {
      // Update the current state
      this.cLSearchCtxt.currentCookie.update(
          theOldestChange.getServiceId(),
          theOldestChange.getUpdateMsg().getChangeNumber());
      // Set the current value of global state in the returned message
      theOldestChange.setCookie(this.cLSearchCtxt.currentCookie);
    }
    return theOldestChange;
  }
  /**
   * Terminates the first (non persistent) phase of the search on the ECL.
   */
  private void closePhase1()
  {
    // starvation of changelog messages
    // all domain have been unactived means are covered
    if (debugEnabled())
    TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer().
        getMonitorInstanceName() + "," + this + " closePhase1()"
        + " searchCtxt=" + cLSearchCtxt);
    // go to persistent phase if one
    for (int i=0; i<clDomCtxts.length; i++)
      clDomCtxts[i].active = true;
    if (this.cLSearchCtxt.isPersistent != StartECLSessionMsg.NON_PERSISTENT)
    {
      // phase = 1 done AND persistent search => goto phase 2
      cLSearchCtxt.searchPhase=2;
      if (writer ==null)
      {
        writer = new ECLServerWriter(session,this,replicationServerDomain);
        writer.start();  // start suspended
      }
    }
    else
    {
      // phase = 1 done AND !persistent search => reinit to phase 0
      cLSearchCtxt.searchPhase=0;
    }
    /* TODO:ECL G Good changelog draft compat.
    if (seqnumDbReadIterator!=null)
    {
      // End of phase 1 => always release the seqnum iterator
      seqnumDbReadIterator.releaseCursor();
      seqnumDbReadIterator = null;
    }
     */
  }
  /**
   * Get the oldest change contained in the subx table.
   * The subx table should be populated before
   * @return the oldest change.
   */
  private int getGeneralizedOldestChange()
  {
    int oldest = -1;
    for (int i=0; i<clDomCtxts.length; i++)
    {
      if ((clDomCtxts[i].active))
      {
        // on the first loop, oldest==-1
        // .msg is null when the previous (non blocking) nextMessage did
        // not have any eligible msg to return
        if (clDomCtxts[i].nextMsg != null)
        {
          if ((oldest==-1) ||
              (clDomCtxts[i].nextMsg.compareTo(clDomCtxts[oldest].nextMsg)<0))
          {
            oldest = i;
          }
        }
      }
    }
    TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer().
        getMonitorInstanceName()
        + "," + this + " getGeneralizedOldestChange() " +
        " returns " +
        ((oldest!=-1)?clDomCtxts[oldest].nextMsg:""));
    return oldest;
  }
  /**
   * Get from the provided domain, the next message elligible regarding
   * the crossDomain elligible CN. Put it in the context table.
   * @param idomain the provided domain.
   */
  private void getNextElligibleMessage(int idomain)
  {
    ChangeNumber crossDomainElligibleCN = computeCrossDomainElligibleCN();
    try
    {
      if (clDomCtxts[idomain].nonElligiblemsg != null)
      {
        TRACER.debugInfo("getNextElligibleMessage tests if the already " +
            " stored nonElligibleMsg has becoem elligible regarding " +
            " the crossDomainElligibleCN ("+crossDomainElligibleCN +
            " ) " +
            clDomCtxts[idomain].nonElligiblemsg.getChangeNumber().
            older(crossDomainElligibleCN));
        // we already got the oldest msg and it was not elligible
        if (clDomCtxts[idomain].nonElligiblemsg.getChangeNumber().
            older(crossDomainElligibleCN))
        {
          // it is now elligible
          clDomCtxts[idomain].nextMsg = clDomCtxts[idomain].nonElligiblemsg;
          clDomCtxts[idomain].nonElligiblemsg = null;
        }
        else
        {
          // the oldest is still not elligible - let's wait next
        }
      }
      else
      {
        // non blocking
        UpdateMsg newMsg = clDomCtxts[idomain].mh.getnextMessage(false);
        TRACER.debugInfo(this +
            " getNextElligibleMessage got the next changelogmsg "
            + " from " + clDomCtxts[idomain].mh.getServiceId()
            + " newCLMsg=" + newMsg);
        clDomCtxts[idomain].nextMsg =
          clDomCtxts[idomain].nonElligiblemsg = null;
        // in non blocking mode, return null when no more msg
        if (newMsg != null)
        {
          /* TODO:ECL Take into account eligibility.
          TRACER.debugInfo("getNextElligibleMessage is "
              + newMsg.getChangeNumber()
              + new Date(newMsg.getChangeNumber().getTime()).toString()
              + " elligible "
              + newMsg.getChangeNumber().older(crossDomainElligibleCN));
          if (newMsg.getChangeNumber().older(crossDomainElligibleCN))
          {
            // is elligible
            clDomCtxts[idomain].nextMsg = newMsg;
          }
          else
          {
            // is not elligible
            clDomCtxts[idomain].nonElligiblemsg = newMsg;
          }
          */
          clDomCtxts[idomain].nextMsg = newMsg;
        }
      }
    }
    catch(Exception e)
    {
      TRACER.debugCaught(DebugLogLevel.ERROR, e);
    }
  }
  /*
   */
  ECLEligibleCNComputerThread eligibleCNComputerThread = null;
  ChangeNumber liveecn;
  private ChangeNumber computeCrossDomainElligibleCN()
  {
    return liveecn;
  }
  /**
   * This class specifies the thread that computes periodically
   * the cross domain eligible CN.
   */
  private final class ECLEligibleCNComputerThread
    extends DirectoryThread
  {
    /**
     * The tracer object for the debug logger.
     */
    private boolean shutdown = false;
    private ECLEligibleCNComputerThread()
    {
      super("ECL eligible CN computer thread");
      start();
    }
    public void run()
    {
      while (shutdown == false)
      {
        try {
          synchronized (this)
          {
            liveecn = computeNewCrossDomainElligibleCN();
            try
            {
              this.wait(1000);
            } catch (InterruptedException e)
            { }
          }
        } catch (Exception end)
        {
          break;
        }
      }
    }
    private ChangeNumber computeNewCrossDomainElligibleCN()
    {
      ChangeNumber computedCrossDomainElligibleCN = null;
      String s = "=> ";
      ReplicationServer rs = replicationServerDomain.getReplicationServer();
      TRACER.debugInfo("ECLSH.computeNewCrossDomainElligibleCN() "
          + " periodic starts rs="+rs);
      Iterator<ReplicationServerDomain> rsdi = rs.getCacheIterator();
      if (rsdi != null)
      {
        while (rsdi.hasNext())
        {
          ReplicationServerDomain domain = rsdi.next();
          if (domain.getBaseDn().equalsIgnoreCase("cn=changelog"))
            continue;
          ChangeNumber domainElligibleCN = computeEligibleCN(domain);
          if (domainElligibleCN==null)
            continue;
          if ((computedCrossDomainElligibleCN == null) ||
              (domainElligibleCN.older(computedCrossDomainElligibleCN)))
          {
            computedCrossDomainElligibleCN = domainElligibleCN;
          }
          s += "\n DN:" + domain.getBaseDn()
          + "\t\t domainElligibleCN :" + domainElligibleCN
          + "/" +
          new Date(domainElligibleCN.getTime()).toString();
        }
      }
      TRACER.debugInfo("SH.computeNewCrossDomainElligibleCN() periodic " +
          " ends with " +
          " the following domainElligibleCN for each domain :" + s +
          "\n thus CrossDomainElligibleCN=" + computedCrossDomainElligibleCN +
          "  ts=" +
          new Date(computedCrossDomainElligibleCN.getTime()).toString());
      return computedCrossDomainElligibleCN;
    }
  }
  /**
   * Compute the eligible CN.
   * @param rsd The provided replication server domain for which we want
   * to retrieve the eligible date.
   * @return null if the domain does not play in eligibility.
   */
  public ChangeNumber computeEligibleCN(ReplicationServerDomain rsd)
  {
    ChangeNumber elligibleCN = null;
    ServerState heartbeatState = rsd.getHeartbeatState();
    if (heartbeatState==null)
      return null;
    // compute elligible CN
    ServerState hbState = heartbeatState.duplicate();
    Iterator<Short> it = hbState.iterator();
    while (it.hasNext())
    {
      short sid = it.next();
      ChangeNumber storedCN = hbState.getMaxChangeNumber(sid);
      // If the most recent UpdateMsg or CLHeartbeatMsg received is very old
      // then the server is considered down and not considered for eligibility
      if (TimeThread.getTime()-storedCN.getTime()>2000)
      {
        TRACER.debugInfo(
            "For RSD." + rsd.getBaseDn() + " Server " + sid
            + " is not considered for eligibility ... potentially down");
        continue;
      }
      if ((elligibleCN == null) || (storedCN.older(elligibleCN)))
      {
        elligibleCN = storedCN;
      }
    }
    TRACER.debugInfo(
        "For RSD." + rsd.getBaseDn() + " ElligibleCN()=" + elligibleCN);
    return elligibleCN;
  }
  /**
   * Returns the client operation id.
   * @return The client operation id.
   */
  public String getOperationId()
  {
    return operationId;
  }
  /**
   * Getter for the persistent property of the current search.
   * @return Whether the current search is persistent or not.
   */
  public short isPersistent() {
    return this.cLSearchCtxt.isPersistent;
  }
  /**
   * Getter for the current search phase (INIT or PERSISTENT).
   * @return Whether the current search is persistent or not.
   */
  public int getSearchPhase() {
    return this.cLSearchCtxt.searchPhase;
  }
}
opends/src/server/org/opends/server/replication/server/ECLServerWriter.java
New file
@@ -0,0 +1,306 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.server;
import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import java.io.IOException;
import java.net.SocketException;
import org.opends.messages.Message;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PersistentSearch;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.replication.protocol.DoneMsg;
import org.opends.server.replication.protocol.ECLUpdateMsg;
import org.opends.server.replication.protocol.ProtocolSession;
import org.opends.server.replication.protocol.StartECLSessionMsg;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.workflowelement.externalchangelog.ECLSearchOperation;
import org.opends.server.workflowelement.externalchangelog.ECLWorkflowElement;
/**
 * This class defines a server writer, which is used to send changes to a
 * directory server.
 */
public class ECLServerWriter extends ServerWriter
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();
  private ProtocolSession session;
  private ECLServerHandler handler;
  private ReplicationServerDomain replicationServerDomain;
  private short protocolVersion = -1;
  private boolean suspended;
  private boolean shutdown;
  /**
   * Create a ServerWriter.
   *
   * @param session     the ProtocolSession that will be used to send updates.
   * @param handler     ECL handler for which the ServerWriter is created.
   * @param replicationServerDomain the ReplicationServerDomain of this
   *                    ServerWriter.
   */
  public ECLServerWriter(ProtocolSession session, ECLServerHandler handler,
      ReplicationServerDomain replicationServerDomain)
  {
    super(session, (short)-1, handler, replicationServerDomain);
    setName("Replication ECL Writer Thread for op:" +
        handler.getOperationId());
    this.session = session;
    this.handler = handler;
    this.replicationServerDomain = replicationServerDomain;
    // Keep protocol version locally for efficiency
    this.protocolVersion = handler.getProtocolVersion();
    this.suspended = false;
    this.shutdown = false;
  }
  /**
   * The writer will start suspended by the Handler for the CL
   * waiting for the startCLSessionMsg. Then it may be
   * suspended between 2 jobs, each job being a separate search.
   */
  public synchronized void suspendWriter()
  {
    synchronized(this)
    {
      suspended = true;
    }
  }
  /**
   * Resume the writer.
   */
  public synchronized void resumeWriter()
  {
    synchronized(this)
    {
      suspended = false;
    }
    notify();
  }
  /**
   * Run method for the ServerWriter.
   * Loops waiting for changes from the ReplicationServerDomain and
   * forward them to the other servers
   */
  public void run()
  {
    Message errMessage = null;
    try
    {
      while (true)
      {
        // wait to be resumed or shutdowned
        if ((suspended) && (!shutdown))
        {
          synchronized(this)
          {
            wait();
          }
        }
        if (shutdown)
        {
          return;
        }
        // Not suspended
        doIt();
        if (shutdown)
        {
          return;
        }
        suspendWriter();
      }
    }
    catch (SocketException e)
    {
      // Just ignore the exception and let the thread die as well
      errMessage = NOTE_SERVER_DISCONNECT.get(handler.toString(),
          "for operation " + handler.getOperationId());
      logError(errMessage);
    }
    catch (Exception e)
    {
      // An unexpected error happened.
      // Log an error and close the connection.
      errMessage = ERR_WRITER_UNEXPECTED_EXCEPTION.get(handler.toString() +
          " " +  stackTraceToSingleLineString(e));
      logError(errMessage);
    }
    finally
    {
      if (session!=null)
      {
        try
        {
          session.close();
        } catch (IOException e)
        {
          // Can't do much more : ignore
        }
      }
      replicationServerDomain.stopServer(handler);
    }
  }
  /**
   * Loop gettting changes from the domain and publishing them either to
   * the provided session or to the ECL session interface.
   * @throws IOException when raised (connection closure)
   * @throws InterruptedException when raised
   */
  public void doIt()
  throws IOException, InterruptedException
  {
    ECLUpdateMsg update = null;
    while (true)
    {
      if (shutdown)
      {
        return;
      }
      if (suspended)
      {
        return;
      }
      // TODO:ECL please document the details of the cases where update could
      // be not null here
      if (update == null)
      {
        try
        {
          update = handler.takeECLUpdate();
        }
        catch(DirectoryException de)
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, de);
        }
      }
      if (update == null)
      {
        if (handler.getSearchPhase() != 1)
        {
          if (session!=null)
          {
            // session is null in pusherOnly mode
            // Done is used to end phase 1
            session.publish(new DoneMsg(
                replicationServerDomain.getReplicationServer().getServerId(),
                handler.getServerId()), protocolVersion);
          }
        }
        if (handler.isPersistent() == StartECLSessionMsg.NON_PERSISTENT)
        {
          // publishing is normally stopped here
          break;
        }
        else
        {
          // except if we are in persistent search
          Thread.sleep(200);
          continue;
        }
      }
      else
      {
        // Publish the update to the remote server using a protocol version he
        // it supports
        publish(update, protocolVersion);
        update = null;
      }
    }
}
  /**
   * Shutdown the writer.
   */
  public synchronized void shutdownWriter()
  {
    shutdown = true;
    this.notify();
  }
  /**
   * Publish a change either on the protocol session or to a persistent search.
   */
  private void publish(ECLUpdateMsg msg, short reqProtocolVersion)
  throws IOException
  {
    if (debugEnabled())
      TRACER.debugInfo(this + " publishes msg=[" + msg.toString() + "]");
    if (session!=null)
    {
      session.publish(msg, protocolVersion);
    }
    else
    {
      ECLWorkflowElement wfe = (ECLWorkflowElement)
      DirectoryServer.getWorkflowElement(
          ECLWorkflowElement.ECL_WORKFLOW_ELEMENT);
      // Notify persistent searches.
      for (PersistentSearch psearch : wfe.getPersistentSearches())
      {
        try
        {
          Entry eclEntry = ECLSearchOperation.createEntryFromMsg(msg);
          psearch.processAdd(eclEntry, -1);
        }
        catch(Exception e)
        {
          Message errMessage =
            ERR_WRITER_UNEXPECTED_EXCEPTION.get(handler.toString() +
              " " +  stackTraceToSingleLineString(e));
          logError(errMessage);
        }
      }
    }
  }
}
opends/src/server/org/opends/server/replication/server/ExternalChangeLogSessionImpl.java
New file
@@ -0,0 +1,85 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.server;
import org.opends.server.replication.common.ExternalChangeLogSession;
import org.opends.server.replication.protocol.ECLUpdateMsg;
import org.opends.server.replication.protocol.StartECLSessionMsg;
import org.opends.server.types.DirectoryException;
/**
 * This interface defines a session used to search the external changelog
 * in the Directory Server.
 */
public class ExternalChangeLogSessionImpl
  implements ExternalChangeLogSession
{
  ECLServerHandler handler;
  ReplicationServer rs;
  /**
   * Create a new external changelog session.
   * @param rs The replication server to which we will request the log.
   * @param startECLSessionMsg The start session message containing the
   *        details of the search request on the ECL.
   * @throws DirectoryException When an error occurs.
   */
  public ExternalChangeLogSessionImpl(
      ReplicationServer rs,
      StartECLSessionMsg startECLSessionMsg)
  throws DirectoryException
  {
    this.rs = rs;
    this.handler = new ECLServerHandler(
        rs.getServerURL(),
        rs.getServerId(),
        rs,
        startECLSessionMsg);
  }
  /**
   * Returns the next message available for the ECL (blocking)
   * null when none.
   * @return the next available message from the ECL.
   * @throws DirectoryException when needed.
   */
  public ECLUpdateMsg getNextUpdate()
  throws DirectoryException
  {
    return handler.getnextUpdate();
  }
  /**
   * Close the session.
   */
  public void close()
  {
    handler.getDomain().stopServer(handler);
  }
}
opends/src/server/org/opends/server/replication/server/LightweightServerHandler.java
@@ -64,7 +64,7 @@
  // The tracer object for the debug logger.
  private static final DebugTracer TRACER = getTracer();
  private ServerHandler replServerHandler;
  private ReplicationServerHandler replServerHandler;
  private ReplicationServerDomain rsDomain;
  // The id of the RS this DS is connected to
  private short replicationServerId = -1;
@@ -103,7 +103,7 @@
   * @param assuredMode The assured mode of the remote DS
   * @param safeDataLevel The safe data level of the remote DS
   */
  public LightweightServerHandler(ServerHandler replServerHandler,
  public LightweightServerHandler(ReplicationServerHandler replServerHandler,
    short replicationServerId, short serverId, long generationId, byte groupId,
    ServerStatus status, List<String> refUrls, boolean assuredFlag,
    AssuredMode assuredMode, byte safeDataLevel)
opends/src/server/org/opends/server/replication/server/MessageHandler.java
New file
@@ -0,0 +1,912 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.server;
import static org.opends.messages.ReplicationMessages.ERR_RS_DN_DOES_NOT_MATCH;
import static org.opends.server.loggers.debug.DebugLogger.*;
import java.util.ArrayList;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import org.opends.messages.Message;
import org.opends.server.admin.std.server.MonitorProviderCfg;
import org.opends.server.api.MonitorProvider;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.replication.common.ChangeNumber;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.types.Attribute;
import org.opends.server.types.Attributes;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import org.opends.server.types.ResultCode;
import org.opends.server.util.TimeThread;
/**
 * This class implements a buffering/producer/consumer mechanism of
 * replication changes (UpdateMsg) used inside the replication server.
 *
 * MessageHandlers are registered into Replication server domains.
 * When an update message is received by a domain, the domain forwards
 * the message to the registered message handlers.
 * Message are buffered into a queue.
 * Consumers are expected to come and consume the UpdateMsg from the queue.
 */
public class MessageHandler extends MonitorProvider<MonitorProviderCfg>
{
  /**
   * The tracer object for the debug logger.
   */
  protected static final DebugTracer TRACER = getTracer();
  /**
   * UpdateMsg queue.
   */
  private final MsgQueue msgQueue = new MsgQueue();
  /**
   * Late queue.
   */
  protected MsgQueue lateQueue = new MsgQueue();
  /**
   * Local hosting RS.
   */
  protected ReplicationServer replicationServer = null;
  /**
   * The URL of the hosting replication server.
   */
  protected String replicationServerURL = null;
  /**
   * The serverID of the hosting replication server.
   */
  protected short replicationServerId;
  /**
   * Specifies the related replication server domain based on serviceId(baseDn).
   */
  protected ReplicationServerDomain replicationServerDomain = null;
  /**
   * Number of update sent to the server.
   */
  protected int outCount = 0;
  /**
   * Number of updates received from the server.
   */
  protected int inCount = 0;
  /**
   * Specifies the max receive queue for this handler.
   */
  protected int maxReceiveQueue = 0;
  /**
   * Specifies the max send queue for this handler.
   */
  protected int maxSendQueue = 0;
  /**
   * Specifies the max receive delay for this handler.
   */
  protected int maxReceiveDelay = 0;
  /**
   * Specifies the max send delay for this handler.
   */
  protected int maxSendDelay = 0;
  /**
   * Specifies the max queue size for this handler.
   */
  protected int maxQueueSize = 5000;
  /**
   * Specifies the max queue size in bytes for this handler.
   */
  protected int maxQueueBytesSize = maxQueueSize * 100;
  /**
   * Specifies the max restart receive queue for this handler.
   */
  protected int restartReceiveQueue;
  /**
   * Specifies the max restart send queue for this handler.
   */
  protected int restartSendQueue;
  /**
   * Specifies the max restart receive delay for this handler.
   */
  protected int restartReceiveDelay;
  /**
   * Specifies the max restart send delay for this handler.
   */
  protected int restartSendDelay;
  /**
   * Specifies whether the consumer is following the producer (is not late).
   */
  protected boolean following = false;
  /**
   * Specifies the current serverState of this handler.
   */
  private ServerState serverState;
  /**
   * Specifies the identifier of the service (usually the baseDn of the domain).
   */
  private String serviceId = null;
  /**
   * Specifies whether the server is flow controlled and should be stopped from
   * sending messages.
   */
  protected boolean flowControl = false;
  /**
   * Specifies whether the consumer is still active or not.
   * If not active, the handler will not return any message.
   * Called at the beginning of shutdown process.
   */
  private boolean activeConsumer = true;
  /**
   * Set when ServerHandler is stopping.
   */
  private AtomicBoolean shuttingDown = new AtomicBoolean(false);
  /**
   * Creates a new server handler instance with the provided socket.
   * @param queueSize The maximum number of update that will be kept
   *                  in memory by this ServerHandler.
   * @param replicationServerURL The URL of the hosting replication server.
   * @param replicationServerId The ID of the hosting replication server.
   * @param replicationServer The hosting replication server.
   */
  public MessageHandler(
      int queueSize,
      String replicationServerURL,
      short replicationServerId,
      ReplicationServer replicationServer)
  {
    super("Message Handler");
    this.maxQueueSize = queueSize;
    this.maxQueueBytesSize = queueSize * 100;
    this.replicationServerURL = replicationServerURL;
    this.replicationServerId = replicationServerId;
    this.replicationServer = replicationServer;
  }
  /**
   * Add an update to the list of updates that must be sent to the server
   * managed by this Handler.
   *
   * @param update The update that must be added to the list of updates of
   * this handler.
   * @param sourceHandler The source handler that generated the update.
   */
  public void add(UpdateMsg update, MessageHandler sourceHandler)
  {
    synchronized (msgQueue)
    {
      /*
       * If queue was empty the writer thread was probably asleep
       * waiting for some changes, wake it up
       */
      if (msgQueue.isEmpty())
        msgQueue.notify();
      msgQueue.add(update);
      /* TODO : size should be configurable
       * and larger than max-receive-queue-size
       */
      while ((msgQueue.count() > maxQueueSize) ||
          (msgQueue.bytesCount() > maxQueueBytesSize))
      {
        setFollowing(false);
        msgQueue.removeFirst();
      }
    }
    if (isSaturated(update.getChangeNumber(), sourceHandler))
    {
      sourceHandler.setSaturated(true);
    }
  }
  /**
   * Set the shut down flag to true and returns the previous value of the flag.
   * @return The previous value of the shut down flag
   */
  public boolean engageShutdown()
  {
    // Use thread safe boolean
    return shuttingDown.getAndSet(true);
  }
  /**
   * Get an approximation of the delay by looking at the age of the oldest
   * message that has not been sent to this server.
   * This is an approximation because the age is calculated using the
   * clock of the server where the replicationServer is currently running
   * while it should be calculated using the clock of the server
   * that originally processed the change.
   *
   * The approximation error is therefore the time difference between
   *
   * @return the approximate delay for the connected server.
   */
  public long getApproxDelay()
  {
    long olderUpdateTime = getOlderUpdateTime();
    if (olderUpdateTime == 0)
      return 0;
    long currentTime = TimeThread.getTime();
    return ((currentTime - olderUpdateTime) / 1000);
  }
  /**
   * Get the age of the older change that has not yet been replicated
   * to the server handled by this ServerHandler.
   * @return The age if the older change has not yet been replicated
   *         to the server handled by this ServerHandler.
   */
  public Long getApproxFirstMissingDate()
  {
    Long result = (long) 0;
    // Get the older CN received
    ChangeNumber olderUpdateCN = getOlderUpdateCN();
    if (olderUpdateCN != null)
    {
      // If not present in the local RS db,
      // then approximate with the older update time
      result = olderUpdateCN.getTime();
    }
    return result;
  }
  /**
   * Returns the Replication Server Domain to which belongs this handler.
   * @param createIfNotExist Creates the domain if it does not exist.
   * @return The replication server domain.
   */
  public ReplicationServerDomain getDomain(boolean createIfNotExist)
  {
    if (replicationServerDomain==null)
    {
      replicationServerDomain =
      replicationServer.getReplicationServerDomain(serviceId,createIfNotExist);
    }
    return replicationServerDomain;
  }
  /**
   * Get the count of updates received from the server.
   * @return the count of update received from the server.
   */
  public int getInCount()
  {
    return inCount;
  }
  /**
   * Retrieves a set of attributes containing monitor data that should be
   * returned to the client if the corresponding monitor entry is requested.
   *
   * @return  A set of attributes containing monitor data that should be
   *          returned to the client if the corresponding monitor entry is
   *          requested.
   */
  @Override
  public ArrayList<Attribute> getMonitorData()
  {
    ArrayList<Attribute> attributes = new ArrayList<Attribute>();
    attributes.add(Attributes.create("handler", getMonitorInstanceName()));
    attributes.add(
        Attributes.create("queue-size", String.valueOf(msgQueue.count())));
    attributes.add(
        Attributes.create(
            "queue-size-bytes", String.valueOf(msgQueue.bytesCount())));
    attributes.add(
        Attributes.create(
            "following", String.valueOf(following)));
    return attributes;
  }
  /**
   * Retrieves the name of this monitor provider.  It should be unique among all
   * monitor providers, including all instances of the same monitor provider.
   *
   * @return  The name of this monitor provider.
   */
  @Override
  public String getMonitorInstanceName()
  {
    return "Message Handler";
  }
  /**
   * Get the next update that must be sent to the consumer
   * from the message queue or from the database.
   *
   * @param  synchronous specifies what to do when the queue is empty.
   *         when true, the method blocks; when false the method return null.
   * @return The next update that must be sent to the consumer.
   *         null when synchronous is false and queue is empty.
   */
  protected UpdateMsg getnextMessage(boolean synchronous)
  {
    UpdateMsg msg;
    while (activeConsumer == true)
    {
      if (following == false)
      {
        /* this server is late with regard to some other masters
         * in the topology or just joined the topology.
         * In such cases, we can't keep all changes in the queue
         * without saturating the memory, we therefore use
         * a lateQueue that is filled with a few changes from the changelogDB
         * If this server is able to close the gap, it will start using again
         * the regular msgQueue later.
         */
        if (lateQueue.isEmpty())
        {
          /*
           * Start from the server State
           * Loop until the queue high mark or until no more changes
           *   for each known LDAP master
           *      get the next CSN after this last one :
           *         - try to get next from the file
           *         - if not found in the file
           *             - try to get the next from the queue
           *   select the smallest of changes
           *   check if it is in the memory tree
           *     yes : lock memory tree.
           *           check all changes from the list, remove the ones that
           *           are already sent
           *           unlock memory tree
           *           restart as usual
           *   load this change on the delayList
           *
           */
          ReplicationIteratorComparator comparator =
            new ReplicationIteratorComparator();
          SortedSet<ReplicationIterator> iteratorSortedSet =
            new TreeSet<ReplicationIterator>(comparator);
          /* fill the lateQueue */
          for (short serverId : replicationServerDomain.getServers())
          {
            ChangeNumber lastCsn = serverState.getMaxChangeNumber(serverId);
            ReplicationIterator iterator =
              replicationServerDomain.getChangelogIterator(serverId, lastCsn);
            if (iterator != null)
            {
              if (iterator.getChange() != null)
              {
                iteratorSortedSet.add(iterator);
              } else
              {
                iterator.releaseCursor();
              }
            }
          }
          // The loop below relies on the fact that it is sorted based
          // on the currentChange of each iterator to consider the next
          // change across all servers.
          // Hence it is necessary to remove and eventual add again an iterator
          // when looping in order to keep consistent the order of the
          // iterators (see ReplicationIteratorComparator.
          while (!iteratorSortedSet.isEmpty() &&
              (lateQueue.count()<100) &&
              (lateQueue.bytesCount()<50000) )
          {
            ReplicationIterator iterator = iteratorSortedSet.first();
            iteratorSortedSet.remove(iterator);
            lateQueue.add(iterator.getChange());
            if (iterator.next())
              iteratorSortedSet.add(iterator);
            else
              iterator.releaseCursor();
          }
          for (ReplicationIterator iterator : iteratorSortedSet)
          {
            iterator.releaseCursor();
          }
          /*
           * Check if the first change in the lateQueue is also on the regular
           * queue
           */
          if (lateQueue.isEmpty())
          {
            synchronized (msgQueue)
            {
              if ((msgQueue.count() < maxQueueSize) &&
                  (msgQueue.bytesCount() < maxQueueBytesSize))
              {
                setFollowing(true);
              }
            }
          } else
          {
            msg = lateQueue.first();
            synchronized (msgQueue)
            {
              if (msgQueue.contains(msg))
              {
                /* we finally catch up with the regular queue */
                setFollowing(true);
                lateQueue.clear();
                UpdateMsg msg1;
                do
                {
                  msg1 = msgQueue.removeFirst();
                } while (!msg.getChangeNumber().equals(msg1.getChangeNumber()));
                this.updateServerState(msg);
                return msg;
              }
            }
          }
        } else
        {
          /* get the next change from the lateQueue */
          msg = lateQueue.removeFirst();
          this.updateServerState(msg);
          return msg;
        }
      }
      synchronized (msgQueue)
      {
        if (following == true)
        {
          try
          {
            while (msgQueue.isEmpty() && (following == true))
            {
              if (!synchronous)
                return null;
              msgQueue.wait(500);
              if (!activeConsumer)
                return null;
            }
          } catch (InterruptedException e)
          {
            return null;
          }
          msg = msgQueue.removeFirst();
          if (this.updateServerState(msg))
          {
            /*
             * Only push the message if it has not yet been seen
             * by the other server.
             * Otherwise just loop to select the next message.
             */
            return msg;
          }
        }
      }
      /*
       * Need to loop because following flag may have gone to false between
       * the first check at the beginning of this method
       * and the second check just above.
       */
    }
    return null;
  }
  /**
   * Get the older Change Number for that server.
   * Returns null when the queue is empty.
   * @return The older change number.
   */
  public ChangeNumber getOlderUpdateCN()
  {
    ChangeNumber result = null;
    synchronized (msgQueue)
    {
      if (isFollowing())
      {
        if (msgQueue.isEmpty())
        {
          result = null;
        } else
        {
          UpdateMsg msg = msgQueue.first();
          result = msg.getChangeNumber();
        }
      } else
      {
        if (lateQueue.isEmpty())
        {
          // isFollowing is false AND lateQueue is empty
          // We may be at the very moment when the writer has emptyed the
          // lateQueue when it sent the last update. The writer will fill again
          // the lateQueue when it will send the next update but we are not yet
          // there. So let's take the last change not sent directly from
          // the db.
          ReplicationIteratorComparator comparator =
            new ReplicationIteratorComparator();
          SortedSet<ReplicationIterator> iteratorSortedSet =
            new TreeSet<ReplicationIterator>(comparator);
          try
          {
            // Build a list of candidates iterator (i.e. db i.e. server)
            for (short serverId : replicationServerDomain.getServers())
            {
              // get the last already sent CN from that server
              ChangeNumber lastCsn = serverState.getMaxChangeNumber(serverId);
              // get an iterator in this server db from that last change
              ReplicationIterator iterator =
                replicationServerDomain.getChangelogIterator(serverId, lastCsn);
              // if that iterator has changes, then it is a candidate
              // it is added in the sorted list at a position given by its
              // current change (see ReplicationIteratorComparator).
              if ((iterator != null) && (iterator.getChange() != null))
              {
                iteratorSortedSet.add(iterator);
              }
            }
            UpdateMsg msg = iteratorSortedSet.first().getChange();
            result = msg.getChangeNumber();
          } catch (Exception e)
          {
            result = null;
          } finally
          {
            for (ReplicationIterator iterator : iteratorSortedSet)
            {
              iterator.releaseCursor();
            }
          }
        } else
        {
          UpdateMsg msg = lateQueue.first();
          result = msg.getChangeNumber();
        }
      }
    }
    return result;
  }
  /**
   * Get the older update time for that server.
   * @return The older update time.
   */
  public long getOlderUpdateTime()
  {
    ChangeNumber olderUpdateCN = getOlderUpdateCN();
    if (olderUpdateCN == null)
      return 0;
    return olderUpdateCN.getTime();
  }
  /**
   * Get the count of updates sent to this server.
   * @return  The count of update sent to this server.
   */
  public int getOutCount()
  {
    return outCount;
  }
  /**
   * Get the number of message in the receive message queue.
   * @return Size of the receive message queue.
   */
  public int getRcvMsgQueueSize()
  {
    synchronized (msgQueue)
    {
      /*
       * When the server is up to date or close to be up to date,
       * the number of updates to be sent is the size of the receive queue.
       */
      if (isFollowing())
        return msgQueue.count();
      else
      {
        /**
         * When the server  is not able to follow, the msgQueue
         * may become too large and therefore won't contain all the
         * changes. Some changes may only be stored in the backing DB
         * of the servers.
         * The total size of the receive queue is calculated by doing
         * the sum of the number of missing changes for every dbHandler.
         */
        ServerState dbState = replicationServerDomain.getDbServerState();
        return ServerState.diffChanges(dbState, serverState);
      }
    }
  }
  /**
   * Get the state of this server.
   *
   * @return ServerState the state for this server..
   */
  public ServerState getServerState()
  {
    return serverState;
  }
  /**
   * Get the name of the serviceId (usually baseDn) for this handler.
   * @return The name of the serviceId.
   */
  protected String getServiceId()
  {
    return serviceId;
  }
  /**
   * Retrieves the length of time in milliseconds that should elapse between
   * calls to the <CODE>updateMonitorData()</CODE> method.  A negative or zero
   * return value indicates that the <CODE>updateMonitorData()</CODE> method
   * should not be periodically invoked.
   *
   * @return  The length of time in milliseconds that should elapse between
   *          calls to the <CODE>updateMonitorData()</CODE> method.
   */
  @Override
  public long getUpdateInterval()
  {
    /* we don't wont to do polling on this monitor */
    return 0;
  }
  /**
   * Increase the counter of update received from the server.
   */
  public void incrementInCount()
  {
    inCount++;
  }
  /**
   * Increase the counter of updates sent to the server.
   */
  public void incrementOutCount()
  {
    outCount++;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public void initializeMonitorProvider(MonitorProviderCfg configuration)
  throws ConfigException, InitializationException
  {
    // Nothing to do for now
  }
  /**
   * Check if the LDAP server can follow the speed of the other servers.
   * @return true when the server has all the not yet sent changes
   *         in its queue.
   */
  public boolean isFollowing()
  {
    return following;
  }
  /**
   * Check is this server is saturated (this server has already been
   * sent a bunch of updates and has not processed them so they are staying
   * in the message queue for this server an the size of the queue
   * for this server is above the configured limit.
   *
   * The limit can be defined in number of updates or with a maximum delay
   *
   * @param changeNumber The changenumber to use to make the delay calculations.
   * @param sourceHandler The ServerHandler which is sending the update.
   * @return true is saturated false if not saturated.
   */
  public boolean isSaturated(ChangeNumber changeNumber,
      MessageHandler sourceHandler)
  {
    synchronized (msgQueue)
    {
      int size = msgQueue.count();
      if ((maxReceiveQueue > 0) && (size >= maxReceiveQueue))
        return true;
      if ((sourceHandler.maxSendQueue > 0) &&
          (size >= sourceHandler.maxSendQueue))
        return true;
      if (!msgQueue.isEmpty())
      {
        UpdateMsg firstUpdate = msgQueue.first();
        if (firstUpdate != null)
        {
          long timeDiff = changeNumber.getTimeSec() -
          firstUpdate.getChangeNumber().getTimeSec();
          if ((maxReceiveDelay > 0) && (timeDiff >= maxReceiveDelay))
            return true;
          if ((sourceHandler.maxSendDelay > 0) &&
              (timeDiff >= sourceHandler.maxSendDelay))
            return true;
        }
      }
      return false;
    }
  }
  /**
   * Check that the size of the Server Handler messages Queue has lowered
   * below the limit and therefore allowing the reception of messages
   * from other servers to restart.
   * @param source The ServerHandler which was sending the update.
   *        can be null.
   * @return true if the processing can restart
   */
  public boolean restartAfterSaturation(MessageHandler source)
  {
    synchronized (msgQueue)
    {
      int queueSize = msgQueue.count();
      if ((maxReceiveQueue > 0) && (queueSize >= restartReceiveQueue))
        return false;
      if ((source != null) && (source.maxSendQueue > 0) &&
          (queueSize >= source.restartSendQueue))
        return false;
      if (!msgQueue.isEmpty())
      {
        UpdateMsg firstUpdate = msgQueue.first();
        UpdateMsg lastUpdate = msgQueue.last();
        if ((firstUpdate != null) && (lastUpdate != null))
        {
          long timeDiff = lastUpdate.getChangeNumber().getTimeSec() -
          firstUpdate.getChangeNumber().getTimeSec();
          if ((maxReceiveDelay > 0) && (timeDiff >= restartReceiveDelay))
            return false;
          if ((source != null) && (source.maxSendDelay > 0) && (timeDiff >=
            source.restartSendDelay))
            return false;
        }
      }
    }
    return true;
  }
  /**
   * Set that the consumer is now becoming inactive and thus getNextMessage
   * should not return any UpdateMsg any more.
   * @param active the provided state of the consumer.
   */
  public void setConsumerActive(boolean active)
  {
    this.activeConsumer = active;
  }
  /**
   * Set the following flag of this server.
   * @param following the value that should be set.
   */
  private void setFollowing(boolean following)
  {
    this.following = following;
  }
  private void setSaturated(boolean value)
  {
    flowControl = value;
  }
  /**
   * Set the initial value of the serverState for this handler.
   * Expected to be done once, then the state will be updated using
   * updateServerState() method.
   * @param serverState the provided serverState.
   * @exception DirectoryException raised when a problem occurs.
   */
  public void setInitialServerState(ServerState serverState)
  throws DirectoryException
  {
    this.serverState = serverState;
  }
  /**
   * Set the serviceId (usually baseDn) for this handler. Expected to be done
   * once and never changed during the handler life.
   * @param serviceId The provided serviceId.
   * @exception DirectoryException raised when a problem occurs.
   */
  protected void setServiceIdAndDomain(String serviceId)
  throws DirectoryException
  {
    if (this.serviceId != null)
    {
      if (!this.serviceId.equalsIgnoreCase(serviceId))
      {
        Message message = ERR_RS_DN_DOES_NOT_MATCH.get(
            this.serviceId.toString(),
            serviceId.toString());
        throw new DirectoryException(ResultCode.OTHER,
            message, null);
      }
    }
    else
    {
      this.serviceId = serviceId;
      this.replicationServerDomain = getDomain(true);
    }
  }
  /**
   * Shutdown this handler.
   */
  public void shutdown()
  {
    /*
     * Shutdown ServerWriter
     */
    synchronized (msgQueue)
    {
      msgQueue.clear();
      msgQueue.notify();
      msgQueue.notifyAll();
    }
    DirectoryServer.deregisterMonitorProvider(getMonitorInstanceName());
  }
  /**
   * Performs any processing periodic processing that may be desired to update
   * the information associated with this monitor.  Note that best-effort
   * attempts will be made to ensure that calls to this method come
   * <CODE>getUpdateInterval()</CODE> milliseconds apart, but no guarantees will
   * be made.
   */
  @Override
  public void updateMonitorData()
  {
    // As long as getUpdateInterval() returns 0, this will never get called
  }
  /**
   * Update the serverState with the last message sent.
   *
   * @param msg the last update sent.
   * @return boolean indicating if the update was meaningful.
   */
  public boolean updateServerState(UpdateMsg msg)
  {
    return serverState.update(msg.getChangeNumber());
  }
  /**
   * Get the groupId of the hosting RS.
   * @return the group id.
   */
  public byte getLocalGroupId()
  {
    return replicationServer.getGroupId();
  }
}
opends/src/server/org/opends/server/replication/server/ReplicationServer.java
@@ -42,16 +42,18 @@
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import org.opends.messages.Category;
import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import org.opends.messages.Severity;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.ReplicationServerCfg;
import org.opends.server.api.Backend;
@@ -61,13 +63,22 @@
import org.opends.server.api.RestoreTaskListener;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.WorkflowImpl;
import org.opends.server.core.networkgroups.NetworkGroup;
import org.opends.server.loggers.LogLevel;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.replication.common.ExternalChangeLogSession;
import org.opends.server.replication.protocol.ProtocolSession;
import org.opends.server.replication.protocol.ReplServerStartMsg;
import org.opends.server.replication.protocol.ReplSessionSecurity;
import org.opends.server.replication.protocol.ReplicationMsg;
import org.opends.server.replication.protocol.ServerStartECLMsg;
import org.opends.server.replication.protocol.ServerStartMsg;
import org.opends.server.replication.protocol.StartECLSessionMsg;
import org.opends.server.types.BackupConfig;
import org.opends.server.types.ConfigChangeResult;
import org.opends.server.types.DN;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.LDIFExportConfig;
@@ -75,7 +86,9 @@
import org.opends.server.types.RestoreConfig;
import org.opends.server.types.ResultCode;
import org.opends.server.util.LDIFReader;
import org.opends.server.util.ServerConstants;
import org.opends.server.util.TimeThread;
import org.opends.server.workflowelement.externalchangelog.ECLWorkflowElement;
import com.sleepycat.je.DatabaseException;
@@ -155,6 +168,8 @@
   */
  private static final DebugTracer TRACER = getTracer();
  private String externalChangeLogWorkflowID = "External Changelog Workflow ID";
  ECLWorkflowElement eclwe;
  private static HashSet<Integer> localPorts = new HashSet<Integer>();
  /**
@@ -267,9 +282,35 @@
             ReplSessionSecurity.HANDSHAKE_TIMEOUT);
        if (session == null) // Error, go back to accept
          continue;
        ServerHandler handler = new ServerHandler(session, queueSize);
        handler.start(null, serverId, serverURL, rcvWindow,
                      false, this);
        ReplicationMsg msg = session.receive();
        if (msg instanceof ServerStartMsg)
        {
          DataServerHandler handler = new DataServerHandler(session,
              queueSize,serverURL,serverId,this,rcvWindow);
          handler.startFromRemoteDS((ServerStartMsg)msg);
        }
        else if (msg instanceof ReplServerStartMsg)
        {
          ReplicationServerHandler handler = new ReplicationServerHandler(
              session,queueSize,serverURL,serverId,this,rcvWindow);
          handler.startFromRemoteRS((ReplServerStartMsg)msg);
        }
        else if (msg instanceof ServerStartECLMsg)
        {
          ECLServerHandler handler = new ECLServerHandler(
              session,queueSize,serverURL,serverId,this,rcvWindow);
          handler.startFromRemoteServer((ServerStartECLMsg)msg);
        }
        else
        {
          // We did not recognize the message, close session as what
          // can happen after is undetermined and we do not want the server to
          // be disturbed
          ServerHandler.closeSession(session, null, null);
          return;
        }
      }
      catch (Exception e)
      {
@@ -277,6 +318,10 @@
        // shutdown or changing the port number process.
        // Just log debug information and loop.
        // Do not log the message during shutdown.
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
        if (shutdown == false) {
          Message message =
            ERR_EXCEPTION_LISTENING.get(e.getLocalizedMessage());
@@ -379,20 +424,20 @@
  /**
   * Establish a connection to the server with the address and port.
   *
   * @param serverURL  The address and port for the server, separated by a
   * @param remoteServerURL  The address and port for the server, separated by a
   *                    colon.
   * @param baseDn     The baseDn of the connection
   */
  private void connect(String serverURL, String baseDn)
  private void connect(String remoteServerURL, String baseDn)
  {
    int separator = serverURL.lastIndexOf(':');
    String port = serverURL.substring(separator + 1);
    String hostname = serverURL.substring(0, separator);
    boolean sslEncryption = replSessionSecurity.isSslEncryption(serverURL);
    int separator = remoteServerURL.lastIndexOf(':');
    String port = remoteServerURL.substring(separator + 1);
    String hostname = remoteServerURL.substring(0, separator);
    boolean sslEncryption =replSessionSecurity.isSslEncryption(remoteServerURL);
    if (debugEnabled())
      TRACER.debugInfo("RS " + this.getMonitorInstanceName() +
               " connects to " + serverURL);
               " connects to " + remoteServerURL);
    try
    {
@@ -402,12 +447,25 @@
      socket.setTcpNoDelay(true);
      socket.connect(ServerAddr, 500);
      /*
      ServerHandler handler = new ServerHandler(
           replSessionSecurity.createClientSession(serverURL, socket,
           ReplSessionSecurity.HANDSHAKE_TIMEOUT),
           queueSize);
      handler.start(baseDn, serverId, this.serverURL, rcvWindow,
                    sslEncryption, this);
      */
      ReplicationServerHandler handler = new ReplicationServerHandler(
          replSessionSecurity.createClientSession(remoteServerURL,
              socket,
              ReplSessionSecurity.HANDSHAKE_TIMEOUT),
              queueSize,
              this.serverURL,
              serverId,
              this,
              rcvWindow);
      handler.connect(baseDn, sslEncryption);
    }
    catch (Exception e)
    {
@@ -470,6 +528,10 @@
        serverId , this);
      listenThread.start();
      // Initialize the External Changelog
      // FIXME: how is WF creation enabed/disabled in the RS ?
      initializeECL();
      if (debugEnabled())
        TRACER.debugInfo("RS " +getMonitorInstanceName()+
            " successfully initialized");
@@ -493,10 +555,88 @@
      Message message =
          ERR_COULD_NOT_BIND_CHANGELOG.get(changelogPort, e.getMessage());
      logError(message);
    } catch (DirectoryException e)
    {
      //FIXME:DirectoryException is raised by initializeECL => fix err msg
      Message message = Message.raw(Category.SYNC, Severity.SEVERE_ERROR,
        "Directory Exception raised by ECL initialization: " + e.getMessage());
      logError(message);
    }
  }
  /**
   * Initializes the ECL access by creating a dedicated workflow element.
   * @throws DirectoryException
   */
  private void initializeECL()
  throws DirectoryException
  {
    WorkflowImpl externalChangeLogWorkflow;
    if (WorkflowImpl.getWorkflow(externalChangeLogWorkflowID)
        !=null)
      return;
    ECLWorkflowElement eclwe = new ECLWorkflowElement(this);
    // Create the workflow for the base DN and register the workflow with
    // the server.
    externalChangeLogWorkflow = new WorkflowImpl(
        externalChangeLogWorkflowID,
        DN.decode(ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT),
        eclwe.getWorkflowElementID(),
        eclwe);
    externalChangeLogWorkflow.register();
    NetworkGroup defaultNetworkGroup = NetworkGroup.getDefaultNetworkGroup();
    defaultNetworkGroup.registerWorkflow(externalChangeLogWorkflow);
    // FIXME:ECL should the ECL Workflow be registered in adminNetworkGroup?
    NetworkGroup adminNetworkGroup = NetworkGroup.getAdminNetworkGroup();
    adminNetworkGroup.registerWorkflow(externalChangeLogWorkflow);
    // FIXME:ECL should the ECL Workflow be registered in internalNetworkGroup?
    NetworkGroup internalNetworkGroup = NetworkGroup.getInternalNetworkGroup();
    internalNetworkGroup.registerWorkflow(externalChangeLogWorkflow);
}
  private void finalizeECL()
  {
    WorkflowImpl eclwf =
      (WorkflowImpl)WorkflowImpl.getWorkflow(externalChangeLogWorkflowID);
    // do it only if not already done by another RS (unit test case)
    // if (DirectoryServer.getWorkflowElement(externalChangeLogWorkflowID)
    if (eclwf!=null)
    {
    // FIXME:ECL should the ECL Workflow be registered in internalNetworkGroup?
    NetworkGroup internalNetworkGroup = NetworkGroup.getInternalNetworkGroup();
    internalNetworkGroup.deregisterWorkflow(externalChangeLogWorkflowID);
    // FIXME:ECL should the ECL Workflow be registered in adminNetworkGroup?
    NetworkGroup adminNetworkGroup = NetworkGroup.getAdminNetworkGroup();
    adminNetworkGroup.deregisterWorkflow(externalChangeLogWorkflowID);
    NetworkGroup defaultNetworkGroup = NetworkGroup.getDefaultNetworkGroup();
    defaultNetworkGroup.deregisterWorkflow(externalChangeLogWorkflowID);
    eclwf.deregister();
    eclwf.finalizeWorkflow();
    }
    eclwe = (ECLWorkflowElement)
    DirectoryServer.getWorkflowElement("EXTERNAL CHANGE LOG");
    if (eclwe!=null)
    {
      DirectoryServer.deregisterWorkflowElement(eclwe);
      eclwe.finalizeWorkflowElement();
    }
}
  /**
   * Get the ReplicationServerDomain associated to the base DN given in
   * parameter.
   *
@@ -571,6 +711,8 @@
    {
      dbEnv.shutdown();
    }
    finalizeECL();
}
@@ -1220,6 +1362,38 @@
  }
  /**
   * Returns the number of domains managed by this replication server.
   * @return the number of domains managed.
   */
  public int getCacheSize()
  {
    return baseDNs.size();
  }
  /**
   * Create a new session to get the ECL.
   * @param msg The message that specifies the ECL request.
   * @return Returns the created session.
   * @throws DirectoryException When an error occurs.
   */
  public ExternalChangeLogSession createECLSession(StartECLSessionMsg msg)
  throws DirectoryException
  {
    ExternalChangeLogSessionImpl session =
      new ExternalChangeLogSessionImpl(this, msg);
    return session;
  }
  /**
   * Getter on the server URL.
   * @return the server URL.
   */
  public String getServerURL()
  {
    return this.serverURL;
  }
  /**
   * This method allows to check if the Replication Server given
   * as the parameter is running in the local JVM.
   *
opends/src/server/org/opends/server/replication/server/ReplicationServerDomain.java
@@ -26,56 +26,59 @@
 */
package org.opends.server.replication.server;
import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import static org.opends.server.loggers.debug.DebugLogger.*;
import org.opends.server.admin.std.server.MonitorProviderCfg;
import org.opends.server.api.MonitorProvider;
import org.opends.server.core.DirectoryServer;
import org.opends.server.loggers.debug.DebugTracer;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.Iterator;
import java.util.concurrent.locks.ReentrantLock;
import org.opends.messages.Category;
import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import org.opends.messages.Severity;
import org.opends.server.admin.std.server.MonitorProviderCfg;
import org.opends.server.api.MonitorProvider;
import org.opends.server.core.DirectoryServer;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.replication.common.AssuredMode;
import org.opends.server.replication.common.ChangeNumber;
import org.opends.server.replication.common.DSInfo;
import org.opends.server.replication.common.RSInfo;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.common.ServerStatus;
import org.opends.server.replication.common.StatusMachineEvent;
import org.opends.server.replication.protocol.AckMsg;
import org.opends.server.replication.protocol.ChangeStatusMsg;
import org.opends.server.replication.protocol.ErrorMsg;
import org.opends.server.replication.protocol.RoutableMsg;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.replication.protocol.TopologyMsg;
import org.opends.server.replication.protocol.MonitorMsg;
import org.opends.server.replication.protocol.MonitorRequestMsg;
import org.opends.server.replication.protocol.ProtocolVersion;
import org.opends.server.replication.protocol.ResetGenerationIdMsg;
import org.opends.server.replication.protocol.RoutableMsg;
import org.opends.server.replication.protocol.TopologyMsg;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.Attributes;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.ResultCode;
import org.opends.server.util.TimeThread;
import com.sleepycat.je.DatabaseException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.locks.ReentrantLock;
import org.opends.server.replication.common.AssuredMode;
import org.opends.server.replication.common.DSInfo;
import org.opends.server.replication.common.RSInfo;
import org.opends.server.replication.common.ServerStatus;
import org.opends.server.replication.common.StatusMachineEvent;
import org.opends.server.replication.protocol.AckMsg;
import org.opends.server.replication.protocol.ChangeStatusMsg;
import org.opends.server.replication.protocol.ProtocolVersion;
/**
 * This class define an in-memory cache that will be used to store
@@ -111,8 +114,8 @@
   * to this replication server.
   *
   */
  private final Map<Short, ServerHandler> directoryServers =
    new ConcurrentHashMap<Short, ServerHandler>();
  private final Map<Short, DataServerHandler> directoryServers =
    new ConcurrentHashMap<Short, DataServerHandler>();
  /*
   * This map contains one ServerHandler for each replication servers
@@ -123,8 +126,11 @@
   * We add new TreeSet in the HashMap when a new replication server register
   * to this replication server.
   */
  private final Map<Short, ServerHandler> replicationServers =
    new ConcurrentHashMap<Short, ServerHandler>();
  private final Map<Short, ReplicationServerHandler> replicationServers =
    new ConcurrentHashMap<Short, ReplicationServerHandler>();
  private final ConcurrentLinkedQueue<MessageHandler> otherHandlers =
    new ConcurrentLinkedQueue<MessageHandler>();
  /*
   * This map contains the List of updates received from each
@@ -134,16 +140,14 @@
    new ConcurrentHashMap<Short, DbHandler>();
  private ReplicationServer replicationServer;
  /* GenerationId management */
  // GenerationId management
  private long generationId = -1;
  private boolean generationIdSavedStatus = false;
  /**
   * The tracer object for the debug logger.
   */
  // The tracer object for the debug logger.
  private static final DebugTracer TRACER = getTracer();
  /* Monitor data management */
  // Monitor data management
  /**
   * The monitor data consolidated over the topology.
   */
@@ -346,9 +350,9 @@
    /*
     * Push the message to the replication servers
     */
    if (sourceHandler.isLDAPserver())
    if (sourceHandler.isDataServer())
    {
      for (ServerHandler handler : replicationServers.values())
      for (ReplicationServerHandler handler : replicationServers.values())
      {
        /**
         * Ignore updates to RS with bad gen id
@@ -397,7 +401,7 @@
    /*
     * Push the message to the LDAP servers
     */
    for (ServerHandler handler : directoryServers.values())
    for (DataServerHandler handler : directoryServers.values())
    {
      // Don't forward the change to the server that just sent it
      if (handler == sourceHandler)
@@ -467,6 +471,14 @@
        handler.add(update, sourceHandler);
      }
    }
    // Push the message to the other subscribing handlers
    Iterator<MessageHandler> otherIter = otherHandlers.iterator();
    while (otherIter.hasNext())
    {
      MessageHandler handler = otherIter.next();
      handler.add(update, sourceHandler);
    }
  }
  /**
@@ -522,10 +534,10 @@
    if (sourceGroupId == groupId)
      // Assured feature does not cross different group ids
    {
      if (sourceHandler.isLDAPserver())
      if (sourceHandler.isDataServer())
      {
        // Look for RS eligible for assured
        for (ServerHandler handler : replicationServers.values())
        for (ReplicationServerHandler handler : replicationServers.values())
        {
          if (handler.getGroupId() == groupId)
            // No ack expected from a RS with different group id
@@ -541,7 +553,7 @@
      }
      // Look for DS eligible for assured
      for (ServerHandler handler : directoryServers.values())
      for (DataServerHandler handler : directoryServers.values())
      {
        // Don't forward the change to the server that just sent it
        if (handler == sourceHandler)
@@ -636,7 +648,7 @@
        (generationId == sourceHandler.getGenerationId()))
        // Ignore assured updates from wrong generationId servers
      {
        if (sourceHandler.isLDAPserver())
        if (sourceHandler.isDataServer())
        {
          if (safeDataLevel == (byte) 1)
          {
@@ -689,10 +701,10 @@
    List<Short> expectedServers = new ArrayList<Short>();
    if (interestedInAcks)
    {
      if (sourceHandler.isLDAPserver())
      if (sourceHandler.isDataServer())
      {
        // Look for RS eligible for assured
        for (ServerHandler handler : replicationServers.values())
        for (ReplicationServerHandler handler : replicationServers.values())
        {
          if (handler.getGroupId() == groupId)
            // No ack expected from a RS with different group id
@@ -879,7 +891,7 @@
            origServer.incrementAssuredSrReceivedUpdatesTimeout();
          } else
          {
            if (origServer.isLDAPserver())
            if (origServer.isDataServer())
            {
              origServer.incrementAssuredSdReceivedUpdatesTimeout();
            }
@@ -957,7 +969,7 @@
   */
  public void stopReplicationServers(Collection<String> replServers)
  {
    for (ServerHandler handler : replicationServers.values())
    for (ReplicationServerHandler handler : replicationServers.values())
    {
      if (replServers.contains(handler.getServerAddressURL()))
        stopServer(handler);
@@ -970,13 +982,13 @@
  public void stopAllServers()
  {
    // Close session with other replication servers
    for (ServerHandler serverHandler : replicationServers.values())
    for (ReplicationServerHandler serverHandler : replicationServers.values())
    {
      stopServer(serverHandler);
    }
    // Close session with other LDAP servers
    for (ServerHandler serverHandler : directoryServers.values())
    for (DataServerHandler serverHandler : directoryServers.values())
    {
      stopServer(serverHandler);
    }
@@ -988,14 +1000,13 @@
   * @param handler the DS we want to check
   * @return true if this is not a duplicate server
   */
  public boolean checkForDuplicateDS(ServerHandler handler)
  public boolean checkForDuplicateDS(DataServerHandler handler)
  {
    ServerHandler oldHandler = directoryServers.get(handler.getServerId());
    DataServerHandler oldHandler = directoryServers.get(handler.getServerId());
    if (directoryServers.containsKey(handler.getServerId()))
    {
      // looks like two LDAP servers have the same serverId
      // log an error message and drop this connection.
      Message message = ERR_DUPLICATE_SERVER_ID.get(
        replicationServer.getMonitorInstanceName(), oldHandler.toString(),
        handler.toString(), handler.getServerId());
@@ -1012,6 +1023,12 @@
   */
  public void stopServer(ServerHandler handler)
  {
      if (debugEnabled())
        TRACER.debugInfo(
            "In RS " + this.replicationServer.getMonitorInstanceName() +
            " domain=" + this +
            " stopServer(SH)" + handler.getMonitorInstanceName() +
          " " + stackTraceToSingleLineString(new Exception()));
    /*
     * We must prevent deadlock on replication server domain lock, when for
     * instance this code is called from dying ServerReader but also dying
@@ -1020,14 +1037,103 @@
     * or not (is already being processed or not).
     */
    if (!handler.engageShutdown())
    // Only do this once (prevent other thread to enter here again)
      // Only do this once (prevent other thread to enter here again)
    {
      if (debugEnabled())
        TRACER.debugInfo(
          "In RS " + this.replicationServer.getMonitorInstanceName() +
          " for " + baseDn + " " +
          " stopServer " + handler.getMonitorInstanceName());
      try
      {
        try
        {
          // Acquire lock on domain (see more details in comment of start()
          // method of ServerHandler)
          lock();
        } catch (InterruptedException ex)
        {
          // Try doing job anyway...
        }
        if (handler.isReplicationServer())
        {
          if (replicationServers.containsValue(handler))
          {
            unregisterServerHandler(handler);
            handler.shutdown();
            // Check if generation id has to be resetted
            mayResetGenerationId();
            // Warn our DSs that a RS or DS has quit (does not use this
            // handler as already removed from list)
            buildAndSendTopoInfoToDSs(null);
          }
        } else
        {
          if (directoryServers.containsValue(handler))
          {
            // If this is the last DS for the domain,
            // shutdown the status analyzer
            if (directoryServers.size() == 1)
            {
              if (debugEnabled())
                TRACER.debugInfo("In " +
                    replicationServer.getMonitorInstanceName() +
                    " remote server " + handler.getMonitorInstanceName() +
                " is the last DS to be stopped: stopping status analyzer");
              stopStatusAnalyzer();
            }
            unregisterServerHandler(handler);
            handler.shutdown();
            // Check if generation id has to be resetted
            mayResetGenerationId();
            // Update the remote replication servers with our list
            // of connected LDAP servers
            buildAndSendTopoInfoToRSs();
            // Warn our DSs that a RS or DS has quit (does not use this
            // handler as already removed from list)
            buildAndSendTopoInfoToDSs(null);
          }
          else if (otherHandlers.contains(handler))
          {
            unRegisterHandler(handler);
            handler.shutdown();
          }
        }
      }
      catch(Exception e)
      {
        logError(Message.raw(Category.SYNC, Severity.NOTICE,
            stackTraceToSingleLineString(e)));
      }
      finally
      {
        release();
      }
    }
  }
  /**
   * Stop the handler.
   * @param handler The handler to stop.
   */
  public void stopServer(MessageHandler handler)
  {
    if (debugEnabled())
      TRACER.debugInfo(
          "In RS " + this.replicationServer.getMonitorInstanceName() +
          " domain=" + this +
          " stopServer(MH)" + handler.getMonitorInstanceName() +
          " " + stackTraceToSingleLineString(new Exception()));
    /*
     * We must prevent deadlock on replication server domain lock, when for
     * instance this code is called from dying ServerReader but also dying
     * ServerWriter at the same time, or from a thread that wants to shut down
     * the handler. So use a thread safe flag to know if the job must be done
     * or not (is already being processed or not).
     */
    if (!handler.engageShutdown())
      // Only do this once (prevent other thread to enter here again)
    {
      try
      {
        // Acquire lock on domain (see more details in comment of start()
@@ -1037,57 +1143,40 @@
      {
        // Try doing job anyway...
      }
      if (handler.isReplicationServer())
      if (otherHandlers.contains(handler))
      {
        if (replicationServers.containsValue(handler))
        {
          replicationServers.remove(handler.getServerId());
          handler.shutdown();
          // Check if generation id has to be resetted
          mayResetGenerationId();
          // Warn our DSs that a RS or DS has quit (does not use this
          // handler as already removed from list)
          sendTopoInfoToDSs(null);
        }
      } else
      {
        if (directoryServers.containsValue(handler))
        {
          // If this is the last DS for the domain, shutdown the status analyzer
          if (directoryServers.size() == 1)
          {
            if (debugEnabled())
              TRACER.debugInfo("In " +
                replicationServer.getMonitorInstanceName() +
                " remote server " + handler.getMonitorInstanceName() +
                " is the last DS to be stopped: stopping status analyzer");
            stopStatusAnalyzer();
          }
          directoryServers.remove(handler.getServerId());
          handler.shutdown();
          // Check if generation id has to be resetted
          mayResetGenerationId();
          // Update the remote replication servers with our list
          // of connected LDAP servers
          sendTopoInfoToRSs();
          // Warn our DSs that a RS or DS has quit (does not use this
          // handler as already removed from list)
          sendTopoInfoToDSs(null);
        }
        unRegisterHandler(handler);
        handler.shutdown();
      }
    }
    release();
  }
      release();
  /**
   * Unregister this handler from the list of handlers registered to this
   * domain.
   * @param handler the provided handler to unregister.
   */
  protected void unregisterServerHandler(ServerHandler handler)
  {
    if (handler.isReplicationServer())
    {
      replicationServers.remove(handler.getServerId());
    }
    else
    {
      directoryServers.remove(handler.getServerId());
    }
  }
  /**
   * Resets the generationId for this domain if there is no LDAP
   * server currently connected and if the generationId has never
   * been saved.
   * This method resets the generationId for this domain if there is no LDAP
   * server currently connected in the whole topology on this domain and
   * if the generationId has never been saved.
   *
   * - test emtpyness of directoryServers list
   * - traverse replicationServers list and test for each if DS are connected
   * So it strongly relies on the directoryServers list
   */
  protected void mayResetGenerationId()
  {
@@ -1104,7 +1193,7 @@
    boolean lDAPServersConnectedInTheTopology = false;
    if (directoryServers.isEmpty())
    {
      for (ServerHandler rsh : replicationServers.values())
      for (ReplicationServerHandler rsh : replicationServers.values())
      {
        if (generationId != rsh.getGenerationId())
        {
@@ -1149,14 +1238,16 @@
  }
  /**
   * Checks that a RS is not already connected.
   *
   * @param handler the RS we want to check
   * @return true if this is not a duplicate server
   * Checks that a remote RS is not already connected to this hosting RS.
   * @param handler The handler for the remote RS.
   * @return flag specifying whether the remote RS is already connected.
   * @throws DirectoryException when a problem occurs.
   */
  public boolean checkForDuplicateRS(ServerHandler handler)
  public boolean checkForDuplicateRS(ReplicationServerHandler handler)
  throws DirectoryException
  {
    ServerHandler oldHandler = replicationServers.get(handler.getServerId());
    ReplicationServerHandler oldHandler =
      replicationServers.get(handler.getServerId());
    if ((oldHandler != null))
    {
      if (oldHandler.getServerAddressURL().equals(
@@ -1166,7 +1257,9 @@
        // have been sent at about the same time and 2 connections
        // have been established.
        // Silently drop this connection.
        } else
        return false;
      }
      else
      {
        // looks like two replication servers have the same serverId
        // log an error message and drop this connection.
@@ -1174,9 +1267,8 @@
          replicationServer.getMonitorInstanceName(), oldHandler.
          getServerAddressURL(), handler.getServerAddressURL(),
          handler.getServerId());
        logError(message);
        throw new DirectoryException(ResultCode.OTHER, message);
      }
      return false;
    }
    return true;
  }
@@ -1223,7 +1315,7 @@
  {
    LinkedHashSet<String> mySet = new LinkedHashSet<String>();
    for (ServerHandler handler : replicationServers.values())
    for (ReplicationServerHandler handler : replicationServers.values())
    {
      mySet.add(handler.getServerAddressURL());
    }
@@ -1232,7 +1324,9 @@
  }
  /**
   * Return a Set containing the servers known by this replicationServer.
   * Return a set containing the server that produced update and known by
   * this replicationServer from all over the topology,
   * whatever directly connected of connected to another RS.
   * @return a set containing the servers known by this replicationServer.
   */
  public Set<Short> getServers()
@@ -1250,7 +1344,7 @@
  {
    List<String> mySet = new ArrayList<String>(0);
    for (ServerHandler handler : directoryServers.values())
    for (DataServerHandler handler : directoryServers.values())
    {
      mySet.add(String.valueOf(handler.getServerId()));
    }
@@ -1348,7 +1442,7 @@
      {
        // Send to all replication servers with a least one remote
        // server connected
        for (ServerHandler rsh : replicationServers.values())
        for (ReplicationServerHandler rsh : replicationServers.values())
        {
          if (rsh.hasRemoteLDAPServers())
          {
@@ -1358,7 +1452,7 @@
      }
      // Sends to all connected LDAP servers
      for (ServerHandler destinationHandler : directoryServers.values())
      for (DataServerHandler destinationHandler : directoryServers.values())
      {
        // Don't loop on the sender
        if (destinationHandler == senderHandler)
@@ -1368,7 +1462,7 @@
    } else
    {
      // Destination is one server
      ServerHandler destinationHandler =
      DataServerHandler destinationHandler =
        directoryServers.get(msg.getDestination());
      if (destinationHandler != null)
      {
@@ -1378,9 +1472,9 @@
        // the targeted server is NOT connected
        // Let's search for THE changelog server that MAY
        // have the targeted server connected.
        if (senderHandler.isLDAPserver())
        if (senderHandler.isDataServer())
        {
          for (ServerHandler h : replicationServers.values())
          for (ReplicationServerHandler h : replicationServers.values())
          {
            // Send to all replication servers with a least one remote
            // server connected
@@ -1421,7 +1515,7 @@
        // build the full list of all servers in the topology
        // and send back a MonitorMsg with the full list of all the servers
        // in the topology.
        if (senderHandler.isLDAPserver())
        if (senderHandler.isDataServer())
        {
          MonitorMsg returnMsg =
            new MonitorMsg(msg.getDestination(), msg.getsenderID());
@@ -1481,7 +1575,7 @@
        // from the states stored in the serverHandler.
        // - the server state
        // - the older missing change
        for (ServerHandler lsh : this.directoryServers.values())
        for (DataServerHandler lsh : this.directoryServers.values())
        {
          monitorMsg.setServerState(
            lsh.getServerId(),
@@ -1491,7 +1585,7 @@
        }
        // Same for the connected RS
        for (ServerHandler rsh : this.replicationServers.values())
        for (ReplicationServerHandler rsh : this.replicationServers.values())
        {
          monitorMsg.setServerState(
            rsh.getServerId(),
@@ -1657,12 +1751,12 @@
   */
  public void checkAllSaturation() throws IOException
  {
    for (ServerHandler handler : replicationServers.values())
    for (ReplicationServerHandler handler : replicationServers.values())
    {
      handler.checkWindow();
    }
    for (ServerHandler handler : directoryServers.values())
    for (DataServerHandler handler : directoryServers.values())
    {
      handler.checkWindow();
    }
@@ -1675,15 +1769,15 @@
   * @return true if the server can restart sending changes.
   *         false if the server can't restart sending changes.
   */
  public boolean restartAfterSaturation(ServerHandler sourceHandler)
  public boolean restartAfterSaturation(MessageHandler sourceHandler)
  {
    for (ServerHandler handler : replicationServers.values())
    for (MessageHandler handler : replicationServers.values())
    {
      if (!handler.restartAfterSaturation(sourceHandler))
        return false;
    }
    for (ServerHandler handler : directoryServers.values())
    for (MessageHandler handler : directoryServers.values())
    {
      if (!handler.restartAfterSaturation(sourceHandler))
        return false;
@@ -1698,9 +1792,9 @@
   * @param notThisOne If not null, the topology message will not be sent to
   * this passed server.
   */
  public void sendTopoInfoToDSs(ServerHandler notThisOne)
  public void buildAndSendTopoInfoToDSs(ServerHandler notThisOne)
  {
    for (ServerHandler handler : directoryServers.values())
    for (DataServerHandler handler : directoryServers.values())
    {
      if ((notThisOne == null) || // All DSs requested
        ((notThisOne != null) && (handler != notThisOne)))
@@ -1725,10 +1819,10 @@
   * Send a TopologyMsg to all the connected replication servers
   * in order to let them know our connected LDAP servers.
   */
  public void sendTopoInfoToRSs()
  public void buildAndSendTopoInfoToRSs()
  {
    TopologyMsg topoMsg = createTopologyMsgForRS();
    for (ServerHandler handler : replicationServers.values())
    for (ReplicationServerHandler handler : replicationServers.values())
    {
      try
      {
@@ -1755,7 +1849,7 @@
    List<DSInfo> dsInfos = new ArrayList<DSInfo>();
    // Go through every DSs
    for (ServerHandler serverHandler : directoryServers.values())
    for (DataServerHandler serverHandler : directoryServers.values())
    {
      dsInfos.add(serverHandler.toDSInfo());
    }
@@ -1785,7 +1879,7 @@
    List<RSInfo> rsInfos = new ArrayList<RSInfo>();
    // Go through every DSs (except recipient of msg)
    for (ServerHandler serverHandler : directoryServers.values())
    for (DataServerHandler serverHandler : directoryServers.values())
    {
      if (serverHandler.getServerId() == destDsId)
        continue;
@@ -1799,7 +1893,7 @@
    // Go through every peer RSs (and get their connected DSs), also add info
    // for RSs
    for (ServerHandler serverHandler : replicationServers.values())
    for (ReplicationServerHandler serverHandler : replicationServers.values())
    {
      // Put RS info
      rsInfos.add(serverHandler.toRSInfo());
@@ -1840,12 +1934,6 @@
  synchronized public long setGenerationId(long generationId,
    boolean savedStatus)
  {
    if (debugEnabled())
      TRACER.debugInfo(
        "In " + this.replicationServer.getMonitorInstanceName() +
        " baseDN=" + baseDn +
        " RCache.set GenerationId=" + generationId);
    long oldGenerationId = this.generationId;
    if (this.generationId != generationId)
@@ -1916,7 +2004,7 @@
        // After we'll have sent the message , the remote RS will adopt
        // the new genId
        rsHandler.setGenerationId(newGenId);
        if (senderHandler.isLDAPserver())
        if (senderHandler.isDataServer())
        {
          rsHandler.forwardGenerationIdToRS(genIdMsg);
        }
@@ -1929,7 +2017,7 @@
    // Change status of the connected DSs according to the requested new
    // reference generation id
    for (ServerHandler dsHandler : directoryServers.values())
    for (DataServerHandler dsHandler : directoryServers.values())
    {
      try
      {
@@ -1948,8 +2036,8 @@
    // (consecutive to reset gen id message), we prefer advertising once for
    // all after changes (less packet sent), here at the end of the reset msg
    // treatment.
    sendTopoInfoToDSs(null);
    sendTopoInfoToRSs();
    buildAndSendTopoInfoToDSs(null);
    buildAndSendTopoInfoToRSs();
    Message message = NOTE_RESET_GENERATION_ID.get(baseDn.toString(),
      Long.toString(newGenId));
@@ -1964,7 +2052,7 @@
   *        that changed his status.
   * @param csMsg The message containing the new status
   */
  public void processNewStatus(ServerHandler senderHandler,
  public void processNewStatus(DataServerHandler senderHandler,
    ChangeStatusMsg csMsg)
  {
    if (debugEnabled())
@@ -1995,8 +2083,8 @@
    }
    // Update every peers (RS/DS) with topology changes
    sendTopoInfoToDSs(senderHandler);
    sendTopoInfoToRSs();
    buildAndSendTopoInfoToDSs(senderHandler);
    buildAndSendTopoInfoToRSs();
    Message message = NOTE_DIRECTORY_SERVER_CHANGED_STATUS.get(
      Short.toString(senderHandler.getServerId()),
@@ -2014,7 +2102,7 @@
   * @param event The event to be used for new status computation
   * @return True if we have been interrupted (must stop), false otherwise
   */
  public boolean changeStatusFromStatusAnalyzer(ServerHandler serverHandler,
  public boolean changeStatusFromStatusAnalyzer(DataServerHandler serverHandler,
    StatusMachineEvent event)
  {
    try
@@ -2066,8 +2154,8 @@
    }
    // Update every peers (RS/DS) with topology changes
    sendTopoInfoToDSs(serverHandler);
    sendTopoInfoToRSs();
    buildAndSendTopoInfoToDSs(serverHandler);
    buildAndSendTopoInfoToRSs();
    release();
    return false;
@@ -2169,10 +2257,12 @@
   * @param allowResetGenId True for allowing to reset the generation id (
   * when called after initial handshake)
   * @throws IOException If an error occurred.
   * @throws DirectoryException If an error occurred.
   */
  public void receiveTopoInfoFromRS(TopologyMsg topoMsg, ServerHandler handler,
  public void receiveTopoInfoFromRS(TopologyMsg topoMsg,
      ReplicationServerHandler handler,
    boolean allowResetGenId)
    throws IOException
    throws IOException, DirectoryException
  {
    if (debugEnabled())
    {
@@ -2186,7 +2276,8 @@
    {
      // Acquire lock on domain (see more details in comment of start() method
      // of ServerHandler)
      lock();
      if (!hasLock())
        lock();
    } catch (InterruptedException ex)
    {
      // Try doing job anyway...
@@ -2228,7 +2319,7 @@
     * Sends the currently known topology information to every connected
     * DS we have.
     */
    sendTopoInfoToDSs(null);
    buildAndSendTopoInfoToDSs(null);
    release();
  }
@@ -2441,7 +2532,7 @@
          {
            // this is the latency of the remote RSi regarding another RSj
            // let's update the latency of the LSes connected to RSj
            ServerHandler rsjHdr = replicationServers.get(rsid);
            ReplicationServerHandler rsjHdr = replicationServers.get(rsid);
            if (rsjHdr != null)
            {
              for (short remotelsid : rsjHdr.getConnectedDirectoryServerIds())
@@ -2496,7 +2587,7 @@
   * Get the map of connected DSs.
   * @return The map of connected DSs
   */
  public Map<Short, ServerHandler> getConnectedDSs()
  public Map<Short, DataServerHandler> getConnectedDSs()
  {
    return directoryServers;
  }
@@ -2505,7 +2596,7 @@
   * Get the map of connected RSs.
   * @return The map of connected RSs
   */
  public Map<Short, ServerHandler> getConnectedRSs()
  public Map<Short, ReplicationServerHandler> getConnectedRSs()
  {
    return replicationServers;
  }
@@ -2708,5 +2799,140 @@
    return attributes;
  }
}
  /**
   * Register in the domain an handler that subscribes to changes.
   * @param handler the provided subscribing handler.
   */
  public void registerHandler(MessageHandler handler)
  {
    this.otherHandlers.add(handler);
  }
  /**
   * Unregister from the domain an handler.
   * @param handler the provided unsubscribing handler.
   * @return Whether this handler has been unregistered with success.
   */
  public boolean unRegisterHandler(MessageHandler handler)
  {
    return this.otherHandlers.remove(handler);
  }
  /**
   * Return the state that contain for each server the time of eligibility.
   * @return the state.
   */
  public ServerState getHeartbeatState()
  {
    // TODO:ECL Eligility must be supported
    return this.getDbServerState();
  }
  /**
   * Computes the change number eligible to the ECL.
   * @return null if the domain does not play in eligibility.
   */
  public ChangeNumber computeEligibleCN()
  {
    ChangeNumber elligibleCN = null;
    ServerState heartbeatState = getHeartbeatState();
    if (heartbeatState==null)
      return null;
    // compute elligible CN
    ServerState hbState = heartbeatState.duplicate();
    Iterator<Short> it = hbState.iterator();
    while (it.hasNext())
    {
      short sid = it.next();
      ChangeNumber storedCN = hbState.getMaxChangeNumber(sid);
      // If the most recent UpdateMsg or CLHeartbeatMsg received is very old
      // then the server is considered down and not considered for eligibility
      if (TimeThread.getTime()-storedCN.getTime()>2000)
      {
        if (debugEnabled())
          TRACER.debugInfo(
            "For RSD." + this.baseDn + " Server " + sid
            + " is not considered for eligibility ... potentially down");
        continue;
      }
      if ((elligibleCN == null) || (storedCN.older(elligibleCN)))
      {
        elligibleCN = storedCN;
      }
    }
    if (debugEnabled())
      TRACER.debugInfo(
        "For RSD." + this.baseDn + " ElligibleCN()=" + elligibleCN);
    return elligibleCN;
  }
  /**
   * Computes the eligible server state by minimizing the dbServerState and the
   * elligibleCN.
   * @return The computed eligible server state.
   */
  public ServerState getCLElligibleState()
  {
    // ChangeNumber elligibleCN = computeEligibleCN();
    ServerState res = new ServerState();
    ServerState dbState = this.getDbServerState();
    res = dbState;
    /* TODO:ECL Eligibility is not yet implemented
    Iterator<Short> it = dbState.iterator();
    while (it.hasNext())
    {
      Short sid = it.next();
      DbHandler h = sourceDbHandlers.get(sid);
      ChangeNumber dbCN = dbState.getMaxChangeNumber(sid);
      try
      {
        if ((elligibleCN!=null)&&(elligibleCN.older(dbCN)))
        {
          // some CN exist in the db newer than elligible CN
          ReplicationIterator ri = h.generateIterator(elligibleCN);
          ChangeNumber newCN = ri.getCurrentCN();
          res.update(newCN);
          ri.releaseCursor();
        }
        else
        {
          // no CN exist in the db newer than elligible CN
          res.update(dbCN);
        }
      }
      catch(Exception e)
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
    }
    */
    if (debugEnabled())
      TRACER.debugInfo("In " + this.getName()
        + " getCLElligibleState returns:" + res);
    return res;
  }
  /**
   * Returns the start state of the domain, made of the first (oldest)
   * change stored for each serverId.
   * @return t start state of the domain.
   */
  public ServerState getStartState()
  {
    ServerState domainStartState = new ServerState();
    Iterator<Short> it = this.getDbServerState().iterator();
    for (DbHandler dbHandler : sourceDbHandlers.values())
    {
      domainStartState.update(dbHandler.getFirstChange());
    }
    return domainStartState;
  }
}
opends/src/server/org/opends/server/replication/server/ReplicationServerHandler.java
New file
@@ -0,0 +1,791 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.server;
import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.opends.messages.Message;
import org.opends.server.replication.common.DSInfo;
import org.opends.server.replication.common.RSInfo;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.common.ServerStatus;
import org.opends.server.replication.protocol.ProtocolSession;
import org.opends.server.replication.protocol.ProtocolVersion;
import org.opends.server.replication.protocol.ReplServerStartMsg;
import org.opends.server.replication.protocol.ReplicationMsg;
import org.opends.server.replication.protocol.TopologyMsg;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.Attributes;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.ResultCode;
/**
 * This class defines a server handler, which handles all interaction with a
 * peer replication server.
 */
public class ReplicationServerHandler extends ServerHandler
{
  /*
   * Properties filled only if remote server is a RS
   */
  private String serverAddressURL;
  /**
   * this collection will contain as many elements as there are
   * LDAP servers connected to the remote replication server.
   */
  private final Map<Short, LightweightServerHandler> remoteDirectoryServers =
    new ConcurrentHashMap<Short, LightweightServerHandler>();
  /**
   * Starts this handler based on a start message received from remote server.
   * @param inReplServerStartMsg The start msg provided by the remote server.
   * @return Whether the remote server requires encryption or not.
   * @throws DirectoryException When a problem occurs.
   */
  public boolean processStartFromRemote(ReplServerStartMsg inReplServerStartMsg)
  throws DirectoryException
  {
    try
    {
      protocolVersion = ProtocolVersion.minWithCurrent(
          inReplServerStartMsg.getVersion());
      generationId = inReplServerStartMsg.getGenerationId();
      serverId = inReplServerStartMsg.getServerId();
      serverURL = inReplServerStartMsg.getServerURL();
      int separator = serverURL.lastIndexOf(':');
      serverAddressURL =
        session.getRemoteAddress() + ":" + serverURL.substring(separator +
            1);
      setServiceIdAndDomain(inReplServerStartMsg.getBaseDn());
      setInitialServerState(inReplServerStartMsg.getServerState());
      setSendWindowSize(inReplServerStartMsg.getWindowSize());
      if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1)
      {
        // We support connection from a V1 RS
        // Only V2 protocol has the group id in repl server start message
        this.groupId = inReplServerStartMsg.getGroupId();
      }
      oldGenerationId = -100;
      // Duplicate server ?
      if (!replicationServerDomain.checkForDuplicateRS(this))
      {
        abortStart(null);
        return false;
      }
    }
    catch(Exception e)
    {
      Message message = Message.raw(e.getLocalizedMessage());
      throw new DirectoryException(ResultCode.OTHER, message);
    }
    return inReplServerStartMsg.getSSLEncryption();
  }
  /**
   * Creates a new handler object to a remote replication server.
   * @param session The session with the remote RS.
   * @param queueSize The queue size to manage updates to that RS.
   * @param replicationServerURL The hosting local RS URL.
   * @param replicationServerId The hosting local RS serverId.
   * @param replicationServer The hosting local RS object.
   * @param rcvWindowSize The receiving window size.
   */
  public ReplicationServerHandler(
      ProtocolSession session,
      int queueSize,
      String replicationServerURL,
      short replicationServerId,
      ReplicationServer replicationServer,
      int rcvWindowSize)
  {
    super(session, queueSize, replicationServerURL, replicationServerId,
        replicationServer, rcvWindowSize);
  }
  /**
   * Connect the hosting RS to the RS represented by THIS handler
   * on an outgoing connection.
   * @param serviceId The serviceId (usually baseDn).
   * @param sslEncryption The sslEncryption requested to the remote RS.
   * @throws DirectoryException when an error occurs.
   */
  public void connect(String serviceId, boolean sslEncryption)
  throws DirectoryException
  {
    //
    // the encryption we will request to the peer as we are the session creator
    this.initSslEncryption = sslEncryption;
    setServiceIdAndDomain(serviceId);
    localGenerationId = replicationServerDomain.getGenerationId();
    oldGenerationId = localGenerationId;
    try
    {
      //
      lockDomain(false); // notimeout
      // we are the initiator and decides of the encryption
      boolean sessionInitiatorSSLEncryption = this.initSslEncryption;
      // Send start
      ReplServerStartMsg outReplServerStartMsg = sendStartToRemote((short)-1);
      // Wait answer
      ReplicationMsg msg = session.receive();
      // Reject bad responses
      if (!(msg instanceof ReplServerStartMsg))
      {
        Message message = ERR_REPLICATION_PROTOCOL_MESSAGE_TYPE.get(
            msg.getClass().getCanonicalName(),
            "ReplServerStartMsg");
        abortStart(message);
        return;
      }
      // Process hello from remote
      processStartFromRemote((ReplServerStartMsg)msg);
      // Log
      logStartHandshakeSNDandRCV(outReplServerStartMsg,(ReplServerStartMsg)msg);
      // Until here session is encrypted then it depends on the negociation
      // The session initiator decides whether to use SSL.
      if (!sessionInitiatorSSLEncryption)
        session.stopEncryption();
      if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1)
      {
        // Only protocol version above V1 has a phase 2 handshake
        // NOW PROCEDE WITH SECOND PHASE OF HANDSHAKE:
        // TopologyMsg then TopologyMsg (with a RS)
        // Send our own TopologyMsg to remote RS
        TopologyMsg outTopoMsg = sendTopoToRemoteRS();
        // wait and process Topo from remote RS
        TopologyMsg inTopoMsg = waitAndProcessTopoFromRemoteRS();
        logTopoHandshakeSNDandRCV(outTopoMsg, inTopoMsg);
        // FIXME: i think this should be done for all protocol version !!
        // not only those > V1
        registerIntoDomain();
        // Process TopologyMsg sent by remote RS: store matching new info
        // (this will also warn our connected DSs of the new received info)
        replicationServerDomain.receiveTopoInfoFromRS(inTopoMsg, this, false);
      }
      super.finalizeStart();
    }
    catch(IOException ioe)
    {
      // FIXME receive
    }
    // catch(DirectoryException de)
    //{ already logged
    //
    catch(Exception e)
    {
      // FIXME more detailed exceptions
    }
    finally
    {
      // Release domain
      if ((replicationServerDomain != null) &&
          replicationServerDomain.hasLock())
        replicationServerDomain.release();
    }
  }
  /**
   * Starts the handler from a remote ReplServerStart message received from
   * the remote replication server.
   * @param inReplServerStartMsg The provided ReplServerStart message received.
   */
  public void startFromRemoteRS(ReplServerStartMsg inReplServerStartMsg)
  {
    //
    localGenerationId = -1;
    oldGenerationId = -100;
    try
    {
      // Process start from remote
      boolean sessionInitiatorSSLEncryption =
        processStartFromRemote(inReplServerStartMsg);
      // lock with timeout
      lockDomain(true);
      short reqVersion = -1;
      if (protocolVersion == ProtocolVersion.REPLICATION_PROTOCOL_V1)
      {
        // We support connection from a V1 RS, send PDU with V1 form
        reqVersion = ProtocolVersion.REPLICATION_PROTOCOL_V1;
      }
      // send start to remote
      ReplServerStartMsg outReplServerStartMsg = sendStartToRemote(reqVersion);
      // log
      logStartHandshakeRCVandSND(inReplServerStartMsg, outReplServerStartMsg);
      // until here session is encrypted then it depends on the negociation
      // The session initiator decides whether to use SSL.
      if (!sessionInitiatorSSLEncryption)
        session.stopEncryption();
      TopologyMsg inTopoMsg = null;
      if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1)
      {
        // Only protocol version above V1 has a phase 2 handshake
        // NOW PROCEDE WITH SECOND PHASE OF HANDSHAKE:
        // TopologyMsg then TopologyMsg (with a RS)
        // wait and process Topo from remote RS
        inTopoMsg = waitAndProcessTopoFromRemoteRS();
        // send our own TopologyMsg to remote RS
        TopologyMsg outTopoMsg = sendTopoToRemoteRS();
        // log
        logTopoHandshakeRCVandSND(inTopoMsg, outTopoMsg);
      }
      else
      {
        // Terminate connection from a V1 RS
        // if the remote RS and the local RS have the same genID
        // then it's ok and nothing else to do
        if (generationId == localGenerationId)
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("In " +
              replicationServerDomain.getReplicationServer().
              getMonitorInstanceName() +
              this + " RS V1 with serverID=" + serverId +
              " is connected with the right generation ID");
          }
        } else
        {
          if (localGenerationId > 0)
          {
            // if the local RS is initialized
            if (generationId > 0)
            {
              // if the remote RS is initialized
              if (generationId != localGenerationId)
              {
                // if the 2 RS have different generationID
                if (replicationServerDomain.getGenerationIdSavedStatus())
                {
                  // if the present RS has received changes regarding its
                  //     gen ID and so won't change without a reset
                  // then  we are just degrading the peer.
                  Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get(
                    getServiceId(),
                    Short.toString(serverId),
                    Long.toString(generationId),
                    Long.toString(localGenerationId));
                  logError(message);
                } else
                {
                  // The present RS has never received changes regarding its
                  // gen ID.
                  //
                  // Example case:
                  // - we are in RS1
                  // - RS2 has genId2 from LS2 (genId2 <=> no data in LS2)
                  // - RS1 has genId1 from LS1 /genId1 comes from data in
                  //   suffix
                  // - we are in RS1 and we receive a START msg from RS2
                  // - Each RS keeps its genID / is degraded and when LS2
                  //   will be populated from LS1 everything will become ok.
                  //
                  // Issue:
                  // FIXME : Would it be a good idea in some cases to just
                  //         set the gen ID received from the peer RS
                  //         specially if the peer has a non null state and
                  //         we have a nul state ?
                  // replicationServerDomain.
                  // setGenerationId(generationId, false);
                  Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get(
                    getServiceId(),
                    Short.toString(serverId),
                    Long.toString(generationId),
                    Long.toString(localGenerationId));
                  logError(message);
                }
              }
            } else
            {
              // The remote RS has no genId. We don't change anything for the
              // current RS.
            }
          } else
          {
            // The local RS is not initialized - take the one received
            oldGenerationId =
              replicationServerDomain.setGenerationId(generationId, false);
          }
        }
        // Note: the supported scenario for V1->V2 upgrade is to upgrade 1 by 1
        // all the servers of the topology. We prefer not not send a TopologyMsg
        // for giving partial/false information to the V2 servers as for
        // instance we don't have the connected DS of the V1 RS...When the V1
        // RS will be upgraded in his turn, topo info will be sent and accurate.
        // That way, there is  no risk to have false/incomplete information in
        // other servers.
      }
      registerIntoDomain();
      // Process TopologyMsg sent by remote RS: store matching new info
      // (this will also warn our connected DSs of the new received info)
      if (inTopoMsg!=null)
        replicationServerDomain.receiveTopoInfoFromRS(inTopoMsg, this, false);
      super.finalizeStart();
    }
    catch(DirectoryException de)
    {
      abortStart(de.getMessageObject());
    }
    catch(Exception e)
    {
      abortStart(Message.raw(e.getLocalizedMessage()));
    }
    finally
    {
      if ((replicationServerDomain != null) &&
          replicationServerDomain.hasLock())
        replicationServerDomain.release();
    }
  }
  /**
   * Registers this handler into its related domain and notifies the domain.
   */
  private void registerIntoDomain()
  {
    // Alright, connected with new RS (either outgoing or incoming
    // connection): store handler.
    Map<Short, ReplicationServerHandler> connectedRSs =
      replicationServerDomain.getConnectedRSs();
    connectedRSs.put(serverId, this);
  }
  /**
   * Create and send the topologyMsg to the remote replication server.
   * @return the topologyMsg sent.
   */
  private TopologyMsg sendTopoToRemoteRS()
  throws IOException
  {
    TopologyMsg outTopoMsg = replicationServerDomain.createTopologyMsgForRS();
    session.publish(outTopoMsg);
    return outTopoMsg;
  }
  /**
   * Wait receiving the TopologyMsg from the remote RS and process it.
   * @return the topologyMsg received.
   * @throws DirectoryException
   * @throws IOException
   */
  private TopologyMsg waitAndProcessTopoFromRemoteRS()
  throws DirectoryException, IOException
  {
    ReplicationMsg msg = null;
    try
    {
      msg = session.receive();
    }
    catch(Exception e)
    {
      Message message = Message.raw(e.getLocalizedMessage());
      throw new DirectoryException(ResultCode.OTHER, message);
    }
    if (!(msg instanceof TopologyMsg))
    {
      Message message = ERR_REPLICATION_PROTOCOL_MESSAGE_TYPE.get(
          msg.getClass().getCanonicalName(),
          "TopologyMsg");
      abortStart(message);
    }
    // Remote RS sent his topo msg
    TopologyMsg inTopoMsg = (TopologyMsg) msg;
    // CONNECTION WITH A RS
    // if the remote RS and the local RS have the same genID
    // then it's ok and nothing else to do
    if (generationId == localGenerationId)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("In " +
            replicationServerDomain.getReplicationServer().
            getMonitorInstanceName() + " RS with serverID=" + serverId +
            " is connected with the right generation ID, same as local ="
            + generationId);
      }
    }
    else
    {
      if (localGenerationId > 0)
      {
        // if the local RS is initialized
        if (generationId > 0)
        {
          // if the remote RS is initialized
          if (generationId != localGenerationId)
          {
            // if the 2 RS have different generationID
            if (replicationServerDomain.getGenerationIdSavedStatus())
            {
              // if the present RS has received changes regarding its
              //     gen ID and so won't change without a reset
              // then  we are just degrading the peer.
              Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get(
                  getServiceId(),
                  Short.toString(serverId),
                  Long.toString(generationId),
                  Long.toString(localGenerationId));
              logError(message);
            }
            else
            {
              // The present RS has never received changes regarding its
              // gen ID.
              //
              // Example case:
              // - we are in RS1
              // - RS2 has genId2 from LS2 (genId2 <=> no data in LS2)
              // - RS1 has genId1 from LS1 /genId1 comes from data in
              //   suffix
              // - we are in RS1 and we receive a START msg from RS2
              // - Each RS keeps its genID / is degraded and when LS2
              //   will be populated from LS1 everything will become ok.
              //
              // Issue:
              // FIXME : Would it be a good idea in some cases to just
              //         set the gen ID received from the peer RS
              //         specially if the peer has a non null state and
              //         we have a nul state ?
              // replicationServerDomain.
              // setGenerationId(generationId, false);
              Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get(
                  getServiceId(),
                  Short.toString(serverId),
                  Long.toString(generationId),
                  Long.toString(localGenerationId));
              logError(message);
            }
          }
        }
        else
        {
          // The remote RS has no genId. We don't change anything for the
          // current RS.
        }
      }
      else
      {
        // The local RS is not initialized - take the one received
        // WARNING: Must be done before computing topo message to send
        // to peer server as topo message must embed valid generation id
        // for our server
        oldGenerationId =
          replicationServerDomain.setGenerationId(generationId, false);
      }
    }
    return inTopoMsg;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isDataServer()
  {
    return false;
  }
  /**
   * Add the DSinfos of the connected Directory Servers
   * to the List of DSInfo provided as a parameter.
   *
   * @param dsInfos The List of DSInfo that should be updated
   *                with the DSInfo for the remoteDirectoryServers
   *                connected to this ServerHandler.
   */
  public void addDSInfos(List<DSInfo> dsInfos)
  {
    synchronized (remoteDirectoryServers)
    {
      for (LightweightServerHandler ls : remoteDirectoryServers.values())
      {
        dsInfos.add(ls.toDSInfo());
      }
    }
  }
  /**
   * Shutdown This ServerHandler.
   */
  public void shutdown()
  {
    super.shutdown();
    /*
     * Stop the remote LSHandler
     */
    synchronized (remoteDirectoryServers)
    {
      for (LightweightServerHandler lsh : remoteDirectoryServers.values())
      {
        lsh.stopHandler();
      }
      remoteDirectoryServers.clear();
    }
  }
  /**
   * Stores topology information received from a peer RS and that must be kept
   * in RS handler.
   *
   * @param topoMsg The received topology message
   */
  public void receiveTopoInfoFromRS(TopologyMsg topoMsg)
  {
    // Store info for remote RS
    List<RSInfo> rsInfos = topoMsg.getRsList();
    // List should only contain RS info for sender
    RSInfo rsInfo = rsInfos.get(0);
    generationId = rsInfo.getGenerationId();
    groupId = rsInfo.getGroupId();
    /**
     * Store info for DSs connected to the peer RS
     */
    List<DSInfo> dsInfos = topoMsg.getDsList();
    synchronized (remoteDirectoryServers)
    {
      // Removes the existing structures
      for (LightweightServerHandler lsh : remoteDirectoryServers.values())
      {
        lsh.stopHandler();
      }
      remoteDirectoryServers.clear();
      // Creates the new structure according to the message received.
      for (DSInfo dsInfo : dsInfos)
      {
        LightweightServerHandler lsh = new LightweightServerHandler(this,
            serverId, dsInfo.getDsId(), dsInfo.getGenerationId(),
            dsInfo.getGroupId(), dsInfo.getStatus(), dsInfo.getRefUrls(),
            dsInfo.isAssured(), dsInfo.getAssuredMode(),
            dsInfo.getSafeDataLevel());
        lsh.startHandler();
        remoteDirectoryServers.put(lsh.getServerId(), lsh);
      }
    }
  }
  /**
   * When this handler is connected to a replication server, specifies if
   * a wanted server is connected to this replication server.
   *
   * @param wantedServer The server we want to know if it is connected
   * to the replication server represented by this handler.
   * @return boolean True is the wanted server is connected to the server
   * represented by this handler.
   */
  public boolean isRemoteLDAPServer(short wantedServer)
  {
    synchronized (remoteDirectoryServers)
    {
      for (LightweightServerHandler server : remoteDirectoryServers.values())
      {
        if (wantedServer == server.getServerId())
        {
          return true;
        }
      }
      return false;
    }
  }
  /**
   * When the handler is connected to a replication server, specifies the
   * replication server has remote LDAP servers connected to it.
   *
   * @return boolean True is the replication server has remote LDAP servers
   * connected to it.
   */
  public boolean hasRemoteLDAPServers()
  {
    synchronized (remoteDirectoryServers)
    {
      return !remoteDirectoryServers.isEmpty();
    }
  }
  /**
   * Return a Set containing the servers known by this replicationServer.
   * @return a set containing the servers known by this replicationServer.
   */
  public Set<Short> getConnectedDirectoryServerIds()
  {
    synchronized (remoteDirectoryServers)
    {
      return remoteDirectoryServers.keySet();
    }
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public String getMonitorInstanceName()
  {
    String str = serverURL + " " + String.valueOf(serverId);
    return "Connected Replication Server " + str +
    ",cn=" + replicationServerDomain.getMonitorInstanceName();
  }
  /**
   * Retrieves a set of attributes containing monitor data that should be
   * returned to the client if the corresponding monitor entry is requested.
   *
   * @return  A set of attributes containing monitor data that should be
   *          returned to the client if the corresponding monitor entry is
   *          requested.
   */
  @Override
  public ArrayList<Attribute> getMonitorData()
  {
    // Get the generic ones
    ArrayList<Attribute> attributes = super.getMonitorData();
    // Add the specific RS ones
    attributes.add(Attributes.create("Replication-Server",
        serverURL));
    try
    {
      MonitorData md;
      md = replicationServerDomain.computeMonitorData();
      // Missing changes
      long missingChanges = md.getMissingChangesRS(serverId);
      attributes.add(Attributes.create("missing-changes", String
          .valueOf(missingChanges)));
      /* get the Server State */
      AttributeBuilder builder = new AttributeBuilder("server-state");
      ServerState state = md.getRSStates(serverId);
      if (state != null)
      {
        for (String str : state.toStringSet())
        {
          builder.add(str);
        }
        attributes.add(builder.toAttribute());
      }
    }
    catch (Exception e)
    {
      Message message =
        ERR_ERROR_RETRIEVING_MONITOR_DATA.get(stackTraceToSingleLineString(e));
      // We failed retrieving the monitor data.
      attributes.add(Attributes.create("error", message.toString()));
    }
    return attributes;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public String toString()
  {
    String localString;
    if (serverId != 0)
    {
      localString = "Replication Server ";
      localString += serverId + " " + serverURL + " " + getServiceId();
    } else
      localString = "Unknown server";
    return localString;
  }
  /**
   * Gets the status of the connected DS.
   * @return The status of the connected DS.
   */
  public ServerStatus getStatus()
  {
    return ServerStatus.INVALID_STATUS;
  }
  /**
   * Retrieves the Address URL for this server handler.
   *
   * @return  The Address URL for this server handler,
   *          in the form of an IP address and port separated by a colon.
   */
  public String getServerAddressURL()
  {
    return serverAddressURL;
  }
}
opends/src/server/org/opends/server/replication/server/SafeDataExpectedAcksInfo.java
@@ -85,7 +85,7 @@
     * him with assured safe data mode, we double check here that the ack sender
     * is a RS to take the ack into account.
     */
     if (ackingServer.isLDAPserver())
     if (ackingServer.isDataServer())
     {
       // Sanity check: this should never happen
        if (debugEnabled())
opends/src/server/org/opends/server/replication/server/ServerHandler.java
@@ -26,181 +26,217 @@
 */
package org.opends.server.replication.server;
import org.opends.messages.*;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.*;
import static org.opends.server.replication.common.StatusMachine.*;
import org.opends.server.loggers.debug.DebugTracer;
import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.opends.messages.Message;
import org.opends.server.admin.std.server.MonitorProviderCfg;
import org.opends.server.api.MonitorProvider;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.replication.common.AssuredMode;
import org.opends.server.replication.common.ChangeNumber;
import org.opends.server.replication.common.DSInfo;
import org.opends.server.replication.common.RSInfo;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.common.ServerStatus;
import org.opends.server.replication.common.StatusMachine;
import org.opends.server.replication.common.StatusMachineEvent;
import org.opends.server.replication.protocol.*;
import org.opends.server.replication.protocol.AckMsg;
import org.opends.server.replication.protocol.ErrorMsg;
import org.opends.server.replication.protocol.HeartbeatThread;
import org.opends.server.replication.protocol.ProtocolSession;
import org.opends.server.replication.protocol.ProtocolVersion;
import org.opends.server.replication.protocol.ReplServerStartMsg;
import org.opends.server.replication.protocol.ResetGenerationIdMsg;
import org.opends.server.replication.protocol.RoutableMsg;
import org.opends.server.replication.protocol.StartECLSessionMsg;
import org.opends.server.replication.protocol.StartMsg;
import org.opends.server.replication.protocol.StartSessionMsg;
import org.opends.server.replication.protocol.TopologyMsg;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.replication.protocol.WindowMsg;
import org.opends.server.replication.protocol.WindowProbeMsg;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.Attributes;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import org.opends.server.types.ResultCode;
import org.opends.server.util.TimeThread;
/**
 * This class defines a server handler, which handles all interaction with a
 * peer server (RS or DS).
 * This class defines a server handler  :
 * - that is a MessageHandler (see this class for more details)
 * - that handles all interaction with a peer server (RS or DS).
 */
public class ServerHandler extends MonitorProvider<MonitorProviderCfg>
public abstract class ServerHandler extends MessageHandler
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();
  /**
   * Time during which the server will wait for existing thread to stop
   * during the shutdownWriter.
   */
  private static final int SHUTDOWN_JOIN_TIMEOUT = 30000;
  /*
   * Properties, filled if remote server is either a DS or a RS
  /**
   * Close the session and log the provided error message
   * Log nothing if message is null.
   * @param providedSession The provided closing session.
   * @param providedMsg     The provided error message.
   * @param handler         The handler that manages that session.
   */
  private short serverId;
  private ProtocolSession session;
  private final MsgQueue msgQueue = new MsgQueue();
  private MsgQueue lateQueue = new MsgQueue();
  private ReplicationServerDomain replicationServerDomain = null;
  private String serverURL;
  static protected void closeSession(ProtocolSession providedSession,
      Message providedMsg, ServerHandler handler)
  {
    if (providedMsg != null)
    {
      if (debugEnabled())
        TRACER.debugInfo("In "
            + ((handler!=null)?handler.toString():"Replication Server")
            + " closing session with err=" +
            providedMsg.toString());
      logError(providedMsg);
    }
    try
    {
      if (providedSession!=null)
        providedSession.close();
    } catch (IOException ee)
    {
      // ignore
    }
  }
  // Number of update sent to the server
  private int outCount = 0;
  // Number of updates received from the server
  private int inCount = 0;
  /**
   * The serverId of the remote server.
   */
  protected short serverId;
  /**
   * The session opened with the remote server.
   */
  protected ProtocolSession session;
  // Number of updates received from the server in assured safe read mode
  private int assuredSrReceivedUpdates = 0;
  // Number of updates received from the server in assured safe read mode that
  // timed out
  private AtomicInteger assuredSrReceivedUpdatesTimeout = new AtomicInteger();
  // Number of updates sent to the server in assured safe read mode
  private int assuredSrSentUpdates = 0;
  // Number of updates sent to the server in assured safe read mode that timed
  // out
  private AtomicInteger assuredSrSentUpdatesTimeout = new AtomicInteger();
  // Number of updates received from the server in assured safe data mode
  private int assuredSdReceivedUpdates = 0;
  // Number of updates received from the server in assured safe data mode that
  // timed out
  private AtomicInteger assuredSdReceivedUpdatesTimeout = new AtomicInteger();
  // Number of updates sent to the server in assured safe data mode
  private int assuredSdSentUpdates = 0;
  // Number of updates sent to the server in assured safe data mode that timed
  // out
  private AtomicInteger assuredSdSentUpdatesTimeout = new AtomicInteger();
  /**
   * The serverURL of the remote server.
   */
  protected String serverURL;
  /**
   * Number of updates received from the server in assured safe read mode.
   */
  protected int assuredSrReceivedUpdates = 0;
  /**
   * Number of updates received from the server in assured safe read mode that
   * timed out.
   */
  protected AtomicInteger assuredSrReceivedUpdatesTimeout = new AtomicInteger();
  /**
   * Number of updates sent to the server in assured safe read mode.
   */
  protected int assuredSrSentUpdates = 0;
  /**
   * Number of updates sent to the server in assured safe read mode that timed
   * out.
   */
  protected AtomicInteger assuredSrSentUpdatesTimeout = new AtomicInteger();
  /**
  // Number of updates received from the server in assured safe data mode.
   */
  protected int assuredSdReceivedUpdates = 0;
  /**
   * Number of updates received from the server in assured safe data mode that
   * timed out.
   */
  protected AtomicInteger assuredSdReceivedUpdatesTimeout = new AtomicInteger();
  /**
   * Number of updates sent to the server in assured safe data mode.
   */
  protected int assuredSdSentUpdates = 0;
  private int maxReceiveQueue = 0;
  private int maxSendQueue = 0;
  private int maxReceiveDelay = 0;
  private int maxSendDelay = 0;
  private int maxQueueSize = 5000;
  private int maxQueueBytesSize = maxQueueSize * 100;
  private int restartReceiveQueue;
  private int restartSendQueue;
  private int restartReceiveDelay;
  private int restartSendDelay;
  private boolean serverIsLDAPserver;
  private boolean following = false;
  private ServerState serverState;
  private boolean activeWriter = true;
  private ServerWriter writer = null;
  private String baseDn = null;
  /**
   * Number of updates sent to the server in assured safe data mode that timed
   * out.
   */
  protected AtomicInteger assuredSdSentUpdatesTimeout = new AtomicInteger();
  /**
   * The associated ServerWriter that sends messages to the remote server.
   */
  protected ServerReader reader;
  /**
   * The associated ServerReader that receives messages from the remote server.
   */
  protected ServerWriter writer = null;
  // window
  private int rcvWindow;
  private int rcvWindowSizeHalf;
  private int maxRcvWindow;
  private ServerReader reader;
  private Semaphore sendWindow;
  private int sendWindowSize;
  private boolean flowControl = false; // indicate that the server is
  // flow controlled and should
  // be stopped from sending messages.
  /**
   * Semaphore that the writer uses to control the flow to the remote server.
   */
  protected Semaphore sendWindow;
  /**
   * The initial size of the sending window.
   */
  int sendWindowSize;
  private int saturationCount = 0;
  private short replicationServerId;
  private short protocolVersion = -1;
  private long generationId = -1;
  // Group id of this remote server
  private byte groupId = (byte) -1;
  /*
   * Properties filled only if remote server is a DS
   */
  // Status of this DS (only used if this server handler represents a DS)
  private ServerStatus status = ServerStatus.INVALID_STATUS;
  // Referrals URLs this DS is exporting
  private List<String> refUrls = new ArrayList<String>();
  // Assured replication enabled on DS or not
  private boolean assuredFlag = false;
  // DS assured mode (relevant if assured replication enabled)
  private AssuredMode assuredMode = AssuredMode.SAFE_DATA_MODE;
  // DS safe data level (relevant if assured mode is safe data)
  private byte safeDataLevel = (byte) -1;
  /*
   * Properties filled only if remote server is a RS
   */
  private String serverAddressURL;
  /**
   * When this Handler is related to a remote replication server
   * this collection will contain as many elements as there are
   * LDAP servers connected to the remote replication server.
   * The protocol version established with the remote server.
   */
  private final Map<Short, LightweightServerHandler> directoryServers =
    new ConcurrentHashMap<Short, LightweightServerHandler>();
  protected short protocolVersion = -1;
  /**
   * remote generation id.
   */
  protected long generationId = -1;
  /**
   * The generation id of the hosting RS.
   */
  protected long localGenerationId = -1;
  /**
   * The generation id before procesing a new start handshake.
   */
  protected long oldGenerationId = -1;
  /**
   * Group id of this remote server.
   */
  protected byte groupId = (byte) -1;
  /**
   * The SSL encryption provided by the creator/starter of this handler.
   */
  protected boolean initSslEncryption;
  /**
   * The SSL encryption after the negociation with the peer.
   */
  protected boolean sslEncryption;
  /**
   * The time in milliseconds between heartbeats from the replication
   * server.  Zero means heartbeats are off.
   */
  private long heartbeatInterval = 0;
  protected long heartbeatInterval = 0;
  /**
   * The thread that will send heartbeats.
   */
  HeartbeatThread heartbeatThread = null;
  /**
   * Set when ServerWriter is stopping.
   */
  private boolean shutdownWriter = false;
  protected boolean shutdownWriter = false;
  /**
   * Set when ServerHandler is stopping.
   */
  private AtomicBoolean shuttingDown = new AtomicBoolean(false);
  /**
   * Creates a new server handler instance with the provided socket.
   *
@@ -208,846 +244,142 @@
   *                 communicate with the remote entity.
   * @param queueSize The maximum number of update that will be kept
   *                  in memory by this ServerHandler.
   * @param replicationServerURL The URL of the hosting replication server.
   * @param replicationServerId The serverId of the hosting replication server.
   * @param replicationServer The hosting replication server.
   * @param rcvWindowSize The window size to receive from the remote server.
   */
  public ServerHandler(ProtocolSession session, int queueSize)
  public ServerHandler(
      ProtocolSession session,
      int queueSize,
      String replicationServerURL,
      short replicationServerId,
      ReplicationServer replicationServer,
      int rcvWindowSize)
  {
    super("Server Handler");
    super(queueSize, replicationServerURL,
        replicationServerId, replicationServer);
    this.session = session;
    this.maxQueueSize = queueSize;
    this.maxQueueBytesSize = queueSize * 100;
    this.protocolVersion = ProtocolVersion.getCurrentVersion();
    this.rcvWindowSizeHalf = rcvWindowSize / 2;
    this.maxRcvWindow = rcvWindowSize;
    this.rcvWindow = rcvWindowSize;
  }
  /**
   * Creates a DSInfo structure representing this remote DS.
   * @return The DSInfo structure representing this remote DS
   * Abort a start procedure currently establishing.
   * @param reason The provided reason.
   */
  public DSInfo toDSInfo()
  protected void abortStart(Message reason)
  {
    DSInfo dsInfo = new DSInfo(serverId, replicationServerId, generationId,
      status, assuredFlag, assuredMode, safeDataLevel, groupId, refUrls);
    return dsInfo;
  }
  /**
   * Creates a RSInfo structure representing this remote RS.
   * @return The RSInfo structure representing this remote RS
   */
  public RSInfo toRSInfo()
  {
    RSInfo rsInfo = new RSInfo(serverId, generationId, groupId);
    return rsInfo;
  }
  /**
   * Do the handshake with either the DS or RS and then create the reader and
   * writer thread.
   *
   * There are 2 possible handshake sequences: DS<->RS and RS<->RS. Each one are
   * divided into 2 logical consecutive phases (phase 1 and phase 2):
   *
   * DS<->RS (DS (always initiating connection) always sends first message):
   * -------
   *
   * phase 1:
   * DS --- ServerStartMsg ---> RS
   * DS <--- ReplServerStartMsg --- RS
   * phase 2:
   * DS --- StartSessionMsg ---> RS
   * DS <--- TopologyMsg --- RS
   *
   * RS<->RS (RS initiating connection always sends first message):
   * -------
   *
   * phase 1:
   * RS1 --- ReplServerStartMsg ---> RS2
   * RS1 <--- ReplServerStartMsg --- RS2
   * phase 2:
   * RS1 --- TopologyMsg ---> RS2
   * RS1 <--- TopologyMsg --- RS2
   *
   * @param baseDn baseDn of the ServerHandler when this is an outgoing conn.
   *               null if this is an incoming connection (listen).
   * @param replicationServerId The identifier of the replicationServer that
   *                            creates this server handler.
   * @param replicationServerURL The URL of the replicationServer that creates
   *                             this server handler.
   * @param windowSize the window size that this server handler must use.
   * @param sslEncryption For outgoing connections indicates whether encryption
   *                      should be used after the exchange of start messages.
   *                      Ignored for incoming connections.
   * @param replicationServer the ReplicationServer that created this server
   *                          handler.
   */
  public void start(String baseDn, short replicationServerId,
    String replicationServerURL,
    int windowSize, boolean sslEncryption,
    ReplicationServer replicationServer)
  {
    // The handshake phase must be done by blocking any access to structures
    // keeping info on connected servers, so that one can safely check for
    // pre-existence of a server, send a coherent snapshot of known topology
    // to peers, update the local view of the topology...
    //
    // For instance a kind of problem could be that while we connect with a
    // peer RS, a DS is connecting at the same time and we could publish the
    // connected DSs to the peer RS forgetting this last DS in the TopologyMsg.
    //
    // This method and every others that need to read/make changes to the
    // structures holding topology for the domain should:
    // - call ReplicationServerDomain.lock()
    // - read/modify structures
    // - call ReplicationServerDomain.release()
    //
    // More information is provided in comment of ReplicationServerDomain.lock()
    // If domain already exists, lock it until handshake is finished otherwise
    // it will be created and locked later in the method
    if (baseDn != null)
    // We did not recognize the message, close session as what
    // can happen after is undetermined and we do not want the server to
    // be disturbed
    if (session!=null)
    {
      ReplicationServerDomain rsd =
        replicationServer.getReplicationServerDomain(baseDn, false);
      if (rsd != null)
      try
      {
        try
        {
          rsd.lock();
        } catch (InterruptedException ex)
        {
          // Thread interrupted, return.
          return;
        }
        session.publish(
            new ErrorMsg(
                replicationServerDomain.getReplicationServer().getServerId(),
                serverId,
                reason));
      }
      catch(Exception e)
      {
      }
      closeSession(session, reason, this);
    }
    long oldGenerationId = -100;
    if ((replicationServerDomain != null) &&
        replicationServerDomain.hasLock())
      replicationServerDomain.release();
    if (debugEnabled())
      TRACER.debugInfo("In " + replicationServer.getMonitorInstanceName() +
        " starts a new LS or RS " +
        ((baseDn == null) ? "incoming connection" : "outgoing connection"));
    this.replicationServerId = replicationServerId;
    rcvWindowSizeHalf = windowSize / 2;
    maxRcvWindow = windowSize;
    rcvWindow = windowSize;
    long localGenerationId = -1;
    ReplServerStartMsg outReplServerStartMsg = null;
    /**
     * This boolean prevents from logging a polluting error when connection\
     * aborted from a DS that wanted only to perform handshake phase 1 in order
     * to determine the best suitable RS:
     * 1) -> ServerStartMsg
     * 2) <- ReplServerStartMsg
     * 3) connection closure
     */
    boolean log_error_message = true;
    try
    // If generation id of domain was changed, set it back to old value
    // We may have changed it as it was -1 and we received a value >0 from
    // peer server and the last topo message sent may have failed being
    // sent: in that case retrieve old value of generation id for
    // replication server domain
    if (oldGenerationId != -100)
    {
      /*
       * PROCEDE WITH FIRST PHASE OF HANDSHAKE:
       * ServerStartMsg then ReplServerStartMsg (with a DS)
       * OR
       * ReplServerStartMsg then ReplServerStartMsg (with a RS)
       */
      replicationServerDomain.setGenerationId(oldGenerationId, false);
    }
  }
      if (baseDn != null) // Outgoing connection
  /**
   * Check the protocol window and send WindowMsg if necessary.
   *
   * @throws IOException when the session becomes unavailable.
   */
  public synchronized void checkWindow() throws IOException
  {
    if (rcvWindow < rcvWindowSizeHalf)
    {
      if (flowControl)
      {
        // This is an outgoing connection. Publish our start message.
        this.baseDn = baseDn;
        // Get or create the ReplicationServerDomain
        replicationServerDomain =
          replicationServer.getReplicationServerDomain(baseDn, true);
        if (!replicationServerDomain.hasLock())
        if (replicationServerDomain.restartAfterSaturation(this))
        {
          try
          {
            replicationServerDomain.lock();
          } catch (InterruptedException ex)
          {
            // Thread interrupted, return.
            return;
          }
          flowControl = false;
        }
        localGenerationId = replicationServerDomain.getGenerationId();
      }
      if (!flowControl)
      {
        WindowMsg msg = new WindowMsg(rcvWindowSizeHalf);
        session.publish(msg);
        rcvWindow += rcvWindowSizeHalf;
      }
    }
  }
        ServerState localServerState =
          replicationServerDomain.getDbServerState();
        outReplServerStartMsg = new ReplServerStartMsg(replicationServerId,
          replicationServerURL,
          baseDn, windowSize, localServerState,
          protocolVersion, localGenerationId,
          sslEncryption,
          replicationServer.getGroupId(),
          replicationServerDomain.
          getReplicationServer().getDegradedStatusThreshold());
  /**
   * Decrement the protocol window, then check if it is necessary
   * to send a WindowMsg and send it.
   *
   * @throws IOException when the session becomes unavailable.
   */
  public synchronized void decAndCheckWindow() throws IOException
  {
    rcvWindow--;
    checkWindow();
  }
        session.publish(outReplServerStartMsg);
  /**
   * Set the shut down flag to true and returns the previous value of the flag.
   * @return The previous value of the shut down flag
   */
  public boolean engageShutdown()
  {
    // Use thread safe boolean
    return shuttingDown.getAndSet(true);
  }
  /**
   * Finalize the initialization, create reader, writer, heartbeat system
   * and monitoring system.
   * @throws DirectoryException When an exception is raised.
   */
  protected void finalizeStart()
  throws DirectoryException
  {
    // FIXME:ECL We should refactor so that a SH always have a session
    if (session != null)
    {
      try
      {
        // Disable timeout for next communications
        session.setSoTimeout(0);
      }
      catch(Exception e)
      {
      }
      // Wait and process ServerStartMsg or ReplServerStartMsg
      ReplicationMsg msg = session.receive();
      if (msg instanceof ServerStartMsg)
      {
        // The remote server is an LDAP Server.
        ServerStartMsg serverStartMsg = (ServerStartMsg) msg;
        generationId = serverStartMsg.getGenerationId();
        protocolVersion = ProtocolVersion.minWithCurrent(
          serverStartMsg.getVersion());
        serverId = serverStartMsg.getServerId();
        serverURL = serverStartMsg.getServerURL();
        this.baseDn = serverStartMsg.getBaseDn();
        this.serverState = serverStartMsg.getServerState();
        this.groupId = serverStartMsg.getGroupId();
        maxReceiveDelay = serverStartMsg.getMaxReceiveDelay();
        maxReceiveQueue = serverStartMsg.getMaxReceiveQueue();
        maxSendDelay = serverStartMsg.getMaxSendDelay();
        maxSendQueue = serverStartMsg.getMaxSendQueue();
        heartbeatInterval = serverStartMsg.getHeartbeatInterval();
        // The session initiator decides whether to use SSL.
        sslEncryption = serverStartMsg.getSSLEncryption();
        if (maxReceiveQueue > 0)
          restartReceiveQueue = (maxReceiveQueue > 1000 ? maxReceiveQueue -
            200 : maxReceiveQueue * 8 / 10);
        else
          restartReceiveQueue = 0;
        if (maxSendQueue > 0)
          restartSendQueue =
            (maxSendQueue > 1000 ? maxSendQueue - 200 : maxSendQueue * 8 /
            10);
        else
          restartSendQueue = 0;
        if (maxReceiveDelay > 0)
          restartReceiveDelay = (maxReceiveDelay > 10 ? maxReceiveDelay - 1
            : maxReceiveDelay);
        else
          restartReceiveDelay = 0;
        if (maxSendDelay > 0)
          restartSendDelay =
            (maxSendDelay > 10 ? maxSendDelay - 1 : maxSendDelay);
        else
          restartSendDelay = 0;
        if (heartbeatInterval < 0)
        {
          heartbeatInterval = 0;
        }
        serverIsLDAPserver = true;
        // Get or Create the ReplicationServerDomain
        replicationServerDomain =
          replicationServer.getReplicationServerDomain(this.baseDn, true);
        // Hack to be sure that if a server disconnects and reconnect, we
        // let the reader thread see the closure and cleanup any reference
        // to old connection
        replicationServerDomain.waitDisconnection(serverStartMsg.getServerId());
        if (!replicationServerDomain.hasLock())
        {
          try
          {
            replicationServerDomain.lock();
          } catch (InterruptedException ex)
          {
            // Thread interrupted, return.
            return;
          }
        }
        // Duplicate server ?
        if (!replicationServerDomain.checkForDuplicateDS(this))
        {
          closeSession(null);
          if ((replicationServerDomain != null) &&
            replicationServerDomain.hasLock())
            replicationServerDomain.release();
          return;
        }
        localGenerationId = replicationServerDomain.getGenerationId();
        ServerState localServerState =
          replicationServerDomain.getDbServerState();
        // This an incoming connection. Publish our start message
        ReplServerStartMsg replServerStartMsg =
          new ReplServerStartMsg(replicationServerId, replicationServerURL,
          this.baseDn, windowSize, localServerState,
          protocolVersion, localGenerationId,
          sslEncryption,
          replicationServer.getGroupId(),
          replicationServerDomain.
          getReplicationServer().getDegradedStatusThreshold());
        session.publish(replServerStartMsg);
        sendWindowSize = serverStartMsg.getWindowSize();
        /* Until here session is encrypted then it depends on the
        negotiation */
        if (!sslEncryption)
        {
          session.stopEncryption();
        }
        if (debugEnabled())
        {
          TRACER.debugInfo("In " +
            replicationServerDomain.getReplicationServer().
            getMonitorInstanceName() + ":" +
            "\nSH HANDSHAKE RECEIVED:\n" + serverStartMsg.toString() +
            "\nAND REPLIED:\n" + replServerStartMsg.toString());
        }
      } else if (msg instanceof ReplServerStartMsg)
      {
        // The remote server is a replication server
        ReplServerStartMsg inReplServerStartMsg = (ReplServerStartMsg) msg;
        protocolVersion = ProtocolVersion.minWithCurrent(
          inReplServerStartMsg.getVersion());
        generationId = inReplServerStartMsg.getGenerationId();
        serverId = inReplServerStartMsg.getServerId();
        serverURL = inReplServerStartMsg.getServerURL();
        int separator = serverURL.lastIndexOf(':');
        serverAddressURL =
          session.getRemoteAddress() + ":" + serverURL.substring(separator +
          1);
        serverIsLDAPserver = false;
        if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1)
        {
          // We support connection from a V1 RS
          // Only V2 protocol has the group id in repl server start message
          this.groupId = inReplServerStartMsg.getGroupId();
        }
        this.baseDn = inReplServerStartMsg.getBaseDn();
        if (baseDn == null) // Reply to incoming RS
        {
          // Get or create the ReplicationServerDomain
          replicationServerDomain =
            replicationServer.getReplicationServerDomain(this.baseDn, true);
          if (!replicationServerDomain.hasLock())
          {
            try
            {
              /**
               * Take the lock on the domain.
               * WARNING: Here we try to acquire the lock with a timeout. This
               * is for preventing a deadlock that may happen if there are cross
               * connection attempts (for same domain) from this replication
               * server and from a peer one:
               * Here is the scenario:
               * - RS1 connect thread takes the domain lock and starts
               * connection to RS2
               * - at the same time RS2 connect thread takes his domain lock and
               * start connection to RS2
               * - RS2 listen thread starts processing received
               * ReplServerStartMsg from RS1 and wants to acquire the lock on
               * the domain (here) but cannot as RS2 connect thread already has
               * it
               * - RS1 listen thread starts processing received
               * ReplServerStartMsg from RS2 and wants to acquire the lock on
               * the domain (here) but cannot as RS1 connect thread already has
               * it
               * => Deadlock: 4 threads are locked.
               * So to prevent that in such situation, the listen threads here
               * will both timeout trying to acquire the lock. The random time
               * for the timeout should allow on connection attempt to be
               * aborted whereas the other one should have time to finish in the
               * same time.
               * Warning: the minimum time (3s) should be big enough to allow
               * normal situation connections to terminate. The added random
               * time should represent a big enough range so that the chance to
               * have one listen thread timing out a lot before the peer one is
               * great. When the first listen thread times out, the remote
               * connect thread should release the lock and allow the peer
               * listen thread to take the lock it was waiting for and process
               * the connection attempt.
               */
              Random random = new Random();
              int randomTime = random.nextInt(6); // Random from 0 to 5
              // Wait at least 3 seconds + (0 to 5 seconds)
              long timeout = (long) (3000 + ( randomTime * 1000 ) );
              boolean noTimeout = replicationServerDomain.tryLock(timeout);
              if (!noTimeout)
              {
                // Timeout
                Message message = NOTE_TIMEOUT_WHEN_CROSS_CONNECTION.get(
                  this.baseDn,
                  Short.toString(serverId),
                  Short.toString(replicationServer.getServerId()));
                closeSession(message);
                return;
              }
            } catch (InterruptedException ex)
            {
              // Thread interrupted, return.
              return;
            }
          }
          localGenerationId = replicationServerDomain.getGenerationId();
          ServerState domServerState =
            replicationServerDomain.getDbServerState();
          // The session initiator decides whether to use SSL.
          sslEncryption = inReplServerStartMsg.getSSLEncryption();
          // Publish our start message
          outReplServerStartMsg = new ReplServerStartMsg(replicationServerId,
            replicationServerURL,
            this.baseDn, windowSize, domServerState,
            protocolVersion,
            localGenerationId,
            sslEncryption,
            replicationServer.getGroupId(),
            replicationServerDomain.
            getReplicationServer().getDegradedStatusThreshold());
          if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1)
          {
            session.publish(outReplServerStartMsg);
          } else {
            // We support connection from a V1 RS, send PDU with V1 form
            session.publish(outReplServerStartMsg,
              ProtocolVersion.REPLICATION_PROTOCOL_V1);
          }
          if (debugEnabled())
          {
            TRACER.debugInfo("In " +
              replicationServerDomain.getReplicationServer().
              getMonitorInstanceName() + ":" +
              "\nSH HANDSHAKE RECEIVED:\n" + inReplServerStartMsg.toString() +
              "\nAND REPLIED:\n" + outReplServerStartMsg.toString());
          }
        } else
        {
          // Did the remote RS answer with the DN we provided him ?
          if (!(this.baseDn.equals(baseDn)))
          {
            Message message = ERR_RS_DN_DOES_NOT_MATCH.get(
              this.baseDn.toString(),
              baseDn.toString());
            closeSession(message);
            if ((replicationServerDomain != null) &&
              replicationServerDomain.hasLock())
              replicationServerDomain.release();
            return;
          }
          if (debugEnabled())
          {
            TRACER.debugInfo("In " +
              replicationServerDomain.getReplicationServer().
              getMonitorInstanceName() + ":" +
              "\nSH HANDSHAKE SENT:\n" + outReplServerStartMsg.toString() +
              "\nAND RECEIVED:\n" + inReplServerStartMsg.toString());
          }
        }
        this.serverState = inReplServerStartMsg.getServerState();
        sendWindowSize = inReplServerStartMsg.getWindowSize();
        // Duplicate server ?
        if (!replicationServerDomain.checkForDuplicateRS(this))
        {
          closeSession(null);
          if ((replicationServerDomain != null) &&
            replicationServerDomain.hasLock())
            replicationServerDomain.release();
          return;
        }
        /* Until here session is encrypted then it depends on the
        negociation */
        if (!sslEncryption)
        {
          session.stopEncryption();
        }
      } else
      {
        // We did not recognize the message, close session as what
        // can happen after is undetermined and we do not want the server to
        // be disturbed
        closeSession(null);
        if ((replicationServerDomain != null) &&
          replicationServerDomain.hasLock())
          replicationServerDomain.release();
        return;
      }
      if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1)
      { // Only protocol version above V1 has a phase 2 handshake
        /*
         * NOW PROCEDE WITH SECOND PHASE OF HANDSHAKE:
         * TopologyMsg then TopologyMsg (with a RS)
         * OR
         * StartSessionMsg then TopologyMsg (with a DS)
         */
        TopologyMsg outTopoMsg = null;
        if (baseDn != null) // Outgoing connection to a RS
        {
          // Send our own TopologyMsg to remote RS
          outTopoMsg = replicationServerDomain.createTopologyMsgForRS();
          session.publish(outTopoMsg);
        }
        // Wait and process TopologyMsg or StartSessionMsg
        log_error_message = false;
        ReplicationMsg msg2 = session.receive();
        log_error_message = true;
        if (msg2 instanceof TopologyMsg)
        {
          // Remote RS sent his topo msg
          TopologyMsg inTopoMsg = (TopologyMsg) msg2;
          // CONNECTION WITH A RS
          // if the remote RS and the local RS have the same genID
          // then it's ok and nothing else to do
          if (generationId == localGenerationId)
          {
            if (debugEnabled())
            {
              TRACER.debugInfo("In " +
                replicationServerDomain.getReplicationServer().
                getMonitorInstanceName() + " RS with serverID=" + serverId +
                " is connected with the right generation ID");
            }
          } else
          {
            if (localGenerationId > 0)
            {
              // if the local RS is initialized
              if (generationId > 0)
              {
                // if the remote RS is initialized
                if (generationId != localGenerationId)
                {
                  // if the 2 RS have different generationID
                  if (replicationServerDomain.getGenerationIdSavedStatus())
                  {
                    // if the present RS has received changes regarding its
                    //     gen ID and so won't change without a reset
                    // then  we are just degrading the peer.
                    Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get(
                      this.baseDn,
                      Short.toString(serverId),
                      Long.toString(generationId),
                      Long.toString(localGenerationId));
                    logError(message);
                  } else
                  {
                    // The present RS has never received changes regarding its
                    // gen ID.
                    //
                    // Example case:
                    // - we are in RS1
                    // - RS2 has genId2 from LS2 (genId2 <=> no data in LS2)
                    // - RS1 has genId1 from LS1 /genId1 comes from data in
                    //   suffix
                    // - we are in RS1 and we receive a START msg from RS2
                    // - Each RS keeps its genID / is degraded and when LS2
                    //   will be populated from LS1 everything will become ok.
                    //
                    // Issue:
                    // FIXME : Would it be a good idea in some cases to just
                    //         set the gen ID received from the peer RS
                    //         specially if the peer has a non null state and
                    //         we have a nul state ?
                    // replicationServerDomain.
                    // setGenerationId(generationId, false);
                    Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get(
                      this.baseDn,
                      Short.toString(serverId),
                      Long.toString(generationId),
                      Long.toString(localGenerationId));
                    logError(message);
                  }
                }
              } else
              {
                // The remote RS has no genId. We don't change anything for the
                // current RS.
              }
            } else
            {
              // The local RS is not initialized - take the one received
              // WARNING: Must be done before computing topo message to send
              // to peer server as topo message must embed valid generation id
              // for our server
              oldGenerationId =
                replicationServerDomain.setGenerationId(generationId, false);
            }
          }
          if (baseDn == null) // Reply to the RS (incoming connection)
          {
            // Send our own TopologyMsg to remote RS
            outTopoMsg = replicationServerDomain.createTopologyMsgForRS();
            session.publish(outTopoMsg);
            if (debugEnabled())
            {
              TRACER.debugInfo("In " +
                replicationServerDomain.getReplicationServer().
                getMonitorInstanceName() + ":" +
                "\nSH HANDSHAKE RECEIVED:\n" + inTopoMsg.toString() +
                "\nAND REPLIED:\n" + outTopoMsg.toString());
            }
          } else
          {
            if (debugEnabled())
            {
              TRACER.debugInfo("In " +
                replicationServerDomain.getReplicationServer().
                getMonitorInstanceName() + ":" +
                "\nSH HANDSHAKE SENT:\n" + outTopoMsg.toString() +
                "\nAND RECEIVED:\n" + inTopoMsg.toString());
            }
          }
          // Alright, connected with new RS (either outgoing or incoming
          // connection): store handler.
          Map<Short, ServerHandler> connectedRSs =
            replicationServerDomain.getConnectedRSs();
          connectedRSs.put(serverId, this);
          // Process TopologyMsg sent by remote RS: store matching new info
          // (this will also warn our connected DSs of the new received info)
          replicationServerDomain.receiveTopoInfoFromRS(inTopoMsg, this, false);
        } else if (msg2 instanceof StartSessionMsg)
        {
          // CONNECTION WITH A DS
          // Process StartSessionMsg sent by remote DS
          StartSessionMsg startSessionMsg = (StartSessionMsg) msg2;
          this.status = startSessionMsg.getStatus();
          // Sanity check: is it a valid initial status?
          if (!isValidInitialStatus(this.status))
          {
            Message mesg = ERR_RS_INVALID_INIT_STATUS.get(
              this.status.toString(), this.baseDn.toString(),
              Short.toString(serverId));
            closeSession(mesg);
            if ((replicationServerDomain != null) &&
              replicationServerDomain.hasLock())
              replicationServerDomain.release();
            return;
          }
          this.refUrls = startSessionMsg.getReferralsURLs();
          this.assuredFlag = startSessionMsg.isAssured();
          this.assuredMode = startSessionMsg.getAssuredMode();
          this.safeDataLevel = startSessionMsg.getSafeDataLevel();
          /*
           * If we have already a generationID set for the domain
           * then
           *   if the connecting replica has not the same
           *   then it is degraded locally and notified by an error message
           * else
           *   we set the generationID from the one received
           *   (unsaved yet on disk . will be set with the 1rst change
           * received)
           */
          if (localGenerationId > 0)
          {
            if (generationId != localGenerationId)
            {
              Message message = NOTE_BAD_GENERATION_ID_FROM_DS.get(
                this.baseDn,
                Short.toString(serverId),
                Long.toString(generationId),
                Long.toString(localGenerationId));
              logError(message);
            }
          } else
          {
            // We are an empty Replicationserver
            if ((generationId > 0) && (!serverState.isEmpty()))
            {
              // If the LDAP server has already sent changes
              // it is not expected to connect to an empty RS
              Message message = NOTE_BAD_GENERATION_ID_FROM_DS.get(
                this.baseDn,
                Short.toString(serverId),
                Long.toString(generationId),
                Long.toString(localGenerationId));
              logError(message);
            } else
            {
              // The local RS is not initialized - take the one received
              // WARNING: Must be done before computing topo message to send
              // to peer server as topo message must embed valid generation id
              // for our server
              oldGenerationId =
                replicationServerDomain.setGenerationId(generationId, false);
            }
          }
          // Send our own TopologyMsg to DS
          outTopoMsg = replicationServerDomain.createTopologyMsgForDS(
            this.serverId);
          session.publish(outTopoMsg);
          if (debugEnabled())
          {
            TRACER.debugInfo("In " +
              replicationServerDomain.getReplicationServer().
              getMonitorInstanceName() + ":" +
              "\nSH HANDSHAKE RECEIVED:\n" + startSessionMsg.toString() +
              "\nAND REPLIED:\n" + outTopoMsg.toString());
          }
          // Alright, connected with new DS: store handler.
          Map<Short, ServerHandler> connectedDSs =
            replicationServerDomain.getConnectedDSs();
          connectedDSs.put(serverId, this);
          // Tell peer DSs a new DS just connected to us
          // No need to resend topo msg to this just new DS so not null
          // argument
          replicationServerDomain.sendTopoInfoToDSs(this);
          // Tell peer RSs a new DS just connected to us
          replicationServerDomain.sendTopoInfoToRSs();
        } else
        {
          // We did not recognize the message, close session as what
          // can happen after is undetermined and we do not want the server to
          // be disturbed
          closeSession(null);
          if ((replicationServerDomain != null) &&
            replicationServerDomain.hasLock())
            replicationServerDomain.release();
          return;
        }
      } else
      {
        // Terminate connection from a V1 RS
        // if the remote RS and the local RS have the same genID
        // then it's ok and nothing else to do
        if (generationId == localGenerationId)
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("In " +
              replicationServerDomain.getReplicationServer().
              getMonitorInstanceName() + " RS V1 with serverID=" + serverId +
              " is connected with the right generation ID");
          }
        } else
        {
          if (localGenerationId > 0)
          {
            // if the local RS is initialized
            if (generationId > 0)
            {
              // if the remote RS is initialized
              if (generationId != localGenerationId)
              {
                // if the 2 RS have different generationID
                if (replicationServerDomain.getGenerationIdSavedStatus())
                {
                  // if the present RS has received changes regarding its
                  //     gen ID and so won't change without a reset
                  // then  we are just degrading the peer.
                  Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get(
                    this.baseDn,
                    Short.toString(serverId),
                    Long.toString(generationId),
                    Long.toString(localGenerationId));
                  logError(message);
                } else
                {
                  // The present RS has never received changes regarding its
                  // gen ID.
                  //
                  // Example case:
                  // - we are in RS1
                  // - RS2 has genId2 from LS2 (genId2 <=> no data in LS2)
                  // - RS1 has genId1 from LS1 /genId1 comes from data in
                  //   suffix
                  // - we are in RS1 and we receive a START msg from RS2
                  // - Each RS keeps its genID / is degraded and when LS2
                  //   will be populated from LS1 everything will become ok.
                  //
                  // Issue:
                  // FIXME : Would it be a good idea in some cases to just
                  //         set the gen ID received from the peer RS
                  //         specially if the peer has a non null state and
                  //         we have a nul state ?
                  // replicationServerDomain.
                  // setGenerationId(generationId, false);
                  Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get(
                    this.baseDn,
                    Short.toString(serverId),
                    Long.toString(generationId),
                    Long.toString(localGenerationId));
                  logError(message);
                }
              }
            } else
            {
              // The remote RS has no genId. We don't change anything for the
              // current RS.
            }
          } else
          {
            // The local RS is not initialized - take the one received
            oldGenerationId =
              replicationServerDomain.setGenerationId(generationId, false);
          }
        }
        // Alright, connected with new incoming V1 RS: store handler.
        Map<Short, ServerHandler> connectedRSs =
          replicationServerDomain.getConnectedRSs();
        connectedRSs.put(serverId, this);
        // Note: the supported scenario for V1->V2 upgrade is to upgrade 1 by 1
        // all the servers of the topology. We prefer not not send a TopologyMsg
        // for giving partial/false information to the V2 servers as for
        // instance we don't have the connected DS of the V1 RS...When the V1
        // RS will be upgraded in his turn, topo info will be sent and accurate.
        // That way, there is  no risk to have false/incomplete information in
        // other servers.
      }
      /*
       * FINALIZE INITIALIZATION:
       * CREATE READER AND WRITER, HEARTBEAT SYSTEM AND UPDATE MONITORING
       * SYSTEM
       */
      // Disable timeout for next communications
      session.setSoTimeout(0);
      // sendWindow MUST be created before starting the writer
      sendWindow = new Semaphore(sendWindowSize);
      writer = new ServerWriter(session, serverId,
        this, replicationServerDomain);
          this, replicationServerDomain);
      reader = new ServerReader(session, serverId,
        this, replicationServerDomain);
          this, replicationServerDomain);
      reader.start();
      writer.start();
@@ -1056,440 +388,30 @@
      if (heartbeatInterval > 0)
      {
        heartbeatThread = new HeartbeatThread(
          "Replication Heartbeat to DS " + serverURL + " " + serverId +
          " for " + this.baseDn + " in RS " + replicationServerId,
          session, heartbeatInterval / 3);
            "Replication Heartbeat to " + this +
            " in RS " + replicationServerDomain.getReplicationServer().
            getMonitorInstanceName(),
            session, heartbeatInterval / 3);
        heartbeatThread.start();
      }
      // Create the status analyzer for the domain if not already started
      if (serverIsLDAPserver)
      {
        if (!replicationServerDomain.isRunningStatusAnalyzer())
        {
          if (debugEnabled())
            TRACER.debugInfo("In " + replicationServerDomain.
              getReplicationServer().
              getMonitorInstanceName() +
              " SH for remote server " + this.getMonitorInstanceName() +
              " is starting status analyzer");
          replicationServerDomain.startStatusAnalyzer();
        }
      }
      DirectoryServer.deregisterMonitorProvider(getMonitorInstanceName());
      DirectoryServer.registerMonitorProvider(this);
    } catch (NotSupportedOldVersionPDUException e)
    {
      // We do not need to support DS V1 connection, we just accept RS V1
      // connection:
      // We just trash the message, log the event for debug purpose and close
      // the connection
      if (debugEnabled())
      TRACER.debugInfo("In " + replicationServer.getMonitorInstanceName() + ":"
        + e.getMessage());
      closeSession(null);
    } catch (Exception e)
    {
      // We do not want polluting error log if error is due to normal session
      // aborted after handshake phase one from a DS that is searching for best
      // suitable RS.
      if ( log_error_message || (baseDn != null) )
      {
        // some problem happened, reject the connection
        MessageBuilder mb = new MessageBuilder();
        mb.append(ERR_REPLICATION_SERVER_CONNECTION_ERROR.get(
          this.getMonitorInstanceName()));
        mb.append(": " + stackTraceToSingleLineString(e));
        closeSession(mb.toMessage());
      } else
      {
        closeSession(null);
      }
      // If generation id of domain was changed, set it back to old value
      // We may have changed it as it was -1 and we received a value >0 from
      // peer server and the last topo message sent may have failed being
      // sent: in that case retrieve old value of generation id for
      // replication server domain
      if (oldGenerationId != -100)
      {
        replicationServerDomain.setGenerationId(oldGenerationId, false);
      }
    }
    // Release domain
    if ((replicationServerDomain != null) &&
      replicationServerDomain.hasLock())
      replicationServerDomain.release();
  }
  /*
   * Close the session logging the passed error message
   * Log nothing if message is null.
   */
  private void closeSession(Message msg)
  {
    if (msg != null)
    {
      logError(msg);
    }
    try
    {
      session.close();
    } catch (IOException ee)
    {
      // ignore
    }
    DirectoryServer.deregisterMonitorProvider(getMonitorInstanceName());
    DirectoryServer.registerMonitorProvider(this);
  }
  /**
   * get the Server Id.
   * Sends a message containing a generationId to a peer server.
   * The peer is expected to be a replication server.
   *
   * @return the ID of the server to which this object is linked
   */
  public short getServerId()
  {
    return serverId;
  }
  /**
   * Retrieves the Address URL for this server handler.
   * @param  msg         The GenerationIdMessage message to be sent.
   * @throws IOException When it occurs while sending the message,
   *
   * @return  The Address URL for this server handler,
   *          in the form of an IP address and port separated by a colon.
   */
  public String getServerAddressURL()
  public void forwardGenerationIdToRS(ResetGenerationIdMsg msg)
  throws IOException
  {
    return serverAddressURL;
  }
  /**
   * Retrieves the URL for this server handler.
   *
   * @return  The URL for this server handler, in the form of an address and
   *          port separated by a colon.
   */
  public String getServerURL()
  {
    return serverURL;
  }
  /**
   * Increase the counter of updates sent to the server.
   */
  public void incrementOutCount()
  {
    outCount++;
  }
  /**
   * Increase the counter of update received from the server.
   */
  public void incrementInCount()
  {
    inCount++;
  }
  /**
   * Get the count of updates received from the server.
   * @return the count of update received from the server.
   */
  public int getInCount()
  {
    return inCount;
  }
  /**
   * Get the count of updates sent to this server.
   * @return  The count of update sent to this server.
   */
  public int getOutCount()
  {
    return outCount;
  }
  /**
   * Get the number of updates received from the server in assured safe read
   * mode.
   * @return The number of updates received from the server in assured safe read
   * mode
   */
  public int getAssuredSrReceivedUpdates()
  {
    return assuredSrReceivedUpdates;
  }
  /**
   * Get the number of updates received from the server in assured safe read
   * mode that timed out.
   * @return The number of updates received from the server in assured safe read
   * mode that timed out.
   */
  public AtomicInteger getAssuredSrReceivedUpdatesTimeout()
  {
    return assuredSrReceivedUpdatesTimeout;
  }
  /**
   * Get the number of updates sent to the server in assured safe read mode.
   * @return The number of updates sent to the server in assured safe read mode
   */
  public int getAssuredSrSentUpdates()
  {
    return assuredSrSentUpdates;
  }
  /**
   * Get the number of updates sent to the server in assured safe read mode that
   * timed out.
   * @return The number of updates sent to the server in assured safe read mode
   * that timed out.
   */
  public AtomicInteger getAssuredSrSentUpdatesTimeout()
  {
    return assuredSrSentUpdatesTimeout;
  }
    /**
   * Get the number of updates received from the server in assured safe data
   * mode.
   * @return The number of updates received from the server in assured safe data
   * mode
   */
  public int getAssuredSdReceivedUpdates()
  {
    return assuredSdReceivedUpdates;
  }
  /**
   * Get the number of updates received from the server in assured safe data
   * mode that timed out.
   * @return The number of updates received from the server in assured safe data
   * mode that timed out.
   */
  public AtomicInteger getAssuredSdReceivedUpdatesTimeout()
  {
    return assuredSdReceivedUpdatesTimeout;
  }
  /**
   * Get the number of updates sent to the server in assured safe data mode.
   * @return The number of updates sent to the server in assured safe data mode
   */
  public int getAssuredSdSentUpdates()
  {
    return assuredSdSentUpdates;
  }
  /**
   * Get the number of updates sent to the server in assured safe data mode that
   * timed out.
   * @return The number of updates sent to the server in assured safe data mode
   * that timed out.
   */
  public AtomicInteger getAssuredSdSentUpdatesTimeout()
  {
    return assuredSdSentUpdatesTimeout;
  }
  /**
   * Increment the number of updates received from the server in assured safe
   * read mode.
   */
  public void incrementAssuredSrReceivedUpdates()
  {
    assuredSrReceivedUpdates++;
  }
  /**
   * Increment the number of updates received from the server in assured safe
   * read mode that timed out.
   */
  public void incrementAssuredSrReceivedUpdatesTimeout()
  {
    assuredSrReceivedUpdatesTimeout.incrementAndGet();
  }
  /**
   * Increment the number of updates sent to the server in assured safe read
   * mode.
   */
  public void incrementAssuredSrSentUpdates()
  {
    assuredSrSentUpdates++;
  }
  /**
   * Increment the number of updates sent to the server in assured safe read
   * mode that timed out.
   */
  public void incrementAssuredSrSentUpdatesTimeout()
  {
    assuredSrSentUpdatesTimeout.incrementAndGet();
  }
  /**
   * Increment the number of updates received from the server in assured safe
   * data mode.
   */
  public void incrementAssuredSdReceivedUpdates()
  {
    assuredSdReceivedUpdates++;
  }
  /**
   * Increment the number of updates received from the server in assured safe
   * data mode that timed out.
   */
  public void incrementAssuredSdReceivedUpdatesTimeout()
  {
    assuredSdReceivedUpdatesTimeout.incrementAndGet();
  }
  /**
   * Increment the number of updates sent to the server in assured safe data
   * mode.
   */
  public void incrementAssuredSdSentUpdates()
  {
    assuredSdSentUpdates++;
  }
  /**
   * Increment the number of updates sent to the server in assured safe data
   * mode that timed out.
   */
  public void incrementAssuredSdSentUpdatesTimeout()
  {
    assuredSdSentUpdatesTimeout.incrementAndGet();
  }
  /**
   * Check is this server is saturated (this server has already been
   * sent a bunch of updates and has not processed them so they are staying
   * in the message queue for this server an the size of the queue
   * for this server is above the configured limit.
   *
   * The limit can be defined in number of updates or with a maximum delay
   *
   * @param changeNumber The changenumber to use to make the delay calculations.
   * @param sourceHandler The ServerHandler which is sending the update.
   * @return true is saturated false if not saturated.
   */
  public boolean isSaturated(ChangeNumber changeNumber,
    ServerHandler sourceHandler)
  {
    synchronized (msgQueue)
    {
      int size = msgQueue.count();
      if ((maxReceiveQueue > 0) && (size >= maxReceiveQueue))
        return true;
      if ((sourceHandler.maxSendQueue > 0) &&
        (size >= sourceHandler.maxSendQueue))
        return true;
      if (!msgQueue.isEmpty())
      {
        UpdateMsg firstUpdate = msgQueue.first();
        if (firstUpdate != null)
        {
          long timeDiff = changeNumber.getTimeSec() -
            firstUpdate.getChangeNumber().getTimeSec();
          if ((maxReceiveDelay > 0) && (timeDiff >= maxReceiveDelay))
            return true;
          if ((sourceHandler.maxSendDelay > 0) &&
            (timeDiff >= sourceHandler.maxSendDelay))
            return true;
        }
      }
      return false;
    }
  }
  /**
   * Check that the size of the Server Handler messages Queue has lowered
   * below the limit and therefore allowing the reception of messages
   * from other servers to restart.
   * @param source The ServerHandler which was sending the update.
   *        can be null.
   * @return true if the processing can restart
   */
  public boolean restartAfterSaturation(ServerHandler source)
  {
    synchronized (msgQueue)
    {
      int queueSize = msgQueue.count();
      if ((maxReceiveQueue > 0) && (queueSize >= restartReceiveQueue))
        return false;
      if ((source != null) && (source.maxSendQueue > 0) &&
        (queueSize >= source.restartSendQueue))
        return false;
      if (!msgQueue.isEmpty())
      {
        UpdateMsg firstUpdate = msgQueue.first();
        UpdateMsg lastUpdate = msgQueue.last();
        if ((firstUpdate != null) && (lastUpdate != null))
        {
          long timeDiff = lastUpdate.getChangeNumber().getTimeSec() -
            firstUpdate.getChangeNumber().getTimeSec();
          if ((maxReceiveDelay > 0) && (timeDiff >= restartReceiveDelay))
            return false;
          if ((source != null) && (source.maxSendDelay > 0) && (timeDiff >=
            source.restartSendDelay))
            return false;
        }
      }
    }
    return true;
  }
  /**
   * Check if the server associated to this ServerHandler is a replication
   * server.
   * @return true if the server associated to this ServerHandler is a
   *         replication server.
   */
  public boolean isReplicationServer()
  {
    return (!serverIsLDAPserver);
  }
  /**
   * Get the number of message in the receive message queue.
   * @return Size of the receive message queue.
   */
  public int getRcvMsgQueueSize()
  {
    synchronized (msgQueue)
    {
      /*
       * When the server is up to date or close to be up to date,
       * the number of updates to be sent is the size of the receive queue.
       */
      if (isFollowing())
        return msgQueue.count();
      else
      {
        /**
         * When the server  is not able to follow, the msgQueue
         * may become too large and therefore won't contain all the
         * changes. Some changes may only be stored in the backing DB
         * of the servers.
         * The total size of the receive queue is calculated by doing
         * the sum of the number of missing changes for every dbHandler.
         */
        ServerState dbState = replicationServerDomain.getDbServerState();
        return ServerState.diffChanges(dbState, serverState);
      }
    }
    session.publish(msg);
  }
  /**
@@ -1536,478 +458,133 @@
  }
  /**
   * Get the older update time for that server.
   * @return The older update time.
   * Get the number of updates received from the server in assured safe data
   * mode.
   * @return The number of updates received from the server in assured safe data
   * mode
   */
  public long getOlderUpdateTime()
  public int getAssuredSdReceivedUpdates()
  {
    ChangeNumber olderUpdateCN = getOlderUpdateCN();
    if (olderUpdateCN == null)
      return 0;
    return olderUpdateCN.getTime();
    return assuredSdReceivedUpdates;
  }
  /**
   * Get the older Change Number for that server.
   * Returns null when the queue is empty.
   * @return The older change number.
   * Get the number of updates received from the server in assured safe data
   * mode that timed out.
   * @return The number of updates received from the server in assured safe data
   * mode that timed out.
   */
  public ChangeNumber getOlderUpdateCN()
  public AtomicInteger getAssuredSdReceivedUpdatesTimeout()
  {
    ChangeNumber result = null;
    synchronized (msgQueue)
    {
      if (isFollowing())
      {
        if (msgQueue.isEmpty())
        {
          result = null;
        } else
        {
          UpdateMsg msg = msgQueue.first();
          result = msg.getChangeNumber();
        }
      } else
      {
        if (lateQueue.isEmpty())
        {
          // isFollowing is false AND lateQueue is empty
          // We may be at the very moment when the writer has emptyed the
          // lateQueue when it sent the last update. The writer will fill again
          // the lateQueue when it will send the next update but we are not yet
          // there. So let's take the last change not sent directly from
          // the db.
          ReplicationIteratorComparator comparator =
            new ReplicationIteratorComparator();
          SortedSet<ReplicationIterator> iteratorSortedSet =
            new TreeSet<ReplicationIterator>(comparator);
          try
          {
            // Build a list of candidates iterator (i.e. db i.e. server)
            for (short serverId : replicationServerDomain.getServers())
            {
              // get the last already sent CN from that server
              ChangeNumber lastCsn = serverState.getMaxChangeNumber(serverId);
              // get an iterator in this server db from that last change
              ReplicationIterator iterator =
                replicationServerDomain.getChangelogIterator(serverId, lastCsn);
              // if that iterator has changes, then it is a candidate
              // it is added in the sorted list at a position given by its
              // current change (see ReplicationIteratorComparator).
              if ((iterator != null) && (iterator.getChange() != null))
              {
                iteratorSortedSet.add(iterator);
              }
            }
            UpdateMsg msg = iteratorSortedSet.first().getChange();
            result = msg.getChangeNumber();
          } catch (Exception e)
          {
            result = null;
          } finally
          {
            for (ReplicationIterator iterator : iteratorSortedSet)
            {
              iterator.releaseCursor();
            }
          }
        } else
        {
          UpdateMsg msg = lateQueue.first();
          result = msg.getChangeNumber();
        }
      }
    }
    return result;
    return assuredSdReceivedUpdatesTimeout;
  }
  /**
   * Check if the LDAP server can follow the speed of the other servers.
   * @return true when the server has all the not yet sent changes
   *         in its queue.
   * Get the number of updates sent to the server in assured safe data mode.
   * @return The number of updates sent to the server in assured safe data mode
   */
  public boolean isFollowing()
  public int getAssuredSdSentUpdates()
  {
    return following;
    return assuredSdSentUpdates;
  }
  /**
   * Set the following flag of this server.
   * @param following the value that should be set.
   * Get the number of updates sent to the server in assured safe data mode that
   * timed out.
   * @return The number of updates sent to the server in assured safe data mode
   * that timed out.
   */
  public void setFollowing(boolean following)
  public AtomicInteger getAssuredSdSentUpdatesTimeout()
  {
    this.following = following;
    return assuredSdSentUpdatesTimeout;
  }
  /**
   * Add an update to the list of updates that must be sent to the server
   * managed by this ServerHandler.
   * Get the number of updates received from the server in assured safe read
   * mode.
   * @return The number of updates received from the server in assured safe read
   * mode
   */
  public int getAssuredSrReceivedUpdates()
  {
    return assuredSrReceivedUpdates;
  }
  /**
   * Get the number of updates received from the server in assured safe read
   * mode that timed out.
   * @return The number of updates received from the server in assured safe read
   * mode that timed out.
   */
  public AtomicInteger getAssuredSrReceivedUpdatesTimeout()
  {
    return assuredSrReceivedUpdatesTimeout;
  }
  /**
   * Get the number of updates sent to the server in assured safe read mode.
   * @return The number of updates sent to the server in assured safe read mode
   */
  public int getAssuredSrSentUpdates()
  {
    return assuredSrSentUpdates;
  }
  /**
   * Get the number of updates sent to the server in assured safe read mode that
   * timed out.
   * @return The number of updates sent to the server in assured safe read mode
   * that timed out.
   */
  public AtomicInteger getAssuredSrSentUpdatesTimeout()
  {
    return assuredSrSentUpdatesTimeout;
  }
  /**
   * Returns the Replication Server Domain to which belongs this server handler.
   *
   * @param update The update that must be added to the list of updates.
   * @param sourceHandler The server that sent the update.
   * @return The replication server domain.
   */
  public void add(UpdateMsg update, ServerHandler sourceHandler)
  public ReplicationServerDomain getDomain()
  {
    synchronized (msgQueue)
    {
      /*
       * If queue was empty the writer thread was probably asleep
       * waiting for some changes, wake it up
       */
      if (msgQueue.isEmpty())
        msgQueue.notify();
      msgQueue.add(update);
      /* TODO : size should be configurable
       * and larger than max-receive-queue-size
       */
      while ((msgQueue.count() > maxQueueSize) ||
          (msgQueue.bytesCount() > maxQueueBytesSize))
      {
        setFollowing(false);
        msgQueue.removeFirst();
      }
    }
    if (isSaturated(update.getChangeNumber(), sourceHandler))
    {
      sourceHandler.setSaturated(true);
    }
  }
  private void setSaturated(boolean value)
  {
    flowControl = value;
    return this.replicationServerDomain;
  }
  /**
   * Select the next update that must be sent to the server managed by this
   * ServerHandler.
   *
   * @return the next update that must be sent to the server managed by this
   *         ServerHandler.
   * Returns the value of generationId for that handler.
   * @return The value of the generationId.
   */
  public UpdateMsg take()
  public long getGenerationId()
  {
    boolean interrupted = true;
    UpdateMsg msg = getnextMessage();
    /*
     * When we remove a message from the queue we need to check if another
     * server is waiting in flow control because this queue was too long.
     * This check might cause a performance penalty an therefore it
     * is not done for every message removed but only every few messages.
     */
    if (++saturationCount > 10)
    {
      saturationCount = 0;
      try
      {
        replicationServerDomain.checkAllSaturation();
      } catch (IOException e)
      {
      }
    }
    boolean acquired = false;
    do
    {
      try
      {
        acquired = sendWindow.tryAcquire((long) 500, TimeUnit.MILLISECONDS);
        interrupted = false;
      } catch (InterruptedException e)
      {
        // loop until not interrupted
      }
    } while (((interrupted) || (!acquired)) && (!shutdownWriter));
    if (msg != null)
    {
      incrementOutCount();
      if (msg.isAssured())
      {
        if (msg.getAssuredMode() == AssuredMode.SAFE_READ_MODE)
        {
          incrementAssuredSrSentUpdates();
        } else
        {
          if (!isLDAPserver())
            incrementAssuredSdSentUpdates();
        }
      }
    }
    return msg;
    return generationId;
  }
  /**
   * Get the next update that must be sent to the server
   * from the message queue or from the database.
   *
   * @return The next update that must be sent to the server.
   * Gets the group id of the server represented by this object.
   * @return The group id of the server represented by this object.
   */
  private UpdateMsg getnextMessage()
  public byte getGroupId()
  {
    UpdateMsg msg;
    while (activeWriter == true)
    {
      if (following == false)
      {
        /* this server is late with regard to some other masters
         * in the topology or just joined the topology.
         * In such cases, we can't keep all changes in the queue
         * without saturating the memory, we therefore use
         * a lateQueue that is filled with a few changes from the changelogDB
         * If this server is able to close the gap, it will start using again
         * the regular msgQueue later.
         */
        if (lateQueue.isEmpty())
        {
          /*
           * Start from the server State
           * Loop until the queue high mark or until no more changes
           *   for each known LDAP master
           *      get the next CSN after this last one :
           *         - try to get next from the file
           *         - if not found in the file
           *             - try to get the next from the queue
           *   select the smallest of changes
           *   check if it is in the memory tree
           *     yes : lock memory tree.
           *           check all changes from the list, remove the ones that
           *           are already sent
           *           unlock memory tree
           *           restart as usual
           *   load this change on the delayList
           *
           */
          ReplicationIteratorComparator comparator =
            new ReplicationIteratorComparator();
          SortedSet<ReplicationIterator> iteratorSortedSet =
            new TreeSet<ReplicationIterator>(comparator);
          /* fill the lateQueue */
          for (short serverId : replicationServerDomain.getServers())
          {
            ChangeNumber lastCsn = serverState.getMaxChangeNumber(serverId);
            ReplicationIterator iterator =
              replicationServerDomain.getChangelogIterator(serverId, lastCsn);
            if (iterator != null)
            {
              if (iterator.getChange() != null)
              {
                iteratorSortedSet.add(iterator);
              } else
              {
                iterator.releaseCursor();
              }
            }
          }
          // The loop below relies on the fact that it is sorted based
          // on the currentChange of each iterator to consider the next
          // change across all servers.
          // Hence it is necessary to remove and eventual add again an iterator
          // when looping in order to keep consistent the order of the
          // iterators (see ReplicationIteratorComparator.
          while (!iteratorSortedSet.isEmpty() &&
                 (lateQueue.count()<100) &&
                 (lateQueue.bytesCount()<50000) )
          {
            ReplicationIterator iterator = iteratorSortedSet.first();
            iteratorSortedSet.remove(iterator);
            lateQueue.add(iterator.getChange());
            if (iterator.next())
              iteratorSortedSet.add(iterator);
            else
              iterator.releaseCursor();
          }
          for (ReplicationIterator iterator : iteratorSortedSet)
          {
            iterator.releaseCursor();
          }
          /*
           * Check if the first change in the lateQueue is also on the regular
           * queue
           */
          if (lateQueue.isEmpty())
          {
            synchronized (msgQueue)
            {
              if ((msgQueue.count() < maxQueueSize) &&
                  (msgQueue.bytesCount() < maxQueueBytesSize))
              {
                setFollowing(true);
              }
            }
          } else
          {
            msg = lateQueue.first();
            synchronized (msgQueue)
            {
              if (msgQueue.contains(msg))
              {
                /* we finally catch up with the regular queue */
                setFollowing(true);
                lateQueue.clear();
                UpdateMsg msg1;
                do
                {
                  msg1 = msgQueue.removeFirst();
                } while (!msg.getChangeNumber().equals(msg1.getChangeNumber()));
                this.updateServerState(msg);
                return msg;
              }
            }
          }
        } else
        {
          /* get the next change from the lateQueue */
          msg = lateQueue.removeFirst();
          this.updateServerState(msg);
          return msg;
        }
      }
      synchronized (msgQueue)
      {
        if (following == true)
        {
          try
          {
            while (msgQueue.isEmpty() && (following == true))
            {
              msgQueue.wait(500);
              if (!activeWriter)
                return null;
            }
          } catch (InterruptedException e)
          {
            return null;
          }
          if (following == true)
          {
            msg = msgQueue.removeFirst();
            if (this.updateServerState(msg))
            {
              /*
               * Only push the message if it has not yet been seen
               * by the other server.
               * Otherwise just loop to select the next message.
               */
              return msg;
            }
          }
        }
      }
    /*
     * Need to loop because following flag may have gone to false between
     * the first check at the beginning of this method
     * and the second check just above.
     */
    }
    return null;
    return groupId;
  }
  /**
   * Update the serverState with the last message sent.
   *
   * @param msg the last update sent.
   * @return boolean indicating if the update was meaningful.
   * Get our heartbeat interval.
   * @return Our heartbeat interval.
   */
  public boolean updateServerState(UpdateMsg msg)
  public long getHeartbeatInterval()
  {
    return serverState.update(msg.getChangeNumber());
    return heartbeatInterval;
  }
  /**
   * Get the state of this server.
   *
   * @return ServerState the state for this server..
   * Get the count of updates received from the server.
   * @return the count of update received from the server.
   */
  public ServerState getServerState()
  public int getInCount()
  {
    return serverState;
  }
  /**
   * Sends an ack message to the server represented by this object.
   *
   * @param ack The ack message to be sent.
   * @throws IOException In case of Exception thrown sending the ack.
   */
  public void sendAck(AckMsg ack) throws IOException
  {
    session.publish(ack);
  }
  /**
   * Check type of server handled.
   *
   * @return true if the handled server is an LDAP server.
   *         false if the handled server is a replicationServer
   */
  public boolean isLDAPserver()
  {
    return serverIsLDAPserver;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public void initializeMonitorProvider(MonitorProviderCfg configuration)
    throws ConfigException, InitializationException
  {
    // Nothing to do for now
  }
  /**
   * Retrieves the name of this monitor provider.  It should be unique among all
   * monitor providers, including all instances of the same monitor provider.
   *
   * @return  The name of this monitor provider.
   */
  @Override
  public String getMonitorInstanceName()
  {
    String str = serverURL + " " + String.valueOf(serverId);
    if (serverIsLDAPserver)
      return "Connected Replica " + str +
                ",cn=" + replicationServerDomain.getMonitorInstanceName();
    else
      return "Connected Replication Server " + str +
                ",cn=" + replicationServerDomain.getMonitorInstanceName();
  }
  /**
   * Retrieves the length of time in milliseconds that should elapse between
   * calls to the <CODE>updateMonitorData()</CODE> method.  A negative or zero
   * return value indicates that the <CODE>updateMonitorData()</CODE> method
   * should not be periodically invoked.
   *
   * @return  The length of time in milliseconds that should elapse between
   *          calls to the <CODE>updateMonitorData()</CODE> method.
   */
  @Override
  public long getUpdateInterval()
  {
    /* we don't wont to do polling on this monitor */
    return 0;
  }
  /**
   * Performs any processing periodic processing that may be desired to update
   * the information associated with this monitor.  Note that best-effort
   * attempts will be made to ensure that calls to this method come
   * <CODE>getUpdateInterval()</CODE> milliseconds apart, but no guarantees will
   * be made.
   */
  @Override
  public void updateMonitorData()
  {
    // As long as getUpdateInterval() returns 0, this will never get called
    return inCount;
  }
  /**
@@ -2021,101 +598,11 @@
  @Override
  public ArrayList<Attribute> getMonitorData()
  {
    ArrayList<Attribute> attributes = new ArrayList<Attribute>();
    if (serverIsLDAPserver)
    {
      attributes.add(Attributes.create("replica", serverURL));
      attributes.add(Attributes.create("connected-to",
          this.replicationServerDomain.getReplicationServer()
              .getMonitorInstanceName()));
    // Get the generic ones
    ArrayList<Attribute> attributes = super.getMonitorData();
    }
    else
    {
      attributes.add(Attributes.create("Replication-Server",
          serverURL));
    }
    attributes.add(Attributes.create("server-id", String
        .valueOf(serverId)));
    attributes.add(Attributes.create("domain-name", baseDn.toString()));
    try
    {
      MonitorData md;
      md = replicationServerDomain.computeMonitorData();
      if (serverIsLDAPserver)
      {
        // Oldest missing update
        Long approxFirstMissingDate = md.getApproxFirstMissingDate(serverId);
        if ((approxFirstMissingDate != null) && (approxFirstMissingDate > 0))
        {
          Date date = new Date(approxFirstMissingDate);
          attributes.add(Attributes.create(
              "approx-older-change-not-synchronized", date.toString()));
          attributes.add(Attributes.create(
              "approx-older-change-not-synchronized-millis", String
              .valueOf(approxFirstMissingDate)));
        }
        // Missing changes
        long missingChanges = md.getMissingChanges(serverId);
        attributes.add(Attributes.create("missing-changes", String
            .valueOf(missingChanges)));
        // Replication delay
        long delay = md.getApproxDelay(serverId);
        attributes.add(Attributes.create("approximate-delay", String
            .valueOf(delay)));
        /* get the Server State */
        AttributeBuilder builder = new AttributeBuilder("server-state");
        ServerState state = md.getLDAPServerState(serverId);
        if (state != null)
        {
          for (String str : state.toStringSet())
          {
            builder.add(str);
          }
          attributes.add(builder.toAttribute());
        }
      }
      else
      {
        // Missing changes
        long missingChanges = md.getMissingChangesRS(serverId);
        attributes.add(Attributes.create("missing-changes", String
            .valueOf(missingChanges)));
        /* get the Server State */
        AttributeBuilder builder = new AttributeBuilder("server-state");
        ServerState state = md.getRSStates(serverId);
        if (state != null)
        {
          for (String str : state.toStringSet())
          {
            builder.add(str);
          }
          attributes.add(builder.toAttribute());
        }
      }
    }
    catch (Exception e)
    {
      Message message =
        ERR_ERROR_RETRIEVING_MONITOR_DATA.get(stackTraceToSingleLineString(e));
      // We failed retrieving the monitor data.
      attributes.add(Attributes.create("error", message.toString()));
    }
    attributes.add(
        Attributes.create("queue-size", String.valueOf(msgQueue.count())));
    attributes.add(
        Attributes.create(
            "queue-size-bytes", String.valueOf(msgQueue.bytesCount())));
    attributes.add(
        Attributes.create(
            "following", String.valueOf(following)));
    attributes.add(Attributes.create("server-id", String.valueOf(serverId)));
    attributes.add(Attributes.create("domain-name", getServiceId().toString()));
    // Deprecated
    attributes.add(Attributes.create("max-waiting-changes", String
@@ -2129,23 +616,23 @@
    attributes.add(Attributes.create("assured-sr-received-updates", String
        .valueOf(getAssuredSrReceivedUpdates())));
    attributes.add(Attributes.create("assured-sr-received-updates-timeout",
      String .valueOf(getAssuredSrReceivedUpdatesTimeout())));
        String .valueOf(getAssuredSrReceivedUpdatesTimeout())));
    attributes.add(Attributes.create("assured-sr-sent-updates", String
        .valueOf(getAssuredSrSentUpdates())));
    attributes.add(Attributes.create("assured-sr-sent-updates-timeout", String
        .valueOf(getAssuredSrSentUpdatesTimeout())));
    attributes.add(Attributes.create("assured-sd-received-updates", String
        .valueOf(getAssuredSdReceivedUpdates())));
    if (!isLDAPserver())
    if (!isDataServer())
    {
      attributes.add(Attributes.create("assured-sd-sent-updates",
        String.valueOf(getAssuredSdSentUpdates())));
          String.valueOf(getAssuredSdSentUpdates())));
      attributes.add(Attributes.create("assured-sd-sent-updates-timeout",
        String.valueOf(getAssuredSdSentUpdatesTimeout())));
          String.valueOf(getAssuredSdSentUpdatesTimeout())));
    } else
    {
      attributes.add(Attributes.create("assured-sd-received-updates-timeout",
        String.valueOf(getAssuredSdReceivedUpdatesTimeout())));
          String.valueOf(getAssuredSdReceivedUpdatesTimeout())));
    }
    // Window stats
@@ -2170,44 +657,484 @@
  }
  /**
   * Retrieves the name of this monitor provider.  It should be unique among all
   * monitor providers, including all instances of the same monitor provider.
   *
   * @return  The name of this monitor provider.
   */
  @Override
  public abstract String getMonitorInstanceName();
  /**
   * Get the older update time for that server.
   * @return The older update time.
   */
  public long getOlderUpdateTime()
  {
    ChangeNumber olderUpdateCN = getOlderUpdateCN();
    if (olderUpdateCN == null)
      return 0;
    return olderUpdateCN.getTime();
  }
  /**
   * Get the count of updates sent to this server.
   * @return  The count of update sent to this server.
   */
  public int getOutCount()
  {
    return outCount;
  }
  /**
   * Gets the protocol version used with this remote server.
   * @return The protocol version used with this remote server.
   */
  public short getProtocolVersion()
  {
    return protocolVersion;
  }
  /**
   * get the Server Id.
   *
   * @return the ID of the server to which this object is linked
   */
  public short getServerId()
  {
    return serverId;
  }
  /**
   * Retrieves the URL for this server handler.
   *
   * @return  The URL for this server handler, in the form of an address and
   *          port separated by a colon.
   */
  public String getServerURL()
  {
    return serverURL;
  }
  /**
   * Return the ServerStatus.
   * @return The server status.
   */
  protected abstract ServerStatus getStatus();
  /**
   * Retrieves the length of time in milliseconds that should elapse between
   * calls to the <CODE>updateMonitorData()</CODE> method.  A negative or zero
   * return value indicates that the <CODE>updateMonitorData()</CODE> method
   * should not be periodically invoked.
   *
   * @return  The length of time in milliseconds that should elapse between
   *          calls to the <CODE>updateMonitorData()</CODE> method.
   */
  @Override
  public long getUpdateInterval()
  {
    /* we don't wont to do polling on this monitor */
    return 0;
  }
  /**
   * Increment the number of updates received from the server in assured safe
   * data mode.
   */
  public void incrementAssuredSdReceivedUpdates()
  {
    assuredSdReceivedUpdates++;
  }
  /**
   * Increment the number of updates received from the server in assured safe
   * data mode that timed out.
   */
  public void incrementAssuredSdReceivedUpdatesTimeout()
  {
    assuredSdReceivedUpdatesTimeout.incrementAndGet();
  }
  /**
   * Increment the number of updates sent to the server in assured safe data
   * mode.
   */
  public void incrementAssuredSdSentUpdates()
  {
    assuredSdSentUpdates++;
  }
  /**
   * Increment the number of updates sent to the server in assured safe data
   * mode that timed out.
   */
  public void incrementAssuredSdSentUpdatesTimeout()
  {
    assuredSdSentUpdatesTimeout.incrementAndGet();
  }
  /**
   * Increment the number of updates received from the server in assured safe
   * read mode.
   */
  public void incrementAssuredSrReceivedUpdates()
  {
    assuredSrReceivedUpdates++;
  }
  /**
   * Increment the number of updates received from the server in assured safe
   * read mode that timed out.
   */
  public void incrementAssuredSrReceivedUpdatesTimeout()
  {
    assuredSrReceivedUpdatesTimeout.incrementAndGet();
  }
  /**
   * Increment the number of updates sent to the server in assured safe read
   * mode.
   */
  public void incrementAssuredSrSentUpdates()
  {
    assuredSrSentUpdates++;
  }
  /**
   * Increment the number of updates sent to the server in assured safe read
   * mode that timed out.
   */
  public void incrementAssuredSrSentUpdatesTimeout()
  {
    assuredSrSentUpdatesTimeout.incrementAndGet();
  }
  /**
   * Increase the counter of update received from the server.
   */
  public void incrementInCount()
  {
    inCount++;
  }
  /**
   * Increase the counter of updates sent to the server.
   */
  public void incrementOutCount()
  {
    outCount++;
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public void initializeMonitorProvider(MonitorProviderCfg configuration)
  throws ConfigException, InitializationException
  {
    // Nothing to do for now
  }
  /**
   * Check if the server associated to this ServerHandler is a data server
   * in the topology.
   * @return true if the server is a data server.
   */
  public abstract boolean isDataServer();
  /**
   * Check if the server associated to this ServerHandler is a replication
   * server.
   * @return true if the server is a replication server.
   */
  public boolean isReplicationServer()
  {
    return (!this.isDataServer());
  }
  /**
   * Lock the domain potentially with a timeout.
   * @param timedout The provided timeout.
   * @throws DirectoryException When an exception occurs.
   */
  protected void lockDomain(boolean timedout)
  throws DirectoryException
  {
    // The handshake phase must be done by blocking any access to structures
    // keeping info on connected servers, so that one can safely check for
    // pre-existence of a server, send a coherent snapshot of known topology
    // to peers, update the local view of the topology...
    //
    // For instance a kind of problem could be that while we connect with a
    // peer RS, a DS is connecting at the same time and we could publish the
    // connected DSs to the peer RS forgetting this last DS in the TopologyMsg.
    //
    // This method and every others that need to read/make changes to the
    // structures holding topology for the domain should:
    // - call ReplicationServerDomain.lock()
    // - read/modify structures
    // - call ReplicationServerDomain.release()
    //
    // More information is provided in comment of ReplicationServerDomain.lock()
    // If domain already exists, lock it until handshake is finished otherwise
    // it will be created and locked later in the method
    try
    {
      if (!timedout)
      {
        // !timedout
        if (!replicationServerDomain.hasLock())
          replicationServerDomain.lock();
      }
      else
      {
        // timedout
        /**
         * Take the lock on the domain.
         * WARNING: Here we try to acquire the lock with a timeout. This
         * is for preventing a deadlock that may happen if there are cross
         * connection attempts (for same domain) from this replication
         * server and from a peer one:
         * Here is the scenario:
         * - RS1 connect thread takes the domain lock and starts
         * connection to RS2
         * - at the same time RS2 connect thread takes his domain lock and
         * start connection to RS2
         * - RS2 listen thread starts processing received
         * ReplServerStartMsg from RS1 and wants to acquire the lock on
         * the domain (here) but cannot as RS2 connect thread already has
         * it
         * - RS1 listen thread starts processing received
         * ReplServerStartMsg from RS2 and wants to acquire the lock on
         * the domain (here) but cannot as RS1 connect thread already has
         * it
         * => Deadlock: 4 threads are locked.
         * So to prevent that in such situation, the listen threads here
         * will both timeout trying to acquire the lock. The random time
         * for the timeout should allow on connection attempt to be
         * aborted whereas the other one should have time to finish in the
         * same time.
         * Warning: the minimum time (3s) should be big enough to allow
         * normal situation connections to terminate. The added random
         * time should represent a big enough range so that the chance to
         * have one listen thread timing out a lot before the peer one is
         * great. When the first listen thread times out, the remote
         * connect thread should release the lock and allow the peer
         * listen thread to take the lock it was waiting for and process
         * the connection attempt.
         */
        Random random = new Random();
        int randomTime = random.nextInt(6); // Random from 0 to 5
        // Wait at least 3 seconds + (0 to 5 seconds)
        long timeout = (long) (3000 + ( randomTime * 1000 ) );
        boolean noTimeout = replicationServerDomain.tryLock(timeout);
        if (!noTimeout)
        {
          // Timeout
          Message message = NOTE_TIMEOUT_WHEN_CROSS_CONNECTION.get(
              getServiceId(),
              Short.toString(serverId),
              Short.toString(replicationServerId));
          throw new DirectoryException(ResultCode.OTHER, message);
        }
      }
    }
    catch (InterruptedException e)
    {
      // Thread interrupted
      Message message = ERR_EXCEPTION_LOCKING_RS_DOMAIN.get(e.getMessage());
      logError(message);
    }
  }
  /**
   * Processes a routable message.
   *
   * @param msg The message to be processed.
   */
  public void process(RoutableMsg msg)
  {
    if (debugEnabled())
      TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() + this +
          " processes received msg:\n" + msg);
    replicationServerDomain.process(msg, this);
  }
  /**
   * Process the reception of a WindowProbeMsg message.
   *
   * @param  windowProbeMsg The message to process.
   *
   * @throws IOException    When the session becomes unavailable.
   */
  public void process(WindowProbeMsg windowProbeMsg) throws IOException
  {
    if (rcvWindow > 0)
    {
      // The LDAP server believes that its window is closed
      // while it is not, this means that some problem happened in the
      // window exchange procedure !
      // lets update the LDAP server with out current window size and hope
      // that everything will work better in the futur.
      // TODO also log an error message.
      WindowMsg msg = new WindowMsg(rcvWindow);
      session.publish(msg);
    } else
    {
      // Both the LDAP server and the replication server believes that the
      // window is closed. Lets check the flowcontrol in case we
      // can now resume operations and send a windowMessage if necessary.
      checkWindow();
    }
  }
  /**
   * Send an InitializeRequestMessage to the server connected through this
   * handler.
   *
   * @param msg The message to be processed
   * @throws IOException when raised by the underlying session
   */
  public void send(RoutableMsg msg) throws IOException
  {
    if (debugEnabled())
      TRACER.debugInfo("In " +
          replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() + this +
          " publishes message:\n" + msg);
    session.publish(msg);
  }
  /**
   * Sends an ack message to the server represented by this object.
   *
   * @param ack The ack message to be sent.
   * @throws IOException In case of Exception thrown sending the ack.
   */
  public void sendAck(AckMsg ack) throws IOException
  {
    session.publish(ack);
  }
  /**
   * Send an ErrorMsg to the peer.
   *
   * @param errorMsg The message to be sent
   * @throws IOException when raised by the underlying session
   */
  public void sendError(ErrorMsg errorMsg) throws IOException
  {
    session.publish(errorMsg);
  }
  /**
   * Send the ReplServerStartMsg to the remote server (RS or DS).
   * @param requestedProtocolVersion The provided protocol version.
   * @return The ReplServerStartMsg sent.
   * @throws IOException When an exception occurs.
   */
  public ReplServerStartMsg sendStartToRemote(short requestedProtocolVersion)
  throws IOException
  {
    this.localGenerationId = replicationServerDomain.getGenerationId();
    ReplServerStartMsg outReplServerStartMsg
    = new ReplServerStartMsg(
        replicationServerId,
        replicationServerURL,
        getServiceId(),
        maxRcvWindow,
        replicationServerDomain.getDbServerState(),
        protocolVersion,
        localGenerationId,
        sslEncryption,
        getLocalGroupId(),
        replicationServerDomain.
        getReplicationServer().getDegradedStatusThreshold());
    if (requestedProtocolVersion>0)
      session.publish(outReplServerStartMsg, requestedProtocolVersion);
    else
      session.publish(outReplServerStartMsg);
    return outReplServerStartMsg;
  }
  /**
   * Sends the provided TopologyMsg to the peer server.
   *
   * @param topoMsg The TopologyMsg message to be sent.
   * @throws IOException When it occurs while sending the message,
   *
   */
  public void sendTopoInfo(TopologyMsg topoMsg)
  throws IOException
  {
    // V1 Rs do not support the TopologyMsg
    if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1)
    {
      session.publish(topoMsg);
    }
  }
  /**
   * Set a new generation ID.
   *
   * @param generationId The new generation ID
   *
   */
  public void setGenerationId(long generationId)
  {
    this.generationId = generationId;
  }
  /**
   * Sets the replication server domain associated.
   * @param rsd The provided replication server domain.
   */
  protected void setReplicationServerDomain(ReplicationServerDomain rsd)
  {
    this.replicationServerDomain = rsd;
  }
  /**
   * Sets the window size when used when sending to the remote.
   * @param size The provided window size.
   */
  protected void setSendWindowSize(int size)
  {
    this.sendWindowSize = size;
  }
  /**
   * Requests to shutdown the writer.
   */
  protected void shutdownWriter()
  {
    shutdownWriter = true;
  }
  /**
   * Shutdown This ServerHandler.
   */
  public void shutdown()
  {
    /*
     * Shutdown ServerWriter
     */
    shutdownWriter = true;
    activeWriter = false;
    synchronized (msgQueue)
    {
      /* wake up the writer thread on an empty queue so that it disappear */
      msgQueue.clear();
      msgQueue.notify();
      msgQueue.notifyAll();
    }
    shutdownWriter();
    setConsumerActive(false);
    super.shutdown();
    /*
     * Close session to end ServerReader or ServerWriter
     */
    try
    if (session != null)
    {
      session.close();
    } catch (IOException e)
    {
      // ignore.
    }
    /*
     * Stop the remote LSHandler
     */
    synchronized (directoryServers)
    {
      for (LightweightServerHandler lsh : directoryServers.values())
      // Close session to end ServerReader or ServerWriter
      try
      {
        lsh.stopHandler();
        session.close();
      } catch (IOException e)
      {
        // ignore.
      }
      directoryServers.clear();
    }
    /*
@@ -2240,68 +1167,93 @@
    {
      // don't try anymore to join and return.
    }
    if (debugEnabled())
      TRACER.debugInfo("SH.shutdowned(" + this + ")");
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public String toString()
  {
    String localString;
    if (serverId != 0)
    {
      if (serverIsLDAPserver)
        localString = "Directory Server ";
      else
        localString = "Replication Server ";
      localString += serverId + " " + serverURL + " " + baseDn;
    } else
      localString = "Unknown server";
    return localString;
  }
  /**
   * Decrement the protocol window, then check if it is necessary
   * to send a WindowMsg and send it.
   * Select the next update that must be sent to the server managed by this
   * ServerHandler.
   *
   * @throws IOException when the session becomes unavailable.
   * @return the next update that must be sent to the server managed by this
   *         ServerHandler.
   */
  public synchronized void decAndCheckWindow() throws IOException
  public UpdateMsg take()
  {
    rcvWindow--;
    checkWindow();
  }
    boolean interrupted = true;
    UpdateMsg msg = getnextMessage(true); // synchronous:block until msg
  /**
   * Check the protocol window and send WindowMsg if necessary.
   *
   * @throws IOException when the session becomes unavailable.
   */
  public synchronized void checkWindow() throws IOException
  {
    if (rcvWindow < rcvWindowSizeHalf)
    /*
     * When we remove a message from the queue we need to check if another
     * server is waiting in flow control because this queue was too long.
     * This check might cause a performance penalty an therefore it
     * is not done for every message removed but only every few messages.
     */
    if (++saturationCount > 10)
    {
      if (flowControl)
      saturationCount = 0;
      try
      {
        if (replicationServerDomain.restartAfterSaturation(this))
        {
          flowControl = false;
        }
      }
      if (!flowControl)
        replicationServerDomain.checkAllSaturation();
      } catch (IOException e)
      {
        WindowMsg msg = new WindowMsg(rcvWindowSizeHalf);
        session.publish(msg);
        rcvWindow += rcvWindowSizeHalf;
      }
    }
    boolean acquired = false;
    do
    {
      try
      {
        acquired = sendWindow.tryAcquire((long) 500, TimeUnit.MILLISECONDS);
        interrupted = false;
      } catch (InterruptedException e)
      {
        // loop until not interrupted
      }
    } while (((interrupted) || (!acquired)) && (!shutdownWriter));
    if (msg != null)
    {
      incrementOutCount();
      if (msg.isAssured())
      {
        if (msg.getAssuredMode() == AssuredMode.SAFE_READ_MODE)
        {
          incrementAssuredSrSentUpdates();
        } else
        {
          if (!isDataServer())
            incrementAssuredSdSentUpdates();
        }
      }
    }
    return msg;
  }
  /**
   * Creates a RSInfo structure representing this remote RS.
   * @return The RSInfo structure representing this remote RS
   */
  public RSInfo toRSInfo()
  {
    RSInfo rsInfo = new RSInfo(serverId, generationId, groupId);
    return rsInfo;
  }
  /**
   * Performs any processing periodic processing that may be desired to update
   * the information associated with this monitor.  Note that best-effort
   * attempts will be made to ensure that calls to this method come
   * <CODE>getUpdateInterval()</CODE> milliseconds apart, but no guarantees will
   * be made.
   */
  @Override
  public void updateMonitorData()
  {
    // As long as getUpdateInterval() returns 0, this will never get called
  }
  /**
   * Update the send window size based on the credit specified in the
   * given window message.
   *
@@ -2314,510 +1266,122 @@
  }
  /**
   * Get our heartbeat interval.
   * @return Our heartbeat interval.
   * Log the messages involved in the start handshake.
   * @param inStartMsg The message received first.
   * @param outStartMsg The message sent in response.
   */
  public long getHeartbeatInterval()
  {
    return heartbeatInterval;
  }
  /**
   * Processes a routable message.
   *
   * @param msg The message to be processed.
   */
  public void process(RoutableMsg msg)
  protected void logStartHandshakeRCVandSND(
      StartMsg inStartMsg,
      StartMsg outStartMsg)
  {
    if (debugEnabled())
      TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer().
        getMonitorInstanceName() +
        " SH for remote server " + this.getMonitorInstanceName() + ":" +
        "\nprocesses received msg:\n" + msg);
    replicationServerDomain.process(msg, this);
  }
  /**
   * Sends the provided TopologyMsg to the peer server.
   *
   * @param topoMsg The TopologyMsg message to be sent.
   * @throws IOException When it occurs while sending the message,
   *
   */
  public void sendTopoInfo(TopologyMsg topoMsg)
    throws IOException
  {
    // V1 Rs do not support the TopologyMsg
    if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1)
    {
      if (debugEnabled())
        TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() +
          " SH for remote server " + this.getMonitorInstanceName() + ":" +
          "\nsends message:\n" + topoMsg);
      session.publish(topoMsg);
    }
  }
  /**
   * Stores topology information received from a peer RS and that must be kept
   * in RS handler.
   *
   * @param topoMsg The received topology message
   */
  public void receiveTopoInfoFromRS(TopologyMsg topoMsg)
  {
    // Store info for remote RS
    List<RSInfo> rsInfos = topoMsg.getRsList();
    // List should only contain RS info for sender
    RSInfo rsInfo = rsInfos.get(0);
    generationId = rsInfo.getGenerationId();
    groupId = rsInfo.getGroupId();
    /**
     * Store info for DSs connected to the peer RS
     */
    List<DSInfo> dsInfos = topoMsg.getDsList();
    synchronized (directoryServers)
    {
      // Removes the existing structures
      for (LightweightServerHandler lsh : directoryServers.values())
      {
        lsh.stopHandler();
      }
      directoryServers.clear();
      // Creates the new structure according to the message received.
      for (DSInfo dsInfo : dsInfos)
      {
        LightweightServerHandler lsh = new LightweightServerHandler(this,
            serverId, dsInfo.getDsId(), dsInfo.getGenerationId(),
            dsInfo.getGroupId(), dsInfo.getStatus(), dsInfo.getRefUrls(),
            dsInfo.isAssured(), dsInfo.getAssuredMode(),
            dsInfo.getSafeDataLevel());
        lsh.startHandler();
        directoryServers.put(lsh.getServerId(), lsh);
      }
    }
  }
  /**
   * Process message of a remote server changing his status.
   * @param csMsg The message containing the new status
   * @return The new server status of the DS
   */
  public ServerStatus processNewStatus(ChangeStatusMsg csMsg)
  {
    // Sanity check
    if (!serverIsLDAPserver)
    {
      Message msg =
        ERR_RECEIVED_CHANGE_STATUS_NOT_FROM_DS.get(baseDn.toString(),
        Short.toString(serverId), csMsg.toString());
      logError(msg);
      return ServerStatus.INVALID_STATUS;
    }
    // Get the status the DS just entered
    ServerStatus reqStatus = csMsg.getNewStatus();
    // Translate new status to a state machine event
    StatusMachineEvent event = StatusMachineEvent.statusToEvent(reqStatus);
    if (event == StatusMachineEvent.INVALID_EVENT)
    {
      Message msg = ERR_RS_INVALID_NEW_STATUS.get(reqStatus.toString(),
        baseDn.toString(), Short.toString(serverId));
      logError(msg);
      return ServerStatus.INVALID_STATUS;
    }
    // Check state machine allows this new status
    ServerStatus newStatus = StatusMachine.computeNewStatus(status, event);
    if (newStatus == ServerStatus.INVALID_STATUS)
    {
      Message msg = ERR_RS_CANNOT_CHANGE_STATUS.get(baseDn.toString(),
        Short.toString(serverId), status.toString(), event.toString());
      logError(msg);
      return ServerStatus.INVALID_STATUS;
    }
    status = newStatus;
    return status;
  }
  /**
   * Change the status according to the event generated from the status
   * analyzer.
   * @param event The event to be used for new status computation
   * @return The new status of the DS
   * @throws IOException When raised by the underlying session
   */
  public ServerStatus changeStatusFromStatusAnalyzer(StatusMachineEvent event)
    throws IOException
  {
    // Check state machine allows this new status (Sanity check)
    ServerStatus newStatus = StatusMachine.computeNewStatus(status, event);
    if (newStatus == ServerStatus.INVALID_STATUS)
    {
      Message msg = ERR_RS_CANNOT_CHANGE_STATUS.get(baseDn.toString(),
        Short.toString(serverId), status.toString(), event.toString());
      logError(msg);
      // Status analyzer must only change from NORMAL_STATUS to DEGRADED_STATUS
      // and vice versa. We may are being trying to change the status while for
      // instance another status has just been entered: e.g a full update has
      // just been engaged. In that case, just ignore attempt to change the
      // status
      return newStatus;
    }
    // Send message requesting to change the DS status
    ChangeStatusMsg csMsg = new ChangeStatusMsg(newStatus,
      ServerStatus.INVALID_STATUS);
    if (debugEnabled())
    {
      TRACER.debugInfo(
        "In RS " +
        replicationServerDomain.getReplicationServer().getServerId() +
        " Sending change status from status analyzer to " + getServerId() +
        " for baseDn " + baseDn + ":\n" + csMsg);
    }
    session.publish(csMsg);
    status = newStatus;
    return newStatus;
  }
  /**
   * When this handler is connected to a replication server, specifies if
   * a wanted server is connected to this replication server.
   *
   * @param wantedServer The server we want to know if it is connected
   * to the replication server represented by this handler.
   * @return boolean True is the wanted server is connected to the server
   * represented by this handler.
   */
  public boolean isRemoteLDAPServer(short wantedServer)
  {
    synchronized (directoryServers)
    {
      for (LightweightServerHandler server : directoryServers.values())
      {
        if (wantedServer == server.getServerId())
        {
          return true;
        }
      }
      return false;
    }
  }
  /**
   * When the handler is connected to a replication server, specifies the
   * replication server has remote LDAP servers connected to it.
   *
   * @return boolean True is the replication server has remote LDAP servers
   * connected to it.
   */
  public boolean hasRemoteLDAPServers()
  {
    synchronized (directoryServers)
    {
      return !directoryServers.isEmpty();
    }
  }
  /**
   * Send an InitializeRequestMessage to the server connected through this
   * handler.
   *
   * @param msg The message to be processed
   * @throws IOException when raised by the underlying session
   */
  public void send(RoutableMsg msg) throws IOException
  {
    if (debugEnabled())
      TRACER.debugInfo("In " +
        replicationServerDomain.getReplicationServer().
        getMonitorInstanceName() +
        " SH for remote server " + this.getMonitorInstanceName() + ":" +
        "\nsends message:\n" + msg);
    session.publish(msg);
  }
  /**
   * Send an ErrorMsg to the peer.
   *
   * @param errorMsg The message to be sent
   * @throws IOException when raised by the underlying session
   */
  public void sendError(ErrorMsg errorMsg) throws IOException
  {
    session.publish(errorMsg);
  }
  /**
   * Process the reception of a WindowProbeMsg message.
   *
   * @param  windowProbeMsg The message to process.
   *
   * @throws IOException    When the session becomes unavailable.
   */
  public void process(WindowProbeMsg windowProbeMsg) throws IOException
  {
    if (rcvWindow > 0)
    {
      // The LDAP server believes that its window is closed
      // while it is not, this means that some problem happened in the
      // window exchange procedure !
      // lets update the LDAP server with out current window size and hope
      // that everything will work better in the futur.
      // TODO also log an error message.
      WindowMsg msg = new WindowMsg(rcvWindow);
      session.publish(msg);
    } else
    {
      // Both the LDAP server and the replication server believes that the
      // window is closed. Lets check the flowcontrol in case we
      // can now resume operations and send a windowMessage if necessary.
      checkWindow();
        getMonitorInstanceName() + ", " +
        this.getClass().getSimpleName() + " " + this + ":" +
        "\nSH START HANDSHAKE RECEIVED:\n" + inStartMsg.toString()+
        "\nAND REPLIED:\n" + outStartMsg.toString());
    }
  }
  /**
   * Returns the value of generationId for that handler.
   * @return The value of the generationId.
   * Log the messages involved in the start handshake.
   * @param outStartMsg The message sent first.
   * @param inStartMsg The message received in response.
   */
  public long getGenerationId()
  protected void logStartHandshakeSNDandRCV(
      StartMsg outStartMsg,
      StartMsg inStartMsg)
  {
    return generationId;
  }
  /**
   * Sends a message containing a generationId to a peer server.
   * The peer is expected to be a replication server.
   *
   * @param  msg         The GenerationIdMessage message to be sent.
   * @throws IOException When it occurs while sending the message,
   *
   */
  public void forwardGenerationIdToRS(ResetGenerationIdMsg msg)
    throws IOException
  {
    session.publish(msg);
  }
  /**
   * Set a new generation ID.
   *
   * @param generationId The new generation ID
   *
   */
  public void setGenerationId(long generationId)
  {
    this.generationId = generationId;
  }
  /**
   * Returns the Replication Server Domain to which belongs this server handler.
   *
   * @return The replication server domain.
   */
  public ReplicationServerDomain getDomain()
  {
    return this.replicationServerDomain;
  }
  /**
   * Return a Set containing the servers known by this replicationServer.
   * @return a set containing the servers known by this replicationServer.
   */
  public Set<Short> getConnectedDirectoryServerIds()
  {
    synchronized (directoryServers)
    {
      return directoryServers.keySet();
    }
  }
  /**
   * Order the peer DS server to change his status or close the connection
   * according to the requested new generation id.
   * @param newGenId The new generation id to take into account
   * @throws IOException If IO error occurred.
   */
  public void changeStatusForResetGenId(long newGenId)
    throws IOException
  {
    StatusMachineEvent event = null;
    if (newGenId == -1)
    {
      // The generation id is being made invalid, let's put the DS
      // into BAD_GEN_ID_STATUS
      event = StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT;
    } else
    {
      if (newGenId == generationId)
      {
        if (status == ServerStatus.BAD_GEN_ID_STATUS)
        {
          // This server has the good new reference generation id.
          // Close connection with him to force his reconnection: DS will
          // reconnect in NORMAL_STATUS or DEGRADED_STATUS.
          if (debugEnabled())
          {
            TRACER.debugInfo(
              "In RS " +
              replicationServerDomain.getReplicationServer().getServerId() +
              ". Closing connection to DS " + getServerId() +
              " for baseDn " + baseDn + " to force reconnection as new local" +
              " generation id and remote one match and DS is in bad gen id: " +
              newGenId);
          }
          // Connection closure must not be done calling RSD.stopHandler() as it
          // would rewait the RSD lock that we already must have entering this
          // method. This would lead to a reentrant lock which we do not want.
          // So simply close the session, this will make the hang up appear
          // after the reader thread that took the RSD lock realeases it.
          try
          {
            if (session != null)
              session.close();
          } catch (IOException e)
          {
            // ignore
          }
          // NOT_CONNECTED_STATUS is the last one in RS session life: handler
          // will soon disappear after this method call...
          status = ServerStatus.NOT_CONNECTED_STATUS;
          return;
        } else
        {
          if (debugEnabled())
          {
            TRACER.debugInfo(
              "In RS " +
              replicationServerDomain.getReplicationServer().getServerId() +
              ". DS " + getServerId() + " for baseDn " + baseDn +
              " has already generation id " + newGenId +
              " so no ChangeStatusMsg sent to him.");
          }
          return;
        }
      } else
      {
        // This server has a bad generation id compared to new reference one,
        // let's put it into BAD_GEN_ID_STATUS
        event = StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT;
      }
    }
    if ((event == StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT) &&
      (status == ServerStatus.FULL_UPDATE_STATUS))
    {
      // Prevent useless error message (full update status cannot lead to bad
      // gen status)
      Message message = NOTE_BAD_GEN_ID_IN_FULL_UPDATE.get(
        Short.toString(replicationServerDomain.
        getReplicationServer().getServerId()),
        baseDn.toString(),
        Short.toString(serverId),
        Long.toString(generationId),
        Long.toString(newGenId));
      logError(message);
      return;
    }
    ServerStatus newStatus = StatusMachine.computeNewStatus(status, event);
    if (newStatus == ServerStatus.INVALID_STATUS)
    {
      Message msg = ERR_RS_CANNOT_CHANGE_STATUS.get(baseDn.toString(),
        Short.toString(serverId), status.toString(), event.toString());
      logError(msg);
      return;
    }
    // Send message requesting to change the DS status
    ChangeStatusMsg csMsg = new ChangeStatusMsg(newStatus,
      ServerStatus.INVALID_STATUS);
    if (debugEnabled())
    {
      TRACER.debugInfo(
        "In RS " +
        replicationServerDomain.getReplicationServer().getServerId() +
        " Sending change status for reset gen id to " + getServerId() +
        " for baseDn " + baseDn + ":\n" + csMsg);
      TRACER.debugInfo("In " +
        replicationServerDomain.getReplicationServer().
        getMonitorInstanceName() + ", " +
        this.getClass().getSimpleName() + " " + this + ":" +
        "\nSH START HANDSHAKE SENT("+ this +
        "):\n" + outStartMsg.toString()+
        "\nAND RECEIVED:\n" + inStartMsg.toString());
    }
    session.publish(csMsg);
    status = newStatus;
  }
  /**
   * Set the shut down flag to true and returns the previous value of the flag.
   * @return The previous value of the shut down flag
   * Log the messages involved in the Topology handshake.
   * @param inTopoMsg The message received first.
   * @param outTopoMsg The message sent in response.
   */
  public boolean engageShutdown()
  protected void logTopoHandshakeRCVandSND(
      TopologyMsg inTopoMsg,
      TopologyMsg outTopoMsg)
  {
    // Use thread safe boolean
    return shuttingDown.getAndSet(true);
  }
  /**
   * Gets the status of the connected DS.
   * @return The status of the connected DS.
   */
  public ServerStatus getStatus()
  {
    return status;
  }
  /**
   * Gets the protocol version used with this remote server.
   * @return The protocol version used with this remote server.
   */
  public short getProtocolVersion()
  {
    return protocolVersion;
  }
  /**
   * Add the DSinfos of the connected Directory Servers
   * to the List of DSInfo provided as a parameter.
   *
   * @param dsInfos The List of DSInfo that should be updated
   *                with the DSInfo for the directoryServers
   *                connected to this ServerHandler.
   */
  public void addDSInfos(List<DSInfo> dsInfos)
  {
    synchronized (directoryServers)
    if (debugEnabled())
    {
      for (LightweightServerHandler ls : directoryServers.values())
      {
        dsInfos.add(ls.toDSInfo());
      }
      TRACER.debugInfo("In " +
          replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() + ", " +
          this.getClass().getSimpleName() + " " + this + ":" +
          "\nSH TOPO HANDSHAKE RECEIVED:\n" + inTopoMsg.toString() +
          "\nAND REPLIED:\n" + outTopoMsg.toString());
    }
  }
  /**
   * Gets the group id of the server represented by this object.
   * @return The group id of the server represented by this object.
   * Log the messages involved in the Topology handshake.
   * @param outTopoMsg The message sent first.
   * @param inTopoMsg The message received in response.
   */
  public byte getGroupId()
  protected void logTopoHandshakeSNDandRCV(
      TopologyMsg outTopoMsg,
      TopologyMsg inTopoMsg)
  {
    return groupId;
    if (debugEnabled())
    {
      TRACER.debugInfo("In " +
          replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() + ", " +
          this.getClass().getSimpleName() + " " + this + ":" +
          "\nSH TOPO HANDSHAKE SENT:\n" + outTopoMsg.toString() +
          "\nAND RECEIVED:\n" + inTopoMsg.toString());
    }
  }
  /**
   * Log the messages involved in the Topology/StartSession handshake.
   * @param inStartSessionMsg The message received first.
   * @param outTopoMsg The message sent in response.
   */
  protected void logStartSessionHandshake(
      StartSessionMsg inStartSessionMsg,
      TopologyMsg outTopoMsg)
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("In " +
          replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() + ", " +
          this.getClass().getSimpleName() + " " + this + " :" +
          "\nSH SESSION HANDSHAKE RECEIVED:\n" +
          "\nAND REPLIED:\n" + outTopoMsg.toString());
    }
  }
  /**
   * Log the messages involved in the Topology/StartSession handshake.
   * @param inStartECLSessionMsg The message received first.
   */
  protected void logStartECLSessionHandshake(
      StartECLSessionMsg inStartECLSessionMsg)
  {
    if (debugEnabled())
    {
      TRACER.debugInfo("In " +
          replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() + ", " +
          this.getClass().getSimpleName() + " " + this + " :" +
          "\nSH SESSION HANDSHAKE RECEIVED:\n" +
          inStartECLSessionMsg.toString());
    }
  }
}
opends/src/server/org/opends/server/replication/server/ServerReader.java
@@ -22,7 +22,7 @@
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.server;
@@ -93,8 +93,9 @@
    ServerHandler handler,
    ReplicationServerDomain replicationServerDomain)
  {
    super("Replication Reader for " + handler.toString() + " in RS " +
      replicationServerDomain.getReplicationServer().getServerId());
    super("Replication Reader Thread for handler of " +
        handler.toString() +
        " in " + replicationServerDomain);
    this.session = session;
    this.serverId = serverId;
    this.handler = handler;
@@ -106,13 +107,10 @@
   */
  public void run()
  {
    Message errMessage = null;
    if (debugEnabled())
    {
      TRACER.debugInfo(
        "In RS " + replicationServerDomain.getReplicationServer().
        getMonitorInstanceName() +
        (handler.isReplicationServer() ? " RS " : " LS") +
        " reader starting for serverId=" + serverId);
      TRACER.debugInfo(this.getName() + " starting");
    }
    /*
     * wait on input stream
@@ -127,16 +125,11 @@
        {
          ReplicationMsg msg = session.receive();
          /*
          if (debugEnabled())
          {
          TRACER.debugInfo(
          "In RS " + replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() +
          (handler.isReplicationServer()?" From RS ":" From LS")+
          " with serverId=" + serverId + " receives " + msg);
            TRACER.debugInfo(this.getName() + " receives " + msg);
          }
           */
          if (msg instanceof AckMsg)
          {
            AckMsg ack = (AckMsg) msg;
@@ -146,7 +139,7 @@
          {
            boolean filtered = false;
            /* Ignore updates in some cases */
            if (handler.isLDAPserver())
            if (handler.isDataServer())
            {
              /**
               * Ignore updates from DS in bad BAD_GENID_STATUS or
@@ -250,12 +243,36 @@
          } else if (msg instanceof TopologyMsg)
          {
            TopologyMsg topoMsg = (TopologyMsg) msg;
            replicationServerDomain.receiveTopoInfoFromRS(topoMsg,
              handler, true);
            try
            {
              ReplicationServerHandler rsh = (ReplicationServerHandler)handler;
              replicationServerDomain.receiveTopoInfoFromRS(topoMsg,
                  rsh, true);
            }
            catch(Exception e)
            {
              errMessage =
                ERR_REPLICATION_PROTOCOL_MESSAGE_TYPE.get(
                    "TopologyMsg", "other");
              logError(errMessage);
            }
          } else if (msg instanceof ChangeStatusMsg)
          {
            ChangeStatusMsg csMsg = (ChangeStatusMsg) msg;
            replicationServerDomain.processNewStatus(handler, csMsg);
            try
            {
              DataServerHandler dsh = (DataServerHandler)handler;
              replicationServerDomain.processNewStatus(dsh, csMsg);
            }
            catch(Exception e)
            {
              errMessage =
                ERR_RECEIVED_CHANGE_STATUS_NOT_FROM_DS.get(
                    replicationServerDomain.getBaseDn(),
                    Short.toString(handler.getServerId()),
                    csMsg.toString());
              logError(errMessage);
            }
          } else if (msg instanceof MonitorRequestMsg)
          {
            MonitorRequestMsg replServerMonitorRequestMsg =
@@ -271,8 +288,8 @@
             * The remote server has sent an unknown message,
             * close the conenction.
             */
            Message message = NOTE_READER_NULL_MSG.get(handler.toString());
            logError(message);
            errMessage = NOTE_READER_NULL_MSG.get(handler.toString());
            logError(errMessage);
            return;
          }
        } catch (NotSupportedOldVersionPDUException e)
@@ -286,7 +303,8 @@
              getMonitorInstanceName() + ":" + e.getMessage());
        }
      }
    } catch (IOException e)
    }
    catch (IOException e)
    {
      /*
       * The connection has been broken
@@ -297,13 +315,15 @@
        TRACER.debugInfo(
          "In RS " + replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() +
          " reader IO EXCEPTION for serverID=" + serverId +
          " reader IO EXCEPTION for serverID=" + serverId + " " +
          this + " " +
          stackTraceToSingleLineString(e) + " " + e.getLocalizedMessage());
      Message message = NOTE_SERVER_DISCONNECT.get(handler.toString(),
      errMessage = NOTE_SERVER_DISCONNECT.get(handler.toString(),
        Short.toString(replicationServerDomain.
        getReplicationServer().getServerId()));
      logError(message);
    } catch (ClassNotFoundException e)
      logError(errMessage);
    }
    catch (ClassNotFoundException e)
    {
      if (debugEnabled())
        TRACER.debugInfo(
@@ -315,49 +335,48 @@
       * The remote server has sent an unknown message,
       * close the connection.
       */
      Message message = ERR_UNKNOWN_MESSAGE.get(handler.toString());
      logError(message);
    } catch (Exception e)
      errMessage = ERR_UNKNOWN_MESSAGE.get(handler.toString());
      logError(errMessage);
    }
    catch (Exception e)
    {
      if (debugEnabled())
        TRACER.debugInfo(
          "In RS <" + replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() +
          " server reader EXCEPTION serverID=" + serverId +
          stackTraceToSingleLineString(e));
          " " + stackTraceToSingleLineString(e));
      /*
       * The remote server has sent an unknown message,
       * close the connection.
       */
      Message message = NOTE_READER_EXCEPTION.get(handler.toString());
      logError(message);
    } finally
      errMessage = NOTE_READER_EXCEPTION.get(handler.toString());
      logError(errMessage);
    }
    finally
    {
      /*
       * The thread only exit the loop above is some error condition
       * happen.
       * Attempt to close the socket and stop the server handler.
       */
      if (debugEnabled())
        TRACER.debugInfo(
          "In RS " + replicationServerDomain.getReplicationServer().
          getMonitorInstanceName() +
          " server reader for serverID=" + serverId +
          " is closing the session");
      try
      {
        if (debugEnabled())
          TRACER.debugInfo(
            "In RS " + replicationServerDomain.getReplicationServer().
            getMonitorInstanceName() +
            this + " is closing the session");
        session.close();
      } catch (IOException e)
      {
      // ignore
      }
      replicationServerDomain.stopServer(handler);
      if (debugEnabled())
      {
        TRACER.debugInfo(this.getName() + " stopped " + errMessage);
      }
    }
    if (debugEnabled())
      TRACER.debugInfo(
        "In RS " + replicationServerDomain.getReplicationServer().
        getMonitorInstanceName() +
        (handler.isReplicationServer() ? " RS" : " LDAP") +
        " server reader stopped for serverID=" + serverId);
  }
}
opends/src/server/org/opends/server/replication/server/ServerWriter.java
@@ -22,7 +22,7 @@
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.server;
import org.opends.messages.Message;
@@ -58,7 +58,6 @@
  private ProtocolSession session;
  private ServerHandler handler;
  private ReplicationServerDomain replicationServerDomain;
  private short serverId;
  private short protocolVersion = -1;
  /**
@@ -76,10 +75,10 @@
                      ServerHandler handler,
                      ReplicationServerDomain replicationServerDomain)
  {
    super("Replication Writer for " + handler.toString() + " in RS " +
      replicationServerDomain.getReplicationServer().getServerId());
    super("Replication Writer Thread for handler of " +
        handler.toString() +
        " in " + replicationServerDomain);
    this.serverId = serverId;
    this.session = session;
    this.handler = handler;
    this.replicationServerDomain = replicationServerDomain;
@@ -94,16 +93,10 @@
   */
  public void run()
  {
    Message errMessage = null;
    if (debugEnabled())
    {
      if (handler.isReplicationServer())
      {
        TRACER.debugInfo("Replication server writer starting " + serverId);
      }
      else
      {
        TRACER.debugInfo("LDAP server writer starting " + serverId);
      }
      TRACER.debugInfo(this.getName() + " starting");
    }
    try
    {
@@ -111,10 +104,14 @@
      {
        UpdateMsg update = replicationServerDomain.take(this.handler);
        if (update == null)
        {
          errMessage = Message.raw(
           "Connection closure: null update returned by domain.");
          return;       /* this connection is closing */
        }
        /* Ignore updates in some cases */
        if (handler.isLDAPserver())
        if (handler.isDataServer())
        {
          /**
           * Ignore updates to DS in bad BAD_GENID_STATUS or FULL_UPDATE_STATUS
@@ -184,6 +181,7 @@
            ", writer to " + this.handler.getMonitorInstanceName() +
            " publishes msg=[" + update.toString() + "]"+
            " refgenId=" + referenceGenerationId +
            " isAssured=" + update.isAssured() +
            " server=" + handler.getServerId() +
            " generationId=" + handler.getGenerationId());
        }
@@ -200,10 +198,10 @@
       * The remote host has disconnected and this particular Tree is going to
       * be removed, just ignore the exception and let the thread die as well
       */
      Message message = NOTE_SERVER_DISCONNECT.get(handler.toString(),
      errMessage = NOTE_SERVER_DISCONNECT.get(handler.toString(),
        Short.toString(replicationServerDomain.
        getReplicationServer().getServerId()));
      logError(message);
      logError(errMessage);
    }
    catch (SocketException e)
    {
@@ -211,10 +209,10 @@
       * The remote host has disconnected and this particular Tree is going to
       * be removed, just ignore the exception and let the thread die as well
       */
      Message message = NOTE_SERVER_DISCONNECT.get(handler.toString(),
      errMessage = NOTE_SERVER_DISCONNECT.get(handler.toString(),
        Short.toString(replicationServerDomain.
        getReplicationServer().getServerId()));
      logError(message);
      logError(errMessage);
    }
    catch (Exception e)
    {
@@ -222,9 +220,9 @@
       * An unexpected error happened.
       * Log an error and close the connection.
       */
      Message message = ERR_WRITER_UNEXPECTED_EXCEPTION.get(handler.toString() +
      errMessage = ERR_WRITER_UNEXPECTED_EXCEPTION.get(handler.toString() +
                        " " +  stackTraceToSingleLineString(e));
      logError(message);
      logError(errMessage);
    }
    finally {
      try
@@ -235,17 +233,9 @@
       // Can't do much more : ignore
      }
      replicationServerDomain.stopServer(handler);
      if (debugEnabled())
      {
        if (handler.isReplicationServer())
        {
          TRACER.debugInfo("Replication server writer stopping " + serverId);
        }
        else
        {
          TRACER.debugInfo("LDAP server writer stopping " + serverId);
        }
        TRACER.debugInfo(this.getName() + " stopped " + errMessage);
      }
    }
  }
opends/src/server/org/opends/server/replication/server/StatusAnalyzer.java
@@ -22,14 +22,13 @@
 * CDDL HEADER END
 *
 *
 *      Copyright 2008 Sun Microsystems, Inc.
 *      Copyright 2008-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.server;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import org.opends.server.api.DirectoryThread;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.replication.common.ServerStatus;
@@ -115,7 +114,7 @@
      // Go through each connected DS, get the number of pending changes we have
      // for it and change status accordingly if threshold value is
      // crossed/uncrossed
      for (ServerHandler serverHandler :
      for (DataServerHandler serverHandler :
        replicationServerDomain.getConnectedDSs(). values())
      {
        // Get number of pending changes for this server
opends/src/server/org/opends/server/replication/service/ReplicationBroker.java
@@ -26,15 +26,9 @@
 */
package org.opends.server.replication.service;
import org.opends.server.replication.protocol.HeartbeatMonitor;
import org.opends.messages.*;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.*;
import org.opends.server.loggers.debug.DebugTracer;
import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import java.io.IOException;
@@ -52,13 +46,33 @@
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import org.opends.messages.Category;
import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import org.opends.messages.Severity;
import org.opends.server.api.DirectoryThread;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.replication.common.ChangeNumber;
import org.opends.server.replication.common.DSInfo;
import org.opends.server.replication.common.RSInfo;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.common.ServerStatus;
import org.opends.server.replication.protocol.*;
import org.opends.server.replication.protocol.ChangeStatusMsg;
import org.opends.server.replication.protocol.HeartbeatMonitor;
import org.opends.server.replication.protocol.ProtocolSession;
import org.opends.server.replication.protocol.ProtocolVersion;
import org.opends.server.replication.protocol.ReplServerStartMsg;
import org.opends.server.replication.protocol.ReplSessionSecurity;
import org.opends.server.replication.protocol.ReplicationMsg;
import org.opends.server.replication.protocol.ServerStartECLMsg;
import org.opends.server.replication.protocol.ServerStartMsg;
import org.opends.server.replication.protocol.StartECLSessionMsg;
import org.opends.server.replication.protocol.StartSessionMsg;
import org.opends.server.replication.protocol.TopologyMsg;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.replication.protocol.WindowMsg;
import org.opends.server.replication.protocol.WindowProbeMsg;
import org.opends.server.util.ServerConstants;
import org.opends.server.replication.server.ReplicationServer;
/**
@@ -300,6 +314,43 @@
    }
  }
  private void connect()
  {
    if (this.baseDn.compareToIgnoreCase(
        ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT)==0)
    {
      connectAsECL();
    }
    else
    {
      connectAsDataServer();
    }
  }
  /**
   * Special aspects of connecting as ECL compared to connecting as data server
   * are :
   * - 1 single RS configured
   * - so no choice of the prefered RS
   * - No same groupID polling
   * - ?? Heartbeat
   * - Start handshake is :
   *    Broker ---> StartECLMsg       ---> RS
   *          <---- ReplServerStartMsg ---
   *           ---> StartSessionECLMsg --> RS
   */
  private void connectAsECL()
  {
    // FIXME:ECL List of RS to connect is for now limited to one RS only
    String bestServer = this.servers.iterator().next();
    ReplServerStartMsg inReplServerStartMsg
      = performECLPhaseOneHandshake(bestServer, true);
    if (inReplServerStartMsg!=null)
      performECLPhaseTwoHandshake(bestServer);
  }
  /**
   * Connect to a ReplicationServer.
   *
@@ -325,7 +376,7 @@
   *
   * @throws NumberFormatException address was invalid
   */
  private void connect()
  private void connectAsDataServer()
  {
    HashMap<String, ServerInfo> rsInfos = new HashMap<String, ServerInfo>();
@@ -350,6 +401,9 @@
       * Connect to each replication server and get their ServerState then find
       * out which one is the best to connect to.
       */
      if (debugEnabled())
        TRACER.debugInfo("phase 1 : will perform PhaseOneH with each RS in " +
            " order to elect the prefered one");
      for (String server : servers)
      {
        // Connect to server and get reply message
@@ -375,6 +429,9 @@
          serverId, baseDn, groupId);
        // Best found, now initialize connection to this one (handshake phase 1)
        if (debugEnabled())
          TRACER.debugInfo(
              "phase 2 : will perform PhaseOneH with the prefered RS.");
        replServerStartMsg = performPhaseOneHandshake(bestServer, true);
        if (replServerStartMsg != null) // Handshake phase 1 exchange went well
@@ -764,6 +821,8 @@
      {
        try
        {
          if (debugEnabled())
            TRACER.debugInfo("In RB, closing session after phase 1");
          localSession.close();
        } catch (IOException e)
        {
@@ -789,6 +848,225 @@
  }
  /**
   * Connect to the provided server performing the first phase handshake
   * (start messages exchange) and return the reply message from the replication
   * server.
   *
   * @param server Server to connect to.
   * @param keepConnection Do we keep session opened or not after handshake.
   *        Use true if want to perform handshake phase 2 with the same session
   *        and keep the session to create as the current one.
   * @return The ReplServerStartMsg the server replied. Null if could not
   *         get an answer.
   */
  private ReplServerStartMsg performECLPhaseOneHandshake(String server,
    boolean keepConnection)
  {
    ReplServerStartMsg replServerStartMsg = null;
    // Parse server string.
    int separator = server.lastIndexOf(':');
    String port = server.substring(separator + 1);
    String hostname = server.substring(0, separator);
    ProtocolSession localSession = null;
    boolean error = false;
    try
    {
      /*
       * Open a socket connection to the next candidate.
       */
      int intPort = Integer.parseInt(port);
      InetSocketAddress serverAddr = new InetSocketAddress(
        InetAddress.getByName(hostname), intPort);
      if (keepConnection)
        tmpReadableServerName = serverAddr.toString();
      Socket socket = new Socket();
      socket.setReceiveBufferSize(1000000);
      socket.setTcpNoDelay(true);
      socket.connect(serverAddr, 500);
      localSession = replSessionSecurity.createClientSession(server, socket,
        ReplSessionSecurity.HANDSHAKE_TIMEOUT);
      boolean isSslEncryption =
        replSessionSecurity.isSslEncryption(server);
      // Send our start msg.
      ServerStartECLMsg serverStartECLMsg = new ServerStartECLMsg(
          baseDn, 0, 0, 0, 0,
          maxRcvWindow, heartbeatInterval, state,
          ProtocolVersion.getCurrentVersion(), this.getGenerationID(),
          isSslEncryption,
          groupId);
      localSession.publish(serverStartECLMsg);
      // Read the ReplServerStartMsg that should come back.
      replServerStartMsg = (ReplServerStartMsg) localSession.receive();
      if (debugEnabled())
      {
        TRACER.debugInfo("In RB for " + baseDn +
          "\nRB HANDSHAKE SENT:\n" + serverStartECLMsg.toString() +
          "\nAND RECEIVED:\n" + replServerStartMsg.toString());
      }
      // Sanity check
      String repDn = replServerStartMsg.getBaseDn();
      if (!(this.baseDn.equals(repDn)))
      {
        Message message = ERR_DS_DN_DOES_NOT_MATCH.get(repDn.toString(),
          this.baseDn);
        logError(message);
        error = true;
      }
      /*
       * We have sent our own protocol version to the replication server.
       * The replication server will use the same one (or an older one
       * if it is an old replication server).
       */
      if (keepConnection)
        protocolVersion = ProtocolVersion.minWithCurrent(
          replServerStartMsg.getVersion());
      if (!isSslEncryption)
      {
        localSession.stopEncryption();
      }
    } catch (ConnectException e)
    {
      /*
       * There was no server waiting on this host:port
       * Log a notice and try the next replicationServer in the list
       */
      if (!connectionError)
      {
        Message message = NOTE_NO_CHANGELOG_SERVER_LISTENING.get(server);
        if (keepConnection) // Log error message only for final connection
        {
          // the error message is only logged once to avoid overflowing
          // the error log
          logError(message);
        } else if (debugEnabled())
        {
          TRACER.debugInfo(message.toString());
        }
      }
      error = true;
    } catch (Exception e)
    {
      if ( (e instanceof SocketTimeoutException) && debugEnabled() )
      {
        TRACER.debugInfo("Timeout trying to connect to RS " + server +
          " for dn: " + baseDn);
      }
      Message message = ERR_EXCEPTION_STARTING_SESSION_PHASE.get("1",
        baseDn, server, e.getLocalizedMessage() +
        stackTraceToSingleLineString(e));
      if (keepConnection) // Log error message only for final connection
      {
        logError(message);
      } else if (debugEnabled())
      {
        TRACER.debugInfo(message.toString());
      }
      error = true;
    }
    // Close session if requested
    if (!keepConnection || error)
    {
      if (localSession != null)
      {
        try
        {
          if (debugEnabled())
            TRACER.debugInfo("In RB, closing session after phase 1");
          localSession.close();
        } catch (IOException e)
        {
          // The session was already closed, just ignore.
        }
        localSession = null;
      }
      if (error)
      {
        replServerStartMsg = null;
      } // Be sure to return null.
    }
    // If this connection as the one to use for sending and receiving updates,
    // store it.
    if (keepConnection)
    {
      session = localSession;
    }
    return replServerStartMsg;
  }
  /**
   * Performs the second phase handshake (send StartSessionMsg and receive
   * TopologyMsg messages exchange) and return the reply message from the
   * replication server.
   *
   * @param server Server we are connecting with.
   * @param initStatus The status we are starting with
   * @return The ReplServerStartMsg the server replied. Null if could not
   *         get an answer.
   */
  private TopologyMsg performECLPhaseTwoHandshake(String server)
  {
    TopologyMsg topologyMsg = null;
    try
    {
      // Send our Start Session
      StartECLSessionMsg startECLSessionMsg = null;
      startECLSessionMsg = new StartECLSessionMsg();
      startECLSessionMsg.setOperationId(Short.toString(serverId));
      session.publish(startECLSessionMsg);
      /* FIXME:ECL In the handshake phase two, should RS send back a topo msg ?
       * Read the TopologyMsg that should come back.
      topologyMsg = (TopologyMsg) session.receive();
       */
      if (debugEnabled())
      {
        TRACER.debugInfo("In RB for " + baseDn +
          "\nRB HANDSHAKE SENT:\n" + startECLSessionMsg.toString());
      //  +   "\nAND RECEIVED:\n" + topologyMsg.toString());
      }
      // Alright set the timeout to the desired value
      session.setSoTimeout(timeout);
      connected = true;
    } catch (Exception e)
    {
      Message message = ERR_EXCEPTION_STARTING_SESSION_PHASE.get("2",
        baseDn, server, e.getLocalizedMessage() +
        stackTraceToSingleLineString(e));
      logError(message);
      if (session != null)
      {
        try
        {
          session.close();
        } catch (IOException ex)
        {
          // The session was already closed, just ignore.
        }
        session = null;
      }
      // Be sure to return null.
      topologyMsg = null;
    }
    return topologyMsg;
  }
  /**
   * Performs the second phase handshake (send StartSessionMsg and receive
   * TopologyMsg messages exchange) and return the reply message from the
   * replication server.
@@ -1720,6 +1998,12 @@
    {
      boolean done = false;
      if (debugEnabled())
      {
        TRACER.debugInfo("SameGroupIdPoller for: " + baseDn.toString() +
          " started.");
      }
      while ((!done) && (!sameGroupIdPollershutdown))
      {
        // Sleep some time between checks
@@ -1850,11 +2134,6 @@
   */
  public void receiveTopo(TopologyMsg topoMsg)
  {
    if (debugEnabled())
      TRACER.debugInfo("Replication domain " + baseDn
        + " received topology info update:\n" + topoMsg);
    // Store new lists
    synchronized(getDsList())
    {
opends/src/server/org/opends/server/util/ServerConstants.java
@@ -747,6 +747,12 @@
  public static final String OC_GROUP_OF_URLS_LC = "groupofurls";
  /**
   * The name of the objectclass that will be used as the structural class for
   * monitor entries.
   */
  public static final String OC_CHANGELOG_ENTRY = "changeLogEntry";
  /**
   * The request OID for the cancel extended operation.
@@ -1961,6 +1967,13 @@
       "1.3.6.1.4.1.42.2.27.9.5.8";
  /**
   *  The OID for the entry change request control.
   *  FIXME:ECL ask for OID_ECL_REQUEST_CONTROL
   */
  public static final String OID_ECL_COOKIE_EXCHANGE_CONTROL =
    "1.3.6.1.4.1.26027.1.5.4";
  /**
   * The IANA-assigned OID for the feature allowing a user to request that all
@@ -2983,5 +2996,12 @@
   * The normalized false value.
   */
  public static final ByteString FALSE_VALUE = ByteString.valueOf("FALSE");
  /**
   * The root Dn for the external change log.
   */
  public static final String DN_EXTERNAL_CHANGELOG_ROOT = "cn=changelog";
}
opends/src/server/org/opends/server/workflowelement/externalchangelog/ECLSearchOperation.java
New file
@@ -0,0 +1,1300 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2009 Sun Microsystems, Inc.
 */
package org.opends.server.workflowelement.externalchangelog;
import static org.opends.messages.CoreMessages.*;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.getExceptionMessage;
import static org.opends.server.util.StaticUtils.needsBase64Encoding;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import org.opends.messages.Category;
import org.opends.messages.Message;
import org.opends.messages.Severity;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.plugin.PluginResult;
import org.opends.server.controls.EntryChangelogNotificationControl;
import org.opends.server.controls.ExternalChangelogRequestControl;
import org.opends.server.controls.LDAPAssertionRequestControl;
import org.opends.server.controls.MatchedValuesControl;
import org.opends.server.controls.PersistentSearchControl;
import org.opends.server.controls.ProxiedAuthV1Control;
import org.opends.server.controls.ProxiedAuthV2Control;
import org.opends.server.core.AccessControlConfigManager;
import org.opends.server.core.AddOperation;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyOperation;
import org.opends.server.core.PersistentSearch;
import org.opends.server.core.PluginConfigManager;
import org.opends.server.core.SearchOperation;
import org.opends.server.core.SearchOperationWrapper;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.replication.common.ChangeNumber;
import org.opends.server.replication.common.ExternalChangeLogSession;
import org.opends.server.replication.common.MultiDomainServerState;
import org.opends.server.replication.plugin.MultimasterReplication;
import org.opends.server.replication.protocol.AddMsg;
import org.opends.server.replication.protocol.DeleteMsg;
import org.opends.server.replication.protocol.ECLUpdateMsg;
import org.opends.server.replication.protocol.ModifyDNMsg;
import org.opends.server.replication.protocol.ModifyMsg;
import org.opends.server.replication.protocol.StartECLSessionMsg;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.replication.server.ReplicationServer;
import org.opends.server.replication.service.ReplicationDomain;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.Attributes;
import org.opends.server.types.CancelRequest;
import org.opends.server.types.CancelResult;
import org.opends.server.types.CanceledOperationException;
import org.opends.server.types.Control;
import org.opends.server.types.DN;
import org.opends.server.types.DebugLogLevel;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.Modification;
import org.opends.server.types.ObjectClass;
import org.opends.server.types.Privilege;
import org.opends.server.types.RawAttribute;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchScope;
import org.opends.server.types.operation.PostOperationSearchOperation;
import org.opends.server.types.operation.PreOperationSearchOperation;
import org.opends.server.types.operation.SearchEntrySearchOperation;
import org.opends.server.types.operation.SearchReferenceSearchOperation;
import org.opends.server.util.Base64;
import org.opends.server.util.ServerConstants;
import org.opends.server.util.TimeThread;
/**
 * This class defines an operation used to search for entries in a local backend
 * of the Directory Server.
 */
public class ECLSearchOperation
       extends SearchOperationWrapper
       implements PreOperationSearchOperation, PostOperationSearchOperation,
                  SearchEntrySearchOperation, SearchReferenceSearchOperation
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();
  /**
   * The ECL Start Session we'll send to the RS.
   */
  private StartECLSessionMsg startECLSessionMsg;
  // The set of objectclasses that will be used in ECL entries.
  private static HashMap<ObjectClass,String> eclObjectClasses;
  // The associated DN.
  private DN rootBaseDN;
  // The cookie received in the ECL request control coming along
  // with the request.
  MultiDomainServerState requestCookie = null;
  /**
   * The replication server in which the search on ECL is to be performed.
   */
  protected ReplicationServer replicationServer;
  /**
   * Indicates whether we should actually process the search.  This should
   * only be false if it's a persistent search with changesOnly=true.
   */
  protected boolean changesOnly;
  /**
   * The client connection for the search operation.
   */
  protected ClientConnection clientConnection;
  /**
   * The base DN for the search.
   */
  protected DN baseDN;
  /**
   * The persistent search request, if applicable.
   */
  protected PersistentSearch persistentSearch;
  /**
   * The filter for the search.
   */
  protected SearchFilter filter;
  private ExternalChangeLogSession eclSession;
  // The set of supported controls for this WE
  private HashSet<String> supportedControls;
  // The set of supported features for this WE
  private HashSet<String> supportedFeatures;
  String privateDomainsBaseDN;
  /**
   * Creates a new operation that may be used to search for entries in a local
   * backend of the Directory Server.
   *
   * @param  search  The operation to process.
   */
  public ECLSearchOperation(SearchOperation search)
  {
    super(search);
    try
    {
      rootBaseDN = DN.decode(ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT);
    }
    catch (Exception e){}
    // Construct the set of objectclasses to include in the base monitor entry.
    eclObjectClasses = new LinkedHashMap<ObjectClass,String>(2);
    ObjectClass topOC = DirectoryServer.getObjectClass(OC_TOP, true);
    eclObjectClasses.put(topOC, OC_TOP);
    ObjectClass eclEntryOC = DirectoryServer.getObjectClass(OC_CHANGELOG_ENTRY,
                                                           true);
    eclObjectClasses.put(eclEntryOC, OC_CHANGELOG_ENTRY);
    // Define an empty sets for the supported controls and features.
    supportedControls = new HashSet<String>(0);
    supportedControls.add(ServerConstants.OID_SERVER_SIDE_SORT_REQUEST_CONTROL);
    supportedControls.add(ServerConstants.OID_VLV_REQUEST_CONTROL);
    supportedFeatures = new HashSet<String>(0);
    ECLWorkflowElement.attachLocalOperation(search, this);
  }
  /**
   * Process this search operation against a local backend.
   *
   * @param wfe
   *          The local backend work-flow element.
   * @throws CanceledOperationException
   *           if this operation should be cancelled
   */
  public void processECLSearch(ECLWorkflowElement wfe)
      throws CanceledOperationException
  {
    boolean executePostOpPlugins = false;
    this.replicationServer = wfe.getReplicationServer();
    clientConnection = getClientConnection();
    // Get the plugin config manager that will be used for invoking plugins.
    PluginConfigManager pluginConfigManager =
      DirectoryServer.getPluginConfigManager();
    changesOnly = false;
    // Check for a request to cancel this operation.
    checkIfCanceled(false);
    // Create a labeled block of code that we can break out of if a problem is
    // detected.
searchProcessing:
    {
      // Process the search base and filter to convert them from their raw forms
      // as provided by the client to the forms required for the rest of the
      // search processing.
      baseDN = getBaseDN();
      filter = getFilter();
      if ((baseDN == null) || (filter == null)){
        break searchProcessing;
      }
      // Check to see if there are any controls in the request.  If so, then
      // see if there is any special processing required.
      try
      {
        this.requestCookie = null;
        handleRequestControls();
        if (this.requestCookie == null)
        {
          setResponseData(new DirectoryException(
              ResultCode.OPERATIONS_ERROR,
              Message.raw(Category.SYNC, Severity.FATAL_ERROR,
                  "Cookie control expected")));
          break searchProcessing;
        }
      }
      catch (DirectoryException de)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, de);
        }
        setResponseData(de);
        break searchProcessing;
      }
      // Check for a request to cancel this operation.
      checkIfCanceled(false);
      // Invoke the pre-operation search plugins.
      executePostOpPlugins = true;
      PluginResult.PreOperation preOpResult =
          pluginConfigManager.invokePreOperationSearchPlugins(this);
      if (!preOpResult.continueProcessing())
      {
        setResultCode(preOpResult.getResultCode());
        appendErrorMessage(preOpResult.getErrorMessage());
        setMatchedDN(preOpResult.getMatchedDN());
        setReferralURLs(preOpResult.getReferralURLs());
        break searchProcessing;
      }
      // Check for a request to cancel this operation.
      checkIfCanceled(false);
      // Test existence of the RS
      if (replicationServer == null)
      {
        setResultCode(ResultCode.OPERATIONS_ERROR);
        appendErrorMessage(ERR_SEARCH_BASE_DOESNT_EXIST.get(
                                String.valueOf(baseDN)));
        break searchProcessing;
      }
      // We'll set the result code to "success".  If a problem occurs, then it
      // will be overwritten.
      setResultCode(ResultCode.SUCCESS);
      // If there's a persistent search, then register it with the server.
      if (persistentSearch != null)
      {
        wfe.registerPersistentSearch(persistentSearch);
        persistentSearch.enable();
      }
      // Process the search.
      try
      {
        processSearch();
      }
      catch (DirectoryException de)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, de);
        }
        setResponseData(de);
        if (persistentSearch != null)
        {
          persistentSearch.cancel();
          setSendResponse(true);
        }
        break searchProcessing;
      }
      catch (CanceledOperationException coe)
      {
        if (persistentSearch != null)
        {
          persistentSearch.cancel();
          setSendResponse(true);
        }
        throw coe;
      }
      catch (Exception e)
      {
        if (debugEnabled())
        {
          TRACER.debugCaught(DebugLogLevel.ERROR, e);
        }
        setResultCode(DirectoryServer.getServerErrorResultCode());
        appendErrorMessage(ERR_SEARCH_BACKEND_EXCEPTION.get(
                                getExceptionMessage(e)));
        if (persistentSearch != null)
        {
          persistentSearch.cancel();
          setSendResponse(true);
        }
        break searchProcessing;
      }
    }
    // Check for a request to cancel this operation.
    checkIfCanceled(false);
    // Invoke the post-operation search plugins.
    if (executePostOpPlugins)
    {
      PluginResult.PostOperation postOpResult =
           pluginConfigManager.invokePostOperationSearchPlugins(this);
      if (!postOpResult.continueProcessing())
      {
        setResultCode(postOpResult.getResultCode());
        appendErrorMessage(postOpResult.getErrorMessage());
        setMatchedDN(postOpResult.getMatchedDN());
        setReferralURLs(postOpResult.getReferralURLs());
      }
    }
  }
  /**
   * Handles any controls contained in the request.
   *
   * @throws  DirectoryException  If there is a problem with any of the request
   *                              controls.
   */
  protected void handleRequestControls()
          throws DirectoryException
  {
    List<Control> requestControls  = getRequestControls();
    if ((requestControls != null) && (! requestControls.isEmpty()))
    {
      for (int i=0; i < requestControls.size(); i++)
      {
        Control c   = requestControls.get(i);
        String  oid = c.getOID();
        if (! AccessControlConfigManager.getInstance().
                   getAccessControlHandler().isAllowed(baseDN, this, c))
        {
          throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
                         ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid));
        }
        if (oid.equals(OID_ECL_COOKIE_EXCHANGE_CONTROL))
        {
          ExternalChangelogRequestControl eclControl =
            getRequestControl(ExternalChangelogRequestControl.DECODER);
          this.requestCookie = eclControl.getCookie();
        }
        else if (oid.equals(OID_LDAP_ASSERTION))
        {
          LDAPAssertionRequestControl assertControl =
                getRequestControl(LDAPAssertionRequestControl.DECODER);
          try
          {
            // FIXME -- We need to determine whether the current user has
            //          permission to make this determination.
            SearchFilter assertionFilter = assertControl.getSearchFilter();
            Entry entry;
            try
            {
              entry = DirectoryServer.getEntry(baseDN);
            }
            catch (DirectoryException de)
            {
              if (debugEnabled())
              {
                TRACER.debugCaught(DebugLogLevel.ERROR, de);
              }
              throw new DirectoryException(de.getResultCode(),
                             ERR_SEARCH_CANNOT_GET_ENTRY_FOR_ASSERTION.get(
                                  de.getMessageObject()));
            }
            if (entry == null)
            {
              throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
                             ERR_SEARCH_NO_SUCH_ENTRY_FOR_ASSERTION.get());
            }
            if (! assertionFilter.matchesEntry(entry))
            {
              throw new DirectoryException(ResultCode.ASSERTION_FAILED,
                                           ERR_SEARCH_ASSERTION_FAILED.get());
            }
          }
          catch (DirectoryException de)
          {
            if (de.getResultCode() == ResultCode.ASSERTION_FAILED)
            {
              throw de;
            }
            if (debugEnabled())
            {
              TRACER.debugCaught(DebugLogLevel.ERROR, de);
            }
            throw new DirectoryException(ResultCode.PROTOCOL_ERROR,
                           ERR_SEARCH_CANNOT_PROCESS_ASSERTION_FILTER.get(
                                de.getMessageObject()), de);
          }
        }
        else if (oid.equals(OID_PROXIED_AUTH_V1))
        {
          // The requester must have the PROXIED_AUTH privilige in order to be
          // able to use this control.
          if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this))
          {
            throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED,
                           ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get());
          }
          ProxiedAuthV1Control proxyControl =
              getRequestControl(ProxiedAuthV1Control.DECODER);
          Entry authorizationEntry = proxyControl.getAuthorizationEntry();
          setAuthorizationEntry(authorizationEntry);
          if (authorizationEntry == null)
          {
            setProxiedAuthorizationDN(DN.nullDN());
          }
          else
          {
            setProxiedAuthorizationDN(authorizationEntry.getDN());
          }
        }
        else if (oid.equals(OID_PROXIED_AUTH_V2))
        {
          // The requester must have the PROXIED_AUTH privilige in order to be
          // able to use this control.
          if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this))
          {
            throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED,
                           ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get());
          }
          ProxiedAuthV2Control proxyControl =
              getRequestControl(ProxiedAuthV2Control.DECODER);
          Entry authorizationEntry = proxyControl.getAuthorizationEntry();
          setAuthorizationEntry(authorizationEntry);
          if (authorizationEntry == null)
          {
            setProxiedAuthorizationDN(DN.nullDN());
          }
          else
          {
            setProxiedAuthorizationDN(authorizationEntry.getDN());
          }
        }
        else if (oid.equals(OID_PERSISTENT_SEARCH))
        {
          PersistentSearchControl psearchControl =
            getRequestControl(PersistentSearchControl.DECODER);
          persistentSearch = new PersistentSearch(this,
                                      psearchControl.getChangeTypes(),
                                      psearchControl.getReturnECs());
          // If we're only interested in changes, then we don't actually want
          // to process the search now.
          if (psearchControl.getChangesOnly())
          {
            changesOnly = true;
          }
        }
        else if (oid.equals(OID_LDAP_SUBENTRIES))
        {
          setReturnLDAPSubentries(true);
        }
        else if (oid.equals(OID_MATCHED_VALUES))
        {
          MatchedValuesControl matchedValuesControl =
                getRequestControl(MatchedValuesControl.DECODER);
          setMatchedValuesControl(matchedValuesControl);
        }
        else if (oid.equals(OID_ACCOUNT_USABLE_CONTROL))
        {
          setIncludeUsableControl(true);
        }
        else if (oid.equals(OID_REAL_ATTRS_ONLY))
        {
          setRealAttributesOnly(true);
        }
        else if (oid.equals(OID_VIRTUAL_ATTRS_ONLY))
        {
          setVirtualAttributesOnly(true);
        }
        else if (oid.equals(OID_GET_EFFECTIVE_RIGHTS) &&
          DirectoryServer.isSupportedControl(OID_GET_EFFECTIVE_RIGHTS))
        {
          // Do nothing here and let AciHandler deal with it.
        }
        // NYI -- Add support for additional controls.
        else if (c.isCritical())
        {
          if ((replicationServer == null) || (! supportsControl(oid)))
          {
            throw new DirectoryException(
                           ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
                           ERR_SEARCH_UNSUPPORTED_CRITICAL_CONTROL.get(oid));
          }
        }
      }
    }
  }
  private void processSearch()
  throws DirectoryException, CanceledOperationException
  {
    startECLSessionMsg = new StartECLSessionMsg();
    startECLSessionMsg.setECLRequestType(
        StartECLSessionMsg.REQUEST_TYPE_FROM_COOKIE);
    startECLSessionMsg.setChangeNumber(
        new ChangeNumber(TimeThread.getTime(),(short)0, (short)0));
    startECLSessionMsg.setCrossDomainServerState(requestCookie.toString());
    if (persistentSearch==null)
      startECLSessionMsg.setPersistent(StartECLSessionMsg.NON_PERSISTENT);
    else
      if (!changesOnly)
        startECLSessionMsg.setPersistent(StartECLSessionMsg.PERSISTENT);
      else
        startECLSessionMsg.setPersistent(
            StartECLSessionMsg.PERSISTENT_CHANGES_ONLY);
    startECLSessionMsg.setFirstDraftChangeNumber(0);
    startECLSessionMsg.setLastDraftChangeNumber(0);
    // Help correlate with access log with the format: "conn=x op=y msgID=z"
    startECLSessionMsg.setOperationId(
      "conn="+String.valueOf(this.getConnectionID())
      + " op="+String.valueOf(this.getOperationID())
      + " msgID="+String.valueOf(getOperationID()));
    startECLSessionMsg.setExcludedDNs(
        MultimasterReplication.getPrivateDomains());
    // Start session
    eclSession = replicationServer.createECLSession(startECLSessionMsg);
    if (!getScope().equals(SearchScope.SINGLE_LEVEL))
    {
      // Root entry
      Entry entry = createRootEntry();
      if (matchFilter(entry))
        returnEntry(entry, null);
    }
    if (true)
    {
      // Loop on result entries
      int INITIAL=0;
      int PSEARCH=1;
      int phase=INITIAL;
      while (true)
      {
        // Check for a request to cancel this operation.
        checkIfCanceled(false);
        ECLUpdateMsg update = eclSession.getNextUpdate();
        if (update!=null)
        {
          if (phase==INITIAL)
            buildAndReturnEntry(update);
        }
        else
        {
          if (phase==INITIAL)
          {
            if (this.persistentSearch == null)
            {
              eclSession.close();
              break;
            }
            else
            {
              phase=PSEARCH;
              break;
            }
          }
        }
      }
    }
  }
  private boolean supportsControl(String oid)
  {
    return ((supportedControls != null) &&
        supportedControls.contains(oid));
  }
  /**
   * Build an ECL entry from a provided ECL msg and return it.
   * @param eclmsg The provided ECL msg.
   * @throws DirectoryException When an errors occurs.
   */
  private void buildAndReturnEntry(ECLUpdateMsg eclmsg)
  throws DirectoryException
  {
    Entry entry = null;
    // build and filter
    entry = createEntryFromMsg(eclmsg);
    if (matchFilter(entry))
    {
      List<Control> controls = new ArrayList<Control>(0);
      EntryChangelogNotificationControl clrc
      = new EntryChangelogNotificationControl(
          true,eclmsg.getCookie().toString());
      controls.add(clrc);
      boolean entryReturned = returnEntry(entry, controls);
      if (entryReturned==false)
      {
        // FIXME:ECL Handle the error case where returnEntry fails
      }
    }
  }
  /**
   * Test if the provided entry matches the filter, base and scope.
   * @param  entry The provided entry
   * @return whether the entry matches.
   * @throws DirectoryException When a problem occurs.
   */
  private boolean matchFilter(Entry entry)
  throws DirectoryException
  {
    boolean ms = entry.matchesBaseAndScope(getBaseDN(), getScope());
    boolean mf = getFilter().matchesEntry(entry);
    return (ms && mf);
  }
  /**
   * Create an ECL entry from a provided ECL msg.
   *
   * @param  eclmsg the provided ECL msg.
   * @return        the created ECL entry.
   * @throws DirectoryException When an error occurs.
   */
  public static Entry createEntryFromMsg(ECLUpdateMsg eclmsg)
  throws DirectoryException
  {
    Entry clEntry = null;
    // Get the meat fro the ecl msg
    UpdateMsg msg = eclmsg.getUpdateMsg();
    if (msg instanceof AddMsg)
    {
      AddMsg addMsg = (AddMsg)msg;
      // Map the addMsg to an LDIF string for the 'changes' attribute
      String LDIFchanges = addMsgToLDIFString(addMsg);
      clEntry = createChangelogEntry(
          eclmsg.getServiceId(),
          eclmsg.getCookie().toString(),
          DN.decode(addMsg.getDn()),
          addMsg.getChangeNumber(),
          LDIFchanges, // entry as created (in LDIF format)
          addMsg.getUniqueId(),
          null, // real time current entry
          null, // real time attrs names
          null, // hist entry attributes
          0,    // TODO:ECL G Good changelog draft compat. addMsg.getSeqnum()
      "add");
    } else
    if (msg instanceof ModifyMsg)
    {
      ModifyMsg modMsg = (ModifyMsg)msg;
      InternalClientConnection conn =
        InternalClientConnection.getRootConnection();
      try
      {
        // Map the modMsg modifications to an LDIF string
        // for the 'changes' attribute of the CL entry
        ModifyOperation modifyOperation =
          (ModifyOperation)modMsg.createOperation(conn);
        String LDIFchanges = modToLDIF(modifyOperation.getModifications());
        // TODO:ECL G Good changelog draft compat. Hist entry attributes
        // ArrayList<RawAttribute> attributes = modMsg.getEntryAttributes();
        clEntry = createChangelogEntry(
            eclmsg.getServiceId(),
            eclmsg.getCookie().toString(),
            DN.decode(modMsg.getDn()),
            modMsg.getChangeNumber(),
            LDIFchanges,
            modMsg.getUniqueId(),
            null, // real time current entry
            null, // real time attrs names
            null, // hist entry attributes
            0,    // TODO:ECL G Good changelog draft compat. modMsg.getSeqnum()
        "modify");
      }
      catch(Exception e)
      {
        // FIXME:ECL Handle error when createOperation raise DataFormatExceptin
      }
    }
    else if (msg instanceof ModifyDNMsg)
    {
      ModifyDNMsg modDNMsg = (ModifyDNMsg)msg;
      clEntry = createChangelogEntry(
          eclmsg.getServiceId(),
          eclmsg.getCookie().toString(),
          DN.decode(modDNMsg.getDn()),
          modDNMsg.getChangeNumber(),
          null,
          modDNMsg.getUniqueId(),
          null, // real time current entry
          null, // real time attrs names
          null, // hist entry attributes
          0,    // TODO:ECL G Good changelog draft compat. modDNMsg.getSeqnum()
          "modrdn");
      Attribute a = Attributes.create("newrdn", modDNMsg.getNewRDN());
      clEntry.addAttribute(a, null);
      Attribute b = Attributes.create("newsuperior", modDNMsg.getNewSuperior());
      clEntry.addAttribute(b, null);
      Attribute c = Attributes.create("deleteoldrdn",
          String.valueOf(modDNMsg.deleteOldRdn()));
      clEntry.addAttribute(c, null);
    }
    else if (msg instanceof DeleteMsg)
    {
      DeleteMsg delMsg = (DeleteMsg)msg;
      ArrayList<RawAttribute> rattributes = new ArrayList<RawAttribute>();
      String delAttrs = delMsgToLDIFString(rattributes);
      clEntry = createChangelogEntry(
          eclmsg.getServiceId(),
          eclmsg.getCookie().toString(),
          DN.decode(delMsg.getDn()),
          delMsg.getChangeNumber(),
          delAttrs,
          delMsg.getUniqueId(),
          null,
          null,
          null, //rattributes,
          0, // TODO:ECL G Good changelog draft compat. delMsg.getSeqnum()
         "delete");
    }
    return clEntry;
  }
  /**
   * Creates the root entry of the external changelog.
   * @return The root entry created.
   */
  private Entry createRootEntry()
  {
    HashMap<ObjectClass,String> oclasses =
      new LinkedHashMap<ObjectClass,String>(3);
    oclasses.putAll(eclObjectClasses);
    HashMap<AttributeType,List<Attribute>> userAttrs =
      new LinkedHashMap<AttributeType,List<Attribute>>();
    HashMap<AttributeType,List<Attribute>> operationalAttrs =
      new LinkedHashMap<AttributeType,List<Attribute>>();
    // Add to the root entry the replication state of each domain
    // TODO:ECL Put in ECL root entry, the ServerState for each domain
    AttributeType descType = DirectoryServer.getAttributeType("description");
    List<ReplicationDomain> supportedReplicationDomains
     = new ArrayList<ReplicationDomain>();
    for (ReplicationDomain domain : supportedReplicationDomains)
    {
      //    Crappy stuff to return the server state to the client
      LinkedList<Attribute> attrList = new LinkedList<Attribute>();
      attrList.add(Attributes.create("description",
          domain.getServiceID() + "/" + domain.getServerState().toString()));
      userAttrs.put(descType, attrList);
    }
    Entry e = new Entry(this.rootBaseDN, oclasses, userAttrs,
        operationalAttrs);
    return e;
  }
  /**
   * Create an ECL entry.
   * @param serviceID       The provided cookie value.
   * @param cookie          The provided cookie value.
   * @param targetDN        The provided targetDN.
   * @param changeNumber    The provided replication changeNumber.
   * @param clearLDIFchanges     The provided LDIF changes for ADD and MODIFY
   * @param targetUUID      The provided targetUUID.
   * @param entry           The provided related current entry.
   * @param targetAttrNames The provided list of attributes names that should
   *                        be read from the entry (real time values)
   * @param histEntryAttributes TODO:ECL Adress hist entry attributes
   * @param seqnum          The provided RCL int change number.
   * @param changetype      The provided change type (add, ...)
   * @return                The created ECL entry.
   * @throws DirectoryException
   *         When any error occurs.
   */
  public static Entry createChangelogEntry(
      String serviceID,
      String cookie,
      DN targetDN,
      ChangeNumber changeNumber,
      String clearLDIFchanges,
      String targetUUID,
      Entry entry,
      List<String> targetAttrNames,
      List<RawAttribute> histEntryAttributes,
      int seqnum,
      String changetype)
  throws DirectoryException
  {
    String dnString = "cn="+ changeNumber +"," +
      serviceID + "," +
      ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT;
    HashMap<ObjectClass,String> oClasses =
      new LinkedHashMap<ObjectClass,String>(3);
    oClasses.putAll(eclObjectClasses);
    ObjectClass extensibleObjectOC =
      DirectoryServer.getObjectClass(OC_EXTENSIBLE_OBJECT_LC, true);
    oClasses.put(extensibleObjectOC, OC_EXTENSIBLE_OBJECT);
    HashMap<AttributeType,List<Attribute>> uAttrs =
      new LinkedHashMap<AttributeType,List<Attribute>>();
    HashMap<AttributeType,List<Attribute>> operationalAttrs =
      new LinkedHashMap<AttributeType,List<Attribute>>();
    ArrayList<Attribute> attrList = new ArrayList<Attribute>(1);
    // TODO:ECL G Good changelog compat.
    if (seqnum>0)
    {
      Attribute a = Attributes.create("changeNumber", String.valueOf(seqnum));
      attrList = new ArrayList<Attribute>(1);
      attrList.add(a);
      uAttrs.put(a.getAttributeType(), attrList);
    }
    //
    Attribute a = Attributes.create("replicationCSN", changeNumber.toString());
    attrList = new ArrayList<Attribute>(1);
    attrList.add(a);
    uAttrs.put(a.getAttributeType(), attrList);
    //
    SimpleDateFormat dateFormat;
    dateFormat = new SimpleDateFormat(DATE_FORMAT_GMT_TIME);
    dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); // ??
    a = Attributes.create(
        DirectoryServer.getAttributeType("changeTime", true),
        dateFormat.format(new Date(changeNumber.getTime())));
    attrList = new ArrayList<Attribute>(1);
    attrList.add(a);
    uAttrs.put(a.getAttributeType(), attrList);
    /* Change time in a friendly format
    Date date = new Date(changeNumber.getTime());
    a = Attributes.create("clearChangeTime", date.toString());
    attrList = new ArrayList<Attribute>(1);
    attrList.add(a);
    uAttrs.put(a.getAttributeType(), attrList);
    */
    //
    a = Attributes.create("changeType", changetype);
    attrList = new ArrayList<Attribute>(1);
    attrList.add(a);
    uAttrs.put(a.getAttributeType(), attrList);
    //
    a = Attributes.create(
        DirectoryServer.getAttributeType("targetdn", true),
        targetDN.toNormalizedString());
    attrList = new ArrayList<Attribute>(1);
    attrList.add(a);
    uAttrs.put(a.getAttributeType(), attrList);
    //
    a = Attributes.create("replicaIdentifier",
        Short.toString(changeNumber.getServerId()));
    attrList = new ArrayList<Attribute>(1);
    attrList.add(a);
    uAttrs.put(a.getAttributeType(), attrList);
    if (clearLDIFchanges != null)
    {
      if (changetype.equalsIgnoreCase("delete"))
      {
        a = Attributes.create("clearDeletedEntryAttrs", clearLDIFchanges);
      }
      attrList = new ArrayList<Attribute>(1);
      attrList.add(a);
      operationalAttrs.put(a.getAttributeType(), attrList);
      if (changetype.equalsIgnoreCase("delete"))
      {
        a = Attributes.create("deletedentryattrs",
            clearLDIFchanges + "\n"); // force base64
      }
      else
      {
        AttributeType at = DirectoryServer.getAttributeType("changes");
        a = Attributes.create(at, clearLDIFchanges + "\n"); // force base64
      }
      attrList = new ArrayList<Attribute>(1);
      attrList.add(a);
      uAttrs.put(a.getAttributeType(), attrList);
    }
    a = Attributes.create("targetentryuuid", targetUUID);
    attrList = new ArrayList<Attribute>(1);
    attrList.add(a);
    operationalAttrs.put(a.getAttributeType(), attrList);
    a = Attributes.create("cookie", cookie);
    attrList = new ArrayList<Attribute>(1);
    attrList.add(a);
    operationalAttrs.put(a.getAttributeType(), attrList);
    // entryAttribute version
    /*
    if (targetAttrNames != null)
    {
      String sEntryAttrs = null;
      for (String attrName : targetAttrNames)
      {
        List<Attribute> attrs = entry.getAttribute(attrName);
        for (Attribute attr : attrs)
        {
          if (sEntryAttrs==null)
            sEntryAttrs="";
          else
            sEntryAttrs+=";";
          sEntryAttrs += attr.toString();
        }
      }
      if (sEntryAttrs!=null)
      {
        a = Attributes.create("entryAttributes", sEntryAttrs);
        attrList = new ArrayList<Attribute>(1);
        attrList.add(a);
        uAttrs.put(a.getAttributeType(), attrList);
      }
    }
    */
    /*
    if (targetAttrNames != null)
    {
      for (String attrName : targetAttrNames)
      {
        String newName = "target"+attrName;
        List<Attribute> attrs = entry.getAttribute(attrName);
        for (Attribute aa : attrs)
        {
          AttributeBuilder builder = new AttributeBuilder(
            DirectoryServer.getDefaultAttributeType(newName));
          builder.setOptions(aa.getOptions());
          builder.addAll(aa);
          attrList = new ArrayList<Attribute>(1);
          attrList.add(builder.toAttribute());
          uAttrs.put(aa.getAttributeType(), attrList);
        }
      }
    }
    */
    /* TODO: Implement entry attributes historical values
    if (histEntryAttributes != null)
    {
      for (RawAttribute rea : histEntryAttributes)
      {
        // uAttrs.put(ea.getAttributeType(), null);
        // FIXME: ERRONEOUS TYPING !!!!!!!!!!!!!
        try
        {
          String newName = "target"+rea.getAttributeType();
          AttributeType nat=DirectoryServer.getDefaultAttributeType(newName);
          rea.setAttributeType(newName);
          attrList = new ArrayList<Attribute>(1);
          attrList.add(rea.toAttribute());
          uAttrs.put(nat, attrList);
        }
        catch(Exception e)
        {
        }
      }
    }
    */
    // at the end build the CL entry to be returned
    Entry cle = new Entry(
        DN.decode(dnString),
        eclObjectClasses,
        uAttrs,
        operationalAttrs);
    return cle;
  }
  private static String addMsgToLDIFString(AddMsg addMsg)
  {
    StringBuilder modTypeLine = new StringBuilder();
    // LinkedList<StringBuilder> ldifLines =
    //  new LinkedList<StringBuilder>();
    try
    {
      AddOperation addOperation = (AddOperation)addMsg.createOperation(
          InternalClientConnection.getRootConnection());
      Map<AttributeType,List<Attribute>> attributes =
        new HashMap<AttributeType,List<Attribute>>();
      for (RawAttribute a : addOperation.getRawAttributes())
      {
        Attribute attr = a.toAttribute();
        AttributeType attrType = attr.getAttributeType();
        List<Attribute> attrs = attributes.get(attrType);
        if (attrs == null)
        {
          attrs = new ArrayList<Attribute>(1);
          attrs.add(attr);
          attributes.put(attrType, attrs);
        }
        else
        {
          attrs.add(attr);
        }
      }
      for (List<Attribute> attrList : attributes.values())
      {
        for (Attribute a : attrList)
        {
          StringBuilder attrName = new StringBuilder(a.getName());
          for (String o : a.getOptions())
          {
            attrName.append(";");
            attrName.append(o);
          }
          for (AttributeValue av : a)
          {
            String stringValue = av.toString();
            modTypeLine.append(attrName);
            if (needsBase64Encoding(stringValue))
            {
              modTypeLine.append(":: ");
              modTypeLine.append(Base64.encode(av.getValue()));
            }
            else
            {
              modTypeLine.append(": ");
              modTypeLine.append(stringValue);
            }
            modTypeLine.append("\n");
          }
        }
      }
      return modTypeLine.toString();
    }
    catch(Exception e)
    {
      TRACER.debugCaught(DebugLogLevel.ERROR, e);
    }
    return null;
  }
  /**
   * q.
   * @param rattributes q.
   * @return
   */
  private static String delMsgToLDIFString(ArrayList<RawAttribute> rattributes)
  {
    StringBuilder modTypeLine = new StringBuilder();
    try
    {
      Map<AttributeType,List<Attribute>> attributes =
        new HashMap<AttributeType,List<Attribute>>();
      for (RawAttribute a : rattributes)
      {
        Attribute attr = a.toAttribute();
        AttributeType attrType = attr.getAttributeType();
        List<Attribute> attrs = attributes.get(attrType);
        if (attrs == null)
        {
          attrs = new ArrayList<Attribute>(1);
          attrs.add(attr);
          attributes.put(attrType, attrs);
        }
        else
        {
          attrs.add(attr);
        }
      }
      for (List<Attribute> attrList : attributes.values())
      {
        for (Attribute a : attrList)
        {
          StringBuilder attrName = new StringBuilder(a.getName());
          for (String o : a.getOptions())
          {
            attrName.append(";");
            attrName.append(o);
          }
          for (AttributeValue av : a)
          {
            // ??
            String stringValue = av.toString();
            modTypeLine.append(attrName);
            if (needsBase64Encoding(stringValue))
            {
              modTypeLine.append(":: ");
              modTypeLine.append(Base64.encode(av.getValue()));
            }
            else
            {
              modTypeLine.append(": ");
              modTypeLine.append(stringValue);
            }
            modTypeLine.append("\n");
          }
        }
      }
      return modTypeLine.toString();
    }
    catch(Exception e)
    {
      TRACER.debugCaught(DebugLogLevel.ERROR, e);
    }
    return null;
  }
  /**
   * Dumps a list of modifications into an LDIF string.
   * @param mods The provided list of modifications.
   * @return The LDIF string.
   */
  public static String modToLDIF(List<Modification> mods)
  {
    StringBuilder modTypeLine = new StringBuilder();
    Iterator<Modification> iterator = mods.iterator();
    while (iterator.hasNext())
    {
      Modification m = iterator.next();
      Attribute a = m.getAttribute();
      String attrName = a.getName();
      modTypeLine.append(m.getModificationType().getLDIFName());
      modTypeLine.append(": ");
      modTypeLine.append(attrName);
      modTypeLine.append("\n");
      Iterator<AttributeValue> iteratorValues = a.iterator();
      while (iteratorValues.hasNext())
      {
        AttributeValue av = iteratorValues.next();
        String stringValue = av.toString();
        modTypeLine.append(attrName);
        if (needsBase64Encoding(stringValue))
        {
          modTypeLine.append(":: ");
          modTypeLine.append(Base64.encode(av.getValue()));
        }
        else
        {
          modTypeLine.append(": ");
          modTypeLine.append(stringValue);
        }
        modTypeLine.append("\n");
      }
      modTypeLine.append("-");
      if (iterator.hasNext())
      {
        modTypeLine.append("\n");
      }
    }
    return modTypeLine.toString();
  }
  /**
   * {@inheritDoc}
   */
  public CancelResult cancel(CancelRequest cancelRequest)
  {
    if (eclSession!=null)
    {
      try
      {
        eclSession.close();
      }
      catch(Exception e){}
    }
    return super.cancel(cancelRequest);
  }
}
opends/src/server/org/opends/server/workflowelement/externalchangelog/ECLWorkflowElement.java
New file
@@ -0,0 +1,220 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.workflowelement.externalchangelog;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.opends.server.admin.std.server.WorkflowElementCfg;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PersistentSearch;
import org.opends.server.core.SearchOperation;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.replication.server.ReplicationServer;
import org.opends.server.types.CanceledOperationException;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Operation;
import org.opends.server.workflowelement.LeafWorkflowElement;
/**
 * This class defines a workflow element for the external changelog (ECL);
 * e-g an entity that handles the processing of an operation against the ECL.
 */
public class ECLWorkflowElement extends
    LeafWorkflowElement<WorkflowElementCfg>
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();
  /**
   *The set of persistent searches registered with this work flow element.
   */
  private final List<PersistentSearch> persistentSearches =
    new CopyOnWriteArrayList<PersistentSearch>();
  /**
   * A string indicating the type of the workflow element.
   */
  public static final String ECL_WORKFLOW_ELEMENT = "EXTERNAL CHANGE LOG";
  /**
   * The replication server object to which we will submits request
   * on the ECL. Retrieved from the local DirectoryServer.
   */
  private ReplicationServer replicationServer;
  /**
   * Creates a new instance of the External Change Log workflow element.
   * @param rs the provided replication server
   * @throws DirectoryException  If the ECL workflow is already registered.
   */
  public ECLWorkflowElement(ReplicationServer rs)
  throws DirectoryException
  {
    this.replicationServer =rs;
    super.initialize(ECL_WORKFLOW_ELEMENT, ECL_WORKFLOW_ELEMENT);
    DirectoryServer.registerWorkflowElement(this);
  }
  /**
   * {@inheritDoc}
   */
  @Override
  public void finalizeWorkflowElement()
  {
    // null all fields so that any use of the finalized object will raise
    // an NPE
    super.initialize(ECL_WORKFLOW_ELEMENT, null);
    // Cancel all persistent searches.
    for (PersistentSearch psearch : persistentSearches) {
      psearch.cancel();
    }
    persistentSearches.clear();
  }
  /**
   * {@inheritDoc}
   */
  public void execute(Operation operation) throws CanceledOperationException {
    switch (operation.getOperationType())
    {
      case SEARCH:
        ECLSearchOperation searchOperation =
             new ECLSearchOperation((SearchOperation) operation);
        searchOperation.processECLSearch(this);
        break;
      case ABANDON:
        // There is no processing for an abandon operation.
        break;
      case BIND:
      case ADD:
      case DELETE:
      case MODIFY:
      case MODIFY_DN:
      case COMPARE:
      default:
        throw new AssertionError("Attempted to execute an invalid operation " +
                                 "type:  " + operation.getOperationType() +
                                 " (" + operation + ")");
    }
  }
  /**
   * Attaches the current local operation to the global operation so that
   * operation runner can execute local operation post response later on.
   *
   * @param <O>              subtype of Operation
   * @param <L>              subtype of LocalBackendOperation
   * @param globalOperation  the global operation to which local operation
   *                         should be attached to
   * @param currentLocalOperation  the local operation to attach to the global
   *                               operation
   */
  @SuppressWarnings("unchecked")
  public static final <O extends Operation,L> void
              attachLocalOperation (O globalOperation, L currentLocalOperation)
  {
    List<?> existingAttachment =
      (List<?>) globalOperation.getAttachment(Operation.LOCALBACKENDOPERATIONS);
    List<L> newAttachment = new ArrayList<L>();
    if (existingAttachment != null)
    {
      // This line raises an unchecked conversion warning.
      // There is nothing we can do to prevent this warning
      // so let's get rid of it since we know the cast is safe.
      newAttachment.addAll ((List<L>) existingAttachment);
    }
    newAttachment.add (currentLocalOperation);
    globalOperation.setAttachment(Operation.LOCALBACKENDOPERATIONS,
                                  newAttachment);
  }
  /**
   * Registers the provided persistent search operation with this
   * local backend workflow element so that it will be notified of any
   * add, delete, modify, or modify DN operations that are performed.
   *
   * @param persistentSearch
   *          The persistent search operation to register with this
   *          local backend workflow element.
   */
  void registerPersistentSearch(PersistentSearch persistentSearch)
  {
    PersistentSearch.CancellationCallback callback =
      new PersistentSearch.CancellationCallback()
    {
      public void persistentSearchCancelled(PersistentSearch psearch)
      {
        psearch.getSearchOperation().cancel(null);
        persistentSearches.remove(psearch);
      }
    };
    persistentSearches.add(persistentSearch);
    persistentSearch.registerCancellationCallback(callback);
  }
  /**
   * Gets the list of persistent searches currently active against
   * this local backend workflow element.
   *
   * @return The list of persistent searches currently active against
   *         this local backend workflow element.
   */
  public List<PersistentSearch> getPersistentSearches()
  {
    return persistentSearches;
  }
  /**
   * Returns the associated replication server.
   * @return the rs.
   */
  public ReplicationServer getReplicationServer()
  {
    return this.replicationServer;
  }
}
opends/src/server/org/opends/server/workflowelement/externalchangelog/package-info.java
New file
@@ -0,0 +1,38 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
/**
 * This package contains source for the local backend workflow element, which
 * are used to process operations against data stored in local backend databases
 * and other repositories that are considered "local".
 */
@org.opends.server.types.PublicAPI(
     stability=org.opends.server.types.StabilityLevel.PRIVATE)
package org.opends.server.workflowelement.externalchangelog;
opends/tests/unit-tests-testng/src/server/org/opends/server/controls/ExternalChangelogControlTest.java
New file
@@ -0,0 +1,100 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2009 Sun Microsystems, Inc.
 */
package org.opends.server.controls;
import static org.opends.server.util.ServerConstants.OID_ECL_COOKIE_EXCHANGE_CONTROL;
import static org.opends.server.util.ServerConstants.OID_PERSISTENT_SEARCH;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import org.opends.server.protocols.asn1.ASN1;
import org.opends.server.protocols.asn1.ASN1Writer;
import org.opends.server.protocols.ldap.LDAPControl;
import org.opends.server.protocols.ldap.LDAPReader;
import org.opends.server.replication.common.MultiDomainServerState;
import org.opends.server.types.ByteString;
import org.opends.server.types.ByteStringBuilder;
import org.opends.server.types.DirectoryException;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
 * Test ChangeNumber and ChangeNumberGenerator
 */
public class ExternalChangelogControlTest
    extends ControlsTestCase
{
  /**
   * Create values for External Changelog Request Control
   */
  @DataProvider(name = "eclRequestControl")
  public Object[][] createECLRequestControlTest()
  {
    return new Object[][]
        {
        {true,  "" },
        {false, "o=test:;" },
        {false, "o=test:000001210b6f21e904b100000002;" },
        {false, "o=test:000001210b6f21e904b100000001;o=test2:000001210b6f21e904b100000002;" },
        {false, "o=test:000001210b6f21e904b100000001 000001210b6f21e904b200000001;o=test2:000001210b6f21e904b100000002 000001210b6f21e904b200000002;" },
        };
  }
  /**
   * Test ExternalChangelogRequestControl
   */
  @Test(dataProvider = "eclRequestControl")
  public void checkECLRequestControlTest(boolean critical, String value)
      throws Exception
  {
    // Test contructor
    MultiDomainServerState mdss = new MultiDomainServerState(value);
    ExternalChangelogRequestControl eclrc
      = new ExternalChangelogRequestControl(critical, mdss);
    assertNotNull(eclrc);
    assertEquals(critical, eclrc.isCritical());
    assertEquals(OID_ECL_COOKIE_EXCHANGE_CONTROL, eclrc.getOID());
    assertTrue(eclrc.getCookie().equalsTo(mdss));
    // Test encode/decode
    ByteStringBuilder bsb = new ByteStringBuilder();
    ASN1Writer writer = ASN1.getWriter(bsb);
    bsb.clear();
    eclrc.write(writer);
    LDAPControl control = LDAPReader.readControl(ASN1.getReader(bsb));
    eclrc =
      ExternalChangelogRequestControl.DECODER.decode(control.isCritical(), control.getValue());
    assertNotNull(eclrc);
    assertEquals(critical, eclrc.isCritical());
    assertEquals(OID_ECL_COOKIE_EXCHANGE_CONTROL, eclrc.getOID());
    assertTrue(eclrc.getCookie().equalsTo(mdss),
       "Expect:"+value+", Got:"+eclrc.getCookie().toString());
  }
}
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/ReplicationTestCase.java
@@ -158,11 +158,6 @@
    // This test suite depends on having the schema available.
    TestCaseUtils.startServer();
    // After start of server because not seen if server not started
    // after server started once, TestCaseUtils.startServer() does nothing.
    logError(Message.raw(Category.SYNC, Severity.NOTICE,
        " ##### Calling ReplicationTestCase.setUp ##### "));
    // Initialize the test backend (TestCaseUtils.TEST_ROOT_DN_STRING)
    // (in case previous (non replication?) tests were run before...)
    TestCaseUtils.initializeTestBackend(true);
@@ -265,7 +260,60 @@
    return broker;
  }
   /**
  /**
   * Open an ECL replicationServer session to the local ReplicationServer
  protected ReplicationBroker openECLReplicationSession(
        int window_size, int port, int timeout, boolean emptyOldChanges,
        Short serverId)
  throws Exception, SocketException
  {
    ServerState state = new ServerState();
    //if (emptyOldChanges)
    //   new PersistentServerState(baseDn, serverId, new ServerState());
    ReplicationBroker broker = new ReplicationBroker(null,
        state, "cn=changelog", serverId, window_size,
        -1, 100000, getReplSessionSecurity(), (byte)1);
    ArrayList<String> servers = new ArrayList<String>(1);
    servers.add("localhost:" + port);
    broker.start(servers);
    if (timeout != 0)
      broker.setSoTimeout(timeout);
    checkConnection(30, broker, port); // give some time to the broker to connect
                                       // to the replicationServer.
    if (emptyOldChanges)
    {
      // loop receiving update until there is nothing left
      // to make sure that message from previous tests have been consumed.
      try
      {
        while (true)
        {
          ReplicationMsg rMsg = broker.receive();
          if (rMsg instanceof ErrorMsg)
          {
            ErrorMsg eMsg = (ErrorMsg)rMsg;
            logError(new MessageBuilder(
                "ReplicationTestCase/openReplicationSession ").append(
                " received ErrorMessage when emptying old changes ").append(
                eMsg.getDetails()).toMessage());
          }
        }
      }
      catch (Exception e)
      {
        logError(new MessageBuilder(
            "ReplicationTestCase/openReplicationSession ").append(e.getMessage())
            .append(" when emptying old changes").toMessage());
      }
    }
    return broker;
  }
  */
  /**
   * Check connection of the provided ds to the
   * replication server. Waits for connection to be ok up to secTimeout seconds
   * before failing.
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/SchemaReplicationTest.java
@@ -22,7 +22,7 @@
 * CDDL HEADER END
 *
 *
 *      Copyright 2008 Sun Microsystems, Inc.
 *      Copyright 2008-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication;
@@ -210,7 +210,7 @@
   * Checks that changes to the schema pushed to the replicationServer
   * are received and correctly replayed by replication plugin.
   */
  @Test(dependsOnMethods = { "pushSchemaChange" })
  @Test(enabled=true,dependsOnMethods = { "pushSchemaChange" })
  public void replaySchemaChange() throws Exception
  {
    logError(Message.raw(Category.SYNC, Severity.NOTICE,
@@ -341,5 +341,7 @@
    {
      broker.stop();
    }
    logError(Message.raw(Category.SYNC, Severity.NOTICE,
    "Ending replication test : pushSchemaFilesChange "));
  }
}
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/common/ExternalChangeLogTest.java
New file
@@ -0,0 +1,1569 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at
 * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
 * add the following below this CDDL HEADER, with the fields enclosed
 * by brackets "[]" replaced with your own identifying information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.common;
import static org.opends.server.TestCaseUtils.TEST_ROOT_DN_STRING;
import static org.opends.server.loggers.ErrorLogger.logError;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import static org.opends.server.replication.protocol.OperationContext.SYNCHROCONTEXT;
import static org.opends.server.util.StaticUtils.createEntry;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import org.opends.messages.Category;
import org.opends.messages.Message;
import org.opends.messages.Severity;
import org.opends.server.TestCaseUtils;
import org.opends.server.api.Backend;
import org.opends.server.api.ConnectionHandler;
import org.opends.server.api.SynchronizationProvider;
import org.opends.server.backends.MemoryBackend;
import org.opends.server.config.ConfigException;
import org.opends.server.controls.ExternalChangelogRequestControl;
import org.opends.server.controls.PersistentSearchChangeType;
import org.opends.server.controls.PersistentSearchControl;
import org.opends.server.core.AddOperationBasis;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyDNOperationBasis;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.plugins.InvocationCounterPlugin;
import org.opends.server.protocols.asn1.ASN1Exception;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.ldap.BindRequestProtocolOp;
import org.opends.server.protocols.ldap.BindResponseProtocolOp;
import org.opends.server.protocols.ldap.LDAPAttribute;
import org.opends.server.protocols.ldap.LDAPConnectionHandler;
import org.opends.server.protocols.ldap.LDAPConstants;
import org.opends.server.protocols.ldap.LDAPFilter;
import org.opends.server.protocols.ldap.LDAPMessage;
import org.opends.server.protocols.ldap.LDAPResultCode;
import org.opends.server.protocols.ldap.LDAPStatistics;
import org.opends.server.protocols.ldap.SearchRequestProtocolOp;
import org.opends.server.protocols.ldap.SearchResultDoneProtocolOp;
import org.opends.server.protocols.ldap.SearchResultEntryProtocolOp;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.plugin.DomainFakeCfg;
import org.opends.server.replication.plugin.LDAPReplicationDomain;
import org.opends.server.replication.plugin.MultimasterReplication;
import org.opends.server.replication.protocol.AddMsg;
import org.opends.server.replication.protocol.DeleteMsg;
import org.opends.server.replication.protocol.DoneMsg;
import org.opends.server.replication.protocol.ECLUpdateMsg;
import org.opends.server.replication.protocol.ModifyDNMsg;
import org.opends.server.replication.protocol.ModifyDnContext;
import org.opends.server.replication.protocol.ModifyMsg;
import org.opends.server.replication.protocol.ReplicationMsg;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.replication.server.ReplServerFakeConfiguration;
import org.opends.server.replication.server.ReplicationServer;
import org.opends.server.replication.server.ReplicationServerDomain;
import org.opends.server.replication.service.ReplicationBroker;
import org.opends.server.tools.LDAPWriter;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.Attributes;
import org.opends.server.types.ByteString;
import org.opends.server.types.Control;
import org.opends.server.types.DN;
import org.opends.server.types.DereferencePolicy;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.InitializationException;
import org.opends.server.types.LDAPException;
import org.opends.server.types.LDIFExportConfig;
import org.opends.server.types.Modification;
import org.opends.server.types.ModificationType;
import org.opends.server.types.RDN;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchScope;
import org.opends.server.util.LDIFWriter;
import org.opends.server.util.TimeThread;
import org.opends.server.workflowelement.localbackend.LocalBackendModifyDNOperation;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
/**
 * Tests for the replicationServer code.
 */
public class ExternalChangeLogTest extends ReplicationTestCase
{
  // The tracer object for the debug logger
  private static final DebugTracer TRACER = getTracer();
  // The replicationServer that will be used in this test.
  private ReplicationServer replicationServer = null;
  // The port of the replicationServer.
  private int replicationServerPort;
  public static final String TEST_ROOT_DN_STRING2 = "o=test2";
  public static final String TEST_BACKEND_ID2 = "test2";
  // The LDAPStatistics object associated with the LDAP connection handler.
  protected LDAPStatistics ldapStatistics;
  /**
   * Set up the environment for performing the tests in this Class.
   * Replication
   *
   * @throws Exception
   *           If the environment could not be set up.
   */
  @BeforeClass
  public void setUp() throws Exception
  {
    super.setUp();
    // This test suite depends on having the schema available.
    configure();
  }
  /**
   * Utility : configure a replicationServer.
   */
  protected void configure() throws Exception
  {
    //  Find  a free port for the replicationServer
    ServerSocket socket = TestCaseUtils.bindFreePort();
    replicationServerPort = socket.getLocalPort();
    socket.close();
    /*
    // Add the replication server to the configuration
    TestCaseUtils.dsconfig(
        "create-replication-server",
        "--provider-name", "Multimaster Synchronization",
        "--set", "replication-db-directory:" + "externalChangeLogTestConfigureDb",
        "--set", "replication-port:" + replicationServerPort,
        "--set", "replication-server-id:71");
    // Retrieves the replicationServer object
    DirectoryServer.getSynchronizationProviders();
    for (SynchronizationProvider<?> provider : DirectoryServer
        .getSynchronizationProviders()) {
      if (provider instanceof MultimasterReplication) {
        MultimasterReplication mmp = (MultimasterReplication) provider;
        ReplicationServerListener list = mmp.getReplicationServerListener();
        if (list != null) {
          replicationServer = list.getReplicationServer();
          if (replicationServer != null) {
            break;
          }
        }
      }
    }
    */
    ReplServerFakeConfiguration conf1 =
      new ReplServerFakeConfiguration(
          replicationServerPort, "ExternalChangeLogTestDb",
          0, 71, 0, 100, null);
    replicationServer = new ReplicationServer(conf1);;
    debugInfo("configure", "ReplicationServer created"+replicationServer);
  }
  /**
   * Launcher.
   */
  @Test(enabled=true)
  public void ECLReplicationServerTest()
  {
    ECLRemoteEmpty();replicationServer.clearDb();
    ECLDirectEmpty();replicationServer.clearDb();
    ECLDirectAllOps();replicationServer.clearDb();
    ECLRemoteNonEmpty();replicationServer.clearDb();
    ECLDirectMsg();replicationServer.clearDb();
    ECLDirectPsearch(true);replicationServer.clearDb();
    ECLDirectPsearch(false);replicationServer.clearDb();
    ECLPrivateDirectMsg();replicationServer.clearDb();
    // TODO:ECL Test SEARCH abandon and check everything shutdown and cleaned
    // TODO:ECL Test PSEARCH abandon and check everything shutdown and cleaned
    // TODO:ECL Test invalid DN in cookie returns UNWILLING + message
    // TODO:ECL Test notif control returned contains the cookie
    // TODO:ECL Test the attributes list and values returned in ECL entries
    // TODO:ECL Test search -s base, -s one
  }
  //=======================================================
  // From 3 remote ECL session,
  // Test DoneMsg is received from 1 suffix on each session.
  private void ECLRemoteEmpty()
  {
    String tn = "ECLRemoteEmpty";
    debugInfo(tn, "Starting test\n\n");
    ReplicationBroker server1 = null;
    ReplicationBroker server2 = null;
    ReplicationBroker server3 = null;
    try
    {
      // Create 3 ECL broker
      server1 = openReplicationSession(
          DN.decode("cn=changelog"), (short)1111,
          100, replicationServerPort, 1000, false);
      assertTrue(server1.isConnected());
      server2 = openReplicationSession(
          DN.decode("cn=changelog"), (short)2222,
          100, replicationServerPort,1000, false);
      assertTrue(server2.isConnected());
      server3 = openReplicationSession(
          DN.decode("cn=changelog"), (short)3333,
          100, replicationServerPort,1000, false);
      assertTrue(server3.isConnected());
      // Test broker1 receives only Done
      ReplicationMsg msg;
      int msgc=0;
      do
      {
        msg = server1.receive();
        msgc++;
      }
      while(!(msg instanceof DoneMsg));
      assertTrue(msgc==1,
          "Ending " + tn + " with incorrect message type :" +
          msg.getClass().getCanonicalName());
      assertTrue(msg instanceof DoneMsg);
      // Test broker2 receives only Done
      msgc=0;
      do
      {
        msg = server2.receive();
        msgc++;
      }
      while(!(msg instanceof DoneMsg));
      assertTrue(msgc==1,
          "Ending " + tn + " with incorrect message type :" +
          msg.getClass().getCanonicalName());
      // Test broker3 receives only Done
      msgc=0;
      do
      {
        msg = server3.receive();
        msgc++;
      }
      while(!(msg instanceof DoneMsg));
      assertTrue(msgc==1,
          "Ending " + tn + " with incorrect message type :" +
          msg.getClass().getCanonicalName());
      server1.stop();
      server2.stop();
      server3.stop();
      sleep(500);
      debugInfo(tn, "Ending test successfully\n\n");
    }
    catch(Exception e)
    {
      debugInfo(tn, "Ending test with exception:"
          +  stackTraceToSingleLineString(e));
      assertTrue(e == null);
    }
  }
  //=======================================================
  // From 1 remote ECL session,
  // test simple update to be received from 2 suffixes
  private void ECLRemoteNonEmpty()
  {
    String tn = "ECLRemoteNonEmpty";
    debugInfo(tn, "Starting test\n\n");
    replicationServer.clearDb();
    // create a broker
    ReplicationBroker server01 = null;
    ReplicationBroker server02 = null;
    ReplicationBroker serverECL = null;
    try
    {
      // create 2 reguler brokers on the 2 suffixes
      server01 = openReplicationSession(
          DN.decode(TEST_ROOT_DN_STRING), (short) 1201,
          100, replicationServerPort,
          1000, true);
      server02 = openReplicationSession(
          DN.decode(TEST_ROOT_DN_STRING2), (short) 1202,
          100, replicationServerPort,
          1000, true, EMPTY_DN_GENID);
      // create and publish 1 change on each suffix
      long time = TimeThread.getTime();
      int ts = 1;
      ChangeNumber cn1 = new ChangeNumber(time, ts++, (short)1201);
      DeleteMsg delMsg1 =
        new DeleteMsg("o=" + tn + "1," + TEST_ROOT_DN_STRING, cn1, "ECLBasicMsg1uid");
      server01.publish(delMsg1);
      debugInfo(tn, "publishes:" + delMsg1);
      ChangeNumber cn2 = new ChangeNumber(time, ts++, (short)1202);
      DeleteMsg delMsg2 =
        new DeleteMsg("o=" + tn + "2," + TEST_ROOT_DN_STRING2, cn2, "ECLBasicMsg2uid");
      server02.publish(delMsg2);
      debugInfo(tn, "publishes:" + delMsg2);
      // wait for the server to take these changes into account
      sleep(500);
      // open ECL broker
      serverECL = openReplicationSession(
          DN.decode("cn=changelog"), (short)10,
          100, replicationServerPort, 1000, false);
      assertTrue(serverECL.isConnected());
      // receive change 1 from suffix 1
      ReplicationMsg msg;
      msg = serverECL.receive();
      ECLUpdateMsg eclu = (ECLUpdateMsg)msg;
      UpdateMsg u = eclu.getUpdateMsg();
      debugInfo(tn, "RESULT:" + u.getChangeNumber());
      assertTrue(u.getChangeNumber().equals(cn1), "RESULT:" + u.getChangeNumber());
      assertTrue(eclu.getCookie().equalsTo(new MultiDomainServerState(
          "o=test:"+delMsg1.getChangeNumber()+";")));
      // receive change 2 from suffix 2
      msg = serverECL.receive();
      eclu = (ECLUpdateMsg)msg;
      u = eclu.getUpdateMsg();
      debugInfo(tn, "RESULT:" + u.getChangeNumber());
      assertTrue(u.getChangeNumber().equals(cn2), "RESULT:" + u.getChangeNumber());
      assertTrue(eclu.getCookie().equalsTo(new MultiDomainServerState(
      "o=test2:"+delMsg2.getChangeNumber()+";"+
      "o=test:"+delMsg1.getChangeNumber()+";")));
      // receive Done
      msg = serverECL.receive();
      debugInfo(tn, "RESULT:" + msg);
      assertTrue(msg instanceof DoneMsg, "RESULT:" + msg);
      // clean
      serverECL.stop();
      server01.stop();
      server02.stop();
      sleep(2000);
      debugInfo(tn, "Ending test successfully");
    }
    catch(Exception e)
    {
      debugInfo(tn, "Ending test with exception:"
          +  stackTraceToSingleLineString(e));
      assertTrue(e == null);
    }
  }
  /**
   * From embedded ECL (no remote session)
   * With empty RS, simple search should return only root entry.
   */
  private void ECLDirectEmpty()
  {
    String tn = "ECLDirectEmpty";
    debugInfo(tn, "Starting test\n\n");
    try
    {
      // search on 'cn=changelog'
      InternalSearchOperation op = connection.processSearch(
        ByteString.valueOf("cn=changelog"),
        SearchScope.WHOLE_SUBTREE,
        LDAPFilter.decode("(objectclass=*)"));
      debugInfo(tn, "Res code=" + op.getResultCode());
      debugInfo(tn, "Res OE=" + ResultCode.OPERATIONS_ERROR);
      // Error because no cookie
      assertEquals(
          op.getResultCode(), ResultCode.OPERATIONS_ERROR,
          op.getErrorMessage().toString());
      // search on 'cn=changelog'
      InternalSearchOperation op2 = connection.processSearch(
          ByteString.valueOf("cn=changelog"),
          SearchScope.WHOLE_SUBTREE,
          DereferencePolicy.NEVER_DEREF_ALIASES,
          0, 0,
          false,
          LDAPFilter.decode("(objectclass=*)"),
          new LinkedHashSet<String>(0),
          getControls(""),
          null);
      // success
      assertEquals(
          op2.getResultCode(), ResultCode.SUCCESS,
          op2.getErrorMessage().toString());
      // root entry returned
      assertEquals(op2.getEntriesSent(), 1);
      debugInfo(tn, "Ending test successfully");
    }
    catch(LDAPException e)
    {
      debugInfo(tn, "Ending test with exception e="
          +  stackTraceToSingleLineString(e));
      assertTrue(e == null);
    }
  }
  private ArrayList<Control> getControls(String cookie)
  {
    ExternalChangelogRequestControl control =
      new ExternalChangelogRequestControl(true,
          new MultiDomainServerState(cookie));
    ArrayList<Control> controls = new ArrayList<Control>(0);
    controls.add(control);
    return controls;
  }
  // Utility - creates an LDIFWriter to dump result entries
  private static LDIFWriter getLDIFWriter()
  {
    LDIFWriter ldifWriter = null;
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    LDIFExportConfig exportConfig = new LDIFExportConfig(stream);
    try
    {
      ldifWriter = new LDIFWriter(exportConfig);
    }
    catch (Exception e)
    {
      assert(e==null);
    }
    return ldifWriter;
  }
  // Add an entry in the database
  private void addEntry(Entry entry) throws Exception
  {
    AddOperationBasis addOp = new AddOperationBasis(connection,
        InternalClientConnection.nextOperationID(), InternalClientConnection
        .nextMessageID(), null, entry.getDN(), entry.getObjectClasses(),
        entry.getUserAttributes(), entry.getOperationalAttributes());
    addOp.setInternalOperation(true);
    addOp.run();
    assertEquals(addOp.getResultCode(), ResultCode.SUCCESS,
        addOp.getErrorMessage().toString() + addOp.getAdditionalLogMessage());
    assertNotNull(getEntry(entry.getDN(), 1000, true));
  }
  //=======================================================
  // From embebbded ECL
  // Search ECL with 1 domain, public then private backend
  private void ECLPrivateDirectMsg()
  {
    ReplicationServer replServer = null;
    String tn = "ECLPrivateDirectMsg";
    debugInfo(tn, "Starting test");
    try
    {
      // Initialize a second test backend
      Backend backend2 = initializeTestBackend2(false);
      DN baseDn = DN.decode(TEST_ROOT_DN_STRING2);
      // configure and start replication of TEST_ROOT_DN_STRING on the server
      SortedSet<String> replServers = new TreeSet<String>();
      replServers.add("localhost:"+replicationServerPort);
      DomainFakeCfg domainConf =
        new DomainFakeCfg(baseDn, (short) 1602, replServers);
      LDAPReplicationDomain domain = MultimasterReplication.createNewDomain(domainConf);
      SynchronizationProvider replicationPlugin = new MultimasterReplication();
      replicationPlugin.completeSynchronizationProvider();
      sleep(1000);
      Entry e = createEntry(baseDn);
      addEntry(e);
      sleep(1000);
      ExternalChangelogRequestControl control =
        new ExternalChangelogRequestControl(true,
            new MultiDomainServerState(""));
      ArrayList<Control> controls = new ArrayList<Control>(0);
      controls.add(control);
      // search on 'cn=changelog'
      LinkedHashSet<String> attributes = new LinkedHashSet<String>();
      attributes.add("+");
      attributes.add("*");
      InternalSearchOperation searchOp = connection.processSearch(
          ByteString.valueOf("cn=changelog"),
          SearchScope.WHOLE_SUBTREE,
          DereferencePolicy.NEVER_DEREF_ALIASES,
          0, // Size limit
          0, // Time limit
          false, // Types only
          LDAPFilter.decode("(targetDN=*)"),
          attributes,
          controls,
          null);
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS,
          searchOp.getErrorMessage().toString() + searchOp.getAdditionalLogMessage());
      LinkedList<SearchResultEntry> entries = searchOp.getSearchEntries();
      assertTrue(entries != null);
      if (entries != null)
      {
        for (SearchResultEntry resultEntry : entries)
        {
          debugInfo(tn, "Result private entry=" + resultEntry.toLDIFString());
        }
      }
      assertEquals(entries.size(),1, "Entries number returned by search");
      // Same with private backend
      domain.getBackend().setPrivateBackend(true);
      searchOp = connection.processSearch(
          ByteString.valueOf("cn=changelog"),
          SearchScope.WHOLE_SUBTREE,
          DereferencePolicy.NEVER_DEREF_ALIASES,
          0, // Size limit
          0, // Time limit
          false, // Types only
          LDAPFilter.decode("(targetDN=*)"),
          attributes,
          controls,
          null);
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS,
          searchOp.getErrorMessage().toString() + searchOp.getAdditionalLogMessage());
      entries = searchOp.getSearchEntries();
      assertTrue(entries != null);
      assertTrue(entries.size()==0);
      if (replServer != null)
        replServer.remove();
      if (domain != null)
        MultimasterReplication.deleteDomain(baseDn);
      if (replicationPlugin != null)
        DirectoryServer.deregisterSynchronizationProvider(replicationPlugin);
      removeTestBackend2(backend2);
    }
    catch(Exception e)
    {
      debugInfo(tn, "Ending test ECLDirectMsg with exception:\n"
          +  stackTraceToSingleLineString(e));
      assertTrue(e == null);
    }
    debugInfo(tn, "Ending test successfully");
  }
  //=======================================================
  // From embebbded ECL
  // Search ECL with 4 messages on 2 suffixes from 2 brokers
  private void ECLDirectMsg()
  {
    String tn = "ECLDirectMsg";
    debugInfo(tn, "Starting test");
    try
    {
      // Initialize a second test backend
      Backend backend2 = initializeTestBackend2(true);
      //
      LDIFWriter ldifWriter = getLDIFWriter();
      // --
      ReplicationBroker s1test = openReplicationSession(
          DN.decode(TEST_ROOT_DN_STRING), (short) 1201,
          100, replicationServerPort,
          1000, true);
      ReplicationBroker s2test2 = openReplicationSession(
          DN.decode(TEST_ROOT_DN_STRING2), (short) 1202,
          100, replicationServerPort,
          1000, true, EMPTY_DN_GENID);
      sleep(500);
      // Produce updates
      long time = TimeThread.getTime();
      int ts = 1;
      ChangeNumber cn = new ChangeNumber(time, ts++, s1test.getServerId());
      DeleteMsg delMsg =
        new DeleteMsg("uid="+tn+"1," + TEST_ROOT_DN_STRING, cn, tn+"uuid1");
      s1test.publish(delMsg);
      debugInfo(tn, " publishes " + delMsg.getChangeNumber());
      cn = new ChangeNumber(time++, ts++, s2test2.getServerId());
      delMsg =
        new DeleteMsg("uid="+tn+"2," + TEST_ROOT_DN_STRING2, cn, tn+"uuid2");
      s2test2.publish(delMsg);
      debugInfo(tn, " publishes " + delMsg.getChangeNumber());
      cn = new ChangeNumber(time++, ts++, s2test2.getServerId());
      delMsg =
        new DeleteMsg("uid="+tn+"3," + TEST_ROOT_DN_STRING2, cn, tn+"uuid3");
      s2test2.publish(delMsg);
      debugInfo(tn, " publishes " + delMsg.getChangeNumber());
      cn = new ChangeNumber(time++, ts++, s1test.getServerId());
      delMsg =
        new DeleteMsg("uid="+tn+"4," + TEST_ROOT_DN_STRING, cn, tn+"uuid4");
      s1test.publish(delMsg);
      debugInfo(tn, " publishes " + delMsg.getChangeNumber());
      sleep(500);
      // Changes are :
      //               s1          s2
      // o=test       msg1/msg4
      // o=test2                 msg2/msg2
      String cookie= "";
      debugInfo(tn, "STEP 1 - from empty cookie("+cookie+")");
      // search on 'cn=changelog'
      LinkedHashSet<String> attributes = new LinkedHashSet<String>();
      attributes.add("+");
      attributes.add("*");
      InternalSearchOperation searchOp =
        connection.processSearch(
            ByteString.valueOf("cn=changelog"),
          SearchScope.WHOLE_SUBTREE,
          DereferencePolicy.NEVER_DEREF_ALIASES,
          0, // Size limit
          0, // Time limit
          false, // Types only
          LDAPFilter.decode("(targetDN=*direct*)"),
          attributes,
          getControls(cookie),
          null);
      // We expect SUCCESS and the 4 changes
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS,
          searchOp.getErrorMessage().toString());
      LinkedList<SearchResultEntry> entries = searchOp.getSearchEntries();
      if (entries != null)
      {
        int i=0;
        for (SearchResultEntry entry : entries)
        {
          debugInfo(tn, " RESULT entry returned:" + entry.toSingleLineString());
          ldifWriter.writeEntry(entry);
          try
          {
            if (i++==2)
              cookie =
                entry.getAttribute("cookie").get(0).iterator().next().toString();
          }
          catch(NullPointerException e)
          {}
        }
      }
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS);
      // We expect the 4 changes
      assertEquals(searchOp.getSearchEntries().size(), 4);
      // Now start from last cookie and expect to get the last change
      // search on 'cn=changelog'
      attributes = new LinkedHashSet<String>();
      attributes.add("+");
      attributes.add("*");
      debugInfo(tn, "STEP 2 - from cookie" + cookie);
      ExternalChangelogRequestControl control =
        new ExternalChangelogRequestControl(true,
            new MultiDomainServerState(cookie));
      ArrayList<Control> controls = new ArrayList<Control>(0);
      controls.add(control);
      searchOp = connection.processSearch(
          ByteString.valueOf("cn=changelog"),
          SearchScope.WHOLE_SUBTREE,
          DereferencePolicy.NEVER_DEREF_ALIASES,
          0, // Size limit
          0, // Time limit
          false, // Types only
          LDAPFilter.decode("(targetDN=*direct*)"),
          attributes,
          controls,
          null);
      // We expect SUCCESS and the 4th change
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS,
          searchOp.getErrorMessage().toString() + searchOp.getAdditionalLogMessage());
      cookie= "";
      entries = searchOp.getSearchEntries();
      if (entries != null)
      {
        for (SearchResultEntry entry : entries)
        {
          debugInfo(tn, "Result entry=\n" + entry.toLDIFString());
          ldifWriter.writeEntry(entry);
          try
          {
          cookie =
            entry.getAttribute("cookie").get(0).iterator().next().toString();
          }
          catch(NullPointerException e)
          {}
        }
      }
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS);
      // we expect msg4
      assertEquals(searchOp.getSearchEntries().size(), 1);
      debugInfo(tn, "STEP 3 - from cookie" + cookie);
      cn = new ChangeNumber(time++, ts++, s1test.getServerId());
      delMsg =
        new DeleteMsg("uid="+tn+"5," + TEST_ROOT_DN_STRING, cn, tn+"uuid5");
      s1test.publish(delMsg);
      sleep(500);
      // Changes are :
      //               s1         s2
      // o=test       msg1,msg5   msg4
      // o=test2      msg3        msg2
      control =
        new ExternalChangelogRequestControl(true,
            new MultiDomainServerState(cookie));
      controls = new ArrayList<Control>(0);
      controls.add(control);
      searchOp = connection.processSearch(
          ByteString.valueOf("cn=changelog"),
          SearchScope.WHOLE_SUBTREE,
          DereferencePolicy.NEVER_DEREF_ALIASES,
          0, // Size limit
          0, // Time limit
          false, // Types only
          LDAPFilter.decode("(targetDN=*direct*)"),
          attributes,
          controls,
          null);
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS,
          searchOp.getErrorMessage().toString() + searchOp.getAdditionalLogMessage());
      cookie= "";
      entries = searchOp.getSearchEntries();
      if (entries != null)
      {
        for (SearchResultEntry resultEntry : entries)
        {
          debugInfo(tn, "Result entry=\n" + resultEntry.toLDIFString());
          ldifWriter.writeEntry(resultEntry);
          try
          {
          cookie =
            resultEntry.getAttribute("cookie").get(0).iterator().next().toString();
          }
          catch(NullPointerException e)
          {}
        }
      }
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS);
      // we expect 1 entries : msg5
      assertEquals(searchOp.getSearchEntries().size(), 1);
      cookie="";
      debugInfo(tn, "STEP 4 - [filter:o=test cookie:" + cookie + "]");
      control =
        new ExternalChangelogRequestControl(true,
            new MultiDomainServerState(cookie));
      controls = new ArrayList<Control>(0);
      controls.add(control);
      searchOp = connection.processSearch(
          ByteString.valueOf("cn=changelog"),
          SearchScope.WHOLE_SUBTREE,
          DereferencePolicy.NEVER_DEREF_ALIASES,
          0, // Size limit
          0, // Time limit
          false, // Types only
          LDAPFilter.decode("(targetDN=*direct*,o=test)"),
          attributes,
          controls,
          null);
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS,
          searchOp.getErrorMessage().toString() + searchOp.getAdditionalLogMessage());
      entries = searchOp.getSearchEntries();
      if (entries != null)
      {
        for (SearchResultEntry resultEntry : entries)
        {
          debugInfo(tn, "Result entry=\n" + resultEntry.toLDIFString());
          ldifWriter.writeEntry(resultEntry);
          try
          {
          cookie =
            resultEntry.getAttribute("cookie").get(0).iterator().next().toString();
          }
          catch(NullPointerException e)
          {}
        }
      }
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS);
      // we expect msg1 + msg4 + msg5
      assertEquals(searchOp.getSearchEntries().size(), 3);
      // --
      ReplicationBroker s1test2 = openReplicationSession(
          DN.decode(TEST_ROOT_DN_STRING2), (short) 1203,
          100, replicationServerPort,
          1000, true, EMPTY_DN_GENID);
      ReplicationBroker s2test = openReplicationSession(
          DN.decode(TEST_ROOT_DN_STRING), (short) 1204,
          100, replicationServerPort,
          1000, true);
      sleep(500);
      // Test startState of the domain for the first cookie feature
      time = TimeThread.getTime();
      cn = new ChangeNumber(time++, ts++, s1test2.getServerId());
      delMsg =
        new DeleteMsg("uid="+tn+"6," + TEST_ROOT_DN_STRING2, cn, tn+"uuid6");
      s1test2.publish(delMsg);
      cn = new ChangeNumber(time++, ts++, s2test.getServerId());
      delMsg =
        new DeleteMsg("uid="+tn+"7," + TEST_ROOT_DN_STRING, cn, tn+"uuid7");
      s2test.publish(delMsg);
      cn = new ChangeNumber(time++, ts++, s1test2.getServerId());
      delMsg =
        new DeleteMsg("uid="+tn+"8," + TEST_ROOT_DN_STRING2, cn, tn+"uuid8");
      s1test2.publish(delMsg);
      cn = new ChangeNumber(time++, ts++, s2test.getServerId());
      delMsg =
        new DeleteMsg("uid="+tn+"9," + TEST_ROOT_DN_STRING, cn, tn+"uuid9");
      s2test.publish(delMsg);
      sleep(500);
      ReplicationServerDomain rsd =
        replicationServer.getReplicationServerDomain(TEST_ROOT_DN_STRING, false);
      ServerState startState = rsd.getStartState();
      assertTrue(startState.getMaxChangeNumber(s1test.getServerId()).getSeqnum()==1);
      assertTrue(startState.getMaxChangeNumber(s2test.getServerId()).getSeqnum()==7);
      rsd =
        replicationServer.getReplicationServerDomain(TEST_ROOT_DN_STRING2, false);
      startState = rsd.getStartState();
      assertTrue(startState.getMaxChangeNumber(s2test2.getServerId()).getSeqnum()==2);
      assertTrue(startState.getMaxChangeNumber(s1test2.getServerId()).getSeqnum()==6);
      s1test.stop();
      s1test2.stop();
      s2test.stop();
      s2test2.stop();
      removeTestBackend2(backend2);
    }
    catch(Exception e)
    {
      debugInfo(tn, "Ending test ECLDirectMsg with exception:\n"
          +  stackTraceToSingleLineString(e));
      assertTrue(e == null);
    }
    debugInfo(tn, "Ending test successfully");
  }
  // simple update to be received
  private void ECLDirectAllOps()
  {
    String tn = "ECLDirectAllOps";
    debugInfo(tn, "Starting test\n\n");
    try
    {
      LDIFWriter ldifWriter = getLDIFWriter();
      // Creates broker on o=test
      ReplicationBroker server01 = openReplicationSession(
          DN.decode(TEST_ROOT_DN_STRING), (short) 1201,
          100, replicationServerPort,
          1000, true);
      int ts = 1;
      String user1entryUUID = "11111111-1111-1111-1111-111111111111";
      String baseUUID       = "22222222-2222-2222-2222-222222222222";
      // Publish DEL
      ChangeNumber cn1 = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201);
      DeleteMsg delMsg =
        new DeleteMsg("uid="+tn+"1," + TEST_ROOT_DN_STRING, cn1, tn+"uuid1");
      server01.publish(delMsg);
      debugInfo(tn, " publishes " + delMsg.getChangeNumber());
      // Publish ADD
      ChangeNumber cn2 = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201);
      String lentry = new String("dn: uid="+tn+"2," + TEST_ROOT_DN_STRING + "\n"
          + "objectClass: top\n" + "objectClass: domain\n"
          + "entryUUID: "+user1entryUUID+"\n");
      Entry entry = TestCaseUtils.entryFromLdifString(lentry);
      AddMsg addMsg = new AddMsg(
          cn2,
          "uid="+tn+"2," + TEST_ROOT_DN_STRING,
          user1entryUUID,
          baseUUID,
          entry.getObjectClassAttribute(),
          entry.getAttributes(),
          new ArrayList<Attribute>());
      server01.publish(addMsg);
      debugInfo(tn, " publishes " + addMsg.getChangeNumber());
      // Publish MOD
      ChangeNumber cn3 = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201);
      Attribute attr1 = Attributes.create("description", "new value");
      Modification mod1 = new Modification(ModificationType.REPLACE, attr1);
      List<Modification> mods = new ArrayList<Modification>();
      mods.add(mod1);
      ModifyMsg modMsg = new ModifyMsg(cn3, DN
          .decode("uid="+tn+"3," + TEST_ROOT_DN_STRING), mods, tn+"uuid3");
      server01.publish(modMsg);
      debugInfo(tn, " publishes " + modMsg.getChangeNumber());
      // Publish modDN
      ChangeNumber cn4 = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201);
      ModifyDNOperationBasis op = new ModifyDNOperationBasis(connection, 1, 1, null,
          DN.decode("uid="+tn+"4," + TEST_ROOT_DN_STRING), // entryDN
          RDN.decode("uid="+tn+"new4"), // new rdn
          true,  // deleteoldrdn
          DN.decode(TEST_ROOT_DN_STRING2)); // new superior
      op.setAttachment(SYNCHROCONTEXT, new ModifyDnContext(cn4, tn+"uuid4",
      "newparentId"));
      LocalBackendModifyDNOperation localOp = new LocalBackendModifyDNOperation(op);
      ModifyDNMsg modDNMsg = new ModifyDNMsg(localOp);
      server01.publish(modDNMsg);
      debugInfo(tn, " publishes " + modDNMsg.getChangeNumber());
      sleep(600);
      String cookie= "";
      // search on 'cn=changelog'
      debugInfo(tn, "STEP 1 - from empty cookie("+cookie+")");
      LinkedHashSet<String> attributes = new LinkedHashSet<String>();
      attributes.add("+");
      attributes.add("*");
      ExternalChangelogRequestControl control =
        new ExternalChangelogRequestControl(true,
            new MultiDomainServerState());
      ArrayList<Control> controls = new ArrayList<Control>(0);
      controls.add(control);
      InternalSearchOperation searchOp =
        connection.processSearch(
            ByteString.valueOf("cn=changelog"),
          SearchScope.WHOLE_SUBTREE,
          DereferencePolicy.NEVER_DEREF_ALIASES,
          0, // Size limit
          0, // Time limit
          false, // Types only
          LDAPFilter.decode("(targetDN=*"+tn+"*,o=test)"),
          attributes,
          controls,
          null);
      sleep(500);
      // test success
      assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS,
          searchOp.getErrorMessage().toString());
      // test 4 entries returned
      LinkedList<SearchResultEntry> entries = searchOp.getSearchEntries();
      // 4 entries expected
      assertEquals(searchOp.getSearchEntries().size(), 4);
      if (entries != null)
      {
        int i=0;
        for (SearchResultEntry resultEntry : entries)
        {
          i++;
          debugInfo(tn, "Result entry returned:" + resultEntry.toLDIFString());
          ldifWriter.writeEntry(resultEntry);
          if (i==1)
          {
            // check the DEL entry has the right content
            checkValue(resultEntry,"replicationcsn",cn1.toString());
            checkValue(resultEntry,"replicaidentifier","1201");
            checkValue(resultEntry,"targetdn","uid="+tn+"1," + TEST_ROOT_DN_STRING);
            checkValue(resultEntry,"changetype","delete");
            checkValue(resultEntry,"cookie","o=test:"+cn1.toString()+";");
            checkValue(resultEntry,"targetentryuuid",tn+"uuid1");
          } else if (i==2)
          {
            // check the ADD entry has the right content
            String expectedValue1 = "objectClass: domain\nobjectClass: top\n" +
            "entryUUID: 11111111-1111-1111-1111-111111111111\n\n";
            String expectedValue2 = "entryUUID: 11111111-1111-1111-1111-111111111111\n" +
            "objectClass: domain\nobjectClass: top\n\n";
            checkPossibleValues(resultEntry,"changes",expectedValue1, expectedValue2);
            checkValue(resultEntry,"replicationcsn",cn2.toString());
            checkValue(resultEntry,"replicaidentifier","1201");
            checkValue(resultEntry,"targetdn","uid="+tn+"2," + TEST_ROOT_DN_STRING);
            checkValue(resultEntry,"changetype","add");
            checkValue(resultEntry,"cookie","o=test:"+cn2.toString()+";");
            checkValue(resultEntry,"targetentryuuid",user1entryUUID);
          } else if (i==3)
          {
            // check the MOD entry has the right content
            String expectedValue = "replace: description\n" +
            "description: new value\n-\n";
            checkValue(resultEntry,"changes",expectedValue);
            checkValue(resultEntry,"replicationcsn",cn3.toString());
            checkValue(resultEntry,"replicaidentifier","1201");
            checkValue(resultEntry,"targetdn","uid="+tn+"3," + TEST_ROOT_DN_STRING);
            checkValue(resultEntry,"changetype","modify");
            checkValue(resultEntry,"cookie","o=test:"+cn3.toString()+";");
            checkValue(resultEntry,"targetentryuuid",tn+"uuid3");
          } else if (i==4)
          {
            // check the MODDN entry has the right content
            checkValue(resultEntry,"replicationcsn",cn4.toString());
            checkValue(resultEntry,"replicaidentifier","1201");
            checkValue(resultEntry,"targetdn","uid="+tn+"4," + TEST_ROOT_DN_STRING);
            checkValue(resultEntry,"changetype","modrdn");
            checkValue(resultEntry,"cookie","o=test:"+cn4.toString()+";");
            checkValue(resultEntry,"targetentryuuid",tn+"uuid4");
            checkValue(resultEntry,"newrdn","uid=ECLDirectAllOpsnew4");
            checkValue(resultEntry,"newsuperior",TEST_ROOT_DN_STRING2);
            checkValue(resultEntry,"deleteoldrdn","true");
          }
        }
      }
      server01.stop();
    }
    catch(Exception e)
    {
      debugInfo(tn, "Ending test with exception:\n"
          +  stackTraceToSingleLineString(e));
      assertTrue(e == null);
    }
    debugInfo(tn, "Ending test with success");
  }
  private static void checkValue(Entry entry, String attrName, String expectedValue)
  {
    AttributeValue av = null;
    try
    {
    List<Attribute> attrs = entry.getAttribute(attrName);
    Attribute a = attrs.iterator().next();
    av = a.iterator().next();
    String encodedValue = av.toString();
      assertTrue(encodedValue.equalsIgnoreCase(expectedValue),
        "In entry " + entry + " attr <" + attrName + "> equals " +
        av + " instead of expected value " + expectedValue);
    }
    catch(Exception e)
    {
      assertTrue(false,
          "In entry " + entry + " attr <" + attrName + "> equals " +
          av + " instead of expected value " + expectedValue);
    }
  }
  private static void checkPossibleValues(Entry entry, String attrName,
      String expectedValue1, String expectedValue2)
  {
    AttributeValue av = null;
    try
    {
    List<Attribute> attrs = entry.getAttribute(attrName);
    Attribute a = attrs.iterator().next();
    av = a.iterator().next();
    String encodedValue = av.toString();
      assertTrue(
          (encodedValue.equalsIgnoreCase(expectedValue1) ||
              encodedValue.equalsIgnoreCase(expectedValue2)),
        "In entry " + entry + " attr <" + attrName + "> equals " +
        av + " instead of one of the expected values " + expectedValue1
        + " or " + expectedValue2);
    }
    catch(Exception e)
    {
      assertTrue(false,
          "In entry " + entry + " attr <" + attrName + "> equals " +
          av + " instead of one of the expected values " + expectedValue1
          + " or " + expectedValue2);
    }
  }
  /**
   * Test persistent search
   */
  private void ECLDirectPsearch(boolean changesOnly)
  {
    String tn = "ECLDirectPsearch_" + String.valueOf(changesOnly);
    debugInfo(tn, "Starting test \n\n");
    Socket s =null;
    // create stats
    for (ConnectionHandler ch : DirectoryServer.getConnectionHandlers())
    {
      if (ch instanceof LDAPConnectionHandler)
      {
        LDAPConnectionHandler lch = (LDAPConnectionHandler) ch;
        if (!lch.useSSL())
        {
          ldapStatistics = lch.getStatTracker();
        }
      }
    }
    assertNotNull(ldapStatistics);
    try
    {
      // Create broker on suffix
      ReplicationBroker server01 = openReplicationSession(
          DN.decode(TEST_ROOT_DN_STRING), (short) 1201,
          100, replicationServerPort,
          1000, true);
      int ts = 1;
      // Produce update on this suffix
      ChangeNumber cn = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201);
      DeleteMsg delMsg =
        new DeleteMsg("uid=" + tn + "1," + TEST_ROOT_DN_STRING, cn, tn+"uuid1");
      debugInfo(tn, " publishing " + delMsg.getChangeNumber());
      server01.publish(delMsg);
      this.sleep(500); // let's be sure the message is in the RS
      // Creates cookie control
      ArrayList<Control> controls = getControls("");
      // Creates psearch control
      HashSet<PersistentSearchChangeType> changeTypes =
        new HashSet<PersistentSearchChangeType>();
      changeTypes.add(PersistentSearchChangeType.ADD);
      changeTypes.add(PersistentSearchChangeType.DELETE);
      changeTypes.add(PersistentSearchChangeType.MODIFY);
      changeTypes.add(PersistentSearchChangeType.MODIFY_DN);
      boolean returnECs = true;
      PersistentSearchControl persSearchControl = new PersistentSearchControl(
          changeTypes, changesOnly, returnECs);
      controls.add(persSearchControl);
      // Creates request
      SearchRequestProtocolOp searchRequest =
        new SearchRequestProtocolOp(
            ByteString.valueOf("cn=changelog"),
            SearchScope.WHOLE_SUBTREE,
            DereferencePolicy.NEVER_DEREF_ALIASES,
            Integer.MAX_VALUE,
            Integer.MAX_VALUE,
            false,
            LDAPFilter.decode("(targetDN=*"+tn+"*,o=test)"),
            null);
      // Connects and bind
      s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
      org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s);
      LDAPWriter w = new LDAPWriter(s);
      s.setSoTimeout(1500000);
      bindAsManager(w, r);
      // Since we are going to be watching the post-response count, we need to
      // wait for the server to become idle before kicking off the next request
      // to ensure that any remaining post-response processing from the previous
      // operation has completed.
      assertTrue(DirectoryServer.getWorkQueue().waitUntilIdle(10000));
      InvocationCounterPlugin.resetAllCounters();
      long searchRequests   = ldapStatistics.getSearchRequests();
      long searchEntries    = ldapStatistics.getSearchResultEntries();
      long searchReferences = ldapStatistics.getSearchResultReferences();
      long searchesDone     = ldapStatistics.getSearchResultsDone();
      debugInfo(tn, "Sending the PSearch request filter=(targetDN=*"+tn+"*,o=test)");
      LDAPMessage message;
      message = new LDAPMessage(2, searchRequest, controls);
      w.writeMessage(message);
      this.sleep(500);
      SearchResultEntryProtocolOp searchResultEntry = null;
      SearchResultDoneProtocolOp searchResultDone = null;
      if (changesOnly == false)
      {
        // Wait for change 1
        debugInfo(tn, "Waiting for : INIT search expected to return change 1");
        searchEntries = 0;
        message = null;
        try
        {
          while ((searchEntries<1) && (message = r.readMessage()) != null)
          {
            debugInfo(tn, "First search returns " +
                message.getProtocolOpType() + message + " " + searchEntries);
            switch (message.getProtocolOpType())
            {
            case LDAPConstants.OP_TYPE_SEARCH_RESULT_ENTRY:
              searchResultEntry = message.getSearchResultEntryProtocolOp();
              searchEntries++;
              break;
            case LDAPConstants.OP_TYPE_SEARCH_RESULT_REFERENCE:
              searchReferences++;
              break;
            case LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE:
              searchResultDone = message.getSearchResultDoneProtocolOp();
              assertEquals(
                  searchResultDone.getResultCode(), ResultCode.SUCCESS,
                  searchResultDone.getErrorMessage().toString());
//            assertEquals(InvocationCounterPlugin.waitForPostResponse(), 1);
              searchesDone++;
              break;
            }
          }
        }
        catch(Exception e)
        {
          debugInfo(tn, "INIT search failed with e=" +
              stackTraceToSingleLineString(e));
        }
        debugInfo(tn, "INIT search done with success. searchEntries="
            + searchEntries + " searchesDone="+ searchesDone);
      }
      // Produces change 2
      cn = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201);
      String expectedDn = "uid=" + tn + "2," +  TEST_ROOT_DN_STRING;
      delMsg = new DeleteMsg(expectedDn, cn, tn + "uuid2");
      debugInfo(tn, " publishing " + delMsg.getChangeNumber());
      server01.publish(delMsg);
      this.sleep(1000);
      debugInfo(tn, delMsg.getChangeNumber() +
          " published , will wait for new entries (Persist)");
      // wait for the 1 new entry
      searchEntries = 0;
      searchResultEntry = null;
      searchResultDone = null;
      message = null;
      while ((searchEntries<1) && (message = r.readMessage()) != null)
      {
        debugInfo(tn, "2nd search returns " +
            message.getProtocolOpType() + message);
        switch (message.getProtocolOpType())
        {
        case LDAPConstants.OP_TYPE_SEARCH_RESULT_ENTRY:
          searchResultEntry = message.getSearchResultEntryProtocolOp();
          searchEntries++;
          break;
        case LDAPConstants.OP_TYPE_SEARCH_RESULT_REFERENCE:
          searchReferences++;
          break;
        case LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE:
          searchResultDone = message.getSearchResultDoneProtocolOp();
          assertEquals(
              searchResultDone.getResultCode(), ResultCode.SUCCESS,
              searchResultDone.getErrorMessage().toString());
//        assertEquals(InvocationCounterPlugin.waitForPostResponse(), 1);
          searchesDone++;
          break;
        }
      }
      sleep(1000);
      // Check we received change 2
      for (LDAPAttribute a : searchResultEntry.getAttributes())
      {
        if (a.getAttributeType().equalsIgnoreCase("targetDN"))
        {
          for (ByteString av : a.getValues())
          {
            assertTrue(av.toString().equalsIgnoreCase(expectedDn),
                "Entry returned by psearch is " + av.toString() +
                " when expected is " + expectedDn);
          }
        }
      }
      debugInfo(tn, "Second search done successfully : " + searchResultEntry);
      server01.stop();
      try { s.close(); } catch (Exception e) {};
      sleep(1000);
      // TODO:  Testing ACI is disabled because it is currently failing when
      // ran in the precommit target while it works well when running alone.
      // anonymous search returns entries from cn=changelog whereas it
      // should not. Probably a previous test in the nightlytests suite is
      // removing/modifying some ACIs...
      // When problem found, we have to re-enable this test.
      if (false)
      {
      // ACI step
      debugInfo(tn, "Starting ACI step");
      s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort());
      r = new org.opends.server.tools.LDAPReader(s);
      w = new LDAPWriter(s);
      s.setSoTimeout(1500000);
      bindAsWhoEver(w, r, "toto", "tutu", LDAPResultCode.OPERATIONS_ERROR);
      searchRequest =
        new SearchRequestProtocolOp(
            ByteString.valueOf("cn=changelog"),
            SearchScope.WHOLE_SUBTREE,
            DereferencePolicy.NEVER_DEREF_ALIASES,
            Integer.MAX_VALUE,
            Integer.MAX_VALUE,
            false,
            LDAPFilter.decode("(targetDN=*directpsearch*,o=test)"),
            null);
      debugInfo(tn, "ACI test : sending search");
      message = new LDAPMessage(2, searchRequest, getControls(""));
      w.writeMessage(message);
      searchesDone=0;
      searchEntries = 0;
      searchResultEntry = null;
      searchResultDone = null;
      while ((searchesDone==0) && (message = r.readMessage()) != null)
      {
        debugInfo(tn, "ACI test : message returned " +
            message.getProtocolOpType() + message);
        switch (message.getProtocolOpType())
        {
        case LDAPConstants.OP_TYPE_SEARCH_RESULT_ENTRY:
          searchResultEntry = message.getSearchResultEntryProtocolOp();
          //assertTrue(false, "Unexpected entry returned in ACI test of " + tn + searchResultEntry);
          searchEntries++;
          break;
        case LDAPConstants.OP_TYPE_SEARCH_RESULT_REFERENCE:
          searchReferences++;
          break;
        case LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE:
          searchResultDone = message.getSearchResultDoneProtocolOp();
          assertEquals(searchResultDone.getResultCode(),
              ResultCode.SUCCESS.getIntValue());
//        assertEquals(InvocationCounterPlugin.waitForPostResponse(), 1);
          searchesDone++;
          break;
        }
      }
      // search should end with success
      assertTrue(searchesDone==1);
      // but returning no entry
      assertEquals(searchEntries,0, "Bad search entry# in ACI test of " + tn);
      }
      try { s.close(); } catch (Exception e) {};
      sleep(1000);
    }
    catch(Exception e)
    {
      assertTrue(e==null, stackTraceToSingleLineString(e));
    }
    debugInfo(tn, "Ends test successfuly");
  }
  // utility - bind as required
  private void bindAsManager(LDAPWriter w, org.opends.server.tools.LDAPReader r)
  throws IOException, LDAPException, ASN1Exception, InterruptedException
  {
    bindAsWhoEver(w, r,
        "cn=Directory Manager", "password", LDAPResultCode.SUCCESS);
  }
  // utility - bind as required
  private void bindAsWhoEver(LDAPWriter w, org.opends.server.tools.LDAPReader r,
      String bindDN, String password,  int expected)
  throws IOException, LDAPException, ASN1Exception, InterruptedException
  {
//  Since we are going to be watching the post-response count, we need to
//  wait for the server to become idle before kicking off the next request to
//  ensure that any remaining post-response processing from the previous
//  operation has completed.
    assertTrue(DirectoryServer.getWorkQueue().waitUntilIdle(10000));
    InvocationCounterPlugin.resetAllCounters();
    BindRequestProtocolOp bindRequest =
      new BindRequestProtocolOp(
          ByteString.valueOf(bindDN),
          3, ByteString.valueOf(password));
    LDAPMessage message = new LDAPMessage(1, bindRequest);
    w.writeMessage(message);
    message = r.readMessage();
    BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp();
//  assertEquals(InvocationCounterPlugin.waitForPostResponse(), 1);
    assertEquals(bindResponse.getResultCode(), expected);
  }
  /**
   * Clean up the environment.
   *
   * @throws Exception If the environment could not be set up.
   */
  @AfterClass
  public void classCleanUp() throws Exception
  {
    callParanoiaCheck = false;
    super.classCleanUp();
    shutdown();
    paranoiaCheck();
  }
  /**
   * After the tests stop the replicationServer.
   */
  protected void shutdown() throws Exception
  {
    if (replicationServer != null)
      replicationServer.remove();
    /*
    TestCaseUtils.dsconfig(
        "delete-replication-server",
        "--provider-name", "Multimaster Synchronization");
        */
    replicationServer = null;
  }
  /**
   * Utility - sleeping as long as required
   */
  private void sleep(long time)
  {
    try
    {
      Thread.sleep(time);
    } catch (InterruptedException ex)
    {
      fail("Error sleeping " + ex.getMessage());
    }
  }
  /**
   * Utility - log debug message - highlight it is from the test and not
   * from the server code. Makes easier to observe the test steps.
   */
  private void debugInfo(String tn, String s)
  {
    //if (debugEnabled())
    {
      logError(Message.raw(Category.SYNC, Severity.NOTICE,
          "** TEST " + tn + " ** " + s));
      TRACER.debugInfo("** TEST " + tn + " ** " + s);
    }
  }
  /**
   * Utility - create a second backend in order to test ECL with 2 suffixes.
   */
  private static Backend initializeTestBackend2(boolean createBaseEntry)
  throws IOException, InitializationException, ConfigException,
         DirectoryException
  {
    DN baseDN = DN.decode(TEST_ROOT_DN_STRING2);
    //  Retrieve backend. Warning: it is important to perform this each time,
    //  because a test may have disabled then enabled the backend (i.e a test
    //  performing an import task). As it is a memory backend, when the backend
    //  is re-enabled, a new backend object is in fact created and old reference
    //  to memory backend must be invalidated. So to prevent this problem, we
    //  retrieve the memory backend reference each time before cleaning it.
    MemoryBackend memoryBackend =
      (MemoryBackend)DirectoryServer.getBackend(TEST_BACKEND_ID2);
    if (memoryBackend == null)
    {
      memoryBackend = new MemoryBackend();
      memoryBackend.setBackendID(TEST_BACKEND_ID2);
      memoryBackend.setBaseDNs(new DN[] {baseDN});
      memoryBackend.initializeBackend();
      DirectoryServer.registerBackend(memoryBackend);
    }
    memoryBackend.clearMemoryBackend();
    if (createBaseEntry)
    {
      Entry e = createEntry(baseDN);
      memoryBackend.addEntry(e, null);
    }
    return memoryBackend;
  }
  private static void removeTestBackend2(Backend backend)
  {
    MemoryBackend memoryBackend = (MemoryBackend)backend;
    memoryBackend.finalizeBackend();
    DirectoryServer.deregisterBackend(memoryBackend);
  }
}
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/protocol/SynchronizationMsgTest.java
@@ -22,20 +22,26 @@
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2008 Sun Microsystems, Inc.
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 */
package org.opends.server.replication.protocol;
import static org.opends.server.replication.protocol.OperationContext.*;
import static org.testng.Assert.*;
import static org.opends.server.TestCaseUtils.TEST_ROOT_DN_STRING;
import static org.opends.server.replication.protocol.OperationContext.SYNCHROCONTEXT;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.zip.DataFormatException;
import org.opends.messages.Message;
import org.opends.server.core.AddOperation;
import org.opends.server.core.AddOperationBasis;
import org.opends.server.core.DeleteOperationBasis;
@@ -44,8 +50,13 @@
import org.opends.server.core.ModifyOperationBasis;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.replication.ReplicationTestCase;
import org.opends.server.replication.common.AssuredMode;
import org.opends.server.replication.common.ChangeNumber;
import org.opends.server.replication.common.DSInfo;
import org.opends.server.replication.common.MultiDomainServerState;
import org.opends.server.replication.common.RSInfo;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.common.ServerStatus;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeBuilder;
import org.opends.server.types.AttributeType;
@@ -60,15 +71,9 @@
import org.opends.server.workflowelement.localbackend.LocalBackendAddOperation;
import org.opends.server.workflowelement.localbackend.LocalBackendDeleteOperation;
import org.opends.server.workflowelement.localbackend.LocalBackendModifyDNOperation;
import org.opends.messages.Message;
import org.opends.server.replication.common.AssuredMode;
import org.opends.server.replication.common.DSInfo;
import org.opends.server.replication.common.RSInfo;
import org.opends.server.replication.common.ServerStatus;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.opends.server.TestCaseUtils.*;
/**
 * Test the constructors, encoders and decoders of the replication protocol
@@ -563,18 +568,18 @@
         throws Exception
  {
    AckMsg msg1, msg2 ;
    // Consctructor test (with ChangeNumber)
    // Chech that retrieved CN is OK
    msg1 = new  AckMsg(cn);
    assertEquals(msg1.getChangeNumber().compareTo(cn), 0);
    // Check default values for error info
    assertFalse(msg1.hasTimeout());
    assertFalse(msg1.hasWrongStatus());
    assertFalse(msg1.hasReplayError());
    assertTrue(msg1.getFailedServers().size() == 0);
    // Check constructor with error info
    msg1 = new  AckMsg(cn, hasTimeout, hasWrongStatus, hasReplayError, failedServers);
    assertEquals(msg1.getChangeNumber().compareTo(cn), 0);
@@ -582,7 +587,7 @@
    assertTrue(msg1.hasWrongStatus() == hasWrongStatus);
    assertTrue(msg1.hasReplayError() == hasReplayError);
    assertEquals(msg1.getFailedServers(), failedServers);
    // Consctructor test (with byte[])
    msg2 = new  AckMsg(msg1.getBytes());
    assertEquals(msg2.getChangeNumber().compareTo(cn), 0);
@@ -590,7 +595,7 @@
    assertTrue(msg1.hasWrongStatus() == msg2.hasWrongStatus());
    assertTrue(msg1.hasReplayError() == msg2.hasReplayError());
    assertEquals(msg1.getFailedServers(), msg2.getFailedServers());
    // Check invalid bytes for constructor
    byte[] b = msg1.getBytes();
    b[0] = ReplicationMsg.MSG_TYPE_ADD;
@@ -604,11 +609,53 @@
    {
      assertTrue(true);
    }
    // Check that retrieved CN is OK
    msg2 = (AckMsg) ReplicationMsg.generateMsg(msg1.getBytes());
  }
  @Test()
  public void eclUpdateMsg()
         throws Exception
  {
    // create a msg to put in the eclupdatemsg
    InternalClientConnection connection =
      InternalClientConnection.getRootConnection();
    DeleteOperationBasis opBasis =
      new DeleteOperationBasis(connection, 1, 1,null, DN.decode("cn=t1"));
    LocalBackendDeleteOperation op = new LocalBackendDeleteOperation(opBasis);
    ChangeNumber cn = new ChangeNumber(TimeThread.getTime(),
        (short) 123, (short) 45);
    op.setAttachment(SYNCHROCONTEXT, new DeleteContext(cn, "uniqueid"));
    DeleteMsg delmsg = new DeleteMsg(op);
    String serviceId = "serviceid";
    // create a cookie
    MultiDomainServerState cookie =
      new MultiDomainServerState(
          "o=test:000001210b6f21e904b100000001 000001210b6f21e904b200000001;" +
          "o=test2:000001210b6f21e904b100000002 000001210b6f21e904b200000002;");
    // Constructor test
    ECLUpdateMsg msg1 = new ECLUpdateMsg(delmsg, cookie, serviceId);
    assertTrue(msg1.getCookie().equalsTo(cookie));
    assertTrue(msg1.getServiceId().equalsIgnoreCase(serviceId));
    DeleteMsg delmsg2 = (DeleteMsg)msg1.getUpdateMsg();
    assertTrue(delmsg.compareTo(delmsg2)==0);
    // Consctructor test (with byte[])
    ECLUpdateMsg msg2 = new ECLUpdateMsg(msg1.getBytes());
    assertTrue(msg2.getCookie().equalsTo(msg2.getCookie()));
    assertTrue(msg2.getCookie().equalsTo(cookie));
    assertTrue(msg2.getServiceId().equalsIgnoreCase(msg1.getServiceId()));
    assertTrue(msg2.getServiceId().equalsIgnoreCase(serviceId));
    DeleteMsg delmsg1 = (DeleteMsg)msg1.getUpdateMsg();
    delmsg2 = (DeleteMsg)msg2.getUpdateMsg();
    assertTrue(delmsg2.compareTo(delmsg)==0);
    assertTrue(delmsg2.compareTo(delmsg1)==0);
  }
  @DataProvider(name="createServerStartData")
  public Object [][] createServerStartData() throws Exception
  {
@@ -1120,4 +1167,85 @@
    UpdateMsg newMsg = new UpdateMsg(msg.getBytes());
    assertEquals(test.getBytes(), newMsg.getPayload());
  }
  /**
   * Test that ServerStartMsg encoding and decoding works
   * by checking that : msg == new ServerStartMsg(msg.getBytes()).
   */
  @Test(dataProvider="createServerStartData")
  public void startECLMsgTest(short serverId, String baseDN, int window,
         ServerState state, long genId, boolean sslEncryption, byte groupId) throws Exception
  {
    ServerStartECLMsg msg = new ServerStartECLMsg(baseDN,
        window, window, window, window, window, window, state,
        ProtocolVersion.getCurrentVersion(), genId, sslEncryption, groupId);
    ServerStartECLMsg newMsg = new ServerStartECLMsg(msg.getBytes());
    assertEquals(msg.getMaxReceiveDelay(), newMsg.getMaxReceiveDelay());
    assertEquals(msg.getMaxReceiveQueue(), newMsg.getMaxReceiveQueue());
    assertEquals(msg.getMaxSendDelay(), newMsg.getMaxSendDelay());
    assertEquals(msg.getMaxSendQueue(), newMsg.getMaxSendQueue());
    assertEquals(msg.getWindowSize(), newMsg.getWindowSize());
    assertEquals(msg.getHeartbeatInterval(), newMsg.getHeartbeatInterval());
    assertEquals(msg.getSSLEncryption(), newMsg.getSSLEncryption());
    assertEquals(msg.getServerState().getMaxChangeNumber((short)1),
        newMsg.getServerState().getMaxChangeNumber((short)1));
    assertEquals(msg.getVersion(), newMsg.getVersion());
    assertEquals(msg.getGenerationId(), newMsg.getGenerationId());
    assertTrue(msg.getGroupId() == newMsg.getGroupId());
  }
  /**
   * Test StartSessionMsg encoding and decoding.
   */
  @Test()
  public void startECLSessionMsgTest()
    throws Exception
  {
    // data
    ChangeNumber changeNumber = new ChangeNumber(TimeThread.getTime(),
        (short) 123, (short) 45);
    String generalizedState = new String("fakegenstate");
    ServerState state = new ServerState();
    assertTrue(state.update(new ChangeNumber((long)75, 5,(short)263)));
    short mode = 3;
    int firstDraftChangeNumber = 13;
    int lastDraftChangeNumber  = 14;
    String myopid = new String("fakeopid");
    // create original
    StartECLSessionMsg msg = new StartECLSessionMsg();
    msg.setChangeNumber(changeNumber);
    msg.setCrossDomainServerState(generalizedState);
    msg.setPersistent(StartECLSessionMsg.PERSISTENT);
    msg.setFirstDraftChangeNumber(firstDraftChangeNumber);
    msg.setLastDraftChangeNumber(lastDraftChangeNumber);
    msg.setECLRequestType(mode);
    msg.setOperationId(myopid);
    ArrayList<String> dns = new ArrayList<String>();
    String dn1 = "cn=admin data";
    String dn2 = "cn=config";
    dns.add(dn1);
    dns.add(dn2);
    msg.setExcludedDNs(dns);
    // create copy
    StartECLSessionMsg newMsg = new StartECLSessionMsg(msg.getBytes());
    // test equality between the two copies
    assertEquals(msg.getChangeNumber(), newMsg.getChangeNumber());
    assertTrue(msg.isPersistent() == newMsg.isPersistent());
    assertTrue(msg.getFirstDraftChangeNumber() == newMsg.getFirstDraftChangeNumber());
    assertEquals(msg.getECLRequestType(), newMsg.getECLRequestType());
    assertEquals(msg.getLastDraftChangeNumber(), newMsg.getLastDraftChangeNumber());
    assertTrue(
        msg.getCrossDomainServerState().equalsIgnoreCase(newMsg.getCrossDomainServerState()));
    assertTrue(
        msg.getOperationId().equalsIgnoreCase(newMsg.getOperationId()));
    ArrayList<String> dns2 = newMsg.getExcludedServiceIDs();
    assertTrue(dns2.size()==2);
    boolean dn1found=false,dn2found=false;
    for (String dn : dns2)
    {
      if (!dn1found) dn1found=(dn.compareTo(dn1)==0);
      if (!dn2found) dn2found=(dn.compareTo(dn2)==0);
    }
    assertTrue(dn1found);
    assertTrue(dn2found);
  }
}
opends/tests/unit-tests-testng/src/server/org/opends/server/replication/server/ReplicationServerTest.java
@@ -90,6 +90,8 @@
import org.opends.server.util.LDIFWriter;
import org.opends.server.util.TimeThread;
import static org.opends.server.util.ServerConstants.OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import org.opends.server.workflowelement.localbackend.LocalBackendModifyDNOperation;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
@@ -175,7 +177,7 @@
  private void debugInfo(String s)
  {
    ErrorLogger.logError(Message.raw(Category.SYNC, Severity.NOTICE, "** TEST ** " + s));
    //ErrorLogger.logError(Message.raw(Category.SYNC, Severity.NOTICE, "** TEST ** " + s));
    if (debugEnabled())
    {
      TRACER.debugInfo("** TEST ** " + s);
@@ -689,7 +691,7 @@
          new ChangeNumberGenerator(serverId , (long) 0);
        broker[i] =
          openReplicationSession( DN.decode(TEST_ROOT_DN_STRING), serverId,
            100, replicationServerPort, 100000, 1000, 0, false);
              100, replicationServerPort, 3000, 1000, 0, true);
        assertTrue(broker[i].isConnected());
@@ -706,10 +708,11 @@
      {
        reader[i].start();
      }
      debugInfo("Ending multipleWriterMultipleReader");
      debugInfo("multipleWriterMultipleReader produces and readers started");
    }
    finally
    {
      debugInfo("multipleWriterMultipleReader wait producers");
      for (int i = 0; i< THREADS; i++)
      {
        if (producer[i] != null)
@@ -717,6 +720,7 @@
        // kill the thread in case it is not yet stopped.
        producer[i].interrupt();
      }
      debugInfo("multipleWriterMultipleReader producers done, wait readers");
      for (int i = 0; i< THREADS; i++)
      {
        if (reader[i] != null)
@@ -724,15 +728,18 @@
        // kill the thread in case it is not yet stopped.
        reader[i].interrupt();
      }
      debugInfo("multipleWriterMultipleReader reader's done");
      for (int i = 0; i< THREADS; i++)
      {
        if (broker[i] != null)
          broker[i].stop();
      }
      debugInfo("multipleWriterMultipleReader brokers stopped");
      replicationServer.clearDb();
      TestCaseUtils.initializeTestBackend(true);
    }
    debugInfo("Ending multipleWriterMultipleReader");
  }
@@ -1123,6 +1130,8 @@
   */
  private class BrokerReader extends Thread
  {
    int count;
    private ReplicationBroker broker;
    private int numMsgRcv = 0;
    private final int numMsgExpected;
@@ -1142,8 +1151,11 @@
    @Override
    public void run()
    {
      debugInfo("BrokerReader " + broker.getServerId() + " starts");
      // loop receiving messages until either we get a timeout
      // because there is nothing left or an error condition happens.
      count = 0;
      try
      {
        while (true)
@@ -1165,7 +1177,8 @@
      } catch (Exception e)
      {
        assertTrue(false,
            "a BrokerReader received an Exception" + e.getMessage());
            "a BrokerReader received an Exception" + e.getMessage()
            + stackTraceToSingleLineString(e));
      }
    }
  }
@@ -1194,6 +1207,8 @@
    @Override
    public void run()
    {
      debugInfo("BrokerWriter " + broker.getServerId() + " starts");
      int ccount = count;
      /*
       * Simple loop creating changes and sending them
       * to the replicationServer.
@@ -1206,7 +1221,11 @@
          new DeleteMsg("o=example," + TEST_ROOT_DN_STRING, gen.newChangeNumber(),
              "uid");
        broker.publish(msg);
        if ((count % 10) == 0)
        debugInfo("BrokerWriter " + broker.getServerId() + "  sent="+count);
      }
      debugInfo("BrokerWriter " + broker.getServerId() + " ends sent="+ccount);
    }
  }