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}