001    /*
002     * Copyright 2007-2012 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2012 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      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        }
265    
266        final long requestTime = System.nanoTime();
267        processAsync(connection, null);
268    
269        try
270        {
271          // Wait for and process the response.
272          final LDAPResponse response;
273          try
274          {
275            final long responseTimeout = getResponseTimeoutMillis(connection);
276            if (responseTimeout > 0)
277            {
278              response = responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS);
279            }
280            else
281            {
282              response = responseQueue.take();
283            }
284          }
285          catch (InterruptedException ie)
286          {
287            debugException(ie);
288            throw new LDAPException(ResultCode.LOCAL_ERROR,
289                 ERR_DELETE_INTERRUPTED.get(connection.getHostPort()), ie);
290          }
291    
292          return handleResponse(connection, response,  requestTime, depth);
293        }
294        finally
295        {
296          connection.deregisterResponseAcceptor(messageID);
297        }
298      }
299    
300    
301    
302      /**
303       * Sends this delete request to the directory server over the provided
304       * connection and returns the message ID for the request.
305       *
306       * @param  connection      The connection to use to communicate with the
307       *                         directory server.
308       * @param  resultListener  The async result listener that is to be notified
309       *                         when the response is received.  It may be
310       *                         {@code null} only if the result is to be processed
311       *                         by this class.
312       *
313       * @return  The async request ID created for the operation, or {@code null} if
314       *          the provided {@code resultListener} is {@code null} and the
315       *          operation will not actually be processed asynchronously.
316       *
317       * @throws  LDAPException  If a problem occurs while sending the request.
318       */
319      AsyncRequestID processAsync(final LDAPConnection connection,
320                                  final AsyncResultListener resultListener)
321                     throws LDAPException
322      {
323        // Create the LDAP message.
324        messageID = connection.nextMessageID();
325        final LDAPMessage message = new LDAPMessage(messageID, this, getControls());
326    
327    
328        // If the provided async result listener is {@code null}, then we'll use
329        // this class as the message acceptor.  Otherwise, create an async helper
330        // and use it as the message acceptor.
331        final AsyncRequestID asyncRequestID;
332        if (resultListener == null)
333        {
334          asyncRequestID = null;
335          connection.registerResponseAcceptor(messageID, this);
336        }
337        else
338        {
339          final AsyncHelper helper = new AsyncHelper(connection,
340               OperationType.DELETE, messageID, resultListener,
341               getIntermediateResponseListener());
342          connection.registerResponseAcceptor(messageID, helper);
343          asyncRequestID = helper.getAsyncRequestID();
344    
345          final long timeout = getResponseTimeoutMillis(connection);
346          if (timeout > 0L)
347          {
348            final Timer timer = connection.getTimer();
349            final AsyncTimeoutTimerTask timerTask =
350                 new AsyncTimeoutTimerTask(helper);
351            timer.schedule(timerTask, timeout);
352            asyncRequestID.setTimerTask(timerTask);
353          }
354        }
355    
356    
357        // Send the request to the server.
358        try
359        {
360          debugLDAPRequest(this);
361          connection.getConnectionStatistics().incrementNumDeleteRequests();
362          connection.sendMessage(message);
363          return asyncRequestID;
364        }
365        catch (LDAPException le)
366        {
367          debugException(le);
368    
369          connection.deregisterResponseAcceptor(messageID);
370          throw le;
371        }
372      }
373    
374    
375    
376      /**
377       * Processes this delete operation in synchronous mode, in which the same
378       * thread will send the request and read the response.
379       *
380       * @param  connection  The connection to use to communicate with the directory
381       *                     server.
382       * @param  depth       The current referral depth for this request.  It should
383       *                     always be one for the initial request, and should only
384       *                     be incremented when following referrals.
385       *
386       * @return  An LDAP result object that provides information about the result
387       *          of the delete processing.
388       *
389       * @throws  LDAPException  If a problem occurs while sending the request or
390       *                         reading the response.
391       */
392      private LDAPResult processSync(final LDAPConnection connection,
393                                     final int depth)
394              throws LDAPException
395      {
396        // Create the LDAP message.
397        messageID = connection.nextMessageID();
398        final LDAPMessage message =
399             new LDAPMessage(messageID,  this, getControls());
400    
401    
402        // Set the appropriate timeout on the socket.
403        try
404        {
405          connection.getConnectionInternals().getSocket().setSoTimeout(
406               (int) getResponseTimeoutMillis(connection));
407        }
408        catch (Exception e)
409        {
410          debugException(e);
411        }
412    
413    
414        // Send the request to the server.
415        final long requestTime = System.nanoTime();
416        debugLDAPRequest(this);
417        connection.getConnectionStatistics().incrementNumDeleteRequests();
418        connection.sendMessage(message);
419    
420        while (true)
421        {
422          final LDAPResponse response;
423          try
424          {
425            response = connection.readResponse(messageID);
426          }
427          catch (final LDAPException le)
428          {
429            debugException(le);
430    
431            if ((le.getResultCode() == ResultCode.TIMEOUT) &&
432                connection.getConnectionOptions().abandonOnTimeout())
433            {
434              connection.abandon(messageID);
435            }
436    
437            throw le;
438          }
439    
440          if (response instanceof IntermediateResponse)
441          {
442            final IntermediateResponseListener listener =
443                 getIntermediateResponseListener();
444            if (listener != null)
445            {
446              listener.intermediateResponseReturned(
447                   (IntermediateResponse) response);
448            }
449          }
450          else
451          {
452            return handleResponse(connection, response, requestTime, depth);
453          }
454        }
455      }
456    
457    
458    
459      /**
460       * Performs the necessary processing for handling a response.
461       *
462       * @param  connection   The connection used to read the response.
463       * @param  response     The response to be processed.
464       * @param  requestTime  The time the request was sent to the server.
465       * @param  depth        The current referral depth for this request.  It
466       *                      should always be one for the initial request, and
467       *                      should only be incremented when following referrals.
468       *
469       * @return  The delete result.
470       *
471       * @throws  LDAPException  If a problem occurs.
472       */
473      private LDAPResult handleResponse(final LDAPConnection connection,
474                                        final LDAPResponse response,
475                                        final long requestTime, final int depth)
476              throws LDAPException
477      {
478        if (response == null)
479        {
480          final long waitTime = nanosToMillis(System.nanoTime() - requestTime);
481          if (connection.getConnectionOptions().abandonOnTimeout())
482          {
483            connection.abandon(messageID);
484          }
485    
486          throw new LDAPException(ResultCode.TIMEOUT,
487               ERR_DELETE_CLIENT_TIMEOUT.get(waitTime, connection.getHostPort()));
488        }
489    
490        connection.getConnectionStatistics().incrementNumDeleteResponses(
491             System.nanoTime() - requestTime);
492        if (response instanceof ConnectionClosedResponse)
493        {
494          // The connection was closed while waiting for the response.
495          final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response;
496          final String message = ccr.getMessage();
497          if (message == null)
498          {
499            throw new LDAPException(ccr.getResultCode(),
500                 ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE.get(
501                      connection.getHostPort(), toString()));
502          }
503          else
504          {
505            throw new LDAPException(ccr.getResultCode(),
506                 ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE_WITH_MESSAGE.get(
507                      connection.getHostPort(), toString(), message));
508          }
509        }
510    
511        final LDAPResult result = (LDAPResult) response;
512        if ((result.getResultCode().equals(ResultCode.REFERRAL)) &&
513            followReferrals(connection))
514        {
515          if (depth >= connection.getConnectionOptions().getReferralHopLimit())
516          {
517            return new LDAPResult(messageID, ResultCode.REFERRAL_LIMIT_EXCEEDED,
518                                  ERR_TOO_MANY_REFERRALS.get(),
519                                  result.getMatchedDN(), result.getReferralURLs(),
520                                  result.getResponseControls());
521          }
522    
523          return followReferral(result, connection, depth);
524        }
525        else
526        {
527          return result;
528        }
529      }
530    
531    
532    
533      /**
534       * Attempts to follow a referral to perform a delete operation in the target
535       * server.
536       *
537       * @param  referralResult  The LDAP result object containing information about
538       *                         the referral to follow.
539       * @param  connection      The connection on which the referral was received.
540       * @param  depth           The number of referrals followed in the course of
541       *                         processing this request.
542       *
543       * @return  The result of attempting to process the delete operation by
544       *          following the referral.
545       *
546       * @throws  LDAPException  If a problem occurs while attempting to establish
547       *                         the referral connection, sending the request, or
548       *                         reading the result.
549       */
550      private LDAPResult followReferral(final LDAPResult referralResult,
551                                        final LDAPConnection connection,
552                                        final int depth)
553              throws LDAPException
554      {
555        for (final String urlString : referralResult.getReferralURLs())
556        {
557          try
558          {
559            final LDAPURL referralURL = new LDAPURL(urlString);
560            final String host = referralURL.getHost();
561    
562            if (host == null)
563            {
564              // We can't handle a referral in which there is no host.
565              continue;
566            }
567    
568            final DeleteRequest deleteRequest;
569            if (referralURL.baseDNProvided())
570            {
571              deleteRequest = new DeleteRequest(referralURL.getBaseDN(),
572                                                getControls());
573            }
574            else
575            {
576              deleteRequest = this;
577            }
578    
579            final LDAPConnection referralConn = connection.getReferralConnector().
580                 getReferralConnection(referralURL, connection);
581            try
582            {
583              return deleteRequest.process(referralConn, depth+1);
584            }
585            finally
586            {
587              referralConn.setDisconnectInfo(DisconnectType.REFERRAL, null, null);
588              referralConn.close();
589            }
590          }
591          catch (LDAPException le)
592          {
593            debugException(le);
594          }
595        }
596    
597        // If we've gotten here, then we could not follow any of the referral URLs,
598        // so we'll just return the original referral result.
599        return referralResult;
600      }
601    
602    
603    
604      /**
605       * {@inheritDoc}
606       */
607      @InternalUseOnly()
608      public void responseReceived(final LDAPResponse response)
609             throws LDAPException
610      {
611        try
612        {
613          responseQueue.put(response);
614        }
615        catch (Exception e)
616        {
617          debugException(e);
618          throw new LDAPException(ResultCode.LOCAL_ERROR,
619               ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e);
620        }
621      }
622    
623    
624    
625      /**
626       * {@inheritDoc}
627       */
628      @Override()
629      public int getLastMessageID()
630      {
631        return messageID;
632      }
633    
634    
635    
636      /**
637       * {@inheritDoc}
638       */
639      @Override()
640      public OperationType getOperationType()
641      {
642        return OperationType.DELETE;
643      }
644    
645    
646    
647      /**
648       * {@inheritDoc}
649       */
650      public DeleteRequest duplicate()
651      {
652        return duplicate(getControls());
653      }
654    
655    
656    
657      /**
658       * {@inheritDoc}
659       */
660      public DeleteRequest duplicate(final Control[] controls)
661      {
662        final DeleteRequest r = new DeleteRequest(dn, controls);
663    
664        if (followReferralsInternal() != null)
665        {
666          r.setFollowReferrals(followReferralsInternal());
667        }
668    
669        r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
670    
671        return r;
672      }
673    
674    
675    
676      /**
677       * {@inheritDoc}
678       */
679      public LDIFDeleteChangeRecord toLDIFChangeRecord()
680      {
681        return new LDIFDeleteChangeRecord(this);
682      }
683    
684    
685    
686      /**
687       * {@inheritDoc}
688       */
689      public String[] toLDIF()
690      {
691        return toLDIFChangeRecord().toLDIF();
692      }
693    
694    
695    
696      /**
697       * {@inheritDoc}
698       */
699      public String toLDIFString()
700      {
701        return toLDIFChangeRecord().toLDIFString();
702      }
703    
704    
705    
706      /**
707       * {@inheritDoc}
708       */
709      @Override()
710      public void toString(final StringBuilder buffer)
711      {
712        buffer.append("DeleteRequest(dn='");
713        buffer.append(dn);
714        buffer.append('\'');
715    
716        final Control[] controls = getControls();
717        if (controls.length > 0)
718        {
719          buffer.append(", controls={");
720          for (int i=0; i < controls.length; i++)
721          {
722            if (i > 0)
723            {
724              buffer.append(", ");
725            }
726    
727            buffer.append(controls[i]);
728          }
729          buffer.append('}');
730        }
731    
732        buffer.append(')');
733      }
734    }