From f93174a06f157fb10b49bb0bf179617943d54710 Mon Sep 17 00:00:00 2001
From: Gaetan Boismal <gaetan.boismal@forgerock.com>
Date: Wed, 23 Jul 2014 14:40:02 +0000
Subject: [PATCH] OPENDJ-1074 (CR-4030) Implement combined add/del rate tool Adding an add/del rate tool named addrate. -opendj-core     ** EntryGenerator.java:     * Changing the subtemplate parsing behavior, add the "generated branches" options     ** TemplateFile.java     * Adding the "generated branches" options     ** addrate.template     * New template used to generate entries

---
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/MakeLDIF.java                 |   45 +
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/MakeLDIFTestCase.java         |    2 
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsITCase.java              |    5 
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateFile.java                           |   86 ++-
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java                  |    6 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java               |    6 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java                 |   10 
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java |   90 +++++
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java        |   86 +---
 opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java                  |  429 +++++++++++++++++++++++
 opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryGenerator.java                         |   22 +
 opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/addrate.template                       |   32 +
 opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java                             |    2 
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/addrate                                               |   33 +
 opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AddRateITCase.java            |  115 ++++++
 opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/addrate                                               |   36 ++
 opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools.properties         |   45 ++
 17 files changed, 924 insertions(+), 126 deletions(-)

diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryGenerator.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryGenerator.java
index e5d636e..898712e 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryGenerator.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/EntryGenerator.java
@@ -111,6 +111,12 @@
      */
     private InputStream templateStream;
 
+    /** Indicates whether branch entries should be generated.
+     *
+     *  Default is {@code true}
+     */
+    private boolean generateBranches = true;
+
     /** Dictionary of constants to use in the template file. */
     private Map<String, String> constants = new HashMap<String, String>();
 
@@ -222,6 +228,20 @@
     }
 
     /**
+     * Sets the flag which indicates whether branch entries should be generated.
+     *
+     * The default is {@code true}.
+     *
+     * @param generateBranches
+     *              Indicates whether or not the branches DN entries has to be generated.
+     * @return A reference to this {@code EntryGenerator}.
+     */
+    public EntryGenerator setGenerateBranches(boolean generateBranches) {
+        this.generateBranches = generateBranches;
+        return this;
+    }
+
+    /**
      * Checks if there are some warning(s) after parsing the template file.
      * <p>
      * Warnings are available only after the first call to {@code hasNext()} or
@@ -286,7 +306,7 @@
         if (schema == null) {
             schema = Schema.getDefaultSchema();
         }
-        templateFile = new TemplateFile(schema, constants, resourcePath, new Random(randomSeed));
+        templateFile = new TemplateFile(schema, constants, resourcePath, new Random(randomSeed), generateBranches);
         try {
             if (templatePath != null) {
                 templateFile.parse(templatePath, warnings);
diff --git a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateFile.java b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateFile.java
index 70b743c..cc20e2d 100644
--- a/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateFile.java
+++ b/opendj-sdk/opendj-core/src/main/java/org/forgerock/opendj/ldif/TemplateFile.java
@@ -104,6 +104,9 @@
     /** The name of the file holding the list of last names. */
     private static final String LAST_NAME_FILE = "last.names";
 
+    /** Default value for infinite number of entries. */
+    private static final int INFINITE_ENTRIES = -1;
+
     /**
      * A map of the contents of various text files used during the parsing
      * process, mapped from absolute path to the array of lines in the file.
@@ -149,6 +152,9 @@
     /** The next last name that should be used. */
     private String lastName;
 
+    /** Indicates whether branch entries should be generated. */
+    private boolean generateBranches;
+
     /**
      * The resource path to use for filesystem elements that cannot be found
      * anywhere else.
@@ -180,7 +186,7 @@
      *             if a problem occurs when initializing
      */
     TemplateFile(Schema schema, Map<String, String> constants, String resourcePath) throws IOException {
-        this(schema, constants, resourcePath, new Random());
+        this(schema, constants, resourcePath, new Random(), true);
     }
 
     /**
@@ -197,12 +203,16 @@
      *            {@code null}.
      * @param random
      *            The random number generator for this template file.
+     * @param generateBranches
+     *            Indicates whether branch entries should be generated.
      * @throws IOException
      *             if a problem occurs when initializing
      */
-    TemplateFile(Schema schema, Map<String, String> constants, String resourcePath, Random random)
+    TemplateFile(Schema schema, Map<String, String> constants, String resourcePath,
+                    Random random, boolean generateBranches)
             throws IOException {
         Reject.ifNull(schema, random);
+        this.generateBranches = generateBranches;
         this.schema = schema;
         this.constants = constants != null ? constants : new HashMap<String, String>();
         this.resourcePath = resourcePath;
@@ -466,7 +476,7 @@
         // Finalize the branch and template definitions
         // and then update the template file variables.
         for (Branch b : templateData.branches.values()) {
-            b.completeBranchInitialization(templateData.templates);
+            b.completeBranchInitialization(templateData.templates, generateBranches);
         }
 
         for (Template t : templateData.templates.values()) {
@@ -835,30 +845,31 @@
     private Pair<String, Integer> parseSubordinateTemplate(final int lineNumber, final String line,
             final Element element, final String elementName, final List<LocalizableMessage> warnings)
             throws DecodeException {
-        // It's a subordinate template, so we'll want to parse
-        // the template name and the number of entries.
+        // It's a subordinate template, so we'll want to parse the template name
+        // and the number of entries if it is provided.
         final int colonPos = line.indexOf(':', SUBORDINATE_TEMPLATE_LABEL.length());
+        final String templateName;
+        int numEntries = INFINITE_ENTRIES;
+
         if (colonPos <= SUBORDINATE_TEMPLATE_LABEL.length()) {
-            throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_SUBORDINATE_TEMPLATE_NO_COLON.get(
-                    lineNumber, element.getLabel(), elementName));
-        }
+            //No number of entries provided, generator will provides an infinite number of entries
+            templateName = line.substring(SUBORDINATE_TEMPLATE_LABEL.length(), line.length()).trim();
+        } else {
+            templateName = line.substring(SUBORDINATE_TEMPLATE_LABEL.length(), colonPos).trim();
 
-        final String templateName = line.substring(SUBORDINATE_TEMPLATE_LABEL.length(), colonPos).trim();
-
-        try {
-            final int numEntries = Integer.parseInt(line.substring(colonPos + 1).trim());
-            if (numEntries < 0) {
-                throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_SUBORDINATE_INVALID_NUM_ENTRIES.get(
-                        lineNumber, element.getLabel(), elementName, numEntries, templateName));
-            } else if (numEntries == 0) {
-                warnings.add(WARN_ENTRY_GENERATOR_SUBORDINATE_ZERO_ENTRIES.get(
-                        lineNumber, element.getLabel(), elementName, templateName));
+            try {
+                numEntries = Integer.parseInt(line.substring(colonPos + 1).trim());
+                if (numEntries == 0) {
+                    warnings.add(WARN_ENTRY_GENERATOR_SUBORDINATE_ZERO_ENTRIES.get(
+                            lineNumber, element.getLabel(), elementName, templateName));
+                }
+            } catch (NumberFormatException nfe) {
+                throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_SUBORDINATE_CANT_PARSE_NUMENTRIES.get(
+                        templateName, lineNumber, element.getLabel(), elementName));
             }
-            return Pair.of(templateName, numEntries);
-        } catch (NumberFormatException nfe) {
-            throw DecodeException.fatalError(ERR_ENTRY_GENERATOR_SUBORDINATE_CANT_PARSE_NUMENTRIES.get(
-                    templateName, lineNumber, element.getLabel(), elementName));
         }
+
+        return Pair.of(templateName, numEntries);
     }
 
     private static final int PARSING_STATIC_TEXT = 0;
