001/* 002 * Copyright 2010-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2010-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) 2010-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.sdk.extensions; 037 038 039import java.util.ArrayList; 040import java.util.Map; 041import java.util.TreeMap; 042 043import com.unboundid.asn1.ASN1Constants; 044import com.unboundid.asn1.ASN1Element; 045import com.unboundid.asn1.ASN1Exception; 046import com.unboundid.asn1.ASN1Integer; 047import com.unboundid.asn1.ASN1OctetString; 048import com.unboundid.asn1.ASN1Sequence; 049import com.unboundid.ldap.sdk.Control; 050import com.unboundid.ldap.sdk.ExtendedResult; 051import com.unboundid.ldap.sdk.LDAPException; 052import com.unboundid.ldap.sdk.ResultCode; 053import com.unboundid.util.Debug; 054import com.unboundid.util.NotMutable; 055import com.unboundid.util.NotNull; 056import com.unboundid.util.Nullable; 057import com.unboundid.util.StaticUtils; 058import com.unboundid.util.ThreadSafety; 059import com.unboundid.util.ThreadSafetyLevel; 060 061import static com.unboundid.ldap.sdk.extensions.ExtOpMessages.*; 062 063 064 065/** 066 * This class provides an implementation of the end transaction extended result 067 * as defined in 068 * <A HREF="http://www.ietf.org/rfc/rfc5805.txt">RFC 5805</A>. It is able to 069 * decode a generic extended result to extract the appropriate response 070 * information. 071 * <BR><BR> 072 * See the documentation for the {@link StartTransactionExtendedRequest} class 073 * for an example of performing a transaction. 074 */ 075@NotMutable() 076@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 077public final class EndTransactionExtendedResult 078 extends ExtendedResult 079{ 080 /** 081 * The serial version UID for this serializable class. 082 */ 083 private static final long serialVersionUID = 1514265185948328221L; 084 085 086 087 // The message ID for the operation that failed, if applicable. 088 private final int failedOpMessageID; 089 090 // A mapping of the response controls for the operations performed as part of 091 // the transaction. 092 @NotNull private final TreeMap<Integer,Control[]> opResponseControls; 093 094 095 096 /** 097 * Creates a new end transaction extended result from the provided extended 098 * result. 099 * 100 * @param extendedResult The extended result to be decoded as an end 101 * transaction extended result. It must not be 102 * {@code null}. 103 * 104 * @throws LDAPException If a problem occurs while attempting to decode the 105 * provided extended result as an end transaction 106 * extended result. 107 */ 108 public EndTransactionExtendedResult( 109 @NotNull final ExtendedResult extendedResult) 110 throws LDAPException 111 { 112 super(extendedResult); 113 114 opResponseControls = new TreeMap<>(); 115 116 final ASN1OctetString value = extendedResult.getValue(); 117 if (value == null) 118 { 119 failedOpMessageID = -1; 120 return; 121 } 122 123 final ASN1Sequence valueSequence; 124 try 125 { 126 final ASN1Element valueElement = ASN1Element.decode(value.getValue()); 127 valueSequence = ASN1Sequence.decodeAsSequence(valueElement); 128 } 129 catch (final ASN1Exception ae) 130 { 131 Debug.debugException(ae); 132 throw new LDAPException(ResultCode.DECODING_ERROR, 133 ERR_END_TXN_RESPONSE_VALUE_NOT_SEQUENCE.get(ae.getMessage()), ae); 134 } 135 136 final ASN1Element[] valueElements = valueSequence.elements(); 137 if (valueElements.length == 0) 138 { 139 failedOpMessageID = -1; 140 return; 141 } 142 else if (valueElements.length > 2) 143 { 144 throw new LDAPException(ResultCode.DECODING_ERROR, 145 ERR_END_TXN_RESPONSE_INVALID_ELEMENT_COUNT.get( 146 valueElements.length)); 147 } 148 149 int msgID = -1; 150 for (final ASN1Element e : valueElements) 151 { 152 if (e.getType() == ASN1Constants.UNIVERSAL_INTEGER_TYPE) 153 { 154 try 155 { 156 msgID = ASN1Integer.decodeAsInteger(e).intValue(); 157 } 158 catch (final ASN1Exception ae) 159 { 160 Debug.debugException(ae); 161 throw new LDAPException(ResultCode.DECODING_ERROR, 162 ERR_END_TXN_RESPONSE_CANNOT_DECODE_MSGID.get(ae), ae); 163 } 164 } 165 else if (e.getType() == ASN1Constants.UNIVERSAL_SEQUENCE_TYPE) 166 { 167 decodeOpControls(e, opResponseControls); 168 } 169 else 170 { 171 throw new LDAPException(ResultCode.DECODING_ERROR, 172 ERR_END_TXN_RESPONSE_INVALID_TYPE.get( 173 StaticUtils.toHex(e.getType()))); 174 } 175 } 176 177 failedOpMessageID = msgID; 178 } 179 180 181 182 /** 183 * Creates a new end transaction extended result with the provided 184 * information. 185 * 186 * @param messageID The message ID for the LDAP message that is 187 * associated with this LDAP result. 188 * @param resultCode The result code from the response. 189 * @param diagnosticMessage The diagnostic message from the response, if 190 * available. 191 * @param matchedDN The matched DN from the response, if available. 192 * @param referralURLs The set of referral URLs from the response, if 193 * available. 194 * @param failedOpMessageID The message ID for the operation that failed, 195 * or {@code null} if there was no failure. 196 * @param opResponseControls A map containing the response controls for each 197 * operation, indexed by message ID. It may be 198 * {@code null} if there were no response 199 * controls. 200 * @param responseControls The set of controls from the response, if 201 * available. 202 */ 203 public EndTransactionExtendedResult(final int messageID, 204 @NotNull final ResultCode resultCode, 205 @Nullable final String diagnosticMessage, 206 @Nullable final String matchedDN, 207 @Nullable final String[] referralURLs, 208 @Nullable final Integer failedOpMessageID, 209 @Nullable final Map<Integer,Control[]> opResponseControls, 210 @Nullable final Control[] responseControls) 211 { 212 super(messageID, resultCode, diagnosticMessage, matchedDN, referralURLs, 213 null, encodeValue(failedOpMessageID, opResponseControls), 214 responseControls); 215 216 if ((failedOpMessageID == null) || (failedOpMessageID <= 0)) 217 { 218 this.failedOpMessageID = -1; 219 } 220 else 221 { 222 this.failedOpMessageID = failedOpMessageID; 223 } 224 225 if (opResponseControls == null) 226 { 227 this.opResponseControls = new TreeMap<>(); 228 } 229 else 230 { 231 this.opResponseControls = new TreeMap<>(opResponseControls); 232 } 233 } 234 235 236 237 /** 238 * Decodes the provided ASN.1 element as an update controls sequence. Each 239 * element of the sequence should itself be a sequence containing the message 240 * ID associated with the operation in which the control was returned and a 241 * sequence of the controls included in the response for that operation. 242 * 243 * @param element The ASN.1 element to be decoded. 244 * @param controlMap The map into which to place the decoded controls. 245 * 246 * @throws LDAPException If a problem occurs while attempting to decode the 247 * contents of the provided ASN.1 element. 248 */ 249 private static void decodeOpControls(@NotNull final ASN1Element element, 250 @NotNull final Map<Integer,Control[]> controlMap) 251 throws LDAPException 252 { 253 final ASN1Sequence ctlsSequence; 254 try 255 { 256 ctlsSequence = ASN1Sequence.decodeAsSequence(element); 257 } 258 catch (final ASN1Exception ae) 259 { 260 Debug.debugException(ae); 261 throw new LDAPException(ResultCode.DECODING_ERROR, 262 ERR_END_TXN_RESPONSE_CONTROLS_NOT_SEQUENCE.get(ae), ae); 263 } 264 265 for (final ASN1Element e : ctlsSequence.elements()) 266 { 267 final ASN1Sequence ctlSequence; 268 try 269 { 270 ctlSequence = ASN1Sequence.decodeAsSequence(e); 271 } 272 catch (final ASN1Exception ae) 273 { 274 Debug.debugException(ae); 275 throw new LDAPException(ResultCode.DECODING_ERROR, 276 ERR_END_TXN_RESPONSE_CONTROL_NOT_SEQUENCE.get(ae), ae); 277 } 278 279 final ASN1Element[] ctlSequenceElements = ctlSequence.elements(); 280 if (ctlSequenceElements.length != 2) 281 { 282 throw new LDAPException(ResultCode.DECODING_ERROR, 283 ERR_END_TXN_RESPONSE_CONTROL_INVALID_ELEMENT_COUNT.get( 284 ctlSequenceElements.length)); 285 } 286 287 final int msgID; 288 try 289 { 290 msgID = ASN1Integer.decodeAsInteger(ctlSequenceElements[0]).intValue(); 291 } 292 catch (final ASN1Exception ae) 293 { 294 Debug.debugException(ae); 295 throw new LDAPException(ResultCode.DECODING_ERROR, 296 ERR_END_TXN_RESPONSE_CONTROL_MSGID_NOT_INT.get(ae), ae); 297 } 298 299 final ASN1Sequence controlsSequence; 300 try 301 { 302 controlsSequence = 303 ASN1Sequence.decodeAsSequence(ctlSequenceElements[1]); 304 } 305 catch (final ASN1Exception ae) 306 { 307 Debug.debugException(ae); 308 throw new LDAPException(ResultCode.DECODING_ERROR, 309 ERR_END_TXN_RESPONSE_CONTROLS_ELEMENT_NOT_SEQUENCE.get(ae), ae); 310 } 311 312 final Control[] controls = Control.decodeControls(controlsSequence); 313 if (controls.length == 0) 314 { 315 continue; 316 } 317 318 controlMap.put(msgID, controls); 319 } 320 } 321 322 323 324 /** 325 * Encodes the provided information into an appropriate value for this 326 * control. 327 * 328 * @param failedOpMessageID The message ID for the operation that failed, 329 * or {@code null} if there was no failure. 330 * @param opResponseControls A map containing the response controls for each 331 * operation, indexed by message ID. It may be 332 * {@code null} if there were no response 333 * controls. 334 * 335 * @return An ASN.1 octet string containing the encoded value for this 336 * control, or {@code null} if there should not be a value. 337 */ 338 @Nullable() 339 private static ASN1OctetString encodeValue( 340 @Nullable final Integer failedOpMessageID, 341 @Nullable final Map<Integer,Control[]> opResponseControls) 342 { 343 if ((failedOpMessageID == null) && (opResponseControls == null)) 344 { 345 return null; 346 } 347 348 final ArrayList<ASN1Element> elements = new ArrayList<>(2); 349 if (failedOpMessageID != null) 350 { 351 elements.add(new ASN1Integer(failedOpMessageID)); 352 } 353 354 if ((opResponseControls != null) && (! opResponseControls.isEmpty())) 355 { 356 final ArrayList<ASN1Element> controlElements = new ArrayList<>(10); 357 for (final Map.Entry<Integer,Control[]> e : opResponseControls.entrySet()) 358 { 359 final ASN1Element[] ctlElements = 360 { 361 new ASN1Integer(e.getKey()), 362 Control.encodeControls(e.getValue()) 363 }; 364 controlElements.add(new ASN1Sequence(ctlElements)); 365 } 366 367 elements.add(new ASN1Sequence(controlElements)); 368 } 369 370 return new ASN1OctetString(new ASN1Sequence(elements).encode()); 371 } 372 373 374 375 /** 376 * Retrieves the message ID of the operation that caused the transaction 377 * processing to fail, if applicable. 378 * 379 * @return The message ID of the operation that caused the transaction 380 * processing to fail, or -1 if no message ID was included in the 381 * end transaction response. 382 */ 383 public int getFailedOpMessageID() 384 { 385 return failedOpMessageID; 386 } 387 388 389 390 /** 391 * Retrieves the set of response controls returned by the operations 392 * processed as part of the transaction. The value returned will contain a 393 * mapping between the message ID of the associated request message and a list 394 * of the response controls for that operation. 395 * 396 * @return The set of response controls returned by the operations processed 397 * as part of the transaction. It may be an empty map if none of the 398 * operations had any response controls. 399 */ 400 @NotNull() 401 public Map<Integer,Control[]> getOperationResponseControls() 402 { 403 return opResponseControls; 404 } 405 406 407 408 /** 409 * Retrieves the set of response controls returned by the specified operation 410 * processed as part of the transaction. 411 * 412 * @param messageID The message ID of the operation for which to retrieve 413 * the response controls. 414 * 415 * @return The response controls for the specified operation, or 416 * {@code null} if there were no controls returned for the specified 417 * operation. 418 */ 419 @Nullable() 420 public Control[] getOperationResponseControls(final int messageID) 421 { 422 return opResponseControls.get(messageID); 423 } 424 425 426 427 /** 428 * {@inheritDoc} 429 */ 430 @Override() 431 @NotNull() 432 public String getExtendedResultName() 433 { 434 return INFO_EXTENDED_RESULT_NAME_END_TXN.get(); 435 } 436 437 438 439 /** 440 * Appends a string representation of this extended result to the provided 441 * buffer. 442 * 443 * @param buffer The buffer to which a string representation of this 444 * extended result will be appended. 445 */ 446 @Override() 447 public void toString(@NotNull final StringBuilder buffer) 448 { 449 buffer.append("EndTransactionExtendedResult(resultCode="); 450 buffer.append(getResultCode()); 451 452 final int messageID = getMessageID(); 453 if (messageID >= 0) 454 { 455 buffer.append(", messageID="); 456 buffer.append(messageID); 457 } 458 459 if (failedOpMessageID > 0) 460 { 461 buffer.append(", failedOpMessageID="); 462 buffer.append(failedOpMessageID); 463 } 464 465 if (! opResponseControls.isEmpty()) 466 { 467 buffer.append(", opResponseControls={"); 468 469 for (final int msgID : opResponseControls.keySet()) 470 { 471 buffer.append("opMsgID="); 472 buffer.append(msgID); 473 buffer.append(", opControls={"); 474 475 boolean first = true; 476 for (final Control c : opResponseControls.get(msgID)) 477 { 478 if (first) 479 { 480 first = false; 481 } 482 else 483 { 484 buffer.append(", "); 485 } 486 487 buffer.append(c); 488 } 489 buffer.append('}'); 490 } 491 492 buffer.append('}'); 493 } 494 495 final String diagnosticMessage = getDiagnosticMessage(); 496 if (diagnosticMessage != null) 497 { 498 buffer.append(", diagnosticMessage='"); 499 buffer.append(diagnosticMessage); 500 buffer.append('\''); 501 } 502 503 final String matchedDN = getMatchedDN(); 504 if (matchedDN != null) 505 { 506 buffer.append(", matchedDN='"); 507 buffer.append(matchedDN); 508 buffer.append('\''); 509 } 510 511 final String[] referralURLs = getReferralURLs(); 512 if (referralURLs.length > 0) 513 { 514 buffer.append(", referralURLs={"); 515 for (int i=0; i < referralURLs.length; i++) 516 { 517 if (i > 0) 518 { 519 buffer.append(", "); 520 } 521 522 buffer.append('\''); 523 buffer.append(referralURLs[i]); 524 buffer.append('\''); 525 } 526 buffer.append('}'); 527 } 528 529 final Control[] responseControls = getResponseControls(); 530 if (responseControls.length > 0) 531 { 532 buffer.append(", responseControls={"); 533 for (int i=0; i < responseControls.length; i++) 534 { 535 if (i > 0) 536 { 537 buffer.append(", "); 538 } 539 540 buffer.append(responseControls[i]); 541 } 542 buffer.append('}'); 543 } 544 545 buffer.append(')'); 546 } 547}