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 import com.unboundid.util.Mutable;
039 import com.unboundid.util.ThreadSafety;
040 import com.unboundid.util.ThreadSafetyLevel;
041
042 import static com.unboundid.ldap.sdk.LDAPMessages.*;
043 import static com.unboundid.util.Debug.*;
044 import static com.unboundid.util.StaticUtils.*;
045 import 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)
083 public 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 }