001 /*
002 * Copyright 2007-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2017 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 Thread.currentThread().interrupt();
295 throw new LDAPException(ResultCode.LOCAL_ERROR,
296 ERR_DELETE_INTERRUPTED.get(connection.getHostPort()), ie);
297 }
298
299 return handleResponse(connection, response, requestTime, depth, false);
300 }
301 finally
302 {
303 connection.deregisterResponseAcceptor(messageID);
304 }
305 }
306
307
308
309 /**
310 * Sends this delete request to the directory server over the provided
311 * connection and returns the message ID for the request.
312 *
313 * @param connection The connection to use to communicate with the
314 * directory server.
315 * @param resultListener The async result listener that is to be notified
316 * when the response is received. It may be
317 * {@code null} only if the result is to be processed
318 * by this class.
319 *
320 * @return The async request ID created for the operation, or {@code null} if
321 * the provided {@code resultListener} is {@code null} and the
322 * operation will not actually be processed asynchronously.
323 *
324 * @throws LDAPException If a problem occurs while sending the request.
325 */
326 AsyncRequestID processAsync(final LDAPConnection connection,
327 final AsyncResultListener resultListener)
328 throws LDAPException
329 {
330 // Create the LDAP message.
331 messageID = connection.nextMessageID();
332 final LDAPMessage message = new LDAPMessage(messageID, this, getControls());
333
334
335 // If the provided async result listener is {@code null}, then we'll use
336 // this class as the message acceptor. Otherwise, create an async helper
337 // and use it as the message acceptor.
338 final AsyncRequestID asyncRequestID;
339 if (resultListener == null)
340 {
341 asyncRequestID = null;
342 connection.registerResponseAcceptor(messageID, this);
343 }
344 else
345 {
346 final AsyncHelper helper = new AsyncHelper(connection,
347 OperationType.DELETE, messageID, resultListener,
348 getIntermediateResponseListener());
349 connection.registerResponseAcceptor(messageID, helper);
350 asyncRequestID = helper.getAsyncRequestID();
351
352 final long timeout = getResponseTimeoutMillis(connection);
353 if (timeout > 0L)
354 {
355 final Timer timer = connection.getTimer();
356 final AsyncTimeoutTimerTask timerTask =
357 new AsyncTimeoutTimerTask(helper);
358 timer.schedule(timerTask, timeout);
359 asyncRequestID.setTimerTask(timerTask);
360 }
361 }
362
363
364 // Send the request to the server.
365 try
366 {
367 debugLDAPRequest(this);
368 connection.getConnectionStatistics().incrementNumDeleteRequests();
369 connection.sendMessage(message);
370 return asyncRequestID;
371 }
372 catch (LDAPException le)
373 {
374 debugException(le);
375
376 connection.deregisterResponseAcceptor(messageID);
377 throw le;
378 }
379 }
380
381
382
383 /**
384 * Processes this delete operation in synchronous mode, in which the same
385 * thread will send the request and read the response.
386 *
387 * @param connection The connection to use to communicate with the directory
388 * server.
389 * @param depth The current referral depth for this request. It should
390 * always be one for the initial request, and should only
391 * be incremented when following referrals.
392 * @param allowRetry Indicates whether the request may be re-tried on a
393 * re-established connection if the initial attempt fails
394 * in a way that indicates the connection is no longer
395 * valid and autoReconnect is true.
396 *
397 * @return An LDAP result object that provides information about the result
398 * of the delete processing.
399 *
400 * @throws LDAPException If a problem occurs while sending the request or
401 * reading the response.
402 */
403 private LDAPResult processSync(final LDAPConnection connection,
404 final int depth, final boolean allowRetry)
405 throws LDAPException
406 {
407 // Create the LDAP message.
408 messageID = connection.nextMessageID();
409 final LDAPMessage message =
410 new LDAPMessage(messageID, this, getControls());
411
412
413 // Set the appropriate timeout on the socket.
414 try
415 {
416 connection.getConnectionInternals(true).getSocket().setSoTimeout(
417 (int) getResponseTimeoutMillis(connection));
418 }
419 catch (Exception e)
420 {
421 debugException(e);
422 }
423
424
425 // Send the request to the server.
426 final long requestTime = System.nanoTime();
427 debugLDAPRequest(this);
428 connection.getConnectionStatistics().incrementNumDeleteRequests();
429 try
430 {
431 connection.sendMessage(message);
432 }
433 catch (final LDAPException le)
434 {
435 debugException(le);
436
437 if (allowRetry)
438 {
439 final LDAPResult retryResult = reconnectAndRetry(connection, depth,
440 le.getResultCode());
441 if (retryResult != null)
442 {
443 return retryResult;
444 }
445 }
446
447 throw le;
448 }
449
450 while (true)
451 {
452 final LDAPResponse response;
453 try
454 {
455 response = connection.readResponse(messageID);
456 }
457 catch (final LDAPException le)
458 {
459 debugException(le);
460
461 if ((le.getResultCode() == ResultCode.TIMEOUT) &&
462 connection.getConnectionOptions().abandonOnTimeout())
463 {
464 connection.abandon(messageID);
465 }
466
467 if (allowRetry)
468 {
469 final LDAPResult retryResult = reconnectAndRetry(connection, depth,
470 le.getResultCode());
471 if (retryResult != null)
472 {
473 return retryResult;
474 }
475 }
476
477 throw le;
478 }
479
480 if (response instanceof IntermediateResponse)
481 {
482 final IntermediateResponseListener listener =
483 getIntermediateResponseListener();
484 if (listener != null)
485 {
486 listener.intermediateResponseReturned(
487 (IntermediateResponse) response);
488 }
489 }
490 else
491 {
492 return handleResponse(connection, response, requestTime, depth,
493 allowRetry);
494 }
495 }
496 }
497
498
499
500 /**
501 * Performs the necessary processing for handling a response.
502 *
503 * @param connection The connection used to read the response.
504 * @param response The response to be processed.
505 * @param requestTime The time the request was sent to the server.
506 * @param depth The current referral depth for this request. It
507 * should always be one for the initial request, and
508 * should only be incremented when following referrals.
509 * @param allowRetry Indicates whether the request may be re-tried on a
510 * re-established connection if the initial attempt fails
511 * in a way that indicates the connection is no longer
512 * valid and autoReconnect is true.
513 *
514 * @return The delete result.
515 *
516 * @throws LDAPException If a problem occurs.
517 */
518 private LDAPResult handleResponse(final LDAPConnection connection,
519 final LDAPResponse response,
520 final long requestTime, final int depth,
521 final boolean allowRetry)
522 throws LDAPException
523 {
524 if (response == null)
525 {
526 final long waitTime = nanosToMillis(System.nanoTime() - requestTime);
527 if (connection.getConnectionOptions().abandonOnTimeout())
528 {
529 connection.abandon(messageID);
530 }
531
532 throw new LDAPException(ResultCode.TIMEOUT,
533 ERR_DELETE_CLIENT_TIMEOUT.get(waitTime, messageID, dn,
534 connection.getHostPort()));
535 }
536
537 connection.getConnectionStatistics().incrementNumDeleteResponses(
538 System.nanoTime() - requestTime);
539 if (response instanceof ConnectionClosedResponse)
540 {
541 // The connection was closed while waiting for the response.
542 if (allowRetry)
543 {
544 final LDAPResult retryResult = reconnectAndRetry(connection, depth,
545 ResultCode.SERVER_DOWN);
546 if (retryResult != null)
547 {
548 return retryResult;
549 }
550 }
551
552 final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response;
553 final String message = ccr.getMessage();
554 if (message == null)
555 {
556 throw new LDAPException(ccr.getResultCode(),
557 ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE.get(
558 connection.getHostPort(), toString()));
559 }
560 else
561 {
562 throw new LDAPException(ccr.getResultCode(),
563 ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE_WITH_MESSAGE.get(
564 connection.getHostPort(), toString(), message));
565 }
566 }
567
568 final LDAPResult result = (LDAPResult) response;
569 if ((result.getResultCode().equals(ResultCode.REFERRAL)) &&
570 followReferrals(connection))
571 {
572 if (depth >= connection.getConnectionOptions().getReferralHopLimit())
573 {
574 return new LDAPResult(messageID, ResultCode.REFERRAL_LIMIT_EXCEEDED,
575 ERR_TOO_MANY_REFERRALS.get(),
576 result.getMatchedDN(), result.getReferralURLs(),
577 result.getResponseControls());
578 }
579
580 return followReferral(result, connection, depth);
581 }
582 else
583 {
584 if (allowRetry)
585 {
586 final LDAPResult retryResult = reconnectAndRetry(connection, depth,
587 result.getResultCode());
588 if (retryResult != null)
589 {
590 return retryResult;
591 }
592 }
593
594 return result;
595 }
596 }
597
598
599
600 /**
601 * Attempts to re-establish the connection and retry processing this request
602 * on it.
603 *
604 * @param connection The connection to be re-established.
605 * @param depth The current referral depth for this request. It should
606 * always be one for the initial request, and should only
607 * be incremented when following referrals.
608 * @param resultCode The result code for the previous operation attempt.
609 *
610 * @return The result from re-trying the add, or {@code null} if it could not
611 * be re-tried.
612 */
613 private LDAPResult reconnectAndRetry(final LDAPConnection connection,
614 final int depth,
615 final ResultCode resultCode)
616 {
617 try
618 {
619 // We will only want to retry for certain result codes that indicate a
620 // connection problem.
621 switch (resultCode.intValue())
622 {
623 case ResultCode.SERVER_DOWN_INT_VALUE:
624 case ResultCode.DECODING_ERROR_INT_VALUE:
625 case ResultCode.CONNECT_ERROR_INT_VALUE:
626 connection.reconnect();
627 return processSync(connection, depth, false);
628 }
629 }
630 catch (final Exception e)
631 {
632 debugException(e);
633 }
634
635 return null;
636 }
637
638
639
640 /**
641 * Attempts to follow a referral to perform a delete operation in the target
642 * server.
643 *
644 * @param referralResult The LDAP result object containing information about
645 * the referral to follow.
646 * @param connection The connection on which the referral was received.
647 * @param depth The number of referrals followed in the course of
648 * processing this request.
649 *
650 * @return The result of attempting to process the delete operation by
651 * following the referral.
652 *
653 * @throws LDAPException If a problem occurs while attempting to establish
654 * the referral connection, sending the request, or
655 * reading the result.
656 */
657 private LDAPResult followReferral(final LDAPResult referralResult,
658 final LDAPConnection connection,
659 final int depth)
660 throws LDAPException
661 {
662 for (final String urlString : referralResult.getReferralURLs())
663 {
664 try
665 {
666 final LDAPURL referralURL = new LDAPURL(urlString);
667 final String host = referralURL.getHost();
668
669 if (host == null)
670 {
671 // We can't handle a referral in which there is no host.
672 continue;
673 }
674
675 final DeleteRequest deleteRequest;
676 if (referralURL.baseDNProvided())
677 {
678 deleteRequest = new DeleteRequest(referralURL.getBaseDN(),
679 getControls());
680 }
681 else
682 {
683 deleteRequest = this;
684 }
685
686 final LDAPConnection referralConn = connection.getReferralConnector().
687 getReferralConnection(referralURL, connection);
688 try
689 {
690 return deleteRequest.process(referralConn, depth+1);
691 }
692 finally
693 {
694 referralConn.setDisconnectInfo(DisconnectType.REFERRAL, null, null);
695 referralConn.close();
696 }
697 }
698 catch (LDAPException le)
699 {
700 debugException(le);
701 }
702 }
703
704 // If we've gotten here, then we could not follow any of the referral URLs,
705 // so we'll just return the original referral result.
706 return referralResult;
707 }
708
709
710
711 /**
712 * {@inheritDoc}
713 */
714 @InternalUseOnly()
715 public void responseReceived(final LDAPResponse response)
716 throws LDAPException
717 {
718 try
719 {
720 responseQueue.put(response);
721 }
722 catch (Exception e)
723 {
724 debugException(e);
725
726 if (e instanceof InterruptedException)
727 {
728 Thread.currentThread().interrupt();
729 }
730
731 throw new LDAPException(ResultCode.LOCAL_ERROR,
732 ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e);
733 }
734 }
735
736
737
738 /**
739 * {@inheritDoc}
740 */
741 @Override()
742 public int getLastMessageID()
743 {
744 return messageID;
745 }
746
747
748
749 /**
750 * {@inheritDoc}
751 */
752 @Override()
753 public OperationType getOperationType()
754 {
755 return OperationType.DELETE;
756 }
757
758
759
760 /**
761 * {@inheritDoc}
762 */
763 public DeleteRequest duplicate()
764 {
765 return duplicate(getControls());
766 }
767
768
769
770 /**
771 * {@inheritDoc}
772 */
773 public DeleteRequest duplicate(final Control[] controls)
774 {
775 final DeleteRequest r = new DeleteRequest(dn, controls);
776
777 if (followReferralsInternal() != null)
778 {
779 r.setFollowReferrals(followReferralsInternal());
780 }
781
782 r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
783
784 return r;
785 }
786
787
788
789 /**
790 * {@inheritDoc}
791 */
792 public LDIFDeleteChangeRecord toLDIFChangeRecord()
793 {
794 return new LDIFDeleteChangeRecord(this);
795 }
796
797
798
799 /**
800 * {@inheritDoc}
801 */
802 public String[] toLDIF()
803 {
804 return toLDIFChangeRecord().toLDIF();
805 }
806
807
808
809 /**
810 * {@inheritDoc}
811 */
812 public String toLDIFString()
813 {
814 return toLDIFChangeRecord().toLDIFString();
815 }
816
817
818
819 /**
820 * {@inheritDoc}
821 */
822 @Override()
823 public void toString(final StringBuilder buffer)
824 {
825 buffer.append("DeleteRequest(dn='");
826 buffer.append(dn);
827 buffer.append('\'');
828
829 final Control[] controls = getControls();
830 if (controls.length > 0)
831 {
832 buffer.append(", controls={");
833 for (int i=0; i < controls.length; i++)
834 {
835 if (i > 0)
836 {
837 buffer.append(", ");
838 }
839
840 buffer.append(controls[i]);
841 }
842 buffer.append('}');
843 }
844
845 buffer.append(')');
846 }
847
848
849
850 /**
851 * {@inheritDoc}
852 */
853 public void toCode(final List<String> lineList, final String requestID,
854 final int indentSpaces, final boolean includeProcessing)
855 {
856 // Create the request variable.
857 ToCodeHelper.generateMethodCall(lineList, indentSpaces, "DeleteRequest",
858 requestID + "Request", "new DeleteRequest",
859 ToCodeArgHelper.createString(dn, "Entry DN"));
860
861 // If there are any controls, then add them to the request.
862 for (final Control c : getControls())
863 {
864 ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null,
865 requestID + "Request.addControl",
866 ToCodeArgHelper.createControl(c, null));
867 }
868
869
870 // Add lines for processing the request and obtaining the result.
871 if (includeProcessing)
872 {
873 // Generate a string with the appropriate indent.
874 final StringBuilder buffer = new StringBuilder();
875 for (int i=0; i < indentSpaces; i++)
876 {
877 buffer.append(' ');
878 }
879 final String indent = buffer.toString();
880
881 lineList.add("");
882 lineList.add(indent + "try");
883 lineList.add(indent + '{');
884 lineList.add(indent + " LDAPResult " + requestID +
885 "Result = connection.delete(" + requestID + "Request);");
886 lineList.add(indent + " // The delete was processed successfully.");
887 lineList.add(indent + '}');
888 lineList.add(indent + "catch (LDAPException e)");
889 lineList.add(indent + '{');
890 lineList.add(indent + " // The delete failed. Maybe the following " +
891 "will help explain why.");
892 lineList.add(indent + " ResultCode resultCode = e.getResultCode();");
893 lineList.add(indent + " String message = e.getMessage();");
894 lineList.add(indent + " String matchedDN = e.getMatchedDN();");
895 lineList.add(indent + " String[] referralURLs = e.getReferralURLs();");
896 lineList.add(indent + " Control[] responseControls = " +
897 "e.getResponseControls();");
898 lineList.add(indent + '}');
899 }
900 }
901 }