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

Valery Kharseko
30.18.2025 b9489f170636cee8af4c5edb824a34e21cc63195
[#462] RFC5805 Lightweight Directory Access Protocol (LDAP) Transactions (#469)

12 files added
10 files modified
1565 ■■■■■ changed files
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/AbortedTransactionExtendedResult.java 94 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedRequest.java 182 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedResult.java 168 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedRequest.java 101 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedResult.java 96 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/TransactionSpecificationRequestControl.java 82 ●●●●● patch | view | raw | blame | history
opendj-doc-generated-ref/src/main/asciidoc/reference/appendix-standards.adoc 9 ●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/EndTransactionExtendedOperationHandlerConfiguration.xml 75 ●●●●● patch | view | raw | blame | history
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/StartTransactionExtendedOperationHandlerConfiguration.xml 55 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/config/config.ldif 17 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/schema/02-config.ldif 11 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/api/ClientConnection.java 52 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/EndTransactionExtendedOperation.java 131 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/StartTransactionExtendedOperation.java 66 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/extensions/TraditionalWorkerThread.java 29 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/types/AbstractOperation.java 14 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/types/operation/RollbackOperation.java 26 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java 25 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java 24 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyDNOperation.java 24 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java 24 ●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/Rfc5808TestCase.java 260 ●●●●● patch | view | raw | blame | history
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/AbortedTransactionExtendedResult.java
New file
@@ -0,0 +1,94 @@
/*
 * 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 3A Systems, LLC
 */
package com.forgerock.opendj.ldap.extensions;
import org.forgerock.opendj.io.ASN1;
import org.forgerock.opendj.io.ASN1Writer;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.responses.AbstractExtendedResult;
import org.forgerock.util.Reject;
import java.io.IOException;
/*
    The Aborted Transaction Notice is an Unsolicited Notification message
   where the responseName is 1.3.6.1.1.21.4 and responseValue is present
   and contains a transaction identifier.
 */
public class AbortedTransactionExtendedResult extends AbstractExtendedResult<AbortedTransactionExtendedResult> {
    @Override
    public String getOID() {
        return "1.3.6.1.1.21.4";
    }
    private AbortedTransactionExtendedResult(final ResultCode resultCode) {
        super(resultCode);
    }
    public static AbortedTransactionExtendedResult newResult(final ResultCode resultCode) {
        Reject.ifNull(resultCode);
        return new AbortedTransactionExtendedResult(resultCode);
    }
    private String transactionID = null;
    public AbortedTransactionExtendedResult setTransactionID(final String transactionID) {
        this.transactionID = transactionID;
        return this;
    }
    public String getTransactionID() {
        return transactionID;
    }
    @Override
    public ByteString getValue() {
        final ByteStringBuilder buffer = new ByteStringBuilder();
        final ASN1Writer writer = ASN1.getWriter(buffer);
        try {
            writer.writeOctetString(transactionID);
        } catch (final IOException ioe) {
            throw new RuntimeException(ioe);
        }
        return buffer.toByteString();
    }
    @Override
    public boolean hasValue() {
        return true;
    }
    @Override
    public String toString() {
        return "AbortedTransactionExtendedResult(resultCode=" +
                getResultCode() +
                ", matchedDN=" +
                getMatchedDN() +
                ", diagnosticMessage=" +
                getDiagnosticMessage() +
                ", referrals=" +
                getReferralURIs() +
                ", responseName=" +
                getOID() +
                ", transactionID=" +
                transactionID +
                ", controls=" +
                getControls() +
                ")";
    }
}
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedRequest.java
New file
@@ -0,0 +1,182 @@
/*
 * 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 3A Systems, LLC
 */
package com.forgerock.opendj.ldap.extensions;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.io.ASN1;
import org.forgerock.opendj.io.ASN1Reader;
import org.forgerock.opendj.io.ASN1Writer;
import org.forgerock.opendj.ldap.*;
import org.forgerock.opendj.ldap.controls.Control;
import org.forgerock.opendj.ldap.requests.*;
import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
import org.forgerock.util.Reject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import static com.forgerock.opendj.ldap.CoreMessages.ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST;
import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
/*
 An End Transaction Request is an LDAPMessage of CHOICE extendedReq
   where the requestName is 1.3.6.1.1.21.3 and the requestValue is
   present and contains a BER-encoded txnEndReq.
      txnEndReq ::= SEQUENCE {
           commit         BOOLEAN DEFAULT TRUE,
           identifier     OCTET STRING }
   A commit value of TRUE indicates a request to commit the transaction
   identified by the identifier.  A commit value of FALSE indicates a
   request to abort the identified transaction.
 */
public class EndTransactionExtendedRequest extends AbstractExtendedRequest<EndTransactionExtendedRequest, EndTransactionExtendedResult> {
    public static final String END_TRANSACTION_REQUEST_OID ="1.3.6.1.1.21.3";
    @Override
    public String getOID() {
        return END_TRANSACTION_REQUEST_OID;
    }
    @Override
    public ExtendedResultDecoder<EndTransactionExtendedResult> getResultDecoder() {
        return RESULT_DECODER;
    }
    Boolean commit=true;
    public EndTransactionExtendedRequest setCommit(final Boolean commit) {
        this.commit = commit;
        return this;
    }
    String transactionID;
    public EndTransactionExtendedRequest setTransactionID(final String transactionID) {
        Reject.ifNull(transactionID);
        this.transactionID = transactionID;
        return this;
    }
    public boolean isCommit() {
        return commit;
    }
    public String getTransactionID() {
        return transactionID;
    }
    @Override
    public ByteString getValue() {
        Reject.ifNull(transactionID);
        final ByteStringBuilder buffer = new ByteStringBuilder();
        final ASN1Writer writer = ASN1.getWriter(buffer);
        try {
            writer.writeStartSequence();
            if (commit!=null) {
                writer.writeBoolean(commit);
            }
            writer.writeOctetString(transactionID);
            writer.writeEndSequence();
        } catch (final IOException ioe) {
            throw new RuntimeException(ioe);
        }
        return buffer.toByteString();
    }
    @Override
    public boolean hasValue() {
        return true;
    }
    private static final EndTransactionExtendedRequest.ResultDecoder RESULT_DECODER = new EndTransactionExtendedRequest.ResultDecoder();
    private static final class ResultDecoder extends  AbstractExtendedResultDecoder<EndTransactionExtendedResult> {
        @Override
        public EndTransactionExtendedResult newExtendedErrorResult(final ResultCode resultCode,final String matchedDN, final String diagnosticMessage) {
            if (!resultCode.isExceptional()) {
                throw new IllegalArgumentException("No response name and value for result code "+ resultCode.intValue());
            }
            return EndTransactionExtendedResult.newResult(resultCode).setMatchedDN(matchedDN).setDiagnosticMessage(diagnosticMessage);
        }
        /*
        txnEndRes ::= SEQUENCE {
           messageID MessageID OPTIONAL,
                -- msgid associated with non-success resultCode
           updatesControls SEQUENCE OF updateControls SEQUENCE {
                messageID MessageID,
                     -- msgid associated with controls
                controls  Controls
           } OPTIONAL
      }
         */
        @Override
        public EndTransactionExtendedResult decodeExtendedResult(final ExtendedResult result,final DecodeOptions options) throws DecodeException {
            if (result instanceof EndTransactionExtendedResult) {
                return (EndTransactionExtendedResult) result;
            }
            final ResultCode resultCode = result.getResultCode();
            final EndTransactionExtendedResult newResult =
                    EndTransactionExtendedResult.newResult(resultCode)
                            .setMatchedDN(result.getMatchedDN())
                            .setDiagnosticMessage(result.getDiagnosticMessage());
            final ByteString responseValue = result.getValue();
            if (!resultCode.isExceptional() && responseValue == null) {
                throw DecodeException.error(LocalizableMessage.raw("Empty response value"));
            }
            if (responseValue != null) {
                try {
                    final ASN1Reader reader = ASN1.getReader(responseValue);
                    if (reader.hasNextElement()) {
                        reader.readStartSequence();
                        if (reader.hasNextElement() && reader.peekType() == ASN1.UNIVERSAL_INTEGER_TYPE) {
                            newResult.setFailedMessageID(Math.toIntExact(reader.readInteger()));
                        } else if (reader.hasNextElement() && reader.peekType() == ASN1.UNIVERSAL_SEQUENCE_TYPE) {
                            reader.readStartSequence();
                            while (reader.hasNextElement() && reader.peekType() == ASN1.UNIVERSAL_SEQUENCE_TYPE) {
                                reader.readStartSequence();
                                final long messageId = reader.readInteger();
                                final List<Control> controls = new ArrayList<>();
                                reader.readStartSequence();
                                while (reader.hasNextElement() && reader.peekType() == ASN1.UNIVERSAL_OCTET_STRING_TYPE) {
                                    final ByteString controlEncoded = reader.readOctetString();
                                    //TODO decode Control
                                }
                                reader.readEndSequence();
                                //newResult.success(messageId, controls.toArray(new Control[]{}));
                                reader.readEndSequence();
                            }
                            reader.readEndSequence();
                        }
                        reader.readEndSequence();
                    }
                } catch (final IOException e) {
                    throw DecodeException.error(LocalizableMessage.raw("Error decoding response value"), e);
                }
            }
            for (final Control control : result.getControls()) {
                newResult.addControl(control);
            }
            return newResult;
        }
    }
}
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/EndTransactionExtendedResult.java
New file
@@ -0,0 +1,168 @@
/*
 * 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 3A Systems, LLC
 */
package com.forgerock.opendj.ldap.extensions;
import org.forgerock.opendj.io.ASN1;
import org.forgerock.opendj.io.ASN1Writer;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.controls.Control;
import org.forgerock.opendj.ldap.responses.AbstractExtendedResult;
import org.forgerock.util.Reject;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/*
   An End Transaction Response is an LDAPMessage sent in response to a
   End Transaction Request.  Its response name is absent.  The
   responseValue when present contains a BER-encoded txnEndRes.
      txnEndRes ::= SEQUENCE {
           messageID MessageID OPTIONAL,
                -- msgid associated with non-success resultCode
           updatesControls SEQUENCE OF updateControls SEQUENCE {
                messageID MessageID,
                     -- msgid associated with controls
                controls  Controls
           } OPTIONAL
      }
      -- where MessageID and Controls are as specified in RFC 4511
   The txnEndRes.messageID provides the message id of the update request
   associated with a non-success response.  txnEndRes.messageID is
   absent when resultCode of the End Transaction Response is success
   The txnEndRes.updatesControls provides a facility for returning
   response controls that normally (i.e., in the absence of
   transactions) would be returned in an update response.  The
   updateControls.messageID provides the message id of the update
   request associated with the response controls provided in
   updateControls.controls.
   The txnEndRes.updatesControls is absent when there are no update
   response controls to return.
   If both txnEndRes.messageID and txnEndRes.updatesControl are absent,
   the responseValue of the End Transaction Response is absent.
 */
public class EndTransactionExtendedResult extends AbstractExtendedResult<EndTransactionExtendedResult> {
    @Override
    public String getOID() {
        return EndTransactionExtendedRequest.END_TRANSACTION_REQUEST_OID;
    }
    private EndTransactionExtendedResult(final ResultCode resultCode) {
        super(resultCode);
    }
    public static EndTransactionExtendedResult newResult(final ResultCode resultCode) {
        Reject.ifNull(resultCode);
        return new EndTransactionExtendedResult(resultCode);
    }
    // The message ID for the operation that failed, if applicable.
    Integer failedOpMessageID=null;
    public EndTransactionExtendedResult setFailedMessageID(final Integer failedOpMessageID) {
        Reject.ifNull(failedOpMessageID);
        this.failedOpMessageID = failedOpMessageID;
        return this;
    }
    // A mapping of the response controls for the operations performed as part of
    // the transaction.
    Map<Integer, List<Control>> opResponseControls= new TreeMap<>();
    public EndTransactionExtendedResult success(Integer messageID, List<Control> responses) {
        Reject.ifNull(messageID);
        Reject.ifNull(responses);
        opResponseControls.put(messageID,responses);
        return this;
    }
    /*
     txnEndRes ::= SEQUENCE {
           messageID MessageID OPTIONAL,
                -- msgid associated with non-success resultCode
           updatesControls SEQUENCE OF updateControls SEQUENCE {
                messageID MessageID,
                     -- msgid associated with controls
                controls  Controls
           } OPTIONAL
      }
    */
    @Override
    public ByteString getValue() {
        final ByteStringBuilder buffer = new ByteStringBuilder();
        final ASN1Writer writer = ASN1.getWriter(buffer);
        try {
            if (failedOpMessageID!=null || (opResponseControls!=null && !opResponseControls.isEmpty()) ) {
                writer.writeStartSequence();
                if (failedOpMessageID != null) {
                    writer.writeInteger(failedOpMessageID);
                }
                if (opResponseControls != null && !opResponseControls.isEmpty()) {
                    writer.writeStartSequence();
                    for (Map.Entry<Integer, List<Control>> entry : opResponseControls.entrySet()) {
                        writer.writeStartSequence();
                            writer.writeInteger(entry.getKey());
                            writer.writeStartSequence();
                                for (Control control : entry.getValue()) {
                                   writer.writeOctetString(control.getValue());
                                }
                            writer.writeEndSequence();
                        writer.writeEndSequence();
                    }
                    writer.writeEndSequence();
                }
                writer.writeEndSequence();
            }
        } catch (final IOException ioe) {
            throw new RuntimeException(ioe);
        }
        return buffer.toByteString();
    }
    @Override
    public boolean hasValue() {
        return true;
    }
    @Override
    public String toString() {
        return "EndTransactionExtendedResult(resultCode=" +
                getResultCode() +
                ", matchedDN=" +
                getMatchedDN() +
                ", diagnosticMessage=" +
                getDiagnosticMessage() +
                ", referrals=" +
                getReferralURIs() +
                ", responseName=" +
                getOID() +
                ", failedOpMessageID=" +
                failedOpMessageID +
                ", opResponseControls=" +
                opResponseControls +
                ", controls=" +
                getControls() +
                ")";
    }
}
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedRequest.java
New file
@@ -0,0 +1,101 @@
/*
 * 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 3A Systems, LLC
 */
package com.forgerock.opendj.ldap.extensions;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.io.ASN1;
import org.forgerock.opendj.io.ASN1Reader;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.controls.Control;
import org.forgerock.opendj.ldap.requests.AbstractExtendedRequest;
import org.forgerock.opendj.ldap.responses.AbstractExtendedResultDecoder;
import org.forgerock.opendj.ldap.responses.ExtendedResult;
import org.forgerock.opendj.ldap.responses.ExtendedResultDecoder;
import java.io.IOException;
/*
 A Start Transaction Request is an LDAPMessage of CHOICE extendedReq
   where the requestName is 1.3.6.1.1.21.1 and the requestValue is
   absent.
 */
public class StartTransactionExtendedRequest extends AbstractExtendedRequest<StartTransactionExtendedRequest, StartTransactionExtendedResult> {
    public static final String START_TRANSACTION_REQUEST_OID ="1.3.6.1.1.21.1";
    @Override
    public String getOID() {
        return START_TRANSACTION_REQUEST_OID;
    }
    @Override
    public ExtendedResultDecoder<StartTransactionExtendedResult> getResultDecoder() {
        return RESULT_DECODER;
    }
    @Override
    public ByteString getValue() {
        return null;
    }
    @Override
    public boolean hasValue() {
        return false;
    }
    private static final StartTransactionExtendedRequest.ResultDecoder RESULT_DECODER = new StartTransactionExtendedRequest.ResultDecoder();
    private static final class ResultDecoder extends  AbstractExtendedResultDecoder<StartTransactionExtendedResult> {
        @Override
        public StartTransactionExtendedResult newExtendedErrorResult(final ResultCode resultCode,final String matchedDN, final String diagnosticMessage) {
            if (!resultCode.isExceptional()) {
                throw new IllegalArgumentException("No response name and value for result code "+ resultCode.intValue());
            }
            return StartTransactionExtendedResult.newResult(resultCode).setMatchedDN(matchedDN).setDiagnosticMessage(diagnosticMessage);
        }
        @Override
        public StartTransactionExtendedResult decodeExtendedResult(final ExtendedResult result,final DecodeOptions options) throws DecodeException {
            if (result instanceof StartTransactionExtendedResult) {
                return (StartTransactionExtendedResult) result;
            }
            final ResultCode resultCode = result.getResultCode();
            final StartTransactionExtendedResult newResult =
                    StartTransactionExtendedResult.newResult(resultCode)
                            .setMatchedDN(result.getMatchedDN())
                            .setDiagnosticMessage(result.getDiagnosticMessage());
            final ByteString responseValue = result.getValue();
            if (!resultCode.isExceptional() && responseValue == null) {
                throw DecodeException.error(LocalizableMessage.raw("Empty response value"));
            }
            if (responseValue != null) {
                try {
                    final ASN1Reader reader = ASN1.getReader(responseValue);
                    newResult.setTransactionID(reader.readOctetStringAsString());
                } catch (final IOException e) {
                    throw DecodeException.error(LocalizableMessage.raw("Error decoding response value"), e);
                }
            }
            for (final Control control : result.getControls()) {
                newResult.addControl(control);
            }
            return newResult;
        }
    }
}
opendj-core/src/main/java/com/forgerock/opendj/ldap/extensions/StartTransactionExtendedResult.java
New file
@@ -0,0 +1,96 @@
/*
 * 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 3A Systems, LLC
 */
package com.forgerock.opendj.ldap.extensions;
import org.forgerock.opendj.io.ASN1;
import org.forgerock.opendj.io.ASN1Writer;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.responses.AbstractExtendedResult;
import org.forgerock.util.Reject;
import java.io.IOException;
/*
    A Start Transaction Response is an LDAPMessage of CHOICE extendedRes
   sent in response to a Start Transaction Request.  Its responseName is
   absent.  When the resultCode is success (0), responseValue is present
   and contains a transaction identifier.  Otherwise, the responseValue
   is absent.
 */
public class StartTransactionExtendedResult extends AbstractExtendedResult<StartTransactionExtendedResult> {
    @Override
    public String getOID() {
        return StartTransactionExtendedRequest.START_TRANSACTION_REQUEST_OID;
    }
    private StartTransactionExtendedResult(final ResultCode resultCode) {
        super(resultCode);
    }
    public static StartTransactionExtendedResult newResult(final ResultCode resultCode) {
        Reject.ifNull(resultCode);
        return new StartTransactionExtendedResult(resultCode);
    }
    private String transactionID = null;
    public StartTransactionExtendedResult setTransactionID(final String transactionID) {
        this.transactionID = transactionID;
        return this;
    }
    public String getTransactionID() {
        return transactionID;
    }
    @Override
    public ByteString getValue() {
        final ByteStringBuilder buffer = new ByteStringBuilder();
        final ASN1Writer writer = ASN1.getWriter(buffer);
        try {
            writer.writeOctetString(transactionID);
        } catch (final IOException ioe) {
            throw new RuntimeException(ioe);
        }
        return buffer.toByteString();
    }
    @Override
    public boolean hasValue() {
        return true;
    }
    @Override
    public String toString() {
        return "StartTransactionExtendedResult(resultCode=" +
                getResultCode() +
                ", matchedDN=" +
                getMatchedDN() +
                ", diagnosticMessage=" +
                getDiagnosticMessage() +
                ", referrals=" +
                getReferralURIs() +
                ", responseName=" +
                getOID() +
                ", transactionID=" +
                transactionID +
                ", controls=" +
                getControls() +
                ")";
    }
}
opendj-core/src/main/java/org/forgerock/opendj/ldap/controls/TransactionSpecificationRequestControl.java
New file
@@ -0,0 +1,82 @@
/*
 * 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 3A Systems,LLC.
 */
package org.forgerock.opendj.ldap.controls;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.DecodeOptions;
import org.forgerock.util.Reject;
import static com.forgerock.opendj.ldap.CoreMessages.ERR_TRANSACTION_ID_CONTROL_BAD_OID;
import static com.forgerock.opendj.ldap.CoreMessages.ERR_TRANSACTION_ID_CONTROL_DECODE_NULL;
public class TransactionSpecificationRequestControl implements Control{
    public final static String OID="1.3.6.1.1.21.2";
    final String transactionId;
    public TransactionSpecificationRequestControl(String transactionId) {
        Reject.ifNull(transactionId);
        this.transactionId = transactionId;
    }
    @Override
    public String getOID() {
        return OID;
    }
    @Override
    public ByteString getValue() {
        return ByteString.valueOfUtf8(transactionId);
    }
    @Override
    public boolean hasValue() {
        return true;
    }
    @Override
    public boolean isCritical() {
        return true;
    }
    public static final ControlDecoder<TransactionSpecificationRequestControl> DECODER = new ControlDecoder<TransactionSpecificationRequestControl>() {
        @Override
        public TransactionSpecificationRequestControl decodeControl(final Control control, final DecodeOptions options)
                throws DecodeException {
            Reject.ifNull(control);
            if (control instanceof TransactionSpecificationRequestControl) {
                return (TransactionSpecificationRequestControl) control;
            }
            if (!control.getOID().equals(OID)) {
                throw DecodeException.error(ERR_TRANSACTION_ID_CONTROL_BAD_OID.get(control.getOID(), OID));
            }
            if (!control.hasValue()) {
                throw DecodeException.error(ERR_TRANSACTION_ID_CONTROL_DECODE_NULL.get());
            }
            return new TransactionSpecificationRequestControl(control.getValue().toString());
        }
        @Override
        public String getOID() {
            return OID;
        }
    };
}
opendj-doc-generated-ref/src/main/asciidoc/reference/appendix-standards.adoc
@@ -24,7 +24,7 @@
[#appendix-standards]
== Standards, RFCs, & Internet-Drafts
OpenDJ 3.5 software implements the following RFCs, Internet-Drafts, and standards:
OpenDJ software implements the following RFCs, Internet-Drafts, and standards:
--
[#rfc1274]
@@ -396,6 +396,13 @@
+
Describes the Lightweight Directory Access Protocol (LDAP) / X.500 'entryDN' operational attribute, that provides a copy of the entry's distinguished name for use in attribute value assertions.
[#rfc5805]
link:http://tools.ietf.org/html/rfc5805[RFC 5805: Lightweight Directory Access Protocol (LDAP) Transactions, window=\_top]::
+
Lightweight Directory Access Protocol (LDAP) update operations, such as Add, Delete, and Modify operations, have atomic, consistency,
isolation, durability (ACID) properties.  Each of these update operations act upon an entry.  It is often desirable to update two or
more entries in a single unit of interaction, a transaction.
[#fips180-1]
link:http://www.itl.nist.gov/fipspubs/fip180-1.htm[FIPS 180-1: Secure Hash Standard (SHA-1), window=\_top]::
+
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/EndTransactionExtendedOperationHandlerConfiguration.xml
New file
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
  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 3A Systems,LLC.
  ! -->
<adm:managed-object name="end-transaction-extended-operation-handler"
  plural-name="end-transaction-extended-operation-handlers"
  package="org.forgerock.opendj.server.config"
  extends="extended-operation-handler"
  xmlns:adm="http://opendj.forgerock.org/admin"
  xmlns:ldap="http://opendj.forgerock.org/admin-ldap">
  <adm:synopsis>
    The
    <adm:user-friendly-name />
    An End Transaction Request is an LDAPMessage of CHOICE extendedReq
    where the requestName is 1.3.6.1.1.21.3 and the requestValue is
    present and contains a BER-encoded txnEndReq.
    txnEndReq ::= SEQUENCE {
    commit         BOOLEAN DEFAULT TRUE,
    identifier     OCTET STRING }
    A commit value of TRUE indicates a request to commit the transaction
    identified by the identifier.  A commit value of FALSE indicates a
    request to abort the identified transaction.
    An End Transaction Response is an LDAPMessage sent in response to a
    End Transaction Request.  Its response name is absent.  The
    responseValue when present contains a BER-encoded txnEndRes.
    txnEndRes ::= SEQUENCE {
    messageID MessageID OPTIONAL,
    -- msgid associated with non-success resultCode
    updatesControls SEQUENCE OF updateControls SEQUENCE {
    messageID MessageID,
    -- msgid associated with controls
    controls  Controls
    } OPTIONAL
    }
    -- where MessageID and Controls are as specified in RFC 4511
    The txnEndRes.messageID provides the message id of the update request
    associated with a non-success response.  txnEndRes.messageID is
    absent when resultCode of the End Transaction Response is success
    (0).
  </adm:synopsis>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:name>
        ds-cfg-end-transaction-extended-operation-handler
      </ldap:name>
      <ldap:superior>ds-cfg-extended-operation-handler</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:property-override name="java-class" advanced="true">
    <adm:default-behavior>
      <adm:defined>
        <adm:value>
          org.opends.server.extensions.EndTransactionExtendedOperation
        </adm:value>
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
</adm:managed-object>
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/StartTransactionExtendedOperationHandlerConfiguration.xml
New file
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
  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 3A Systems,LLC.
  ! -->
<adm:managed-object name="start-transaction-extended-operation-handler"
  plural-name="start-transaction-extended-operation-handlers"
  package="org.forgerock.opendj.server.config"
  extends="extended-operation-handler"
  xmlns:adm="http://opendj.forgerock.org/admin"
  xmlns:ldap="http://opendj.forgerock.org/admin-ldap">
  <adm:synopsis>
    The
    <adm:user-friendly-name />
    A client wishing to perform a sequence of directory updates as a
    transaction issues a Start Transaction Request.  A server that is
    willing and able to support transactions responds to this request
    with a Start Transaction Response providing a transaction identifier
    and with a resultCode of success (0).  Otherwise, the server responds
    with a Start Transaction Response with a resultCode other than
    success indicating the nature of the failure.
    The transaction identifier provided upon successful start of a
    transaction is used in subsequent protocol messages to identify this
    transaction.
  </adm:synopsis>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:name>
        ds-cfg-start-transaction-extended-operation-handler
      </ldap:name>
      <ldap:superior>ds-cfg-extended-operation-handler</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:property-override name="java-class" advanced="true">
    <adm:default-behavior>
      <adm:defined>
        <adm:value>
          org.opends.server.extensions.StartTransactionExtendedOperation
        </adm:value>
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
</adm:managed-object>
opendj-server-legacy/resource/config/config.ldif
@@ -14,6 +14,7 @@
# Portions Copyright 2012-2014 Manuel Gaupp
# Portions Copyright 2010-2016 ForgeRock AS.
# Portions copyright 2015 Edan Idzerda
# Portions Copyright 2025 3A Systems, LLC.
# This file contains the primary Directory Server configuration.  It must not
# be directly edited while the server is online.  The server configuration
@@ -603,6 +604,22 @@
ds-cfg-java-class: org.opends.server.extensions.WhoAmIExtendedOperation
ds-cfg-enabled: true
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
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
dn: cn=Group Implementations,cn=config
objectClass: top
objectClass: ds-cfg-branch
opendj-server-legacy/resource/schema/02-config.ldif
@@ -6265,4 +6265,13 @@
  STRUCTURAL
  MAY ds-cfg-pbkdf2-iterations
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.60142.1.2.1
  NAME 'ds-cfg-start-transaction-extended-operation-handler'
  SUP ds-cfg-extended-operation-handler
  STRUCTURAL
  X-ORIGIN 'OpenDS Directory Server' )
objectClasses: ( 1.3.6.1.4.1.60142.1.2.2
  NAME 'ds-cfg-end-transaction-extended-operation-handler'
  SUP ds-cfg-extended-operation-handler
  STRUCTURAL
  X-ORIGIN 'OpenDS Directory Server' )
opendj-server-legacy/src/main/java/org/opends/server/api/ClientConnection.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2009 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2025 3A Systems, LLC.
 */
package org.opends.server.api;
@@ -20,11 +21,8 @@
import java.nio.channels.ByteChannel;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -53,6 +51,7 @@
import org.opends.server.types.Privilege;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchResultReference;
import org.opends.server.types.operation.RollbackOperation;
import org.opends.server.util.TimeThread;
import static org.opends.messages.CoreMessages.*;
@@ -1576,4 +1575,47 @@
  {
    return getConnectionID() < 0;
  }
  public class Transaction {
      final String transactionId=UUID.randomUUID().toString().toLowerCase();
      public Transaction() {
          transactions.put(getTransactionId(),this);
      }
      public String getTransactionId() {
          return transactionId;
      }
      final Queue<Operation> waiting=new LinkedList<>();
      public void add(Operation operation) {
          waiting.add(operation);
      }
      public Queue<Operation> getWaiting() {
          return waiting;
      }
      public void clear() {
          transactions.remove(getTransactionId());
      }
      final Deque<RollbackOperation> completed =new ArrayDeque<>();
      public void success(RollbackOperation operation) {
          completed.add(operation);
      }
      public Deque<RollbackOperation> getCompleted() {
          return completed;
      }
  }
  Map<String,Transaction> transactions=new ConcurrentHashMap<>();
  public Transaction startTransaction() {
      return new Transaction();
  }
  public Transaction getTransaction(String id) {
      return transactions.get(id);
  }
}
opendj-server-legacy/src/main/java/org/opends/server/extensions/EndTransactionExtendedOperation.java
New file
@@ -0,0 +1,131 @@
/*
 * 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 3A Systems, LLC
 */
package org.opends.server.extensions;
import com.forgerock.opendj.ldap.extensions.EndTransactionExtendedRequest;
import com.forgerock.opendj.ldap.extensions.EndTransactionExtendedResult;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.io.ASN1;
import org.forgerock.opendj.io.ASN1Reader;
import org.forgerock.opendj.ldap.DecodeException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.server.config.server.EndTransactionExtendedOperationHandlerCfg;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.ExtendedOperationHandler;
import org.opends.server.core.ExtendedOperation;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import org.opends.server.types.Operation;
import org.opends.server.types.operation.RollbackOperation;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import static com.forgerock.opendj.ldap.CoreMessages.ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST;
import static com.forgerock.opendj.util.StaticUtils.getExceptionMessage;
public class EndTransactionExtendedOperation extends ExtendedOperationHandler<EndTransactionExtendedOperationHandlerCfg>
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  public EndTransactionExtendedOperation()
  {
    super();
  }
  @Override
  public void initializeExtendedOperationHandler(EndTransactionExtendedOperationHandlerCfg config) throws ConfigException, InitializationException
  {
    super.initializeExtendedOperationHandler(config);
  }
  @Override
  public void processExtendedOperation(ExtendedOperation operation)
  {
    final EndTransactionExtendedRequest request =new EndTransactionExtendedRequest();
    if (operation.getRequestValue()!= null) {
        final ASN1Reader reader = ASN1.getReader(operation.getRequestValue());
        try {
            reader.readStartSequence();
            if (reader.hasNextElement()&& (reader.peekType() == ASN1.UNIVERSAL_BOOLEAN_TYPE)) {
              request.setCommit(reader.readBoolean());
            }
            request.setTransactionID(reader.readOctetStringAsString());
            reader.readEndSequence();
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
    }
    final ClientConnection.Transaction trn=operation.getClientConnection().getTransaction(request.getTransactionID());
    if (trn==null) {
        operation.setResultCode(ResultCode.CANCELLED);
        operation.appendErrorMessage(LocalizableMessage.raw("unknown transactionId="+request.getTransactionID()));
        return;
    }
    final EndTransactionExtendedResult res=EndTransactionExtendedResult.newResult(ResultCode.SUCCESS);
    operation.setResultCode(res.getResultCode());
    Operation currentOperation=null;
    try {
        while((currentOperation=trn.getWaiting().poll())!=null) {
            if (request.isCommit()) {
                currentOperation.run();
                if (!ResultCode.SUCCESS.equals(currentOperation.getResultCode())) {
                    throw new InterruptedException();
                }
                currentOperation.operationCompleted();
                //res.success(currentOperation.getMessageID(),currentOperation.getResponseControls());
            }
        }
    }catch (Throwable e){
        res.setFailedMessageID(currentOperation.getMessageID());
        operation.setResultCode(currentOperation.getResultCode());
        operation.setErrorMessage(currentOperation.getErrorMessage());
        //rollback
        RollbackOperation cancelOperation=null;
        while((cancelOperation=trn.getCompleted().pollLast())!=null) {
            try {
                cancelOperation.rollback();
            }catch (Throwable e2){
                throw new RuntimeException("rollback error",e2);
            }
        }
    }finally {
        trn.clear();
    }
    operation.setResponseOID(res.getOID());
    operation.setResponseValue(res.getValue());
  }
  @Override
  public String getExtendedOperationOID()
  {
    return EndTransactionExtendedRequest.END_TRANSACTION_REQUEST_OID;
  }
  @Override
  public String getExtendedOperationName()
  {
    return "End Transaction";
  }
}
opendj-server-legacy/src/main/java/org/opends/server/extensions/StartTransactionExtendedOperation.java
New file
@@ -0,0 +1,66 @@
/*
 * 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 3A Systems, LLC
 */
package org.opends.server.extensions;
import com.forgerock.opendj.ldap.extensions.StartTransactionExtendedRequest;
import com.forgerock.opendj.ldap.extensions.StartTransactionExtendedResult;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.server.config.server.StartTransactionExtendedOperationHandlerCfg;
import org.opends.server.api.ExtendedOperationHandler;
import org.opends.server.core.ExtendedOperation;
import org.opends.server.types.InitializationException;
public class StartTransactionExtendedOperation extends ExtendedOperationHandler<StartTransactionExtendedOperationHandlerCfg>
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
  public StartTransactionExtendedOperation()
  {
    super();
  }
  @Override
  public void initializeExtendedOperationHandler(StartTransactionExtendedOperationHandlerCfg config) throws ConfigException, InitializationException
  {
    super.initializeExtendedOperationHandler(config);
  }
  @Override
  public void processExtendedOperation(ExtendedOperation operation)
  {
    final StartTransactionExtendedResult res=StartTransactionExtendedResult
            .newResult(ResultCode.SUCCESS)
            .setTransactionID(operation.getClientConnection().startTransaction().getTransactionId());
    operation.setResponseOID(res.getOID());
    operation.setResponseValue(res.getValue());
    operation.setResultCode(res.getResultCode());
  }
  @Override
  public String getExtendedOperationOID()
  {
    return StartTransactionExtendedRequest.START_TRANSACTION_REQUEST_OID;
  }
  @Override
  public String getExtendedOperationName()
  {
    return "Start Transaction";
  }
}
opendj-server-legacy/src/main/java/org/opends/server/extensions/TraditionalWorkerThread.java
@@ -13,15 +13,19 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2025 3A Systems, LLC.
 */
package org.opends.server.extensions;
import java.util.Map;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.DirectoryThread;
import org.opends.server.core.DirectoryServer;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.server.types.AbstractOperation;
import org.opends.server.types.CancelRequest;
import org.opends.server.types.DisconnectReason;
import org.opends.server.types.Operation;
@@ -145,8 +149,29 @@
        {
          // The operation is not null, so process it.  Make sure that when
          // processing is complete.
          operation.run();
          operation.operationCompleted();
          //check has transactionId control
          ClientConnection.Transaction transaction=null;
          if (operation instanceof AbstractOperation) {
            String transactionId = ((AbstractOperation) operation).getTransactionId();
            if (transactionId!=null){
              transaction=operation.getClientConnection().getTransaction(transactionId);
              if (transaction==null){ //unknown transactionId
                operation.setResultCode(ResultCode.CANCELLED);
                operation.appendErrorMessage(LocalizableMessage.raw("unknown transactionId="+transactionId));
                operation.getClientConnection().sendResponse(operation);
                continue;
              }
            }
          }
          if (transaction==null) {  //run
            operation.run();
            operation.operationCompleted();
          }else { //suspend for commit
            transaction.add(operation);
            operation.setResultCode(ResultCode.SUCCESS);
            operation.getClientConnection().sendResponse(operation);
          }
        }
      }
      catch (Throwable t)
opendj-server-legacy/src/main/java/org/opends/server/types/AbstractOperation.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2023-2025 3A Systems, LLC.
 */
package org.opends.server.types;
@@ -29,6 +30,7 @@
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl;
import org.forgerock.util.Reject;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.plugin.PluginResult.OperationResult;
@@ -721,4 +723,16 @@
    }
    return true;
  }
  public String getTransactionId() {
      for (Control control : getRequestControls()) {
          if (control.getOID().equals(TransactionSpecificationRequestControl.OID)) {
            if ((control instanceof LDAPControl) && ((LDAPControl)control).getValue()!=null){
              return ((LDAPControl) control).getValue().toString();
            }
          }
      }
      return null;
  }
}
opendj-server-legacy/src/main/java/org/opends/server/types/operation/RollbackOperation.java
New file
@@ -0,0 +1,26 @@
/*
 * 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 3A Systems,LLC.
 */
