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 */
021package com.unboundid.ldap.sdk;
022
023
024
025import java.util.Timer;
026import java.util.concurrent.LinkedBlockingQueue;
027import java.util.concurrent.TimeUnit;
028
029import com.unboundid.asn1.ASN1Buffer;
030import com.unboundid.asn1.ASN1Element;
031import com.unboundid.asn1.ASN1OctetString;
032import com.unboundid.ldap.protocol.LDAPMessage;
033import com.unboundid.ldap.protocol.LDAPResponse;
034import com.unboundid.ldap.protocol.ProtocolOp;
035import com.unboundid.ldif.LDIFDeleteChangeRecord;
036import com.unboundid.util.InternalUseOnly;
037import com.unboundid.util.Mutable;
038import com.unboundid.util.ThreadSafety;
039import com.unboundid.util.ThreadSafetyLevel;
040
041import static com.unboundid.ldap.sdk.LDAPMessages.*;
042import static com.unboundid.util.Debug.*;
043import static com.unboundid.util.StaticUtils.*;
044import 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)
082public 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}