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

39 files modified
6 files added
2903 ■■■■■ changed files
.github/benchmark/compare-opendj.sh 8 ●●●●● patch | view | raw | blame | history
.github/workflows/benchmark.yml 9 ●●●●● patch | view | raw | blame | history
opendj-cli/src/main/java/com/forgerock/opendj/cli/DocGenerationHelper.java 28 ●●●●● patch | view | raw | blame | history
opendj-cli/src/test/java/com/forgerock/opendj/cli/DocGenerationHelperTestCase.java 107 ●●●●● patch | view | raw | blame | history
opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/DSConfig.java 15 ●●●●● patch | view | raw | blame | history
opendj-config/src/main/resources/com/forgerock/opendj/dsconfig/dsconfig.properties 9 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-import-export.adoc 2 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-indexing.adoc 4 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-monitoring.adoc 2 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc 1 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/man-pages/_description-upgrade.adoc 39 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/man-pages/_variablelist-backendstat-index-status.adoc 49 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/reference/appendix-interface-stability.adoc 24 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc 2 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-writing-plugins.adoc 10 ●●●●● patch | view | raw | blame | history
opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListener.java 14 ●●●●● patch | view | raw | blame | history
opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPServerFilter.java 24 ●●●●● patch | view | raw | blame | history
opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java 62 ●●●●● patch | view | raw | blame | history
opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java 40 ●●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/BackendIndexConfiguration.xml 6 ●●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/LDAPConnectionHandlerConfiguration.xml 71 ●●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PluginConfiguration.xml 6 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/pom.xml 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/forgerock/opendj/reactive/LDAPConnectionHandler2.java 278 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/api/plugin/DirectoryServerPlugin.java 7 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendStat.java 158 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/core/PluginConfigManager.java 206 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/plugins/PasswordPolicyImportPlugin.java 9 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java 10 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/plugins/SambaPasswordPlugin.java 10 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeCli.java 2 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/plugin.properties 5 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/tool.properties 10 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/BackendStatTest.java 87 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/PluggableBackendImplTestCase.java 87 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/core/PluginConfigManagerTestCase.java 229 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/plugins/OtherPluginTypeTrackingPlugin.java 24 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/plugins/PasswordPolicyImportPluginTestCase.java 41 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/plugins/PluginTypeTrackingPlugin.java 193 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java 125 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/plugins/SambaPasswordPluginTestCase.java 34 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/protocols/ldap/LDAPConnectionHandler2TransportTestCase.java 767 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/tools/UpgradeTestCase.java 31 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/tools/dsconfig/DsconfigOptionsTestCase.java 32 ●●●●● patch | view | raw | blame | history
opendj-server-msad-plugin/src/main/java/opendj/MsadPlugin.java 21 ●●●●● patch | view | raw | blame | history
.github/benchmark/compare-opendj.sh
@@ -50,6 +50,14 @@
  tar -xzf /tmp/jmeter.tgz -C "$HOME/jmeter"
fi
# ---------------------------------------------------------------- ephemeral ports
# BIND opens a new connection per iteration (~500/s), and docker-proxy relays each one to the
# container over a second connection from an ephemeral port. With the default range
# (32768-60999) and 60 s of TIME_WAIT that leg runs out of ports within a minute, and BIND then
# fails with "LDAP connection has been closed". Widen the range and let connect() reuse
# TIME_WAIT ports.
sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535" net.ipv4.tcp_tw_reuse=1
# Poll OpenDJ readiness on localhost:1389. An image with a HEALTHCHECK has to report healthy
# first: on a first start the server the bootstrap started answers, then is stopped and
# started again, and a request sent in between fails. An older image's health check
.github/workflows/benchmark.yml
@@ -94,6 +94,15 @@
          fi
          echo "JMETER_BIN=$HOME/jmeter/apache-jmeter-$JMETER/bin/jmeter" >> "$GITHUB_ENV"
      - name: Widen the ephemeral port range
        run: |
          # BIND opens a new connection per iteration, and docker-proxy relays each one to the
          # container over a second connection from an ephemeral port. With the default range
          # (32768-60999) and 60 s of TIME_WAIT that leg runs out of ports within a minute, and
          # BIND then fails with "LDAP connection has been closed". Widen the range and let
          # connect() reuse TIME_WAIT ports.
          sudo sysctl -w net.ipv4.ip_local_port_range="1024 65535" net.ipv4.tcp_tw_reuse=1
      - name: Start OpenLDAP
        run: |
          docker run -d --name openldap -p 2389:389 \
opendj-cli/src/main/java/com/forgerock/opendj/cli/DocGenerationHelper.java
@@ -12,6 +12,7 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2015 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package com.forgerock.opendj.cli;
@@ -23,6 +24,7 @@
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Map;
import java.util.regex.Pattern;
/**
 * This class provides utility functions to help generate reference documentation.
@@ -38,6 +40,13 @@
    private static Configuration configuration;
    /**
     * An AsciiDoc attribute reference, such as {@code {name}}, that no backslash escapes.
     * Word characters are Unicode ones, as in Asciidoctor.
     */
    private static final Pattern ATTRIBUTE_REFERENCE =
            Pattern.compile("(?<!\\\\)(\\{\\w[\\w-]*\\})", Pattern.UNICODE_CHARACTER_CLASS);
    /**
     * Gets a FreeMarker configuration for applying templates.
     *
     * @return              A FreeMarker configuration.
@@ -71,13 +80,30 @@
            Writer writer = new OutputStreamWriter(outputStream)) {
            Template configurationTemplate = configuration.getTemplate(template);
            configurationTemplate.process(map, writer);
            builder.append(outputStream.toString());
            builder.append(escapeAttributeReferences(outputStream.toString()));
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage(), e);
        }
    }
    /**
     * Escapes the AsciiDoc attribute references in generated reference text.
     *
     * <br>
     *
     * The generated reference refers to no AsciiDoc attribute: a {@code {name}} in it is the placeholder
     * of an option value, written as is in the messages, which AsciiDoc would read as a reference
     * to a missing attribute. A reference that is already escaped is left as is,
     * so the result of a template can go through another template that includes it.
     *
     * @param text  The generated AsciiDoc text.
     * @return      The text with each attribute reference escaped by a backslash.
     */
    static String escapeAttributeReferences(final String text) {
        return ATTRIBUTE_REFERENCE.matcher(text).replaceAll("\\\\$1");
    }
    /**
     * Returns an option synopsis.
     *
     * <br>
opendj-cli/src/test/java/com/forgerock/opendj/cli/DocGenerationHelperTestCase.java
New file
@@ -0,0 +1,107 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package com.forgerock.opendj.cli;
import static org.fest.assertions.Assertions.assertThat;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.forgerock.i18n.LocalizableMessage;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
/**
 * Tests that the generated AsciiDoc reference writes the value placeholders of the options
 * as text, not as AsciiDoc attribute references.
 */
@SuppressWarnings("javadoc")
public final class DocGenerationHelperTestCase extends CliTestCase {
    private static final String GENDOC = "org.forgerock.opendj.gendoc";
    /** An AsciiDoc attribute reference that no backslash escapes. */
    private static final Pattern ATTRIBUTE_REFERENCE = Pattern.compile("(?<!\\\\)\\{\\w[\\w-]*\\}");
    private String scriptName;
    private String gendoc;
    @BeforeClass
    public void enableDocGeneration() {
        scriptName = System.setProperty(ArgumentParser.PROPERTY_SCRIPT_NAME, "test-tool");
        gendoc = System.setProperty(GENDOC, "true");
    }
    @AfterClass(alwaysRun = true)
    public void restoreProperties() {
        restore(ArgumentParser.PROPERTY_SCRIPT_NAME, scriptName);
        restore(GENDOC, gendoc);
    }
    private static void restore(final String name, final String value) {
        if (value != null) {
            System.setProperty(name, value);
        } else {
            System.clearProperty(name);
        }
    }
    @Test
    public void toolReferenceEscapesPlaceholders() throws Exception {
        final ArgumentParser parser =
                new ArgumentParser(getClass().getName(), LocalizableMessage.raw("Reads the {path} you give."), false);
        nameArgument().buildAndAddToParser(parser);
        final String doc = parser.getUsage();
        assertThat(doc).contains("Reads the \\{path} you give.");
        assertThat(doc).contains("`--backend-name \\{name}`::");
        assertThat(doc).contains("Depends on the \\{name} you provide, as {PROP:VALUE} or {name=value} do not.");
        assertNoAttributeReference(doc);
    }
    @Test
    public void subcommandReferenceEscapesPlaceholdersOnce() throws Exception {
        final SubCommandArgumentParser parser =
                new SubCommandArgumentParser(getClass().getName(), LocalizableMessage.raw("A tool."), false);
        final SubCommand subCommand =
                new SubCommand(parser, "get-backend-prop", LocalizableMessage.raw("Shows the {name} backend."));
        nameArgument().buildAndAddToSubCommand(subCommand);
        final String doc = parser.getUsage();
        assertThat(doc).contains("Shows the \\{name} backend.");
        assertThat(doc).contains("`--backend-name \\{name}`::");
        assertThat(doc).contains("Depends on the \\{name} you provide, as {PROP:VALUE} or {name=value} do not.");
        assertThat(doc).doesNotContain("\\\\{");
        assertNoAttributeReference(doc);
    }
    private static StringArgument.Builder nameArgument() {
        return StringArgument.builder("backend-name")
                .description(LocalizableMessage.raw(
                        "Depends on the {name} you provide, as {PROP:VALUE} or {name=value} do not"))
                .valuePlaceholder(LocalizableMessage.raw("{name}"));
    }
    private static void assertNoAttributeReference(final String doc) {
        final Matcher matcher = ATTRIBUTE_REFERENCE.matcher(doc);
        if (matcher.find()) {
            throw new AssertionError("Unescaped attribute reference " + matcher.group() + " in:\n" + doc);
        }
    }
}
opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/DSConfig.java
@@ -414,7 +414,7 @@
                @Override
                public String visitACI(ACIPropertyDefinition prop, Void p) {
                    b.append(op).append(REF_DSCFG_ACI_SYNTAX_REL_URL.get()).append(cp).append(EOL);
                    b.append(op).append(REF_DSCFG_ACI_SYNTAX.get()).append(cp).append(EOL);
                    return null;
                }
