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