001 /* 002 * Copyright 2007-2014 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005 /* 006 * Copyright (C) 2008-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 */ 021 package com.unboundid.ldap.sdk; 022 023 024 025 import java.util.Timer; 026 import java.util.concurrent.LinkedBlockingQueue; 027 import java.util.concurrent.TimeUnit; 028 029 import com.unboundid.asn1.ASN1Buffer; 030 import com.unboundid.asn1.ASN1Element; 031 import com.unboundid.asn1.ASN1OctetString; 032 import com.unboundid.ldap.protocol.LDAPMessage; 033 import com.unboundid.ldap.protocol.LDAPResponse; 034 import com.unboundid.ldap.protocol.ProtocolOp; 035 import com.unboundid.ldif.LDIFDeleteChangeRecord; 036 import com.unboundid.util.InternalUseOnly; 037 038 import static com.unboundid.ldap.sdk.LDAPMessages.*; 039 import static com.unboundid.util.Debug.*; 040 import static com.unboundid.util.StaticUtils.*; 041 import static com.unboundid.util.Validator.*; 042 043 044 045 /** 046 * This class implements the processing necessary to perform an LDAPv3 delete 047 * operation, which removes an entry from the directory. A delete request 048 * contains the DN of the entry to remove. It may also include a set of 049 * controls to send to the server. 050 * {@code DeleteRequest} objects are mutable and therefore can be altered and 051 * re-used for multiple requests. Note, however, that {@code DeleteRequest} 052 * objects are not threadsafe and therefore a single {@code DeleteRequest} 053 * object instance should not be used to process multiple requests at the same 054 * time. 055 * <BR><BR> 056 * <H2>Example</H2> 057 * The following example demonstrates the process for performing a delete 058 * operation: 059 * <PRE> 060 * DeleteRequest deleteRequest = 061 * new DeleteRequest("cn=entry to delete,dc=example,dc=com"); 062 * LDAPResult deleteResult; 063 * try 064 * { 065 * deleteResult = connection.delete(deleteRequest); 066 * // If we get here, the delete was successful. 067 * } 068 * catch (LDAPException le) 069 * { 070 * // The delete operation failed. 071 * deleteResult = le.toLDAPResult(); 072 * ResultCode resultCode = le.getResultCode(); 073 * String errorMessageFromServer = le.getDiagnosticMessage(); 074 * } 075 * </PRE> 076 */ 077 public final class DeleteRequest 078 extends UpdatableLDAPRequest 079 implements ReadOnlyDeleteRequest, ResponseAcceptor, ProtocolOp 080 { 081 /** 082 * The serial version UID for this serializable class. 083 */ 084 private static final long serialVersionUID = -6126029442850884239L; 085 086 087 088 // The message ID from the last LDAP message sent from this request. 089 private int messageID = -1; 090 091 // The queue that will be used to receive response messages from the server. 092 private final LinkedBlockingQueue<LDAPResponse> responseQueue = 093 new LinkedBlockingQueue<LDAPResponse>(); 094 095 // The DN of the entry to delete. 096 private String dn; 097 098 099 100 /** 101 * Creates a new delete request with the provided DN. 102 * 103 * @param dn The DN of the entry to delete. It must not be {@code null}. 104 */ 105 public DeleteRequest(final String dn) 106 { 107 super(null); 108 109 ensureNotNull(dn); 110 111 this.dn = dn; 112 } 113 114 115 116 /** 117 * Creates a new delete request with the provided DN. 118 * 119 * @param dn The DN of the entry to delete. It must not be 120 * {@code null}. 121 * @param controls The set of controls to include in the request. 122 */ 123 public DeleteRequest(final String dn, final Control[] controls) 124 { 125 super(controls); 126 127 ensureNotNull(dn); 128 129 this.dn = dn; 130 } 131 132 133 134 /** 135 * Creates a new delete request with the provided DN. 136 * 137 * @param dn The DN of the entry to delete. It must not be {@code null}. 138 */ 139 public DeleteRequest(final DN dn) 140 { 141 super(null); 142 143 ensureNotNull(dn); 144 145 this.dn = dn.toString(); 146 } 147 148 149 150 /** 151 * Creates a new delete request with the provided DN. 152 * 153 * @param dn The DN of the entry to delete. It must not be 154 * {@code null}. 155 * @param controls The set of controls to include in the request. 156 */ 157 public DeleteRequest(final DN dn, final Control[] controls) 158 { 159 super(controls); 160 161 ensureNotNull(dn); 162 163 this.dn = dn.toString(); 164 } 165 166 167 168 /** 169 * {@inheritDoc} 170 */ 171 public String getDN() 172 { 173 return dn; 174 } 175 176 177 178 /** 179 * Specifies the DN of the entry to delete. 180 * 181 * @param dn The DN of the entry to delete. It must not be {@code null}. 182 */ 183 public void setDN(final String dn) 184 { 185 ensureNotNull(dn); 186 187 this.dn = dn; 188 } 189 190 191 192 /** 193 * Specifies the DN of the entry to delete. 194 * 195 * @param dn The DN of the entry to delete. It must not be {@code null}. 196 */ 197 public void setDN(final DN dn) 198 { 199 ensureNotNull(dn); 200 201 this.dn = dn.toString(); 202 } 203 204 205 206 /** 207 * {@inheritDoc} 208 */ 209 public byte getProtocolOpType() 210 { 211 return LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST; 212 } 213 214 215 216 /** 217 * {@inheritDoc} 218 */ 219 public void writeTo(final ASN1Buffer buffer) 220 { 221 buffer.addOctetString(LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST, dn); 222 } 223 224 225 226 /** 227 * Encodes the delete request protocol op to an ASN.1 element. 228 * 229 * @return The ASN.1 element with the encoded delete request protocol op. 230 */ 231 public ASN1Element encodeProtocolOp() 232 { 233 return new ASN1OctetString(LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST, dn); 234 } 235 236 237 238 /** 239 * Sends this delete request to the directory server over the provided 240 * connection and returns the associated response. 241 * 242 * @param connection The connection to use to communicate with the directory 243 * server. 244 * @param depth The current referral depth for this request. It should 245 * always be one for the initial request, and should only 246 * be incremented when following referrals. 247 * 248 * @return An LDAP result object that provides information about the result 249 * of the delete processing. 250 * 251 * @throws LDAPException If a problem occurs while sending the request or 252 * reading the response. 253 */ 254 @Override() 255 protected LDAPResult process(final LDAPConnection connection, final int depth) 256 throws LDAPException 257 { 258 if (connection.synchronousMode()) 259 { 260 return processSync(connection, depth, 261 connection.getConnectionOptions().autoReconnect()); 262 } 263 264 final long requestTime = System.nanoTime(); 265 processAsync(connection, null); 266 267 try 268 { 269 // Wait for and process the response. 270 final LDAPResponse response; 271 try 272 { 273 final long responseTimeout = getResponseTimeoutMillis(connection); 274 if (responseTimeout > 0) 275 { 276 response = responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS); 277 } 278 else 279 { 280 response = responseQueue.take(); 281 } 282 } 283 catch (InterruptedException ie) 284 { 285 debugException(ie); 286 throw new LDAPException(ResultCode.LOCAL_ERROR, 287 ERR_DELETE_INTERRUPTED.get(connection.getHostPort()), ie); 288 } 289 290 return handleResponse(connection, response, requestTime, depth, false); 291 } 292 finally 293 { 294 connection.deregisterResponseAcceptor(messageID); 295 } 296 } 297 298 299 300 /** 301 * Sends this delete request to the directory server over the provided 302 * connection and returns the message ID for the request. 303 * 304 * @param connection The connection to use to communicate with the 305 * directory server. 306 * @param resultListener The async result listener that is to be notified 307 * when the response is received. It may be 308 * {@code null} only if the result is to be processed 309 * by this class. 310 * 311 * @return The async request ID created for the operation, or {@code null} if 312 * the provided {@code resultListener} is {@code null} and the 313 * operation will not actually be processed asynchronously. 314 * 315 * @throws LDAPException If a problem occurs while sending the request. 316 */ 317 AsyncRequestID processAsync(final LDAPConnection connection, 318 final AsyncResultListener resultListener) 319 throws LDAPException 320 { 321 // Create the LDAP message. 322 messageID = connection.nextMessageID(); 323 final LDAPMessage message = new LDAPMessage(messageID, this, getControls()); 324 325 326 // If the provided async result listener is {@code null}, then we'll use 327 // this class as the message acceptor. Otherwise, create an async helper 328 // and use it as the message acceptor. 329 final AsyncRequestID asyncRequestID; 330 if (resultListener == null) 331 { 332 asyncRequestID = null; 333 connection.registerResponseAcceptor(messageID, this); 334 } 335 else 336 { 337 final AsyncHelper helper = new AsyncHelper(connection, 338 OperationType.DELETE, messageID, resultListener, 339 getIntermediateResponseListener()); 340 connection.registerResponseAcceptor(messageID, helper); 341 asyncRequestID = helper.getAsyncRequestID(); 342 343 final long timeout = getResponseTimeoutMillis(connection); 344 if (timeout > 0L) 345 { 346 final Timer timer = connection.getTimer(); 347 final AsyncTimeoutTimerTask timerTask = 348 new AsyncTimeoutTimerTask(helper); 349 timer.schedule(timerTask, timeout); 350 asyncRequestID.setTimerTask(timerTask); 351 } 352 } 353 354 355 // Send the request to the server. 356 try 357 { 358 debugLDAPRequest(this); 359 connection.getConnectionStatistics().incrementNumDeleteRequests(); 360 connection.sendMessage(message); 361 return asyncRequestID; 362 } 363 catch (LDAPException le) 364 { 365 debugException(le); 366 367 connection.deregisterResponseAcceptor(messageID); 368 throw le; 369 } 370 } 371 372 373 374 /** 375 * Processes this delete operation in synchronous mode, in which the same 376 * thread will send the request and read the response. 377 * 378 * @param connection The connection to use to communicate with the directory 379 * server. 380 * @param depth The current referral depth for this request. It should 381 * always be one for the initial request, and should only 382 * be incremented when following referrals. 383 * @param allowRetry Indicates whether the request may be re-tried on a 384 * re-established connection if the initial attempt fails 385 * in a way that indicates the connection is no longer 386 * valid and autoReconnect is true. 387 * 388 * @return An LDAP result object that provides information about the result 389 * of the delete processing. 390 * 391 * @throws LDAPException If a problem occurs while sending the request or 392 * reading the response. 393 */ 394 private LDAPResult processSync(final LDAPConnection connection, 395 final int depth, final boolean allowRetry) 396 throws LDAPException 397 { 398 // Create the LDAP message. 399 messageID = connection.nextMessageID(); 400 final LDAPMessage message = 401 new LDAPMessage(messageID, this, getControls()); 402 403 404 // Set the appropriate timeout on the socket. 405 try 406 { 407 connection.getConnectionInternals(true).getSocket().setSoTimeout( 408 (int) getResponseTimeoutMillis(connection)); 409 } 410 catch (Exception e) 411 { 412 debugException(e); 413 } 414 415 416 // Send the request to the server. 417 final long requestTime = System.nanoTime(); 418 debugLDAPRequest(this); 419 connection.getConnectionStatistics().incrementNumDeleteRequests(); 420 try 421 { 422 connection.sendMessage(message); 423 } 424 catch (final LDAPException le) 425 { 426 debugException(le); 427 428 if (allowRetry) 429 { 430 final LDAPResult retryResult = reconnectAndRetry(connection, depth, 431 le.getResultCode()); 432 if (retryResult != null) 433 { 434 return retryResult; 435 } 436 } 437 438 throw le; 439 } 440 441 while (true) 442 { 443 final LDAPResponse response; 444 try 445 { 446 response = connection.readResponse(messageID); 447 } 448 catch (final LDAPException le) 449 { 450 debugException(le); 451 452 if ((le.getResultCode() == ResultCode.TIMEOUT) && 453 connection.getConnectionOptions().abandonOnTimeout()) 454 { 455 connection.abandon(messageID); 456 } 457 458 if (allowRetry) 459 { 460 final LDAPResult retryResult = reconnectAndRetry(connection, depth, 461 le.getResultCode()); 462 if (retryResult != null) 463 { 464 return retryResult; 465 } 466 } 467 468 throw le; 469 } 470 471 if (response instanceof IntermediateResponse) 472 { 473 final IntermediateResponseListener listener = 474 getIntermediateResponseListener(); 475 if (listener != null) 476 { 477 listener.intermediateResponseReturned( 478 (IntermediateResponse) response); 479 } 480 } 481 else 482 { 483 return handleResponse(connection, response, requestTime, depth, 484 allowRetry); 485 } 486 } 487 } 488 489 490 491 /** 492 * Performs the necessary processing for handling a response. 493 * 494 * @param connection The connection used to read the response. 495 * @param response The response to be processed. 496 * @param requestTime The time the request was sent to the server. 497 * @param depth The current referral depth for this request. It 498 * should always be one for the initial request, and 499 * should only be incremented when following referrals. 500 * @param allowRetry Indicates whether the request may be re-tried on a 501 * re-established connection if the initial attempt fails 502 * in a way that indicates the connection is no longer 503 * valid and autoReconnect is true. 504 * 505 * @return The delete result. 506 * 507 * @throws LDAPException If a problem occurs. 508 */ 509 private LDAPResult handleResponse(final LDAPConnection connection, 510 final LDAPResponse response, 511 final long requestTime, final int depth, 512 final boolean allowRetry) 513 throws LDAPException 514 { 515 if (response == null) 516 { 517 final long waitTime = nanosToMillis(System.nanoTime() - requestTime); 518 if (connection.getConnectionOptions().abandonOnTimeout()) 519 { 520 connection.abandon(messageID); 521 } 522 523 throw new LDAPException(ResultCode.TIMEOUT, 524 ERR_DELETE_CLIENT_TIMEOUT.get(waitTime, messageID, dn, 525 connection.getHostPort())); 526 } 527 528 connection.getConnectionStatistics().incrementNumDeleteResponses( 529 System.nanoTime() - requestTime); 530 if (response instanceof ConnectionClosedResponse) 531 { 532 // The connection was closed while waiting for the response. 533 if (allowRetry) 534 { 535 final LDAPResult retryResult = reconnectAndRetry(connection, depth, 536 ResultCode.SERVER_DOWN); 537 if (retryResult != null) 538 { 539 return retryResult; 540 } 541 } 542 543 final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response; 544 final String message = ccr.getMessage(); 545 if (message == null) 546 { 547 throw new LDAPException(ccr.getResultCode(), 548 ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE.get( 549 connection.getHostPort(), toString())); 550 } 551 else 552 { 553 throw new LDAPException(ccr.getResultCode(), 554 ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE_WITH_MESSAGE.get( 555 connection.getHostPort(), toString(), message)); 556 } 557 } 558 559 final LDAPResult result = (LDAPResult) response; 560 if ((result.getResultCode().equals(ResultCode.REFERRAL)) && 561 followReferrals(connection)) 562 { 563 if (depth >= connection.getConnectionOptions().getReferralHopLimit()) 564 { 565 return new LDAPResult(messageID, ResultCode.REFERRAL_LIMIT_EXCEEDED, 566 ERR_TOO_MANY_REFERRALS.get(), 567 result.getMatchedDN(), result.getReferralURLs(), 568 result.getResponseControls()); 569 } 570 571 return followReferral(result, connection, depth); 572 } 573 else 574 { 575 if (allowRetry) 576 { 577 final LDAPResult retryResult = reconnectAndRetry(connection, depth, 578 result.getResultCode()); 579 if (retryResult != null) 580 { 581 return retryResult; 582 } 583 } 584 585 return result; 586 } 587 } 588 589 590 591 /** 592 * Attempts to re-establish the connection and retry processing this request 593 * on it. 594 * 595 * @param connection The connection to be re-established. 596 * @param depth The current referral depth for this request. It should 597 * always be one for the initial request, and should only 598 * be incremented when following referrals. 599 * @param resultCode The result code for the previous operation attempt. 600 * 601 * @return The result from re-trying the add, or {@code null} if it could not 602 * be re-tried. 603 */ 604 private LDAPResult reconnectAndRetry(final LDAPConnection connection, 605 final int depth, 606 final ResultCode resultCode) 607 { 608 try 609 { 610 // We will only want to retry for certain result codes that indicate a 611 // connection problem. 612 switch (resultCode.intValue()) 613 { 614 case ResultCode.SERVER_DOWN_INT_VALUE: 615 case ResultCode.DECODING_ERROR_INT_VALUE: 616 case ResultCode.CONNECT_ERROR_INT_VALUE: 617 connection.reconnect(); 618 return processSync(connection, depth, false); 619 } 620 } 621 catch (final Exception e) 622 { 623 debugException(e); 624 } 625 626 return null; 627 } 628 629 630 631 /** 632 * Attempts to follow a referral to perform a delete operation in the target 633 * server. 634 * 635 * @param referralResult The LDAP result object containing information about 636 * the referral to follow. 637 * @param connection The connection on which the referral was received. 638 * @param depth The number of referrals followed in the course of 639 * processing this request. 640 * 641 * @return The result of attempting to process the delete operation by 642 * following the referral. 643 * 644 * @throws LDAPException If a problem occurs while attempting to establish 645 * the referral connection, sending the request, or 646 * reading the result. 647 */ 648 private LDAPResult followReferral(final LDAPResult referralResult, 649 final LDAPConnection connection, 650 final int depth) 651 throws LDAPException 652 { 653 for (final String urlString : referralResult.getReferralURLs()) 654 { 655 try 656 { 657 final LDAPURL referralURL = new LDAPURL(urlString); 658 final String host = referralURL.getHost(); 659 660 if (host == null) 661 { 662 // We can't handle a referral in which there is no host. 663 continue; 664 } 665 666 final DeleteRequest deleteRequest; 667 if (referralURL.baseDNProvided()) 668 { 669 deleteRequest = new DeleteRequest(referralURL.getBaseDN(), 670 getControls()); 671 } 672 else 673 { 674 deleteRequest = this; 675 } 676 677 final LDAPConnection referralConn = connection.getReferralConnector(). 678 getReferralConnection(referralURL, connection); 679 try 680 { 681 return deleteRequest.process(referralConn, depth+1); 682 } 683 finally 684 { 685 referralConn.setDisconnectInfo(DisconnectType.REFERRAL, null, null); 686 referralConn.close(); 687 } 688 } 689 catch (LDAPException le) 690 { 691 debugException(le); 692 } 693 } 694 695 // If we've gotten here, then we could not follow any of the referral URLs, 696 // so we'll just return the original referral result. 697 return referralResult; 698 } 699 700 701 702 /** 703 * {@inheritDoc} 704 */ 705 @InternalUseOnly() 706 public void responseReceived(final LDAPResponse response) 707 throws LDAPException 708 { 709 try 710 { 711 responseQueue.put(response); 712 } 713 catch (Exception e) 714 { 715 debugException(e); 716 throw new LDAPException(ResultCode.LOCAL_ERROR, 717 ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e); 718 } 719 } 720 721 722 723 /** 724 * {@inheritDoc} 725 */ 726 @Override() 727 public int getLastMessageID() 728 { 729 return messageID; 730 } 731 732 733 734 /** 735 * {@inheritDoc} 736 */ 737 @Override() 738 public OperationType getOperationType() 739 { 740 return OperationType.DELETE; 741 } 742 743 744 745 /** 746 * {@inheritDoc} 747 */ 748 public DeleteRequest duplicate() 749 { 750 return duplicate(getControls()); 751 } 752 753 754 755 /** 756 * {@inheritDoc} 757 */ 758 public DeleteRequest duplicate(final Control[] controls) 759 { 760 final DeleteRequest r = new DeleteRequest(dn, controls); 761 762 if (followReferralsInternal() != null) 763 { 764 r.setFollowReferrals(followReferralsInternal()); 765 } 766 767 r.setResponseTimeoutMillis(getResponseTimeoutMillis(null)); 768 769 return r; 770 } 771 772 773 774 /** 775 * {@inheritDoc} 776 */ 777 public LDIFDeleteChangeRecord toLDIFChangeRecord() 778 { 779 return new LDIFDeleteChangeRecord(this); 780 } 781 782 783 784 /** 785 * {@inheritDoc} 786 */ 787 public String[] toLDIF() 788 { 789 return toLDIFChangeRecord().toLDIF(); 790 } 791 792 793 794 /** 795 * {@inheritDoc} 796 */ 797 public String toLDIFString() 798 { 799 return toLDIFChangeRecord().toLDIFString(); 800 } 801 802 803 804 /** 805 * {@inheritDoc} 806 */ 807 @Override() 808 public void toString(final StringBuilder buffer) 809 { 810 buffer.append("DeleteRequest(dn='"); 811 buffer.append(dn); 812 buffer.append('\''); 813 814 final Control[] controls = getControls(); 815 if (controls.length > 0) 816 { 817 buffer.append(", controls={"); 818 for (int i=0; i < controls.length; i++) 819 { 820 if (i > 0) 821 { 822 buffer.append(", "); 823 } 824 825 buffer.append(controls[i]); 826 } 827 buffer.append('}'); 828 } 829 830 buffer.append(')'); 831 } 832 }