001/*
002 * Copyright 2011-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2011-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2011-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.listener;
037
038
039
040import java.util.ArrayList;
041import java.util.Arrays;
042import java.util.LinkedHashMap;
043import java.util.List;
044import java.util.Map;
045import java.util.concurrent.atomic.AtomicLong;
046
047import com.unboundid.asn1.ASN1OctetString;
048import com.unboundid.ldap.protocol.AddResponseProtocolOp;
049import com.unboundid.ldap.protocol.DeleteResponseProtocolOp;
050import com.unboundid.ldap.protocol.ModifyResponseProtocolOp;
051import com.unboundid.ldap.protocol.ModifyDNResponseProtocolOp;
052import com.unboundid.ldap.protocol.LDAPMessage;
053import com.unboundid.ldap.sdk.Control;
054import com.unboundid.ldap.sdk.ExtendedRequest;
055import com.unboundid.ldap.sdk.ExtendedResult;
056import com.unboundid.ldap.sdk.LDAPException;
057import com.unboundid.ldap.sdk.ResultCode;
058import com.unboundid.ldap.sdk.extensions.AbortedTransactionExtendedResult;
059import com.unboundid.ldap.sdk.extensions.EndTransactionExtendedRequest;
060import com.unboundid.ldap.sdk.extensions.EndTransactionExtendedResult;
061import com.unboundid.ldap.sdk.extensions.StartTransactionExtendedRequest;
062import com.unboundid.ldap.sdk.extensions.StartTransactionExtendedResult;
063import com.unboundid.util.Debug;
064import com.unboundid.util.NotMutable;
065import com.unboundid.util.NotNull;
066import com.unboundid.util.ObjectPair;
067import com.unboundid.util.StaticUtils;
068import com.unboundid.util.ThreadSafety;
069import com.unboundid.util.ThreadSafetyLevel;
070
071import static com.unboundid.ldap.listener.ListenerMessages.*;
072
073
074
075/**
076 * This class provides an implementation of an extended operation handler for
077 * the start transaction and end transaction extended operations as defined in
078 * <A HREF="http://www.ietf.org/rfc/rfc5805.txt">RFC 5805</A>.
079 */
080@NotMutable()
081@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
082public final class TransactionExtendedOperationHandler
083       extends InMemoryExtendedOperationHandler
084{
085  /**
086   * The counter that will be used to generate transaction IDs.
087   */
088  @NotNull private static final AtomicLong TXN_ID_COUNTER = new AtomicLong(1L);
089
090
091
092  /**
093   * The name of the connection state variable that will be used to hold the
094   * transaction ID for the active transaction on the associated connection.
095   */
096  @NotNull static final String STATE_VARIABLE_TXN_INFO = "TXN-INFO";
097
098
099
100  /**
101   * Creates a new instance of this extended operation handler.
102   */
103  public TransactionExtendedOperationHandler()
104  {
105    // No initialization is required.
106  }
107
108
109
110  /**
111   * {@inheritDoc}
112   */
113  @Override()
114  @NotNull()
115  public String getExtendedOperationHandlerName()
116  {
117    return "LDAP Transactions";
118  }
119
120
121
122  /**
123   * {@inheritDoc}
124   */
125  @Override()
126  @NotNull()
127  public List<String> getSupportedExtendedRequestOIDs()
128  {
129    return Arrays.asList(
130         StartTransactionExtendedRequest.START_TRANSACTION_REQUEST_OID,
131         EndTransactionExtendedRequest.END_TRANSACTION_REQUEST_OID);
132  }
133
134
135
136  /**
137   * {@inheritDoc}
138   */
139  @Override()
140  @NotNull()
141  public ExtendedResult processExtendedOperation(
142                             @NotNull final InMemoryRequestHandler handler,
143                             final int messageID,
144                             @NotNull final ExtendedRequest request)
145  {
146    // This extended operation handler does not support any controls.  If the
147    // request has any critical controls, then reject it.
148    for (final Control c : request.getControls())
149    {
150      if (c.isCritical())
151      {
152        // See if there is a transaction already in progress.  If so, then abort
153        // it.
154        final ObjectPair<?,?> existingTxnInfo = (ObjectPair<?,?>)
155             handler.getConnectionState().remove(STATE_VARIABLE_TXN_INFO);
156        if (existingTxnInfo != null)
157        {
158          final ASN1OctetString txnID =
159               (ASN1OctetString) existingTxnInfo.getFirst();
160          try
161          {
162            handler.getClientConnection().sendUnsolicitedNotification(
163                 new AbortedTransactionExtendedResult(txnID,
164                      ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
165                      ERR_TXN_EXTOP_ABORTED_BY_UNSUPPORTED_CONTROL.get(
166                           txnID.stringValue(), c.getOID()),
167                      null, null, null));
168          }
169          catch (final LDAPException le)
170          {
171            Debug.debugException(le);
172            return new ExtendedResult(le);
173          }
174        }
175
176        return new ExtendedResult(messageID,
177             ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
178             ERR_TXN_EXTOP_UNSUPPORTED_CONTROL.get(c.getOID()), null, null,
179             null, null, null);
180      }
181    }
182
183
184    // Figure out whether the request represents a start or end transaction
185    // request and handle it appropriately.
186    final String oid = request.getOID();
187    if (oid.equals(
188             StartTransactionExtendedRequest.START_TRANSACTION_REQUEST_OID))
189    {
190      return handleStartTransaction(handler, messageID, request);
191    }
192    else
193    {
194      return handleEndTransaction(handler, messageID, request);
195    }
196  }
197
198
199
200  /**
201   * Performs the appropriate processing for a start transaction extended
202   * request.
203   *
204   * @param  handler    The in-memory request handler that received the request.
205   * @param  messageID  The message ID for the associated request.
206   * @param  request    The extended request that was received.
207   *
208   * @return  The result for the extended operation processing.
209   */
210  @NotNull()
211  private static StartTransactionExtendedResult handleStartTransaction(
212                      @NotNull final InMemoryRequestHandler handler,
213                      final int messageID,
214                      @NotNull final ExtendedRequest request)
215  {
216    // If there is already an active transaction on the associated connection,
217    // then make sure it gets aborted.
218    final Map<String,Object> connectionState = handler.getConnectionState();
219    final ObjectPair<?,?> existingTxnInfo =
220         (ObjectPair<?,?>) connectionState.remove(STATE_VARIABLE_TXN_INFO);
221    if (existingTxnInfo != null)
222    {
223      final ASN1OctetString txnID =
224           (ASN1OctetString) existingTxnInfo.getFirst();
225
226      try
227      {
228        handler.getClientConnection().sendUnsolicitedNotification(
229             new AbortedTransactionExtendedResult(txnID,
230                  ResultCode.CONSTRAINT_VIOLATION,
231                  ERR_TXN_EXTOP_TXN_ABORTED_BY_NEW_START_TXN.get(
232                       txnID.stringValue()),
233                  null, null, null));
234      }
235      catch (final LDAPException le)
236      {
237        Debug.debugException(le);
238        return new StartTransactionExtendedResult(
239             new ExtendedResult(le));
240      }
241    }
242
243
244    // Make sure that we can decode the provided request as a start transaction
245    // request.
246    try
247    {
248      new StartTransactionExtendedRequest(request);
249    }
250    catch (final LDAPException le)
251    {
252      Debug.debugException(le);
253      return new StartTransactionExtendedResult(messageID,
254           ResultCode.PROTOCOL_ERROR, le.getMessage(), null, null, null,
255           null);
256    }
257
258
259    // Create a new object with information to use for the transaction.  It will
260    // include the transaction ID and a list of LDAP messages that are part of
261    // the transaction.  Store it in the connection state.
262    final ASN1OctetString txnID =
263         new ASN1OctetString(String.valueOf(TXN_ID_COUNTER.getAndIncrement()));
264    final List<LDAPMessage> requestList = new ArrayList<>(10);
265    final ObjectPair<ASN1OctetString,List<LDAPMessage>> txnInfo =
266         new ObjectPair<>(txnID, requestList);
267    connectionState.put(STATE_VARIABLE_TXN_INFO, txnInfo);
268
269
270    // Return the response to the client.
271    return new StartTransactionExtendedResult(messageID, ResultCode.SUCCESS,
272         INFO_TXN_EXTOP_CREATED_TXN.get(txnID.stringValue()), null, null, txnID,
273         null);
274  }
275
276
277
278  /**
279   * Performs the appropriate processing for an end transaction extended
280   * request.
281   *
282   * @param  handler    The in-memory request handler that received the request.
283   * @param  messageID  The message ID for the associated request.
284   * @param  request    The extended request that was received.
285   *
286   * @return  The result for the extended operation processing.
287   */
288  @NotNull()
289  private static EndTransactionExtendedResult handleEndTransaction(
290                      @NotNull final InMemoryRequestHandler handler,
291                      final int messageID,
292                      @NotNull final ExtendedRequest request)
293  {
294    // Get information about any transaction currently in progress on the
295    // connection.  If there isn't one, then fail.
296    final Map<String,Object> connectionState = handler.getConnectionState();
297    final ObjectPair<?,?> txnInfo =
298         (ObjectPair<?,?>) connectionState.remove(STATE_VARIABLE_TXN_INFO);
299    if (txnInfo == null)
300    {
301      return new EndTransactionExtendedResult(messageID,
302           ResultCode.CONSTRAINT_VIOLATION,
303           ERR_TXN_EXTOP_END_NO_ACTIVE_TXN.get(), null, null, null, null,
304           null);
305    }
306
307
308    // Make sure that we can decode the end transaction request.
309    final ASN1OctetString existingTxnID = (ASN1OctetString) txnInfo.getFirst();
310    final EndTransactionExtendedRequest endTxnRequest;
311    try
312    {
313      endTxnRequest = new EndTransactionExtendedRequest(request);
314    }
315    catch (final LDAPException le)
316    {
317      Debug.debugException(le);
318
319      try
320      {
321        handler.getClientConnection().sendUnsolicitedNotification(
322             new AbortedTransactionExtendedResult(existingTxnID,
323                  ResultCode.PROTOCOL_ERROR,
324                  ERR_TXN_EXTOP_ABORTED_BY_MALFORMED_END_TXN.get(
325                       existingTxnID.stringValue()),
326                  null, null, null));
327      }
328      catch (final LDAPException le2)
329      {
330        Debug.debugException(le2);
331      }
332
333      return new EndTransactionExtendedResult(messageID,
334           ResultCode.PROTOCOL_ERROR, le.getMessage(), null, null, null, null,
335           null);
336    }
337
338
339    // Make sure that the transaction ID of the existing transaction matches the
340    // transaction ID from the end transaction request.
341    final ASN1OctetString targetTxnID = endTxnRequest.getTransactionID();
342    if (! existingTxnID.stringValue().equals(targetTxnID.stringValue()))
343    {
344      // Send an unsolicited notification indicating that the existing
345      // transaction has been aborted.
346      try
347      {
348        handler.getClientConnection().sendUnsolicitedNotification(
349             new AbortedTransactionExtendedResult(existingTxnID,
350                  ResultCode.CONSTRAINT_VIOLATION,
351                  ERR_TXN_EXTOP_ABORTED_BY_WRONG_END_TXN.get(
352                       existingTxnID.stringValue(), targetTxnID.stringValue()),
353                  null, null, null));
354      }
355      catch (final LDAPException le)
356      {
357        Debug.debugException(le);
358        return new EndTransactionExtendedResult(messageID,
359             le.getResultCode(), le.getMessage(), le.getMatchedDN(),
360             le.getReferralURLs(), null, null, le.getResponseControls());
361      }
362
363      return new EndTransactionExtendedResult(messageID,
364           ResultCode.CONSTRAINT_VIOLATION,
365           ERR_TXN_EXTOP_END_WRONG_TXN.get(targetTxnID.stringValue(),
366                existingTxnID.stringValue()),
367           null, null, null, null, null);
368    }
369
370
371    // If the transaction should be aborted, then we can just send the response.
372    if (! endTxnRequest.commit())
373    {
374      return new EndTransactionExtendedResult(messageID, ResultCode.SUCCESS,
375           INFO_TXN_EXTOP_END_TXN_ABORTED.get(existingTxnID.stringValue()),
376           null, null, null, null, null);
377    }
378
379
380    // If we've gotten here, then we'll try to commit the transaction.  First,
381    // get a snapshot of the current state so that we can roll back to it if
382    // necessary.
383    final InMemoryDirectoryServerSnapshot snapshot = handler.createSnapshot();
384    boolean rollBack = true;
385
386    try
387    {
388      // Create a map to hold information about response controls from
389      // operations processed as part of the transaction.
390      final List<?> requestMessages = (List<?>) txnInfo.getSecond();
391      final Map<Integer,Control[]> opResponseControls = new LinkedHashMap<>(
392           StaticUtils.computeMapCapacity(requestMessages.size()));
393
394      // Iterate through the requests that have been submitted as part of the
395      // transaction and attempt to process them.
396      ResultCode resultCode        = ResultCode.SUCCESS;
397      String     diagnosticMessage = null;
398      String     failedOpType      = null;
399      Integer    failedOpMessageID = null;
400txnOpLoop:
401      for (final Object o : requestMessages)
402      {
403        final LDAPMessage m = (LDAPMessage) o;
404        switch (m.getProtocolOpType())
405        {
406          case LDAPMessage.PROTOCOL_OP_TYPE_ADD_REQUEST:
407            final LDAPMessage addResponseMessage = handler.processAddRequest(
408                 m.getMessageID(), m.getAddRequestProtocolOp(),
409                 m.getControls());
410            final AddResponseProtocolOp addResponseOp =
411                 addResponseMessage.getAddResponseProtocolOp();
412            final List<Control> addControls = addResponseMessage.getControls();
413            if ((addControls != null) && (! addControls.isEmpty()))
414            {
415              final Control[] controls = new Control[addControls.size()];
416              addControls.toArray(controls);
417              opResponseControls.put(m.getMessageID(), controls);
418            }
419            if (addResponseOp.getResultCode() != ResultCode.SUCCESS_INT_VALUE)
420            {
421              resultCode = ResultCode.valueOf(addResponseOp.getResultCode());
422              diagnosticMessage = addResponseOp.getDiagnosticMessage();
423              failedOpType = INFO_TXN_EXTOP_OP_TYPE_ADD.get();
424              failedOpMessageID = m.getMessageID();
425              break txnOpLoop;
426            }
427            break;
428
429          case LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST:
430            final LDAPMessage deleteResponseMessage =
431                 handler.processDeleteRequest(m.getMessageID(),
432                      m.getDeleteRequestProtocolOp(), m.getControls());
433            final DeleteResponseProtocolOp deleteResponseOp =
434                 deleteResponseMessage.getDeleteResponseProtocolOp();
435            final List<Control> deleteControls =
436                 deleteResponseMessage.getControls();
437            if ((deleteControls != null) && (! deleteControls.isEmpty()))
438            {
439              final Control[] controls = new Control[deleteControls.size()];
440              deleteControls.toArray(controls);
441              opResponseControls.put(m.getMessageID(), controls);
442            }
443            if (deleteResponseOp.getResultCode() !=
444                     ResultCode.SUCCESS_INT_VALUE)
445            {
446              resultCode = ResultCode.valueOf(deleteResponseOp.getResultCode());
447              diagnosticMessage = deleteResponseOp.getDiagnosticMessage();
448              failedOpType = INFO_TXN_EXTOP_OP_TYPE_DELETE.get();
449              failedOpMessageID = m.getMessageID();
450              break txnOpLoop;
451            }
452            break;
453
454          case LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_REQUEST:
455            final LDAPMessage modifyResponseMessage =
456                 handler.processModifyRequest(m.getMessageID(),
457                      m.getModifyRequestProtocolOp(), m.getControls());
458            final ModifyResponseProtocolOp modifyResponseOp =
459                 modifyResponseMessage.getModifyResponseProtocolOp();
460            final List<Control> modifyControls =
461                 modifyResponseMessage.getControls();
462            if ((modifyControls != null) && (! modifyControls.isEmpty()))
463            {
464              final Control[] controls = new Control[modifyControls.size()];
465              modifyControls.toArray(controls);
466              opResponseControls.put(m.getMessageID(), controls);
467            }
468            if (modifyResponseOp.getResultCode() !=
469                     ResultCode.SUCCESS_INT_VALUE)
470            {
471              resultCode = ResultCode.valueOf(modifyResponseOp.getResultCode());
472              diagnosticMessage = modifyResponseOp.getDiagnosticMessage();
473              failedOpType = INFO_TXN_EXTOP_OP_TYPE_MODIFY.get();
474              failedOpMessageID = m.getMessageID();
475              break txnOpLoop;
476            }
477            break;
478
479          case LDAPMessage.PROTOCOL_OP_TYPE_MODIFY_DN_REQUEST:
480            final LDAPMessage modifyDNResponseMessage =
481                 handler.processModifyDNRequest(m.getMessageID(),
482                      m.getModifyDNRequestProtocolOp(), m.getControls());
483            final ModifyDNResponseProtocolOp modifyDNResponseOp =
484                 modifyDNResponseMessage.getModifyDNResponseProtocolOp();
485            final List<Control> modifyDNControls =
486                 modifyDNResponseMessage.getControls();
487            if ((modifyDNControls != null) && (! modifyDNControls.isEmpty()))
488            {
489              final Control[] controls = new Control[modifyDNControls.size()];
490              modifyDNControls.toArray(controls);
491              opResponseControls.put(m.getMessageID(), controls);
492            }
493            if (modifyDNResponseOp.getResultCode() !=
494                     ResultCode.SUCCESS_INT_VALUE)
495            {
496              resultCode =
497                   ResultCode.valueOf(modifyDNResponseOp.getResultCode());
498              diagnosticMessage = modifyDNResponseOp.getDiagnosticMessage();
499              failedOpType = INFO_TXN_EXTOP_OP_TYPE_MODIFY_DN.get();
500              failedOpMessageID = m.getMessageID();
501              break txnOpLoop;
502            }
503            break;
504        }
505      }
506
507      if (resultCode == ResultCode.SUCCESS)
508      {
509        diagnosticMessage =
510             INFO_TXN_EXTOP_COMMITTED.get(existingTxnID.stringValue());
511        rollBack = false;
512      }
513      else
514      {
515        diagnosticMessage = ERR_TXN_EXTOP_COMMIT_FAILED.get(
516             existingTxnID.stringValue(), failedOpType, failedOpMessageID,
517             diagnosticMessage);
518      }
519
520      return new EndTransactionExtendedResult(messageID, resultCode,
521           diagnosticMessage, null, null, failedOpMessageID, opResponseControls,
522           null);
523    }
524    finally
525    {
526      if (rollBack)
527      {
528        handler.restoreSnapshot(snapshot);
529      }
530    }
531  }
532}