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