001 /*
002 * Copyright 2011-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2011-2016 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 }