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