@@ -470,14 +470,14 @@
                @Override
                public String visitDuration(DurationPropertyDefinition prop, Void p) {
                    b.append(REF_DSCFG_DURATION_SYNTAX_REL_URL.get()).append(EOL);
                    b.append(REF_DSCFG_DURATION_SYNTAX.get()).append(EOL);
                    b.append(op);
                    if (prop.isAllowUnlimited()) {
                        b.append(REF_DSCFG_ALLOW_UNLIMITED.get()).append(" ");
                    }
                    if (prop.getMaximumUnit() != null) {
                        final String maxUnitName = prop.getMaximumUnit().getLongName();
                        b.append(REF_DSCFG_DURATION_MAX_UNIT.get(maxUnitName)).append(".");
                        b.append(REF_DSCFG_DURATION_MAX_UNIT.get(maxUnitName)).append(". ");
                    }
                    final DurationUnit baseUnit = prop.getBaseUnit();
                    final long lowerLimit = valueOf(baseUnit, prop.getLowerLimit());
@@ -485,7 +485,7 @@
                    b.append(REF_DSCFG_DURATION_LOWER_LIMIT.get(lowerLimit, unitName)).append(".");
                    if (prop.getUpperLimit() != null) {
                        final long upperLimit = valueOf(baseUnit, prop.getUpperLimit());
                        b.append(REF_DSCFG_DURATION_UPPER_LIMIT.get(upperLimit, unitName)).append(".");
                        b.append(" ").append(REF_DSCFG_DURATION_UPPER_LIMIT.get(upperLimit, unitName)).append(".");
                    }
                    b.append(cp).append(EOL);
                    return null;
@@ -540,14 +540,17 @@
                @Override
                public String visitSize(SizePropertyDefinition prop, Void p) {
                    b.append(op);
                    String separator = "";
                    if (prop.getLowerLimit() != 0) {
                        b.append(REF_DSCFG_INT_LOWER_LIMIT.get(prop.getLowerLimit())).append(".");
                        separator = " ";
                    }
                    if (prop.getUpperLimit() != null) {
                        b.append(REF_DSCFG_INT_UPPER_LIMIT.get(prop.getUpperLimit())).append(".");
                        b.append(separator).append(REF_DSCFG_INT_UPPER_LIMIT.get(prop.getUpperLimit())).append(".");
                        separator = " ";
                    }
                    if (prop.isAllowUnlimited()) {
                        b.append(REF_DSCFG_ALLOW_UNLIMITED.get());
                        b.append(separator).append(REF_DSCFG_ALLOW_UNLIMITED.get());
                    }
                    b.append(cp).append(EOL);
                    return null;
opendj-config/src/main/resources/com/forgerock/opendj/dsconfig/dsconfig.properties
@@ -12,6 +12,7 @@
#
# Copyright 2006-2010 Sun Microsystems, Inc.
# Portions Copyright 2011-2016 ForgeRock AS.
# Portions Copyright 2026 3A Systems, LLC.
#
# Format string definitions
@@ -398,8 +399,12 @@
# Strings for generated reference documentation.
REF_DSCFG_ALLOW_UNLIMITED_1000=A value of "-1" or "unlimited" for no limit.
REF_DSCFG_ACI_SYNTAX_REL_URL_1001=<olink targetdoc="admin-guide" targetptr="about-acis" />
REF_DSCFG_DURATION_SYNTAX_REL_URL_1002=<xinclude:include href="itemizedlist-duration.xml" />
REF_DSCFG_ACI_SYNTAX_1001=An access control instruction, as described in \
 xref:../admin-guide/chap-privileges-acis.adoc#about-acis["About Access Control Instructions"] \
 in the __Administration Guide__.
REF_DSCFG_DURATION_SYNTAX_1002=A duration: a number followed by a unit, one of \
 `ms` (milliseconds), `s` (seconds), `m` (minutes), `h` (hours), `d` (days) \
 or `w` (weeks), for example `1 s` or `2 w`.
REF_DSCFG_ARG_ADDITIONAL_INFO_1003=%s properties depend on the %s type, \
 which depends on the %s option.
REF_DSCFG_SUBTYPE_DEPENDENCIES_1004=%s properties depend on the %s type, \
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-import-export.adoc
@@ -498,7 +498,7 @@
OpenDJ directory server encrypts data using a symmetric key that is stored with the server configuration. The symmetric key is encrypted in turn with the server's public key that is also stored with the server configuration. When multiple servers are configured to replicate data as described in xref:chap-replication.adoc#configure-repl["Configuring Replication"], the servers replicate the keys as well, allowing any server replica to decrypt the data.
In addition to entry encryption, you can enable confidentiality by backend index, as long as confidentiality is enabled for the backend itself. Confidentiality hashes keys for equality type indexes using SHA-1, and encrypts the list of entries matching a substring key for substring indexes. The following example shows how to enable confidentiality for the `mail` index:
In addition to entry encryption, you can enable confidentiality by backend index, as long as confidentiality is enabled for the backend itself. Confidentiality encrypts the list of entries matching each key, for every index type of the attribute, and hashes the keys of the equality index using SHA-1. The keys of the other index types are stored as they are. The following example shows how to enable confidentiality for the `mail` index:
[source, console]
----
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-indexing.adoc
@@ -12,7 +12,7 @@
  information: "Portions copyright [year] [name of copyright owner]".
 
  Copyright 2017 ForgeRock AS.
  Portions Copyright 2024 3A Systems LLC.
  Portions Copyright 2024-2026 3A Systems LLC.
////
:figure-caption!:
@@ -849,7 +849,7 @@
[source, console]
----
$ backendstat show-index-status --backendID userRoot --baseDN dc=example,dc=com
Index Name                            ... Index Valid  Record Count  Over Entry Limit  95%  90%  85%
Index Name                            ... Index Valid  Record Count  Over Entry Limit  95%  90%  80%
--------------------------------------...-----------------------------------------------------------
uniqueMember.uniqueMemberMatch        ... true         0             0                 0    0    0
mail.caseIgnoreIA5Match               ... true         10000         0                 0    0    0
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-monitoring.adoc
@@ -79,7 +79,7 @@
To run the OpenDMK installer, use the self-extracting .jar:
[source, console]
[source, console, subs="attributes"]
----
$ java -jar ~/Downloads/opendmk-1.0-b02-*.jar
$ cd ~/Downloads/
opendj-doc-generated-ref/src/main/asciidoc/install-guide/chap-uninstall.adoc
@@ -18,6 +18,7 @@
:figure-caption!:
:example-caption!:
:table-caption!:
:opendj-version: x.y.z
[#chap-uninstall]
opendj-doc-generated-ref/src/main/asciidoc/man-pages/_description-upgrade.adoc
New file
@@ -0,0 +1,39 @@
////
  The contents of this file are subject to the terms of the Common Development and
  Distribution License (the License). You may not use this file except in compliance with the
  License.
  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
  specific language governing permission and limitations under the License.
  When distributing Covered Software, include this CDDL Header Notice in each file and include
  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
  Header, with the fields enclosed by brackets [] replaced by your own identifying
  information: "Portions Copyright [year] [name of copyright owner]".
  Copyright 2015 ForgeRock AS.
  Portions Copyright 2026 3A Systems, LLC.
////
This utility thus performs only part of the upgrade process, which includes the following phases for a single server.
. Get and unpack a newer version of OpenDJ directory server software.
. Stop the current OpenDJ directory server.
. Overwrite existing binary and script files with those of the newer version, and then run this utility before restarting OpenDJ.
. Start the upgraded OpenDJ directory server.
[IMPORTANT]
====
This utility __does not back up OpenDJ before you upgrade, nor does it restore OpenDJ if the utility fails__. In order to revert a failed upgrade, make sure you back up OpenDJ directory server before you overwrite existing binary and script files.
====
By default this utility requests confirmation before making important configuration changes. You can use the `--no-prompt` option to run the command non-interactively.
When using the `--no-prompt` option, if this utility cannot complete because it requires confirmation for a potentially very long or critical task, then it exits with an error and a message about how to finish making the changes. You can add the `--force` option to force a non-interactive upgrade to continue in this case, also performing long running and critical tasks.
After upgrading, see the resulting `logs/upgrade.log` file for a full list of operations performed.
opendj-doc-generated-ref/src/main/asciidoc/man-pages/_variablelist-backendstat-index-status.adoc
New file
@@ -0,0 +1,49 @@
////
  The contents of this file are subject to the terms of the Common Development and
  Distribution License (the License). You may not use this file except in compliance with the
  License.
  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
  specific language governing permission and limitations under the License.
  When distributing Covered Software, include this CDDL Header Notice in each file and include
  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
  Header, with the fields enclosed by brackets [] replaced by your own identifying
  information: "Portions Copyright [year] [name of copyright owner]".
  Copyright 2015 ForgeRock AS.
  Portions Copyright 2026 3A Systems, LLC.
////
When you run the `show-index-status` subcommand, the result is a table, followed by a "Total", which is the total number of indexes, followed by a list of indexes with "Over index-entry-limit keys" to show the values for which the number of entries exceeded the index entry limit. The table has the following columns.
Index Name::
Name of the index, which takes the form __attr.type__ for attribute indexes, and vlv.__name__ for VLV indexes. Some indexes are for OpenDJ directory server's internal use.
+
Example: `givenName.caseIgnoreSubstringsMatch:6`
Raw DB Name::
Name of the backend tree, which reflects how OpenDJ directory server organizes the data in the database.
+
Example: `/dc=com,dc=example/givenName.caseIgnoreSubstringsMatch:6`
Valid::
This is `true` for valid indexes. If this is `false`, the index might be degraded. Verify the index, and rebuild the index if necessary. For an attribute index that is not valid, the record count and the key counts that follow it show `-`.
Confidential::
This is `true` for indexes with `confidentiality-enabled`, whose lists of entry IDs are stored encrypted. The keys are not encrypted: an equality index stores its keys hashed, the other index types store them as they are. This is recorded as `-` for VLV indexes, which have no confidentiality setting.
Record Count::
Number of indexed keys. Use the `backendstat dump-index` command to see how many entry IDs correspond to each key.
Over Entry Limit::
Number of keys for which there are too many values to maintain an index, based on the index entry limit. This is recorded as `-` for VLV indexes.
+
In other words, with the default index entry limit of 4000, if every user in your large directory has an email address ending in `@example.com`, and a substring index with default substring length of 6 is maintained for `mail`, then OpenDJ directory server does not maintain indexes for keys corresponding to substrings in `@example.com`.
+
As a result, an LDAP search with the filter `"(mail=*@example.com)"` becomes an unindexed search even though a substring index exists for the mail attribute. By default OpenDJ directory server does not allow unindexed searches except by privileged users. This is usually exactly the behavior you want in order to prevent client applications from sending searches that return every user in the directory for example. Clients should refine their search filters instead.
95%, 90%, 80%::
Number of keys for which the number of values is approaching the index entry limit. This is a measure of how full the entry ID lists are. The `95%` column counts keys holding at least 95% of the limit, the `90%` column keys holding at least 90% but less than 95%, and the `80%` column keys holding at least 80% but less than 90%. These columns are recorded as `-` for VLV indexes, and stay at 0 for an index whose entry limit is 0, which means no limit.
opendj-doc-generated-ref/src/main/asciidoc/reference/appendix-interface-stability.adoc
@@ -12,7 +12,7 @@
  information: "Portions copyright [year] [name of copyright owner]".
 
  Copyright 2017 ForgeRock AS.
  Portions Copyright 2024 3A Systems LLC.
  Portions Copyright 2024-2026 3A Systems LLC.
////
:figure-caption!:
@@ -29,7 +29,7 @@
* Client tools—`ldap*`, `ldif*`, and `*rate` commands—are Evolving.
* The following classes, interfaces, and methods in the link:../javadoc/index.html[OpenDJ APIs, window=\_blank] are Evolving:
* The following classes, interfaces, and methods in the link:https://doc.openidentityplatform.org/opendj/apidocs/index.html[OpenDJ APIs, window=\_blank] are Evolving:
+
** `org.forgerock.opendj.ldap.Connections#newInternalConnection`
@@ -38,13 +38,11 @@
** `org.forgerock.opendj.ldap.Connections#newServerConnectionFactory`
** `org.forgerock.opendj.ldap.FutureResult`
** `org.forgerock.opendj.ldap.LDAPClientContext`
** `org.forgerock.opendj.ldap.LDAPListener`
** `org.forgerock.opendj.ldap.LDAPListenerOptions`
** `org.forgerock.opendj.ldap.LdapPromise`
** `org.forgerock.opendj.ldap.MemoryBackend`
@@ -75,21 +73,7 @@
** `org.forgerock.opendj.ldap.schema.SyntaxImpl`
* The following methods are Deprecated:
+
** `org.forgerock.opendj.ldap.Connections#newHeartBeatConnectionFactory`
** `org.forgerock.opendj.ldap.LDAPListenerOptions#getTCPNIOTransport`
** `org.forgerock.opendj.ldap.LDAPListenerOptions#setTCPNIOTransport`
** `org.forgerock.opendj.ldap.LDAPOptions#getTCPNIOTransport`
** `org.forgerock.opendj.ldap.LDAPOptions#setTCPNIOTransport`
* The class `org.forgerock.opendj.ldap.CoreMessages` is Internal.
* The class `com.forgerock.opendj.ldap.CoreMessages` is Internal.
* For all Java APIs, `com.*` packages are Internal.
opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-groups.adoc
@@ -539,6 +539,6 @@
----
By default, the referential integrity plugin is configured to manage `member` and `uniqueMember` attributes. These attributes take values that are DNs, and are indexed for equality by default for the default backend. Before you add an additional attribute to manage, make sure that it has DN syntax and that it is indexed for equality. OpenDJ directory server requires that the attribute be indexed because an unindexed search for integrity would potentially consume too many of the server's resources. Attribute syntax is explained in xref:../admin-guide/chap-schema.adoc#chap-schema["Managing Schema"] in the __Administration Guide__. For instructions on indexing attributes, see xref:../admin-guide/chap-indexing.adoc#configure-indexes["Configuring and Rebuilding Indexes"] in the __Administration Guide__.
You can also configure the referential integrity plugin to check that new entries added to groups actually exist in the directory by setting the `check-references` property to `true`. You can specify additional criteria once you have activated the check. To ensure that entries added must match a filter, set the `check-references-filter-criteria` to identify the attribute and the filter. For example, you can specify that group members must be person entries by setting `check-references-filter-criteria` to `member:(objectclass=person)`. To ensure that entries must be located in the same naming context, set `check-references-scope-criteria` to `naming-context`. The check runs when entries are added and modified, so the plugin must be registered for the `preOperationAdd` and `preOperationModify` plugin types, as the default configuration is. When the plugin is enabled, OpenDJ refuses to set `check-references` to `true` if `plugin-type` lacks either of them, and it refuses to enable a plugin configured that way. A plugin already configured that way when the server starts is loaded with a warning, and it does not check references until the types are added. Plugin types take effect when the plugin is enabled, so add them before enabling the plugin, or disable and re-enable it afterwards.
You can also configure the referential integrity plugin to check that new entries added to groups actually exist in the directory by setting the `check-references` property to `true`. You can specify additional criteria once you have activated the check. To ensure that entries added must match a filter, set the `check-references-filter-criteria` to identify the attribute and the filter. For example, you can specify that group members must be person entries by setting `check-references-filter-criteria` to `member:(objectclass=person)`. To ensure that entries must be located in the same naming context, set `check-references-scope-criteria` to `naming-context`. The check runs when entries are added and modified, so the plugin must be registered for the `preOperationAdd` and `preOperationModify` plugin types, as the default configuration is. When the plugin is enabled, OpenDJ refuses to set `check-references` to `true` if `plugin-type` lacks either of them, and it refuses to enable a plugin configured that way. A plugin already configured that way when the server starts is loaded with a warning, and it does not check references until the types are added. Added plugin types take effect at once, and the plugin does not need to be disabled and enabled again.
opendj-doc-generated-ref/src/main/asciidoc/server-dev-guide/chap-writing-plugins.adoc
@@ -44,7 +44,7 @@
[#about-server-plugins]
=== About OpenDJ Directory Server Plugins
OpenDJ directory server plugins are Java libraries compiled against the OpenDJ link:../javadoc/index.html[Java API, window=\_blank]. Plugins are built to be configured as part of the server and to be invoked at specific points in the lifecycle of a client request, or in the server process lifecycle.
OpenDJ directory server plugins are Java libraries compiled against the OpenDJ link:https://doc.openidentityplatform.org/opendj/apidocs/index.html[Java API, window=\_blank]. Plugins are built to be configured as part of the server and to be invoked at specific points in the lifecycle of a client request, or in the server process lifecycle.
[NOTE]
====
@@ -57,7 +57,7 @@
==== Plugin Types
Plugin types correspond to the points where the server invokes the plugin.
For the full list of plugin invocation points, see the Javadoc for link:../javadoc/index.html?org/opends/server/api/plugin/PluginType.html[PluginType, window=\_blank]. The following list summarizes the plugin invocation points:
For the full list of plugin invocation points, see the Javadoc for link:https://doc.openidentityplatform.org/opendj/apidocs/org/opends/server/api/plugin/PluginType.html[PluginType, window=\_blank]. The following list summarizes the plugin invocation points:
* At server startup and shutdown
@@ -345,7 +345,7 @@
====
`ExamplePlugin` statically imports everything from the generated message implementation sources. Resolution of `ExamplePluginMessages.*` fails until the implementation is generated by the `i18n-maven-plugin`.
`ExamplePlugin` extends link:../javadoc/index.html?org/opends/server/api/plugin/DirectoryServerPlugin.html[DirectoryServerPlugin, window=\_blank] with its own type of configuration, `ExamplePluginCfg`. The implementation for `ExamplePluginCfg` is generated from the configuration declared in XML. Therefore, resolution of `ExamplePluginCfg` fails until the sources are generated by the `opendj-maven-plugin`.
`ExamplePlugin` extends link:https://doc.openidentityplatform.org/opendj/apidocs/org/opends/server/api/plugin/DirectoryServerPlugin.html[DirectoryServerPlugin, window=\_blank] with its own type of configuration, `ExamplePluginCfg`. The implementation for `ExamplePluginCfg` is generated from the configuration declared in XML. Therefore, resolution of `ExamplePluginCfg` fails until the sources are generated by the `opendj-maven-plugin`.
`ExamplePlugin` implements `ConfigurationChangeListener` so the plugin can be notified of changes to its configuration. The plugin can then potentially update its configuration without the need to restart the plugin or OpenDJ directory server.
@@ -373,7 +373,9 @@
In the `applyConfigurationChange()` method the plugin must modify its configuration as necessary. The example plugin can handle configuration changes without further intervention by the administrator. Other plugins might require administrative intervention because changes can be made that can only be taken into account at plugin initialization.
In the example plugin, the method that extends the server's behavior is the `doStartup()` method. Which method is implemented depends on what class the plugin extends. For example, a password validator extending link:../javadoc/index.html?org/opends/server/api/PasswordValidator.html[PasswordValidator, window=\_blank] would implement a `passwordIsAcceptable()` method.
The plugin types are the exception: a plugin receives them only when it is initialized, so the server does not pass a change to `plugin-type` to the running plugin. When `plugin-type` changes on an enabled plugin, the server creates a new instance of the plugin, initializes it for the new plugin types, registers it in place of the running instance, and then calls the `finalizePlugin()` method of the running instance. If `initializePlugin()` refuses the new plugin types, the change fails and the running instance stays registered for the plugin types it had. The server then calls `finalizePlugin()` of the refused instance, so `finalizePlugin()` must release whatever `initializePlugin()` acquired before it failed, such as a change listener. Until `plugin-type` is changed to types the plugin accepts, every later change of the plugin configuration also fails with the same reason, although the other changes are applied to the running instance. State that the plugin keeps in memory does not carry over to the new instance.
In the example plugin, the method that extends the server's behavior is the `doStartup()` method. Which method is implemented depends on what class the plugin extends. For example, a password validator extending link:https://doc.openidentityplatform.org/opendj/apidocs/org/opends/server/api/PasswordValidator.html[PasswordValidator, window=\_blank] would implement a `passwordIsAcceptable()` method.
[#example-plugin-i18n]
opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListener.java
@@ -13,7 +13,7 @@
 *
 * Copyright 2010 Sun Microsystems, Inc.
 * Portions copyright 2011-2016 ForgeRock AS.
 * Portions copyright 2025 3A Systems, LLC.
 * Portions copyright 2025-2026 3A Systems, LLC.
 */
package org.forgerock.opendj.grizzly;
@@ -37,6 +37,7 @@
import org.forgerock.opendj.ldap.spi.LDAPListenerImpl;
import org.forgerock.opendj.ldap.spi.LdapMessages.LdapRequestEnvelope;
import org.forgerock.util.Function;
import org.forgerock.util.Option;
import org.forgerock.util.Options;
import org.glassfish.grizzly.filterchain.FilterChain;
import org.glassfish.grizzly.nio.transport.TCPNIOBindingHandler;
@@ -52,6 +53,13 @@
 * LDAP listener implementation using Grizzly for transport.
 */
public final class GrizzlyLDAPListener implements LDAPListenerImpl {
    /**
     * Grizzly TCP Transport NIO implementation to bind the listener to and to serve its connections with. If
     * {@code null}, the default server transport shared by every listener in the JVM will be used. A transport
     * provided here is not shut down when the listener is closed: its owner remains responsible for it.
     */
    public static final Option<TCPNIOTransport> GRIZZLY_TRANSPORT = Option.of(TCPNIOTransport.class, null);
    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
    private final ReferenceCountedObject<TCPNIOTransport>.Reference transport;
    private final Collection<TCPNIOServerConnection> serverConnections;
@@ -66,7 +74,7 @@
     * @param addresses
     *            The addresses to listen on.
     * @param options
     *            The LDAP listener options.
     *            The LDAP listener options, including the optional {@link #GRIZZLY_TRANSPORT}.
     * @param requestHandlerFactory
     *            The server connection factory which will be used to create server connections.
     * @throws IOException
@@ -76,7 +84,7 @@
            final Function<LDAPClientContext,
                           ReactiveHandler<LDAPClientContext, LdapRequestEnvelope, Stream<Response>>,
                           LdapException> requestHandlerFactory) throws IOException {
        this(addresses, requestHandlerFactory, options, null);
        this(addresses, requestHandlerFactory, options, options.get(GRIZZLY_TRANSPORT));
    }
    /**
opendj-grizzly/src/main/java/org/forgerock/opendj/grizzly/LDAPServerFilter.java
@@ -82,10 +82,10 @@
import com.forgerock.reactive.Action;
import com.forgerock.reactive.Completable;
import com.forgerock.reactive.Consumer;
import com.forgerock.reactive.ReactiveHandler;
import com.forgerock.reactive.Stream;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import org.openidentityplatform.rxjava3.internal.util.BackpressureHelper;
/**
@@ -542,7 +542,19 @@
                    // handleClose() will be invoked once this connection has been closed.
                    connection.closeSilently();
                }
            }).subscribe();
            }).subscribe(new Action() {
                @Override
                public void run() throws Exception {
                    // Nothing to do: the connection is closed on either outcome.
                }
            }, new Consumer<Throwable>() {
                @Override
                public void accept(final Throwable error) throws Exception {
                    // Expected when the client has already closed its end: the notice cannot be delivered,
                    // and the connection is closed anyway.
                    logger.traceException(error);
                }
            });
        }
        private void notifyConnectionClosedRawUnbind(final LdapRequestEnvelope rawUnbindRequest) {
@@ -645,13 +657,7 @@
                    }).thenOnException(new ExceptionHandler<Exception>() {
                        @Override
                        public void handleException(Exception exception) {
                            try {
                                 s.onError(exception);
                            } catch (Throwable t) {
                                if (!(t instanceof OnErrorNotImplementedException)) {
                                    throw t;
                                }
                            }
                            s.onError(exception);
                        }
                    }).thenOnRuntimeException(new RuntimeExceptionHandler() {
                        @Override
opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/ConnectionFactoryTestCase.java
@@ -37,7 +37,9 @@
import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -95,6 +97,8 @@
import com.forgerock.reactive.ServerConnectionFactoryAdapter;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
/**
 * Tests the {@code ConnectionFactory} classes.
 */
@@ -665,6 +669,64 @@
        }
    }
    /**
     * A Notice of Disconnection that cannot be written because the client has already closed its end is an
     * expected outcome: it must not be reported as an unhandled error (issue #1143).
     */
    @SuppressWarnings("unchecked")
    @Test
    public void testDisconnectWithNotificationToClosedClientIsNotReportedAsUnhandledError() throws Exception {
        final CountDownLatch connectLatch = new CountDownLatch(1);
        final AtomicReference<LDAPClientContext> contextHolder = new AtomicReference<>();
        final ServerConnectionFactory<LDAPClientContext, Integer> mockServer =
                mock(ServerConnectionFactory.class);
        when(mockServer.handleAccept(any(LDAPClientContext.class))).thenAnswer(
                new Answer<ServerConnection<Integer>>() {
                    @Override
                    public ServerConnection<Integer> answer(InvocationOnMock invocation) throws Throwable {
                        contextHolder.set((LDAPClientContext) invocation.getArguments()[0]);
                        connectLatch.countDown();
                        return mock(ServerConnection.class);
                    }
                });
        final List<Throwable> unhandledErrors = new CopyOnWriteArrayList<>();
        final io.reactivex.rxjava3.functions.Consumer<? super Throwable> previousErrorHandler =
                RxJavaPlugins.getErrorHandler();
        RxJavaPlugins.setErrorHandler(new io.reactivex.rxjava3.functions.Consumer<Throwable>() {
            @Override
            public void accept(Throwable error) {
                unhandledErrors.add(error);
            }
        });
        LDAPListener listener = new LDAPListener(Collections.singleton(loopbackWithDynamicPort()),
                new ServerConnectionFactoryAdapter(Options.defaultOptions().get(LDAP_DECODE_OPTIONS), mockServer));
        try {
            final InetSocketAddress listenerAddr = listener.getSocketAddresses().iterator().next();
            final Connection client = new LDAPConnectionFactory(listenerAddr.getHostName(),
                    listenerAddr.getPort()).getConnection();
            assertThat(connectLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS)).isTrue();
            final LDAPClientContext context = contextHolder.get();
            // The client leaves first: wait until the server has seen the connection close.
            client.close();
            waitForCondition(new Callable<Boolean>() {
                @Override
                public Boolean call() throws Exception {
                    return context.isClosed();
                }
            });
            // Writing the notice now fails, and does so before disconnect() returns.
            context.disconnect(ResultCode.BUSY, "busy");
            assertThat(unhandledErrors).isEmpty();
        } finally {
            RxJavaPlugins.setErrorHandler(previousErrorHandler);
            listener.close();
        }
    }
    @Test(description = "Test for OPENDJ-1121: Closing a connection after "
            + "closing the connection factory causes NPE")
    public void testFactoryCloseBeforeConnectionClose() throws Exception {
opendj-grizzly/src/test/java/org/forgerock/opendj/grizzly/GrizzlyLDAPListenerTestCase.java
@@ -29,6 +29,7 @@
import static org.mockito.Mockito.mock;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.Arrays;
@@ -72,6 +73,8 @@
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.util.Options;
import org.forgerock.util.promise.PromiseImpl;
import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
import org.glassfish.grizzly.nio.transport.TCPNIOTransportBuilder;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
@@ -281,6 +284,43 @@
    }
    /**
     * A listener given a transport with {@link GrizzlyLDAPListener#GRIZZLY_TRANSPORT} serves its connections with that
     * transport, and leaves it running when it is closed.
     */
    @Test(timeOut = 10000)
    public void testLDAPListenerWithProvidedTransport() throws Exception {
        final TCPNIOTransport transport = TCPNIOTransportBuilder.newInstance().build();
        transport.start();
        try {
            final MockServerConnection serverConnection = new MockServerConnection();
            final Options options = defaultOptions().set(GrizzlyLDAPListener.GRIZZLY_TRANSPORT, transport);
            final LDAPListener listener = new LDAPListener(Collections.singleton(loopbackWithDynamicPort()),
                    new ServerConnectionFactoryAdapter(options.get(LDAP_DECODE_OPTIONS),
                            new MockServerConnectionFactory(serverConnection)),
                    options);
            try {
                final InetSocketAddress addr = listener.firstSocketAddress();
                final Connection connection =
                        new LDAPConnectionFactory(addr.getHostName(), addr.getPort()).getConnection();
                try {
                    final LDAPClientContext context = serverConnection.context.get(10, TimeUnit.SECONDS);
                    final Field field = context.getClass().getDeclaredField("connection");
                    field.setAccessible(true);
                    assertThat(((org.glassfish.grizzly.Connection<?>) field.get(context)).getTransport())
                            .isSameAs(transport);
                } finally {
                    connection.close();
                }
            } finally {
                listener.close();
            }
            assertThat(transport.isStopped()).isFalse();
        } finally {
            transport.shutdownNow();
        }
    }
    /**
     * Tests LDAP listener which attempts to open a connection to a remote
     * offline server at the point when the listener accepts the client
     * connection.
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/BackendIndexConfiguration.xml
@@ -14,6 +14,7 @@
  Copyright 2007-2009 Sun Microsystems, Inc.
  Portions copyright 2014-2016 ForgeRock AS.
  Portions Copyright 2026 3A Systems, LLC.
  ! -->
<adm:managed-object name="backend-index" plural-name="backend-indexes"
  package="org.forgerock.opendj.server.config"
@@ -224,8 +225,9 @@
      Specifies whether contents of the index should be confidential.
    </adm:synopsis>
    <adm:description>
      Setting the flag to true will hash keys for equality type indexes using SHA-1
      and encrypt the list of entries matching a substring key for substring indexes.
      Setting the flag to true will encrypt the list of entries matching each key, for
      every index type of the attribute, and hash the keys of the equality index using SHA-1.
      The keys of the other index types are stored as they are.
    </adm:description>
    <adm:requires-admin-action>
      <adm:other>
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/LDAPConnectionHandlerConfiguration.xml
@@ -88,8 +88,72 @@
  <adm:property-reference name="listen-port" />
  <adm:property-reference name="use-ssl" />
  <adm:property-reference name="ssl-cert-nickname" />
  <adm:property-reference name="use-tcp-keep-alive" />
  <adm:property-reference name="use-tcp-no-delay" />
  <adm:property name="use-tcp-keep-alive" advanced="true">
    <adm:synopsis>
      Indicates whether the
      <adm:user-friendly-name />
      should use TCP keep-alive.
    </adm:synopsis>
    <adm:description>
      If enabled, the SO_KEEPALIVE socket option is used to indicate that TCP
      keepalive messages should periodically be sent to the client to
      verify that the associated connection is still valid. This may
      also help prevent cases in which intermediate network hardware
      could silently drop an otherwise idle client connection, provided
      that the keepalive interval configured in the underlying operating
      system is smaller than the timeout enforced by the network
      hardware.
    </adm:description>
    <adm:requires-admin-action>
      <adm:component-restart />
    </adm:requires-admin-action>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>true</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:boolean />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-use-tcp-keep-alive</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="use-tcp-no-delay" advanced="true">
    <adm:synopsis>
      Indicates whether the
      <adm:user-friendly-name />
      should use TCP no-delay.
    </adm:synopsis>
    <adm:description>
      If enabled, the TCP_NODELAY socket option is used to ensure
      that response messages to the client are sent immediately rather
      than potentially waiting to determine whether additional response
      messages can be sent in the same packet. In most cases, using the
      TCP_NODELAY socket option provides better performance and
      lower response times, but disabling it may help for some cases in
      which the server sends a large number of entries to a client
      in response to a search request.
    </adm:description>
    <adm:requires-admin-action>
      <adm:component-restart />
    </adm:requires-admin-action>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>true</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:boolean />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-use-tcp-no-delay</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property-reference name="allow-tcp-reuse-address" />
  <adm:property name="key-manager-provider">
    <adm:synopsis>
@@ -338,6 +402,9 @@
      each client connection and used to buffer LDAP response messages data
      when writing.
    </adm:description>
    <adm:requires-admin-action>
      <adm:component-restart />
    </adm:requires-admin-action>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>4096 bytes</adm:value>
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/PluginConfiguration.xml
@@ -14,6 +14,7 @@
  Copyright 2007-2010 Sun Microsystems, Inc.
  Portions Copyright 2011 ForgeRock AS.
  Portions Copyright 2026 3A Systems, LLC.
  ! -->
<adm:managed-object name="plugin" plural-name="plugins"
  package="org.forgerock.opendj.server.config"
@@ -72,11 +73,8 @@
  <adm:property name="plugin-type" mandatory="true"
    multi-valued="true">
    <adm:synopsis>
      Specifies the set of plug-in types for the plug-in, which specifies the times at which the plug-in is invoked.
      Specifies the set of plug-in types for the plug-in, which specifies the times at which the plug-in is invoked.
    </adm:synopsis>
    <adm:requires-admin-action>
      <adm:component-restart />
    </adm:requires-admin-action>
    <adm:syntax>
      <adm:enumeration>
        <adm:value name="startup">
opendj-server-legacy/pom.xml
@@ -97,6 +97,11 @@
    <dependency>
      <groupId>org.openidentityplatform.opendj</groupId>
      <artifactId>opendj-grizzly</artifactId>
    </dependency>
    <dependency>
      <groupId>org.openidentityplatform.opendj</groupId>
      <artifactId>opendj-ldap-toolkit</artifactId>
      <version>${project.version}</version>
    </dependency>
opendj-server-legacy/src/main/java/org/forgerock/opendj/reactive/LDAPConnectionHandler2.java
@@ -18,6 +18,7 @@
package org.forgerock.opendj.reactive;
import static java.util.Collections.*;
import static org.opends.messages.CoreMessages.INFO_CONNHANDLER_CLOSED_BY_SHUTDOWN;
import static org.opends.messages.ProtocolMessages.*;
import static org.opends.server.loggers.AccessLogger.logConnect;
import static org.opends.server.util.ServerConstants.*;
@@ -41,9 +42,11 @@
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
@@ -55,6 +58,7 @@
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
import org.forgerock.opendj.grizzly.GrizzlyLDAPListener;
import org.forgerock.opendj.ldap.AddressMask;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.LDAPClientContext;
@@ -69,6 +73,14 @@
import org.forgerock.opendj.server.config.server.LDAPConnectionHandlerCfg;
import org.forgerock.util.Function;
import org.forgerock.util.Options;
import org.glassfish.grizzly.Connection;
import org.glassfish.grizzly.ConnectionProbe;
import org.glassfish.grizzly.memory.MemoryManager;
import org.glassfish.grizzly.memory.PooledMemoryManager;
import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
import org.glassfish.grizzly.nio.transport.TCPNIOTransportBuilder;
import org.glassfish.grizzly.strategies.SameThreadIOStrategy;
import org.glassfish.grizzly.threadpool.ThreadPoolConfig;
import org.glassfish.grizzly.utils.ArrayUtils;
import org.opends.server.api.AlertGenerator;
import org.opends.server.api.ClientConnection;
@@ -125,8 +137,155 @@
        }
    }
    /**
     * Holds the memory manager shared by the transports of every LDAP connection handler. It pre-allocates a share of
     * the heap, so one instance per transport would multiply that share by the number of handlers.
     */
    private static final class MemoryManagerHolder {
        /** Pooled, as in the SDK server transport, so that Grizzly buffers can be used across threads. */
        private static final MemoryManager INSTANCE = new PooledMemoryManager(true);
    }
    /**
     * Tracks the connections a transport has accepted and not yet closed, down to the socket. A connection leaves the
     * set of client connections as soon as it is disconnected, before its notice of disconnection is written, so only
     * this tracking tells when shutting the transport down can no longer drop a write.
     */
    private static final class OpenConnections extends ConnectionProbe.Adapter {
        private final Collection<Connection<?>> open = ConcurrentHashMap.newKeySet();
        @Override
        public void onAcceptEvent(Connection serverConnection, Connection clientConnection) {
            open.add(clientConnection);
        }
        @Override
        public void onCloseEvent(Connection connection) {
            if (open.remove(connection)) {
                synchronized (this) {
                    notifyAll();
                }
            }
        }
        /** Waits until every accepted connection is closed, for at most the given time. */
        private synchronized void awaitClosed(long timeoutMs) throws InterruptedException {
            final long deadlineNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs);
            while (!open.isEmpty()) {
                final long remainingMs = TimeUnit.NANOSECONDS.toMillis(deadlineNanos - System.nanoTime());
                if (remainingMs <= 0) {
                    return;
                }
                wait(remainingMs);
            }
        }
    }
    /**
     * Keeps the transport of a stopped listener running for the connections it has accepted, so that disabling,
     * deleting or restarting the handler leaves them open, as the transport the SDK shares between listeners did. The
     * transport is shut down once the last of them is closed, or when the server shuts down.
     */
    private final class TransportDrain implements ServerShutdownListener {
        private final TCPNIOTransport drained;
        /** The client connections accepted by the drained transport, and only those. */
        private final Collection<ClientConnection> accepted;
        private final OpenConnections open;
        private final AtomicBoolean finished = new AtomicBoolean();
        /** Whether this drain is registered as a shutdown listener, which only {@link #start()} does. */
        private volatile boolean registered;
        private TransportDrain(TCPNIOTransport drained, Collection<ClientConnection> accepted, OpenConnections open) {
            this.drained = drained;
            this.accepted = accepted;
            this.open = open;
        }
        private void start() {
            drains.add(this);
            if (!server.isShuttingDown()) {
                // Registers with the server of this handler: another one is current only once this one shut down.
                registered = true;
                DirectoryServer.registerShutdownListener(this);
                if (finished.get()) {
                    // The last connection closed while this drain registered, and finished it before it was registered.
                    DirectoryServer.deregisterShutdownListener(this);
                }
            }
            if (server.isShuttingDown()) {
                // The server began shutting down before this drain registered, and may have notified its listeners
                // already: end the connections as it would have. Their handler is not finalized again by then.
                processServerShutdown(INFO_CONNHANDLER_CLOSED_BY_SHUTDOWN.get());
            } else {
                connectionClosed();
            }
        }
        /** Shuts the transport down if no connection it has accepted is left. */
        private void connectionClosed() {
            if (accepted.isEmpty() && finish()) {
                // The last connection may be closed on a selector thread of the transport being shut down.
                new DirectoryThread(this::shutdownTransport, getShutdownListenerName()).start();
            }
        }
        @Override
        public String getShutdownListenerName() {
            return "Transport drain of " + handlerName;
        }
        @Override
        public void processServerShutdown(LocalizableMessage reason) {
            if (finish()) {
                // Shutting the transport down closes every connection it accepted: end them as a server shutdown
                // first, as the legacy connection handler does, rather than let them fail as protocol errors.
                for (ClientConnection clientConnection : accepted) {
                    clientConnection.disconnect(DisconnectReason.SERVER_SHUTDOWN, true, reason);
                }
                shutdownTransport();
            }
        }
        private boolean finish() {
            if (!finished.compareAndSet(false, true)) {
                return false;
            }
            drains.remove(this);
            if (registered) {
                // An unregistered drain must not touch the listeners: during an in-core restart they may already
                // belong to the next server instance, which has none yet.
                DirectoryServer.deregisterShutdownListener(this);
            }
            return true;
        }
        private void shutdownTransport() {
            try {
                // A notice of disconnection queued behind a response the client has not read yet is written once the
                // client reads, and a shutdown drops it: give the connections a bounded time to take it and close.
                open.awaitClosed(NOTICE_DELIVERY_TIMEOUT_MS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            try {
                drained.shutdownNow();
            } catch (IOException e) {
                logger.traceException(e);
            }
        }
    }
    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
    /**
     * The system property that sized the transport shared by every listener in the JVM. It is still honoured when the
     * configuration lets the server choose the number of request handlers.
     */
    private static final String SELECTORS_PROPERTY = "org.forgerock.opendj.transport.selectors";
    /** How long a transport being shut down waits for its connections to write what they have queued and close. */
    private static final long NOTICE_DELIVERY_TIMEOUT_MS = 2000;
    /** Default friendly name for the LDAP connection handler. */
    private static final String DEFAULT_FRIENDLY_NAME = "LDAP Connection Handler";
@@ -135,6 +294,21 @@
    private LDAPListener listener;
    /**
     * The transport serving the connections of this connection handler only. It is started with the listener, and
     * handed over to a {@link TransportDrain} when the listener stops.
     */
    private TCPNIOTransport transport;
    /** The client connections accepted by {@link #transport}, handed over with it to a {@link TransportDrain}. */
    private Collection<ClientConnection> transportConnections;
    /** The connections {@link #transport} has accepted and not yet closed, handed over with it. */
    private OpenConnections transportOpenConnections;
    /** The transports of stopped listeners still serving the connections they accepted. */
    private final Collection<TransportDrain> drains = new CopyOnWriteArrayList<>();
    /** The current configuration state. */
    private LDAPConnectionHandlerCfg currentConfig;
@@ -152,9 +326,28 @@
    /** Indicates whether to allow the reuse address socket option. */
    private boolean allowReuseAddress;
    /** Indicates whether to use the SO_KEEPALIVE socket option on client connections. */
    private boolean useTCPKeepAlive;
    /** Indicates whether to use the TCP_NODELAY socket option on client connections. */
    private boolean useTCPNoDelay;
    /** The size in bytes of the write buffer of each client connection. */
    private int bufferSize;
    /** The number of threads reading requests from the client connections. */
    private int numRequestHandlers;
    /** Indicates whether the Directory Server is in the process of shutting down. */
    private volatile boolean shutdownRequested;
    /**
     * The server this handler was initialized for. The listener stops on the thread of the handler, up to a second
     * after the handler is finalized: after an in-core restart the current server instance is by then the next one,
     * so only this one tells whether the server of the connections is shutting down.
     */
    private DirectoryServer server;
    /* Internal LDAP connection handler state */
    /** Indicates whether this connection handler is enabled. */
@@ -280,7 +473,8 @@
        // * ssl policy
        // * ssl cert nickname
        // * accept backlog
        // * tcp reuse address
        // * tcp reuse address, keep alive and no delay
        // * buffer size
        // * num request handler
        // Clear the stat tracker if LDAPv2 is being enabled.
@@ -484,6 +678,7 @@
            friendlyName = config.name();
        }
        server = DirectoryServer.getInstance();
        // Save this configuration for future reference.
        currentConfig = config;
        enabled = config.isEnabled();
@@ -502,6 +697,10 @@
        // Save properties that cannot be dynamically modified.
        allowReuseAddress = config.isAllowTCPReuseAddress();
        backlog = config.getAcceptBacklog();
        useTCPKeepAlive = config.isUseTCPKeepAlive();
        useTCPNoDelay = config.isUseTCPNoDelay();
        bufferSize = (int) config.getBufferSize();
        numRequestHandlers = getNumRequestHandlers(config);
        listenAddresses = new HashSet<>();
        for (InetAddress addr : config.getListenAddress()) {
            listenAddresses.add(new InetSocketAddress(addr, config.getListenPort()));
@@ -561,6 +760,21 @@
        config.addLDAPChangeListener(this);
    }
    /**
     * Returns the number of request handlers set in the configuration, or when the configuration lets the server decide,
     * the value of {@link #SELECTORS_PROPERTY}, or else a number chosen from the number of processors.
     */
    private int getNumRequestHandlers(LDAPConnectionHandlerCfg config) {
        Integer configured = config.getNumRequestHandlers();
        if (configured == null) {
            final Integer selectors = Integer.getInteger(SELECTORS_PROPERTY);
            if (selectors != null && selectors > 0) {
                configured = selectors;
            }
        }
        return getNumRequestHandlers(configured, friendlyName);
    }
    @Override
    public boolean isConfigurationAcceptable(ConnectionHandlerCfg configuration,
            List<LocalizableMessage> unacceptableReasons) {
@@ -640,9 +854,58 @@
            listener = null;
            logger.info(NOTE_CONNHANDLER_STOPPED_LISTENING, handlerName);
        }
        if (transport != null) {
            final TransportDrain drain = new TransportDrain(transport, transportConnections, transportOpenConnections);
            transport = null;
            transportConnections = null;
            transportOpenConnections = null;
            drain.start();
        }
    }
    private void connectionClosed(ClientConnection connection, Collection<ClientConnection> accepted) {
        accepted.remove(connection);
        clientConnections.remove(connection);
        for (TransportDrain drain : drains) {
            drain.connectionClosed();
        }
    }
    /**
     * Creates and starts the transport of this connection handler. Each handler has its own, so that its selector
     * threads and its socket options follow its configuration rather than the JVM-wide settings of the transport the
     * SDK shares between listeners.
     */
    private TCPNIOTransport newTransport(OpenConnections openConnections) throws IOException {
        final TCPNIOTransport newTransport = TCPNIOTransportBuilder.newInstance()
                .setIOStrategy(SameThreadIOStrategy.getInstance())
                .setSelectorThreadPoolConfig(ThreadPoolConfig.defaultConfig()
                                                             .setCorePoolSize(numRequestHandlers)
                                                             .setMaxPoolSize(numRequestHandlers)
                                                             .setPoolName(handlerName + " Request Handler"))
                .setMemoryManager(MemoryManagerHolder.INSTANCE)
                .setReuseAddress(allowReuseAddress)
                .setWriteBufferSize(bufferSize)
                .build();
        // As in the SDK server transport: fewer selector runners than selector threads cause deadlocks.
        newTransport.setSelectorRunnersCount(numRequestHandlers);
        // Before the transport binds: a connection takes the probes of its transport when it is created.
        newTransport.getConnectionMonitoringConfig().addProbes(openConnections);
        try {
            newTransport.start();
        } catch (IOException e) {
            newTransport.shutdownNow();
            throw e;
        }
        return newTransport;
    }
    private void startListener() throws IOException {
        final OpenConnections openConnections = new OpenConnections();
        final Collection<ClientConnection> accepted = ConcurrentHashMap.newKeySet();
        transport = newTransport(openConnections);
        transportConnections = accepted;
        transportOpenConnections = openConnections;
        listener = new LDAPListener(
                listenAddresses,
                new Function<LDAPClientContext,
@@ -653,23 +916,24 @@
                            LDAPClientContext clientContext) throws LdapException {
                        final LDAPClientConnection2 conn = canAccept(clientContext);
                        clientConnections.add(conn);
                        accepted.add(conn);
                        logConnect(conn);
                        clientContext.addListener(new LDAPClientContextEventListener() {
                            @Override
                            public void handleConnectionError(final LDAPClientContext context, final Throwable error) {
                                clientConnections.remove(conn);
                                connectionClosed(conn, accepted);
                            }
                            @Override
                            public void handleConnectionDisconnected(final LDAPClientContext context,
                                    final ResultCode resultCode, String diagnosticMessage) {
                                clientConnections.remove(conn);
                                connectionClosed(conn, accepted);
                            }
                            @Override
                            public void handleConnectionClosed(final LDAPClientContext context,
                                    final UnbindRequest unbindRequest) {
                                clientConnections.remove(conn);
                                connectionClosed(conn, accepted);
                            }
                        });
                        return new ReactiveHandler<LDAPClientContext, LdapRequestEnvelope, Stream<Response>>() {
@@ -682,7 +946,11 @@
                    }
                }, Options.defaultOptions()
                          .set(LDAPListener.CONNECT_MAX_BACKLOG, backlog)
                          .set(LDAPListener.REQUEST_MAX_SIZE_IN_BYTES, (int) currentConfig.getMaxRequestSize()));
                          .set(LDAPListener.REQUEST_MAX_SIZE_IN_BYTES, (int) currentConfig.getMaxRequestSize())
                          // Set by the listener on every accepted connection, over the values of the transport.
                          .set(LDAPListener.SO_KEEPALIVE, useTCPKeepAlive)
                          .set(LDAPListener.TCP_NO_DELAY, useTCPNoDelay)
                          .set(GrizzlyLDAPListener.GRIZZLY_TRANSPORT, transport));
        logger.info(NOTE_CONNHANDLER_STARTED_LISTENING, handlerName);
    }