package org.opends.server.types.operation;
import org.opends.server.api.ClientConnection;
import org.opends.server.types.CanceledOperationException;
import org.opends.server.types.DirectoryException;
public interface RollbackOperation
{
  void rollback() throws CanceledOperationException, DirectoryException;
}
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendAddOperation.java
@@ -13,7 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions copyright 2024 3A Systems,LLC.
 * Portions Copyright 2024-2025 3A Systems,LLC.
 */
package org.opends.server.workflowelement.localbackend;
@@ -39,6 +39,7 @@
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.controls.RelaxRulesControl;
import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.ldap.schema.ObjectClass;
import org.forgerock.opendj.ldap.schema.Syntax;
@@ -61,6 +62,7 @@
import org.opends.server.core.PasswordPolicy;
import org.opends.server.core.PersistentSearch;
import org.opends.server.core.ServerContext;
import org.opends.server.protocols.ldap.LDAPControl;
import org.opends.server.schema.AuthPasswordSyntax;
import org.opends.server.schema.UserPasswordSyntax;
import org.opends.server.types.Attribute;
@@ -73,10 +75,7 @@
import org.opends.server.types.LockManager.DNLock;
import org.opends.server.types.Privilege;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.operation.PostOperationAddOperation;
import org.opends.server.types.operation.PostResponseAddOperation;
import org.opends.server.types.operation.PostSynchronizationAddOperation;
import org.opends.server.types.operation.PreOperationAddOperation;
import org.opends.server.types.operation.*;
import org.opends.server.util.TimeThread;
/**
@@ -86,7 +85,7 @@
public class LocalBackendAddOperation
       extends AddOperationWrapper
       implements PreOperationAddOperation, PostOperationAddOperation,
                  PostResponseAddOperation, PostSynchronizationAddOperation
                  PostResponseAddOperation, PostSynchronizationAddOperation,RollbackOperation
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
@@ -470,6 +469,9 @@
        }
        backend.addEntry(entry, this);
        if (trx!=null) {
          trx.success(this);
        }
      }
      LocalBackendWorkflowElement.addPostReadResponse(this, postReadRequest,
@@ -496,7 +498,10 @@
    }
  }
  @Override
  public void rollback() throws CanceledOperationException, DirectoryException {
    backend.deleteEntry(entryDN,null);
  }
  private void processSynchPostOperationPlugins()
  {
@@ -968,6 +973,10 @@
      {
        RelaxRulesControlRequested = true;
      }
      else if (TransactionSpecificationRequestControl.OID.equals(oid))
      {
        trx=getClientConnection().getTransaction(((LDAPControl)c).getValue().toString());
      }
      else if (c.isCritical() && !backend.supportsControl(oid))
      {
        throw newDirectoryException(entryDN, ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
@@ -975,9 +984,11 @@
      }
    }
  }
  ClientConnection.Transaction trx=null;
  private AccessControlHandler<?> getAccessControlHandler()
  {
    return AccessControlConfigManager.getInstance().getAccessControlHandler();
  }
}
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendDeleteOperation.java
@@ -13,7 +13,7 @@
 *
 * Copyright 2008-2009 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2022-2024 3A Systems, LLC.
 * Portions Copyright 2022-2025 3A Systems, LLC.
 */
