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 */
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 }