001/*
002 * Copyright 2007-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2018 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.InternalUseOnly;
039import com.unboundid.util.Mutable;
040import com.unboundid.util.ThreadSafety;
041import com.unboundid.util.ThreadSafetyLevel;
042
043import static com.unboundid.ldap.sdk.LDAPMessages.*;
044import static com.unboundid.util.Debug.*;
045import static com.unboundid.util.StaticUtils.*;
046import static com.unboundid.util.Validator.*;
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<LDAPResponse>();
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    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    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    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    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    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    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        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      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      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    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      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        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 = nanosToMillis(System.nanoTime() - requestTime);
520      if (connection.getConnectionOptions().abandonOnTimeout())
521      {
522        connection.abandon(messageID);
523      }
524
525      throw new LDAPException(ResultCode.TIMEOUT,
526           ERR_DELETE_CLIENT_TIMEOUT.get(waitTime, messageID, dn,
527                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 (final 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  @Override()
709  public void responseReceived(final LDAPResponse response)
710         throws LDAPException
711  {
712    try
713    {
714      responseQueue.put(response);
715    }
716    catch (final Exception e)
717    {
718      debugException(e);
719
720      if (e instanceof InterruptedException)
721      {
722        Thread.currentThread().interrupt();
723      }
724
725      throw new LDAPException(ResultCode.LOCAL_ERROR,
726           ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e);
727    }
728  }
729
730
731
732  /**
733   * {@inheritDoc}
734   */
735  @Override()
736  public int getLastMessageID()
737  {
738    return messageID;
739  }
740
741
742
743  /**
744   * {@inheritDoc}
745   */
746  @Override()
747  public OperationType getOperationType()
748  {
749    return OperationType.DELETE;
750  }
751
752
753
754  /**
755   * {@inheritDoc}
756   */
757  @Override()
758  public DeleteRequest duplicate()
759  {
760    return duplicate(getControls());
761  }
762
763
764
765  /**
766   * {@inheritDoc}
767   */
768  @Override()
769  public DeleteRequest duplicate(final Control[] controls)
770  {
771    final DeleteRequest r = new DeleteRequest(dn, controls);
772
773    if (followReferralsInternal() != null)
774    {
775      r.setFollowReferrals(followReferralsInternal());
776    }
777
778    r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
779
780    return r;
781  }
782
783
784
785  /**
786   * {@inheritDoc}
787   */
788  @Override()
789  public LDIFDeleteChangeRecord toLDIFChangeRecord()
790  {
791    return new LDIFDeleteChangeRecord(this);
792  }
793
794
795
796  /**
797   * {@inheritDoc}
798   */
799  @Override()
800  public String[] toLDIF()
801  {
802    return toLDIFChangeRecord().toLDIF();
803  }
804
805
806
807  /**
808   * {@inheritDoc}
809   */
810  @Override()
811  public String toLDIFString()
812  {
813    return toLDIFChangeRecord().toLDIFString();
814  }
815
816
817
818  /**
819   * {@inheritDoc}
820   */
821  @Override()
822  public void toString(final StringBuilder buffer)
823  {
824    buffer.append("DeleteRequest(dn='");
825    buffer.append(dn);
826    buffer.append('\'');
827
828    final Control[] controls = getControls();
829    if (controls.length > 0)
830    {
831      buffer.append(", controls={");
832      for (int i=0; i < controls.length; i++)
833      {
834        if (i > 0)
835        {
836          buffer.append(", ");
837        }
838
839        buffer.append(controls[i]);
840      }
841      buffer.append('}');
842    }
843
844    buffer.append(')');
845  }
846
847
848
849  /**
850   * {@inheritDoc}
851   */
852  @Override()
853  public void toCode(final List<String> lineList, final String requestID,
854                     final int indentSpaces, final boolean includeProcessing)
855  {
856    // Create the request variable.
857    ToCodeHelper.generateMethodCall(lineList, indentSpaces, "DeleteRequest",
858         requestID + "Request", "new DeleteRequest",
859         ToCodeArgHelper.createString(dn, "Entry DN"));
860
861    // If there are any controls, then add them to the request.
862    for (final Control c : getControls())
863    {
864      ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null,
865           requestID + "Request.addControl",
866           ToCodeArgHelper.createControl(c, null));
867    }
868
869
870    // Add lines for processing the request and obtaining the result.
871    if (includeProcessing)
872    {
873      // Generate a string with the appropriate indent.
874      final StringBuilder buffer = new StringBuilder();
875      for (int i=0; i < indentSpaces; i++)
876      {
877        buffer.append(' ');
878      }
879      final String indent = buffer.toString();
880
881      lineList.add("");
882      lineList.add(indent + "try");
883      lineList.add(indent + '{');
884      lineList.add(indent + "  LDAPResult " + requestID +
885           "Result = connection.delete(" + requestID + "Request);");
886      lineList.add(indent + "  // The delete was processed successfully.");
887      lineList.add(indent + '}');
888      lineList.add(indent + "catch (LDAPException e)");
889      lineList.add(indent + '{');
890      lineList.add(indent + "  // The delete failed.  Maybe the following " +
891           "will help explain why.");
892      lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
893      lineList.add(indent + "  String message = e.getMessage();");
894      lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
895      lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
896      lineList.add(indent + "  Control[] responseControls = " +
897           "e.getResponseControls();");
898      lineList.add(indent + '}');
899    }
900  }
901}