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