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