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