opendj-server-legacy/src/main/java/org/opends/server/api/plugin/DirectoryServerPlugin.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.api.plugin;
@@ -167,7 +168,11 @@
  /**
   * Performs any necessary finalization for this plugin.  This will
   * be called just after the plugin has been deregistered with the
   * server but before it has been unloaded.
   * server but before it has been unloaded.  It is also called when
   * {@link #initializePlugin} throws, on an instance that was never
   * registered: it must then release whatever
   * {@code initializePlugin} acquired before it failed, and must not
   * assume that it completed.
   */
  public void finalizePlugin()
  {
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/BackendStat.java
@@ -314,6 +314,7 @@
   */
  public static int main(String[] args, OutputStream outStream, OutputStream errStream)
  {
    JDKLogging.disableLogging();
    BackendStat app = new BackendStat(outStream, errStream);
    return app.run(args);
  }
@@ -328,7 +329,6 @@
  {
    this.out = NullOutputStream.wrapOrNullStream(out);
    this.err = NullOutputStream.wrapOrNullStream(err);
    JDKLogging.disableLogging();
    LocalizableMessage toolDescription = INFO_DESCRIPTION_BACKEND_TOOL.get();
    this.parser = new SubCommandArgumentParser(getClass().getName(), toolDescription, false);
@@ -1022,58 +1022,16 @@
    try
    {
      // Create a table of their properties.
      TableBuilder builder = new TableBuilder();
      int count = 0;
      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_NAME.get());
      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_RAW_DB_NAME.get());
      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_STATUS.get());
      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_CONFIDENTIAL.get());
      builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_RECORD_COUNT.get());
      builder.appendHeading(INFO_LABEL_BACKEND_TOOL_INDEX_UNDEFINED_RECORD_COUNT.get());
      builder.appendHeading(LocalizableMessage.raw("95%"));
      builder.appendHeading(LocalizableMessage.raw("90%"));
      builder.appendHeading(LocalizableMessage.raw("85%"));
      EntryContainer ec = rc.getEntryContainer(base);
      if (ec == null)
      {
        return printEntryContainerError(backend, base);
      }
      // Create a table of their properties.
      TableBuilder builder = new TableBuilder();
      Map<Index, StringBuilder> undefinedKeys = new HashMap<>();
      for (AttributeIndex attrIndex : ec.getAttributeIndexes())
      {
        for (AttributeIndex.MatchingRuleIndex index : attrIndex.getNameToIndexes().values())
        {
          builder.startRow();
          builder.appendCell(index.getName().getIndexId());
          builder.appendCell(index.getName());
          builder.appendCell(index.isTrusted());
          builder.appendCell(index.isEncrypted());
          if (index.isTrusted())
          {
            appendIndexStats(builder, ec, index, undefinedKeys);
          }
          else
          {
            appendStatsNoData(builder, 5);
          }
          count++;
        }
      }
      for (VLVIndex vlvIndex : ec.getVLVIndexes())
      {
        builder.startRow();
        builder.appendCell(vlvIndex.getName().getIndexId());
        builder.appendCell(vlvIndex.getName());
        builder.appendCell(vlvIndex.isTrusted());
        builder.appendCell(getTreeRecordCount(ec, vlvIndex));
        appendStatsNoData(builder, 4);
        count++;
      }
      int count = appendIndexStatusTable(builder, ec, undefinedKeys);
      builder.print(new TextTablePrinter(out));
      out.print(INFO_LABEL_BACKEND_TOOL_TOTAL.get(count).toString());
