001    /*
002     * Copyright 2011-2015 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2011-2015 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     */
021    package com.unboundid.ldap.listener;
022    
023    
024    
025    import java.util.ArrayList;
026    import java.util.Arrays;
027    import java.util.LinkedHashMap;
028    import java.util.List;
029    import java.util.Map;
030    import java.util.concurrent.atomic.AtomicLong;
031    
032    import com.unboundid.asn1.ASN1OctetString;
033    import com.unboundid.ldap.protocol.AddResponseProtocolOp;
034    import com.unboundid.ldap.protocol.DeleteResponseProtocolOp;
035    import com.unboundid.ldap.protocol.ModifyResponseProtocolOp;
036    import com.unboundid.ldap.protocol.ModifyDNResponseProtocolOp;
037    import com.unboundid.ldap.protocol.LDAPMessage;
038    import com.unboundid.ldap.sdk.Control;
039    import com.unboundid.ldap.sdk.ExtendedRequest;
040    import com.unboundid.ldap.sdk.ExtendedResult;
041    import com.unboundid.ldap.sdk.LDAPException;
042    import com.unboundid.ldap.sdk.ResultCode;
043    import com.unboundid.ldap.sdk.extensions.AbortedTransactionExtendedResult;
044    import com.unboundid.ldap.sdk.extensions.EndTransactionExtendedRequest;
045    import com.unboundid.ldap.sdk.extensions.EndTransactionExtendedResult;
046    import com.unboundid.ldap.sdk.extensions.StartTransactionExtendedRequest;
047    import com.unboundid.ldap.sdk.extensions.StartTransactionExtendedResult;
048    import com.unboundid.util.Debug;
049    import com.unboundid.util.NotMutable;
050    import com.unboundid.util.ObjectPair;
051    import com.unboundid.util.ThreadSafety;
052    import com.unboundid.util.ThreadSafetyLevel;
053    
054    import 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)
065    public 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;
375    txnOpLoop:
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    }