package org.opends.server.workflowelement.localbackend;
@@ -23,6 +23,7 @@
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl;
import org.opends.server.api.AccessControlHandler;
import org.opends.server.api.LocalBackend;
import org.opends.server.api.ClientConnection;
@@ -35,6 +36,7 @@
import org.opends.server.core.DeleteOperationWrapper;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PersistentSearch;
import org.opends.server.protocols.ldap.LDAPControl;
import org.opends.server.types.CanceledOperationException;
import org.opends.server.types.Control;
import org.forgerock.opendj.ldap.DN;
@@ -43,10 +45,7 @@
import org.opends.server.types.LockManager.DNLock;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SynchronizationProviderResult;
import org.opends.server.types.operation.PostOperationDeleteOperation;
import org.opends.server.types.operation.PostResponseDeleteOperation;
import org.opends.server.types.operation.PostSynchronizationDeleteOperation;
import org.opends.server.types.operation.PreOperationDeleteOperation;
import org.opends.server.types.operation.*;
import static org.opends.messages.CoreMessages.*;
import static org.opends.server.core.DirectoryServer.*;
@@ -63,7 +62,7 @@
       extends DeleteOperationWrapper
       implements PreOperationDeleteOperation, PostOperationDeleteOperation,
                  PostResponseDeleteOperation,
                  PostSynchronizationDeleteOperation
                  PostSynchronizationDeleteOperation, RollbackOperation
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
@@ -299,6 +298,9 @@
          return;
        }
        backend.deleteEntry(entryDN, this);
        if (trx!=null) {
          trx.success(this);
        }
      }
      LocalBackendWorkflowElement.addPreReadResponse(this, preReadRequest, entry);