@@ -1091,6 +1049,61 @@
    }
  }
  /**
   * Fills the table {@code show-index-status} prints for the indexes of an entry container, a row
   * per index, and returns the number of its rows. The keys of an index that are over its entry
   * limit are collected into {@code undefinedKeys}.
   */
  int appendIndexStatusTable(TableBuilder builder, EntryContainer ec, Map<Index, StringBuilder> undefinedKeys)
  {
    builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_NAME.get());
    builder.appendHeading(INFO_LABEL_BACKEND_TOOL_RAW_DB_NAME.get());
    builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_STATUS.get());
    builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_INDEX_CONFIDENTIAL.get());
    builder.appendHeading(INFO_LABEL_BACKEND_DEBUG_RECORD_COUNT.get());
    builder.appendHeading(INFO_LABEL_BACKEND_TOOL_INDEX_UNDEFINED_RECORD_COUNT.get());
    for (int percent : NEAR_LIMIT_PERCENTS)
    {
      builder.appendHeading(LocalizableMessage.raw(percent + "%"));
    }
    int count = 0;
    for (AttributeIndex attrIndex : ec.getAttributeIndexes())
    {
      for (AttributeIndex.MatchingRuleIndex index : attrIndex.getNameToIndexes().values())
      {
        builder.startRow();
        builder.appendCell(index.getName().getIndexId());
        builder.appendCell(index.getName());
        builder.appendCell(index.isTrusted());
        builder.appendCell(index.isEncrypted());
        if (index.isTrusted())
        {
          appendIndexStats(builder, ec, index, undefinedKeys);
        }
        else
        {
          appendStatsNoData(builder, 5);
        }
        count++;
      }
    }
    for (VLVIndex vlvIndex : ec.getVLVIndexes())
    {
      builder.startRow();
      builder.appendCell(vlvIndex.getName().getIndexId());
      builder.appendCell(vlvIndex.getName());
      builder.appendCell(vlvIndex.isTrusted());
      // A VLV index has no confidentiality setting, but its row still needs the cell of that column
      appendStatsNoData(builder, 1);
      builder.appendCell(getTreeRecordCount(ec, vlvIndex));
      appendStatsNoData(builder, 4);
      count++;
    }
    return count;
  }
  private void appendStatsNoData(TableBuilder builder, int columns)
  {
    while (columns > 0)
@@ -1101,12 +1114,30 @@
  }
  /**
   * Whether a key holding this many entries has come near the entry limit of its index. An
   * index-entry-limit of 0 is no limit at all, and no key is near it.
   * The percentages of the index entry limit heading the columns of keys near the limit, highest
   * first. Each column counts the keys holding from its percentage of the limit up to the
   * percentage of the column before it.
   */
  static boolean nearLimit(long size, long entryLimit)
  static final int[] NEAR_LIMIT_PERCENTS = { 95, 90, 80 };
  /**
   * The column of {@link #NEAR_LIMIT_PERCENTS} that counts a key holding this many entries, or -1
   * when the key has not come near the entry limit of its index. An index-entry-limit of 0 is no
   * limit at all, and no key is near it.
   */
  static int nearLimitColumn(long size, long entryLimit)
  {
    return entryLimit > 0 && size >= entryLimit * 0.8;
    if (entryLimit > 0)
    {
      for (int column = 0; column < NEAR_LIMIT_PERCENTS.length; column++)
      {
        if (size * 100 >= entryLimit * NEAR_LIMIT_PERCENTS[column])
        {
          return column;
        }
      }
    }
    return -1;
  }
  private void appendIndexStats(final TableBuilder builder, EntryContainer ec, final Index index,
@@ -1121,9 +1152,7 @@
        @Override
        public Void run(ReadableTransaction txn) throws Exception
        {
          long eighty = 0;
          long ninety = 0;
          long ninetyFive = 0;
          long[] nearLimit = new long[NEAR_LIMIT_PERCENTS.length];
          long undefined = 0;
          long count = 0;
          BackendTreeKeyValue keyDecoder = new BackendTreeKeyValue(index);
@@ -1145,20 +1174,10 @@
              if (entryIDSet.isDefined())
              {
                if (nearLimit(entryIDSet.size(), entryLimit))
                int column = nearLimitColumn(entryIDSet.size(), entryLimit);
                if (column >= 0)
                {
                  if (entryIDSet.size() >= entryLimit * 0.95)
                  {
                    ninetyFive++;
                  }
                  else if (entryIDSet.size() >= entryLimit * 0.9)
                  {
                    ninety++;
                  }
                  else
                  {
                    eighty++;
                  }
                  nearLimit[column]++;
                }
              }
              else
@@ -1180,9 +1199,10 @@
          }
          builder.appendCell(count);
          builder.appendCell(undefined);
          builder.appendCell(ninetyFive);
          builder.appendCell(ninety);
          builder.appendCell(eighty);
          for (long keys : nearLimit)
          {
            builder.appendCell(keys);
          }
          return null;
        }
      });
