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