@@ -416,6 +418,10 @@
      {
        continue;
      }
      else if (TransactionSpecificationRequestControl.OID.equals(oid))
      {
        trx=getClientConnection().getTransaction(((LDAPControl)c).getValue().toString());
      }
      else if (c.isCritical() && !backend.supportsControl(oid))
      {
        throw newDirectoryException(entry, ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
@@ -423,6 +429,7 @@
      }
    }
  }
  ClientConnection.Transaction trx=null;
  /**
   * Handle conflict resolution.
@@ -488,4 +495,9 @@
      }
      return true;
  }
  @Override
  public void rollback() throws CanceledOperationException, DirectoryException {
    backend.addEntry(entry,null);
  }
}
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyDNOperation.java
@@ -13,7 +13,7 @@
 *
 * Copyright 2008-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions copyright 2024 3A Systems,LLC.
 * Portions copyright 2024-2025 3A Systems,LLC.
 */
package org.opends.server.workflowelement.localbackend;
@@ -29,6 +29,7 @@
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.opends.server.api.AccessControlHandler;
import org.opends.server.api.LocalBackend;
@@ -43,6 +44,7 @@
import org.opends.server.core.ModifyDNOperation;
import org.opends.server.core.ModifyDNOperationWrapper;
import org.opends.server.core.PersistentSearch;
import org.opends.server.protocols.ldap.LDAPControl;
import org.opends.server.types.Attribute;
import org.opends.server.types.Attributes;
import org.opends.server.types.CanceledOperationException;
@@ -54,10 +56,7 @@
import org.opends.server.types.Modification;
import org.forgerock.opendj.ldap.RDN;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.operation.PostOperationModifyDNOperation;
import org.opends.server.types.operation.PostResponseModifyDNOperation;
import org.opends.server.types.operation.PostSynchronizationModifyDNOperation;
import org.opends.server.types.operation.PreOperationModifyDNOperation;
import org.opends.server.types.operation.*;
import static org.opends.messages.CoreMessages.*;
import static org.opends.server.core.DirectoryServer.*;
@@ -75,7 +74,7 @@
  implements PreOperationModifyDNOperation,
             PostOperationModifyDNOperation,
             PostResponseModifyDNOperation,
             PostSynchronizationModifyDNOperation
             PostSynchronizationModifyDNOperation, RollbackOperation
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
@@ -439,6 +438,9 @@
          return;
        }
        currentBackend.renameEntry(entryDN, newEntry, this);
        if (trx!=null) {
          trx.success(this);
        }
      }
      // Attach the pre-read and/or post-read controls to the response if
