001 /*
002 * Copyright 2007-2013 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2013 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.Timer;
026 import java.util.concurrent.LinkedBlockingQueue;
027 import java.util.concurrent.TimeUnit;
028
029 import com.unboundid.asn1.ASN1Buffer;
030 import com.unboundid.asn1.ASN1Element;
031 import com.unboundid.asn1.ASN1OctetString;
032 import com.unboundid.ldap.protocol.LDAPMessage;
033 import com.unboundid.ldap.protocol.LDAPResponse;
034 import com.unboundid.ldap.protocol.ProtocolOp;
035 import com.unboundid.ldif.LDIFDeleteChangeRecord;
036 import com.unboundid.util.InternalUseOnly;
037 import com.unboundid.util.Mutable;
038 import com.unboundid.util.ThreadSafety;
039 import com.unboundid.util.ThreadSafetyLevel;
040
041 import static com.unboundid.ldap.sdk.LDAPMessages.*;
042 import static com.unboundid.util.Debug.*;
043 import static com.unboundid.util.StaticUtils.*;
044 import static com.unboundid.util.Validator.*;
045
046
047
048 /**
049 * This class implements the processing necessary to perform an LDAPv3 delete
050 * operation, which removes an entry from the directory. A delete request
051 * contains the DN of the entry to remove. It may also include a set of
052 * controls to send to the server.
053 * {@code DeleteRequest} objects are mutable and therefore can be altered and
054 * re-used for multiple requests. Note, however, that {@code DeleteRequest}
055 * objects are not threadsafe and therefore a single {@code DeleteRequest}
056 * object instance should not be used to process multiple requests at the same
057 * time.
058 * <BR><BR>
059 * <H2>Example</H2>
060 * The following example demonstrates the process for performing a delete
061 * operation:
062 * <PRE>
063 * DeleteRequest deleteRequest =
064 * new DeleteRequest("cn=entry to delete,dc=example,dc=com");
065 *
066 * try
067 * {
068 * LDAPResult deleteResult = connection.delete(deleteRequest);
069 *
070 * System.out.println("The entry was successfully deleted.");
071 * }
072 * catch (LDAPException le)
073 * {
074 * System.err.println("The delete operation failed.");
075 * }
076 * </PRE>
077 */
078 @Mutable()
079 @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
080 public final class DeleteRequest
081 extends UpdatableLDAPRequest
082 implements ReadOnlyDeleteRequest, ResponseAcceptor, ProtocolOp
083 {
084 /**
085 * The serial version UID for this serializable class.
086 */
087 private static final long serialVersionUID = -6126029442850884239L;
088
089
090
091 // The message ID from the last LDAP message sent from this request.
092 private int messageID = -1;
093
094 // The queue that will be used to receive response messages from the server.
095 private final LinkedBlockingQueue<LDAPResponse> responseQueue =
096 new LinkedBlockingQueue<LDAPResponse>();
097
098 // The DN of the entry to delete.
099 private String dn;
100
101
102
103 /**
104 * Creates a new delete request with the provided DN.
105 *
106 * @param dn The DN of the entry to delete. It must not be {@code null}.
107 */
108 public DeleteRequest(final String dn)
109 {
110 super(null);
111
112 ensureNotNull(dn);
113
114 this.dn = dn;
115 }
116
117
118
119 /**
120 * Creates a new delete request with the provided DN.
121 *
122 * @param dn The DN of the entry to delete. It must not be
123 * {@code null}.
124 * @param controls The set of controls to include in the request.
125 */
126 public DeleteRequest(final String dn, final Control[] controls)
127 {
128 super(controls);
129
130 ensureNotNull(dn);
131
132 this.dn = dn;
133 }
134
135
136
137 /**
138 * Creates a new delete request with the provided DN.
139 *
140 * @param dn The DN of the entry to delete. It must not be {@code null}.
141 */
142 public DeleteRequest(final DN dn)
143 {
144 super(null);
145
146 ensureNotNull(dn);
147
148 this.dn = dn.toString();
149 }
150
151
152
153 /**
154 * Creates a new delete request with the provided DN.
155 *
156 * @param dn The DN of the entry to delete. It must not be
157 * {@code null}.
158 * @param controls The set of controls to include in the request.
159 */
160 public DeleteRequest(final DN dn, final Control[] controls)
161 {
162 super(controls);
163
164 ensureNotNull(dn);
165
166 this.dn = dn.toString();
167 }
168
169
170
171 /**
172 * {@inheritDoc}
173 */
174 public String getDN()
175 {
176 return dn;
177 }
178
179
180
181 /**
182 * Specifies the DN of the entry to delete.
183 *
184 * @param dn The DN of the entry to delete. It must not be {@code null}.
185 */
186 public void setDN(final String dn)
187 {
188 ensureNotNull(dn);
189
190 this.dn = dn;
191 }
192
193
194
195 /**
196 * Specifies the DN of the entry to delete.
197 *
198 * @param dn The DN of the entry to delete. It must not be {@code null}.
199 */
200 public void setDN(final DN dn)
201 {
202 ensureNotNull(dn);
203
204 this.dn = dn.toString();
205 }
206
207
208
209 /**
210 * {@inheritDoc}
211 */
212 public byte getProtocolOpType()
213 {
214 return LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST;
215 }
216
217
218
219 /**
220 * {@inheritDoc}
221 */
222 public void writeTo(final ASN1Buffer buffer)
223 {
224 buffer.addOctetString(LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST, dn);
225 }
226
227
228
229 /**
230 * Encodes the delete request protocol op to an ASN.1 element.
231 *
232 * @return The ASN.1 element with the encoded delete request protocol op.
233 */
234 public ASN1Element encodeProtocolOp()
235 {
236 return new ASN1OctetString(LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST, dn);
237 }
238
239
240
241 /**
242 * Sends this delete request to the directory server over the provided
243 * connection and returns the associated response.
244 *
245 * @param connection The connection to use to communicate with the directory
246 * server.
247 * @param depth The current referral depth for this request. It should
248 * always be one for the initial request, and should only
249 * be incremented when following referrals.
250 *
251 * @return An LDAP result object that provides information about the result
252 * of the delete processing.
253 *
254 * @throws LDAPException If a problem occurs while sending the request or
255 * reading the response.
256 */
257 @Override()
258 protected LDAPResult process(final LDAPConnection connection, final int depth)
259 throws LDAPException
260 {
261 if (connection.synchronousMode())
262 {
263 return processSync(connection, depth,
264 connection.getConnectionOptions().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, connection.getHostPort()));
528 }
529
530 connection.getConnectionStatistics().incrementNumDeleteResponses(
531 System.nanoTime() - requestTime);
532 if (response instanceof ConnectionClosedResponse)
533 {
534 // The connection was closed while waiting for the response.
535 if (allowRetry)
536 {
537 final LDAPResult retryResult = reconnectAndRetry(connection, depth,
538 ResultCode.SERVER_DOWN);
539 if (retryResult != null)
540 {
541 return retryResult;
542 }
543 }
544
545 final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response;
546 final String message = ccr.getMessage();
547 if (message == null)
548 {
549 throw new LDAPException(ccr.getResultCode(),
550 ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE.get(
551 connection.getHostPort(), toString()));
552 }
553 else
554 {
555 throw new LDAPException(ccr.getResultCode(),
556 ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE_WITH_MESSAGE.get(
557 connection.getHostPort(), toString(), message));
558 }
559 }
560
561 final LDAPResult result = (LDAPResult) response;
562 if ((result.getResultCode().equals(ResultCode.REFERRAL)) &&
563 followReferrals(connection))
564 {
565 if (depth >= connection.getConnectionOptions().getReferralHopLimit())
566 {
567 return new LDAPResult(messageID, ResultCode.REFERRAL_LIMIT_EXCEEDED,
568 ERR_TOO_MANY_REFERRALS.get(),
569 result.getMatchedDN(), result.getReferralURLs(),
570 result.getResponseControls());
571 }
572
573 return followReferral(result, connection, depth);
574 }
575 else
576 {
577 if (allowRetry)
578 {
579 final LDAPResult retryResult = reconnectAndRetry(connection, depth,
580 result.getResultCode());
581 if (retryResult != null)
582 {
583 return retryResult;
584 }
585 }
586
587 return result;
588 }
589 }
590
591
592
593 /**
594 * Attempts to re-establish the connection and retry processing this request
595 * on it.
596 *
597 * @param connection The connection to be re-established.
598 * @param depth The current referral depth for this request. It should
599 * always be one for the initial request, and should only
600 * be incremented when following referrals.
601 * @param resultCode The result code for the previous operation attempt.
602 *
603 * @return The result from re-trying the add, or {@code null} if it could not
604 * be re-tried.
605 */
606 private LDAPResult reconnectAndRetry(final LDAPConnection connection,
607 final int depth,
608 final ResultCode resultCode)
609 {
610 try
611 {
612 // We will only want to retry for certain result codes that indicate a
613 // connection problem.
614 switch (resultCode.intValue())
615 {
616 case ResultCode.SERVER_DOWN_INT_VALUE:
617 case ResultCode.DECODING_ERROR_INT_VALUE:
618 case ResultCode.CONNECT_ERROR_INT_VALUE:
619 connection.reconnect();
620 return processSync(connection, depth, false);
621 }
622 }
623 catch (final Exception e)
624 {
625 debugException(e);
626 }
627
628 return null;
629 }
630
631
632
633 /**
634 * Attempts to follow a referral to perform a delete operation in the target
635 * server.
636 *
637 * @param referralResult The LDAP result object containing information about
638 * the referral to follow.
639 * @param connection The connection on which the referral was received.
640 * @param depth The number of referrals followed in the course of
641 * processing this request.
642 *
643 * @return The result of attempting to process the delete operation by
644 * following the referral.
645 *
646 * @throws LDAPException If a problem occurs while attempting to establish
647 * the referral connection, sending the request, or
648 * reading the result.
649 */
650 private LDAPResult followReferral(final LDAPResult referralResult,
651 final LDAPConnection connection,
652 final int depth)
653 throws LDAPException
654 {
655 for (final String urlString : referralResult.getReferralURLs())
656 {
657 try
658 {
659 final LDAPURL referralURL = new LDAPURL(urlString);
660 final String host = referralURL.getHost();
661
662 if (host == null)
663 {
664 // We can't handle a referral in which there is no host.
665 continue;
666 }
667
668 final DeleteRequest deleteRequest;
669 if (referralURL.baseDNProvided())
670 {
671 deleteRequest = new DeleteRequest(referralURL.getBaseDN(),
672 getControls());
673 }
674 else
675 {
676 deleteRequest = this;
677 }
678
679 final LDAPConnection referralConn = connection.getReferralConnector().
680 getReferralConnection(referralURL, connection);
681 try
682 {
683 return deleteRequest.process(referralConn, depth+1);
684 }
685 finally
686 {
687 referralConn.setDisconnectInfo(DisconnectType.REFERRAL, null, null);
688 referralConn.close();
689 }
690 }
691 catch (LDAPException le)
692 {
693 debugException(le);
694 }
695 }
696
697 // If we've gotten here, then we could not follow any of the referral URLs,
698 // so we'll just return the original referral result.
699 return referralResult;
700 }
701
702
703
704 /**
705 * {@inheritDoc}
706 */
707 @InternalUseOnly()
708 public void responseReceived(final LDAPResponse response)
709 throws LDAPException
710 {
711 try
712 {
713 responseQueue.put(response);
714 }
715 catch (Exception e)
716 {
717 debugException(e);
718 throw new LDAPException(ResultCode.LOCAL_ERROR,
719 ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e);
720 }
721 }
722
723
724
725 /**
726 * {@inheritDoc}
727 */
728 @Override()
729 public int getLastMessageID()
730 {
731 return messageID;
732 }
733
734
735
736 /**
737 * {@inheritDoc}
738 */
739 @Override()
740 public OperationType getOperationType()
741 {
742 return OperationType.DELETE;
743 }
744
745
746
747 /**
748 * {@inheritDoc}
749 */
750 public DeleteRequest duplicate()
751 {
752 return duplicate(getControls());
753 }
754
755
756
757 /**
758 * {@inheritDoc}
759 */
760 public DeleteRequest duplicate(final Control[] controls)
761 {
762 final DeleteRequest r = new DeleteRequest(dn, controls);
763
764 if (followReferralsInternal() != null)
765 {
766 r.setFollowReferrals(followReferralsInternal());
767 }
768
769 r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
770
771 return r;
772 }
773
774
775
776 /**
777 * {@inheritDoc}
778 */
779 public LDIFDeleteChangeRecord toLDIFChangeRecord()
780 {
781 return new LDIFDeleteChangeRecord(this);
782 }
783
784
785
786 /**
787 * {@inheritDoc}
788 */
789 public String[] toLDIF()
790 {
791 return toLDIFChangeRecord().toLDIF();
792 }
793
794
795
796 /**
797 * {@inheritDoc}
798 */
799 public String toLDIFString()
800 {
801 return toLDIFChangeRecord().toLDIFString();
802 }
803
804
805
806 /**
807 * {@inheritDoc}
808 */
809 @Override()
810 public void toString(final StringBuilder buffer)
811 {
812 buffer.append("DeleteRequest(dn='");
813 buffer.append(dn);
814 buffer.append('\'');
815
816 final Control[] controls = getControls();
817 if (controls.length > 0)
818 {
819 buffer.append(", controls={");
820 for (int i=0; i < controls.length; i++)
821 {
822 if (i > 0)
823 {
824 buffer.append(", ");
825 }
826
827 buffer.append(controls[i]);
828 }
829 buffer.append('}');
830 }
831
832 buffer.append(')');
833 }
834 }