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