@@ -576,6 +578,10 @@
      {
        continue;
      }
      else if (TransactionSpecificationRequestControl.OID.equals(oid))
      {
        trx=getClientConnection().getTransaction(((LDAPControl)c).getValue().toString());
      }
      else if (c.isCritical() && !backend.supportsControl(oid))
      {
        throw new DirectoryException(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
@@ -583,6 +589,7 @@
      }
    }
  }
  ClientConnection.Transaction trx=null;
  private AccessControlHandler<?> getAccessControlHandler()
  {
@@ -816,4 +823,9 @@
          }
      }
  }
  @Override
  public void rollback() throws CanceledOperationException, DirectoryException {
     backend.renameEntry(newEntry.getName(), currentEntry, this);
  }
}
opendj-server-legacy/src/main/java/org/opends/server/workflowelement/localbackend/LocalBackendModifyOperation.java
@@ -13,7 +13,7 @@
 *
 * Copyright 2008-2011 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions copyright 2024 3A Systems,LLC.
 * Portions Copyright 2024-2025 3A Systems,LLC.
 */
package org.opends.server.workflowelement.localbackend;
@@ -35,6 +35,7 @@
import org.forgerock.opendj.ldap.RDN;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.controls.RelaxRulesControl;
import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.ldap.schema.MatchingRule;
import org.forgerock.opendj.ldap.schema.ObjectClass;
@@ -60,6 +61,7 @@
import org.opends.server.core.PasswordPolicy;
import org.opends.server.core.PasswordPolicyState;
import org.opends.server.core.PersistentSearch;
import org.opends.server.protocols.ldap.LDAPControl;
import org.opends.server.schema.AuthPasswordSyntax;
import org.opends.server.schema.UserPasswordSyntax;
import org.opends.server.types.AcceptRejectWarn;
@@ -77,10 +79,7 @@
import org.opends.server.types.Privilege;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SynchronizationProviderResult;
import org.opends.server.types.operation.PostOperationModifyOperation;
import org.opends.server.types.operation.PostResponseModifyOperation;
import org.opends.server.types.operation.PostSynchronizationModifyOperation;
import org.opends.server.types.operation.PreOperationModifyOperation;
import org.opends.server.types.operation.*;
import static org.opends.messages.CoreMessages.*;
import static org.opends.server.config.ConfigConstants.*;
@@ -96,7 +95,7 @@
       extends ModifyOperationWrapper
       implements PreOperationModifyOperation, PostOperationModifyOperation,
                  PostResponseModifyOperation,
                  PostSynchronizationModifyOperation
                  PostSynchronizationModifyOperation, RollbackOperation
{
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
@@ -500,6 +499,9 @@
        }
        backend.replaceEntry(currentEntry, modifiedEntry, this);
        if (trx!=null) {
          trx.success(this);
        }
        if (isAuthnManagedLocally())
        {
@@ -697,6 +699,10 @@
      {
        RelaxRulesControlRequested = true;
      }
      else if (TransactionSpecificationRequestControl.OID.equals(oid))
      {
        trx=getClientConnection().getTransaction(((LDAPControl)c).getValue().toString());
      }
      else if (c.isCritical() && !backend.supportsControl(oid))
      {
        throw newDirectoryException(currentEntry, ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
@@ -704,6 +710,7 @@
      }
    }
  }
  ClientConnection.Transaction trx=null;
  private void processNonPasswordModifications() throws DirectoryException
  {
@@ -1658,4 +1665,9 @@
          }
      }
  }
  @Override
  public void rollback() throws CanceledOperationException, DirectoryException {
      backend.replaceEntry(modifiedEntry,currentEntry, this);
  }
}
opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/Rfc5808TestCase.java
New file
@@ -0,0 +1,260 @@
/*
 * 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 3A Systems, LLC.
 */
