001    /*
002     * Copyright 2007-2017 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2017 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.ArrayList;
026    import java.util.Arrays;
027    import java.util.Collections;
028    import java.util.List;
029    import java.util.Timer;
030    import java.util.concurrent.LinkedBlockingQueue;
031    import java.util.concurrent.TimeUnit;
032    
033    import com.unboundid.asn1.ASN1Boolean;
034    import com.unboundid.asn1.ASN1Buffer;
035    import com.unboundid.asn1.ASN1BufferSequence;
036    import com.unboundid.asn1.ASN1Element;
037    import com.unboundid.asn1.ASN1Enumerated;
038    import com.unboundid.asn1.ASN1Integer;
039    import com.unboundid.asn1.ASN1OctetString;
040    import com.unboundid.asn1.ASN1Sequence;
041    import com.unboundid.ldap.protocol.LDAPMessage;
042    import com.unboundid.ldap.protocol.LDAPResponse;
043    import com.unboundid.ldap.protocol.ProtocolOp;
044    import com.unboundid.util.InternalUseOnly;
045    import com.unboundid.util.Mutable;
046    import com.unboundid.util.ThreadSafety;
047    import com.unboundid.util.ThreadSafetyLevel;
048    
049    import static com.unboundid.ldap.sdk.LDAPMessages.*;
050    import static com.unboundid.util.Debug.*;
051    import static com.unboundid.util.StaticUtils.*;
052    import static com.unboundid.util.Validator.*;
053    
054    
055    
056    /**
057     * This class implements the processing necessary to perform an LDAPv3 search
058     * operation, which can be used to retrieve entries that match a given set of
059     * criteria.  A search request may include the following elements:
060     * <UL>
061     *   <LI>Base DN -- Specifies the base DN for the search.  Only entries at or
062     *       below this location in the server (based on the scope) will be
063     *       considered potential matches.</LI>
064     *   <LI>Scope -- Specifies the range of entries relative to the base DN that
065     *       may be considered potential matches.</LI>
066     *   <LI>Dereference Policy -- Specifies the behavior that the server should
067     *       exhibit if any alias entries are encountered while processing the
068     *       search.  If no dereference policy is provided, then a default of
069     *       {@code DereferencePolicy.NEVER} will be used.</LI>
070     *   <LI>Size Limit -- Specifies the maximum number of entries that should be
071     *       returned from the search.  A value of zero indicates that there should
072     *       not be any limit enforced.  Note that the directory server may also
073     *       be configured with a server-side size limit which can also limit the
074     *       number of entries that may be returned to the client and in that case
075     *       the smaller of the client-side and server-side limits will be
076     *       used.  If no size limit is provided, then a default of zero (unlimited)
077     *       will be used.</LI>
078     *   <LI>Time Limit -- Specifies the maximum length of time in seconds that the
079     *       server should spend processing the search.  A value of zero indicates
080     *       that there should not be any limit enforced.  Note that the directory
081     *       server may also be configured with a server-side time limit which can
082     *       also limit the processing time, and in that case the smaller of the
083     *       client-side and server-side limits will be used.  If no time limit is
084     *       provided, then a default of zero (unlimited) will be used.</LI>
085     *   <LI>Types Only -- Indicates whether matching entries should include only
086     *       attribute names, or both attribute names and values.  If no value is
087     *       provided, then a default of {@code false} will be used.</LI>
088     *   <LI>Filter -- Specifies the criteria for determining which entries should
089     *       be returned.  See the {@link Filter} class for the types of filters
090     *       that may be used.
091     *       <BR><BR>
092     *       Note that filters can be specified using either their string
093     *       representations or as {@link Filter} objects.  As noted in the
094     *       documentation for the {@link Filter} class, using the string
095     *       representation may be somewhat dangerous if the data is not properly
096     *       sanitized because special characters contained in the filter may cause
097     *       it to be invalid or worse expose a vulnerability that could cause the
098     *       filter to request more information than was intended.  As a result, if
099     *       the filter may include special characters or user-provided strings,
100     *       then it is recommended that you use {@link Filter} objects created from
101     *       their individual components rather than their string representations.
102     * </LI>
103     *   <LI>Attributes -- Specifies the set of attributes that should be included
104     *       in matching entries.  If no attributes are provided, then the server
105     *       will default to returning all user attributes.  If a specified set of
106     *       attributes is given, then only those attributes will be included.
107     *       Values that may be included to indicate a special meaning include:
108     *       <UL>
109     *         <LI>{@code NO_ATTRIBUTES} -- Indicates that no attributes should be
110     *             returned.  That is, only the DNs of matching entries will be
111     *             returned.</LI>
112     *         <LI>{@code ALL_USER_ATTRIBUTES} -- Indicates that all user attributes
113     *             should be included in matching entries.  This is the default if
114     *             no attributes are provided, but this special value may be
115     *             included if a specific set of operational attributes should be
116     *             included along with all user attributes.</LI>
117     *         <LI>{@code ALL_OPERATIONAL_ATTRIBUTES} -- Indicates that all
118     *             operational attributes should be included in matching
119     *             entries.</LI>
120     *       </UL>
121     *       These special values may be used alone or in conjunction with each
122     *       other and/or any specific attribute names or OIDs.</LI>
123     *   <LI>An optional set of controls to include in the request to send to the
124     *       server.</LI>
125     *   <LI>An optional {@link SearchResultListener} which may be used to process
126     *       search result entries and search result references returned by the
127     *       server in the course of processing the request.  If this is
128     *       {@code null}, then the entries and references will be collected and
129     *       returned in the {@link SearchResult} object that is returned.</LI>
130     * </UL>
131     * When processing a search operation, there are three ways that the returned
132     * entries and references may be accessed:
133     * <UL>
134     *   <LI>If the {@link LDAPInterface#search(SearchRequest)} method is used and
135     *       the provided search request does not include a
136     *       {@link SearchResultListener} object, then the entries and references
137     *       will be collected internally and made available in the
138     *       {@link SearchResult} object that is returned.</LI>
139     *   <LI>If the {@link LDAPInterface#search(SearchRequest)} method is used and
140     *       the provided search request does include a {@link SearchResultListener}
141     *       object, then that listener will be used to provide access to the
142     *       entries and references, and they will not be present in the
143     *       {@link SearchResult} object (although the number of entries and
144     *       references returned will still be available).</LI>
145     *   <LI>The {@link LDAPEntrySource} object may be used to access the entries
146     *        and references returned from the search.  It uses an
147     *        {@code Iterator}-like API to provide access to the entries that are
148     *        returned, and any references returned will be included in the
149     *        {@link EntrySourceException} thrown on the appropriate call to
150     *        {@link LDAPEntrySource#nextEntry()}.</LI>
151     * </UL>
152     * <BR><BR>
153     * {@code SearchRequest} objects are mutable and therefore can be altered and
154     * re-used for multiple requests.  Note, however, that {@code SearchRequest}
155     * objects are not threadsafe and therefore a single {@code SearchRequest}
156     * object instance should not be used to process multiple requests at the same
157     * time.
158     * <BR><BR>
159     * <H2>Example</H2>
160     * The following example demonstrates a simple search operation in which the
161     * client performs a search to find all users in the "Sales" department and then
162     * retrieves the name and e-mail address for each matching user:
163     * <PRE>
164     * // Construct a filter that can be used to find everyone in the Sales
165     * // department, and then create a search request to find all such users
166     * // in the directory.
167     * Filter filter = Filter.createEqualityFilter("ou", "Sales");
168     * SearchRequest searchRequest =
169     *      new SearchRequest("dc=example,dc=com", SearchScope.SUB, filter,
170     *           "cn", "mail");
171     * SearchResult searchResult;
172     *
173     * try
174     * {
175     *   searchResult = connection.search(searchRequest);
176     *
177     *   for (SearchResultEntry entry : searchResult.getSearchEntries())
178     *   {
179     *     String name = entry.getAttributeValue("cn");
180     *     String mail = entry.getAttributeValue("mail");
181     *   }
182     * }
183     * catch (LDAPSearchException lse)
184     * {
185     *   // The search failed for some reason.
186     *   searchResult = lse.getSearchResult();
187     *   ResultCode resultCode = lse.getResultCode();
188     *   String errorMessageFromServer = lse.getDiagnosticMessage();
189     * }
190     * </PRE>
191     */
192    @Mutable()
193    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
194    public final class SearchRequest
195           extends UpdatableLDAPRequest
196           implements ReadOnlySearchRequest, ResponseAcceptor, ProtocolOp
197    {
198      /**
199       * The special value "*" that can be included in the set of requested
200       * attributes to indicate that all user attributes should be returned.
201       */
202      public static final String ALL_USER_ATTRIBUTES = "*";
203    
204    
205    
206      /**
207       * The special value "+" that can be included in the set of requested
208       * attributes to indicate that all operational attributes should be returned.
209       */
210      public static final String ALL_OPERATIONAL_ATTRIBUTES = "+";
211    
212    
213    
214      /**
215       * The special value "1.1" that can be included in the set of requested
216       * attributes to indicate that no attributes should be returned, with the
217       * exception of any other attributes explicitly named in the set of requested
218       * attributes.
219       */
220      public static final String NO_ATTRIBUTES = "1.1";
221    
222    
223    
224      /**
225       * The default set of requested attributes that will be used, which will
226       * return all user attributes but no operational attributes.
227       */
228      public static final String[] REQUEST_ATTRS_DEFAULT = NO_STRINGS;
229    
230    
231    
232      /**
233       * The serial version UID for this serializable class.
234       */
235      private static final long serialVersionUID = 1500219434086474893L;
236    
237    
238    
239      // The set of requested attributes.
240      private String[] attributes;
241    
242      // Indicates whether to retrieve attribute types only or both types and
243      // values.
244      private boolean typesOnly;
245    
246      // The behavior to use when aliases are encountered.
247      private DereferencePolicy derefPolicy;
248    
249      // The message ID from the last LDAP message sent from this request.
250      private int messageID = -1;
251    
252      // The size limit for this search request.
253      private int sizeLimit;
254    
255      // The time limit for this search request.
256      private int timeLimit;
257    
258      // The parsed filter for this search request.
259      private Filter filter;
260    
261      // The queue that will be used to receive response messages from the server.
262      private final LinkedBlockingQueue<LDAPResponse> responseQueue =
263           new LinkedBlockingQueue<LDAPResponse>(50);
264    
265      // The search result listener that should be used to return results
266      // interactively to the requester.
267      private final SearchResultListener searchResultListener;
268    
269      // The scope for this search request.
270      private SearchScope scope;
271    
272      // The base DN for this search request.
273      private String baseDN;
274    
275    
276    
277      /**
278       * Creates a new search request with the provided information.  Search result
279       * entries and references will be collected internally and included in the
280       * {@code SearchResult} object returned when search processing is completed.
281       *
282       * @param  baseDN      The base DN for the search request.  It must not be
283       *                     {@code null}.
284       * @param  scope       The scope that specifies the range of entries that
285       *                     should be examined for the search.
286       * @param  filter      The string representation of the filter to use to
287       *                     identify matching entries.  It must not be
288       *                     {@code null}.
289       * @param  attributes  The set of attributes that should be returned in
290       *                     matching entries.  It may be {@code null} or empty if
291       *                     the default attribute set (all user attributes) is to
292       *                     be requested.
293       *
294       * @throws  LDAPException  If the provided filter string cannot be parsed as
295       *                         an LDAP filter.
296       */
297      public SearchRequest(final String baseDN, final SearchScope scope,
298                           final String filter, final String... attributes)
299             throws LDAPException
300      {
301        this(null, null, baseDN, scope, DereferencePolicy.NEVER, 0, 0, false,
302             Filter.create(filter), attributes);
303      }
304    
305    
306    
307      /**
308       * Creates a new search request with the provided information.  Search result
309       * entries and references will be collected internally and included in the
310       * {@code SearchResult} object returned when search processing is completed.
311       *
312       * @param  baseDN      The base DN for the search request.  It must not be
313       *                     {@code null}.
314       * @param  scope       The scope that specifies the range of entries that
315       *                     should be examined for the search.
316       * @param  filter      The string representation of the filter to use to
317       *                     identify matching entries.  It must not be
318       *                     {@code null}.
319       * @param  attributes  The set of attributes that should be returned in
320       *                     matching entries.  It may be {@code null} or empty if
321       *                     the default attribute set (all user attributes) is to
322       *                     be requested.
323       */
324      public SearchRequest(final String baseDN, final SearchScope scope,
325                           final Filter filter, final String... attributes)
326      {
327        this(null, null, baseDN, scope, DereferencePolicy.NEVER, 0, 0, false,
328             filter, attributes);
329      }
330    
331    
332    
333      /**
334       * Creates a new search request with the provided information.
335       *
336       * @param  searchResultListener  The search result listener that should be
337       *                               used to return results to the client.  It may
338       *                               be {@code null} if the search results should
339       *                               be collected internally and returned in the
340       *                               {@code SearchResult} object.
341       * @param  baseDN                The base DN for the search request.  It must
342       *                               not be {@code null}.
343       * @param  scope                 The scope that specifies the range of entries
344       *                               that should be examined for the search.
345       * @param  filter                The string representation of the filter to
346       *                               use to identify matching entries.  It must
347       *                               not be {@code null}.
348       * @param  attributes            The set of attributes that should be returned
349       *                               in matching entries.  It may be {@code null}
350       *                               or empty if the default attribute set (all
351       *                               user attributes) is to be requested.
352       *
353       * @throws  LDAPException  If the provided filter string cannot be parsed as
354       *                         an LDAP filter.
355       */
356      public SearchRequest(final SearchResultListener searchResultListener,
357                           final String baseDN, final SearchScope scope,
358                           final String filter, final String... attributes)
359             throws LDAPException
360      {
361        this(searchResultListener, null, baseDN, scope, DereferencePolicy.NEVER, 0,
362             0, false, Filter.create(filter), attributes);
363      }
364    
365    
366    
367      /**
368       * Creates a new search request with the provided information.
369       *
370       * @param  searchResultListener  The search result listener that should be
371       *                               used to return results to the client.  It may
372       *                               be {@code null} if the search results should
373       *                               be collected internally and returned in the
374       *                               {@code SearchResult} object.
375       * @param  baseDN                The base DN for the search request.  It must
376       *                               not be {@code null}.
377       * @param  scope                 The scope that specifies the range of entries
378       *                               that should be examined for the search.
379       * @param  filter                The string representation of the filter to
380       *                               use to identify matching entries.  It must
381       *                               not be {@code null}.
382       * @param  attributes            The set of attributes that should be returned
383       *                               in matching entries.  It may be {@code null}
384       *                               or empty if the default attribute set (all
385       *                               user attributes) is to be requested.
386       */
387      public SearchRequest(final SearchResultListener searchResultListener,
388                           final String baseDN, final SearchScope scope,
389                           final Filter filter, final String... attributes)
390      {
391        this(searchResultListener, null, baseDN, scope, DereferencePolicy.NEVER, 0,
392             0, false, filter, attributes);
393      }
394    
395    
396    
397      /**
398       * Creates a new search request with the provided information.  Search result
399       * entries and references will be collected internally and included in the
400       * {@code SearchResult} object returned when search processing is completed.
401       *
402       * @param  baseDN       The base DN for the search request.  It must not be
403       *                      {@code null}.
404       * @param  scope        The scope that specifies the range of entries that
405       *                      should be examined for the search.
406       * @param  derefPolicy  The dereference policy the server should use for any
407       *                      aliases encountered while processing the search.
408       * @param  sizeLimit    The maximum number of entries that the server should
409       *                      return for the search.  A value of zero indicates that
410       *                      there should be no limit.
411       * @param  timeLimit    The maximum length of time in seconds that the server
412       *                      should spend processing this search request.  A value
413       *                      of zero indicates that there should be no limit.
414       * @param  typesOnly    Indicates whether to return only attribute names in
415       *                      matching entries, or both attribute names and values.
416       * @param  filter       The filter to use to identify matching entries.  It
417       *                      must not be {@code null}.
418       * @param  attributes   The set of attributes that should be returned in
419       *                      matching entries.  It may be {@code null} or empty if
420       *                      the default attribute set (all user attributes) is to
421       *                      be requested.
422       *
423       * @throws  LDAPException  If the provided filter string cannot be parsed as
424       *                         an LDAP filter.
425       */
426      public SearchRequest(final String baseDN, final SearchScope scope,
427                           final DereferencePolicy derefPolicy, final int sizeLimit,
428                           final int timeLimit, final boolean typesOnly,
429                           final String filter, final String... attributes)
430             throws LDAPException
431      {
432        this(null, null, baseDN, scope, derefPolicy, sizeLimit, timeLimit,
433             typesOnly, Filter.create(filter), attributes);
434      }
435    
436    
437    
438      /**
439       * Creates a new search request with the provided information.  Search result
440       * entries and references will be collected internally and included in the
441       * {@code SearchResult} object returned when search processing is completed.
442       *
443       * @param  baseDN       The base DN for the search request.  It must not be
444       *                      {@code null}.
445       * @param  scope        The scope that specifies the range of entries that
446       *                      should be examined for the search.
447       * @param  derefPolicy  The dereference policy the server should use for any
448       *                      aliases encountered while processing the search.
449       * @param  sizeLimit    The maximum number of entries that the server should
450       *                      return for the search.  A value of zero indicates that
451       *                      there should be no limit.
452       * @param  timeLimit    The maximum length of time in seconds that the server
453       *                      should spend processing this search request.  A value
454       *                      of zero indicates that there should be no limit.
455       * @param  typesOnly    Indicates whether to return only attribute names in
456       *                      matching entries, or both attribute names and values.
457       * @param  filter       The filter to use to identify matching entries.  It
458       *                      must not be {@code null}.
459       * @param  attributes   The set of attributes that should be returned in
460       *                      matching entries.  It may be {@code null} or empty if
461       *                      the default attribute set (all user attributes) is to
462       *                      be requested.
463       */
464      public SearchRequest(final String baseDN, final SearchScope scope,
465                           final DereferencePolicy derefPolicy, final int sizeLimit,
466                           final int timeLimit, final boolean typesOnly,
467                           final Filter filter, final String... attributes)
468      {
469        this(null, null, baseDN, scope, derefPolicy, sizeLimit, timeLimit,
470             typesOnly, filter, attributes);
471      }
472    
473    
474    
475      /**
476       * Creates a new search request with the provided information.
477       *
478       * @param  searchResultListener  The search result listener that should be
479       *                               used to return results to the client.  It may
480       *                               be {@code null} if the search results should
481       *                               be collected internally and returned in the
482       *                               {@code SearchResult} object.
483       * @param  baseDN                The base DN for the search request.  It must
484       *                               not be {@code null}.
485       * @param  scope                 The scope that specifies the range of entries
486       *                               that should be examined for the search.
487       * @param  derefPolicy           The dereference policy the server should use
488       *                               for any aliases encountered while processing
489       *                               the search.
490       * @param  sizeLimit             The maximum number of entries that the server
491       *                               should return for the search.  A value of
492       *                               zero indicates that there should be no limit.
493       * @param  timeLimit             The maximum length of time in seconds that
494       *                               the server should spend processing this
495       *                               search request.  A value of zero indicates
496       *                               that there should be no limit.
497       * @param  typesOnly             Indicates whether to return only attribute
498       *                               names in matching entries, or both attribute
499       *                               names and values.
500       * @param  filter                The filter to use to identify matching
501       *                               entries.  It must not be {@code null}.
502       * @param  attributes            The set of attributes that should be returned
503       *                               in matching entries.  It may be {@code null}
504       *                               or empty if the default attribute set (all
505       *                               user attributes) is to be requested.
506       *
507       * @throws  LDAPException  If the provided filter string cannot be parsed as
508       *                         an LDAP filter.
509       */
510      public SearchRequest(final SearchResultListener searchResultListener,
511                           final String baseDN, final SearchScope scope,
512                           final DereferencePolicy derefPolicy, final int sizeLimit,
513                           final int timeLimit, final boolean typesOnly,
514                           final String filter, final String... attributes)
515             throws LDAPException
516      {
517        this(searchResultListener, null, baseDN, scope, derefPolicy, sizeLimit,
518             timeLimit, typesOnly, Filter.create(filter), attributes);
519      }
520    
521    
522    
523      /**
524       * Creates a new search request with the provided information.
525       *
526       * @param  searchResultListener  The search result listener that should be
527       *                               used to return results to the client.  It may
528       *                               be {@code null} if the search results should
529       *                               be collected internally and returned in the
530       *                               {@code SearchResult} object.
531       * @param  baseDN                The base DN for the search request.  It must
532       *                               not be {@code null}.
533       * @param  scope                 The scope that specifies the range of entries
534       *                               that should be examined for the search.
535       * @param  derefPolicy           The dereference policy the server should use
536       *                               for any aliases encountered while processing
537       *                               the search.
538       * @param  sizeLimit             The maximum number of entries that the server
539       *                               should return for the search.  A value of
540       *                               zero indicates that there should be no limit.
541       * @param  timeLimit             The maximum length of time in seconds that
542       *                               the server should spend processing this
543       *                               search request.  A value of zero indicates
544       *                               that there should be no limit.
545       * @param  typesOnly             Indicates whether to return only attribute
546       *                               names in matching entries, or both attribute
547       *                               names and values.
548       * @param  filter                The filter to use to identify matching
549       *                               entries.  It must not be {@code null}.
550       * @param  attributes            The set of attributes that should be returned
551       *                               in matching entries.  It may be {@code null}
552       *                               or empty if the default attribute set (all
553       *                               user attributes) is to be requested.
554       */
555      public SearchRequest(final SearchResultListener searchResultListener,
556                           final String baseDN, final SearchScope scope,
557                           final DereferencePolicy derefPolicy, final int sizeLimit,
558                           final int timeLimit, final boolean typesOnly,
559                           final Filter filter, final String... attributes)
560      {
561        this(searchResultListener, null, baseDN, scope, derefPolicy, sizeLimit,
562             timeLimit, typesOnly, filter, attributes);
563      }
564    
565    
566    
567      /**
568       * Creates a new search request with the provided information.
569       *
570       * @param  searchResultListener  The search result listener that should be
571       *                               used to return results to the client.  It may
572       *                               be {@code null} if the search results should
573       *                               be collected internally and returned in the
574       *                               {@code SearchResult} object.
575       * @param  controls              The set of controls to include in the
576       *                               request.  It may be {@code null} or empty if
577       *                               no controls should be included in the
578       *                               request.
579       * @param  baseDN                The base DN for the search request.  It must
580       *                               not be {@code null}.
581       * @param  scope                 The scope that specifies the range of entries
582       *                               that should be examined for the search.
583       * @param  derefPolicy           The dereference policy the server should use
584       *                               for any aliases encountered while processing
585       *                               the search.
586       * @param  sizeLimit             The maximum number of entries that the server
587       *                               should return for the search.  A value of
588       *                               zero indicates that there should be no limit.
589       * @param  timeLimit             The maximum length of time in seconds that
590       *                               the server should spend processing this
591       *                               search request.  A value of zero indicates
592       *                               that there should be no limit.
593       * @param  typesOnly             Indicates whether to return only attribute
594       *                               names in matching entries, or both attribute
595       *                               names and values.
596       * @param  filter                The filter to use to identify matching
597       *                               entries.  It must not be {@code null}.
598       * @param  attributes            The set of attributes that should be returned
599       *                               in matching entries.  It may be {@code null}
600       *                               or empty if the default attribute set (all
601       *                               user attributes) is to be requested.
602       *
603       * @throws  LDAPException  If the provided filter string cannot be parsed as
604       *                         an LDAP filter.
605       */
606      public SearchRequest(final SearchResultListener searchResultListener,
607                           final Control[] controls, final String baseDN,
608                           final SearchScope scope,
609                           final DereferencePolicy derefPolicy, final int sizeLimit,
610                           final int timeLimit, final boolean typesOnly,
611                           final String filter, final String... attributes)
612             throws LDAPException
613      {
614        this(searchResultListener, controls, baseDN, scope, derefPolicy, sizeLimit,
615             timeLimit, typesOnly, Filter.create(filter), attributes);
616      }
617    
618    
619    
620      /**
621       * Creates a new search request with the provided information.
622       *
623       * @param  searchResultListener  The search result listener that should be
624       *                               used to return results to the client.  It may
625       *                               be {@code null} if the search results should
626       *                               be collected internally and returned in the
627       *                               {@code SearchResult} object.
628       * @param  controls              The set of controls to include in the
629       *                               request.  It may be {@code null} or empty if
630       *                               no controls should be included in the
631       *                               request.
632       * @param  baseDN                The base DN for the search request.  It must
633       *                               not be {@code null}.
634       * @param  scope                 The scope that specifies the range of entries
635       *                               that should be examined for the search.
636       * @param  derefPolicy           The dereference policy the server should use
637       *                               for any aliases encountered while processing
638       *                               the search.
639       * @param  sizeLimit             The maximum number of entries that the server
640       *                               should return for the search.  A value of
641       *                               zero indicates that there should be no limit.
642       * @param  timeLimit             The maximum length of time in seconds that
643       *                               the server should spend processing this
644       *                               search request.  A value of zero indicates
645       *                               that there should be no limit.
646       * @param  typesOnly             Indicates whether to return only attribute
647       *                               names in matching entries, or both attribute
648       *                               names and values.
649       * @param  filter                The filter to use to identify matching
650       *                               entries.  It must not be {@code null}.
651       * @param  attributes            The set of attributes that should be returned
652       *                               in matching entries.  It may be {@code null}
653       *                               or empty if the default attribute set (all
654       *                               user attributes) is to be requested.
655       */
656      public SearchRequest(final SearchResultListener searchResultListener,
657                           final Control[] controls, final String baseDN,
658                           final SearchScope scope,
659                           final DereferencePolicy derefPolicy, final int sizeLimit,
660                           final int timeLimit, final boolean typesOnly,
661                           final Filter filter, final String... attributes)
662      {
663        super(controls);
664    
665        ensureNotNull(baseDN, filter);
666    
667        this.baseDN               = baseDN;
668        this.scope                = scope;
669        this.derefPolicy          = derefPolicy;
670        this.typesOnly            = typesOnly;
671        this.filter               = filter;
672        this.searchResultListener = searchResultListener;
673    
674        if (sizeLimit < 0)
675        {
676          this.sizeLimit = 0;
677        }
678        else
679        {
680          this.sizeLimit = sizeLimit;
681        }
682    
683        if (timeLimit < 0)
684        {
685          this.timeLimit = 0;
686        }
687        else
688        {
689          this.timeLimit = timeLimit;
690        }
691    
692        if (attributes == null)
693        {
694          this.attributes = REQUEST_ATTRS_DEFAULT;
695        }
696        else
697        {
698          this.attributes = attributes;
699        }
700      }
701    
702    
703    
704      /**
705       * {@inheritDoc}
706       */
707      public String getBaseDN()
708      {
709        return baseDN;
710      }
711    
712    
713    
714      /**
715       * Specifies the base DN for this search request.
716       *
717       * @param  baseDN  The base DN for this search request.  It must not be
718       *                 {@code null}.
719       */
720      public void setBaseDN(final String baseDN)
721      {
722        ensureNotNull(baseDN);
723    
724        this.baseDN = baseDN;
725      }
726    
727    
728    
729      /**
730       * Specifies the base DN for this search request.
731       *
732       * @param  baseDN  The base DN for this search request.  It must not be
733       *                 {@code null}.
734       */
735      public void setBaseDN(final DN baseDN)
736      {
737        ensureNotNull(baseDN);
738    
739        this.baseDN = baseDN.toString();
740      }
741    
742    
743    
744      /**
745       * {@inheritDoc}
746       */
747      public SearchScope getScope()
748      {
749        return scope;
750      }
751    
752    
753    
754      /**
755       * Specifies the scope for this search request.
756       *
757       * @param  scope  The scope for this search request.
758       */
759      public void setScope(final SearchScope scope)
760      {
761        this.scope = scope;
762      }
763    
764    
765    
766      /**
767       * {@inheritDoc}
768       */
769      public DereferencePolicy getDereferencePolicy()
770      {
771        return derefPolicy;
772      }
773    
774    
775    
776      /**
777       * Specifies the dereference policy that should be used by the server for any
778       * aliases encountered during search processing.
779       *
780       * @param  derefPolicy  The dereference policy that should be used by the
781       *                      server for any aliases encountered during search
782       *                      processing.
783       */
784      public void setDerefPolicy(final DereferencePolicy derefPolicy)
785      {
786        this.derefPolicy = derefPolicy;
787      }
788    
789    
790    
791      /**
792       * {@inheritDoc}
793       */
794      public int getSizeLimit()
795      {
796        return sizeLimit;
797      }
798    
799    
800    
801      /**
802       * Specifies the maximum number of entries that should be returned by the
803       * server when processing this search request.  A value of zero indicates that
804       * there should be no limit.
805       * <BR><BR>
806       * Note that if an attempt to process a search operation fails because the
807       * size limit has been exceeded, an {@link LDAPSearchException} will be
808       * thrown.  If one or more entries or references have already been returned
809       * for the search, then the {@code LDAPSearchException} methods like
810       * {@code getEntryCount}, {@code getSearchEntries}, {@code getReferenceCount},
811       * and {@code getSearchReferences} may be used to obtain information about
812       * those entries and references (although if a search result listener was
813       * provided, then it will have been used to make any entries and references
814       * available, and they will not be available through the
815       * {@code getSearchEntries} and {@code getSearchReferences} methods).
816       *
817       * @param  sizeLimit  The maximum number of entries that should be returned by
818       *                    the server when processing this search request.
819       */
820      public void setSizeLimit(final int sizeLimit)
821      {
822        if (sizeLimit < 0)
823        {
824          this.sizeLimit = 0;
825        }
826        else
827        {
828          this.sizeLimit = sizeLimit;
829        }
830      }
831    
832    
833    
834      /**
835       * {@inheritDoc}
836       */
837      public int getTimeLimitSeconds()
838      {
839        return timeLimit;
840      }
841    
842    
843    
844      /**
845       * Specifies the maximum length of time in seconds that the server should
846       * spend processing this search request.  A value of zero indicates that there
847       * should be no limit.
848       * <BR><BR>
849       * Note that if an attempt to process a search operation fails because the
850       * time limit has been exceeded, an {@link LDAPSearchException} will be
851       * thrown.  If one or more entries or references have already been returned
852       * for the search, then the {@code LDAPSearchException} methods like
853       * {@code getEntryCount}, {@code getSearchEntries}, {@code getReferenceCount},
854       * and {@code getSearchReferences} may be used to obtain information about
855       * those entries and references (although if a search result listener was
856       * provided, then it will have been used to make any entries and references
857       * available, and they will not be available through the
858       * {@code getSearchEntries} and {@code getSearchReferences} methods).
859       *
860       * @param  timeLimit  The maximum length of time in seconds that the server
861       *                    should spend processing this search request.
862       */
863      public void setTimeLimitSeconds(final int timeLimit)
864      {
865        if (timeLimit < 0)
866        {
867          this.timeLimit = 0;
868        }
869        else
870        {
871          this.timeLimit = timeLimit;
872        }
873      }
874    
875    
876    
877      /**
878       * {@inheritDoc}
879       */
880      public boolean typesOnly()
881      {
882        return typesOnly;
883      }
884    
885    
886    
887      /**
888       * Specifies whether the server should return only attribute names in matching
889       * entries, rather than both names and values.
890       *
891       * @param  typesOnly  Specifies whether the server should return only
892       *                    attribute names in matching entries, rather than both
893       *                    names and values.
894       */
895      public void setTypesOnly(final boolean typesOnly)
896      {
897        this.typesOnly = typesOnly;
898      }
899    
900    
901    
902      /**
903       * {@inheritDoc}
904       */
905      public Filter getFilter()
906      {
907        return filter;
908      }
909    
910    
911    
912      /**
913       * Specifies the filter that should be used to identify matching entries.
914       *
915       * @param  filter  The string representation for the filter that should be
916       *                 used to identify matching entries.  It must not be
917       *                 {@code null}.
918       *
919       * @throws  LDAPException  If the provided filter string cannot be parsed as a
920       *                         search filter.
921       */
922      public void setFilter(final String filter)
923             throws LDAPException
924      {
925        ensureNotNull(filter);
926    
927        this.filter = Filter.create(filter);
928      }
929    
930    
931    
932      /**
933       * Specifies the filter that should be used to identify matching entries.
934       *
935       * @param  filter  The filter that should be used to identify matching
936       *                 entries.  It must not be {@code null}.
937       */
938      public void setFilter(final Filter filter)
939      {
940        ensureNotNull(filter);
941    
942        this.filter = filter;
943      }
944    
945    
946    
947      /**
948       * Retrieves the set of requested attributes to include in matching entries.
949       * The caller must not attempt to alter the contents of the array.
950       *
951       * @return  The set of requested attributes to include in matching entries, or
952       *          an empty array if the default set of attributes (all user
953       *          attributes but no operational attributes) should be requested.
954       */
955      public String[] getAttributes()
956      {
957        return attributes;
958      }
959    
960    
961    
962      /**
963       * {@inheritDoc}
964       */
965      public List<String> getAttributeList()
966      {
967        return Collections.unmodifiableList(Arrays.asList(attributes));
968      }
969    
970    
971    
972      /**
973       * Specifies the set of requested attributes to include in matching entries.
974       *
975       * @param  attributes  The set of requested attributes to include in matching
976       *                     entries.  It may be {@code null} if the default set of
977       *                     attributes (all user attributes but no operational
978       *                     attributes) should be requested.
979       */
980      public void setAttributes(final String... attributes)
981      {
982        if (attributes == null)
983        {
984          this.attributes = REQUEST_ATTRS_DEFAULT;
985        }
986        else
987        {
988          this.attributes = attributes;
989        }
990      }
991    
992    
993    
994      /**
995       * Specifies the set of requested attributes to include in matching entries.
996       *
997       * @param  attributes  The set of requested attributes to include in matching
998       *                     entries.  It may be {@code null} if the default set of
999       *                     attributes (all user attributes but no operational
1000       *                     attributes) should be requested.
1001       */
1002      public void setAttributes(final List<String> attributes)
1003      {
1004        if (attributes == null)
1005        {
1006          this.attributes = REQUEST_ATTRS_DEFAULT;
1007        }
1008        else
1009        {
1010          this.attributes = new String[attributes.size()];
1011          for (int i=0; i < this.attributes.length; i++)
1012          {
1013            this.attributes[i] = attributes.get(i);
1014          }
1015        }
1016      }
1017    
1018    
1019    
1020      /**
1021       * Retrieves the search result listener for this search request, if available.
1022       *
1023       * @return  The search result listener for this search request, or
1024       *          {@code null} if none has been configured.
1025       */
1026      public SearchResultListener getSearchResultListener()
1027      {
1028        return searchResultListener;
1029      }
1030    
1031    
1032    
1033      /**
1034       * {@inheritDoc}
1035       */
1036      public byte getProtocolOpType()
1037      {
1038        return LDAPMessage.PROTOCOL_OP_TYPE_SEARCH_REQUEST;
1039      }
1040    
1041    
1042    
1043      /**
1044       * {@inheritDoc}
1045       */
1046      public void writeTo(final ASN1Buffer writer)
1047      {
1048        final ASN1BufferSequence requestSequence =
1049             writer.beginSequence(LDAPMessage.PROTOCOL_OP_TYPE_SEARCH_REQUEST);
1050        writer.addOctetString(baseDN);
1051        writer.addEnumerated(scope.intValue());
1052        writer.addEnumerated(derefPolicy.intValue());
1053        writer.addInteger(sizeLimit);
1054        writer.addInteger(timeLimit);
1055        writer.addBoolean(typesOnly);
1056        filter.writeTo(writer);
1057    
1058        final ASN1BufferSequence attrSequence = writer.beginSequence();
1059        for (final String s : attributes)
1060        {
1061          writer.addOctetString(s);
1062        }
1063        attrSequence.end();
1064        requestSequence.end();
1065      }
1066    
1067    
1068    
1069      /**
1070       * Encodes the search request protocol op to an ASN.1 element.
1071       *
1072       * @return  The ASN.1 element with the encoded search request protocol op.
1073       */
1074      public ASN1Element encodeProtocolOp()
1075      {
1076        // Create the search request protocol op.
1077        final ASN1Element[] attrElements = new ASN1Element[attributes.length];
1078        for (int i=0; i < attrElements.length; i++)
1079        {
1080          attrElements[i] = new ASN1OctetString(attributes[i]);
1081        }
1082    
1083        final ASN1Element[] protocolOpElements =
1084        {
1085          new ASN1OctetString(baseDN),
1086          new ASN1Enumerated(scope.intValue()),
1087          new ASN1Enumerated(derefPolicy.intValue()),
1088          new ASN1Integer(sizeLimit),
1089          new ASN1Integer(timeLimit),
1090          new ASN1Boolean(typesOnly),
1091          filter.encode(),
1092          new ASN1Sequence(attrElements)
1093        };
1094    
1095        return new ASN1Sequence(LDAPMessage.PROTOCOL_OP_TYPE_SEARCH_REQUEST,
1096                                protocolOpElements);
1097      }
1098    
1099    
1100    
1101      /**
1102       * Sends this search request to the directory server over the provided
1103       * connection and returns the associated response.  The search result entries
1104       * and references will either be collected and returned in the
1105       * {@code SearchResult} object that is returned, or will be interactively
1106       * returned via the {@code SearchResultListener} interface.
1107       *
1108       * @param  connection  The connection to use to communicate with the directory
1109       *                     server.
1110       * @param  depth       The current referral depth for this request.  It should
1111       *                     always be one for the initial request, and should only
1112       *                     be incremented when following referrals.
1113       *
1114       * @return  An object that provides information about the result of the
1115       *          search processing, potentially including the sets of matching
1116       *          entries and/or search references.
1117       *
1118       * @throws  LDAPException  If a problem occurs while sending the request or
1119       *                         reading the response.
1120       */
1121      @Override()
1122      protected SearchResult process(final LDAPConnection connection,
1123                                     final int depth)
1124                throws LDAPException
1125      {
1126        if (connection.synchronousMode())
1127        {
1128          @SuppressWarnings("deprecation")
1129          final boolean autoReconnect =
1130               connection.getConnectionOptions().autoReconnect();
1131          return processSync(connection, depth, autoReconnect);
1132        }
1133    
1134        final long requestTime = System.nanoTime();
1135        processAsync(connection, null);
1136    
1137        try
1138        {
1139          // Wait for and process the response.
1140          final ArrayList<SearchResultEntry> entryList;
1141          final ArrayList<SearchResultReference> referenceList;
1142          if (searchResultListener == null)
1143          {
1144            entryList     = new ArrayList<SearchResultEntry>(5);
1145            referenceList = new ArrayList<SearchResultReference>(5);
1146          }
1147          else
1148          {
1149            entryList     = null;
1150            referenceList = null;
1151          }
1152    
1153          int numEntries    = 0;
1154          int numReferences = 0;
1155          ResultCode intermediateResultCode = ResultCode.SUCCESS;
1156          final long responseTimeout = getResponseTimeoutMillis(connection);
1157          while (true)
1158          {
1159            final LDAPResponse response;
1160            try
1161            {
1162              if (responseTimeout > 0)
1163              {
1164                response =
1165                     responseQueue.poll(responseTimeout, TimeUnit.MILLISECONDS);
1166              }
1167              else
1168              {
1169                response = responseQueue.take();
1170              }
1171            }
1172            catch (InterruptedException ie)
1173            {
1174              debugException(ie);
1175              Thread.currentThread().interrupt();
1176              throw new LDAPException(ResultCode.LOCAL_ERROR,
1177                   ERR_SEARCH_INTERRUPTED.get(connection.getHostPort()), ie);
1178            }
1179    
1180            if (response == null)
1181            {
1182              if (connection.getConnectionOptions().abandonOnTimeout())
1183              {
1184                connection.abandon(messageID);
1185              }
1186    
1187              final SearchResult searchResult =
1188                   new SearchResult(messageID, ResultCode.TIMEOUT,
1189                        ERR_SEARCH_CLIENT_TIMEOUT.get(responseTimeout, messageID,
1190                             baseDN, scope.getName(), filter.toString(),
1191                             connection.getHostPort()),
1192                        null, null, entryList, referenceList, numEntries,
1193                        numReferences, null);
1194              throw new LDAPSearchException(searchResult);
1195            }
1196    
1197            if (response instanceof ConnectionClosedResponse)
1198            {
1199              final ConnectionClosedResponse ccr =
1200                   (ConnectionClosedResponse) response;
1201              final String message = ccr.getMessage();
1202              if (message == null)
1203              {
1204                // The connection was closed while waiting for the response.
1205                final SearchResult searchResult =
1206                     new SearchResult(messageID, ccr.getResultCode(),
1207                          ERR_CONN_CLOSED_WAITING_FOR_SEARCH_RESPONSE.get(
1208                               connection.getHostPort(), toString()),
1209                          null, null, entryList, referenceList, numEntries,
1210                          numReferences, null);
1211                throw new LDAPSearchException(searchResult);
1212              }
1213              else
1214              {
1215                // The connection was closed while waiting for the response.
1216                final SearchResult searchResult =
1217                     new SearchResult(messageID, ccr.getResultCode(),
1218                          ERR_CONN_CLOSED_WAITING_FOR_SEARCH_RESPONSE_WITH_MESSAGE.
1219                               get(connection.getHostPort(), toString(), message),
1220                          null, null, entryList, referenceList, numEntries,
1221                          numReferences, null);
1222                throw new LDAPSearchException(searchResult);
1223              }
1224            }
1225            else if (response instanceof SearchResultEntry)
1226            {
1227              final SearchResultEntry searchEntry = (SearchResultEntry) response;
1228              numEntries++;
1229              if (searchResultListener == null)
1230              {
1231                entryList.add(searchEntry);
1232              }
1233              else
1234              {
1235                searchResultListener.searchEntryReturned(searchEntry);
1236              }
1237            }
1238            else if (response instanceof SearchResultReference)
1239            {
1240              final SearchResultReference searchReference =
1241                   (SearchResultReference) response;
1242              if (followReferrals(connection))
1243              {
1244                final LDAPResult result = followSearchReference(messageID,
1245                     searchReference, connection, depth);
1246                if (! result.getResultCode().equals(ResultCode.SUCCESS))
1247                {
1248                  // We couldn't follow the reference.  We don't want to fail the
1249                  // entire search because of this right now, so treat it as if
1250                  // referral following had not been enabled.  Also, set the
1251                  // intermediate result code to match that of the result.
1252                  numReferences++;
1253                  if (searchResultListener == null)
1254                  {
1255                    referenceList.add(searchReference);
1256                  }
1257                  else
1258                  {
1259                    searchResultListener.searchReferenceReturned(searchReference);
1260                  }
1261    
1262                  if (intermediateResultCode.equals(ResultCode.SUCCESS))
1263                  {
1264                    intermediateResultCode = result.getResultCode();
1265                  }
1266                }
1267                else if (result instanceof SearchResult)
1268                {
1269                  final SearchResult searchResult = (SearchResult) result;
1270                  numEntries += searchResult.getEntryCount();
1271                  if (searchResultListener == null)
1272                  {
1273                    entryList.addAll(searchResult.getSearchEntries());
1274                  }
1275                }
1276              }
1277              else
1278              {
1279                numReferences++;
1280                if (searchResultListener == null)
1281                {
1282                  referenceList.add(searchReference);
1283                }
1284                else
1285                {
1286                  searchResultListener.searchReferenceReturned(searchReference);
1287                }
1288              }
1289            }
1290            else
1291            {
1292              connection.getConnectionStatistics().incrementNumSearchResponses(
1293                   numEntries, numReferences,
1294                   (System.nanoTime() - requestTime));
1295              SearchResult result = (SearchResult) response;
1296              result.setCounts(numEntries, entryList, numReferences, referenceList);
1297    
1298              if ((result.getResultCode().equals(ResultCode.REFERRAL)) &&
1299                  followReferrals(connection))
1300              {
1301                if (depth >=
1302                    connection.getConnectionOptions().getReferralHopLimit())
1303                {
1304                  return new SearchResult(messageID,
1305                                          ResultCode.REFERRAL_LIMIT_EXCEEDED,
1306                                          ERR_TOO_MANY_REFERRALS.get(),
1307                                          result.getMatchedDN(),
1308                                          result.getReferralURLs(), entryList,
1309                                          referenceList, numEntries,
1310                                          numReferences,
1311                                          result.getResponseControls());
1312                }
1313    
1314                result = followReferral(result, connection, depth);
1315              }
1316    
1317              if ((result.getResultCode().equals(ResultCode.SUCCESS)) &&
1318                  (! intermediateResultCode.equals(ResultCode.SUCCESS)))
1319              {
1320                return new SearchResult(messageID, intermediateResultCode,
1321                                        result.getDiagnosticMessage(),
1322                                        result.getMatchedDN(),
1323                                        result.getReferralURLs(),
1324                                        entryList, referenceList, numEntries,
1325                                        numReferences,
1326                                        result.getResponseControls());
1327              }
1328    
1329              return result;
1330            }
1331          }
1332        }
1333        finally
1334        {
1335          connection.deregisterResponseAcceptor(messageID);
1336        }
1337      }
1338    
1339    
1340    
1341      /**
1342       * Sends this search request to the directory server over the provided
1343       * connection and returns the message ID for the request.
1344       *
1345       * @param  connection      The connection to use to communicate with the
1346       *                         directory server.
1347       * @param  resultListener  The async result listener that is to be notified
1348       *                         when the response is received.  It may be
1349       *                         {@code null} only if the result is to be processed
1350       *                         by this class.
1351       *
1352       * @return  The async request ID created for the operation, or {@code null} if
1353       *          the provided {@code resultListener} is {@code null} and the
1354       *          operation will not actually be processed asynchronously.
1355       *
1356       * @throws  LDAPException  If a problem occurs while sending the request.
1357       */
1358      AsyncRequestID processAsync(final LDAPConnection connection,
1359                                  final AsyncSearchResultListener resultListener)
1360                     throws LDAPException
1361      {
1362        // Create the LDAP message.
1363        messageID = connection.nextMessageID();
1364        final LDAPMessage message = new LDAPMessage(messageID, this, getControls());
1365    
1366    
1367        // If the provided async result listener is {@code null}, then we'll use
1368        // this class as the message acceptor.  Otherwise, create an async helper
1369        // and use it as the message acceptor.
1370        final AsyncRequestID asyncRequestID;
1371        if (resultListener == null)
1372        {
1373          asyncRequestID = null;
1374          connection.registerResponseAcceptor(messageID, this);
1375        }
1376        else
1377        {
1378          final AsyncSearchHelper helper = new AsyncSearchHelper(connection,
1379               messageID, resultListener, getIntermediateResponseListener());
1380          connection.registerResponseAcceptor(messageID, helper);
1381          asyncRequestID = helper.getAsyncRequestID();
1382    
1383          final long timeout = getResponseTimeoutMillis(connection);
1384          if (timeout > 0L)
1385          {
1386            final Timer timer = connection.getTimer();
1387            final AsyncTimeoutTimerTask timerTask =
1388                 new AsyncTimeoutTimerTask(helper);
1389            timer.schedule(timerTask, timeout);
1390            asyncRequestID.setTimerTask(timerTask);
1391          }
1392        }
1393    
1394    
1395        // Send the request to the server.
1396        try
1397        {
1398          debugLDAPRequest(this);
1399          connection.getConnectionStatistics().incrementNumSearchRequests();
1400          connection.sendMessage(message);
1401          return asyncRequestID;
1402        }
1403        catch (LDAPException le)
1404        {
1405          debugException(le);
1406    
1407          connection.deregisterResponseAcceptor(messageID);
1408          throw le;
1409        }
1410      }
1411    
1412    
1413    
1414      /**
1415       * Processes this search operation in synchronous mode, in which the same
1416       * thread will send the request and read the response.
1417       *
1418       * @param  connection  The connection to use to communicate with the directory
1419       *                     server.
1420       * @param  depth       The current referral depth for this request.  It should
1421       *                     always be one for the initial request, and should only
1422       *                     be incremented when following referrals.
1423       * @param  allowRetry  Indicates whether the request may be re-tried on a
1424       *                     re-established connection if the initial attempt fails
1425       *                     in a way that indicates the connection is no longer
1426       *                     valid and autoReconnect is true.
1427       *
1428       * @return  An LDAP result object that provides information about the result
1429       *          of the search processing.
1430       *
1431       * @throws  LDAPException  If a problem occurs while sending the request or
1432       *                         reading the response.
1433       */
1434      private SearchResult processSync(final LDAPConnection connection,
1435                                       final int depth, final boolean allowRetry)
1436              throws LDAPException
1437      {
1438        // Create the LDAP message.
1439        messageID = connection.nextMessageID();
1440        final LDAPMessage message =
1441             new LDAPMessage(messageID,  this, getControls());
1442    
1443    
1444        // Set the appropriate timeout on the socket.
1445        final long responseTimeout = getResponseTimeoutMillis(connection);
1446        try
1447        {
1448          connection.getConnectionInternals(true).getSocket().setSoTimeout(
1449               (int) responseTimeout);
1450        }
1451        catch (Exception e)
1452        {
1453          debugException(e);
1454        }
1455    
1456    
1457        // Send the request to the server.
1458        final long requestTime = System.nanoTime();
1459        debugLDAPRequest(this);
1460        connection.getConnectionStatistics().incrementNumSearchRequests();
1461        try
1462        {
1463          connection.sendMessage(message);
1464        }
1465        catch (final LDAPException le)
1466        {
1467          debugException(le);
1468    
1469          if (allowRetry)
1470          {
1471            final SearchResult retryResult = reconnectAndRetry(connection, depth,
1472                 le.getResultCode(), 0, 0);
1473            if (retryResult != null)
1474            {
1475              return retryResult;
1476            }
1477          }
1478    
1479          throw le;
1480        }
1481    
1482        final ArrayList<SearchResultEntry> entryList;
1483        final ArrayList<SearchResultReference> referenceList;
1484        if (searchResultListener == null)
1485        {
1486          entryList     = new ArrayList<SearchResultEntry>(5);
1487          referenceList = new ArrayList<SearchResultReference>(5);
1488        }
1489        else
1490        {
1491          entryList     = null;
1492          referenceList = null;
1493        }
1494    
1495        int numEntries    = 0;
1496        int numReferences = 0;
1497        ResultCode intermediateResultCode = ResultCode.SUCCESS;
1498        while (true)
1499        {
1500          final LDAPResponse response;
1501          try
1502          {
1503            response = connection.readResponse(messageID);
1504          }
1505          catch (final LDAPException le)
1506          {
1507            debugException(le);
1508    
1509            if ((le.getResultCode() == ResultCode.TIMEOUT) &&
1510                connection.getConnectionOptions().abandonOnTimeout())
1511            {
1512              connection.abandon(messageID);
1513            }
1514    
1515            if (allowRetry)
1516            {
1517              final SearchResult retryResult = reconnectAndRetry(connection, depth,
1518                   le.getResultCode(), numEntries, numReferences);
1519              if (retryResult != null)
1520              {
1521                return retryResult;
1522              }
1523            }
1524    
1525            throw le;
1526          }
1527    
1528          if (response == null)
1529          {
1530            if (connection.getConnectionOptions().abandonOnTimeout())
1531            {
1532              connection.abandon(messageID);
1533            }
1534    
1535            throw new LDAPException(ResultCode.TIMEOUT,
1536                 ERR_SEARCH_CLIENT_TIMEOUT.get(responseTimeout, messageID, baseDN,
1537                      scope.getName(), filter.toString(),
1538                      connection.getHostPort()));
1539          }
1540          else if (response instanceof ConnectionClosedResponse)
1541          {
1542    
1543            if (allowRetry)
1544            {
1545              final SearchResult retryResult = reconnectAndRetry(connection, depth,
1546                   ResultCode.SERVER_DOWN, numEntries, numReferences);
1547              if (retryResult != null)
1548              {
1549                return retryResult;
1550              }
1551            }
1552    
1553            final ConnectionClosedResponse ccr =
1554                 (ConnectionClosedResponse) response;
1555            final String msg = ccr.getMessage();
1556            if (msg == null)
1557            {
1558              // The connection was closed while waiting for the response.
1559              final SearchResult searchResult =
1560                   new SearchResult(messageID, ccr.getResultCode(),
1561                        ERR_CONN_CLOSED_WAITING_FOR_SEARCH_RESPONSE.get(
1562                             connection.getHostPort(), toString()),
1563                        null, null, entryList, referenceList, numEntries,
1564                        numReferences, null);
1565              throw new LDAPSearchException(searchResult);
1566            }
1567            else
1568            {
1569              // The connection was closed while waiting for the response.
1570              final SearchResult searchResult =
1571                   new SearchResult(messageID, ccr.getResultCode(),
1572                        ERR_CONN_CLOSED_WAITING_FOR_SEARCH_RESPONSE_WITH_MESSAGE.
1573                             get(connection.getHostPort(), toString(), msg),
1574                        null, null, entryList, referenceList, numEntries,
1575                        numReferences, null);
1576              throw new LDAPSearchException(searchResult);
1577            }
1578          }
1579          else if (response instanceof IntermediateResponse)
1580          {
1581            final IntermediateResponseListener listener =
1582                 getIntermediateResponseListener();
1583            if (listener != null)
1584            {
1585              listener.intermediateResponseReturned(
1586                   (IntermediateResponse) response);
1587            }
1588          }
1589          else if (response instanceof SearchResultEntry)
1590          {
1591            final SearchResultEntry searchEntry = (SearchResultEntry) response;
1592            numEntries++;
1593            if (searchResultListener == null)
1594            {
1595              entryList.add(searchEntry);
1596            }
1597            else
1598            {
1599              searchResultListener.searchEntryReturned(searchEntry);
1600            }
1601          }
1602          else if (response instanceof SearchResultReference)
1603          {
1604            final SearchResultReference searchReference =
1605                 (SearchResultReference) response;
1606            if (followReferrals(connection))
1607            {
1608              final LDAPResult result = followSearchReference(messageID,
1609                   searchReference, connection, depth);
1610              if (! result.getResultCode().equals(ResultCode.SUCCESS))
1611              {
1612                // We couldn't follow the reference.  We don't want to fail the
1613                // entire search because of this right now, so treat it as if
1614                // referral following had not been enabled.  Also, set the
1615                // intermediate result code to match that of the result.
1616                numReferences++;
1617                if (searchResultListener == null)
1618                {
1619                  referenceList.add(searchReference);
1620                }
1621                else
1622                {
1623                  searchResultListener.searchReferenceReturned(searchReference);
1624                }
1625    
1626                if (intermediateResultCode.equals(ResultCode.SUCCESS))
1627                {
1628                  intermediateResultCode = result.getResultCode();
1629                }
1630              }
1631              else if (result instanceof SearchResult)
1632              {
1633                final SearchResult searchResult = (SearchResult) result;
1634                numEntries += searchResult.getEntryCount();
1635                if (searchResultListener == null)
1636                {
1637                  entryList.addAll(searchResult.getSearchEntries());
1638                }
1639              }
1640            }
1641            else
1642            {
1643              numReferences++;
1644              if (searchResultListener == null)
1645              {
1646                referenceList.add(searchReference);
1647              }
1648              else
1649              {
1650                searchResultListener.searchReferenceReturned(searchReference);
1651              }
1652            }
1653          }
1654          else
1655          {
1656            final SearchResult result = (SearchResult) response;
1657            if (allowRetry)
1658            {
1659              final SearchResult retryResult = reconnectAndRetry(connection,
1660                   depth, result.getResultCode(), numEntries, numReferences);
1661              if (retryResult != null)
1662              {
1663                return retryResult;
1664              }
1665            }
1666    
1667            return handleResponse(connection, response, requestTime, depth,
1668                                  numEntries, numReferences, entryList,
1669                                  referenceList, intermediateResultCode);
1670          }
1671        }
1672      }
1673    
1674    
1675    
1676      /**
1677       * Attempts to re-establish the connection and retry processing this request
1678       * on it.
1679       *
1680       * @param  connection     The connection to be re-established.
1681       * @param  depth          The current referral depth for this request.  It
1682       *                        should always be one for the initial request, and
1683       *                        should only be incremented when following referrals.
1684       * @param  resultCode     The result code for the previous operation attempt.
1685       * @param  numEntries     The number of search result entries already sent for
1686       *                        the search operation.
1687       * @param  numReferences  The number of search result references already sent
1688       *                        for the search operation.
1689       *
1690       * @return  The result from re-trying the search, or {@code null} if it could
1691       *          not be re-tried.
1692       */
1693      private SearchResult reconnectAndRetry(final LDAPConnection connection,
1694                                             final int depth,
1695                                             final ResultCode resultCode,
1696                                             final int numEntries,
1697                                             final int numReferences)
1698      {
1699        try
1700        {
1701          // We will only want to retry for certain result codes that indicate a
1702          // connection problem.
1703          switch (resultCode.intValue())
1704          {
1705            case ResultCode.SERVER_DOWN_INT_VALUE:
1706            case ResultCode.DECODING_ERROR_INT_VALUE:
1707            case ResultCode.CONNECT_ERROR_INT_VALUE:
1708              // We want to try to re-establish the connection no matter what, but
1709              // we only want to retry the search if we haven't yet sent any
1710              // results.
1711              connection.reconnect();
1712              if ((numEntries == 0) && (numReferences == 0))
1713              {
1714                return processSync(connection, depth, false);
1715              }
1716              break;
1717          }
1718        }
1719        catch (final Exception e)
1720        {
1721          debugException(e);
1722        }
1723    
1724        return null;
1725      }
1726    
1727    
1728    
1729      /**
1730       * Performs the necessary processing for handling a response.
1731       *
1732       * @param  connection              The connection used to read the response.
1733       * @param  response                The response to be processed.
1734       * @param  requestTime             The time the request was sent to the
1735       *                                 server.
1736       * @param  depth                   The current referral depth for this
1737       *                                 request.  It should always be one for the
1738       *                                 initial request, and should only be
1739       *                                 incremented when following referrals.
1740       * @param  numEntries              The number of entries received from the
1741       *                                 server.
1742       * @param  numReferences           The number of references received from
1743       *                                 the server.
1744       * @param  entryList               The list of search result entries received
1745       *                                 from the server, if applicable.
1746       * @param  referenceList           The list of search result references
1747       *                                 received from the server, if applicable.
1748       * @param  intermediateResultCode  The intermediate result code so far for the
1749       *                                 search operation.
1750       *
1751       * @return  The search result.
1752       *
1753       * @throws  LDAPException  If a problem occurs.
1754       */
1755      private SearchResult handleResponse(final LDAPConnection connection,
1756                   final LDAPResponse response, final long requestTime,
1757                   final int depth, final int numEntries, final int numReferences,
1758                   final List<SearchResultEntry> entryList,
1759                   final List<SearchResultReference> referenceList,
1760                   final ResultCode intermediateResultCode)
1761              throws LDAPException
1762      {
1763        connection.getConnectionStatistics().incrementNumSearchResponses(
1764             numEntries, numReferences,
1765             (System.nanoTime() - requestTime));
1766        SearchResult result = (SearchResult) response;
1767        result.setCounts(numEntries, entryList, numReferences, referenceList);
1768    
1769        if ((result.getResultCode().equals(ResultCode.REFERRAL)) &&
1770            followReferrals(connection))
1771        {
1772          if (depth >=
1773              connection.getConnectionOptions().getReferralHopLimit())
1774          {
1775            return new SearchResult(messageID,
1776                                    ResultCode.REFERRAL_LIMIT_EXCEEDED,
1777                                    ERR_TOO_MANY_REFERRALS.get(),
1778                                    result.getMatchedDN(),
1779                                    result.getReferralURLs(), entryList,
1780                                    referenceList, numEntries,
1781                                    numReferences,
1782                                    result.getResponseControls());
1783          }
1784    
1785          result = followReferral(result, connection, depth);
1786        }
1787    
1788        if ((result.getResultCode().equals(ResultCode.SUCCESS)) &&
1789            (! intermediateResultCode.equals(ResultCode.SUCCESS)))
1790        {
1791          return new SearchResult(messageID, intermediateResultCode,
1792                                  result.getDiagnosticMessage(),
1793                                  result.getMatchedDN(),
1794                                  result.getReferralURLs(),
1795                                  entryList, referenceList, numEntries,
1796                                  numReferences,
1797                                  result.getResponseControls());
1798        }
1799    
1800        return result;
1801      }
1802    
1803    
1804    
1805      /**
1806       * Attempts to follow a search result reference to continue a search in a
1807       * remote server.
1808       *
1809       * @param  messageID        The message ID for the LDAP message that is
1810       *                          associated with this result.
1811       * @param  searchReference  The search result reference to follow.
1812       * @param  connection       The connection on which the reference was
1813       *                          received.
1814       * @param  depth            The number of referrals followed in the course of
1815       *                          processing this request.
1816       *
1817       * @return  The result of attempting to follow the search result reference.
1818       *
1819       * @throws  LDAPException  If a problem occurs while attempting to establish
1820       *                         the referral connection, sending the request, or
1821       *                         reading the result.
1822       */
1823      private LDAPResult followSearchReference(final int messageID,
1824                              final SearchResultReference searchReference,
1825                              final LDAPConnection connection, final int depth)
1826              throws LDAPException
1827      {
1828        for (final String urlString : searchReference.getReferralURLs())
1829        {
1830          try
1831          {
1832            final LDAPURL referralURL = new LDAPURL(urlString);
1833            final String host = referralURL.getHost();
1834    
1835            if (host == null)
1836            {
1837              // We can't handle a referral in which there is no host.
1838              continue;
1839            }
1840    
1841            final String requestBaseDN;
1842            if (referralURL.baseDNProvided())
1843            {
1844              requestBaseDN = referralURL.getBaseDN().toString();
1845            }
1846            else
1847            {
1848              requestBaseDN = baseDN;
1849            }
1850    
1851            final SearchScope requestScope;
1852            if (referralURL.scopeProvided())
1853            {
1854              requestScope = referralURL.getScope();
1855            }
1856            else
1857            {
1858              requestScope = scope;
1859            }
1860    
1861            final Filter requestFilter;
1862            if (referralURL.filterProvided())
1863            {
1864              requestFilter = referralURL.getFilter();
1865            }
1866            else
1867            {
1868              requestFilter = filter;
1869            }
1870    
1871    
1872            final SearchRequest searchRequest =
1873                 new SearchRequest(searchResultListener, getControls(),
1874                                   requestBaseDN, requestScope, derefPolicy,
1875                                   sizeLimit, timeLimit, typesOnly, requestFilter,
1876                                   attributes);
1877    
1878            final LDAPConnection referralConn = connection.getReferralConnector().
1879                 getReferralConnection(referralURL, connection);
1880    
1881            try
1882            {
1883              return searchRequest.process(referralConn, depth+1);
1884            }
1885            finally
1886            {
1887              referralConn.setDisconnectInfo(DisconnectType.REFERRAL, null, null);
1888              referralConn.close();
1889            }
1890          }
1891          catch (LDAPException le)
1892          {
1893            debugException(le);
1894    
1895            if (le.getResultCode().equals(ResultCode.REFERRAL_LIMIT_EXCEEDED))
1896            {
1897              throw le;
1898            }
1899          }
1900        }
1901    
1902        // If we've gotten here, then we could not follow any of the referral URLs,
1903        // so we'll create a failure result.
1904        return new SearchResult(messageID, ResultCode.REFERRAL, null, null,
1905                                searchReference.getReferralURLs(), 0, 0, null);
1906      }
1907    
1908    
1909    
1910      /**
1911       * Attempts to follow a referral to perform an add operation in the target
1912       * server.
1913       *
1914       * @param  referralResult  The LDAP result object containing information about
1915       *                         the referral to follow.
1916       * @param  connection      The connection on which the referral was received.
1917       * @param  depth           The number of referrals followed in the course of
1918       *                         processing this request.
1919       *
1920       * @return  The result of attempting to process the add operation by following
1921       *          the referral.
1922       *
1923       * @throws  LDAPException  If a problem occurs while attempting to establish
1924       *                         the referral connection, sending the request, or
1925       *                         reading the result.
1926       */
1927      private SearchResult followReferral(final SearchResult referralResult,
1928                                          final LDAPConnection connection,
1929                                          final int depth)
1930              throws LDAPException
1931      {
1932        for (final String urlString : referralResult.getReferralURLs())
1933        {
1934          try
1935          {
1936            final LDAPURL referralURL = new LDAPURL(urlString);
1937            final String host = referralURL.getHost();
1938    
1939            if (host == null)
1940            {
1941              // We can't handle a referral in which there is no host.
1942              continue;
1943            }
1944    
1945            final String requestBaseDN;
1946            if (referralURL.baseDNProvided())
1947            {
1948              requestBaseDN = referralURL.getBaseDN().toString();
1949            }
1950            else
1951            {
1952              requestBaseDN = baseDN;
1953            }
1954    
1955            final SearchScope requestScope;
1956            if (referralURL.scopeProvided())
1957            {
1958              requestScope = referralURL.getScope();
1959            }
1960            else
1961            {
1962              requestScope = scope;
1963            }
1964    
1965            final Filter requestFilter;
1966            if (referralURL.filterProvided())
1967            {
1968              requestFilter = referralURL.getFilter();
1969            }
1970            else
1971            {
1972              requestFilter = filter;
1973            }
1974    
1975    
1976            final SearchRequest searchRequest =
1977                 new SearchRequest(searchResultListener, getControls(),
1978                                   requestBaseDN, requestScope, derefPolicy,
1979                                   sizeLimit, timeLimit, typesOnly, requestFilter,
1980                                   attributes);
1981    
1982            final LDAPConnection referralConn = connection.getReferralConnector().
1983                 getReferralConnection(referralURL, connection);
1984            try
1985            {
1986              return searchRequest.process(referralConn, depth+1);
1987            }
1988            finally
1989            {
1990              referralConn.setDisconnectInfo(DisconnectType.REFERRAL, null, null);
1991              referralConn.close();
1992            }
1993          }
1994          catch (LDAPException le)
1995          {
1996            debugException(le);
1997    
1998            if (le.getResultCode().equals(ResultCode.REFERRAL_LIMIT_EXCEEDED))
1999            {
2000              throw le;
2001            }
2002          }
2003        }
2004    
2005        // If we've gotten here, then we could not follow any of the referral URLs,
2006        // so we'll just return the original referral result.
2007        return referralResult;
2008      }
2009    
2010    
2011    
2012      /**
2013       * {@inheritDoc}
2014       */
2015      @InternalUseOnly()
2016      public void responseReceived(final LDAPResponse response)
2017             throws LDAPException
2018      {
2019        try
2020        {
2021          responseQueue.put(response);
2022        }
2023        catch (Exception e)
2024        {
2025          debugException(e);
2026    
2027          if (e instanceof InterruptedException)
2028          {
2029            Thread.currentThread().interrupt();
2030          }
2031    
2032          throw new LDAPException(ResultCode.LOCAL_ERROR,
2033               ERR_EXCEPTION_HANDLING_RESPONSE.get(getExceptionMessage(e)), e);
2034        }
2035      }
2036    
2037    
2038    
2039      /**
2040       * {@inheritDoc}
2041       */
2042      @Override()
2043      public int getLastMessageID()
2044      {
2045        return messageID;
2046      }
2047    
2048    
2049    
2050      /**
2051       * {@inheritDoc}
2052       */
2053      @Override()
2054      public OperationType getOperationType()
2055      {
2056        return OperationType.SEARCH;
2057      }
2058    
2059    
2060    
2061      /**
2062       * {@inheritDoc}
2063       */
2064      public SearchRequest duplicate()
2065      {
2066        return duplicate(getControls());
2067      }
2068    
2069    
2070    
2071      /**
2072       * {@inheritDoc}
2073       */
2074      public SearchRequest duplicate(final Control[] controls)
2075      {
2076        final SearchRequest r = new SearchRequest(searchResultListener, controls,
2077             baseDN, scope, derefPolicy, sizeLimit, timeLimit, typesOnly, filter,
2078             attributes);
2079        if (followReferralsInternal() != null)
2080        {
2081          r.setFollowReferrals(followReferralsInternal());
2082        }
2083    
2084        r.setResponseTimeoutMillis(getResponseTimeoutMillis(null));
2085    
2086        return r;
2087      }
2088    
2089    
2090    
2091      /**
2092       * {@inheritDoc}
2093       */
2094      @Override()
2095      public void toString(final StringBuilder buffer)
2096      {
2097        buffer.append("SearchRequest(baseDN='");
2098        buffer.append(baseDN);
2099        buffer.append("', scope=");
2100        buffer.append(scope);
2101        buffer.append(", deref=");
2102        buffer.append(derefPolicy);
2103        buffer.append(", sizeLimit=");
2104        buffer.append(sizeLimit);
2105        buffer.append(", timeLimit=");
2106        buffer.append(timeLimit);
2107        buffer.append(", filter='");
2108        buffer.append(filter);
2109        buffer.append("', attrs={");
2110    
2111        for (int i=0; i < attributes.length; i++)
2112        {
2113          if (i > 0)
2114          {
2115            buffer.append(", ");
2116          }
2117    
2118          buffer.append(attributes[i]);
2119        }
2120        buffer.append('}');
2121    
2122        final Control[] controls = getControls();
2123        if (controls.length > 0)
2124        {
2125          buffer.append(", controls={");
2126          for (int i=0; i < controls.length; i++)
2127          {
2128            if (i > 0)
2129            {
2130              buffer.append(", ");
2131            }
2132    
2133            buffer.append(controls[i]);
2134          }
2135          buffer.append('}');
2136        }
2137    
2138        buffer.append(')');
2139      }
2140    
2141    
2142    
2143      /**
2144       * {@inheritDoc}
2145       */
2146      public void toCode(final List<String> lineList, final String requestID,
2147                         final int indentSpaces, final boolean includeProcessing)
2148      {
2149        // Create the request variable.
2150        final ArrayList<ToCodeArgHelper> constructorArgs =
2151             new ArrayList<ToCodeArgHelper>(10);
2152        constructorArgs.add(ToCodeArgHelper.createString(baseDN, "Base DN"));
2153        constructorArgs.add(ToCodeArgHelper.createScope(scope, "Scope"));
2154        constructorArgs.add(ToCodeArgHelper.createDerefPolicy(derefPolicy,
2155             "Alias Dereference Policy"));
2156        constructorArgs.add(ToCodeArgHelper.createInteger(sizeLimit, "Size Limit"));
2157        constructorArgs.add(ToCodeArgHelper.createInteger(timeLimit, "Time Limit"));
2158        constructorArgs.add(ToCodeArgHelper.createBoolean(typesOnly, "Types Only"));
2159        constructorArgs.add(ToCodeArgHelper.createFilter(filter, "Filter"));
2160    
2161        String comment = "Requested Attributes";
2162        for (final String s : attributes)
2163        {
2164          constructorArgs.add(ToCodeArgHelper.createString(s, comment));
2165          comment = null;
2166        }
2167    
2168        ToCodeHelper.generateMethodCall(lineList, indentSpaces, "SearchRequest",
2169             requestID + "Request", "new SearchRequest", constructorArgs);
2170    
2171    
2172        // If there are any controls, then add them to the request.
2173        for (final Control c : getControls())
2174        {
2175          ToCodeHelper.generateMethodCall(lineList, indentSpaces, null, null,
2176               requestID + "Request.addControl",
2177               ToCodeArgHelper.createControl(c, null));
2178        }
2179    
2180    
2181        // Add lines for processing the request and obtaining the result.
2182        if (includeProcessing)
2183        {
2184          // Generate a string with the appropriate indent.
2185          final StringBuilder buffer = new StringBuilder();
2186          for (int i=0; i < indentSpaces; i++)
2187          {
2188            buffer.append(' ');
2189          }
2190          final String indent = buffer.toString();
2191    
2192          lineList.add("");
2193          lineList.add(indent + "SearchResult " + requestID + "Result;");
2194          lineList.add(indent + "try");
2195          lineList.add(indent + '{');
2196          lineList.add(indent + "  " + requestID + "Result = connection.search(" +
2197               requestID + "Request);");
2198          lineList.add(indent + "  // The search was processed successfully.");
2199          lineList.add(indent + '}');
2200          lineList.add(indent + "catch (LDAPSearchException e)");
2201          lineList.add(indent + '{');
2202          lineList.add(indent + "  // The search failed.  Maybe the following " +
2203               "will help explain why.");
2204          lineList.add(indent + "  ResultCode resultCode = e.getResultCode();");
2205          lineList.add(indent + "  String message = e.getMessage();");
2206          lineList.add(indent + "  String matchedDN = e.getMatchedDN();");
2207          lineList.add(indent + "  String[] referralURLs = e.getReferralURLs();");
2208          lineList.add(indent + "  Control[] responseControls = " +
2209               "e.getResponseControls();");
2210          lineList.add("");
2211          lineList.add(indent + "  // Even though there was an error, we may " +
2212               "have gotten some results.");
2213          lineList.add(indent + "  " + requestID + "Result = e.getSearchResult();");
2214          lineList.add(indent + '}');
2215          lineList.add("");
2216          lineList.add(indent + "// If there were results, then process them.");
2217          lineList.add(indent + "for (SearchResultEntry e : " + requestID +
2218               "Result.getSearchEntries())");
2219          lineList.add(indent + '{');
2220          lineList.add(indent + "  // Do something with the entry.");
2221          lineList.add(indent + '}');
2222        }
2223      }
2224    }