opendj-server-legacy/src/main/java/org/opends/server/core/PluginConfigManager.java
@@ -396,7 +396,18 @@
      {
        plugin.initializeInternal(serverContext, configuration.dn(), pluginTypes,
            configuration.isInvokeForInternalOperations());
        plugin.initializePlugin(pluginTypes, configuration);
        try
        {
          plugin.initializePlugin(pluginTypes, configuration);
        }
        catch (Exception e)
        {
          // The plugin may have registered a change listener or started a
          // thread before it failed, and it is never registered, so nothing
          // else would finalize it.
          finalizePluginQuietly(plugin);
          throw e;
        }
      }
      else
      {
@@ -420,6 +431,24 @@
  }
  /**
   * Finalizes the provided plugin, logging rather than throwing anything its
   * {@code finalizePlugin()} method throws.
   *
   * @param  plugin  The plugin to finalize.
   */
  private void finalizePluginQuietly(DirectoryServerPlugin<?> plugin)
  {
    try
    {
      plugin.finalizePlugin();
    }
    catch (Exception e)
    {
      logger.traceException(e);
    }
  }
  /**
   * Gets the OpenDS plugin type object that corresponds to the configuration
   * counterpart.
   *
@@ -2322,9 +2351,12 @@
      throws CanceledOperationException {
    PluginResult.PreOperation result = null;
    for (int i = 0; i < preOperationAddPlugins.length; i++)
    // Read the array once: it is replaced when a plugin is registered or
    // deregistered, and the index must stay within the one being iterated.
    DirectoryServerPlugin[] plugins = preOperationAddPlugins;
    for (int i = 0; i < plugins.length; i++)
    {
      DirectoryServerPlugin p = preOperationAddPlugins[i];
      DirectoryServerPlugin p = plugins[i];
      if (isInternalOperation(addOperation, p))
      {
        continue;
@@ -2340,18 +2372,18 @@
      }
      catch (Exception e)
      {
        return handlePreOperationException(e, i, preOperationAddPlugins,
        return handlePreOperationException(e, i, plugins,
            addOperation, p);
      }
      if (result == null)
      {
        return handlePreOperationResult(addOperation, i, preOperationAddPlugins,
        return handlePreOperationResult(addOperation, i, plugins,
            p);
      }
      else if (!result.continuePluginProcessing())
      {
        registerSkippedPreOperationPlugins(i, preOperationAddPlugins,
        registerSkippedPreOperationPlugins(i, plugins,
            addOperation);
        return result;
      }
@@ -2381,9 +2413,12 @@
  {
    PluginResult.PreOperation result = null;
    for (int i = 0; i < preOperationBindPlugins.length; i++)
    // Read the array once: it is replaced when a plugin is registered or
    // deregistered, and the index must stay within the one being iterated.
    DirectoryServerPlugin[] plugins = preOperationBindPlugins;
    for (int i = 0; i < plugins.length; i++)
    {
      DirectoryServerPlugin p = preOperationBindPlugins[i];
      DirectoryServerPlugin p = plugins[i];
      if (isInternalOperation(bindOperation, p))
      {
        continue;
@@ -2395,18 +2430,18 @@
      }
      catch (Exception e)
      {
        return handlePreOperationException(e, i, preOperationBindPlugins,
        return handlePreOperationException(e, i, plugins,
            bindOperation, p);
      }
      if (result == null)
      {
        return handlePreOperationResult(bindOperation, i,
            preOperationBindPlugins, p);
            plugins, p);
      }
      else if (!result.continuePluginProcessing())
      {
        registerSkippedPreOperationPlugins(i, preOperationBindPlugins,
        registerSkippedPreOperationPlugins(i, plugins,
            bindOperation);
        return result;
@@ -2439,9 +2474,12 @@
      throws CanceledOperationException {
    PluginResult.PreOperation result = null;
    for (int i = 0; i < preOperationComparePlugins.length; i++)
    // Read the array once: it is replaced when a plugin is registered or
    // deregistered, and the index must stay within the one being iterated.
    DirectoryServerPlugin[] plugins = preOperationComparePlugins;
    for (int i = 0; i < plugins.length; i++)
    {
      DirectoryServerPlugin p = preOperationComparePlugins[i];
      DirectoryServerPlugin p = plugins[i];
      if (isInternalOperation(compareOperation, p))
      {
        continue;
@@ -2457,14 +2495,14 @@
      }
      catch (Exception e)
      {
        return handlePreOperationException(e, i, preOperationComparePlugins,
        return handlePreOperationException(e, i, plugins,
            compareOperation, p);
      }
      if (result == null)
      {
        return handlePreOperationResult(compareOperation, i,
            preOperationComparePlugins, p);
            plugins, p);
      }
      else if (!result.continuePluginProcessing())
      {
@@ -2498,9 +2536,12 @@
      throws CanceledOperationException {
    PluginResult.PreOperation result = null;
    for (int i = 0; i < preOperationDeletePlugins.length; i++)
    // Read the array once: it is replaced when a plugin is registered or
    // deregistered, and the index must stay within the one being iterated.
    DirectoryServerPlugin[] plugins = preOperationDeletePlugins;
    for (int i = 0; i < plugins.length; i++)
    {
      DirectoryServerPlugin p = preOperationDeletePlugins[i];
      DirectoryServerPlugin p = plugins[i];
      if (isInternalOperation(deleteOperation, p))
      {
        continue;
@@ -2516,18 +2557,18 @@
      }
      catch (Exception e)
      {
        return handlePreOperationException(e, i, preOperationDeletePlugins,
        return handlePreOperationException(e, i, plugins,
            deleteOperation, p);
      }
      if (result == null)
      {
        return handlePreOperationResult(deleteOperation, i,
            preOperationDeletePlugins, p);
            plugins, p);
      }
      else if (!result.continuePluginProcessing())
      {
        registerSkippedPreOperationPlugins(i, preOperationDeletePlugins,
        registerSkippedPreOperationPlugins(i, plugins,
            deleteOperation);
        return result;
@@ -2596,9 +2637,12 @@
      throws CanceledOperationException {
    PluginResult.PreOperation result = null;
    for (int i = 0; i < preOperationExtendedPlugins.length; i++)
    // Read the array once: it is replaced when a plugin is registered or
    // deregistered, and the index must stay within the one being iterated.
    DirectoryServerPlugin[] plugins = preOperationExtendedPlugins;
    for (int i = 0; i < plugins.length; i++)
    {
      DirectoryServerPlugin p = preOperationExtendedPlugins[i];
      DirectoryServerPlugin p = plugins[i];
      if (isInternalOperation(extendedOperation, p))
      {
        registerSkippedPreOperationPlugin(p, extendedOperation);
@@ -2615,18 +2659,18 @@
      }
      catch (Exception e)
      {
        return handlePreOperationException(e, i, preOperationExtendedPlugins,
        return handlePreOperationException(e, i, plugins,
            extendedOperation, p);
      }
      if (result == null)
      {
        return handlePreOperationResult(extendedOperation, i,
            preOperationExtendedPlugins, p);
            plugins, p);
      }
      else if (!result.continuePluginProcessing())
      {
        registerSkippedPreOperationPlugins(i, preOperationExtendedPlugins,
        registerSkippedPreOperationPlugins(i, plugins,
            extendedOperation);
        return result;
@@ -2659,9 +2703,12 @@
      throws CanceledOperationException {
    PluginResult.PreOperation result = null;
    for (int i = 0; i < preOperationModifyPlugins.length; i++)
    // Read the array once: it is replaced when a plugin is registered or
    // deregistered, and the index must stay within the one being iterated.
    DirectoryServerPlugin[] plugins = preOperationModifyPlugins;
    for (int i = 0; i < plugins.length; i++)
    {
      DirectoryServerPlugin p = preOperationModifyPlugins[i];
      DirectoryServerPlugin p = plugins[i];
      if (isInternalOperation(modifyOperation, p))
      {
        continue;
@@ -2677,18 +2724,18 @@
      }
      catch (Exception e)
      {
        return handlePreOperationException(e, i, preOperationModifyPlugins,
        return handlePreOperationException(e, i, plugins,
            modifyOperation, p);
      }
      if (result == null)
      {
        return handlePreOperationResult(modifyOperation, i,
            preOperationModifyPlugins, p);
            plugins, p);
      }
      else if (!result.continuePluginProcessing())
      {
        registerSkippedPreOperationPlugins(i, preOperationModifyPlugins,
        registerSkippedPreOperationPlugins(i, plugins,
            modifyOperation);
        return result;
@@ -2721,9 +2768,12 @@
      throws CanceledOperationException {
    PluginResult.PreOperation result = null;
    for (int i = 0; i < preOperationModifyDNPlugins.length; i++)
    // Read the array once: it is replaced when a plugin is registered or
    // deregistered, and the index must stay within the one being iterated.
    DirectoryServerPlugin[] plugins = preOperationModifyDNPlugins;
    for (int i = 0; i < plugins.length; i++)
    {
      DirectoryServerPlugin p = preOperationModifyDNPlugins[i];
      DirectoryServerPlugin p = plugins[i];
      if (isInternalOperation(modifyDNOperation, p))
      {
        continue;
@@ -2739,18 +2789,18 @@
      }
      catch (Exception e)
      {
        return handlePreOperationException(e, i, preOperationModifyDNPlugins,
        return handlePreOperationException(e, i, plugins,
            modifyDNOperation, p);
      }
      if (result == null)
      {
        return handlePreOperationResult(modifyDNOperation, i,
            preOperationModifyDNPlugins, p);
            plugins, p);
      }
      else if (!result.continuePluginProcessing())
      {
        registerSkippedPreOperationPlugins(i, preOperationModifyDNPlugins,
        registerSkippedPreOperationPlugins(i, plugins,
            modifyDNOperation);
        return result;
@@ -2783,9 +2833,12 @@
      throws CanceledOperationException {
    PluginResult.PreOperation result = null;
    for (int i = 0; i < preOperationSearchPlugins.length; i++)
    // Read the array once: it is replaced when a plugin is registered or
    // deregistered, and the index must stay within the one being iterated.
    DirectoryServerPlugin[] plugins = preOperationSearchPlugins;
    for (int i = 0; i < plugins.length; i++)
    {
      DirectoryServerPlugin p = preOperationSearchPlugins[i];
      DirectoryServerPlugin p = plugins[i];
      if (isInternalOperation(searchOperation, p))
      {
        continue;
@@ -2801,18 +2854,18 @@
      }
      catch (Exception e)
      {
        return handlePreOperationException(e, i, preOperationSearchPlugins,
        return handlePreOperationException(e, i, plugins,
            searchOperation, p);
      }
      if (result == null)
      {
        return handlePreOperationResult(searchOperation, i,
            preOperationSearchPlugins, p);
            plugins, p);
      }
      else if (!result.continuePluginProcessing())
      {
        registerSkippedPreOperationPlugins(i, preOperationSearchPlugins,
        registerSkippedPreOperationPlugins(i, plugins,
             searchOperation);
        return result;
@@ -4485,16 +4538,28 @@
    // required.  If the mapper is disabled, then instantiate the class and
    // initialize and register it as an identity mapper.  Also, update the
    // plugin to indicate whether it should be invoked for internal operations.
    // If only the plugin types have changed, then replace the plugin with one
    // initialized for the new types.
    String className = configuration.getJavaClass();
    if (existingPlugin != null)
    {
      // Update the running instance first, so that it has the new value even
      // when it stays in use because a replacement cannot be initialized.
      existingPlugin.setInvokeForInternalOperations(
                          configuration.isInvokeForInternalOperations());
      if (! className.equals(existingPlugin.getClass().getName()))
      {
        ccr.setAdminActionRequired(true);
      }
      existingPlugin.setInvokeForInternalOperations(
                          configuration.isInvokeForInternalOperations());
      else
      {
        HashSet<PluginType> pluginTypes = getPluginTypes(configuration);
        if (!pluginTypes.equals(existingPlugin.getPluginTypes()))
        {
          replacePlugin(configuration, pluginTypes, ccr);
        }
      }
      return ccr;
    }
@@ -4521,6 +4586,61 @@
    return ccr;
  }
  /**
   * Replaces the registered instance of an enabled plugin with a new one initialized for the
   * plugin types of its new configuration. A plugin receives its plugin types, and may refuse
   * them, only when it is initialized, so the registered instance cannot be moved to other types
   * in place. The new instance is initialized first: if that fails, the registered one stays in
   * use for the plugin types it was initialized for. Otherwise the registered instance is
   * deregistered, the new one is registered in its place, and only then is the old one
   * finalized, so that the plugin is missing only for the time the arrays are rewritten, not for
   * the time the old instance takes to finalize, and a failure to finalize does not leave the
   * plugin unregistered.
   *
   * @param configuration
   *          The new configuration of the plugin.
   * @param pluginTypes
   *          The plugin types of the new configuration.
   * @param ccr
   *          The result of the configuration change, which receives the failure, if any.
   */
  private void replacePlugin(PluginCfg configuration, Set<PluginType> pluginTypes,
                             ConfigChangeResult ccr)
  {
    DirectoryServerPlugin<? extends PluginCfg> plugin;
    try
    {
      plugin = loadPlugin(configuration.getJavaClass(), pluginTypes, configuration, true);
    }
    catch (InitializationException ie)
    {
      ccr.setResultCodeIfSuccess(serverContext.getCoreConfigManager().getServerErrorResultCode());
      ccr.addMessage(ie.getMessageObject());
      return;
    }
    DirectoryServerPlugin<? extends PluginCfg> oldPlugin;
    pluginLock.lock();
    try
    {
      oldPlugin = registeredPlugins.remove(configuration.dn());
      if (oldPlugin != null)
      {
        deregisterPlugin0(oldPlugin);
      }
      registerPlugin(plugin, configuration.dn(), pluginTypes);
    }
    finally
    {
      pluginLock.unlock();
    }
    if (oldPlugin != null)
    {
      finalizePluginQuietly(oldPlugin);
    }
  }
  private HashSet<PluginType> getPluginTypes(PluginCfg configuration)
  {
    HashSet<PluginType> pluginTypes = new HashSet<>();
opendj-server-legacy/src/main/java/org/opends/server/plugins/PasswordPolicyImportPlugin.java
@@ -74,6 +74,8 @@
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  /** The configuration which this plugin is registered with as a change listener. */
  private PasswordPolicyImportPluginCfg currentConfig;
  /** The attribute type used to specify the password policy for an entry. */
  private AttributeType customPolicyAttribute;
  /** The set of attribute types defined in the schema with the auth password syntax. */
@@ -107,6 +109,7 @@
         throws ConfigException
  {
    configuration.addPasswordPolicyImportChangeListener(this);
    currentConfig = configuration;
    Schema schema = DirectoryServer.getInstance().getServerContext().getSchema();
    customPolicyAttribute = schema.getAttributeType(OP_ATTR_PWPOLICY_POLICY_DN);
@@ -224,6 +227,12 @@
  }
  @Override
  public final void finalizePlugin()
  {
    currentConfig.removePasswordPolicyImportChangeListener(this);
  }
  @Override
  public void processImportBegin(LocalBackend<?> backend, LDIFImportConfig config)
  {
    // Find the set of attribute types with the auth password and user password
opendj-server-legacy/src/main/java/org/opends/server/plugins/ReferentialIntegrityPlugin.java
@@ -170,7 +170,6 @@
                                     ReferentialIntegrityPluginCfg pluginCfg)
         throws ConfigException
  {
    pluginCfg.addReferentialIntegrityChangeListener(this);
    LinkedList<LocalizableMessage> unacceptableReasons = new LinkedList<>();
    if (!isConfigurationAcceptableIgnoringCheckReferencesPluginTypes(pluginCfg, unacceptableReasons))
@@ -178,6 +177,10 @@
      throw new ConfigException(unacceptableReasons.getFirst());
    }
    // Only after the check: finalizePlugin() cannot remove a listener registered by a refused
    // configuration, because the configuration it removes it from is set in applyConfigurationChange().
    pluginCfg.addReferentialIntegrityChangeListener(this);
    // Enabling the plugin or changing its configuration is refused without these types, but a configuration
    // already stored without them (the entry shipped before issue #1118) is loaded with a warning: refusing
    // it would also stop the delete and modify DN clean-up, which does not need them.
@@ -928,6 +931,11 @@
  @Override
  public final void finalizePlugin() {
    if (currentConfiguration == null)
    {
      // initializePlugin() refused the configuration before registering anything.
      return;
    }
    currentConfiguration.removeReferentialIntegrityChangeListener(this);
    if(interval > 0)
    {
opendj-server-legacy/src/main/java/org/opends/server/plugins/SambaPasswordPlugin.java
@@ -865,6 +865,16 @@
    this.config = configuration;
  }
  @Override
  public void finalizePlugin()
  {
    // Null when initializePlugin() refused the configuration before registering the listener.
    if (config != null)
    {
      config.removeSambaPasswordChangeListener(this);
    }
  }
  /**
opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/UpgradeCli.java
@@ -218,7 +218,7 @@
                      .buildArgument();
      force =
              BooleanArgument.builder(OPTION_LONG_FORCE_UPGRADE)
                      .description(INFO_UPGRADE_OPTION_FORCE.get(OPTION_LONG_NO_PROMPT))
                      .description(INFO_UPGRADE_OPTION_FORCE.get(OPTION_LONG_ACCEPT_LICENSE, OPTION_LONG_NO_PROMPT))
                      .buildArgument();
      acceptLicense = acceptLicenseArgument();
      showUsageArgument = showUsageArgument();
opendj-server-legacy/src/messages/org/opends/messages/plugin.properties
@@ -357,10 +357,9 @@
ERR_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE_131=The property \
 'check-references' is set to true, but the property 'plugin-type' does not list \
 '%s', so the references added by that operation would not be checked. Add '%s' to \
 'plugin-type', then disable and re-enable the plugin, or set 'check-references' to false
 'plugin-type', or set 'check-references' to false
WARN_PLUGIN_REFERENT_CHECK_REFERENCES_WITHOUT_PLUGIN_TYPE_132=The Referential \
 Integrity plugin %s has 'check-references' set to true, but its property \
 'plugin-type' does not list '%s', so the references added by that operation are \
 not checked. The plugin is loaded and still removes the references to deleted and \
 renamed entries. Add '%s' to 'plugin-type', then disable and re-enable the plugin, \
 or set 'check-references' to false
 renamed entries. Add '%s' to 'plugin-type', or set 'check-references' to false
opendj-server-legacy/src/messages/org/opends/messages/tool.properties
@@ -2326,7 +2326,9 @@
known in advance and resolved after the upgrade has completed
INFO_UPGRADE_OPTION_FORCE_1741=Forces a non-interactive upgrade to continue even if it requires user \
interaction. In particular, long running or critical upgrade tasks, such as re-indexing, which \
require user confirmation will be skipped. This option may only be used with the '%s' option
require user confirmation will be performed. This option does not accept the license: if the \
upgrade requires accepting a license, use the '%s' option. This option may only be used with the \
'%s' option
INFO_UPGRADE_DESCRIPTION_CLI_1743=Upgrades OpenDJ configuration and application data so that it is \
compatible with the installed binaries.%n%nThis tool should be run immediately after upgrading \
the OpenDJ binaries and before restarting the server.%n%nNOTE: this tool does not provide backup \
@@ -2613,9 +2615,7 @@
REF_SHORT_DESC_BACKEND_TOOL_15031=gather OpenDJ backend debugging information
# Supplements to descriptions for generated reference documentation.
SUPPLEMENT_DESCRIPTION_PSEARCH_INFO_20002=<xinclude:include href="description-psearch-info.xml" />
SUPPLEMENT_DESCRIPTION_CONTROLS_20003=<xinclude:include href="variablelist-ldap-controls.xml" />
SUPPLEMENT_DESCRIPTION_UPGRADE_CLI_20004=<xinclude:include href="description-upgrade.xml" />
SUPPLEMENT_DESCRIPTION_UPGRADE_CLI_20004=include::./_description-upgrade.adoc[]
INFO_ARGUMENT_DESCRIPTION_TESTONLY_20005=Just verify that the JVM can be \
 started properly
INFO_INSTALLDS_BACKEND_TYPE_PLACEHOLDER_20006={backendType}
@@ -2632,7 +2632,7 @@
ERR_SEARCH_INVALID_DEREFERENCE_POLICY_20014=Invalid deref alias specified: %s
ERR_FILE_NOT_FULLY_READABLE_20015=Could not completely read file '%s'
SUPPLEMENT_DESCRIPTION_BACKEND_TOOL_SUBCMD_LIST_INDEX_STATUS_20016=\
  <xinclude:include href="variablelist-backendstat-index-status.xml" />
  include::./_variablelist-backendstat-index-status.adoc[]
INFO_DESCRIPTION_DEFAULT_ADD_20017=Legacy argument for ForgeRock OpenDJ compatibility.
WARN_CONFIGDS_KEY_WRAPPING_TRANSFORMATION_UNSUPPORTED_20018=This Java runtime \
 supports neither the default key wrapping transformation %s nor an alternative \
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/BackendStatTest.java
@@ -15,7 +15,11 @@
 */
package org.opends.server.backends.pluggable;
import static com.forgerock.opendj.cli.ArgumentParser.PROPERTY_SCRIPT_NAME;
import static org.assertj.core.api.Assertions.*;
import static org.opends.server.backends.pluggable.BackendStat.*;
import java.io.ByteArrayOutputStream;
import org.opends.server.DirectoryServerTestCase;
import org.testng.annotations.Test;
@@ -29,15 +33,90 @@
  @Test
  public void testAKeyIsNearItsLimitFromEightyPercentOn()
  {
    assertThat(BackendStat.nearLimit(79, 100)).isFalse();
    assertThat(BackendStat.nearLimit(80, 100)).isTrue();
    assertThat(nearLimitColumn(79, 100)).isEqualTo(-1);
    assertThat(nearLimitColumn(80, 100)).isNotEqualTo(-1);
  }
  /** An index-entry-limit of 0 is no limit at all, and no key is near it (#1059). */
  @Test
  public void testNoKeyIsNearNoLimit()
  {
    assertThat(BackendStat.nearLimit(1, 0)).isFalse();
    assertThat(BackendStat.nearLimit(Integer.MAX_VALUE, 0)).isFalse();
    assertThat(nearLimitColumn(1, 0)).isEqualTo(-1);
    assertThat(nearLimitColumn(Integer.MAX_VALUE, 0)).isEqualTo(-1);
  }
  /** The columns are headed 95%, 90% and 80%: the last one counts keys from 80% of the limit (#1135). */
  @Test
  public void testTheColumnsAreHeadedByTheirThresholds()
  {
    assertThat(NEAR_LIMIT_PERCENTS).containsExactly(95, 90, 80);
    assertThat(NEAR_LIMIT_PERCENTS[nearLimitColumn(80, 100)]).isEqualTo(80);
    assertThat(NEAR_LIMIT_PERCENTS[nearLimitColumn(89, 100)]).isEqualTo(80);
    assertThat(NEAR_LIMIT_PERCENTS[nearLimitColumn(90, 100)]).isEqualTo(90);
    assertThat(NEAR_LIMIT_PERCENTS[nearLimitColumn(94, 100)]).isEqualTo(90);
    assertThat(NEAR_LIMIT_PERCENTS[nearLimitColumn(95, 100)]).isEqualTo(95);
    assertThat(NEAR_LIMIT_PERCENTS[nearLimitColumn(100, 100)]).isEqualTo(95);
  }
  /**
   * Every key a column counts holds at least the percentage of the limit that heads the column, and
   * less than the percentage heading the column before it.
   */
  @Test
  public void testEachColumnCountsTheKeysFromItsHeadingUpToThePreviousOne()
  {
    for (long entryLimit : new long[] { 1, 7, 100, 4000, 4001 })
    {
      for (long size = 0; size <= entryLimit; size++)
      {
        int column = nearLimitColumn(size, entryLimit);
        boolean nearLimit = size * 100 >= entryLimit * NEAR_LIMIT_PERCENTS[NEAR_LIMIT_PERCENTS.length - 1];
        assertThat(column >= 0).as("size %d of %d", size, entryLimit).isEqualTo(nearLimit);
        if (column >= 0)
        {
          assertThat(size * 100).as("size %d of %d", size, entryLimit)
              .isGreaterThanOrEqualTo(entryLimit * NEAR_LIMIT_PERCENTS[column]);
          if (column > 0)
          {
            assertThat(size * 100).as("size %d of %d", size, entryLimit)
                .isLessThan(entryLimit * NEAR_LIMIT_PERCENTS[column - 1]);
          }
        }
      }
    }
  }
  /** The generated reference of show-index-status includes its AsciiDoc description of the columns (#1128). */
  @Test
  public void testGenerateDocIncludesTheIndexStatusSupplement() throws Exception
  {
    final String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
    System.setProperty("org.forgerock.opendj.gendoc", "true");
    System.setProperty(PROPERTY_SCRIPT_NAME, "backendstat");
    final ByteArrayOutputStream out = new ByteArrayOutputStream();
    try
    {
      assertThat(BackendStat.main(new String[] { "-?" }, out, System.err)).isEqualTo(0);
    }
    finally
    {
      System.clearProperty("org.forgerock.opendj.gendoc");
      restoreProperty(PROPERTY_SCRIPT_NAME, scriptName);
    }
    assertThat(out.toString("UTF-8"))
        .contains("include::./_variablelist-backendstat-index-status.adoc[]")
        .doesNotContain("<xinclude:include");
  }
  private static void restoreProperty(String name, String value)
  {
    if (value != null)
    {
      System.setProperty(name, value);
    }
    else
    {
      System.clearProperty(name);
    }
  }
}
opendj-server-legacy/src/test/java/org/opends/server/backends/pluggable/PluggableBackendImplTestCase.java
@@ -18,6 +18,8 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.forgerock.opendj.ldap.ModificationType.*;
import static org.opends.messages.ToolMessages.INFO_LABEL_BACKEND_DEBUG_INDEX_CONFIDENTIAL;
import static org.opends.messages.ToolMessages.INFO_LABEL_BACKEND_DEBUG_RECORD_COUNT;
import static org.mockito.Mockito.*;
import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection;
import static org.opends.server.protocols.internal.Requests.newSearchRequest;
@@ -39,6 +41,9 @@
import java.util.concurrent.atomic.AtomicBoolean;
import com.google.common.io.Resources;
import com.forgerock.opendj.cli.TableBuilder;
import com.forgerock.opendj.cli.TablePrinter;
import com.forgerock.opendj.cli.TableSerializer;
import org.forgerock.opendj.ldap.*;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.ldap.schema.CoreSchema;
@@ -1178,6 +1183,88 @@
    assertThat(backend.verifyBackend(config)).isEqualTo(0);
  }
  /**
   * Every row {@code backendstat show-index-status} prints has a cell under each of its headings.
   * A VLV index, which has no confidentiality, shows {@code -} under Confidential, so that its
   * record count lands under Record Count; the key counts that follow are {@code -} as well.
   */
  @Test
  public void testShowIndexStatusPutsEachCellOfAVlvRowUnderItsHeading() throws Exception
  {
    final EntryContainer ec = backend.getRootContainer().getEntryContainer(testBaseDN);
    final VLVIndex vlvIndex = ec.getVLVIndexes().iterator().next();
    final long recordCount = backend.getRootContainer().getStorage().read(new ReadOperation<Long>()
    {
      @Override
      public Long run(ReadableTransaction txn) throws Exception
      {
        return vlvIndex.getRecordCount(txn);
      }
    });
    final ByteArrayOutputStream err = new ByteArrayOutputStream();
    final TableBuilder builder = new TableBuilder();
    final int count =
        new BackendStat(null, err).appendIndexStatusTable(builder, ec, new HashMap<Index, StringBuilder>());
    final CapturedTable table = new CapturedTable();
    builder.print(table);
    assertThat(err.toString()).as("errors").isEmpty();
    assertThat(table.rows).hasSize(count);
    for (List<String> row : table.rows)
    {
      assertThat(row).as("the cells of " + row.get(0) + " under " + table.headings).doesNotContain("");
    }
    List<String> vlvRow = null;
    for (List<String> row : table.rows)
    {
      if (row.get(0).equals(vlvIndex.getName().getIndexId()))
      {
        vlvRow = row;
      }
    }
    assertThat(vlvRow).as("the row of " + vlvIndex.getName()).isNotNull();
    final int confidential = table.headings.indexOf(INFO_LABEL_BACKEND_DEBUG_INDEX_CONFIDENTIAL.get().toString());
    final int recordCountColumn = table.headings.indexOf(INFO_LABEL_BACKEND_DEBUG_RECORD_COUNT.get().toString());
    assertThat(vlvRow.get(confidential)).as(table.headings.get(confidential)).isEqualTo("-");
    assertThat(vlvRow.get(recordCountColumn)).as(table.headings.get(recordCountColumn))
        .isEqualTo(String.valueOf(recordCount));
    assertThat(vlvRow.subList(recordCountColumn + 1, vlvRow.size())).containsExactly("-", "-", "-", "-");
  }
  /** The headings and the cells of a table, as a table printer is handed them. */
  private static final class CapturedTable extends TablePrinter
  {
    private final List<String> headings = new ArrayList<>();
    private final List<List<String>> rows = new ArrayList<>();
    @Override
    protected TableSerializer getSerializer()
    {
      return new TableSerializer()
      {
        @Override
        public void addHeading(String s)
        {
          headings.add(s);
        }
        @Override
        public void startRow()
        {
          rows.add(new ArrayList<String>());
        }
        @Override
        public void addCell(String s)
        {
          rows.get(rows.size() - 1).add(s);
        }
      };
    }
  }
  @Test
  public void testRebuildDegradedIndex() throws Exception
  {
opendj-server-legacy/src/test/java/org/opends/server/core/PluginConfigManagerTestCase.java
@@ -13,20 +13,30 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.core;
import java.util.ArrayList;
import java.util.EnumSet;
import org.opends.server.TestCaseUtils;
import org.opends.server.api.plugin.DirectoryServerPlugin;
import org.opends.server.api.plugin.PluginType;
import org.opends.server.plugins.OtherPluginTypeTrackingPlugin;
import org.opends.server.plugins.PluginTypeTrackingPlugin;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.forgerock.opendj.ldap.ModificationType.*;
import static org.forgerock.opendj.ldap.requests.Requests.*;
import static org.opends.server.api.plugin.PluginType.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.opends.server.util.ServerConstants.*;
import static org.testng.Assert.*;
@@ -636,5 +646,222 @@
               EOL + "Expected order:  " + expectedOrder + EOL +
               "Actual order:    " + actualOrder);
  }
}
  /**
   * A change to the plugin types of an enabled plugin takes effect at once: the plugin is
   * re-created for the new types, and the instance registered for the old ones is finalized once
   * the new one is registered in its place.
   */
  @Test
  public void testPluginTypeChangeAppliesToEnabledPlugin() throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    DN pluginDN = addTrackingPlugin();
    try
    {
      PluginTypeTrackingPlugin original = getTrackingPlugin(pluginDN);
      int changeListeners = countChangeListeners(pluginDN);
      assertEquals(modifyTestEntryAndCountPreOperation(), 0);
      assertEquals(setPluginTypes(pluginDN, "postOperationModify", "preOperationModify"), ResultCode.SUCCESS);
      PluginTypeTrackingPlugin replacement = getTrackingPlugin(pluginDN);
      assertNotSame(replacement, original);
      assertTrue(original.isFinalized(), "The instance registered for the old plugin types is not finalized");
      assertSame(original.getRegisteredWhenFinalized(), replacement,
          "The old instance was finalized before the new one was registered in its place");
      assertFalse(replacement.isFinalized());
      assertEquals(replacement.getPluginTypes(), EnumSet.of(POST_OPERATION_MODIFY, PRE_OPERATION_MODIFY));
      assertEquals(countChangeListeners(pluginDN), changeListeners);
      assertEquals(modifyTestEntryAndCountPreOperation(), 1);
      // A change that leaves the plugin types alone keeps the running instance.
      assertEquals(setInvokeForInternalOperations(pluginDN, false), ResultCode.SUCCESS);
      assertSame(getTrackingPlugin(pluginDN), replacement);
      assertFalse(replacement.isFinalized());
      assertFalse(replacement.invokeForInternalOperations());
      assertEquals(setInvokeForInternalOperations(pluginDN, true), ResultCode.SUCCESS);
      assertSame(getTrackingPlugin(pluginDN), replacement);
      assertEquals(setPluginTypes(pluginDN, "postOperationModify"), ResultCode.SUCCESS);
      assertEquals(getTrackingPlugin(pluginDN).getPluginTypes(), EnumSet.of(POST_OPERATION_MODIFY));
      assertTrue(replacement.isFinalized());
      assertEquals(countChangeListeners(pluginDN), changeListeners);
      assertEquals(PluginTypeTrackingPlugin.takeChangesSeenWhenFinalized(), 0,
          "A finalized instance received a change of its configuration entry");
      assertEquals(modifyTestEntryAndCountPreOperation(), 0);
    }
    finally
    {
      TestCaseUtils.deleteEntry(pluginDN);
    }
  }
  /**
   * When the plugin cannot be initialized for the new plugin types, the change fails and the
   * instance registered for the old plugin types stays registered and in use. The rejected
   * instance leaves nothing behind, and later changes of the entry still reach the running
   * instance.
   */
  @Test
  public void testPluginTypeChangeRefusedByPluginKeepsRunningPlugin() throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    DN pluginDN = addTrackingPlugin();
    try
    {
      assertEquals(setPluginTypes(pluginDN, "postOperationModify", "preOperationModify"), ResultCode.SUCCESS);
      PluginTypeTrackingPlugin running = getTrackingPlugin(pluginDN);
      int changeListeners = countChangeListeners(pluginDN);
      ResultCode resultCode = setPluginTypes(pluginDN, "postOperationModify", "preOperationDelete");
      assertNotEquals(resultCode, ResultCode.SUCCESS);
      assertSame(getTrackingPlugin(pluginDN), running);
      assertFalse(running.isFinalized());
      assertEquals(running.getPluginTypes(), EnumSet.of(POST_OPERATION_MODIFY, PRE_OPERATION_MODIFY));
      assertEquals(countChangeListeners(pluginDN), changeListeners,
          "The instance which refused the new plugin types is still listening to the entry");
      assertEquals(modifyTestEntryAndCountPreOperation(), 1);
      // The stored plugin types are still the refused ones, so the plugin is re-created again, and
      // refuses again, but the other properties are applied to the running instance.
      resultCode = setInvokeForInternalOperations(pluginDN, false);
      assertNotEquals(resultCode, ResultCode.SUCCESS);
      assertSame(getTrackingPlugin(pluginDN), running);
      assertFalse(running.invokeForInternalOperations());
      assertEquals(countChangeListeners(pluginDN), changeListeners);
    }
    finally
    {
      TestCaseUtils.deleteEntry(pluginDN);
    }
  }
  /**
   * When the instance registered for the old plugin types fails to finalize, the new instance is
   * already registered in its place and stays in use.
   */
  @Test
  public void testPluginTypeChangeWhenOldInstanceFailsToFinalize() throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    DN pluginDN = addTrackingPlugin();
    try
    {
      PluginTypeTrackingPlugin original = getTrackingPlugin(pluginDN);
      PluginTypeTrackingPlugin.setFailToFinalize(true);
      ResultCode resultCode;
      try
      {
        resultCode = setPluginTypes(pluginDN, "postOperationModify", "preOperationModify");
      }
      finally
      {
        PluginTypeTrackingPlugin.setFailToFinalize(false);
      }
      assertEquals(resultCode, ResultCode.SUCCESS);
      assertTrue(original.isFinalized());
      PluginTypeTrackingPlugin replacement = getTrackingPlugin(pluginDN);
      assertNotSame(replacement, original);
      assertEquals(replacement.getPluginTypes(), EnumSet.of(POST_OPERATION_MODIFY, PRE_OPERATION_MODIFY));
      assertEquals(modifyTestEntryAndCountPreOperation(), 1);
    }
    finally
    {
      TestCaseUtils.deleteEntry(pluginDN);
    }
  }
  /**
   * A change of the Java class needs administrative action, so a change of the plugin types which
   * comes with it is not applied: the running instance stays registered for its plugin types.
   */
  @Test
  public void testPluginTypeChangeWithJavaClassChangeKeepsRunningPlugin() throws Exception
  {
    TestCaseUtils.initializeTestBackend(true);
    DN pluginDN = addTrackingPlugin();
    try
    {
      PluginTypeTrackingPlugin original = getTrackingPlugin(pluginDN);
      ModifyRequest request = newModifyRequest(pluginDN)
          .addModification(REPLACE, "ds-cfg-java-class", OtherPluginTypeTrackingPlugin.class.getName())
          .addModification(REPLACE, "ds-cfg-plugin-type", "postOperationModify", "preOperationModify");
      assertEquals(getRootConnection().processModify(request).getResultCode(), ResultCode.SUCCESS);
      assertSame(getTrackingPlugin(pluginDN), original);
      assertFalse(original.isFinalized());
      assertEquals(original.getPluginTypes(), EnumSet.of(POST_OPERATION_MODIFY));
      assertEquals(modifyTestEntryAndCountPreOperation(), 0);
    }
    finally
    {
      TestCaseUtils.deleteEntry(pluginDN);
    }
  }
  private static DN addTrackingPlugin() throws Exception
  {
    DN pluginDN = DN.valueOf("cn=Plugin Type Tracking Plugin,cn=Plugins,cn=config");
    TestCaseUtils.addEntry(
        "dn: " + pluginDN,
        "objectClass: top",
        "objectClass: ds-cfg-plugin",
        "cn: Plugin Type Tracking Plugin",
        "ds-cfg-java-class: " + PluginTypeTrackingPlugin.class.getName(),
        "ds-cfg-enabled: true",
        "ds-cfg-plugin-type: postOperationModify",
        "ds-cfg-invoke-for-internal-operations: true");
    PluginTypeTrackingPlugin.takeChangesSeenWhenFinalized();
    return pluginDN;
  }
  private static PluginTypeTrackingPlugin getTrackingPlugin(DN pluginDN)
  {
    DirectoryServerPlugin<?> plugin = DirectoryServer.getPluginConfigManager().getRegisteredPlugin(pluginDN);
    assertNotNull(plugin, "The " + pluginDN + " plugin is not registered with the server");
    return (PluginTypeTrackingPlugin) plugin;
  }
  private static int countChangeListeners(DN pluginDN)
  {
    return TestCaseUtils.getServerContext().getConfigurationHandler().getChangeListeners(pluginDN).size();
  }
  private static ResultCode setPluginTypes(DN pluginDN, String... pluginTypes)
  {
    ModifyRequest request = newModifyRequest(pluginDN).addModification(REPLACE, "ds-cfg-plugin-type", pluginTypes);
    return getRootConnection().processModify(request).getResultCode();
  }
  private static ResultCode setInvokeForInternalOperations(DN pluginDN, boolean invoke)
  {
    ModifyRequest request = newModifyRequest(pluginDN)
        .addModification(REPLACE, "ds-cfg-invoke-for-internal-operations", String.valueOf(invoke));
    return getRootConnection().processModify(request).getResultCode();
  }
  /**
   * Modifies the test entry and returns how many times the tracking plugin was invoked before it.
   * The plugin always has the post-operation modify type, so it must be invoked once after it.
   */
  private static int modifyTestEntryAndCountPreOperation() throws Exception
  {
    DN testEntryDN = DN.valueOf(TestCaseUtils.TEST_ROOT_DN_STRING);
    PluginTypeTrackingPlugin.takePreOperationModifyCount(testEntryDN);
    PluginTypeTrackingPlugin.takePostOperationModifyCount(testEntryDN);
    ModifyRequest request = newModifyRequest(testEntryDN).addModification(REPLACE, "description", "modified");
    assertEquals(getRootConnection().processModify(request).getResultCode(), ResultCode.SUCCESS);
    assertEquals(PluginTypeTrackingPlugin.takePostOperationModifyCount(testEntryDN), 1,
        "The tracking plugin was not invoked once after the modify of the test entry");
    return PluginTypeTrackingPlugin.takePreOperationModifyCount(testEntryDN);
  }
}
opendj-server-legacy/src/test/java/org/opends/server/plugins/OtherPluginTypeTrackingPlugin.java
New file
@@ -0,0 +1,24 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.plugins;
/**
 * A {@link PluginTypeTrackingPlugin} of another class, so that a test can change the Java class of
 * a plugin to one which the server can load.
 */
