001    /*
002     * Copyright 2007-2015 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2015 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    }