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

27 files modified
4 files added
962 ■■■■ changed files
.github/workflows/build.yml 23 ●●●●● patch | view | raw | blame | history
.github/workflows/codeql.yml 2 ●●● patch | view | raw | blame | history
.github/workflows/deploy.yml 5 ●●●●● patch | view | raw | blame | history
.github/workflows/release.yml 41 ●●●●● patch | view | raw | blame | history
opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommand.java 5 ●●●● patch | view | raw | blame | history
opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandArgumentParser.java 17 ●●●● patch | view | raw | blame | history
opendj-cli/src/test/java/com/forgerock/opendj/cli/TestSubCommandArgumentParserTestCase.java 57 ●●●●● patch | view | raw | blame | history
opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/DSConfig.java 3 ●●●● patch | view | raw | blame | history
opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/PropertyValueEditor.java 4 ●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java 21 ●●●● patch | view | raw | blame | history
opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateMessageFileMojo.java 59 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/browser/NodeRefresher.java 3 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/browser/NodeSearcherQueue.java 9 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/datamodel/ControlPanelInfo.java 101 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/task/DeleteEntryTask.java 3 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanel.java 67 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/NewIndexPanel.java 9 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanel.java 35 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanel.java 45 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/OnDiskMergeImporter.java 2 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/core/EntryCacheConfigManager.java 10 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/BCrypt.java 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/ErrorLogAccountStatusNotificationHandler.java 14 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java 3 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/util/cli/LDAPConnectionConsoleInteraction.java 26 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/messages/org/opends/messages/protocol.properties 2 ●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanelTestCase.java 181 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanelTestCase.java 72 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanelTestCase.java 69 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/extensions/BCryptTest.java 13 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/util/cli/LDAPConnectionConsoleInteractionTestCase.java 59 ●●●●● patch | view | raw | blame | history
.github/workflows/build.yml
@@ -23,6 +23,11 @@
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
# Nothing in this workflow writes back to the repository: the docker jobs push to
# the local registry service, not to a remote one.
permissions:
  contents: read
jobs:
  build-maven:
    runs-on: ${{ matrix.os }}
@@ -69,7 +74,7 @@
         restore-keys: ${{ runner.os }}-m2-repository
    - name: Setup MSVC Developer Command Prompt (x86)
      if: runner.os == 'Windows'
      uses: ilammy/msvc-dev-cmd@v1
      uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
      env:
        # Opt in to Node.js 24 for this action, which still ships on Node.js 20.
        # See https://github.com/ilammy/msvc-dev-cmd/issues/99
@@ -408,16 +413,16 @@
          echo "release_version=$git_version_last" >> $GITHUB_ENV
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v6
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
        with:
          images: |
            localhost:5000/${{ github.repository }}
          tags: |
            type=raw,value=${{ env.release_version }}
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v4
        uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
        with:
          driver-opts: network=host
      - name: Prepare Dockerfile
@@ -427,7 +432,7 @@
          cp ./opendj-server-legacy/target/package/opendj-*.zip ./opendj-packages/opendj-docker
          sed -i -E '/^#COPY opendj/s/^#//' ./opendj-packages/opendj-docker/Dockerfile
      - name: Build image (default)
        uses: docker/build-push-action@v7
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        with:
          context: ./opendj-packages/opendj-docker
          file: ./opendj-packages/opendj-docker/Dockerfile
@@ -527,7 +532,7 @@
          echo "release_version=$git_version_last" >> $GITHUB_ENV
      - name: Docker meta 
        id: meta
        uses: docker/metadata-action@v6
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
        with:
          images: |
            localhost:5000/${{ github.repository }}
@@ -535,9 +540,9 @@
            type=raw,value=alpine
            type=raw,value=${{ env.release_version }}-alpine
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v4
        uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
        with:
          driver-opts: network=host
      - name: Prepare Dockerfile
@@ -547,7 +552,7 @@
          cp ./opendj-server-legacy/target/package/opendj-*.zip ./opendj-packages/opendj-docker
          sed -i -E '/^#COPY opendj/s/^#//' ./opendj-packages/opendj-docker/Dockerfile-alpine
      - name: Build image
        uses: docker/build-push-action@v7
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        with:
          context: ./opendj-packages/opendj-docker
          file: ./opendj-packages/opendj-docker/Dockerfile-alpine
.github/workflows/codeql.yml
@@ -102,7 +102,7 @@
      # --- C/C++ manual build (Windows native executables) -----------------
      - name: Setup MSVC Developer Command Prompt (x86)
        if: matrix.language == 'c-cpp'
        uses: ilammy/msvc-dev-cmd@v1
        uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
        env:
          # Opt in to Node.js 24 for this action, which still ships on Node.js 20.
          # See https://github.com/ilammy/msvc-dev-cmd/issues/99
.github/workflows/deploy.yml
@@ -24,6 +24,11 @@
  group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch }}
  cancel-in-progress: false
# contents: write is required to push the generated documentation to the project wiki
# with github.token. The doc site push uses a separate PAT, not this token.
permissions:
  contents: write
jobs:
  package-deploy-maven:
    if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event=='push'}}
.github/workflows/release.yml
@@ -30,9 +30,16 @@
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false
permissions:
  contents: read
jobs:
  release-maven:
    runs-on: 'ubuntu-latest'
    # contents: write is required to push the release commits and tags produced by
    # maven-release-plugin, to create the GitHub release and to update the wiki.
    permissions:
      contents: write
    steps:
      - name: Print github context
        env:
@@ -90,7 +97,7 @@
        if: ${{ env.MAVEN_USERNAME!='' && env.MAVEN_PASSWORD!='' }}
        run: mvn --batch-mode -Darguments="-Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }}" -DsignTag=true -DtagNameFormat="${{ github.event.inputs.releaseVersion }}" -DreleaseVersion=${{ github.event.inputs.releaseVersion }} -DdevelopmentVersion=${{ github.event.inputs.developmentVersion }} release:prepare release:perform --file pom.xml
      - name: Release on GitHub
        uses: softprops/action-gh-release@v2
        uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2
        with:
          name: ${{ github.event.inputs.releaseVersion }}
          tag_name: ${{ github.event.inputs.releaseVersion }}
@@ -154,6 +161,10 @@
    runs-on: 'ubuntu-latest'
    needs:
      - release-maven
    # packages: write is required for the GHCR login that authenticates with GITHUB_TOKEN.
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v6
        with:
@@ -162,7 +173,7 @@
          submodules: recursive
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v6
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
        with:
          images: |
            ${{ github.repository }}
@@ -171,22 +182,22 @@
            type=raw,value=latest
            type=raw,value=${{ github.event.inputs.releaseVersion }}
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v4
        uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
      - name: Login to DockerHub
        uses: docker/login-action@v4
        uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      - name: Login to GHCR
        uses: docker/login-action@v4
        uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build and push image
        uses: docker/build-push-action@v7
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        continue-on-error: true
        with:
          context: ./opendj-packages/opendj-docker
@@ -202,6 +213,10 @@
    runs-on: 'ubuntu-latest'
    needs:
      - release-maven
    # packages: write is required for the GHCR login that authenticates with GITHUB_TOKEN.
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v6
        with:
@@ -210,7 +225,7 @@
          submodules: recursive
      - name: Docker meta
        id: meta
        uses: docker/metadata-action@v6
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
        with:
          images: |
            ${{ github.repository }}
@@ -219,23 +234,23 @@
            type=raw,value=alpine
            type=raw,value=${{ github.event.inputs.releaseVersion }}-alpine
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v4
        uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v4
        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
      - name: Login to DockerHub
        uses: docker/login-action@v4
        uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      - name: Login to GHCR
        uses: docker/login-action@v4
        uses: docker/login-action@371161bbe7024a29a25c5e19bfcbc0804fe9ad2c # v4.5.2
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build and push image
        continue-on-error: true
        uses: docker/build-push-action@v7
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        with:
          context: ./opendj-packages/opendj-docker
          file: ./opendj-packages/opendj-docker/Dockerfile-alpine
opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommand.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package com.forgerock.opendj.cli;
@@ -226,10 +227,6 @@
            throw new ArgumentException(ERR_ARG_SUBCOMMAND_DUPLICATE_ARGUMENT_NAME.get(name, argumentLongID));
        }
        if (parser.hasGlobalArgument(argumentLongID)) {
            throw new ArgumentException(ERR_ARG_SUBCOMMAND_ARGUMENT_GLOBAL_CONFLICT.get(argumentLongID, name));
        }
        Character shortID = argument.getShortIdentifier();
        if (shortID != null) {
            if (shortIDMap.containsKey(shortID)) {
opendj-cli/src/main/java/com/forgerock/opendj/cli/SubCommandArgumentParser.java
@@ -13,7 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2018-2024 3A Systems, LLC.
 * Portions Copyright 2018-2026 3A Systems, LLC.
 */
package com.forgerock.opendj.cli;
@@ -63,8 +63,6 @@
    private final Map<Character, Argument> globalShortIDMap = new HashMap<>();
    /** The set of global arguments defined for this parser, referenced by long ID. */
    private final Map<String, Argument> globalLongIDMap = new HashMap<>();
    /** The set of global arguments defined for this parser, referenced by argument name. */
    private final Map<String, Argument> globalArgumentMap = new HashMap<>();
    /** The total set of global arguments defined for this parser. */
    private final List<Argument> globalArgumentList = new LinkedList<>();
    /** The set of subcommands defined for this parser, referenced by subcommand name. */
@@ -98,7 +96,7 @@
     * @return <CODE>true</CODE> if a global argument exists with the specified name, or <CODE>false</CODE> if not.
     */
    public boolean hasGlobalArgument(String argumentName) {
        return globalArgumentMap.containsKey(argumentName);
        return getGlobalArgumentForLongID(formatLongIdentifier(argumentName)) != null;
    }
    /**
@@ -199,9 +197,6 @@
     */
    public void addGlobalArgument(Argument argument, ArgumentGroup group) throws ArgumentException {
        String longID = argument.getLongIdentifier();
        if (globalArgumentMap.containsKey(longID)) {
            throw new ArgumentException(ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_NAME.get(longID));
        }
        for (SubCommand s : subCommands.values()) {
            if (s.getArgumentForLongIdentifier(longID) != null) {
                throw new ArgumentException(ERR_SUBCMDPARSER_GLOBAL_ARG_NAME_SUBCMD_CONFLICT.get(
@@ -226,11 +221,9 @@
            }
        }
        if (!longArgumentsCaseSensitive()) {
            longID = toLowerCase(longID);
            if (globalLongIDMap.containsKey(longID)) {
                throw new ArgumentException(ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_LONG_ID.get(longID));
            }
        longID = formatLongIdentifier(longID);
        if (globalLongIDMap.containsKey(longID)) {
            throw new ArgumentException(ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_LONG_ID.get(longID));
        }
        for (SubCommand s : subCommands.values()) {
opendj-cli/src/test/java/com/forgerock/opendj/cli/TestSubCommandArgumentParserTestCase.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package com.forgerock.opendj.cli;
@@ -157,4 +158,60 @@
        SubCommandArgumentParser.indentAndWrap(indent, buffer, wrapColumn, LocalizableMessage.raw(text));
        Assertions.assertThat(buffer.toString()).isEqualTo(expected);
    }
    private static SubCommandArgumentParser newParser(final boolean longArgumentsCaseSensitive) {
        return new SubCommandArgumentParser(TestSubCommandArgumentParserTestCase.class.getName(),
                LocalizableMessage.raw("test description"), longArgumentsCaseSensitive);
    }
    private static Argument booleanArg(final String longID) throws ArgumentException {
        return BooleanArgument.builder(longID).description(LocalizableMessage.raw(longID)).buildArgument();
    }
    /** A registered global argument must be discoverable by its long identifier. */
    @Test
    public void testHasGlobalArgument() throws Exception {
        final SubCommandArgumentParser caseInsensitive = newParser(false);
        caseInsensitive.addGlobalArgument(booleanArg("globalArg"));
        Assertions.assertThat(caseInsensitive.hasGlobalArgument("globalArg")).isTrue();
        Assertions.assertThat(caseInsensitive.hasGlobalArgument("GLOBALARG")).isTrue();
        Assertions.assertThat(caseInsensitive.hasGlobalArgument("otherArg")).isFalse();
        final SubCommandArgumentParser caseSensitive = newParser(true);
        caseSensitive.addGlobalArgument(booleanArg("globalArg"));
        Assertions.assertThat(caseSensitive.hasGlobalArgument("globalArg")).isTrue();
        Assertions.assertThat(caseSensitive.hasGlobalArgument("GLOBALARG")).isFalse();
    }
    /** Two global arguments sharing a long identifier must be rejected, whatever the case sensitivity. */
    @Test
    public void testDuplicateGlobalArgumentIsRejected() throws Exception {
        for (final boolean caseSensitive : new boolean[] { false, true }) {
            final SubCommandArgumentParser aParser = newParser(caseSensitive);
            aParser.addGlobalArgument(booleanArg("globalArg"));
            try {
                aParser.addGlobalArgument(booleanArg("globalArg"));
                Assert.fail("A duplicate global argument should have been rejected "
                        + "(longArgumentsCaseSensitive=" + caseSensitive + ")");
            } catch (final ArgumentException expected) {
                // Expected.
            }
        }
    }
    /** A sub-command argument must not shadow a global argument. */
    @Test
    public void testSubCommandArgumentConflictingWithGlobalIsRejected() throws Exception {
        final SubCommandArgumentParser aParser = newParser(false);
        aParser.addGlobalArgument(booleanArg("globalArg"));
        final SubCommand subCommand =
                new SubCommand(aParser, "a-sub-command", LocalizableMessage.raw("a-sub-command"));
        try {
            subCommand.addArgument(booleanArg("globalArg"));
            Assert.fail("A sub-command argument conflicting with a global argument should have been rejected");
        } catch (final ArgumentException expected) {
            // Expected.
        }
    }
}
opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/DSConfig.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2007-2010 Sun Microsystems, Inc.
 * Portions Copyright 2012-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.forgerock.opendj.config.dsconfig;
@@ -1168,7 +1169,6 @@
        final Map<RelationDefinition<?, ?>, CreateSubCommandHandler<?, ?>> createHandlers = new HashMap<>();
        final Map<RelationDefinition<?, ?>, DeleteSubCommandHandler> deleteHandlers = new HashMap<>();
        final Map<RelationDefinition<?, ?>, ListSubCommandHandler> listHandlers = new HashMap<>();
        final Map<RelationDefinition<?, ?>, GetPropSubCommandHandler> getPropHandlers = new HashMap<>();
        final Map<RelationDefinition<?, ?>, SetPropSubCommandHandler> setPropHandlers = new HashMap<>();
        for (final CreateSubCommandHandler<?, ?> ch : handlerFactory.getCreateSubCommandHandlers()) {
@@ -1188,7 +1188,6 @@
        for (final GetPropSubCommandHandler gh : handlerFactory.getGetPropSubCommandHandlers()) {
            relations.add(gh.getRelationDefinition());
            getPropHandlers.put(gh.getRelationDefinition(), gh);
        }
        for (final SetPropSubCommandHandler sh : handlerFactory.getSetPropSubCommandHandlers()) {
opendj-config/src/main/java/org/forgerock/opendj/config/dsconfig/PropertyValueEditor.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.forgerock.opendj.config.dsconfig;
@@ -857,10 +858,7 @@
                    @Override
                    public MenuResult<Boolean> invoke(ConsoleApplication app) throws ClientException {
                        app.println();
                        SortedSet<T> previousValues = new TreeSet<>(currentValues);
                        readPropertyValues(app, mo.getManagedObjectDefinition(), d, currentValues);
                        SortedSet<T> addedValues = new TreeSet<>(currentValues);
                        addedValues.removeAll(previousValues);
                        isLastChoiceReset = false;
                        return MenuResult.success(false);
                    }
opendj-core/src/main/java/org/forgerock/opendj/ldap/AttributeDescription.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2009-2010 Sun Microsystems, Inc.
 * Portions copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.forgerock.opendj.ldap;
@@ -62,7 +63,12 @@
        public abstract boolean hasOption(String normalizedOption);
        public abstract boolean equals(Impl other);
        /**
         * Options are compared by value, so this method must be kept consistent
         * with {@link #hashCode()}.
         */
        @Override
        public abstract boolean equals(Object other);
        public abstract String firstNormalizedOption();
@@ -130,7 +136,7 @@
        }
        @Override
        public boolean equals(final Impl other) {
        public boolean equals(final Object other) {
            if (other instanceof MultiOptionImpl) {
                final MultiOptionImpl tmp = (MultiOptionImpl) other;
                return Arrays.equals(normalizedOptions, tmp.normalizedOptions);
@@ -229,8 +235,13 @@
        }
        @Override
        public boolean equals(final Impl other) {
            return other.size() == 1 && other.hasOption(normalizedOption);
        public boolean equals(final Object other) {
            if (other instanceof Impl) {
                final Impl tmp = (Impl) other;
                return tmp.size() == 1 && tmp.hasOption(normalizedOption);
            } else {
                return false;
            }
        }
        @Override
@@ -289,7 +300,7 @@
        }
        @Override
        public boolean equals(final Impl other) {
        public boolean equals(final Object other) {
            return this == other;
        }
opendj-doc-maven-plugin/src/main/java/org/forgerock/opendj/maven/doc/GenerateMessageFileMojo.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.forgerock.opendj.maven.doc;
@@ -32,6 +33,7 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
@@ -47,7 +49,6 @@
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.forgerock.i18n.LocalizableMessage;
/** Generates an XML file of log messages found in properties files. */
@Mojo(name = "generate-xml-messages-doc", defaultPhase = PRE_SITE)
@@ -69,34 +70,6 @@
    /** A list which contains all file names, the extension is not needed. */
    @Parameter(required = true)
    private List<String> messageFileNames;
    /** One-line descriptions for log reference categories. */
    private static final Map<String, LocalizableMessage> CATEGORY_DESCRIPTIONS = new HashMap<>();
    static {
        CATEGORY_DESCRIPTIONS.put("ACCESS_CONTROL", CATEGORY_ACCESS_CONTROL.get());
        CATEGORY_DESCRIPTIONS.put("ADMIN", CATEGORY_ADMIN.get());
        CATEGORY_DESCRIPTIONS.put("ADMIN_TOOL", CATEGORY_ADMIN_TOOL.get());
        CATEGORY_DESCRIPTIONS.put("AUDIT", CATEGORY_AUDIT.get());
        CATEGORY_DESCRIPTIONS.put("BACKEND", CATEGORY_BACKEND.get());
        CATEGORY_DESCRIPTIONS.put("CONFIG", CATEGORY_CONFIG.get());
        CATEGORY_DESCRIPTIONS.put("CORE", CATEGORY_CORE.get());
        CATEGORY_DESCRIPTIONS.put("DSCONFIG", CATEGORY_DSCONFIG.get());
        CATEGORY_DESCRIPTIONS.put("EXTENSIONS", CATEGORY_EXTENSIONS.get());
        CATEGORY_DESCRIPTIONS.put("JVM", CATEGORY_JVM.get());
        CATEGORY_DESCRIPTIONS.put("LOG", CATEGORY_LOG.get());
        CATEGORY_DESCRIPTIONS.put("PLUGIN", CATEGORY_PLUGIN.get());
        CATEGORY_DESCRIPTIONS.put("PROTOCOL", CATEGORY_PROTOCOL.get());
        CATEGORY_DESCRIPTIONS.put("QUICKSETUP", CATEGORY_QUICKSETUP.get());
        CATEGORY_DESCRIPTIONS.put("RUNTIME_INFORMATION", CATEGORY_RUNTIME_INFORMATION.get());
        CATEGORY_DESCRIPTIONS.put("SCHEMA", CATEGORY_SCHEMA.get());
        CATEGORY_DESCRIPTIONS.put("SDK", CATEGORY_SDK.get());
        CATEGORY_DESCRIPTIONS.put("SYNC", CATEGORY_SYNC.get());
        CATEGORY_DESCRIPTIONS.put("TASK", CATEGORY_TASK.get());
        CATEGORY_DESCRIPTIONS.put("THIRD_PARTY", CATEGORY_THIRD_PARTY.get());
        CATEGORY_DESCRIPTIONS.put("TOOLS", CATEGORY_TOOLS.get());
        CATEGORY_DESCRIPTIONS.put("USER_DEFINED", CATEGORY_USER_DEFINED.get());
        CATEGORY_DESCRIPTIONS.put("UTIL", CATEGORY_UTIL.get());
        CATEGORY_DESCRIPTIONS.put("VERSION", CATEGORY_VERSION.get());
    }
    /** Message giving formatting rules for string keys. */
    public static final String KEY_FORM_MSG = ".\n\nOpenDJ message property keys must be of the form\n\n"
@@ -173,16 +146,26 @@
        }
        /**
         * Compare message entries by unique identifier.
         * Compare message entries by ordinal, then by unique identifier.
         * <p>
         * Entries are held in a {@code TreeSet}, so this must be a total order:
         * returning 0 for entries that are not the same message drops them from
         * the log reference. Entries without an ordinal sort first, as they do
         * in {@link MessagePropertyKey#compareTo(MessagePropertyKey)}.
         *
         * @return See {@link java.lang.Comparable#compareTo(Object)}.
         */
        @Override
        public int compareTo(MessageRefEntry mre) {
            if (this.ordinal != null && mre.ordinal != null) {
                return this.ordinal.compareTo(mre.ordinal);
            if (Objects.equals(ordinal, mre.ordinal)) {
                return xmlId.compareTo(mre.xmlId);
            } else if (ordinal == null) {
                return -1;
            } else if (mre.ordinal == null) {
                return 1;
            } else {
                return ordinal.compareTo(mre.ordinal);
            }
            return 0;
        }
    }
@@ -273,12 +256,14 @@
        @Override
        public int compareTo(MessagePropertyKey k) {
            if (ordinal == k.ordinal) {
            if (Objects.equals(ordinal, k.ordinal)) {
                return description.compareTo(k.description);
            } else if (ordinal != null && k.ordinal != null) {
                return ordinal.compareTo(k.ordinal);
            } else if (ordinal == null) {
                return -1;
            } else if (k.ordinal == null) {
                return 1;
            } else {
                return 0;
                return ordinal.compareTo(k.ordinal);
            }
        }
    }
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/browser/NodeRefresher.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2012-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.guitools.controlpanel.browser;
@@ -460,7 +461,7 @@
        }
        conn = connectionPool.getConnection(url);
        remoteDn = DN.valueOf(url.getRawBaseDN());
        if (remoteDn == null || "".equals(remoteDn))
        if (remoteDn.isRootDN())
        {
          /* The referral has not a target DN specified: we
             have to use the DN of the entry that contains the
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/browser/NodeSearcherQueue.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2015-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.guitools.controlpanel.browser;
@@ -37,7 +38,6 @@
  private final String name;
  private final List<AbstractNodeTask> waitingQueue = new ArrayList<>();
  private final Map<BasicNode, AbstractNodeTask> workingList = new HashMap<>();
  private final Map<BasicNode, BasicNode> cancelList = new HashMap<>();
  private final ThreadGroup threadGroup;
@@ -111,7 +111,6 @@
    // Mark the on-going task as cancelled
    AbstractNodeTask task = workingList.get(node);
    if (task != null) {
      cancelList.put(node, node);
      task.cancel();
    }
    notify();
@@ -134,10 +133,7 @@
    waitingQueue.clear();
    for (Map.Entry<BasicNode, AbstractNodeTask> entry : workingList.entrySet())
    {
      BasicNode node = entry.getKey();
      AbstractNodeTask task = entry.getValue();
      cancelList.put(node, node);
      task.cancel();
      entry.getValue().cancel();
    }
  }
@@ -201,7 +197,6 @@
      throw new IllegalArgumentException("null argument");
    }
    workingList.remove(task.getNode());
    cancelList.remove(task.getNode());
    notify();
//    System.out.println("Flushed " + task + " from " + _name);
  }
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/datamodel/ControlPanelInfo.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.guitools.controlpanel.datamodel;
@@ -86,9 +87,17 @@
  private final IconPool iconPool = new IconPool();
  private long poolingPeriod = 20000;
  /**
   * Guards the pooling thread life cycle.  A dedicated lock is used on purpose:
   * the pooling thread calls methods that are synchronized on this object (for
   * instance {@link #regenerateDescriptor()}), so waiting for that thread to
   * die while holding the monitor of this object would deadlock.
   */
  private final Object poolingLock = new Object();
  /** Guarded by {@link #poolingLock}. */
  private Thread poolingThread;
  private boolean stopPooling;
  private boolean pooling;
  /** Read by the pooling thread without any lock, hence volatile. */
  private volatile boolean stopPooling;
  private ApplicationTrustManager trustManager;
  private int connectTimeout = CliConstants.DEFAULT_LDAP_CONNECT_TIMEOUT;
@@ -694,59 +703,81 @@
   * specified as a parameter.  This method is asynchronous and it will start
   * the pooling in another thread.
   */
  public synchronized void startPooling()
  public void startPooling()
  {
    if (poolingThread != null)
    synchronized (poolingLock)
    {
      return;
    }
    pooling = true;
    stopPooling = false;
    poolingThread = new Thread(new Runnable()
    {
      @Override
      public void run()
      if (poolingThread != null && poolingThread.isAlive())
      {
        try
        return;
      }
      stopPooling = false;
      poolingThread = new Thread(new Runnable()
      {
        @Override
        public void run()
        {
          while (!stopPooling)
          try
          {
            cleanupTasks();
            regenerateDescriptor();
            Thread.sleep(poolingPeriod);
            while (!stopPooling)
            {
              cleanupTasks();
              regenerateDescriptor();
              Thread.sleep(poolingPeriod);
            }
          }
          catch (InterruptedException e)
          {
            // stopPooling() asked this thread to stop.
          }
          catch (Throwable t)
          {
            logger.warn(LocalizableMessage.raw("Error polling the server: " + t, t));
          }
        }
        catch (Throwable t)
        {
        }
        pooling = false;
      }
    });
    poolingThread.start();
      });
      poolingThread.start();
    }
  }
  /**
   * Stops pooling the server.  This method is synchronous, it does not return
   * until the pooling is actually stopped.
   */
  public synchronized void stopPooling()
  public void stopPooling()
  {
    stopPooling = true;
    while (poolingThread != null && pooling)
    synchronized (poolingLock)
    {
      try
      stopPooling = true;
      boolean interrupted = false;
      if (poolingThread != null)
      {
        poolingThread.interrupt();
        Thread.sleep(100);
        // Keep interrupting the pooling thread until it dies: it may be blocked
        // in an operation that swallows the interruption.  Note that this must
        // not be done while holding the monitor of this object, see poolingLock.
        while (poolingThread.isAlive())
        {
          poolingThread.interrupt();
          try
          {
            poolingThread.join(100);
          }
          catch (InterruptedException e)
          {
            // Do not give up the wait: this method must not return before the
            // pooling thread has actually stopped.  The interruption is
            // propagated to the caller once the thread is dead.
            interrupted = true;
          }
        }
        poolingThread = null;
      }
      catch (Throwable t)
      if (interrupted)
      {
        // do nothing;
        Thread.currentThread().interrupt();
      }
    }
    poolingThread = null;
    pooling = false;
  }
  /**
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/task/DeleteEntryTask.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.guitools.controlpanel.task;
@@ -315,7 +316,7 @@
      while (entryDNs.hasNext())
      {
        SearchResultEntry sr = entryDNs.readEntry();
        if (!sr.getName().equals(""))
        if (!sr.getName().isRootDN())
        {
          deleteSubtreeRecursively(conn, sr.getName(), null, toNotify);
        }
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanel.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2009-2010 Sun Microsystems, Inc.
 * Portions Copyright 2012-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.guitools.controlpanel.ui;
@@ -43,7 +44,10 @@
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.schema.MatchingRule;
import org.forgerock.opendj.ldap.schema.Schema;
import org.opends.guitools.controlpanel.browser.BrowserController;
import org.opends.guitools.controlpanel.browser.ConnectionWithControls;
import org.opends.guitools.controlpanel.ui.nodes.BasicNode;
@@ -342,33 +346,48 @@
  @Override
  protected String getLDIF()
  {
    String dn = this.dn.getText();
    return getLDIF(entryToDuplicate, dn.getText(), rdnAttribute, new String(password.getPassword()),
        getInfo().getServerDescriptor().getSchema());
  }
  /**
   * Returns the LDIF representation of the duplicated entry.
   *
   * @param entryToDuplicate the entry that is being duplicated.
   * @param dn the DN of the new entry.
   * @param rdnAttribute the name of the attribute used in the RDN of the entry being duplicated.
   * @param password the password typed by the user, empty if the user typed none.
   * @param schema the schema of the server, {@code null} if it could not be read.
   * @return the LDIF representation of the duplicated entry.
   */
  static String getLDIF(Entry entryToDuplicate, String dn, String rdnAttribute, String password, Schema schema)
  {
    StringBuilder sb = new StringBuilder();
    sb.append("dn: ").append(dn);
    for (Attribute attr : entryToDuplicate.getAllAttributes())
    {
      AttributeDescription attrDesc = attr.getAttributeDescription();
      String attrName = attr.getAttributeDescriptionAsString();
      if (attrDesc.equals(getUserPasswordAttributeType()))
      if (attrDesc.getAttributeType().equals(getUserPasswordAttributeType()))
      {
        sb.append("\n");
        String pwd = new String(password.getPassword());
        if (!pwd.isEmpty())
        // The password is optional: the original password must not be copied and no line at all
        // must be written when the user typed none.  Writing an empty line would terminate the
        // LDIF record and silently drop every attribute that comes after this one.
        if (!password.isEmpty())
        {
          sb.append(attrName).append(": ").append(pwd);
          sb.append("\n").append(attrName).append(": ").append(password);
        }
      }
      else if (!attrName.equalsIgnoreCase(rdnAttribute))
      {
        if (!ViewEntryPanel.isEditable(attrDesc,
            getInfo().getServerDescriptor().getSchema()))
        if (!ViewEntryPanel.isEditable(attrDesc, schema))
        {
          continue;
        }
        for (ByteString value : attr)
        {
          sb.append("\n");
          if (isBinary(attrName))
          if (Utilities.hasBinarySyntax(attrName, schema))
          {
            sb.append(attrName).append(":: ").append(value.toBase64String());
          }
@@ -392,7 +411,7 @@
          for (ByteString value : attr)
          {
            sb.append("\n");
            if (oldValue.equals(value))
            if (isRDNValue(attrDesc, value, oldValue))
            {
              sb.append(attrName).append(": ").append(newValue);
            }
@@ -407,7 +426,33 @@
    return sb.toString();
  }
  private String getFirstValue(DN dn)
  /**
   * Returns whether the provided value is the value the RDN of the entry being duplicated asserts.
   * <p>
   * The attributes used in an RDN usually have a case insensitive equality matching rule
   * ({@code cn}, {@code ou} or {@code uid} for instance) and the value stored in the entry may
   * therefore differ from the value written in the DN.  The comparison must consequently be done
   * through the equality matching rule of the attribute and not with {@link String#equals(Object)}.
   */
  private static boolean isRDNValue(AttributeDescription attrDesc, ByteString value, String rdnValue)
  {
    MatchingRule equalityMatchingRule = attrDesc.getAttributeType().getEqualityMatchingRule();
    if (equalityMatchingRule != null)
    {
      try
      {
        return equalityMatchingRule.normalizeAttributeValue(value)
            .equals(equalityMatchingRule.normalizeAttributeValue(ByteString.valueOfUtf8(rdnValue)));
      }
      catch (DecodeException e)
      {
        // The values cannot be normalized: fall back to the case insensitive comparison below.
      }
    }
    return rdnValue.equalsIgnoreCase(value.toString());
  }
  private static String getFirstValue(DN dn)
  {
    return dn.rdn().getFirstAVA().getAttributeValue().toString();
  }
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/NewIndexPanel.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2009 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.guitools.controlpanel.ui;
@@ -120,7 +121,6 @@
      BackendDescriptor backend = getBackendByID(backendName.getText());
      TreeSet<String> standardAttrNames = new TreeSet<>();
      TreeSet<String> configurationAttrNames = new TreeSet<>();
      TreeSet<String> customAttrNames = new TreeSet<>();
      for (AttributeType attr : schema.getAttributeTypes())
      {
@@ -131,12 +131,9 @@
          {
            standardAttrNames.add(name);
          }
          else if (Utilities.isConfiguration(attr))
          else if (!Utilities.isConfiguration(attr))
          {
            configurationAttrNames.add(name);
          }
          else
          {
            // Configuration attributes are not offered for indexing.
            customAttrNames.add(name);
          }
        }
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanel.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.guitools.controlpanel.ui;
@@ -571,15 +572,7 @@
  {
    if (OBJECTCLASS_ATTRIBUTE_TYPE_NAME.equalsIgnoreCase(attr))
    {
      int nOcs = 0;
      for (ByteString v : values)
      {
        if (!"top".equals(v))
        {
          nOcs++;
        }
      }
      return nOcs > 1 ? GridBagConstraints.NORTHWEST : GridBagConstraints.WEST;
      return countObjectClassesBesidesTop(values) > 1 ? GridBagConstraints.NORTHWEST : GridBagConstraints.WEST;
    }
    else if (isSingleValue(attr))
    {
@@ -595,6 +588,26 @@
    }
  }
  /**
   * Returns the number of object classes of an entry, not counting {@code top}.
   *
   * @param values the values of the object class attribute of the entry.
   * @return the number of object classes, not counting {@code top}.
   */
  static int countObjectClassesBesidesTop(List<ByteString> values)
  {
    int nOcs = 0;
    for (ByteString v : values)
    {
      // Object class values are case insensitive.
      if (!OC_TOP.equalsIgnoreCase(v.toString()))
      {
        nOcs++;
      }
    }
    return nOcs;
  }
  private int anchor1(List<ByteString> values)
  {
    int size = values.size();
@@ -693,7 +706,7 @@
    }
    // Handle the root entry separately: most of its attributes are operational
    // so we filter a list of hardcoded attributes.
    boolean isRootEntry = "".equals(sr.getName());
    boolean isRootEntry = sr.getName().isRootDN();
    Schema schema = getInfo().getServerDescriptor().getSchema();
    if (isRootEntry)
    {
@@ -984,7 +997,7 @@
      else if (isConfirmPassword(attrName) || isPassword(attrName))
      {
        JPasswordField pf = Utilities.createPasswordField();
        if (!"".equals(v))
        if (!v.isEmpty())
        {
          pf.setText(getPasswordStringValue(getAttributeForConfirmPasswordKey(attrName), v));
        }
opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanel.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.guitools.controlpanel.ui;
@@ -383,6 +384,37 @@
    return tableModel.getValues(attrName);
  }
  /**
   * Returns whether the provided attribute must be displayed as an {@link ObjectClassValue}
   * instead of as a plain list of values.
   * <p>
   * The object class descriptor cannot be built without the schema, so when the schema could not
   * be read the raw values must be displayed instead: otherwise the object class attribute would
   * not be displayed at all.
   *
   * @param attrDesc the description of the attribute.
   * @param schema the schema of the server, {@code null} if it could not be read.
   * @return {@code true} if the attribute must be displayed as an {@link ObjectClassValue},
   *         {@code false} otherwise.
   */
  static boolean isObjectClassWithSchema(AttributeDescription attrDesc, Schema schema)
  {
    return schema != null && attrDesc.getAttributeType().equals(getObjectClassAttributeType());
  }
  /**
   * Returns whether the provided attribute is one of the required attributes of the entry.
   *
   * @param requiredAttrs the names of the required attributes, in lower case.
   * @param attrDesc the description of the attribute.
   * @return {@code true} if the attribute is required, {@code false} otherwise.
   */
  static boolean isRequired(Set<String> requiredAttrs, AttributeDescription attrDesc)
  {
    // The required attributes are stored in lower case since attribute names are case insensitive.
    return requiredAttrs.contains(attrDesc.getNameOrOID().toLowerCase());
  }
  /** The table model used by the tree in the panel. */
  private class LDAPEntryTableModel extends SortableTableModel
  implements Comparator<AttributeValuePair>
@@ -596,14 +628,11 @@
      for (Attribute attr : searchResult.getAllAttributes())
      {
        AttributeDescription attrDesc = attr.getAttributeDescription();
        if (attrDesc.equals(getObjectClassAttributeType()))
        if (isObjectClassWithSchema(attrDesc, schema))
        {
          if (schema != null)
          {
            ocs = attr;
            ObjectClassValue ocValue = getObjectClassDescriptor(ocs, schema);
            allSortedValues.add(new AttributeValuePair(attrDesc, ocValue));
          }
          ocs = attr;
          ObjectClassValue ocValue = getObjectClassDescriptor(ocs, schema);
          allSortedValues.add(new AttributeValuePair(attrDesc, ocValue));
        }
        else
        {
@@ -781,7 +810,7 @@
    private boolean isRequired(AttributeValuePair value)
    {
      return requiredAttrs.contains(value.attrDesc.getNameOrOID());
      return TableViewEntryPanel.isRequired(requiredAttrs, value.attrDesc);
    }
    private boolean hasValue(AttributeValuePair value)
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/OnDiskMergeImporter.java
@@ -1251,10 +1251,8 @@
    try (final PhaseTwoProgressReporter progressReporter = new PhaseTwoProgressReporter())
    {
      final List<Callable<Void>> tasks = new ArrayList<>();
      final Set<String> importedBaseDNs = new HashSet<>();
      for (Map.Entry<TreeName, Chunk> treeChunk : transaction.getChunks().entrySet())
      {
        importedBaseDNs.add(treeChunk.getKey().getBaseDN());
        tasks.add(importStrategy.newPhaseTwoTask(treeChunk.getKey(), treeChunk.getValue(), progressReporter));
      }
      invokeParallel(phase2ThreadNameTemplate, tasks);
opendj-server-legacy/src/main/java/org/opends/server/core/EntryCacheConfigManager.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2013-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.core;
@@ -20,7 +21,6 @@
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.util.Utils;
import org.forgerock.opendj.config.ClassPropertyDefinition;
import org.forgerock.opendj.config.server.ConfigurationAddListener;
@@ -207,9 +207,9 @@
    if (!cacheOrderMap.isEmpty() && !cacheNameToLevelMap.isEmpty())
    {
      final ByteString normDN = configuration.dn().toNormalizedByteString();
      if (cacheNameToLevelMap.containsKey(normDN)) {
        int currentCacheLevel = cacheNameToLevelMap.get(normDN);
      final DN dn = configuration.dn();
      if (cacheNameToLevelMap.containsKey(dn)) {
        int currentCacheLevel = cacheNameToLevelMap.get(dn);
        // Check if there any existing cache at the same level.
        if (currentCacheLevel != configuration.getCacheLevel() &&
@@ -411,7 +411,7 @@
      }
      entryCache.finalizeEntryCache();
      cacheOrderMap.remove(configuration.getCacheLevel());
      cacheNameToLevelMap.remove(configuration.dn().toNormalizedByteString());
      cacheNameToLevelMap.remove(configuration.dn());
      // Push any changes made to the cache order map.
      setCacheOrder(cacheOrderMap);
opendj-server-legacy/src/main/java/org/opends/server/extensions/BCrypt.java
@@ -437,7 +437,7 @@
   * @return  the decoded value of x
   */
  private static byte char64(char x) {
    if (x < 0 || x > index_64.length) {
    if (x >= index_64.length) {
      return -1;
    }
    return index_64[x];
opendj-server-legacy/src/main/java/org/opends/server/extensions/ErrorLogAccountStatusNotificationHandler.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2008 Sun Microsystems, Inc.
 * Portions Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.extensions;
@@ -53,19 +54,6 @@
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  /**
   * The set of names for the account status notification types that may be
   * logged by this notification handler.
   */
  private static final HashSet<String> NOTIFICATION_TYPE_NAMES = new HashSet<>();
  static
  {
    for (AccountStatusNotificationType t : AccountStatusNotificationType.values())
    {
      NOTIFICATION_TYPE_NAMES.add(t.getName());
    }
  }
  /** The DN of the configuration entry for this notification handler. */
  private DN configEntryDN;
opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java
@@ -12,6 +12,7 @@
 * information: "Portions Copyright [year] [name of copyright owner]".
 *
 * Copyright 2014-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.replication.server.changelog.file;
@@ -983,7 +984,7 @@
    private void tracePurgeDetails(final CSN purgeCSN, final CSN oldestNotPurgedCSN, final long sleepTime)
    {
      if (purgeCSN.equals(oldestNotPurgedCSN.toStringUI()))
      if (purgeCSN.equals(oldestNotPurgedCSN))
      {
        logger.trace("Purged up to %s. "
            + "now sleeping until next purge during %s",
opendj-server-legacy/src/main/java/org/opends/server/util/cli/LDAPConnectionConsoleInteraction.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.util.cli;
@@ -1603,12 +1604,10 @@
    {
      logger.warn(ERROR_CERTIFICATE_NULL_AUTH_TYPE.get());
    }
    else
    {
      app.println(ApplicationTrustManager.Cause.NOT_TRUSTED.equals(authType)
          ? INFO_CERTIFICATE_NOT_TRUSTED_TEXT_CLI.get(host, port)
          : INFO_CERTIFICATE_NAME_MISMATCH_TEXT_CLI.get(host, port, host, host, port));
    }
    // The explanation only depends on the cause of the rejection, so it must be displayed even
    // when the authentication type is unknown: otherwise the user is prompted to accept a
    // certificate without being told what is wrong with it.
    app.println(getCertificateRejectionMessage(cause, host, port));
    final X509Certificate[] chain = usedTrustManager.getLastRefusedChain();
    if (chain == null)
@@ -1625,6 +1624,21 @@
  }
  /**
   * Returns the message explaining why the certificate presented by the server was rejected.
   *
   * @param cause the reason why the certificate was rejected.
   * @param host the host name of the server.
   * @param port the port of the server.
   * @return the message explaining why the certificate was rejected.
   */
  static LocalizableMessage getCertificateRejectionMessage(ApplicationTrustManager.Cause cause, String host, int port)
  {
    return cause == ApplicationTrustManager.Cause.NOT_TRUSTED
        ? INFO_CERTIFICATE_NOT_TRUSTED_TEXT_CLI.get(host, port)
        : INFO_CERTIFICATE_NAME_MISMATCH_TEXT_CLI.get(host, port, host, host, port);
  }
  /**
   * Sets the heading that is displayed in interactive mode.
   *
   * @param heading
opendj-server-legacy/src/messages/org/opends/messages/protocol.properties
@@ -860,5 +860,5 @@
ERR_SNMP_CONNHANDLER_OPENDMK_JARFILES_NOT_OPERATIONAL_1507=The required \
 classes could not be loaded using jar file '%s'. Verify that the jar file \
 is not corrupted
ERR_HTTP_ERROR_WHILE_PROCESSING_REQUEST_1508=An error occurred while processing the request \
ERR_HTTP_ERROR_WHILE_PROCESSING_REQUEST_1539=An error occurred while processing the request \
 %s: %s
opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanelTestCase.java
New file
@@ -0,0 +1,181 @@
/*
 * 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.guitools.controlpanel.ui;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import org.forgerock.opendj.ldap.Attribute;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Entry;
import org.forgerock.opendj.ldap.LinkedHashMapEntry;
import org.forgerock.opendj.ldap.schema.Schema;
import org.forgerock.opendj.ldif.LDIFEntryReader;
import org.opends.server.DirectoryServerTestCase;
import org.testng.annotations.Test;
/** Tests the LDIF generated by {@link DuplicateEntryPanel}. */
@SuppressWarnings("javadoc")
public class DuplicateEntryPanelTestCase extends DirectoryServerTestCase
{
  private static final Schema SCHEMA = Schema.getCoreSchema();
  private static final String NEW_DN = "uid=user.2,ou=people,dc=example,dc=com";
  /** An entry whose RDN attribute is single valued and which holds a password. */
  private static Entry userEntry()
  {
    return new LinkedHashMapEntry(
        "dn: uid=user.1,ou=people,dc=example,dc=com",
        "objectClass: top",
        "objectClass: person",
        "objectClass: organizationalPerson",
        "objectClass: inetOrgPerson",
        "uid: user.1",
        "userPassword: {SSHA}originalHashOfTheEntryBeingDuplicated",
        "cn: User 1",
        "sn: One");
  }
  /**
   * When the user types no password, the original password must not be copied and no empty line
   * must be written: an empty line terminates the LDIF record and every attribute written after
   * the password would be silently dropped.
   */
  @Test
  public void testEmptyPasswordDoesNotTruncateTheEntry() throws Exception
  {
    String ldif = DuplicateEntryPanel.getLDIF(userEntry(), NEW_DN, "uid", "", SCHEMA);
    assertThat(ldif).doesNotContain("\n\n");
    Entry duplicate = readEntry(ldif);
    assertThat((Object) duplicate.getName()).isEqualTo(DN.valueOf(NEW_DN));
    assertThat(valuesOf(duplicate, "userPassword")).isEmpty();
    assertThat(valuesOf(duplicate, "objectClass"))
        .containsExactly("top", "person", "organizationalPerson", "inetOrgPerson");
    assertThat(valuesOf(duplicate, "uid")).containsExactly("user.2");
    assertThat(valuesOf(duplicate, "cn")).containsExactly("User 1");
    assertThat(valuesOf(duplicate, "sn")).containsExactly("One");
  }
  /** The password of the duplicated entry is the one typed by the user, not the original hash. */
  @Test
  public void testTypedPasswordReplacesTheOriginalOne() throws Exception
  {
    String ldif = DuplicateEntryPanel.getLDIF(userEntry(), NEW_DN, "uid", "typedPassword", SCHEMA);
    Entry duplicate = readEntry(ldif);
    assertThat(valuesOf(duplicate, "userPassword")).containsExactly("typedPassword");
    assertThat(valuesOf(duplicate, "sn")).containsExactly("One");
  }
  /** The value of the RDN of the new entry replaces the value of the RDN of the original entry. */
  @Test
  public void testSingleValuedRDNAttributeUsesTheNewValue() throws Exception
  {
    String ldif = DuplicateEntryPanel.getLDIF(userEntry(), NEW_DN, "uid", "", SCHEMA);
    assertThat(valuesOf(readEntry(ldif), "uid")).containsExactly("user.2");
  }
  /**
   * The attributes used in an RDN have a case insensitive equality matching rule, so the value
   * stored in the entry may differ from the value written in the DN. The old value must be
   * replaced nonetheless, otherwise the duplicated entry keeps the value of the original RDN.
   */
  @Test
  public void testMultiValuedRDNAttributeReplacesTheOldValueCaseInsensitively() throws Exception
  {
    Entry entry = new LinkedHashMapEntry(
        "dn: cn=John Smith,ou=people,dc=example,dc=com",
        "objectClass: top",
        "objectClass: person",
        "cn: john smith",
        "cn: Johnny",
        "sn: Smith");
    String ldif = DuplicateEntryPanel.getLDIF(entry, "cn=John Smith-1,ou=people,dc=example,dc=com", "cn", "", SCHEMA);
    assertThat(valuesOf(readEntry(ldif), "cn")).containsExactlyInAnyOrder("John Smith-1", "Johnny");
  }
  /** Attributes the user cannot modify must not be copied into the duplicated entry. */
  @Test
  public void testNoUserModificationAttributesAreNotCopied() throws Exception
  {
    Entry entry = new LinkedHashMapEntry(
        "dn: uid=user.1,ou=people,dc=example,dc=com",
        "objectClass: top",
        "objectClass: person",
        "uid: user.1",
        "createTimestamp: 20260101000000Z",
        "sn: One");
    String ldif = DuplicateEntryPanel.getLDIF(entry, NEW_DN, "uid", "", SCHEMA);
    Entry duplicate = readEntry(ldif);
    assertThat(valuesOf(duplicate, "createTimestamp")).isEmpty();
    assertThat(valuesOf(duplicate, "sn")).containsExactly("One");
  }
  /** Attributes with a binary syntax must be base64 encoded. */
  @Test
  public void testBinaryAttributesAreBase64Encoded() throws Exception
  {
    Entry entry = new LinkedHashMapEntry(DN.valueOf("uid=user.1,ou=people,dc=example,dc=com"))
        .addAttribute("objectClass", "top", "person")
        .addAttribute("uid", "user.1")
        .addAttribute("sn", "One")
        .addAttribute("userCertificate", ByteString.wrap(new byte[] { 0x30, 0x00, (byte) 0xFF }));
    String ldif = DuplicateEntryPanel.getLDIF(entry, NEW_DN, "uid", "", SCHEMA);
    // "MAD/" is the base64 encoding of the three bytes above.
    assertThat(ldif).contains("userCertificate:: MAD/");
    assertThat(valuesOf(readEntry(ldif), "sn")).containsExactly("One");
  }
  /**
   * Reads the generated LDIF the same way {@code AbstractNewEntryPanel.getEntry()} does: an empty
   * line terminates the record, so anything written after one is not part of the entry that is
   * added to the server.
   */
  private static Entry readEntry(String ldif) throws IOException
  {
    try (LDIFEntryReader reader = new LDIFEntryReader(new StringReader(ldif)))
    {
      return reader.readEntry();
    }
  }
  private static List<String> valuesOf(Entry entry, String attrName)
  {
    List<String> values = new ArrayList<>();
    Attribute attr = entry.getAttribute(attrName);
    if (attr != null)
    {
      for (ByteString value : attr)
      {
        values.add(value.toString());
      }
    }
    return values;
  }
}
opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanelTestCase.java
New file
@@ -0,0 +1,72 @@
/*
 * 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.guitools.controlpanel.ui;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.forgerock.opendj.ldap.ByteString;
import org.opends.server.DirectoryServerTestCase;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/** Tests the object class counting done by {@link SimplifiedViewEntryPanel}. */
@SuppressWarnings("javadoc")
public class SimplifiedViewEntryPanelTestCase extends DirectoryServerTestCase
{
  @DataProvider
  public Object[][] objectClassValues()
  {
    return new Object[][] {
      { Collections.<String> emptyList(), 0 },
      { list("top"), 0 },
      // Object class values are case insensitive: "top" must be filtered out whatever its case is.
      { list("TOP"), 0 },
      { list("Top"), 0 },
      { list("top", "person"), 1 },
      { list("TOP", "person"), 1 },
      { list("top", "person", "organizationalPerson"), 2 },
      { list("person", "organizationalPerson"), 2 },
    };
  }
  @Test(dataProvider = "objectClassValues")
  public void testCountObjectClassesBesidesTop(List<String> objectClasses, int expectedCount)
  {
    assertThat(SimplifiedViewEntryPanel.countObjectClassesBesidesTop(byteStrings(objectClasses)))
        .isEqualTo(expectedCount);
  }
  private static List<String> list(String... values)
  {
    List<String> result = new ArrayList<>();
    Collections.addAll(result, values);
    return result;
  }
  private static List<ByteString> byteStrings(List<String> values)
  {
    List<ByteString> result = new ArrayList<>();
    for (String value : values)
    {
      result.add(ByteString.valueOfUtf8(value));
    }
    return result;
  }
}
opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanelTestCase.java
New file
@@ -0,0 +1,69 @@
/*
 * 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.guitools.controlpanel.ui;
import static org.assertj.core.api.Assertions.assertThat;
import static org.opends.server.util.CollectionUtils.newHashSet;
import java.util.Set;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.schema.Schema;
import org.opends.server.DirectoryServerTestCase;
import org.testng.annotations.Test;
/** Tests how {@link TableViewEntryPanel} decides what to display for a given attribute. */
@SuppressWarnings("javadoc")
public class TableViewEntryPanelTestCase extends DirectoryServerTestCase
{
  private static final Schema SCHEMA = Schema.getCoreSchema();
  private static final AttributeDescription OBJECT_CLASS = AttributeDescription.valueOf("objectClass", SCHEMA);
  @Test
  public void testObjectClassIsDisplayedAsADescriptorWhenTheSchemaIsAvailable()
  {
    assertThat(TableViewEntryPanel.isObjectClassWithSchema(OBJECT_CLASS, SCHEMA)).isTrue();
  }
  /**
   * The object class descriptor cannot be built without the schema: the raw values must then be
   * displayed instead, otherwise the object class attribute disappears from the table.
   */
  @Test
  public void testObjectClassFallsBackToItsRawValuesWithoutSchema()
  {
    assertThat(TableViewEntryPanel.isObjectClassWithSchema(OBJECT_CLASS, null)).isFalse();
  }
  @Test
  public void testOtherAttributesAreNeverDisplayedAsADescriptor()
  {
    assertThat(TableViewEntryPanel.isObjectClassWithSchema(AttributeDescription.valueOf("cn", SCHEMA), SCHEMA))
        .isFalse();
  }
  /** Attribute names are case insensitive, and the required attributes are stored in lower case. */
  @Test
  public void testRequiredAttributesAreMatchedCaseInsensitively()
  {
    Set<String> requiredAttrs = newHashSet("objectclass", "sn", "cn");
    assertThat(TableViewEntryPanel.isRequired(requiredAttrs, OBJECT_CLASS)).isTrue();
    assertThat(TableViewEntryPanel.isRequired(requiredAttrs, AttributeDescription.valueOf("sn", SCHEMA))).isTrue();
    assertThat(TableViewEntryPanel.isRequired(requiredAttrs, AttributeDescription.valueOf("description", SCHEMA)))
        .isFalse();
  }
}
opendj-server-legacy/src/test/java/org/opends/server/extensions/BCryptTest.java
@@ -24,6 +24,7 @@
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 * Portions Copyright 2026 3A Systems, LLC.
 *
 */
@@ -198,6 +199,18 @@
    }
    /**
     * A stored password whose salt holds a character beyond the base64 decoding table
     * must fail with the IllegalArgumentException that
     * BcryptPasswordStorageScheme.passwordMatches() catches, and not with an
     * ArrayIndexOutOfBoundsException escaping into bind processing.
     */
    @Test(expectedExceptions = IllegalArgumentException.class)
    public void checkPwRejectsSaltCharBeyondDecodingTable() {
        // U+0080 is the first character past the end of the 128 entry index_64 table.
        BCrypt.checkpw("secret", "$2a$10$" + ((char) 0x80) + "aaaaaaaaaaaaaaaaaaaaa");
    }
    /**
     *     Test for correct hashing of non-US-ASCII passwords.
     */
    @Test
opendj-server-legacy/src/test/java/org/opends/server/util/cli/LDAPConnectionConsoleInteractionTestCase.java
New file
@@ -0,0 +1,59 @@
/*
 * 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.util.cli;
import static org.assertj.core.api.Assertions.assertThat;
import static org.opends.messages.ToolMessages.INFO_CERTIFICATE_NAME_MISMATCH_TEXT_CLI;
import static org.opends.messages.ToolMessages.INFO_CERTIFICATE_NOT_TRUSTED_TEXT_CLI;
import org.opends.admin.ads.util.ApplicationTrustManager;
import org.opends.server.DirectoryServerTestCase;
import org.testng.annotations.Test;
/** Tests the message shown when the certificate presented by a server is rejected. */
@SuppressWarnings("javadoc")
public class LDAPConnectionConsoleInteractionTestCase extends DirectoryServerTestCase
{
  private static final String HOST = "localhost";
  private static final int PORT = 4444;
  @Test
  public void testUntrustedCertificateIsReported()
  {
    assertThat(rejectionMessage(ApplicationTrustManager.Cause.NOT_TRUSTED))
        .isEqualTo(INFO_CERTIFICATE_NOT_TRUSTED_TEXT_CLI.get(HOST, PORT).toString());
  }
  @Test
  public void testHostNameMismatchIsReported()
  {
    assertThat(rejectionMessage(ApplicationTrustManager.Cause.HOST_NAME_MISMATCH))
        .isEqualTo(INFO_CERTIFICATE_NAME_MISMATCH_TEXT_CLI.get(HOST, PORT, HOST, HOST, PORT).toString());
  }
  /** The two causes must not lead to the same message: the user needs to know what is wrong. */
  @Test
  public void testTheTwoCausesAreReportedDifferently()
  {
    assertThat(rejectionMessage(ApplicationTrustManager.Cause.NOT_TRUSTED))
        .isNotEqualTo(rejectionMessage(ApplicationTrustManager.Cause.HOST_NAME_MISMATCH));
  }
  private static String rejectionMessage(ApplicationTrustManager.Cause cause)
  {
    return LDAPConnectionConsoleInteraction.getCertificateRejectionMessage(cause, HOST, PORT).toString();
  }
}