public class OtherPluginTypeTrackingPlugin extends PluginTypeTrackingPlugin
{
}
opendj-server-legacy/src/test/java/org/opends/server/plugins/PasswordPolicyImportPluginTestCase.java
@@ -13,13 +13,21 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.plugins;
import static org.forgerock.opendj.ldap.ModificationType.*;
import static org.forgerock.opendj.ldap.requests.Requests.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.testng.Assert.*;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.List;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.opends.server.TestCaseUtils;
import org.forgerock.opendj.server.config.meta.PasswordPolicyImportPluginCfgDefn;
import org.opends.server.api.plugin.PluginType;
@@ -291,5 +299,38 @@
      plugin.doLDIFImport(importConfig, e);
    }
  }
  /**
   * Disabling the plugin finalizes it, and the finalized instance no longer listens to the
   * configuration entry, so enabling the plugin again adds no listener.
   */
  @Test
  public void testDisableAndEnableLeavesNoChangeListenerBehind()
         throws Exception
  {
    DN dn = DN.valueOf("cn=Password Policy Import,cn=plugins,cn=config");
    int changeListeners = countChangeListeners(dn);
    try
    {
      assertEquals(setEnabled(dn, false), ResultCode.SUCCESS);
    }
    finally
    {
      assertEquals(setEnabled(dn, true), ResultCode.SUCCESS);
    }
    assertNotNull(DirectoryServer.getPluginConfigManager().getRegisteredPlugin(dn));
    assertEquals(countChangeListeners(dn), changeListeners);
  }
  private static int countChangeListeners(DN dn)
  {
    return TestCaseUtils.getServerContext().getConfigurationHandler().getChangeListeners(dn).size();
  }
  private static ResultCode setEnabled(DN dn, boolean enabled)
  {
    ModifyRequest request = newModifyRequest(dn).addModification(REPLACE, "ds-cfg-enabled", String.valueOf(enabled));
    return getRootConnection().processModify(request).getResultCode();
  }
}
opendj-server-legacy/src/test/java/org/opends/server/plugins/PluginTypeTrackingPlugin.java
New file
@@ -0,0 +1,193 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.plugins;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.server.config.server.PluginCfg;
import org.opends.server.api.plugin.DirectoryServerPlugin;
import org.opends.server.api.plugin.PluginResult;
import org.opends.server.api.plugin.PluginType;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.operation.PostOperationModifyOperation;
import org.opends.server.types.operation.PreOperationModifyOperation;
/**
 * A plugin which records the modify operations it is invoked for, per target entry, and whether
 * it has been finalized. It refuses {@link #REFUSED_TYPE} in {@link #initializePlugin} only, the
 * way a plugin which checks its plugin types there and not in
 * {@link #isConfigurationAcceptable} does: such a configuration passes the acceptance phase.
 * Like most plugins, it registers a change listener on its configuration before it checks its
 * plugin types, and removes it in {@link #finalizePlugin()}.
 */
public class PluginTypeTrackingPlugin extends DirectoryServerPlugin<PluginCfg>
{
  /** The plugin type which {@link #initializePlugin} refuses. */
  public static final PluginType REFUSED_TYPE = PluginType.PRE_OPERATION_DELETE;
  private static final ConcurrentHashMap<DN, AtomicInteger> PRE_OPERATION_MODIFY_COUNTS = new ConcurrentHashMap<>();
  private static final ConcurrentHashMap<DN, AtomicInteger> POST_OPERATION_MODIFY_COUNTS = new ConcurrentHashMap<>();
  private static final AtomicInteger CHANGES_SEEN_WHEN_FINALIZED = new AtomicInteger();
  private static volatile boolean failToFinalize;
  private final ConfigurationChangeListener<PluginCfg> listener = new ConfigurationChangeListener<PluginCfg>()
  {
    @Override
    public boolean isConfigurationChangeAcceptable(PluginCfg configuration, List<LocalizableMessage> reasons)
    {
      return true;
    }
    @Override
    public ConfigChangeResult applyConfigurationChange(PluginCfg configuration)
    {
      if (finalized)
      {
        CHANGES_SEEN_WHEN_FINALIZED.incrementAndGet();
      }
      return new ConfigChangeResult();
    }
  };
  private volatile PluginCfg configuration;
  private volatile boolean finalized;
  private volatile DirectoryServerPlugin<?> registeredWhenFinalized;
  /**
   * Returns how many times a pre-operation modify of the given entry has invoked a plugin of this
   * class, and resets that count.
   *
   * @param entryDN
   *          The DN of the modified entry.
   * @return The number of invocations since the previous call.
   */
  public static int takePreOperationModifyCount(DN entryDN)
  {
    return take(PRE_OPERATION_MODIFY_COUNTS, entryDN);
  }
  /**
   * Returns how many times a post-operation modify of the given entry has invoked a plugin of this
   * class, and resets that count.
   *
   * @param entryDN
   *          The DN of the modified entry.
   * @return The number of invocations since the previous call.
   */
  public static int takePostOperationModifyCount(DN entryDN)
  {
    return take(POST_OPERATION_MODIFY_COUNTS, entryDN);
  }
  private static int take(ConcurrentHashMap<DN, AtomicInteger> counts, DN entryDN)
  {
    AtomicInteger count = counts.remove(entryDN);
    return count != null ? count.get() : 0;
  }
  /**
   * Returns how many configuration changes the change listeners of finalized plugins of this class
   * have received, and resets that count.
   *
   * @return The number of changes since the previous call.
   */
  public static int takeChangesSeenWhenFinalized()
  {
    return CHANGES_SEEN_WHEN_FINALIZED.getAndSet(0);
  }
  /**
   * Makes {@link #finalizePlugin()} throw once it has done its work, or stop throwing.
   *
   * @param fail
   *          Whether {@link #finalizePlugin()} throws.
   */
  public static void setFailToFinalize(boolean fail)
  {
    failToFinalize = fail;
  }
  @Override
  public void initializePlugin(Set<PluginType> pluginTypes, PluginCfg configuration) throws ConfigException
  {
    this.configuration = configuration;
    configuration.addChangeListener(listener);
    if (pluginTypes.contains(REFUSED_TYPE))
    {
      throw new ConfigException(LocalizableMessage.raw("Plugin type " + REFUSED_TYPE + " is not supported"));
    }
  }
  @Override
  public PluginResult.PreOperation doPreOperation(PreOperationModifyOperation modifyOperation)
  {
    count(PRE_OPERATION_MODIFY_COUNTS, modifyOperation.getEntryDN());
    return PluginResult.PreOperation.continueOperationProcessing();
  }
  @Override
  public PluginResult.PostOperation doPostOperation(PostOperationModifyOperation modifyOperation)
  {
    count(POST_OPERATION_MODIFY_COUNTS, modifyOperation.getEntryDN());
    return PluginResult.PostOperation.continueOperationProcessing();
  }
  private static void count(ConcurrentHashMap<DN, AtomicInteger> counts, DN entryDN)
  {
    counts.computeIfAbsent(entryDN, dn -> new AtomicInteger()).incrementAndGet();
  }
  @Override
  public void finalizePlugin()
  {
    configuration.removeChangeListener(listener);
    registeredWhenFinalized = DirectoryServer.getPluginConfigManager().getRegisteredPlugin(getPluginEntryDN());
    finalized = true;
    if (failToFinalize)
    {
      throw new IllegalStateException("The test makes finalizePlugin() fail");
    }
  }
  /**
   * Indicates whether the plugin manager has finalized this instance.
   *
   * @return {@code true} once {@link #finalizePlugin()} has been called.
   */
  public boolean isFinalized()
  {
    return finalized;
  }
  /**
   * Returns the plugin which the plugin manager had registered for the configuration entry of this
   * instance when it finalized this instance.
   *
   * @return The registered plugin, or {@code null} if there was none.
   */
  public DirectoryServerPlugin<?> getRegisteredWhenFinalized()
  {
    return registeredWhenFinalized;
  }
}
opendj-server-legacy/src/test/java/org/opends/server/plugins/ReferentialIntegrityPluginTestCase.java
@@ -37,6 +37,7 @@
import org.opends.server.TestCaseUtils;
import org.forgerock.opendj.server.config.meta.PluginCfgDefn.PluginType;
import org.forgerock.opendj.server.config.meta.ReferentialIntegrityPluginCfgDefn;
import org.forgerock.opendj.server.config.server.ReferentialIntegrityPluginCfg;
import org.opends.server.api.Group;
import org.opends.server.controls.SubtreeDeleteControl;
import org.opends.server.core.AddOperation;
@@ -789,6 +790,82 @@
  }
  /**
   * Configurations that the plugin refuses in {@code initializePlugin()}: one in its configuration check, one when it
   * sets up the log file. Their DN is not the one of the running plugin, so that an instance left listening does not
   * take part in the changes the other cases make.
   */
  @DataProvider(name = "configsRefusedByInitializePlugin")
  public Object[][] createConfigsRefusedByInitializePlugin() throws Exception
  {
    List<Entry> entries = TestCaseUtils.makeEntries(
            "dn: cn=Refused Referential Integrity,cn=Plugins,cn=config",
            "objectClass: top",
            "objectClass: ds-cfg-plugin",
            "objectClass: ds-cfg-referential-integrity-plugin",
            "cn: Refused Referential Integrity",
            "ds-cfg-java-class: org.opends.server.plugins.ReferentialIntegrityPlugin",
            "ds-cfg-enabled: true",
            "ds-cfg-plugin-type: postOperationDelete",
            "ds-cfg-plugin-type: postOperationModifyDN",
            "ds-cfg-plugin-type: subordinateModifyDN",
            "ds-cfg-attribute-type: cn",
            "",
            "dn: cn=Refused Referential Integrity,cn=Plugins,cn=config",
            "objectClass: top",
            "objectClass: ds-cfg-plugin",
            "objectClass: ds-cfg-referential-integrity-plugin",
            "cn: Refused Referential Integrity",
            "ds-cfg-java-class: org.opends.server.plugins.ReferentialIntegrityPlugin",
            "ds-cfg-enabled: true",
            "ds-cfg-plugin-type: postOperationDelete",
            "ds-cfg-plugin-type: postOperationModifyDN",
            "ds-cfg-plugin-type: subordinateModifyDN",
            "ds-cfg-attribute-type: member",
            "ds-cfg-update-interval: 300 seconds",
            "ds-cfg-log-file: /hopefully/doesn't/file/exist");
    Object[][] array = new Object[entries.size()][];
    for (int i = 0; i < array.length; i++)
    {
      array[i] = new Object[] { entries.get(i) };
    }
    return array;
  }
  /**
   * An instance whose {@code initializePlugin()} refused its configuration is finalized by the plugin manager, which
   * never registers it. That must remove whatever change listener it registered, and must not fail. When the server
   * starts there is no acceptance phase, so this is the only thing that stops a refused instance from listening to
   * its configuration entry.
   */
  @Test(dataProvider = "configsRefusedByInitializePlugin")
  public void testConfigurationRefusedByInitializePluginLeavesNoChangeListenerBehind(Entry e) throws Exception
  {
    ReferentialIntegrityPluginCfg configuration =
        InitializationUtils.getConfiguration(ReferentialIntegrityPluginCfgDefn.getInstance(), e);
    int changeListeners = countChangeListeners(configuration.dn());
    ReferentialIntegrityPlugin plugin = new ReferentialIntegrityPlugin();
    try
    {
      plugin.initializePlugin(TestCaseUtils.getPluginTypes(e), configuration);
      fail("The plugin accepted a configuration it must refuse: " + e);
    }
    catch (ConfigException expected)
    {
      // As expected.
    }
    plugin.finalizePlugin();
    assertEquals(countChangeListeners(configuration.dn()), changeListeners,
        "The refused instance is still listening to its configuration entry");
  }
  private static int countChangeListeners(DN dn)
  {
    return TestCaseUtils.getServerContext().getConfigurationHandler().getChangeListeners(dn).size();
  }
  /**
   * Issue #1118: configurations with {@code check-references} set to true that lack a pre-operation plugin type,
   * with the plugin types each one lacks.
   */
