/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 *
 */
package org.apache.directory.server.ldap.replication;

import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.cursor.EntryCursor;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.message.DeleteRequest;
import org.apache.directory.api.ldap.model.message.DeleteRequestImpl;
import org.apache.directory.api.ldap.model.message.DeleteResponse;
import org.apache.directory.api.ldap.model.message.ModifyDnRequest;
import org.apache.directory.api.ldap.model.message.ModifyDnRequestImpl;
import org.apache.directory.api.ldap.model.message.ModifyDnResponse;
import org.apache.directory.api.ldap.model.message.ModifyRequest;
import org.apache.directory.api.ldap.model.message.ModifyRequestImpl;
import org.apache.directory.api.ldap.model.message.ModifyResponse;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.api.ldap.model.url.LdapUrl;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.directory.server.annotations.CreateConsumer;
import org.apache.directory.server.annotations.CreateLdapServer;
import org.apache.directory.server.annotations.CreateTransport;
import org.apache.directory.server.constants.ServerDNConstants;
import org.apache.directory.server.core.annotations.CreateDS;
import org.apache.directory.server.core.annotations.CreateIndex;
import org.apache.directory.server.core.annotations.CreatePartition;
import org.apache.directory.server.core.api.CoreSession;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.factory.DSAnnotationProcessor;
import org.apache.directory.server.core.factory.MavibotPartitionFactory;
import org.apache.directory.server.core.integ.FrameworkRunner;
import org.apache.directory.server.factory.ServerAnnotationProcessor;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.ldap.replication.consumer.ReplicationConsumer;
import org.apache.directory.server.ldap.replication.consumer.ReplicationConsumerImpl;
import org.apache.directory.server.ldap.replication.provider.SyncReplRequestHandler;

