From 4300052ea147c776119033f3c46b1286af8bd4af Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 05 Aug 2026 17:02:21 +0000
Subject: [PATCH] [#851] Add upgrade task for RFC 5805 transaction extended operation handlers (#852)

---
 opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/Upgrade.java              |   28 +++++++
 opendj-server-legacy/src/test/java/org/opends/server/tools/upgrade/UpgradeUtilsTestCase.java |  155 ++++++++++++++++++++++++++++++++++++++
 opendj-server-legacy/src/messages/org/opends/messages/tool.properties                        |    3 
 3 files changed, 186 insertions(+), 0 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/Upgrade.java b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/Upgrade.java
index 7b67cde..10b0590 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/Upgrade.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/tools/upgrade/Upgrade.java
@@ -79,6 +79,25 @@
   private static final NavigableMap<BuildVersion, List<UpgradeTask>> TASKS = new TreeMap<>();
   private static final List<UpgradeTask> MANDATORY_TASKS = new LinkedList<>();
 
+  /* Payloads of the issue #851 upgrade task, mirroring the fresh-install config.ldif template.
+   * Package-private so UpgradeUtilsTestCase applies exactly what the task applies. */
+  static final String[] START_TRANSACTION_HANDLER_ENTRY = {
+      "dn: cn=Start Transaction,cn=Extended Operations,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-extended-operation-handler",
+      "objectClass: ds-cfg-start-transaction-extended-operation-handler",
+      "cn: Start Transaction",
+      "ds-cfg-java-class: org.opends.server.extensions.StartTransactionExtendedOperation",
+      "ds-cfg-enabled: true" };
+  static final String[] END_TRANSACTION_HANDLER_ENTRY = {
+      "dn: cn=End Transaction,cn=Extended Operations,cn=config",
+      "objectClass: top",
+      "objectClass: ds-cfg-extended-operation-handler",
+      "objectClass: ds-cfg-end-transaction-extended-operation-handler",
+      "cn: End Transaction",
+      "ds-cfg-java-class: org.opends.server.extensions.EndTransactionExtendedOperation",
+      "ds-cfg-enabled: true" };
+
   static
   {
     // @formatter:off
@@ -610,6 +629,15 @@
     register("4.0.0", moveSubordinateBaseDnToGlobalConfiguration());
     register("4.0.0", removeTools("ldif-diff", "make-ldif", "dsjavaproperties"));
 
+    /* See issue #851: RFC 5805 LDAP transactions (#462, shipped in 4.10.0) added these entries to
+     * the fresh-install config.ldif template only, leaving upgraded instances without them.
+     * Keyed at 5.2.0 (the release shipping this task) rather than 4.10.0 so that instances already
+     * upgraded to 4.10.0-5.1.x without the entries are healed too; addConfigEntry is idempotent. */
+    register("5.2.0",
+        addConfigEntry(INFO_UPGRADE_TASK_ADD_TRANSACTION_EXTENDED_OPERATIONS.get(),
+            START_TRANSACTION_HANDLER_ENTRY),
+        addConfigEntry(END_TRANSACTION_HANDLER_ENTRY));
+
     /*
      * See issue #746. Builds before #661 (fixed in 5.1.2) shipped a duplicate
      * org.openidentityplatform.opendj.opendj-server-legacy.jar alongside opendj.jar in lib/.
diff --git a/opendj-server-legacy/src/messages/org/opends/messages/tool.properties b/opendj-server-legacy/src/messages/org/opends/messages/tool.properties
index b488fc1..c03e0bc 100644
--- a/opendj-server-legacy/src/messages/org/opends/messages/tool.properties
+++ b/opendj-server-legacy/src/messages/org/opends/messages/tool.properties
@@ -12,6 +12,7 @@
 #
 # Copyright 2006-2010 Sun Microsystems, Inc.
 # Portions Copyright 2011-2016 ForgeRock AS.
+# Portions Copyright 2026 3A Systems, LLC.
 
 
 
@@ -2699,3 +2700,5 @@
  Root DSE configuration
 INFO_UPGRADE_TASK_ADD_SUBORDINATE_BASE_DN_TO_GLOBAL_CONFIG=Adding subordinate-base-dn attribute to \
  Global configuration
+INFO_UPGRADE_TASK_ADD_TRANSACTION_EXTENDED_OPERATIONS=Adding configuration for LDAP transactions \
+ extended operation handlers (RFC 5805)
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/tools/upgrade/UpgradeUtilsTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/tools/upgrade/UpgradeUtilsTestCase.java
new file mode 100644
index 0000000..fb9eed1
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/tools/upgrade/UpgradeUtilsTestCase.java
@@ -0,0 +1,155 @@
+/*
+ * 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.tools.upgrade;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.forgerock.opendj.ldif.LDIFEntryWriter;
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.util.ChangeOperationType;
+import org.opends.server.util.StaticUtils;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+/**
+ * Tests that the issue #851 upgrade task payloads, applied through
+ * {@link UpgradeUtils#updateConfigFile}, add the RFC 5805 transaction extended operation handler
+ * entries exactly once and match the fresh-install template.
+ */
+@SuppressWarnings("javadoc")
+@Test(groups = { "precommit", "tools" }, sequential = true)
+public class UpgradeUtilsTestCase extends DirectoryServerTestCase
+{
+  /** Unknown config attributes must not fail parsing, as in the upgrade tool's own schema. */
+  private final Schema schema = Schema.getCoreSchema().asNonStrictSchema();
+
+  /** The config.ldif template a fresh install starts from. */
+  private File freshInstallTemplate()
+  {
+    return new File(TestCaseUtils.getBuildRoot(), "resource/config/config.ldif");
+  }
+
+  /** The upgrade task must apply exactly what a fresh install ships. */
+  @Test
+  public void testTaskPayloadsMirrorFreshInstallTemplate() throws Exception
+  {
+    final List<Entry> template = readEntries(freshInstallTemplate());
+    assertEntryPresentOnce(template, Upgrade.START_TRANSACTION_HANDLER_ENTRY);
+    assertEntryPresentOnce(template, Upgrade.END_TRANSACTION_HANDLER_ENTRY);
+  }
+
+  @Test
+  public void testAddTransactionHandlersAppliesOnceAndIsIdempotent() throws Exception
+  {
+    final File tempDir = TestCaseUtils.createTemporaryDirectory("upgradeTask851");
+    try
+    {
+      // Simulates an instance upgraded from a pre-4.10.0 version: the fresh-install
+      // template with the two transaction handler entries missing.
+      final File config = new File(tempDir, "config.ldif");
+      writeConfigWithoutTransactionEntries(config);
+
+      assertEquals(applyAdd(config, Upgrade.START_TRANSACTION_HANDLER_ENTRY), 1);
+      assertEquals(applyAdd(config, Upgrade.END_TRANSACTION_HANDLER_ENTRY), 1);
+
+      assertEquals(applyAdd(config, Upgrade.START_TRANSACTION_HANDLER_ENTRY), 0);
+      assertEquals(applyAdd(config, Upgrade.END_TRANSACTION_HANDLER_ENTRY), 0);
+
+      final List<Entry> entries = readEntries(config);
+      assertEntryPresentOnce(entries, Upgrade.START_TRANSACTION_HANDLER_ENTRY);
+      assertEntryPresentOnce(entries, Upgrade.END_TRANSACTION_HANDLER_ENTRY);
+    }
+    finally
+    {
+      StaticUtils.recursiveDelete(tempDir);
+    }
+  }
+
+  private int applyAdd(final File config, final String... ldifLines) throws Exception
+  {
+    return UpgradeUtils.updateConfigFile(config, null, ChangeOperationType.ADD, ldifLines);
+  }
+
+  private void writeConfigWithoutTransactionEntries(final File config) throws Exception
+  {
+    final DN startDN = dnOf(Upgrade.START_TRANSACTION_HANDLER_ENTRY);
+    final DN endDN = dnOf(Upgrade.END_TRANSACTION_HANDLER_ENTRY);
+    int removed = 0;
+    try (LDIFEntryReader reader =
+            new LDIFEntryReader(new FileInputStream(freshInstallTemplate())).setSchema(schema);
+        LDIFEntryWriter writer = new LDIFEntryWriter(new FileOutputStream(config)))
+    {
+      while (reader.hasNext())
+      {
+        final Entry entry = reader.readEntry();
+        if (entry.getName().equals(startDN) || entry.getName().equals(endDN))
+        {
+          removed++;
+          continue;
+        }
+        writer.writeEntry(entry);
+      }
+    }
+    assertEquals(removed, 2, "fresh-install template no longer ships the transaction entries");
+  }
+
+  private void assertEntryPresentOnce(final List<Entry> entries, final String... ldifLines)
+      throws Exception
+  {
+    final DN dn = dnOf(ldifLines);
+    final List<Entry> matches = new ArrayList<>();
+    for (final Entry entry : entries)
+    {
+      if (entry.getName().equals(dn))
+      {
+        matches.add(entry);
+      }
+    }
+    assertEquals(matches.size(), 1, "expected exactly one entry " + dn);
+    try (LDIFEntryReader reader = new LDIFEntryReader(ldifLines).setSchema(schema))
+    {
+      assertEquals(matches.get(0), reader.readEntry());
+    }
+  }
+
+  private DN dnOf(final String... ldifLines)
+  {
+    return DN.valueOf(ldifLines[0].replaceFirst("dn: ", ""), schema);
+  }
+
+  private List<Entry> readEntries(final File ldifFile) throws Exception
+  {
+    final List<Entry> entries = new ArrayList<>();
+    try (LDIFEntryReader reader = new LDIFEntryReader(new FileInputStream(ldifFile)).setSchema(schema))
+    {
+      while (reader.hasNext())
+      {
+        entries.add(reader.readEntry());
+      }
+    }
+    return entries;
+  }
+}

--
Gitblit v1.10.0