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