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    }