package org.openidentityplatform.opendj;
import com.forgerock.opendj.ldap.extensions.EndTransactionExtendedRequest;
import com.forgerock.opendj.ldap.extensions.EndTransactionExtendedResult;
import com.forgerock.opendj.ldap.extensions.StartTransactionExtendedRequest;
import org.forgerock.opendj.ldap.*;
import org.forgerock.opendj.ldap.controls.TransactionSpecificationRequestControl;
import org.forgerock.opendj.ldap.requests.*;
import com.forgerock.opendj.ldap.extensions.StartTransactionExtendedResult;
import org.forgerock.opendj.ldap.responses.Result;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.forgerock.opendj.ldap.requests.Requests.newAddRequest;
import static org.testng.Assert.assertThrows;
@Test(sequential = true)
public class Rfc5808TestCase extends DirectoryServerTestCase {
    Connection connection;
    @BeforeClass
    public void startServer() throws Exception {
        TestCaseUtils.startServer();
        TestCaseUtils.initializeTestBackend(true);
        final LDAPConnectionFactory factory =new LDAPConnectionFactory("localhost", TestCaseUtils.getServerLdapPort());
        connection = factory.getConnection();
        connection.bind("cn=Directory Manager", "password".toCharArray());
        assertThat(connection.isValid()).isTrue();
    }
    @Test
    public void test() throws LdapException {
        //unknown transaction in TransactionSpecificationRequestControl
        assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() {
            @Override
            public void run() throws Throwable {
                AddRequest add=Requests
                        .newAddRequest("ou=People,o=test")
                        .addAttribute("objectClass", "top", "organizationalUnit")
                        .addAttribute("ou", "People")
                        .addControl(new TransactionSpecificationRequestControl("bad"))
                        ;
                Result result = connection.add(add);
            }
        });
        //unknown transaction in EndTransactionExtendedRequest
        assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() {
            @Override
            public void run() throws Throwable {
                EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID("unknown").setCommit(true));
            }
        });
        assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() {
            @Override
            public void run() throws Throwable {
                EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID("unknown"));
            }
        });
        //commit
        StartTransactionExtendedResult resStart=connection.extendedRequest(new StartTransactionExtendedRequest());
        assertThat(resStart.isSuccess()).isTrue();
        assertThat(resStart.getOID()).isEqualTo("1.3.6.1.1.21.1");
        String transactionID=resStart.getTransactionID();
        assertThat(transactionID).isNotEmpty();
        AddRequest add=Requests
                .newAddRequest("ou=People,o=test")
                .addAttribute("objectClass", "top", "organizationalUnit")
                .addAttribute("ou", "People")
                .addControl(new TransactionSpecificationRequestControl(transactionID))
                ;
        Result result = connection.add(add);
        assertThat(result.isSuccess()).isTrue();
        add= Requests.newAddRequest("sn=bjensen,ou=People,o=test")
                .addAttribute("objectClass","top","person")
                .addAttribute("cn","bjensen")
                .addControl(new TransactionSpecificationRequestControl(transactionID))
        ;
        result = connection.add(add);
        assertThat(result.isSuccess()).isTrue();
        ModifyDNRequest mdn=Requests.newModifyDNRequest("sn=bjensen,ou=People,o=test","sn=bjensen2")
        .addControl(new TransactionSpecificationRequestControl(transactionID))
        ;
        result = connection.modifyDN(mdn);
        assertThat(result.isSuccess()).isTrue();
        ModifyRequest edit= Requests.newModifyRequest("sn=bjensen2,ou=People,o=test")
                .addModification(ModificationType.REPLACE,"cn","bjensen2")
        .addControl(new TransactionSpecificationRequestControl(transactionID))
        ;
        result = connection.modify(edit);
        assertThat(result.isSuccess()).isTrue();
        DeleteRequest delete=Requests.newDeleteRequest("sn=bjensen2,ou=People,o=test")
        .addControl(new TransactionSpecificationRequestControl(transactionID))
        ;
        result = connection.delete(delete);
        assertThat(result.isSuccess()).isTrue();
        assertThrows(EntryNotFoundException.class, new Assert.ThrowingRunnable() {
            @Override
            public void run() throws Throwable {
                connection.searchSingleEntry("o=test",SearchScope.SINGLE_LEVEL,"(ou=People)");
            }
        });
        EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(transactionID));
        assertThat(resEnd.isSuccess()).isTrue();
        assertThat(resEnd.getOID()).isEqualTo("1.3.6.1.1.21.3");
        //check commit successfully
        assertThat(connection.searchSingleEntry("o=test",SearchScope.SINGLE_LEVEL,"(ou=People)")).isNotNull();
        //check transaction finished
        String finalTransactionID = transactionID;
        assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() {
            @Override
            public void run() throws Throwable {
                EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(finalTransactionID).setCommit(true));
            }
        });
        //rollback by EndTransactionExtendedRequest
        resStart=connection.extendedRequest(new StartTransactionExtendedRequest());
        assertThat(resStart.isSuccess()).isTrue();
        assertThat(resStart.getOID()).isEqualTo("1.3.6.1.1.21.1");
        transactionID=resStart.getTransactionID();
        assertThat(transactionID).isNotEmpty();
        add=Requests
                .newAddRequest("ou=People2,o=test")
                .addAttribute("objectClass", "top", "organizationalUnit")
                .addAttribute("ou", "People2")
                .addControl(new TransactionSpecificationRequestControl(transactionID))
                ;
        result = connection.add(add);
        assertThat(result.isSuccess()).isTrue();
        resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(transactionID).setCommit(false));
        assertThat(resEnd.isSuccess()).isTrue();
        assertThat(resEnd.getOID()).isEqualTo("1.3.6.1.1.21.3");
        //check transaction finished
        String finalTransactionID1 = transactionID;
        assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() {
            @Override
            public void run() throws Throwable {
                EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(finalTransactionID1).setCommit(false));
            }
        });
        //check rollback successfully
        assertThrows(EntryNotFoundException.class, new Assert.ThrowingRunnable() {
            @Override
            public void run() throws Throwable {
                connection.searchSingleEntry("o=test",SearchScope.SINGLE_LEVEL,"(ou=People2)");
            }
        });
        //rollback by error
        resStart=connection.extendedRequest(new StartTransactionExtendedRequest());
        assertThat(resStart.isSuccess()).isTrue();
        assertThat(resStart.getOID()).isEqualTo("1.3.6.1.1.21.1");
        transactionID=resStart.getTransactionID();
        assertThat(transactionID).isNotEmpty();
        add= Requests.newAddRequest("sn=bjensen0,ou=People,o=test")
                .addAttribute("objectClass","top","person")
                .addAttribute("cn","bjensen0")
                .addControl(new TransactionSpecificationRequestControl(transactionID))
        ;
        result = connection.add(add);
        assertThat(result.isSuccess()).isTrue();
        add= Requests.newAddRequest("sn=bjensen,ou=People,o=test")
                .addAttribute("objectClass","top","person")
                .addAttribute("cn","bjensen")
                .addControl(new TransactionSpecificationRequestControl(transactionID))
        ;
        result = connection.add(add);
        assertThat(result.isSuccess()).isTrue();
        mdn=Requests.newModifyDNRequest("sn=bjensen,ou=People,o=test","sn=bjensen2")
                .addControl(new TransactionSpecificationRequestControl(transactionID))
                ;
        result = connection.modifyDN(mdn);
        assertThat(result.isSuccess()).isTrue();
        edit= Requests.newModifyRequest("sn=bjensen2,ou=People,o=test")
                .addModification(ModificationType.REPLACE,"cn","bjensen2")
                .addControl(new TransactionSpecificationRequestControl(transactionID))
                ;
        result = connection.modify(edit);
        assertThat(result.isSuccess()).isTrue();
        delete=Requests.newDeleteRequest("sn=bjensen2,ou=People,o=test")
                .addControl(new TransactionSpecificationRequestControl(transactionID))
                ;
        result = connection.delete(delete);
        assertThat(result.isSuccess()).isTrue();
        delete=Requests.newDeleteRequest("sn=bjensen3,ou=People,o=test")
                .addControl(new TransactionSpecificationRequestControl(transactionID))
        ;
        result = connection.delete(delete);
        assertThat(result.isSuccess()).isTrue();
        String finalTransactionID3 = transactionID;
        assertThrows(EntryNotFoundException.class, new Assert.ThrowingRunnable() {
            @Override
            public void run() throws Throwable {
                EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(finalTransactionID3).setCommit(true));
            }
        });
        //check transaction finished
        String finalTransactionID2 = transactionID;
        assertThrows(CancelledResultException.class, new Assert.ThrowingRunnable() {
            @Override
            public void run() throws Throwable {
                EndTransactionExtendedResult resEnd=connection.extendedRequest(new EndTransactionExtendedRequest().setTransactionID(finalTransactionID2).setCommit(false));
            }
        });
        //check rollback successfully
        assertThrows(EntryNotFoundException.class, new Assert.ThrowingRunnable() {
            @Override
            public void run() throws Throwable {
                connection.searchSingleEntry("ou=People,o=test",SearchScope.SINGLE_LEVEL,"(cn=bjensen0)");
            }
        });
    }
}