/**
 * Holds the configuration and instances of connections
 * 
 * WARN: this class is not yet ready to run as a junit test cause it requires 
 *       some manual setup of servers and test is invoked by main()
 *       The existing annotation based server instances are not used yet.
 *       
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class MmrTester
{
    // replicate a branch under partition instead of partition
    // this will enable to us to test the case where a entry was
    // moved out of replication area
    private static final String REPL_AREA_SUFFIX = "ou=replicationarea,dc=example,dc=com";

    private static final String RDN_PREFIX = "p";

    private Set<String> urls = new LinkedHashSet<String>();
    
    private List<LdapNetworkConnection> connections;
    
    private static final int TOTAL_ENTRY_COUNT = 200;
    
    private AtomicInteger count = new AtomicInteger(-1);
    
    private Random rand = new Random();

    private boolean verbose = false;
    
    private static LdapServer peer1Server;

    private static LdapServer peer2Server;

    public MmrTester( String... ldapUrls )
    {
        if( ( ldapUrls == null ) || ( ldapUrls.length < 2 ) )
        {
            throw new IllegalArgumentException( "Atleast two servers are required to run MMR tests" );
        }
        
        for( String u : ldapUrls )
        {
            urls.add( u.toLowerCase() );
        }
    }
    
    
    public void prepareConnections() throws Exception
    {
        connections = new ArrayList<LdapNetworkConnection>();
        
        for( String u : urls )
        {
            LdapUrl url = new LdapUrl( u );
            boolean useSsl = false;
            if( url.getScheme().equals( "ldaps" ) )
            {
                useSsl = true;
            }
            
            LdapNetworkConnection c = new LdapNetworkConnection( url.getHost(), url.getPort(), useSsl );
            c.setTimeOut( Long.MAX_VALUE );
            c.connect();
            c.bind( ServerDNConstants.ADMIN_SYSTEM_DN, "secret" );
            System.out.println( "connected to the server " + url );
            connections.add( c );
        }
    }
    
    
    public void addAndCompare()
    {
        List<Dn> injected = new ArrayList<Dn>();
        int batch = 20;
        
        if( batch > TOTAL_ENTRY_COUNT )
        {
            batch = TOTAL_ENTRY_COUNT;
        }
        else
        {
            batch = TOTAL_ENTRY_COUNT;
        }
        
        for( int i=0; i < batch; i++ )
        {
            int index = rand.nextInt( connections.size() );

            LdapNetworkConnection nc = connections.get( index );
            
            if( verbose )
            {
                System.out.println( "inserting into the server " + nc.getConfig().getLdapHost() + ":" + nc.getConfig().getLdapPort() );
            }
            
            Entry e = createEntry( count.incrementAndGet() );
            if ( inject( nc, e ) )
            {
                injected.add( e.getDn() );
            }
        }
        
        compareEntries( injected );
    }
    
    
    public List<Dn> modify() throws Exception
    {
        List<Dn> modified = new ArrayList<Dn>();
        int batch = 20;
        
        if( batch > TOTAL_ENTRY_COUNT )
        {
            batch = TOTAL_ENTRY_COUNT;
        }
        else
        {
            batch = (int) ( TOTAL_ENTRY_COUNT * 0.10 ); // modify 10% of total entries
        }

        for( int i=0; i < batch; i++ )
        {
            int connectionIndex = rand.nextInt( connections.size() );
            
            int entryIndex = rand.nextInt( batch );
            
            LdapNetworkConnection nc = connections.get( connectionIndex );
            
            String cn = RDN_PREFIX + entryIndex;
            Dn personDn = new Dn( "cn=" + cn + "," + REPL_AREA_SUFFIX );

            if ( verbose )
            {
                System.out.println( "modifying " + personDn + " on the server " + nc.getConfig().getLdapHost() + ":" + nc.getConfig().getLdapPort() );
            }
            
            ModifyRequest modReq = new ModifyRequestImpl();
            modReq.setName( personDn );
            modReq.replace( SchemaConstants.SN_AT, "sn_" + i );
            
            ModifyResponse resp = nc.modify( modReq );
            ResultCodeEnum rc = resp.getLdapResult().getResultCode();
            if( rc != ResultCodeEnum.SUCCESS )
            {
                System.out.println( "Error modifying " + personDn + " on the server " + nc.getConfig().getLdapHost() + ":" + nc.getConfig().getLdapPort() + " with result code " + rc );
            }

            modified.add( personDn );
        }
        
        return modified;
    }
    
    
    public void moveAndCompare( Dn superiorDn ) throws Exception
    {
        List<Dn> moved = new ArrayList<Dn>();
        int batch = 5;
        
        if( batch > TOTAL_ENTRY_COUNT )
        {
            batch = TOTAL_ENTRY_COUNT;
        }
        else
        {
            batch = (int) ( TOTAL_ENTRY_COUNT * 0.20 ); // move 20% of total entries
        }
        
        for( int i=0; i < batch; i++ )
        {
            int connectionIndex = rand.nextInt( connections.size() );
            
            LdapNetworkConnection nc = connections.get( connectionIndex );
            
            String cn = RDN_PREFIX + i;
            Dn personDn = new Dn( "cn=" + cn + "," + REPL_AREA_SUFFIX );
            
            if( verbose )
            {
                System.out.println( "moving " + personDn + " on the server " + nc.getConfig().getLdapHost() + ":" + nc.getConfig().getLdapPort() + ":" + nc.getConfig().getLdapPort() );
            }
            
            ModifyDnRequest modReq = new ModifyDnRequestImpl();
            modReq.setName( personDn );
            modReq.setNewRdn( personDn.getRdn() );
            modReq.setNewSuperior( superiorDn );
            
            ModifyDnResponse resp = nc.modifyDn( modReq );
            ResultCodeEnum rc = resp.getLdapResult().getResultCode();
            if( rc != ResultCodeEnum.SUCCESS )
            {
                System.out.println( "Error moving " + personDn + " on the server " + nc.getConfig().getLdapHost() + ":" + nc.getConfig().getLdapPort() + ":" + nc.getConfig().getLdapPort() + " with result code " + rc );
            }
            
            moved.add( superiorDn.add( personDn.getRdn() ) );
        }
        
        compareEntries( moved );
    }

    
    public List<Dn> renameAndCompare( Dn containerDn ) throws Exception
    {
        List<Dn> renamed = new ArrayList<Dn>();
        List<Dn> present = new ArrayList<Dn>();
        
        int connectionIndex = rand.nextInt( connections.size() );

        LdapNetworkConnection nc = connections.get( connectionIndex );
        EntryCursor cursor = nc.search( containerDn, "(cn=" + RDN_PREFIX + "*)", SearchScope.ONELEVEL, SchemaConstants.NO_ATTRIBUTE_ARRAY );
        
        while( cursor.next() )
        {
            present.add( cursor.get().getDn() );
        }
        
        cursor.close();
        
        for( int i=0; i < present.size(); i++ )
        {
            connectionIndex = rand.nextInt( connections.size() );
            nc = connections.get( connectionIndex );
            
            Dn personDn = present.get( i );
            
            ModifyDnRequest modReq = new ModifyDnRequestImpl();
            Rdn newRdn = new Rdn( "cn=p_rename" + i );
            modReq.setName( personDn );
            modReq.setNewRdn( newRdn );
            
            if( verbose )
            {
                System.out.println( "renaming " + personDn + " on the server " + nc.getConfig().getLdapHost() + ":" + nc.getConfig().getLdapPort() );    
            }
            
            ModifyDnResponse resp = nc.modifyDn( modReq );
            ResultCodeEnum rc = resp.getLdapResult().getResultCode();
            if( rc != ResultCodeEnum.SUCCESS )
            {
                System.out.println( "Error renaming " + personDn + " on the server " + nc.getConfig().getLdapHost() + ":" + nc.getConfig().getLdapPort() + " with result code " + rc );
            }
            
            renamed.add( personDn.getParent().add( newRdn ) );
        }
        
        compareEntries( renamed );
        
        return renamed;
    }

    
    public void deleteAndVerify( List<Dn> present ) throws Exception
    {
        List<Dn> deleted = new ArrayList<Dn>();
        
        int count = present.size();//( present.size() - 2 );
        for( int i=0; i < count; i++ )
        {
            int connectionIndex = rand.nextInt( connections.size() );
            
            LdapNetworkConnection nc = connections.get( connectionIndex );
            
            Dn personDn = present.get( i );
            
            DeleteRequest delReq = new DeleteRequestImpl();
            delReq.setName( personDn );
            
            if( verbose )
            {
                System.out.println( "deleting " + personDn + " on the server " + nc.getConfig().getLdapHost() + ":" + nc.getConfig().getLdapPort() );
            }
            
            DeleteResponse resp = nc.delete( delReq );
            ResultCodeEnum rc = resp.getLdapResult().getResultCode();

            if( rc != ResultCodeEnum.SUCCESS )
            {
                System.out.println( "Error deleting " + personDn + " on the server " + nc.getConfig().getLdapHost() + ":" + nc.getConfig().getLdapPort() + " with result code " + rc );
            }
            
            deleted.add( personDn );
        }
        
        verifyDeleted( deleted );
    }

    
    public void moveOutOfReplAreaAndCompare() throws Exception
    {
        Dn parentDn = new Dn( "ou=parent,ou=children,ou=grandchildren");
        
        Dn currentDn = new Dn( REPL_AREA_SUFFIX );
        
        LdapNetworkConnection nc = connections.get( 0 );
        
        for( Rdn rdn : parentDn.getRdns() )
        {
            currentDn = new Dn( rdn.getName() + "," + currentDn.getName() );
            Entry e = new DefaultEntry( currentDn.getName(),
            "objectclass: top",
            "objectclass: organizationalUnit",
            "ou: " + rdn.getAva().getValue().getString() );
            nc.add( e );
        }
        
        compareEntries( Collections.singletonList( currentDn ) );
        
        Dn ouDn = currentDn.getParent().getParent();
        
        if( verbose )
        {
            System.out.println( "moving " + ouDn + " on the server " + nc.getConfig().getLdapHost() + ":" + nc.getConfig().getLdapPort() + ":" + nc.getConfig().getLdapPort() );
        }
        
        ModifyDnRequest modReq = new ModifyDnRequestImpl();
        modReq.setName( ouDn );
        modReq.setNewRdn( ouDn.getRdn() );
        modReq.setNewSuperior( ouDn.getParent().getParent() );
        
        ModifyDnResponse resp = nc.modifyDn( modReq );
        ResultCodeEnum rc = resp.getLdapResult().getResultCode();
        if( rc != ResultCodeEnum.SUCCESS )
        {
            System.out.println( "Error moving " + ouDn + " on the server " + nc.getConfig().getLdapHost() + ":" + nc.getConfig().getLdapPort() + ":" + nc.getConfig().getLdapPort() + " with result code " + rc );
        }

        Thread.sleep( 2000 );
        verifyDeleted( Collections.singletonList( ouDn ) );
    }

    public void compareEntries( List<Dn> injected )
    {
        for( Dn dn : injected )
        {
            Entry baseEntry = null;
            Iterator<LdapNetworkConnection> itr = connections.iterator();
            LdapNetworkConnection c = itr.next();
            baseEntry = lookupWithWait( c, dn );
            
            while( itr.hasNext() )
            {
                c = itr.next();
                Entry replicaEntry = lookupWithWait( c, dn );
                boolean equal = baseEntry.equals( replicaEntry );
                if( !equal )
                {
                    System.out.println( "base entry: " + baseEntry );
                    System.out.println( "replica entry: " + replicaEntry );
                }
                
                assertTrue( equal );
            }
        }
    }
    

    public void verifyDeleted( List<Dn> injected )
    {
        for( Dn dn : injected )
        {
            Iterator<LdapNetworkConnection> itr = connections.iterator();
            outer: while( itr.hasNext() )
            {
                LdapNetworkConnection c = itr.next();
                try
                {
                    for( int i = 1; i <= 10; i++ )
                    {
                        if( !c.exists( dn ) )
                        {
                            System.out.println( dn + " doesn't exist in the server " + c.getConfig().getLdapHost() + ":" + c.getConfig().getLdapPort() );
                            continue outer;
                        }
                        
                        Thread.sleep( 1000 * i );
                    }
                    
                    throw new RuntimeException( "deleted Entry " + dn + " found on server " + c.getConfig().getLdapHost() + ":" + c.getConfig().getLdapPort() );
                }
                catch( Exception e )
                {
                    throw new RuntimeException( e );
                }
            }
        }
    }
    
    
    public Entry lookupWithWait( LdapNetworkConnection c, Dn dn )
    {
        try
        {
            for( int i = 1; i <= 10; )
            {
                if( !c.exists( dn ) )
                {
                    Thread.sleep( 1000 * i );
                    continue;
                }
                
                return c.lookup( dn, SchemaConstants.ALL_USER_ATTRIBUTES, SchemaConstants.ENTRY_UUID_AT );
            }
        }
        catch( Exception e )
        {
            throw new RuntimeException( e );
        }
        
        throw new RuntimeException( "Entry " + dn + " not found on server " + c.getConfig().getLdapHost() + ":" + c.getConfig().getLdapPort() );
    }
    
    
    public boolean inject( LdapNetworkConnection nc, Entry e )
    {
        try
        {
            nc.add( e );
            return true;
        }
        catch( Exception ex )
        {
            count.decrementAndGet();
            ex.printStackTrace();
            return false;
        }
    }

    
    public boolean isCountReached()
    {
        return ( count.get() >= TOTAL_ENTRY_COUNT );
    }
    
    
    public void injectAndWaitTillReplicates( Entry ctxEntry ) throws Exception
    {
        LdapNetworkConnection c = connections.get( 0 );
        
        if( !c.exists( ctxEntry.getDn() ) )
        {
            c.add( ctxEntry );
        }
        
        compareEntries( Collections.singletonList( ctxEntry.getDn() ) );
    }
    
    
    private Entry createEntry( int num )
    {
        try
        {
            String cn = RDN_PREFIX + num;
            Dn personDn = new Dn( "cn=" + cn + "," + REPL_AREA_SUFFIX );
            Entry person = new DefaultEntry(
                personDn.toString(),
                "ObjectClass: top",
                "ObjectClass: person",
                "cn: " + cn,
                "sn: sn_" + cn,
                "userPassword: 12345" );
            return person;
        }
        catch( Exception e )
        {
            throw new RuntimeException( e );
        }
    }
    
    
    public void closeConnections()
    {
        for( LdapNetworkConnection nc : connections )
        {
            try
            {
                nc.close();
            }
            catch( Exception e )
            {
                //ignore
                e.printStackTrace();
            }
        }
    }
    
    public static void main( String[] args )
    {
        //System.setProperty( "apacheds.partition.factory", MavibotPartitionFactory.class.getName() );
        
        MmrTester cc = new MmrTester( "ldap://localhost:16001", "ldap://localhost:16000" );
        
        if( TOTAL_ENTRY_COUNT < 150 )
        {
            cc.verbose = true;
        }
        
        try
        {
            Thread t1 = new Thread( new Runnable()
            {
                
                @Override
                public void run()
                {
                    try
                    {
                        startPeer1();
                    }
                    catch(Exception e )
                    {
                        e.printStackTrace();
                    }
                }
            } );

            t1.setDaemon( true );
            
            Thread t2 = new Thread( new Runnable()
            {
                
                @Override
                public void run()
                {
                    try
                    {
                        startPeer2();
                    }
                    catch(Exception e )
                    {
                        e.printStackTrace();
                    }
                }
            } );

            t2.setDaemon( true );
            
            t1.start();
            t2.start();
            
            t1.join();
            t2.join();
            
            cc.prepareConnections();

            Entry ctxEntry = new DefaultEntry( "dc=example,dc=com",
                "objectClass: domain",
                "objectClass: top",
                "dc: example" );

            for( LdapConnection lc : cc.connections )
            {
                lc.add( ctxEntry );
            }

            Entry replAreaEntry = new DefaultEntry( REPL_AREA_SUFFIX,
                "objectClass: organizationalunit",
                "ou: replicationarea" );

            cc.injectAndWaitTillReplicates( replAreaEntry );
            
            cc.moveOutOfReplAreaAndCompare();
            
            cc.addAndCompare();
            
            List<Dn> modified = cc.modify();
            Thread.sleep( 15000 );
            
            cc.compareEntries( modified );
            
            Entry groupEntry = new DefaultEntry( "ou=groups," + REPL_AREA_SUFFIX,
                "objectClass: organizationalUnit",
                "objectClass: top",
                "ou: groups" );
            
            cc.injectAndWaitTillReplicates( groupEntry );
            
            cc.moveAndCompare( groupEntry.getDn() );
            
            List<Dn> renamed = cc.renameAndCompare( groupEntry.getDn() );
            
            cc.deleteAndVerify( renamed );
        }
        catch( Exception e )
        {
            e.printStackTrace();
        }
        finally
        {
            cc.closeConnections();
            shutdown();
        }
    }

    @CreateDS(
        allowAnonAccess = true,
        enableChangeLog = false,
        name = "peer1",
        partitions =
            {
                @CreatePartition(
                    name = "example",
                    suffix = "dc=example,dc=com",
                    indexes =
                        {
                            @CreateIndex(attribute = "objectClass"),
                            @CreateIndex(attribute = "dc"),
                            @CreateIndex(attribute = "ou")
                    })
        })
    @CreateLdapServer(transports =
        { @CreateTransport(port = 16000, protocol = "LDAP") })
    @CreateConsumer
    (
        remoteHost = "localhost",
        remotePort = 16001,
        replUserDn = "uid=admin,ou=system",
        replUserPassword = "secret",
        useTls = false,
        baseDn = REPL_AREA_SUFFIX,
        replicaId = 1,
        refreshNPersist = true
    )
    public static void startPeer1() throws Exception
    {
        DirectoryService provDirService = DSAnnotationProcessor.getDirectoryService();

        peer1Server = ServerAnnotationProcessor.getLdapServer( provDirService );
        peer1Server.setReplicationReqHandler( new SyncReplRequestHandler() );
        peer1Server.startReplicationProducer();
        
        final ReplicationConsumerImpl consumer = ( ReplicationConsumerImpl ) ServerAnnotationProcessor.createConsumer();
        List<ReplicationConsumer> replConsumers = new ArrayList<ReplicationConsumer>();
        replConsumers.add( consumer );
        
        peer1Server.setReplConsumers( replConsumers );
        peer1Server.startReplicationConsumers();

        Runnable r = new Runnable()
        {
            public void run()
            {
                try
                {
                    DirectoryService ds = peer1Server.getDirectoryService();

                    System.out.println(ds.getInstanceLayout().getInstanceDirectory());
                    Dn configDn = new Dn( ds.getSchemaManager(), "ads-replConsumerId=localhost,ou=system" );
                    consumer.getConfig().setConfigEntryDn( configDn );

                    Entry provConfigEntry = new DefaultEntry( ds.getSchemaManager(), configDn,
                        "objectClass: ads-replConsumer",
                        "ads-replConsumerId: localhost",
                        "ads-searchBaseDN", consumer.getConfig().getBaseDn(),
                        "ads-replProvHostName", consumer.getConfig().getRemoteHost(),
                        "ads-replProvPort", String.valueOf( consumer.getConfig().getRemotePort() ),
                        "ads-replRefreshInterval", String.valueOf( consumer.getConfig().getRefreshInterval() ),
                        "ads-replRefreshNPersist", String.valueOf( consumer.getConfig().isRefreshNPersist() ),
                        "ads-replSearchScope", consumer.getConfig().getSearchScope().getLdapUrlValue(),
                        "ads-replSearchFilter", consumer.getConfig().getFilter(),
                        "ads-replSearchSizeLimit", String.valueOf( consumer.getConfig().getSearchSizeLimit() ),
                        "ads-replSearchTimeOut", String.valueOf( consumer.getConfig().getSearchTimeout() ),
                        "ads-replUserDn", consumer.getConfig().getReplUserDn(),
                        "ads-replUserPassword", consumer.getConfig().getReplUserPassword() );

                    provConfigEntry.put( "ads-replAliasDerefMode", consumer.getConfig().getAliasDerefMode()
                        .getJndiValue() );
                    provConfigEntry.put( "ads-replAttributes", consumer.getConfig().getAttributes() );

                    CoreSession consumerSession = peer1Server.getDirectoryService().getAdminSession();
                    consumerSession.add( provConfigEntry );
                }
                catch ( Exception e )
                {
                    throw new RuntimeException( e );
                }
            }
        };

        Thread t = new Thread( r );
        t.setDaemon( true );
        t.start();
        t.join();
    }

    @CreateDS(
        allowAnonAccess = true,
        enableChangeLog = false,
        name = "peer2",
        partitions =
            {
                @CreatePartition(
                    name = "example",
                    suffix = "dc=example,dc=com",
                    indexes =
                        {
                            @CreateIndex(attribute = "objectClass"),
                            @CreateIndex(attribute = "dc"),
                            @CreateIndex(attribute = "ou")
                    })
        })
    @CreateLdapServer(transports =
        { @CreateTransport(port = 16001, protocol = "LDAP") })
    @CreateConsumer
    (
        remoteHost = "localhost",
        remotePort = 16000,
        replUserDn = "uid=admin,ou=system",
        replUserPassword = "secret",
        useTls = false,
        baseDn = REPL_AREA_SUFFIX,
        refreshNPersist = true,
        replicaId = 1
    )
    public static void startPeer2() throws Exception
    {
        DirectoryService provDirService = DSAnnotationProcessor.getDirectoryService();

        peer2Server = ServerAnnotationProcessor.getLdapServer( provDirService );
        peer2Server.setReplicationReqHandler( new SyncReplRequestHandler() );
        peer2Server.startReplicationProducer();
        
        final ReplicationConsumerImpl consumer = ( ReplicationConsumerImpl ) ServerAnnotationProcessor.createConsumer();
        List<ReplicationConsumer> replConsumers = new ArrayList<ReplicationConsumer>();
        replConsumers.add( consumer );
        
        peer2Server.setReplConsumers( replConsumers );
        peer2Server.startReplicationConsumers();

        Runnable r = new Runnable()
        {
            public void run()
            {
                try
                {
                    DirectoryService ds = peer2Server.getDirectoryService();

                    Dn configDn = new Dn( ds.getSchemaManager(), "ads-replConsumerId=localhost,ou=system" );
                    consumer.getConfig().setConfigEntryDn( configDn );

                    Entry provConfigEntry = new DefaultEntry( ds.getSchemaManager(), configDn,
                        "objectClass: ads-replConsumer",
                        "ads-replConsumerId: localhost",
                        "ads-searchBaseDN", consumer.getConfig().getBaseDn(),
                        "ads-replProvHostName", consumer.getConfig().getRemoteHost(),
                        "ads-replProvPort", String.valueOf( consumer.getConfig().getRemotePort() ),
                        "ads-replRefreshInterval", String.valueOf( consumer.getConfig().getRefreshInterval() ),
                        "ads-replRefreshNPersist", String.valueOf( consumer.getConfig().isRefreshNPersist() ),
                        "ads-replSearchScope", consumer.getConfig().getSearchScope().getLdapUrlValue(),
                        "ads-replSearchFilter", consumer.getConfig().getFilter(),
                        "ads-replSearchSizeLimit", String.valueOf( consumer.getConfig().getSearchSizeLimit() ),
                        "ads-replSearchTimeOut", String.valueOf( consumer.getConfig().getSearchTimeout() ),
                        "ads-replUserDn", consumer.getConfig().getReplUserDn(),
                        "ads-replUserPassword", consumer.getConfig().getReplUserPassword() );

                    provConfigEntry.put( "ads-replAliasDerefMode", consumer.getConfig().getAliasDerefMode()
                        .getJndiValue() );
                    provConfigEntry.put( "ads-replAttributes", consumer.getConfig().getAttributes() );

                    CoreSession consumerSession = peer2Server.getDirectoryService().getAdminSession();
                    consumerSession.add( provConfigEntry );
                }
                catch ( Exception e )
                {
                    throw new RuntimeException( e );
                }
            }
        };

        Thread t = new Thread( r );
        t.setDaemon( true );
        t.start();
        t.join();
    }
    
    public static void shutdown()
    {
        peer1Server.stop();
        peer2Server.stop();
    }
}