@@ -1694,6 +1771,54 @@
  /**
   * Test case:
   * - the plugin is enabled for the post-operation and subordinate plugin
   *   types only
   * - while it stays enabled, the pre-operation add and modify plugin types
   *   are added and integrity is enforced on the attribute 'member'
   * - add a group 'referent group' to the 'dc=example,dc=com' with the
   *   'member' attribute pointing to the existing user entries and one missing
   * - CONSTRAINT VIOLATION: the new plugin types take effect without the
   *   plugin being disabled and enabled again
   * @throws Exception
   */
  @Test
  public void testEnforceIntegrityAfterPluginTypesAddedToEnabledPlugin() throws Exception
  {
    replaceAttrEntry(configDN, "ds-cfg-enabled", "false");
    replaceAttrEntry(configDN, dsConfigPluginType,
                               "postoperationdelete",
                               "postoperationmodifydn",
                               "subordinatemodifydn",
                               "subordinatedelete");
    addAttrEntry(configDN, dsConfigBaseDN, "dc=example,dc=com");
    replaceAttrEntry(configDN, dsConfigAttrType, "member");
    replaceAttrEntry(configDN, "ds-cfg-enabled", "true");
    assertEquals(replaceAttrEntry(configDN, dsConfigPluginType,
                               "postoperationdelete",
                               "postoperationmodifydn",
                               "subordinatemodifydn",
                               "subordinatedelete",
                               "preoperationadd",
                               "preoperationmodify").getResultCode(), ResultCode.SUCCESS);
    assertEquals(replaceAttrEntry(configDN, dsConfigEnforceIntegrity, "true").getResultCode(),
                 ResultCode.SUCCESS);
    Entry entry = TestCaseUtils.makeEntry(
      "dn: cn=referent group,ou=groups,dc=example,dc=com",
      "objectclass: top",
      "objectclass: groupofnames",
      "cn: refetent group",
      "member: uid=user.1,ou=people,ou=dept,dc=example,dc=com",
      "member: uid=bad,ou=people,ou=dept,dc=example,dc=com"
      );
    AddOperation addOperation = getRootConnection().processAdd(entry);
    assertEquals(addOperation.getResultCode(), ResultCode.CONSTRAINT_VIOLATION);
  }
  /**
   * Test case:
   * - integrity is enforced on the attribute 'member'
   * - value of the 'manager' attribute should match the filter:
   *  (objectclass=person)
opendj-server-legacy/src/test/java/org/opends/server/plugins/SambaPasswordPluginTestCase.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2011-2012 profiq s.r.o.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.plugins;
@@ -794,4 +795,37 @@
      plugin.setTimeStampProvider(null);
    }
  }
  /**
   * A change of the plugin types re-creates the plugin, and the finalized instance no longer
   * listens to the configuration entry.
   */
  @Test
  public void testPluginTypeChangeLeavesNoChangeListenerBehind() throws Exception
  {
    DN pluginDN = DN.valueOf("cn=samba password,cn=Plugins,cn=config");
    int changeListeners = countChangeListeners(pluginDN);
    try
    {
      assertEquals(setPluginTypes(pluginDN, "preoperationmodify"), ResultCode.SUCCESS);
      assertEquals(countChangeListeners(pluginDN), changeListeners);
    }
    finally
    {
      assertEquals(setPluginTypes(pluginDN, "postoperationextended", "preoperationmodify"), ResultCode.SUCCESS);
    }
    assertEquals(countChangeListeners(pluginDN), changeListeners);
  }
  private static int countChangeListeners(DN pluginDN)
  {
    return TestCaseUtils.getServerContext().getConfigurationHandler().getChangeListeners(pluginDN).size();
  }
  private static ResultCode setPluginTypes(DN pluginDN, String... pluginTypes)
  {
    ModifyRequest request = Requests.newModifyRequest(pluginDN)
        .addModification(REPLACE, "ds-cfg-plugin-type", pluginTypes);
    return getRootConnection().processModify(request).getResultCode();
  }
}
opendj-server-legacy/src/test/java/org/opends/server/protocols/ldap/LDAPConnectionHandler2TransportTestCase.java
New file
@@ -0,0 +1,767 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.protocols.ldap;
import static org.opends.messages.CoreMessages.INFO_CONNHANDLER_CLOSED_BY_SHUTDOWN;
import static org.testng.Assert.*;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.reactive.LDAPConnectionHandler2;
import org.forgerock.opendj.server.config.meta.LDAPConnectionHandlerCfgDefn;
import org.forgerock.opendj.server.config.server.LDAPConnectionHandlerCfg;
import org.glassfish.grizzly.memory.Buffers;
import org.glassfish.grizzly.nio.transport.TCPNIOConnection;
import org.glassfish.grizzly.nio.transport.TCPNIOServerConnection;
import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.ConnectionHandler;
import org.opends.server.api.ServerShutdownListener;
import org.opends.server.core.DirectoryServer;
import org.opends.server.extensions.InitializationUtils;
import org.opends.server.tools.LDAPReader;
import org.opends.server.types.Entry;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
 * {@link LDAPConnectionHandler2} serves its connections with a transport of its own, built from its configuration:
 * {@code use-tcp-keep-alive} and {@code use-tcp-no-delay} reach every accepted socket, {@code buffer-size} is the write
 * buffer of every connection, {@code allow-tcp-reuse-address} reaches the listen socket and {@code num-request-handlers}
 * is the number of selector threads. Stopping the handler leaves its connections open until they are closed, or until
 * the server shuts down, which ends them with a notice of disconnection; then its threads stop.
 */
