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