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