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