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