@SuppressWarnings("javadoc")
@Test(groups = { "precommit" }, sequential = true)
public class LDAPConnectionHandler2TransportTestCase extends DirectoryServerTestCase
{
  private static final LocalizableMessage STOP_REASON = LocalizableMessage.raw("Stopped by the transport test.");
  private static final String NOTICE_OF_DISCONNECTION_OID = "1.3.6.1.4.1.1466.20036";
  private static final String SELECTORS_PROPERTY = "org.forgerock.opendj.transport.selectors";
  /** Unlike any socket buffer size a system would choose. */
  private static final int WRITE_BUFFER_SIZE = 12345;
  private static final long TIMEOUT_MS = 10000;
  /** How long the connection of a stopped handler must stay open. */
  private static final long KEEPS_OPEN_MS = 3000;
  /** What is written at a time to fill the socket buffers of both ends and then the write queue of the server. */
  private static final int UNREAD_CHUNK = 64 * 1024;
  /** Far more than any socket buffers hold. */
  private static final int MAX_UNREAD_BYTES = 64 * 1024 * 1024;
  /**
   * How soon a drain ends once its last connection is closed: well below the 2 s it waits at most for connections to
   * close, which the notice queued behind unread data takes about 1.3 s to reach.
   */
  private static final long CLOSED_DRAIN_ENDS_MS = 1000;
  @BeforeClass
  public void setUp() throws Exception
  {
    TestCaseUtils.startServer();
  }
  @DataProvider
  public Object[][] socketOptions()
  {
    return new Object[][] { { true, true }, { true, false }, { false, true }, { false, false } };
  }
  @Test(dataProvider = "socketOptions")
  public void acceptedSocketUsesTheConfiguredOptions(boolean keepAlive, boolean noDelay) throws Exception
  {
    final int port = TestCaseUtils.findFreePort();
    final LDAPConnectionHandler2 handler = start(configuration(port, keepAlive, noDelay, true, 2));
    try (Socket client = new Socket("127.0.0.1", port))
    {
      final Socket accepted = ((SocketChannel) acceptedConnection(handler).getChannel()).socket();
      assertEquals(accepted.getKeepAlive(), keepAlive, "SO_KEEPALIVE");
      assertEquals(accepted.getTcpNoDelay(), noDelay, "TCP_NODELAY");
    }
    finally
    {
      stop(handler);
    }
  }
  @Test
  public void connectionIsServedByTheTransportOfItsHandlerWithTheConfiguredWriteBuffer() throws Exception
  {
    final int port = TestCaseUtils.findFreePort();
    final LDAPConnectionHandler2 handler = start(configuration(port, true, true, true, 2));
    try (Socket client = new Socket("127.0.0.1", port))
    {
      final TCPNIOConnection accepted = acceptedConnection(handler);
      assertSame(accepted.getTransport(), transport(handler), "the connection is not served by its handler's transport");
      assertEquals(accepted.getWriteBufferSize(), WRITE_BUFFER_SIZE);
    }
    finally
    {
      stop(handler);
    }
  }
  @DataProvider
  public Object[][] reuseAddress()
  {
    return new Object[][] { { true }, { false } };
  }
  @Test(dataProvider = "reuseAddress")
  public void listenSocketUsesTheConfiguredReuseAddress(boolean reuseAddress) throws Exception
  {
    final LDAPConnectionHandler2 handler =
        start(configuration(freePort(reuseAddress), true, true, reuseAddress, 2));
    try
    {
      final Collection<?> serverConnections = (Collection<?>) field(field(handler, "listener"), "impl", "serverConnections");
      assertFalse(serverConnections.isEmpty(), "the handler does not listen");
      for (Object serverConnection : serverConnections)
      {
        final ServerSocketChannel channel = (ServerSocketChannel) ((TCPNIOServerConnection) serverConnection).getChannel();
        assertEquals(channel.socket().getReuseAddress(), reuseAddress, "SO_REUSEADDR");
      }
    }
    finally
    {
      stop(handler);
    }
  }
  @Test
  public void selectorThreadsFollowNumRequestHandlers() throws Exception
  {
    assertSelectorThreads(configuration(TestCaseUtils.findFreePort(), true, true, true, 3), 3);
  }
  /** The system property that sized the transport shared by every listener still applies when nothing is set. */
  @Test
  public void selectorThreadsFollowTheSelectorsPropertyWhenNumRequestHandlersIsUnset() throws Exception
  {
    final String saved = System.getProperty(SELECTORS_PROPERTY);
    System.setProperty(SELECTORS_PROPERTY, "13");
    try
    {
      assertSelectorThreads(configuration(TestCaseUtils.findFreePort(), true, true, true, null), 13);
    }
    finally
    {
      restore(saved);
    }
  }
  @Test
  public void numRequestHandlersTakesPrecedenceOverTheSelectorsProperty() throws Exception
  {
    final String saved = System.getProperty(SELECTORS_PROPERTY);
    System.setProperty(SELECTORS_PROPERTY, "13");
    try
    {
      assertSelectorThreads(configuration(TestCaseUtils.findFreePort(), true, true, true, 3), 3);
    }
    finally
    {
      restore(saved);
    }
  }
  /**
   * Decisive only on hosts with 6 processors or more: below that the count is 2, which a constant would give too.
   */
  @Test
  public void selectorThreadsAreChosenFromTheProcessorsWhenNothingIsSet() throws Exception
  {
    final String saved = System.getProperty(SELECTORS_PROPERTY);
    System.clearProperty(SELECTORS_PROPERTY);
    try
    {
      assertSelectorThreads(configuration(TestCaseUtils.findFreePort(), true, true, true, null),
          Math.max(2, Runtime.getRuntime().availableProcessors() / 2));
    }
    finally
    {
      restore(saved);
    }
  }
  /**
   * Stopping the handler, as disabling, deleting or restarting it does, leaves the connections it has accepted open:
   * its transport keeps serving them, and is shut down once the last of them is closed.
   */
  @Test
  public void stoppedHandlerKeepsItsConnectionsUntilTheyAreClosed() throws Exception
  {
    final int port = TestCaseUtils.findFreePort();
    final LDAPConnectionHandler2 handler = start(configuration(port, true, true, true, 2));
    final String threadPrefix = selectorThreadPrefix(handler);
    try (Socket client = new Socket("127.0.0.1", port))
    {
      acceptedConnection(handler);
      stop(handler);
      client.setSoTimeout((int) KEEPS_OPEN_MS);
      try
      {
        final int read = client.getInputStream().read();
        fail("the connection of the stopped handler was " + (read == -1 ? "closed" : "written to"));
      }
      catch (SocketTimeoutException expected)
      {
        // still open
      }
      assertFalse(threadsNamed(threadPrefix).isEmpty(), "the transport stopped while a connection was open");
    }
    finally
    {
      stop(handler);
    }
    assertThreadsStop(threadPrefix);
  }
  /** The server shutting down ends the connections a stopped handler left open, and shuts its transport down. */
  @Test
  public void serverShutdownEndsTheConnectionsOfAStoppedHandler() throws Exception
  {
    final int port = TestCaseUtils.findFreePort();
    final LDAPConnectionHandler2 handler = start(configuration(port, true, true, true, 2));
    final String threadPrefix = selectorThreadPrefix(handler);
    try (Socket client = new Socket("127.0.0.1", port))
    {
      acceptedConnection(handler);
      stop(handler);
      final ServerShutdownListener drain = onlyDrain(handler);
      assertTrue(((Collection<?>) field(DirectoryServer.getInstance(), "shutdownListeners")).contains(drain),
          "the drain is not registered as a shutdown listener");
      drain.processServerShutdown(STOP_REASON);
      assertNoticeOfDisconnection(client, STOP_REASON);
    }
    finally
    {
      stop(handler);
    }
    assertThreadsStop(threadPrefix);
  }
  /**
   * A notice of disconnection queued behind data the client has not read yet still reaches the client: the transport
   * is not shut down before the client has read up to it.
   */
  @Test
  public void serverShutdownDeliversANoticeQueuedBehindUnreadData() throws Exception
  {
    final int port = TestCaseUtils.findFreePort();
    final LDAPConnectionHandler2 handler = start(configuration(port, true, true, true, 2));
    try (Socket client = new Socket())
    {
      // Keeps what the client side holds small, so that the backlog is quick to read once the client reads.
      client.setReceiveBufferSize(8 * 1024);
      client.connect(new InetSocketAddress("127.0.0.1", port));
      final TCPNIOConnection accepted = acceptedConnection(handler);
      final TCPNIOTransport transport = transport(handler);
      // Written below the LDAP filters, as raw bytes the client will skip, until the socket buffers are full and some
      // are left waiting in the write queue.
      final byte[] chunk = new byte[UNREAD_CHUNK];
      int unread = 0;
      while (accepted.getAsyncWriteQueue().spaceInBytes() == 0)
      {
        assertTrue(unread < MAX_UNREAD_BYTES, "the socket buffers took " + unread + " bytes");
        transport.getAsyncQueueIO().getWriter().write(accepted, Buffers.wrap(transport.getMemoryManager(), chunk), null);
        unread += chunk.length;
        // Let the selector move what it can into the socket.
        Thread.sleep(50);
      }
      stop(handler);
      final ServerShutdownListener drain = onlyDrain(handler);
      final Thread shutdown = new Thread(() -> drain.processServerShutdown(STOP_REASON));
      shutdown.start();
      // Let the notice join the queue while the client reads nothing.
      Thread.sleep(300);
      client.setSoTimeout((int) TIMEOUT_MS);
      final InputStream in = client.getInputStream();
      final byte[] buffer = new byte[64 * 1024];
      for (int left = unread; left > 0;)
      {
        final int read = in.read(buffer, 0, Math.min(buffer.length, left));
        assertTrue(read > 0, "the connection ended with " + left + " unread bytes still queued");
        left -= read;
      }
      assertNoticeOfDisconnection(client, STOP_REASON);
      // The client has read everything and the server has closed: the drain must not wait out its bound.
      shutdown.join(CLOSED_DRAIN_ENDS_MS);
      assertFalse(shutdown.isAlive(), "the drain is still waiting for a closed connection");
    }
    finally
    {
      stop(handler);
    }
  }
  /**
   * The server shutting down, here for an in-core restart, ends the connections of a handler that is still listening
   * with a notice of disconnection. The handler stops its listener on its own thread, which may wake before or after
   * the next server instance is current: {@link #drainEndsTheConnectionsWhenTheServerOfItsHandlerIsShuttingDown()}
   * covers the latter.
   */
  @Test
  public void serverRestartEndsTheConnectionsOfAListeningHandler() throws Exception
  {
    try (Socket client = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort()))
    {
      awaitServerConnection(client.getLocalPort());
      // Read while the server restarts: macOS resets a closed loopback connection after net.inet.tcp.fin_timeout
      // (60 s), and drops what the client has not read yet.
      final ExecutorService reader = Executors.newSingleThreadExecutor();
      try
      {
        final Future<?> notice = reader.submit(() -> {
          assertNoticeOfDisconnection(client, INFO_CONNHANDLER_CLOSED_BY_SHUTDOWN.get());
          return null;
        });
        TestCaseUtils.restartServer();
        notice.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
      }
      finally
      {
        reader.shutdownNow();
      }
    }
  }
  /**
   * A handler finalized while its server runs, as disabling or deleting it does, may stop its listener after that
   * server has begun shutting down, and even after the next server instance is current. The drain then ends the
   * connections with a notice of disconnection as the server shutdown would have, and registers with no server.
   */
  @Test
  public void drainEndsTheConnectionsWhenTheServerOfItsHandlerIsShuttingDown() throws Exception
  {
    final int port = TestCaseUtils.findFreePort();
    final LDAPConnectionHandler2 handler = start(configuration(port, true, true, true, 2));
    final String threadPrefix = selectorThreadPrefix(handler);
    try (Socket client = new Socket("127.0.0.1", port))
    {
      acceptedConnection(handler);
      // The server of the handler is shutting down, and the current instance is another one that is not.
      final Constructor<DirectoryServer> newServer = DirectoryServer.class.getDeclaredConstructor();
      newServer.setAccessible(true);
      final DirectoryServer previous = newServer.newInstance();
      setField(previous, "shuttingDown", true);
      setField(handler, "server", previous);
      stop(handler);
      assertNoticeOfDisconnection(client, INFO_CONNHANDLER_CLOSED_BY_SHUTDOWN.get());
      assertTrue(((Collection<?>) field(handler, "drains")).isEmpty(), "the drain is still serving the connections");
      for (Object listener : (Collection<?>) field(DirectoryServer.getInstance(), "shutdownListeners"))
      {
        assertNotEquals(((ServerShutdownListener) listener).getShutdownListenerName(),
            "Transport drain of " + handler.getConnectionHandlerName(), "the drain registered with the current server");
      }
    }
    finally
    {
      stop(handler);
    }
    assertThreadsStop(threadPrefix);
  }
  /**
   * A handler that stops listening and starts again, as it does when an SSL change it cannot use is applied and then
   * undone, runs two transports. The drained one ends with its own connections, whatever the other one serves.
   */
  @Test
  public void drainEndsWithTheConnectionsOfItsOwnTransport() throws Exception
  {
    final int port = TestCaseUtils.findFreePort();
    final LDAPConnectionHandler2 handler = start(configuration(port, true, true, true, 2));
    try (Socket first = new Socket("127.0.0.1", port))
    {
      acceptedConnection(handler);
      final TCPNIOTransport drained = listenAgain(handler);
      final TCPNIOTransport live = transport(handler);
      try (Socket second = new Socket("127.0.0.1", port))
      {
        awaitClientConnections(handler, 2);
        first.close();
        awaitStopped(drained);
        assertFalse(live.isStopped(), "the transport of the listening handler stopped");
      }
    }
    finally
    {
      stop(handler);
    }
  }
  /** The server shutting down ends, through a drain, only the connections of the drained transport. */
  @Test
  public void drainEndsAtServerShutdownOnlyTheConnectionsOfItsOwnTransport() throws Exception
  {
    final int port = TestCaseUtils.findFreePort();
    final LDAPConnectionHandler2 handler = start(configuration(port, true, true, true, 2));
    try (Socket first = new Socket("127.0.0.1", port))
    {
      acceptedConnection(handler);
      final TCPNIOTransport drained = listenAgain(handler);
      try (Socket second = new Socket("127.0.0.1", port))
      {
        awaitClientConnections(handler, 2);
        onlyDrain(handler).processServerShutdown(STOP_REASON);
        assertNoticeOfDisconnection(first, STOP_REASON);
        awaitStopped(drained);
        second.setSoTimeout((int) KEEPS_OPEN_MS);
        try
        {
          final int read = second.getInputStream().read();
          fail("the connection of the listening transport was " + (read == -1 ? "closed" : "written to"));
        }
        catch (SocketTimeoutException expected)
        {
          // still open
        }
      }
    }
    finally
    {
      stop(handler);
    }
  }
  /** A listener that cannot bind leaves no transport behind: the transport it started for it is shut down. */
  @Test
  public void failedListenLeavesNoSelectorThreads() throws Exception
  {
    final int port = freePort(false);
    final LDAPConnectionHandler2 handler = new LDAPConnectionHandler2();
    // Both initialization and the configuration check verify the port first: take it only afterwards.
    handler.initializeConnectionHandler(DirectoryServer.getInstance().getServerContext(),
        configuration(port, true, true, false, 2));
    try (ServerSocket taken = new ServerSocket())
    {
      taken.bind(new InetSocketAddress("127.0.0.1", port));
      handler.start();
      final long deadline = System.currentTimeMillis() + TIMEOUT_MS;
      while ((Boolean) field(handler, "enabled"))
      {
        assertTrue(System.currentTimeMillis() < deadline, "the handler did not give up listening");
        Thread.sleep(50);
      }
      assertThreadsStop(handler.getConnectionHandlerName() + " Request Handler");
    }
    finally
    {
      stop(handler);
    }
  }
  /**
   * Returns a free port that a listen socket with the given SO_REUSEADDR setting can bind to on 127.0.0.1.
   * {@link TestCaseUtils#findFreePort()} checks its ports with SO_REUSEADDR only, and every test class counts them down
   * from the same number in a JVM of its own: a port can still carry a connection a previous class left in TIME_WAIT,
   * which refuses only a socket without SO_REUSEADDR.
   */
  private static int freePort(boolean reuseAddress) throws IOException
  {
    while (true)
    {
      final int port = TestCaseUtils.findFreePort();
      if (reuseAddress)
      {
        return port;
      }
      try (ServerSocket probe = new ServerSocket())
      {
        probe.setReuseAddress(false);
        probe.bind(new InetSocketAddress("127.0.0.1", port));
        return port;
      }
      catch (BindException inUse)
      {
        // Try the next one: findFreePort() hands out each port once, and throws when none is left.
      }
    }
  }
  /**
   * Makes the handler stop listening and start again in the same instance, and returns the transport it stopped with.
   */
  private static TCPNIOTransport listenAgain(LDAPConnectionHandler2 handler) throws Exception
  {
    final TCPNIOTransport stopped = transport(handler);
    // What the handler does to itself when it cannot use an SSL change: its configuration still enables it.
    setField(handler, "enabled", false);
    awaitDrains(handler, 1);
    setField(handler, "enabled", true);
    final long deadline = System.currentTimeMillis() + TIMEOUT_MS;
    // The transport starts before the listener binds: only the listener tells that the port accepts connections.
    while (field(handler, "listener") == null)
    {
      assertTrue(System.currentTimeMillis() < deadline, "the handler did not listen again");
      Thread.sleep(50);
    }
    return stopped;
  }
  private static ServerShutdownListener onlyDrain(LDAPConnectionHandler2 handler) throws Exception
  {
    final Collection<?> drains = (Collection<?>) field(handler, "drains");
    assertEquals(drains.size(), 1, "no transport is left serving the connections of the stopped listener");
    return (ServerShutdownListener) drains.iterator().next();
  }
  private static void awaitDrains(LDAPConnectionHandler2 handler, int expected) throws Exception
  {
    final long deadline = System.currentTimeMillis() + TIMEOUT_MS;
    while (((Collection<?>) field(handler, "drains")).size() != expected)
    {
      assertTrue(System.currentTimeMillis() < deadline, "the handler did not stop listening");
      Thread.sleep(50);
    }
  }
  private static void awaitClientConnections(LDAPConnectionHandler2 handler, int expected) throws Exception
  {
    final long deadline = System.currentTimeMillis() + TIMEOUT_MS;
    while (handler.getClientConnections().size() != expected)
    {
      assertTrue(System.currentTimeMillis() < deadline, "the handler did not accept the connections");
      Thread.sleep(50);
    }
  }
  /** Waits for a connection handler of the server to accept the connection from the given client port. */
  private static void awaitServerConnection(int clientPort) throws Exception
  {
    final long deadline = System.currentTimeMillis() + TIMEOUT_MS;
    while (true)
    {
      for (ConnectionHandler<?> connectionHandler : DirectoryServer.getConnectionHandlers())
      {
        for (ClientConnection connection : connectionHandler.getClientConnections())
        {
          if (connection.getClientPort() == clientPort)
          {
            return;
          }
        }
      }
      assertTrue(System.currentTimeMillis() < deadline, "the server did not accept the connection");
      Thread.sleep(50);
    }
  }
  private static void awaitStopped(TCPNIOTransport transport) throws InterruptedException
  {
    final long deadline = System.currentTimeMillis() + TIMEOUT_MS;
    while (!transport.isStopped())
    {
      assertTrue(System.currentTimeMillis() < deadline, "the drained transport is still running");
      Thread.sleep(50);
    }
  }
  /** Reads the next message from the client, checks it is the expected notice of disconnection, then the end. */
  private static void assertNoticeOfDisconnection(Socket client, LocalizableMessage reason) throws Exception
  {
    client.setSoTimeout((int) TIMEOUT_MS);
    final LDAPMessage message = new LDAPReader(client).readMessage();
    assertNotNull(message, "the connection was closed without a notice of disconnection");
    final ExtendedResponseProtocolOp notice = message.getExtendedResponseProtocolOp();
    assertEquals(notice.getOID(), NOTICE_OF_DISCONNECTION_OID);
    assertEquals(notice.getResultCode(), LDAPResultCode.UNAVAILABLE, "result code");
    assertEquals(String.valueOf(notice.getErrorMessage()), reason.toString(), "diagnostic message");
    assertEquals(client.getInputStream().read(), -1, "the connection stayed open after the notice");
  }
  /** Returns the prefix of the names of the selector threads of the handler, after checking that some run. */
  private static String selectorThreadPrefix(LDAPConnectionHandler2 handler)
  {
    final String prefix = handler.getConnectionHandlerName() + " Request Handler";
    assertFalse(threadsNamed(prefix).isEmpty(), "no thread is named after the handler: " + prefix);
    return prefix;
  }
  private static void assertThreadsStop(String prefix) throws InterruptedException
  {
    final long deadline = System.currentTimeMillis() + TIMEOUT_MS;
    List<String> left;
    while (!(left = threadsNamed(prefix)).isEmpty())
    {
      assertTrue(System.currentTimeMillis() < deadline, "threads of the stopped handler are still alive: " + left);
      Thread.sleep(100);
    }
  }
  private static void assertSelectorThreads(LDAPConnectionHandlerCfg config, int expected) throws Exception
  {
    final LDAPConnectionHandler2 handler = start(config);
    final String threadPrefix = selectorThreadPrefix(handler);
    try
    {
      final TCPNIOTransport transport = transport(handler);
      assertEquals(transport.getSelectorRunnersCount(), expected, "selector runners");
      assertEquals(transport.getKernelThreadPoolConfig().getMaxPoolSize(), expected, "selector threads");
    }
    finally
    {
      stop(handler);
    }
    assertThreadsStop(threadPrefix);
  }
  private static void restore(String selectors)
  {
    if (selectors != null)
    {
      System.setProperty(SELECTORS_PROPERTY, selectors);
    }
    else
    {
      System.clearProperty(SELECTORS_PROPERTY);
    }
  }
  private static List<String> threadsNamed(String prefix)
  {
    final List<String> names = new ArrayList<>();
    for (Thread thread : Thread.getAllStackTraces().keySet())
    {
      if (thread.isAlive() && thread.getName().startsWith(prefix))
      {
        names.add(thread.getName());
      }
    }
    return names;
  }
  private static TCPNIOTransport transport(LDAPConnectionHandler2 handler) throws Exception
  {
    final TCPNIOTransport transport = (TCPNIOTransport) field(handler, "transport");
    assertNotNull(transport, "the handler has no transport");
    return transport;
  }
  /** Waits for the handler to accept a connection and returns the Grizzly connection behind it. */
  private static TCPNIOConnection acceptedConnection(LDAPConnectionHandler2 handler) throws Exception
  {
    final long deadline = System.currentTimeMillis() + TIMEOUT_MS;
    while (handler.getClientConnections().isEmpty())
    {
      assertTrue(System.currentTimeMillis() < deadline, "the handler did not accept the connection");
      Thread.sleep(50);
    }
    final ClientConnection connection = handler.getClientConnections().iterator().next();
    return (TCPNIOConnection) field(connection, "clientContext", "connection");
  }
  /** Follows a chain of fields declared by the class of each object met. */
  private static Object field(Object target, String... names) throws Exception
  {
    Object value = target;
    for (String name : names)
    {
      final Field field = value.getClass().getDeclaredField(name);
      field.setAccessible(true);
      value = field.get(value);
    }
    return value;
  }
  private static void setField(Object target, String name, Object value) throws Exception
  {
    final Field field = target.getClass().getDeclaredField(name);
    field.setAccessible(true);
    field.set(target, value);
  }
  private static LDAPConnectionHandlerCfg configuration(int port, boolean keepAlive, boolean noDelay,
      boolean reuseAddress, Integer numRequestHandlers) throws Exception
  {
    final List<String> lines = new ArrayList<>();
    lines.add("dn: cn=Transport Test Handler,cn=Connection Handlers,cn=config");
    lines.add("objectClass: top");
    lines.add("objectClass: ds-cfg-connection-handler");
    lines.add("objectClass: ds-cfg-ldap-connection-handler");
    lines.add("cn: Transport Test Handler");
    lines.add("ds-cfg-java-class: " + LDAPConnectionHandler2.class.getName());
    lines.add("ds-cfg-enabled: true");
    lines.add("ds-cfg-listen-address: 127.0.0.1");
    lines.add("ds-cfg-listen-port: " + port);
    lines.add("ds-cfg-accept-backlog: 128");
    lines.add("ds-cfg-keep-stats: false");
    lines.add("ds-cfg-use-tcp-keep-alive: " + keepAlive);
    lines.add("ds-cfg-use-tcp-no-delay: " + noDelay);
    lines.add("ds-cfg-allow-tcp-reuse-address: " + reuseAddress);
    lines.add("ds-cfg-buffer-size: " + WRITE_BUFFER_SIZE + " bytes");
    lines.add("ds-cfg-use-ssl: false");
    lines.add("ds-cfg-allow-start-tls: false");
    lines.add("ds-cfg-allow-ldap-v2: false");
    lines.add("ds-cfg-send-rejection-notice: true");
    if (numRequestHandlers != null)
    {
      lines.add("ds-cfg-num-request-handlers: " + numRequestHandlers);
    }
    final Entry entry = TestCaseUtils.makeEntry(lines.toArray(new String[0]));
    return InitializationUtils.getConfiguration(LDAPConnectionHandlerCfgDefn.getInstance(), entry);
  }
  private static LDAPConnectionHandler2 start(LDAPConnectionHandlerCfg config) throws Exception
  {
    final LDAPConnectionHandler2 handler = new LDAPConnectionHandler2();
    handler.initializeConnectionHandler(DirectoryServer.getInstance().getServerContext(), config);
    handler.start();
    return handler;
  }
  private static void stop(LDAPConnectionHandler2 handler) throws InterruptedException
  {
    if (!handler.isAlive())
    {
      return;
    }
    handler.processServerShutdown(STOP_REASON);
    handler.finalizeConnectionHandler(STOP_REASON);
    handler.join(TIMEOUT_MS);
    assertFalse(handler.isAlive(), "the connection handler thread is still running");
  }
}
opendj-server-legacy/src/test/java/org/opends/server/tools/UpgradeTestCase.java
@@ -12,6 +12,7 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.tools;
@@ -29,6 +30,7 @@
import org.testng.annotations.Test;
import static com.forgerock.opendj.cli.ArgumentConstants.*;
import static com.forgerock.opendj.cli.ArgumentParser.PROPERTY_SCRIPT_NAME;
import static org.opends.messages.ToolMessages.*;
import static org.testng.Assert.*;
@@ -114,6 +116,35 @@
    }
  }
  /** The generated reference of the upgrade tool includes its AsciiDoc description (#1128). */
  @Test
  public void testUpgradeToolGenerateDocIncludesItsDescriptionSupplement() throws Exception
  {
    final String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
    System.setProperty("org.forgerock.opendj.gendoc", "true");
    System.setProperty(PROPERTY_SCRIPT_NAME, "upgrade");
    try (final ByteArrayOutputStream baos = new ByteArrayOutputStream();
        final PrintStream ps = new PrintStream(baos))
    {
      assertEquals(UpgradeCli.main(setArgs("-?"), true, ps, ps), 0);
      Assertions.assertThat(baos.toString())
          .contains("include::./_description-upgrade.adoc[]")
          .doesNotContain("<xinclude:include");
    }
    finally
    {
      System.clearProperty("org.forgerock.opendj.gendoc");
      if (scriptName != null)
      {
        System.setProperty(PROPERTY_SCRIPT_NAME, scriptName);
      }
      else
      {
        System.clearProperty(PROPERTY_SCRIPT_NAME);
      }
    }
  }
  /** Tests the upgrade tool with an invalid sub-command. */
  @Test
  public void testUpgradeToolDoesntAllowWrongSubcommand() throws Exception
opendj-server-legacy/src/test/java/org/opends/server/tools/dsconfig/DsconfigOptionsTestCase.java
@@ -12,6 +12,7 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.tools.dsconfig;
@@ -19,6 +20,8 @@
import static org.testng.Assert.*;
import java.io.ByteArrayOutputStream;
import org.forgerock.opendj.config.dsconfig.DSConfig;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
@@ -182,6 +185,35 @@
    }
  }
  /**
   * Tests that the generated reference describes the duration and ACI values in AsciiDoc,
   * and keeps the duration and size limits apart.
   */
  @Test
  public void testGenerateDocHasNoDocBookLeftovers() throws Exception
  {
    System.setProperty("org.forgerock.opendj.gendoc", "true");
    System.setProperty("com.forgerock.opendj.ldap.tools.scriptName", "dsconfig");
    final ByteArrayOutputStream out = new ByteArrayOutputStream();
    try
    {
      assertEquals(DSConfig.main(new String[] { "--no-prompt", "-?" }, out, System.err), SUCCESS.get());
    }
    finally
    {
      System.clearProperty("org.forgerock.opendj.gendoc");
    }
    final String doc = out.toString("UTF-8");
    assertTrue(doc.contains("Upper limit is"), "no duration property with an upper limit was generated");
    assertTrue(doc.contains("A duration: a number followed by a unit"), "duration syntax");
    assertTrue(doc.contains("xref:../admin-guide/chap-privileges-acis.adoc#about-acis"), "ACI syntax");
    assertFalse(doc.contains("<xinclude:include"), "DocBook xinclude leftover");
    assertFalse(doc.contains("<olink"), "DocBook olink leftover");
    assertFalse(doc.contains(".Lower limit") || doc.contains(".Upper limit"), "duration sentences glued together");
    assertTrue(doc.contains("Lower value is 512"), "no size property with a lower limit was generated");
    assertFalse(doc.contains(".Upper value"), "size sentences glued together");
  }
  private int dsconfigMain(String[] args)
  {
    return DSConfig.main(args, System.out, System.err);
opendj-server-msad-plugin/src/main/java/opendj/MsadPlugin.java
@@ -1,3 +1,18 @@
/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2025-2026 3A Systems, LLC.
 */
package opendj;
import java.util.List;
@@ -92,6 +107,12 @@
    }
    @Override
    public void finalizePlugin() {
        // Also called when initializePlugin() refused the plugin types after registering the listener.
        config.removeMsadChangeListener(this);
    }
    @Override
    public PreOperation doPreOperation(PreOperationBindOperation bindOperation) {
        DN bindDN = bindOperation.getBindDN();
        Entry userEntry;