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