001/*
002 * Copyright 2007-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2019 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk;
022
023
024
025import java.util.List;
026import java.util.Timer;
027import java.util.concurrent.LinkedBlockingQueue;
028import java.util.concurrent.TimeUnit;
029import java.util.logging.Level;
030
031import com.unboundid.asn1.ASN1Buffer;
032import com.unboundid.asn1.ASN1Element;
033import com.unboundid.asn1.ASN1OctetString;
034import com.unboundid.ldap.protocol.LDAPMessage;
035import com.unboundid.ldap.protocol.LDAPResponse;
036import com.unboundid.ldap.protocol.ProtocolOp;
037import com.unboundid.ldif.LDIFDeleteChangeRecord;
038import com.unboundid.util.Debug;
039import com.unboundid.util.InternalUseOnly;
040import com.unboundid.util.Mutable;
041import com.unboundid.util.StaticUtils;
042import com.unboundid.util.ThreadSafety;
043import com.unboundid.util.ThreadSafetyLevel;
044import com.unboundid.util.Validator;
045
046import static com.unboundid.ldap.sdk.LDAPMessages.*;
047
048
049
050/**
051 * This class implements the processing necessary to perform an LDAPv3 delete
052 * operation, which removes an entry from the directory.  A delete request
053 * contains the DN of the entry to remove.  It may also include a set of
054 * controls to send to the server.
055 * {@code DeleteRequest} objects are mutable and therefore can be altered and
056 * re-used for multiple requests.  Note, however, that {@code DeleteRequest}
057 * objects are not threadsafe and therefore a single {@code DeleteRequest}
058 * object instance should not be used to process multiple requests at the same
059 * time.
060 * <BR><BR>
061 * <H2>Example</H2>
062 * The following example demonstrates the process for performing a delete
063 * operation:
064 * <PRE>
065 * DeleteRequest deleteRequest =
066 *      new DeleteRequest("cn=entry to delete,dc=example,dc=com");
067 * LDAPResult deleteResult;
068 * try
069 * {
070 *   deleteResult = connection.delete(deleteRequest);
071 *   // If we get here, the delete was successful.
072 * }
073 * catch (LDAPException le)
074 * {
075 *   // The delete operation failed.
076 *   deleteResult = le.toLDAPResult();
077 *   ResultCode resultCode = le.getResultCode();
078 *   String errorMessageFromServer = le.getDiagnosticMessage();
079 * }
080 * </PRE>
081 */
082@Mutable()
083@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
084public final class DeleteRequest
085       extends UpdatableLDAPRequest
086       implements ReadOnlyDeleteRequest, ResponseAcceptor, ProtocolOp
087{
088  /**
089   * The serial version UID for this serializable class.
090   */
091  private static final long serialVersionUID = -6126029442850884239L;
092
093
094
095  // The message ID from the last LDAP message sent from this request.
096  private int messageID = -1;
097
098  // The queue that will be used to receive response messages from the server.
099  private final LinkedBlockingQueue<LDAPResponse> responseQueue =
100       new LinkedBlockingQueue<>();
101
102  // The DN of the entry to delete.
103  private String dn;
104
105
106
107  /**
108   * Creates a new delete request with the provided DN.
109   *
110   * @param  dn  The DN of the entry to delete.  It must not be {@code null}.
111   */
112  public DeleteRequest(final String dn)
113  {
114    super(null);
115
116    Validator.ensureNotNull(dn);
117
118    this.dn = dn;
119  }
120
121
122
123  /**
124   * Creates a new delete request with the provided DN.
125   *
126   * @param  dn        The DN of the entry to delete.  It must not be
127   *                   {@code null}.
128   * @param  controls  The set of controls to include in the request.
129   */
130  public DeleteRequest(final String dn, final Control[] controls)
131  {
132    super(controls);
133
134    Validator.ensureNotNull(dn);
135
136    this.dn = dn;
137  }
138
139
140
141  /**
142   * Creates a new delete request with the provided DN.
143   *
144   * @param  dn  The DN of the entry to delete.  It must not be {@code null}.
145   */
146  public DeleteRequest(final DN dn)
147  {
148    super(null);
149
150    Validator.ensureNotNull(dn);
151
152    this.dn = dn.toString();
153  }
154
155
156
157  /**
158   * Creates a new delete request with the provided DN.
159   *
160   * @param  dn        The DN of the entry to delete.  It must not be
161   *                   {@code null}.
162   * @param  controls  The set of controls to include in the request.
163   */
164  public DeleteRequest(final DN dn, final Control[] controls)
165  {
166    super(controls);
167
168    Validator.ensureNotNull(dn);
169
170    this.dn = dn.toString();
171  }
172
173
174
175  /**
176   * {@inheritDoc}
177   */
178  @Override()
179  public String getDN()
180  {
181    return dn;
182  }
183
184
185
186  /**
187   * Specifies the DN of the entry to delete.
188   *
189   * @param  dn  The DN of the entry to delete.  It must not be {@code null}.
190   */
191  public void setDN(final String dn)
192  {
193    Validator.ensureNotNull(dn);
194
195    this.dn = dn;
196  }
197
198
199
200  /**
201   * Specifies the DN of the entry to delete.
202   *
203   * @param  dn  The DN of the entry to delete.  It must not be {@code null}.
204   */
205  public void setDN(final DN dn)
206  {
207    Validator.ensureNotNull(dn);
208
209    this.dn = dn.toString();
210  }
211
212
213
214  /**
215   * {@inheritDoc}
216   */
217  @Override()
218  public byte getProtocolOpType()
219  {
220    return LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST;
221  }
222
223
224
225  /**
226   * {@inheritDoc}
227   */
228  @Override()
229  public void writeTo(final ASN1Buffer buffer)
230  {
231    buffer.addOctetString(LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST, dn);
232  }
233
234
235
236  /**
237   * Encodes the delete request protocol op to an ASN.1 element.
238   *
239   * @return  The ASN.1 element with the encoded delete request protocol op.
240   */
241  @Override()
242  public ASN1Element encodeProtocolOp()
243  {
244    return new ASN1OctetString(LDAPMessage.PROTOCOL_OP_TYPE_DELETE_REQUEST, dn);
245  }
246
247
248
249  /**
250   * Sends this delete request to the directory server over the provided
251   * connection and returns the associated response.
252   *
253   * @param  connection  The connection to use to communicate with the directory
254   *                     server.
255   * @param  depth       The current referral depth for this request.  It should
256   *                     always be one for the initial request, and should only
257   *                     be incremented when following referrals.
258   *
259   * @return  An LDAP result object that provides information about the result
260   *          of the delete processing.
261   *
262   * @throws  LDAPException  If a problem occurs while sending the request or
263   *                         reading the response.
264   */
265  @Override()
266  protected LDAPResult process(final LDAPConnection connection, final int depth)
267            throws LDAPException
268  {
269    if (connection.synchronousMode())
270    {
271      @SuppressWarnings("deprecation")
272      final boolean autoReconnect =
273           connection.getConnectionOptions().autoReconnect();
274      return processSync(connection, depth, autoReconnect);
275    }
276
277    final long requestTime = System.nanoTime();
278    processAsync(connection, null);
279
280    try
281    {
282      // Wait for and process the response.
283      final LDAPResponse response;
284      try
285      {
286        final long responseTimeout = getResponseTimeoutMillis(connection);
287        if (responseTimeout > 0)
288        {
289          response = responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS);
290        }
291        else
292        {
293          response = responseQueue.take();
294        }
295      }
296      catch (final InterruptedException ie)
297      {
298        Debug.debugException(ie);
299        Thread.currentThread().interrupt();
300        throw new LDAPException(ResultCode.LOCAL_ERROR,
301             ERR_DELETE_INTERRUPTED.get(connection.getHostPort()), ie);
302      }
303
304      return handleResponse(connection, response,  requestTime, depth, false);
305    }
306    finally
307    {
308      connection.deregisterResponseAcceptor(messageID);
309    }
310  }
311
312
313
314  /**
315   * Sends this delete request to the directory server over the provided
316   * connection and returns the message ID for the request.
317   *
318   * @param  connection      The connection to use to communicate with the
319   *                         directory server.
320   * @param  resultListener  The async result listener that is to be notified
321   *                         when the response is received.  It may be
322   *                         {@code null} only if the result is to be processed
323   *                         by this class.
324   *
325   * @return  The async request ID created for the operation, or {@code null} if
326   *          the provided {@code resultListener} is {@code null} and the
327   *          operation will not actually be processed asynchronously.
328   *
329   * @throws  LDAPException  If a problem occurs while sending the request.
330   */
331  AsyncRequestID processAsync(final LDAPConnection connection,
332                              final AsyncResultListener resultListener)
333                 throws LDAPException
334  {
335    // Create the LDAP message.
336    messageID = connection.nextMessageID();
337    final LDAPMessage message = new LDAPMessage(messageID, this, getControls());
338
339
340    // If the provided async result listener is {@code null}, then we'll use
341    // this class as the message acceptor.  Otherwise, create an async helper
342    // and use it as the message acceptor.
343    final AsyncRequestID asyncRequestID;
344    final long timeout = getResponseTimeoutMillis(connection);
345    if (resultListener == null)
346    {
347      asyncRequestID = null;
348      connection.registerResponseAcceptor(messageID, this);
349    }
350    else
351    {
352      final AsyncHelper helper = new AsyncHelper(connection,
353           OperationType.DELETE, messageID, resultListener,
354           getIntermediateResponseListener());
355      connection.registerResponseAcceptor(messageID, helper);
356      asyncRequestID = helper.getAsyncRequestID();
357
358      if (timeout > 0L)
359      {
360        final Timer timer = connection.getTimer();
361        final AsyncTimeoutTimerTask timerTask =
362             new AsyncTimeoutTimerTask(helper);
363        timer.schedule(timerTask, timeout);
364        asyncRequestID.setTimerTask(timerTask);
365      }
366    }
367
368
369    // Send the request to the server.
370    try
371    {
372      Debug.debugLDAPRequest(Level.INFO, this, messageID, connection);
373      connection.getConnectionStatistics().incrementNumDeleteRequests();
374      connection.sendMessage(message, timeout);
375      return asyncRequestID;
376    }
377    catch (final LDAPException le)
378    {
379      Debug.debugException(le);
380
381      connection.deregisterResponseAcceptor(messageID);
382      throw le;
383    }
384  }
385
386
387
388  /**
389   * Processes this delete operation in synchronous mode, in which the same
390   * thread will send the request and read the response.
391   *
392   * @param  connection  The connection to use to communicate with the directory
393   *                     server.
394   * @param  depth       The current referral depth for this request.  It should
395   *                     always be one for the initial request, and should only
396   *                     be incremented when following referrals.
397   * @param  allowRetry  Indicates whether the request may be re-tried on a
398   *                     re-established connection if the initial attempt fails
399   *                     in a way that indicates the connection is no longer
400   *                     valid and autoReconnect is true.
401   *
402   * @return  An LDAP result object that provides information about the result
403   *          of the delete processing.
404   *
405   * @throws  LDAPException  If a problem occurs while sending the request or
406   *                         reading the response.
407   */
408  private LDAPResult processSync(final LDAPConnection connection,
409                                 final int depth, final boolean allowRetry)
410          throws LDAPException
411  {
412    // Create the LDAP message.
413    messageID = connection.nextMessageID();
414    final LDAPMessage message =
415         new LDAPMessage(messageID,  this, getControls());
416
417
418    // Send the request to the server.
419    final long requestTime = System.nanoTime();
420    Debug.debugLDAPRequest(Level.INFO, this, messageID, connection);
421    connection.getConnectionStatistics().incrementNumDeleteRequests();
422    try
423    {
424      connection.sendMessage(message, getResponseTimeoutMillis(connection));
425    }
426    catch (final LDAPException le)
427    {
428      Debug.debugException(le);
429
430      if (allowRetry)
431      {
432        final LDAPResult retryResult = reconnectAndRetry(connection, depth,
433             le.getResultCode());
434        if (retryResult != null)
435        {
436          return retryResult;
437        }
438      }
439
440      throw le;
441    }
442
443    while (true)
444    {
445      final LDAPResponse response;
446      try
447      {
448        response = connection.readResponse(messageID);
449      }
450      catch (final LDAPException le)
451      {
452        Debug.debugException(le);
453
454        if ((le.getResultCode() == ResultCode.TIMEOUT) &&
455            connection.getConnectionOptions().abandonOnTimeout())
456        {
457          connection.abandon(messageID);
458        }
459
460        if (allowRetry)
461        {
462          final LDAPResult retryResult = reconnectAndRetry(connection, depth,
463               le.getResultCode());
464          if (retryResult != null)
465          {
466            return retryResult;
467          }
468        }
469
470        throw le;
471      }
472
473      if (response instanceof IntermediateResponse)
474      {
475        final IntermediateResponseListener listener =
476             getIntermediateResponseListener();
477        if (listener != null)
478        {
479          listener.intermediateResponseReturned(
480               (IntermediateResponse) response);
481        }
482      }
483      else
484      {
485        return handleResponse(connection, response, requestTime, depth,
486             allowRetry);
487      }
488    }
489  }
490
491
492
493  /**
494   * Performs the necessary processing for handling a response.
495   *
496   * @param  connection   The connection used to read the response.
497   * @param  response     The response to be processed.
498   * @param  requestTime  The time the request was sent to the server.
499   * @param  depth        The current referral depth for this request.  It
500   *                      should always be one for the initial request, and
501   *                      should only be incremented when following referrals.
502   * @param  allowRetry   Indicates whether the request may be re-tried on a
503   *                      re-established connection if the initial attempt fails
504   *                      in a way that indicates the connection is no longer
505   *                      valid and autoReconnect is true.
506   *
507   * @return  The delete result.
508   *
509   * @throws  LDAPException  If a problem occurs.
510   */
511  private LDAPResult handleResponse(final LDAPConnection connection,
512                                    final LDAPResponse response,
513                                    final long requestTime, final int depth,
514                                    final boolean allowRetry)
515          throws LDAPException
516  {
517    if (response == null)
518    {
519      final long waitTime =
520           StaticUtils.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, messageID, dn,
528                connection.getHostPort()));
529    }
530
531    connection.getConnectionStatistics().incrementNumDeleteResponses(
532         System.nanoTime() - requestTime);
533    if (response instanceof ConnectionClosedResponse)
534    {
535      // The connection was closed while waiting for the response.
536      if (allowRetry)
537      {
538        final LDAPResult retryResult = reconnectAndRetry(connection, depth,
539             ResultCode.SERVER_DOWN);
540        if (retryResult != null)
541        {
542          return retryResult;
543        }
544      }
545
546      final ConnectionClosedResponse ccr = (ConnectionClosedResponse) response;
547      final String message = ccr.getMessage();
548      if (message == null)
549      {
550        throw new LDAPException(ccr.getResultCode(),
551             ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE.get(
552                  connection.getHostPort(), toString()));
553      }
554      else
555      {
556        throw new LDAPException(ccr.getResultCode(),
557             ERR_CONN_CLOSED_WAITING_FOR_DELETE_RESPONSE_WITH_MESSAGE.get(
558                  connection.getHostPort(), toString(), message));
559      }
560    }
561
562    final LDAPResult result = (LDAPResult) response;
563    if ((result.getResultCode().equals(ResultCode.REFERRAL)) &&
564        followReferrals(connection))
565    {
566      if (depth >= connection.getConnectionOptions().getReferralHopLimit())
567      {
568        return new LDAPResult(messageID, ResultCode.REFERRAL_LIMIT_EXCEEDED,
569                              ERR_TOO_MANY_REFERRALS.get(),
570                              result.getMatchedDN(), result.getReferralURLs(),
571                              result.getResponseControls());
572      }
573
574      return followReferral(result, connection, depth);
575    }
576    else
577    {
578      if (allowRetry)
579      {
580        final LDAPResult retryResult = reconnectAndRetry(connection, depth,
581             result.getResultCode());
582        if (retryResult != null)
583        {
584          return retryResult;
585        }
586      }
587
588      return result;
589    }
590  }
591
592
593
594  /**
595   * Attempts to re-establish the connection and retry processing this request
596   * on it.
597   *
598   * @param  connection  The connection to be re-established.
599   * @param  depth       The current referral depth for this request.  It should
600   *                     always be one for the initial request, and should only
601   *                     be incremented when following referrals.
602   * @param  resultCode  The result code for the previous operation attempt.
603   *
604   * @return  The result from re-trying the add, or {@code null} if it could not
605   *          be re-tried.
606   */
607  private LDAPResult reconnectAndRetry(final LDAPConnection connection,
608                                       final int depth,
609                                       final ResultCode resultCode)
610  {
611    try
612    {
613      // We will only want to retry for certain result codes that indicate a
614      // connection problem.
615      switch (resultCode.intValue())
616      {
617        case ResultCode.SERVER_DOWN_INT_VALUE:
618        case ResultCode.DECODING_ERROR_INT_VALUE:
619        case ResultCode.CONNECT_ERROR_INT_VALUE:
620          connection.reconnect();
621          return processSync(connection, depth, false);
622      }
623    }
624    catch (final Exception e)
625    {
626      Debug.debugException(e);
627    }
628
629    return null;
630  }
631
632
633
634  /**
635   * Attempts to follow a referral to perform a delete operation in the target
636   * server.
637   *
638   * @param  referralResult  The LDAP result object containing information about
639   *                         the referral to follow.
640   * @param  connection      The connection on which the referral was received.
641   * @param  depth           The number of referrals followed in the course of
642   *                         processing this request.
643   *
644   * @return  The result of attempting to process the delete operation by
645   *          following the referral.
646   *
647   * @throws  LDAPException  If a problem occurs while attempting to establish
648   *                         the referral connection, sending the request, or
649   *                         reading the result.
650   */
651  private LDAPResult followReferral(final LDAPResult referralResult,
652                                    final LDAPConnection connection,
653                                    final int depth)
654          throws LDAPException
655  {
656    for (final String urlString : referralResult.getReferralURLs())
657    {
658      try
659      {
660        final LDAPURL referralURL = new LDAPURL(urlString);
661        final String host = referralURL.getHost();
662
663        if (host == null)
664        {
665          // We can't handle a referral in which there is no host.
666          continue;
667        }
668
669        final DeleteRequest deleteRequest;
670        if (referralURL.baseDNProvided())
671        {
672          deleteRequest = new DeleteRequest(referralURL.getBaseDN(),
673                                            getControls());
674        }
675        else
676        {
677          deleteRequest = this;
678        }
679
680        final LDAPConnection referralConn = getReferralConnector(connection).
681             getReferralConnection(referralURL, connection);
682        try
683        {
684          return deleteRequest.process(referralConn, depth+1);
685        }
686        finally
687        {
688          referralConn.setDisconnectInfo(DisconnectType.REFERRAL, null, null);
689          referralConn.close();
690        }
691      }
692      catch (final LDAPException le)
693      {
694        Debug.debugException(le);
695      }
696    }
697
698    // If we've gotten here, then we could not follow any of the referral URLs,
699    // so we'll just return the original referral result.
700    return referralResult;
701  }
702
703
704
705  /**
706   * {@inheritDoc}
707   */
708  @InternalUseOnly()
709  @Override()
710  public void responseReceived(final LDAPResponse response)
711         throws LDAPException
712  {
713    try
714    {
715      responseQueue.put(response);
716    }
717    catch (final Exception e)
718    {
719      Debug.debugException(e);
720
721      if (e instanceof InterruptedException)
722      {
723        Thread.currentThread().interrupt();
724      }
725
726      throw new LDAPException(ResultCode.LOCAL_ERROR,
727           ERR_EXCEPTION_HANDLING_RESPONSE.get(
728                StaticUtils.getExceptionMessage(e)),
729           e);
730    }
731  }
732
733
734
735  /**
736   * {@inheritDoc}
737   */
738  @Override()
739  public int getLastMessageID()
740  {
741    return messageID;
742  }
743
744
745
746  /**
747   * {@inheritDoc}
748   */
749  @Override()
750  public OperationType getOperationType()
751  {
752    return OperationType.DELETE;
753  }
754
755
756
757  /**
758   * {@inheritDoc}
759   */
760  @Override()
761  public DeleteRequest duplicate()
762  {
763    return duplicate(getControls());
764  }
765
766
767
768  /**
769   * {@inheritDoc}
770   */
771  @Override()
772  public DeleteRequest duplicate(final Control[] controls)
773  {
774    final DeleteRequest r = new DeleteRequest(dn, controls);
775
776    if (followReferralsInternal() != null)
777    {
778      r.setFollowReferrals(followReferralsInternal());
779    }
780
781    if (getReferralConnectorInternal() != null)
782    {
783      r.setReferralConnector(getReferralConnectorInternal());
784    }
785
786    r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
787
788    return r;
789  }
790
791
792
793  /**
794   * {@inheritDoc}
795   */
796  @Override()
797  public LDIFDeleteChangeRecord toLDIFChangeRecord()
798  {
799    return new LDIFDeleteChangeRecord(this);
800  }
801
802
803
804  /**
805   * {@inheritDoc}
806   */
807  @Override()
808  public String[] toLDIF()
809  {
810    return toLDIFChangeRecord().toLDIF();
811  }
812
813
814
815  /**
816   * {@inheritDoc}
817   */
818  @Override()
819  public String toLDIFString()
820  {
821    return toLDIFChangeRecord().toLDIFString();
822  }
823
824
825
826  /**
827   * {@inheritDoc}
828   */
829  @Override()
830  public void toString(final StringBuilder buffer)
831  {
832    buffer.append("DeleteRequest(dn='");
833    buffer.append(dn);
834    buffer.append('\'');
835
836    final Control[] controls = getControls();
837    if (controls.length > 0)
838    {
839      buffer.append(", controls={");
840      for (int i=0; i < controls.length; i++)
841      {
842        if (i > 0)
843        {
844          buffer.append(", ");
845        }
846
847        buffer.append(controls[i]);
848      }
849      buffer.append('}');
850    }
851
852    buffer.append(')');
853  }
854
855
856
857  /**
858   * {@inheritDoc}
859   */
860  @Override()
861  public void toCode(final List<String> lineList, final String requestID,
862                     final int indentSpaces, final boolean includeProcessing)
863  {
864    // Create the request variable.
865    ToCodeHelper.generateMethodCall(lineList, indentSpaces, "DeleteRequest",
866         requestID + "Request", "new DeleteRequest",
867         ToCodeArgHelper.createString(dn, "Entry DN"));
868
869    // If there are any controls, then add them to the request.
870    for (final Control c : getControls())
871    {
872      ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null,
873           requestID + "Request.addControl",
874           ToCodeArgHelper.createControl(c, null));
875    }
876
877
878    // Add lines for processing the request and obtaining the result.
879    if (includeProcessing)
880    {
881      // Generate a string with the appropriate indent.
882      final StringBuilder buffer = new StringBuilder();
883      for (int i=0; i < indentSpaces; i++)
884      {
885        buffer.append(' ');
886      }
887      final String indent = buffer.toString();
888
889      lineList.add("");
890      lineList.add(indent + "try");
891      lineList.add(indent + '{');
892      lineList.add(indent + "  LDAPResult " + requestID +
893           "Result = connection.delete(" + requestID + "Request);");
894      lineList.add(indent + "  // The delete was processed successfully.");
895      lineList.add(indent + '}');
896      lineList.add(indent + "catch (LDAPException e)");
897      lineList.add(indent + '{');
898      lineList.add(indent + "  // The delete failed.  Maybe the following " +
899           "will help explain why.");
900      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
901      lineList.add(indent + "  String message = e.getMessage();");
902      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
903      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
904      lineList.add(indent + "  Control[] responseControls = " +
905           "e.getResponseControls();");
906      lineList.add(indent + '}');
907    }
908  }
909}