@@ -1417,14 +1428,9 @@
          * initialization is completed. In particular, it should make sure that
          * all referenced subordinate templates actually exist in the template
          * file.
-         *
-         * @param templates
-         *            The set of templates defined in the template file.
-         * @throws DecodeException
-         *             If any of the subordinate templates are not defined in
-         *             the template file.
          */
-        private void completeBranchInitialization(final Map<String, Template> templates) throws DecodeException {
+        private void completeBranchInitialization(final Map<String, Template> templates,
+                            boolean generateBranches) throws DecodeException {
             subordinateTemplates = new ArrayList<Template>();
             for (int i = 0; i < subordinateTemplateNames.size(); i++) {
                 subordinateTemplates.add(templates.get(subordinateTemplateNames.get(i).toLowerCase()));
@@ -1434,7 +1440,7 @@
                 }
             }
 
-            nextEntry = buildBranchEntry();
+            nextEntry = buildBranchEntry(generateBranches);
         }
 
         DN getBranchDN() {
@@ -1491,10 +1497,8 @@
 
         /**
          * Returns the entry corresponding to this branch.
-         *
-         * @return the entry, or null if it can't be generated
          */
-        private TemplateEntry buildBranchEntry() {
+        private TemplateEntry buildBranchEntry(boolean generateBranches) {
             final TemplateEntry entry = new TemplateEntry(this);
             final List<TemplateLine> lines = new ArrayList<TemplateLine>(rdnLines);
             lines.addAll(extraLines);
@@ -1504,6 +1508,11 @@
             for (int i = 0; i < subordinateTemplates.size(); i++) {
                 subordinateTemplates.get(i).reset(entry.getDN(), numEntriesPerTemplate.get(i));
             }
+
+            if (!generateBranches) {
+                return null;
+            }
+
             return entry;
         }
 
@@ -1685,7 +1694,10 @@
         /** parent DN of entries to generate for this template. */
         private DN parentDN;
 
-        /** Number of entries to generate for this template. */
+        /**
+         * Number of entries to generate for this template.
+         * Negative number means infinite generation.
+         */
         private int numberOfEntries;
 
         /** Current count of generated entries for this template. */
@@ -1747,7 +1759,7 @@
             if (nextEntry != null) {
                 return true;
             }
-            while (entriesCount < numberOfEntries) {
+            while ((entriesCount < numberOfEntries) || generateForever()) {
                 // get the template entry
                 if (!currentEntryIsInitialized) {
                     nextEntry = buildTemplateEntry();
@@ -1773,6 +1785,10 @@
             return false;
         }
 
+        private boolean generateForever() {
+            return numberOfEntries < 0;
+        }
+
         /**
          * Returns the next generated entry.
          *
diff --git a/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/addrate.template b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/addrate.template
new file mode 100644
index 0000000..09085e4
--- /dev/null
+++ b/opendj-sdk/opendj-core/src/main/resources/org/forgerock/opendj/ldif/addrate.template
@@ -0,0 +1,32 @@
+define suffix=dc=example,dc=com
+define maildomain=example.com
+
+branch: [suffix]
+
+branch: ou=People,[suffix]
+subordinateTemplate: person
+
+template: person
+rdnAttr: uid
+objectClass: top
+objectClass: person
+objectClass: organizationalPerson
+objectClass: inetOrgPerson
+givenName: <first>
+sn: <last>
+cn: {givenName} {sn}
+initials: {givenName:1}<random:chars:ABCDEFGHIJKLMNOPQRSTUVWXYZ:1>{sn:1}
+employeeNumber: <sequential:0>
+uid: user.{employeeNumber}
+mail: {uid}@[maildomain]
+userPassword: password
+telephoneNumber: <random:telephone>
+homePhone: <random:telephone>
+pager: <random:telephone>
+mobile: <random:telephone>
+street: <random:numeric:5> <file:streets> Street
+l: <file:cities>
+st: <file:states>
+postalCode: <random:numeric:5>
+postalAddress: {cn}${street}${l}, {st}  {postalCode}
+description: This is the description for {cn}.
diff --git a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
index 2b2ecbe..c212fca 100644
--- a/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
+++ b/opendj-sdk/opendj-core/src/test/java/org/forgerock/opendj/ldap/LDAPServer.java
@@ -410,6 +410,8 @@
             // Remove this from the map.
             entryMap.remove(dn);
             requestsInProgress.remove(context);
+            result = Responses.newResult(ResultCode.SUCCESS);
+            handler.handleResult(result);
         }
 
         public <R extends ExtendedResult> void handleExtendedRequest(final Integer context,
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/addrate b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/addrate
new file mode 100644
index 0000000..d6e8d42
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bat/addrate
@@ -0,0 +1,33 @@
+
+@echo off
+rem CDDL HEADER START
+rem
+rem The contents of this file are subject to the terms of the
+rem Common Development and Distribution License, Version 1.0 only
+rem (the "License").  You may not use this file except in compliance
+rem with the License.
+rem
+rem You can obtain a copy of the license at
+rem legal-notices/CDDLv1_0.txt
+rem or http://forgerock.org/license/CDDLv1.0.html.
+rem See the License for the specific language governing permissions
+rem and limitations under the License.
+rem
+rem When distributing Covered Code, include this CDDL HEADER in each
+rem file and include the License file at legal-notices/CDDLv1_0.txt.
+rem legal-notices/CDDLv1_0.txt.  If applicable,
+rem add the following below this CDDL HEADER, with the fields enclosed
+rem by brackets "[]" replaced with your own identifying information:
+rem      Portions Copyright [yyyy] [name of copyright owner]
+rem
+rem CDDL HEADER END
+rem
+rem
+rem      Copyright 2014 ForgeRock AS
+
+setlocal
+
+set OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.AddRate"
+set SCRIPT_NAME=addrate
+for %%i in (%~sf0) do call "%%~dPsi\..\lib\_client-script.bat" %*
+
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/addrate b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/addrate
new file mode 100644
index 0000000..7975cc1
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/assembly/bin/addrate
@@ -0,0 +1,36 @@
+#!/bin/sh
+#
+# 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/opendj3/legal-notices/CDDLv1_0.txt
+# or http://forgerock.org/license/CDDLv1.0.html.
+# 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/opendj3/legal-notices/CDDLv1_0.txt.  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 2014 ForgeRock AS
+
+
+OPENDJ_INVOKE_CLASS="com.forgerock.opendj.ldap.tools.AddRate"
+export OPENDJ_INVOKE_CLASS
+
+SCRIPT_NAME="addrate"
+export SCRIPT_NAME
+
+SCRIPT_DIR=`dirname "${0}"`
+"${SCRIPT_DIR}/../lib/_client-script.sh" "${@}"
\ No newline at end of file
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java
new file mode 100644
index 0000000..b7ea17c
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AddRate.java
@@ -0,0 +1,429 @@
+/*
+ * 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 legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * 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 legal-notices/CDDLv1_0.txt.
+ * 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 2014 ForgeRock AS
+ */
+
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.cli.ArgumentConstants.*;
+import static com.forgerock.opendj.cli.Utils.filterExitCode;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.ldap.Connection;
+import org.forgerock.opendj.ldap.ConnectionFactory;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.ErrorResultException;
+import org.forgerock.opendj.ldap.FutureResult;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.requests.AddRequest;
+import org.forgerock.opendj.ldap.requests.DeleteRequest;
+import org.forgerock.opendj.ldap.requests.Requests;
+import org.forgerock.opendj.ldap.responses.Result;
+import org.forgerock.opendj.ldif.EntryGenerator;
+
+import com.forgerock.opendj.cli.ArgumentException;
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.BooleanArgument;
+import com.forgerock.opendj.cli.CommonArguments;
+import com.forgerock.opendj.cli.ConnectionFactoryProvider;
+import com.forgerock.opendj.cli.ConsoleApplication;
+import com.forgerock.opendj.cli.IntegerArgument;
+import com.forgerock.opendj.cli.MultiChoiceArgument;
+import com.forgerock.opendj.cli.StringArgument;
+
+/**
+ * A load generation tool that can be used to load a Directory Server with Add
+ * and Delete requests using one or more LDAP connections.
+ */
+public class AddRate extends ConsoleApplication {
+
+    private static final class AddPerformanceRunner extends PerformanceRunner {
+        private final class AddStatsHandler extends UpdateStatsResultHandler<Result> {
+            private String entryDN;
+
+            private AddStatsHandler(final long currentTime, String entryDN) {
+                super(currentTime);
+                this.entryDN = entryDN;
+            }
+
+            @Override
+            public void handleResult(final Result result) {
+                super.handleResult(result);
+
+                switch (delStrategy) {
+                case RANDOM:
+                    long newKey;
+                    do {
+                        newKey = randomSeq.get().nextInt();
+                    } while (dnEntriesAdded.putIfAbsent(newKey, this.entryDN) != null);
+                    break;
+                case FIFO:
+                    long uniqueTime = currentTime;
+                    while (dnEntriesAdded.putIfAbsent(uniqueTime, this.entryDN) != null) {
+                        uniqueTime++;
+                    }
+                    break;
+                default:
+                    break;
+                }
+
+                nbAdd.getAndIncrement();
+            }
+        }
+
+        private final class DeleteStatsHandler extends UpdateStatsResultHandler<Result> {
+
+            private DeleteStatsHandler(final long startTime) {
+                super(startTime);
+            }
+
+            @Override
+            public void handleResult(final Result result) {
+                super.handleResult(result);
+                nbDelete.getAndIncrement();
+            }
+        }
+
+        private final class AddRateStatsThread extends StatsThread {
+            private final String[] extraColumn = new String[1];
+
+            private AddRateStatsThread() {
+                super(new String[] { "Add%" });
+            }
+
+            @Override
+            String[] getAdditionalColumns() {
+                final int nbAddStat = nbAdd.getAndSet(0);
+                final int nbDelStat = nbDelete.getAndSet(0);
+                final int total = nbAddStat + nbDelStat;
+
+                extraColumn[0] = String.format("%.2f", total > 0 ? ((double) nbAddStat / total) * 100 : 0.0);
+
+                return extraColumn;
+            }
+        }
+
+        private final class AddWorkerThread extends WorkerThread {
+
+            AddWorkerThread(Connection connection, ConnectionFactory connectionFactory) {
+                super(connection, connectionFactory);
+            }
+
+            @Override
+            public FutureResult<?> performOperation(Connection connection,
+                    DataSource[] dataSources, long currentTime) {
+                if (needsDelete(currentTime)) {
+                    DeleteRequest dr = Requests.newDeleteRequest(getDNEntryToRemove());
+                    return connection.deleteAsync(dr, null, new DeleteStatsHandler(currentTime));
+                } else {
+                    return performAddOperation(connection, currentTime);
+                }
+            }
+
+            private FutureResult<?> performAddOperation(Connection connection, long currentTime) {
+                try {
+                    Entry entry;
+                    synchronized (generator) {
+                        entry = generator.readEntry();
+                    }
+
+                    AddRequest ar = Requests.newAddRequest(entry);
+                    return connection
+                        .addAsync(ar, null, new AddStatsHandler(currentTime, entry.getName().toString()));
+                } catch (IOException e) {
+                    // faking an error result by notifying the Handler
+                    UpdateStatsResultHandler<Result> resHandler = new UpdateStatsResultHandler<Result>(currentTime);
+                    resHandler.handleErrorResult(ErrorResultException.newErrorResult(ResultCode.OTHER, e));
+                    return null;
+                }
+            }
+
+            private boolean needsDelete(final long currentTime) {
+                if (dnEntriesAdded.isEmpty() || delStrategy == DeleteStrategy.OFF) {
+                    return false;
+                }
+
+                switch (delThreshold) {
+                case SIZE_THRESHOLD:
+                    return dnEntriesAdded.size() > sizeThreshold;
+                case AGE_THRESHOLD:
+                    long olderEntryTimestamp = (Long) dnEntriesAdded.firstKey();
+                    return (olderEntryTimestamp + timeToWait) < currentTime;
+                default:
+                    return false;
+                }
+            }
+
+            private String getDNEntryToRemove() {
+                String removedEntry = null;
+
+                while (removedEntry == null) {
+                    long minKey = dnEntriesAdded.firstKey();
+                    long maxKey = dnEntriesAdded.lastKey();
+                    long randomIndex = Math.round(Math.random() * (maxKey - minKey) + minKey);
+                    Long key = dnEntriesAdded.ceilingKey(randomIndex);
+
+                    if (key != null) {
+                        removedEntry = dnEntriesAdded.remove(key);
+                    }
+                }
+
+                return removedEntry;
+            }
+
+        }
+
+        private final ConcurrentSkipListMap<Long, String> dnEntriesAdded =
+            new ConcurrentSkipListMap<Long, String>();
+        private final ThreadLocal<Random> randomSeq = new ThreadLocal<Random>() {
+            @Override
+            protected Random initialValue() {
+                return new Random();
+            }
+        };
+
+        private EntryGenerator generator;
+        private DeleteStrategy delStrategy;
+        private DeleteThreshold delThreshold;
+        private Integer sizeThreshold;
+        private long timeToWait;
+        private final AtomicInteger nbAdd = new AtomicInteger();
+        private final AtomicInteger nbDelete = new AtomicInteger();
+
+        private AddPerformanceRunner(final PerformanceRunnerOptions options) throws ArgumentException {
+            super(options);
+        }
+
+        @Override
+        WorkerThread newWorkerThread(Connection connection, ConnectionFactory connectionFactory) {
+            return new AddWorkerThread(connection, connectionFactory);
+        }
+
+        @Override
+        StatsThread newStatsThread() {
+            return new AddRateStatsThread();
+        }
+
+        public void validate(MultiChoiceArgument<DeleteStrategy> delModeArg, IntegerArgument delSizeThresholdArg,
+                                IntegerArgument delAgeThresholdArg) throws ArgumentException {
+            super.validate();
+            delStrategy = delModeArg.getTypedValue();
+            // Check for inconsistent use cases
+            if (delSizeThresholdArg.isPresent() && delAgeThresholdArg.isPresent()) {
+                throw new ArgumentException(ERR_ADDRATE_THRESHOLD_SIZE_AND_AGE.get());
+            } else if (delStrategy == DeleteStrategy.OFF
+                && (delSizeThresholdArg.isPresent() || delAgeThresholdArg.isPresent())) {
+                throw new ArgumentException(ERR_ADDRATE_DELMODE_OFF_THRESHOLD_ON.get());
+            } else if (delStrategy == DeleteStrategy.RANDOM && delAgeThresholdArg.isPresent()) {
+                throw new ArgumentException(ERR_ADDRATE_DELMODE_RAND_THRESHOLD_AGE.get());
+            }
+
+            if (delStrategy != DeleteStrategy.OFF) {
+                delThreshold =
+                    delAgeThresholdArg.isPresent() ? DeleteThreshold.AGE_THRESHOLD : DeleteThreshold.SIZE_THRESHOLD;
+                if (delThreshold == DeleteThreshold.SIZE_THRESHOLD) {
+                    sizeThreshold = delSizeThresholdArg.getIntValue();
+                } else {
+                    timeToWait = delAgeThresholdArg.getIntValue() * 1000000000L;
+                }
+            }
+        }
+    }
+
+    private enum DeleteStrategy {
+        OFF, RANDOM, FIFO;
+    }
+
+    private enum DeleteThreshold {
+        SIZE_THRESHOLD, AGE_THRESHOLD, OFF;
+    }
+
+    private static final int EXIT_CODE_SUCCESS = 0;
+
+    /**
+     * The main method for AddRate tool.
+     *
+     * @param args
+     *            The command-line arguments provided to this program.
+     */
+    public static void main(final String[] args) {
+        final int retCode = new AddRate().run(args);
+        System.exit(filterExitCode(retCode));
+    }
+
+    private BooleanArgument verbose;
+
+    private BooleanArgument scriptFriendly;
+
+    private AddRate() {
+        // Nothing to do
+    }
+
+    AddRate(PrintStream out, PrintStream err) {
+        super(out, err);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isInteractive() {
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isScriptFriendly() {
+        return scriptFriendly.isPresent();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean isVerbose() {
+        return verbose.isPresent();
+    }
+
+    int run(final String[] args) {
+        // Create the command-line argument parser for use with this program.
+        final LocalizableMessage toolDescription = INFO_ADDRATE_TOOL_DESCRIPTION.get();
+        final ArgumentParser argParser =
+            new ArgumentParser(AddRate.class.getName(), toolDescription, false, true, 1, 1, "template-file-path");
+
+        ConnectionFactoryProvider connectionFactoryProvider;
+        ConnectionFactory connectionFactory;
+        AddPerformanceRunner runner;
+
+        /* Entries generation parameters */
+        IntegerArgument randomSeedArg;
+        StringArgument resourcePathArg;
+        StringArgument constantsArg;
+
+        /* addrate specifics arguments */
+        MultiChoiceArgument<DeleteStrategy> deleteMode;
+        IntegerArgument deleteSizeThreshold;
+        IntegerArgument deleteAgeThreshold;
+
+        try {
+            Utils.setDefaultPerfToolProperties();
+            PerformanceRunnerOptions options = new PerformanceRunnerOptions(argParser, this);
+            options.setSupportsGeneratorArgument(false);
+
+            connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
+            runner = new AddPerformanceRunner(options);
+
+            addCommonArguments(argParser);
+
+            /* Entries generation parameters */
+            resourcePathArg =
+                new StringArgument("resourcepath", 'r', MakeLDIF.OPTION_LONG_RESOURCE_PATH, false, false, true,
+                    INFO_PATH_PLACEHOLDER.get(), null, null, INFO_ADDRATE_DESCRIPTION_RESOURCE_PATH.get());
+            argParser.addArgument(resourcePathArg);
+
+            randomSeedArg =
+                new IntegerArgument("randomseed", 'R', OPTION_LONG_RANDOM_SEED, false, false, true,
+                    INFO_SEED_PLACEHOLDER.get(), 0, null, INFO_ADDRATE_DESCRIPTION_SEED.get());
+            argParser.addArgument(randomSeedArg);
+
+            constantsArg =
+                new StringArgument("constant", 'g', MakeLDIF.OPTION_LONG_CONSTANT, false, true, true,
+                    INFO_CONSTANT_PLACEHOLDER.get(), null, null, INFO_ADDRATE_DESCRIPTION_CONSTANT.get());
+            argParser.addArgument(constantsArg);
+
+            /* addrate specifics arguments */
+            deleteMode =
+                new MultiChoiceArgument<DeleteStrategy>("deletemode", 'C', "deleteMode", false, true,
+                    INFO_DELETEMODE_PLACEHOLDER.get(), Arrays.asList(DeleteStrategy.values()), false,
+                    INFO_ADDRATE_DESCRIPTION_DELETEMODE.get());
+            deleteMode.setDefaultValue(DeleteStrategy.FIFO.toString());
+            argParser.addArgument(deleteMode);
+
+            deleteSizeThreshold =
+                new IntegerArgument("deletesizethreshold", 's', "deleteSizeThreshold", false, true,
+                    INFO_DELETESIZETHRESHOLD_PLACEHOLDER.get(), INFO_ADDRATE_DESCRIPTION_DELETESIZETHRESHOLD.get());
+            deleteSizeThreshold.setDefaultValue(String.valueOf(10000));
+            argParser.addArgument(deleteSizeThreshold);
+
+            deleteAgeThreshold =
+                new IntegerArgument("deleteagethreshold", 'a', "deleteAgeThreshold", false, true,
+                    INFO_DELETEAGETHRESHOLD_PLACEHOLDER.get(), INFO_ADDRATE_DESCRIPTION_DELETEAGETHRESHOLD.get());
+            argParser.addArgument(deleteAgeThreshold);
+        } catch (final ArgumentException ae) {
+            errPrintln(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage()));
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        // Parse the command-line arguments provided to this program.
+        try {
+            argParser.parseArguments(args);
+
+            if (argParser.usageOrVersionDisplayed()) {
+                return EXIT_CODE_SUCCESS;
+            }
+
+            connectionFactory = connectionFactoryProvider.getAuthenticatedConnectionFactory();
+            runner.validate(deleteMode, deleteSizeThreshold, deleteAgeThreshold);
+        } catch (final ArgumentException ae) {
+            final LocalizableMessage message = ERR_ERROR_PARSING_ARGS.get(ae.getMessage());
+            errPrintln(message);
+            errPrintln(argParser.getUsageMessage());
+            return ResultCode.CLIENT_SIDE_PARAM_ERROR.intValue();
+        }
+
+        final String templatePath = argParser.getTrailingArguments().get(0);
+
+        runner.generator =
+            MakeLDIF.createGenerator(templatePath, resourcePathArg, randomSeedArg, constantsArg, false, this);
+
+        return runner.run(connectionFactory);
+    }
+
+    private void addCommonArguments(ArgumentParser argParser) throws ArgumentException {
+        StringArgument propertiesFileArgument = CommonArguments.getPropertiesFile();
+        argParser.addArgument(propertiesFileArgument);
+        argParser.setFilePropertiesArgument(propertiesFileArgument);
+
+        BooleanArgument noPropertiesFileArgument = CommonArguments.getNoPropertiesFile();
+        argParser.addArgument(noPropertiesFileArgument);
+        argParser.setNoPropertiesFileArgument(noPropertiesFileArgument);
+
+        BooleanArgument showUsage = CommonArguments.getShowUsage();
+        argParser.addArgument(showUsage);
+        argParser.setUsageArgument(showUsage, getOutputStream());
+
+        verbose = CommonArguments.getVerbose();
+        argParser.addArgument(verbose);
+
+        scriptFriendly =
+            new BooleanArgument("scriptFriendly", 'S', "scriptFriendly", INFO_DESCRIPTION_SCRIPT_FRIENDLY.get());
+        scriptFriendly.setPropertyName("scriptFriendly");
+        argParser.addArgument(scriptFriendly);
+
+    }
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java
index 3e81eec..6ba512a 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/AuthRate.java
@@ -321,9 +321,9 @@
         private BindRequest bindRequest;
         private int invalidCredPercent;
 
-        private BindPerformanceRunner(final ArgumentParser argParser, final ConsoleApplication app)
+        private BindPerformanceRunner(final PerformanceRunnerOptions options)
                 throws ArgumentException {
-            super(argParser, app, true, true, true);
+            super(options);
         }
 
         @Override
@@ -408,9 +408,13 @@
 
         try {
             setDefaultPerfToolProperties();
+            PerformanceRunnerOptions options = new PerformanceRunnerOptions(argParser, this);
+            options.setSupportsRebind(false);
+            options.setSupportsAsynchronousRequests(false);
+            options.setSupportsMultipleThreadsPerConnection(false);
 
             connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
-            runner = new BindPerformanceRunner(argParser, this);
+            runner = new BindPerformanceRunner(options);
 
             propertiesFileArgument = CommonArguments.getPropertiesFile();
             argParser.addArgument(propertiesFileArgument);
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/MakeLDIF.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/MakeLDIF.java
index 66fccf2..c5b98e3 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/MakeLDIF.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/MakeLDIF.java
@@ -55,6 +55,16 @@
  */
 public final class MakeLDIF extends ConsoleApplication {
 
+    /**
+     * The value for the constant option in LDIF generator tools.
+     */
+    public static final String OPTION_LONG_CONSTANT = "constant";
+
+    /**
+     * The value for the path to look for LDIF resources (e.g data files).
+     */
+    public static final String OPTION_LONG_RESOURCE_PATH = "resourcePath";
+
     private static final int EXIT_CODE_SUCCESS = 0;
     private static final int EXIT_CODE_FAILURE = 1;
 
@@ -84,7 +94,7 @@
         StringArgument resourcePath;
         StringArgument constants;
         try {
-            resourcePath = new StringArgument("resourcepath", 'r', "resourcePath", false, false, true,
+            resourcePath = new StringArgument("resourcepath", 'r', OPTION_LONG_RESOURCE_PATH, false, false, true,
                     INFO_PATH_PLACEHOLDER.get(), null, null, INFO_MAKELDIF_DESCRIPTION_RESOURCE_PATH.get());
             argParser.addArgument(resourcePath);
 
@@ -97,7 +107,7 @@
                     false, true, INFO_SEED_PLACEHOLDER.get(), 0, null, INFO_MAKELDIF_DESCRIPTION_SEED.get());
             argParser.addArgument(randomSeed);
 
-            constants = new StringArgument("constant", 'c', "constant", false, true, true,
+            constants = new StringArgument("constant", 'c', OPTION_LONG_CONSTANT, false, true, true,
                     INFO_CONSTANT_PLACEHOLDER.get(),
                     null, null, INFO_MAKELDIF_DESCRIPTION_CONSTANT.get());
             argParser.addArgument(constants);
@@ -168,17 +178,15 @@
         return EXIT_CODE_SUCCESS;
     }
 
-    /**
-     * Returns the initialized generator, or null if a problem occurs.
-     */
-    private EntryGenerator createGenerator(final String templatePath, final StringArgument resourcePath,
-            final IntegerArgument randomSeedArg, final StringArgument constants) {
-        final EntryGenerator generator = new EntryGenerator(templatePath);
+    static EntryGenerator createGenerator(final String templatePath, final StringArgument resourcePath,
+                                            final IntegerArgument randomSeedArg, final StringArgument constants,
+                                            final boolean generateBranches, final ConsoleApplication app) {
+        final EntryGenerator generator = new EntryGenerator(templatePath).setGenerateBranches(generateBranches);
 
         if (resourcePath.isPresent()) {
             final File resourceDir = new File(resourcePath.getValue());
             if (!resourceDir.exists()) {
-                errPrintln(ERR_MAKELDIF_NO_SUCH_RESOURCE_DIRECTORY.get(resourcePath.getValue()));
+                app.errPrintln(ERR_LDIF_GEN_TOOL_NO_SUCH_RESOURCE_DIRECTORY.get(resourcePath.getValue()));
                 generator.close();
                 return null;
             }
@@ -189,14 +197,14 @@
             try {
                 generator.setRandomSeed(randomSeedArg.getIntValue());
             } catch (ArgumentException ae) {
-                errPrintln(ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
+                app.errPrintln(ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
                 generator.close();
                 return null;
             }
         }
 
         if (constants.isPresent()) {
-            if (!addConstantsToGenerator(constants, generator)) {
+            if (!addConstantsToGenerator(constants, generator, app)) {
                 generator.close();
                 return null;
             }
@@ -206,7 +214,7 @@
         try {
             generator.hasNext();
         } catch (IOException e) {
-            errPrintln(ERR_MAKELDIF_EXCEPTION_DURING_PARSE.get(e.getMessage()));
+            app.errPrintln(ERR_LDIF_GEN_TOOL_EXCEPTION_DURING_PARSE.get(e.getMessage()));
             generator.close();
             return null;
         }
@@ -217,11 +225,12 @@
     /**
      * Returns true if all constants are added to generator, false otherwise.
      */
-    private boolean addConstantsToGenerator(StringArgument constants, EntryGenerator generator) {
+    private static boolean addConstantsToGenerator(StringArgument constants, EntryGenerator generator,
+                                                       final ConsoleApplication app) {
         for (final String constant : constants.getValues()) {
             final String[] chunks = constant.split("=");
             if (chunks.length != 2) {
-                errPrintln(ERR_CONSTANT_ARG_CANNOT_DECODE.get(constant));
+                app.errPrintln(ERR_CONSTANT_ARG_CANNOT_DECODE.get(constant));
                 return false;
             }
             generator.setConstant(chunks[0], chunks[1]);
@@ -229,8 +238,14 @@
         return true;
     }
 
+
+    private EntryGenerator createGenerator(final String templatePath, final StringArgument resourcePath,
+            final IntegerArgument randomSeedArg, final StringArgument constants) {
+        return createGenerator(templatePath, resourcePath, randomSeedArg, constants, true, this);
+    }
+
     /**
-     * Returns true if generation is successfull, false otherwise.
+     * Returns true if generation is successful, false otherwise.
      */
     private boolean generateEntries(final EntryGenerator generator, final LDIFEntryWriter writer,
             final StringArgument ldifFile) {
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java
index 419a736..bea207d 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/ModRate.java
@@ -102,9 +102,9 @@
         private String baseDN;
         private String[] modStrings;
 
-        private ModifyPerformanceRunner(final ArgumentParser argParser, final ConsoleApplication app)
+        private ModifyPerformanceRunner(final PerformanceRunnerOptions options)
                 throws ArgumentException {
-            super(argParser, app, false, false, false);
+            super(options);
         }
 
         @Override
@@ -175,7 +175,7 @@
             Utils.setDefaultPerfToolProperties();
 
             connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
-            runner = new ModifyPerformanceRunner(argParser, this);
+            runner = new ModifyPerformanceRunner(new PerformanceRunnerOptions(argParser, this));
 
             propertiesFileArgument = CommonArguments.getPropertiesFile();
             argParser.addArgument(propertiesFileArgument);
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java
index 3fd70e3..8398959 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunner.java
@@ -28,6 +28,7 @@
 package com.forgerock.opendj.ldap.tools;
 
 import static org.forgerock.util.Utils.closeSilently;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.*;
 
 import java.io.IOException;
 import java.lang.management.GarbageCollectorMXBean;
@@ -70,39 +71,22 @@
      */
     class StatsThread extends Thread {
         private final String[] additionalColumns;
-
         private final List<GarbageCollectorMXBean> beans;
-
         private final Set<Double> percentiles;
-
         private final int numColumns;
-
         private ReversableArray etimes = new ReversableArray(100000);
-
         private final ReversableArray array = new ReversableArray(200000);
-
         protected long totalSuccessCount;
-
         protected long totalOperationCount;
-
         protected long totalFailedCount;
-
         protected long totalWaitTime;
-
         protected int successCount;
-
         protected int operationCount;
-
         protected int failedCount;
-
         protected long waitTime;
-
         protected long lastStatTime;
-
         protected long lastGCDuration;
-
         protected double recentDuration;
-
         protected double averageDuration;
 
         public StatsThread(final String[] additionalColumns) {
@@ -351,10 +335,10 @@
      *            The type of expected result.
      */
     class UpdateStatsResultHandler<S extends Result> implements ResultHandler<S> {
-        private final long startTime;
+        protected final long currentTime;
 
-        UpdateStatsResultHandler(final long startTime) {
-            this.startTime = startTime;
+        UpdateStatsResultHandler(final long currentTime) {
+            this.currentTime = currentTime;
         }
 
         @Override
@@ -371,7 +355,7 @@
         }
 
         private void updateStats() {
-            final long eTime = System.nanoTime() - startTime;
+            final long eTime = System.nanoTime() - currentTime;
             waitRecentTime.getAndAdd(eTime);
             synchronized (this) {
                 final ReversableArray array = eTimeBuffer.get();
@@ -451,7 +435,9 @@
                 count++;
                 if (!isAsync) {
                     try {
-                        future.get();
+                        if (future != null) {
+                            future.get();
+                        }
                     } catch (final InterruptedException e) {
                         // Ignore and check stop requested
                         continue;
@@ -603,20 +589,15 @@
     }
 
     private static final String[] EMPTY_STRINGS = new String[0];
-
     private final AtomicInteger operationRecentCount = new AtomicInteger();
-
     protected final AtomicInteger successRecentCount = new AtomicInteger();
-
     protected final AtomicInteger failedRecentCount = new AtomicInteger();
-
     private final AtomicLong waitRecentTime = new AtomicLong();
 
     private final AtomicReference<ReversableArray> eTimeBuffer =
             new AtomicReference<ReversableArray>(new ReversableArray(100000));
 
     private final ConsoleApplication app;
-
     private DataSource[] dataSourcePrototypes;
 
     // Thread local copies of the data sources
@@ -638,51 +619,34 @@
     };
 
     private volatile boolean stopRequested;
-
     private int numThreads;
-
     private int numConnections;
-
     private int targetThroughput;
-
     private int maxIterations;
-
     private boolean isAsync;
-
     private boolean noRebind;
-
     private int statsInterval;
-
     private final IntegerArgument numThreadsArgument;
-
     private final IntegerArgument maxIterationsArgument;
-
     private final IntegerArgument statsIntervalArgument;
-
     private final IntegerArgument targetThroughputArgument;
-
     private final IntegerArgument numConnectionsArgument;
-
     private final IntegerArgument percentilesArgument;
-
     private final BooleanArgument keepConnectionsOpen;
-
     private final BooleanArgument noRebindArgument;
-
     private final BooleanArgument asyncArgument;
-
     private final StringArgument arguments;
 
-    PerformanceRunner(final ArgumentParser argParser, final ConsoleApplication app,
-            final boolean neverRebind, final boolean neverAsynchronous,
-            final boolean alwaysSingleThreaded) throws ArgumentException {
-        this.app = app;
+    PerformanceRunner(final PerformanceRunnerOptions options) throws ArgumentException {
+        ArgumentParser argParser = options.getArgumentParser();
+
+        this.app = options.getConsoleApplication();
         numThreadsArgument =
                 new IntegerArgument("numThreads", 't', "numThreads", false, false, true,
                         LocalizableMessage.raw("{numThreads}"), 1, null, true, 1, false, 0,
                         LocalizableMessage.raw("Number of worker threads per connection"));
         numThreadsArgument.setPropertyName("numThreads");
-        if (!alwaysSingleThreaded) {
+        if (options.supportsMultipleThreadsPerConnection()) {
             argParser.addArgument(numThreadsArgument);
         } else {
             numThreadsArgument.addValue("1");
@@ -735,7 +699,7 @@
                 new BooleanArgument("noRebind", 'F', "noRebind", LocalizableMessage
                         .raw("Keep connections open and don't rebind"));
         noRebindArgument.setPropertyName("noRebind");
-        if (!neverRebind) {
+        if (options.supportsRebind()) {
             argParser.addArgument(noRebindArgument);
         }
 
@@ -744,7 +708,7 @@
                         .raw("Use asynchronous mode and don't "
                                 + "wait for results before sending the next request"));
         asyncArgument.setPropertyName("asynchronous");
-        if (!neverAsynchronous) {
+        if (options.supportsAsynchronousRequests()) {
             argParser.addArgument(asyncArgument);
         }
 
@@ -767,7 +731,9 @@
                                         + "arguments, they can be generated per iteration with the "
                                         + "following functions: " + StaticUtils.EOL
                                         + DataSource.getUsage()));
-        argParser.addArgument(arguments);
+        if (options.supportsGeneratorArgument()) {
+            argParser.addArgument(arguments);
+        }
     }
 
     @Override
@@ -804,15 +770,19 @@
         noRebind = noRebindArgument.isPresent();
 
         if (!noRebindArgument.isPresent() && this.numThreads > 1) {
-            throw new ArgumentException(LocalizableMessage.raw("--"
-                    + noRebindArgument.getLongIdentifier() + " must be used if --"
-                    + numThreadsArgument.getLongIdentifier() + " is > 1"));
+            throw new ArgumentException(ERR_TOOL_ARG_MUST_BE_USED_WHEN_ARG_CONDITION.get(
+                "--" + noRebindArgument.getLongIdentifier(), "--" + numThreadsArgument.getLongIdentifier(), "> 1"));
         }
 
         if (!noRebindArgument.isPresent() && asyncArgument.isPresent()) {
-            throw new ArgumentException(LocalizableMessage.raw("--"
-                    + noRebindArgument.getLongIdentifier() + " must be used when using --"
-                    + asyncArgument.getLongIdentifier()));
+            throw new ArgumentException(ERR_TOOL_ARG_NEEDED_WHEN_USING_ARG.get(
+                "--" + noRebindArgument.getLongIdentifier(), asyncArgument.getLongIdentifier()));
+        }
+
+        if (maxIterationsArgument.isPresent() && maxIterations <= 0) {
+            throw new ArgumentException(ERR_TOOL_NOT_ENOUGH_ITERATIONS.get(
+                "--" + maxIterationsArgument.getLongIdentifier(), numConnections * numThreads,
+                numConnectionsArgument.getLongIdentifier(), numThreadsArgument.getLongIdentifier()));
         }
 
         dataSourcePrototypes = DataSource.parse(arguments.getValues());
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java
new file mode 100644
index 0000000..9497c26
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/PerformanceRunnerOptions.java
@@ -0,0 +1,90 @@
+/*
+ * 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 legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * 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 legal-notices/CDDLv1_0.txt.
+ * 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
+ *
+ *
+ *      Portions Copyright 2014 ForgeRock AS
+ */
+
+package com.forgerock.opendj.ldap.tools;
+
+import com.forgerock.opendj.cli.ArgumentParser;
+import com.forgerock.opendj.cli.ConsoleApplication;
+
+/**
+ * SDK Performance Runner options wrapper.
+ */
+class PerformanceRunnerOptions {
+    private ArgumentParser argParser;
+    private ConsoleApplication app;
+
+    private boolean supportsRebind = true;
+    private boolean supportAsynchronousRequests = true;
+    private boolean supportsMultipleThreadsPerConnection = true;
+    private boolean supportsGeneratorArgument = true;
+
+    PerformanceRunnerOptions(ArgumentParser argParser, ConsoleApplication app) {
+        super();
+        this.argParser = argParser;
+        this.app = app;
+    }
+
+    boolean supportsRebind() {
+        return supportsRebind;
+    }
+
+    void setSupportsRebind(boolean supportsRebind) {
+        this.supportsRebind = !supportsRebind;
+    }
+
+    boolean supportsAsynchronousRequests() {
+        return supportAsynchronousRequests;
+    }
+
+    void setSupportsAsynchronousRequests(boolean supportAsynchronousRequests) {
+        this.supportAsynchronousRequests = supportAsynchronousRequests;
+    }
+
+    boolean supportsMultipleThreadsPerConnection() {
+        return supportsMultipleThreadsPerConnection;
+    }
+
+    void setSupportsMultipleThreadsPerConnection(boolean supportsMultipleThreadsPerConnection) {
+        this.supportsMultipleThreadsPerConnection = supportsMultipleThreadsPerConnection;
+    }
+
+    boolean supportsGeneratorArgument() {
+        return supportsGeneratorArgument;
+    }
+
+    void setSupportsGeneratorArgument(boolean supportsGeneratorArgument) {
+        this.supportsGeneratorArgument = supportsGeneratorArgument;
+    }
+
+    ArgumentParser getArgumentParser() {
+        return argParser;
+    }
+
+    ConsoleApplication getConsoleApplication() {
+        return app;
+    }
+
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java
index 66a312b..8318c0c 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/java/com/forgerock/opendj/ldap/tools/SearchRate.java
@@ -139,9 +139,9 @@
         private DereferenceAliasesPolicy dereferencesAliasesPolicy;
         private String[] attributes;
 
-        private SearchPerformanceRunner(final ArgumentParser argParser, final ConsoleApplication app)
+        private SearchPerformanceRunner(final PerformanceRunnerOptions options)
                 throws ArgumentException {
-            super(argParser, app, false, false, false);
+            super(options);
         }
 
         @Override
@@ -225,7 +225,7 @@
             Utils.setDefaultPerfToolProperties();
 
             connectionFactoryProvider = new ConnectionFactoryProvider(argParser, this);
-            runner = new SearchPerformanceRunner(argParser, this);
+            runner = new SearchPerformanceRunner(new PerformanceRunnerOptions(argParser, this));
 
             propertiesFileArgument = CommonArguments.getPropertiesFile();
             argParser.addArgument(propertiesFileArgument);
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools.properties b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools.properties
index 02ecc68..41bcb5a 100755
--- a/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools.properties
+++ b/opendj-sdk/opendj-ldap-toolkit/src/main/resources/com/forgerock/opendj/ldap/tools/tools.properties
@@ -22,7 +22,7 @@
 #
 #
 #      Copyright 2010 Sun Microsystems, Inc.
-#      Portions copyright 2012 ForgeRock AS.
+#      Portions copyright 2012-2014 ForgeRock AS.
 #
 #
 # Utility messages
@@ -429,6 +429,15 @@
   user-defined searches.\n\n\
   Example:\n\n\ \ searchrate -p 1389 -D "cn=directory manager" -w password \\\n\
   \ \ \ \ -F -c 4 -t 4 -b "dc=example,dc=com" -g "rand(0,2000)" "(uid=user.%%d)"
+INFO_ADDRATE_TOOL_DESCRIPTION=This utility can be used to measure \
+  add and optionally delete throughput and response time of a directory server using \
+  user-defined entries.\n\
+  \nExamples:\n \  This example is adding entries and randomly deleting them while \
+  the number of entries added is greater than 10,000: \n \
+  addrate -p 1389 -f -c 10 -d rand -s 10000 addrate.template \n \
+  This example adds entries and starts to delete them in the same \
+  order if their age is greater than a certain time: \n \
+  addrate -p 1389 -f -c 10 -d fifo -a 2 addrate.template 
 INFO_SEARCHRATE_TOOL_DESCRIPTION_BASEDN=Base DN format string.
 INFO_MODRATE_TOOL_DESCRIPTION=This utility can be used to measure \
   modify throughput and response time of a directory service using \
@@ -471,6 +480,14 @@
  files and report the differences in LDIF format
 INFO_LDIFSEARCH_TOOL_DESCRIPTION=This utility can be used to perform search \
  operations against entries contained in an LDIF file
+ERR_LDIF_GEN_TOOL_EXCEPTION_DURING_PARSE=An error occurred while \
+ parsing template file:  %s
+ERR_LDIF_GEN_TOOL_NO_SUCH_RESOURCE_DIRECTORY=The specified resource \
+ directory %s does not exist
+ERR_TOOL_NOT_ENOUGH_ITERATIONS=%s argument must be greater than or equal to %s \
+ (%s per %s)
+ERR_TOOL_ARG_NEEDED_WHEN_USING_ARG=%s must be used when using %s
+ERR_TOOL_ARG_MUST_BE_USED_WHEN_ARG_CONDITION=%s must be used if %s is %s
  #
  # MakeLDIF tool
  #
@@ -487,18 +504,34 @@
 INFO_MAKELDIF_DESCRIPTION_HELP=Show this usage information
 INFO_MAKELDIF_DESCRIPTION_RESOURCE_PATH=Path to look for \
  MakeLDIF resources (e.g., data files)
-ERR_MAKELDIF_NO_SUCH_RESOURCE_DIRECTORY=The specified resource \
- directory %s could not be found
 INFO_MAKELDIF_PROCESSED_N_ENTRIES=Processed %d entries
 INFO_MAKELDIF_PROCESSING_COMPLETE=LDIF processing complete. %d entries \
  written
-ERR_MAKELDIF_EXCEPTION_DURING_PARSE=An error occurred while \
- parsing template file :  %s
 ERR_MAKELDIF_UNABLE_TO_CREATE_LDIF=An error occurred while \
  attempting to open LDIF file %s for writing:  %s
 ERR_MAKELDIF_ERROR_WRITING_LDIF=An error occurred while writing data \
  to LDIF file %s:  %s
 ERR_MAKELDIF_EXCEPTION_DURING_PROCESSING=An error occurred while \
  processing :  %s
-ERR_CONSTANT_ARG_CANNOT_DECODE=Unable to parse a constant argument, \
+ERR_CONSTANT_ARG_CANNOT_DECODE=Unable to parse a constant argument \
  expecting name=value but got %s
+#
+# AddRate Tool
+#
+INFO_ADDRATE_DESCRIPTION_RESOURCE_PATH=Path to look for template resources (e.g. data files)
+INFO_ADDRATE_DESCRIPTION_SEED=The seed to use for initializing the random number generator
+INFO_ADDRATE_DESCRIPTION_CONSTANT=A constant that overrides the value set in the template file
+INFO_ADDRATE_DESCRIPTION_DELETEMODE=The algorithm used for selecting entries to be deleted which \
+ must be one of "fifo", "random", or "off".
+INFO_ADDRATE_DESCRIPTION_DELETESIZETHRESHOLD=Specifies the number of entries \
+ to be added before deletion begins
+INFO_ADDRATE_DESCRIPTION_DELETEAGETHRESHOLD=Specifies the age at which added entries \
+ will become candidates for deletion
+INFO_DELETEMODE_PLACEHOLDER={fifo | random | off}
+INFO_DELETESIZETHRESHOLD_PLACEHOLDER={count}
+INFO_DELETEAGETHRESHOLD_PLACEHOLDER={seconds}
+ERR_ADDRATE_DELMODE_OFF_THRESHOLD_ON=A deletion threshold should not be specified when deletion is disabled
+ERR_ADDRATE_THRESHOLD_SIZE_AND_AGE=Size and age based deletion thresholds were both \
+ specified, but only only one at a time is supported
+ERR_ADDRATE_DELMODE_RAND_THRESHOLD_AGE=A age based deletion threshold should not be used \
+ with a random deletion mode
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AddRateITCase.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AddRateITCase.java
new file mode 100644
index 0000000..cabd601
--- /dev/null
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/AddRateITCase.java
@@ -0,0 +1,115 @@
+/*
+ * 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 legal-notices/CDDLv1_0.txt
+ * or http://forgerock.org/license/CDDLv1.0.html.
+ * 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 legal-notices/CDDLv1_0.txt.
+ * 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 2014 ForgeRock AS.
+ */
+package com.forgerock.opendj.ldap.tools;
+
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.ERR_ADDRATE_DELMODE_OFF_THRESHOLD_ON;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.ERR_ADDRATE_DELMODE_RAND_THRESHOLD_AGE;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.ERR_ADDRATE_THRESHOLD_SIZE_AND_AGE;
+import static com.forgerock.opendj.ldap.tools.ToolsMessages.INFO_ADDRATE_TOOL_DESCRIPTION;
+import static org.fest.assertions.Assertions.assertThat;
+import static org.forgerock.util.Utils.closeSilently;
+
+import java.io.PrintStream;
+
+import org.forgerock.opendj.ldap.ByteStringBuilder;
+import org.forgerock.opendj.ldap.TestCaseUtils;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+public class AddRateITCase extends ToolsITCase {
+    private static final String TEMPLATE_NAME = "addrate.template";
+    private static final String ADD_PERCENT_TEXT = "Add%";
+    private static final int THROUGHPUT_COLUMN = 1;
+    private static final int ERR_PER_SEC_COLUMN_NUMBER = 8;
+
+    @DataProvider
+    public Object[][] addRateArgs() throws Exception {
+        String[] commonArgs =
+            args("-h", TestCaseUtils.getServerSocketAddress().getHostName(),
+                "-p", Integer.toString(TestCaseUtils.getServerSocketAddress().getPort()),
+                "-c", "1", "-t", "1", "-i", "1", "-m", "1000", "-S");
+
+        return new Object[][] {
+            // Check if the help message is correctly prompted
+            { args("-H"), INFO_ADDRATE_TOOL_DESCRIPTION.get(), "" },
+
+            // Should report inconsistent use of options
+            { args(commonArgs, "-C", "off", "-a", "3", TEMPLATE_NAME), "",
+                ERR_ADDRATE_DELMODE_OFF_THRESHOLD_ON.get() },
+            { args(commonArgs, "-C", "off", "-s", "30000", TEMPLATE_NAME), "",
+                ERR_ADDRATE_DELMODE_OFF_THRESHOLD_ON.get() },
+            { args(commonArgs, "-C", "fifo", "-a", "3", "-s", "20000", TEMPLATE_NAME), "",
+                ERR_ADDRATE_THRESHOLD_SIZE_AND_AGE.get() },
+            { args(commonArgs, "-C", "random", "-a", "3", TEMPLATE_NAME), "",
+                ERR_ADDRATE_DELMODE_RAND_THRESHOLD_AGE.get() },
+
+            // Correct test case
+            { args(commonArgs, "-s", "10", TEMPLATE_NAME), ADD_PERCENT_TEXT, "" },
+
+        };
+    }
+
+    public String[] args(String[] startLine, String... args) {
+        String[] res = new String[startLine.length + args.length];
+
+        System.arraycopy(startLine, 0, res, 0, startLine.length);
+        System.arraycopy(args, 0, res, startLine.length, args.length);
+
+        return res;
+    }
+
+    @Test(dataProvider = "addRateArgs")
+    public void testITAddRate(String[] arguments, Object expectedOut, Object expectedErr) throws Exception {
+        ByteStringBuilder out = new ByteStringBuilder();
+        ByteStringBuilder err = new ByteStringBuilder();
+
+        PrintStream outStream = new PrintStream(out.asOutputStream());
+        PrintStream errStream = new PrintStream(err.asOutputStream());
+
+        try {
+            AddRate addRate = new AddRate(outStream, errStream);
+            addRate.run(arguments);
+
+            checkOuputStreams(out, err, expectedOut, expectedErr);
+            String outContent = out.toString();
+
+            if (outContent.contains(ADD_PERCENT_TEXT)) {
+                // Check that there was no error in search
+                String[] addRateResLines = outContent.split(System.getProperty("line.separator"));
+                // Skip header line
+                for (int i = 1; i < addRateResLines.length; i++) {
+                    String[] addhRateLineData = addRateResLines[i].split(",");
+                    assertThat(addhRateLineData[ERR_PER_SEC_COLUMN_NUMBER].trim()).isEqualTo("0.0");
+                    assertThat(addhRateLineData[THROUGHPUT_COLUMN].trim()).isNotEqualTo("0.0");
+                }
+
+            }
+        } finally {
+            closeSilently(outStream, errStream);
+        }
+    }
+
+}
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/MakeLDIFTestCase.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/MakeLDIFTestCase.java
index bd33975..3672dd1 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/MakeLDIFTestCase.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/MakeLDIFTestCase.java
@@ -68,7 +68,7 @@
               expectedErrOutput(ERR_ERROR_PARSING_ARGS.get("")) },
 
             { args("-r", "unknown/path" , "example.template"),
-              expectedErrOutput(ERR_MAKELDIF_NO_SUCH_RESOURCE_DIRECTORY.get("unknown/path")) },
+              expectedErrOutput(ERR_LDIF_GEN_TOOL_NO_SUCH_RESOURCE_DIRECTORY.get("unknown/path")) },
 
             { args("-o", "unknown/path" , "example.template"),
               expectedErrOutput(ERR_MAKELDIF_UNABLE_TO_CREATE_LDIF.get("unknown/path", "")) },
diff --git a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsITCase.java b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsITCase.java
index 3297650..ec20d54 100644
--- a/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsITCase.java
+++ b/opendj-sdk/opendj-ldap-toolkit/src/test/java/com/forgerock/opendj/ldap/tools/ToolsITCase.java
@@ -74,16 +74,19 @@
     }
 
     private void checkOutputStream(ByteStringBuilder out, Object expectedOutput) {
+        String lineSeparator = System.getProperty("line.separator");
         String toCompare = expectedOutput.toString();
 
         if (expectedOutput instanceof LocalizableMessage) {
             toCompare = wrapText((LocalizableMessage) expectedOutput, MAX_LINE_WIDTH);
         }
 
+        toCompare = toCompare.replace(lineSeparator, " ");
+
         if (toCompare.isEmpty()) {
             assertThat(out.toString().length()).isEqualTo(0);
         } else {
-            assertThat(out.toString()).contains(toCompare);
+            assertThat(out.toString().replace(lineSeparator, " ")).contains(toCompare);
         }
 
     }

--
Gitblit v1.10.0