001/* 002 * Copyright 2014-2020 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2014-2020 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) 2014-2020 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.unboundidds.controls; 037 038 039 040import java.util.ArrayList; 041import java.util.Collection; 042import java.util.Collections; 043import java.util.Iterator; 044import java.util.List; 045 046import com.unboundid.asn1.ASN1Boolean; 047import com.unboundid.asn1.ASN1Element; 048import com.unboundid.asn1.ASN1Integer; 049import com.unboundid.asn1.ASN1Null; 050import com.unboundid.asn1.ASN1OctetString; 051import com.unboundid.asn1.ASN1Sequence; 052import com.unboundid.ldap.sdk.Control; 053import com.unboundid.ldap.sdk.DecodeableControl; 054import com.unboundid.ldap.sdk.LDAPException; 055import com.unboundid.ldap.sdk.ResultCode; 056import com.unboundid.ldap.sdk.SearchResult; 057import com.unboundid.util.Debug; 058import com.unboundid.util.NotMutable; 059import com.unboundid.util.NotNull; 060import com.unboundid.util.Nullable; 061import com.unboundid.util.StaticUtils; 062import com.unboundid.util.ThreadSafety; 063import com.unboundid.util.ThreadSafetyLevel; 064import com.unboundid.util.Validator; 065 066import static com.unboundid.ldap.sdk.unboundidds.controls.ControlMessages.*; 067 068 069 070/** 071 * This class provides a response control that may be used to provide 072 * information about the number of entries that match a given set of search 073 * criteria. The control will be included in the search result done message 074 * for any successful search operation in which the request contained a matching 075 * entry count request control. 076 * <BR> 077 * <BLOCKQUOTE> 078 * <B>NOTE:</B> This class, and other classes within the 079 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 080 * supported for use against Ping Identity, UnboundID, and 081 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 082 * for proprietary functionality or for external specifications that are not 083 * considered stable or mature enough to be guaranteed to work in an 084 * interoperable way with other types of LDAP servers. 085 * </BLOCKQUOTE> 086 * <BR> 087 * The matching entry count response control has an OID of 088 * "1.3.6.1.4.1.30221.2.5.37", a criticality of false, and a value with the 089 * following encoding: 090 * <PRE> 091 * MatchingEntryCountResponse ::= SEQUENCE { 092 * entryCount CHOICE { 093 * examinedCount [0] INTEGER, 094 * unexaminedCount [1] INTEGER, 095 * upperBound [2] INTEGER, 096 * unknown [3] NULL, 097 * ... } 098 * debugInfo [0] SEQUENCE OF OCTET STRING OPTIONAL, 099 * searchIndexed [1] BOOLEAN DEFAULT TRUE, 100 * ... } 101 * </PRE> 102 * 103 * @see MatchingEntryCountRequestControl 104 */ 105@NotMutable() 106@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 107public final class MatchingEntryCountResponseControl 108 extends Control 109 implements DecodeableControl 110{ 111 /** 112 * The OID (1.3.6.1.4.1.30221.2.5.37) for the matching entry count response 113 * control. 114 */ 115 @NotNull public static final String MATCHING_ENTRY_COUNT_RESPONSE_OID = 116 "1.3.6.1.4.1.30221.2.5.37"; 117 118 119 120 /** 121 * The BER type for the element used to hold the list of debug messages. 122 */ 123 private static final byte TYPE_DEBUG_INFO = (byte) 0xA0; 124 125 126 127 /** 128 * The BER type for the element used to indicate whether the search criteria 129 * is at least partially indexed. 130 */ 131 private static final byte TYPE_SEARCH_INDEXED = (byte) 0x81; 132 133 134 135 /** 136 * The serial version UID for this serializable class. 137 */ 138 private static final long serialVersionUID = -5488025806310455564L; 139 140 141 142 // Indicates whether the search criteria is considered at least partially 143 // indexed by the server. 144 private final boolean searchIndexed; 145 146 // The count value for this matching entry count response control. 147 private final int countValue; 148 149 // A list of messages providing debug information about the processing 150 // performed by the server. 151 @NotNull private final List<String> debugInfo; 152 153 // The count type for this matching entry count response control. 154 @NotNull private final MatchingEntryCountType countType; 155 156 157 158 /** 159 * Creates a new empty control instance that is intended to be used only for 160 * decoding controls via the {@code DecodeableControl} interface. 161 */ 162 MatchingEntryCountResponseControl() 163 { 164 searchIndexed = false; 165 countType = null; 166 countValue = -1; 167 debugInfo = null; 168 } 169 170 171 172 /** 173 * Creates a new matching entry count response control with the provided 174 * information. 175 * 176 * @param countType The matching entry count type. It must not be 177 * {@code null}. 178 * @param countValue The matching entry count value. It must be greater 179 * than or equal to zero for a count type of either 180 * {@code EXAMINED_COUNT} or {@code UNEXAMINED_COUNT}. 181 * It must be greater than zero for a count type of 182 * {@code UPPER_BOUND}. It must be -1 for a count type 183 * of {@code UNKNOWN}. 184 * @param searchIndexed Indicates whether the search criteria is considered 185 * at least partially indexed and could be processed 186 * more efficiently than examining all entries with a 187 * full database scan. 188 * @param debugInfo An optional list of messages providing debug 189 * information about the processing performed by the 190 * server. It may be {@code null} or empty if no debug 191 * messages should be included. 192 */ 193 private MatchingEntryCountResponseControl( 194 @NotNull final MatchingEntryCountType countType, 195 final int countValue, 196 final boolean searchIndexed, 197 @Nullable final Collection<String> debugInfo) 198 { 199 super(MATCHING_ENTRY_COUNT_RESPONSE_OID, false, 200 encodeValue(countType, countValue, searchIndexed, debugInfo)); 201 202 this.countType = countType; 203 this.countValue = countValue; 204 this.searchIndexed = searchIndexed; 205 206 if (debugInfo == null) 207 { 208 this.debugInfo = Collections.emptyList(); 209 } 210 else 211 { 212 this.debugInfo = 213 Collections.unmodifiableList(new ArrayList<>(debugInfo)); 214 } 215 } 216 217 218 219 /** 220 * Creates a new matching entry count response control decoded from the given 221 * generic control contents. 222 * 223 * @param oid The OID for the control. 224 * @param isCritical Indicates whether this control should be marked 225 * critical. 226 * @param value The encoded value for the control. 227 * 228 * @throws LDAPException If a problem occurs while attempting to decode the 229 * generic control as a matching entry count response 230 * control. 231 */ 232 public MatchingEntryCountResponseControl(@NotNull final String oid, 233 final boolean isCritical, 234 @Nullable final ASN1OctetString value) 235 throws LDAPException 236 { 237 super(oid, isCritical, value); 238 239 if (value == null) 240 { 241 throw new LDAPException(ResultCode.DECODING_ERROR, 242 ERR_MATCHING_ENTRY_COUNT_RESPONSE_MISSING_VALUE.get()); 243 } 244 245 try 246 { 247 final ASN1Element[] elements = 248 ASN1Sequence.decodeAsSequence(value.getValue()).elements(); 249 countType = MatchingEntryCountType.valueOf(elements[0].getType()); 250 if (countType == null) 251 { 252 throw new LDAPException(ResultCode.DECODING_ERROR, 253 ERR_MATCHING_ENTRY_COUNT_RESPONSE_INVALID_COUNT_TYPE.get( 254 StaticUtils.toHex(elements[0].getType()))); 255 } 256 257 switch (countType) 258 { 259 case EXAMINED_COUNT: 260 case UNEXAMINED_COUNT: 261 countValue = ASN1Integer.decodeAsInteger(elements[0]).intValue(); 262 if (countValue < 0) 263 { 264 throw new LDAPException(ResultCode.DECODING_ERROR, 265 ERR_MATCHING_ENTRY_COUNT_RESPONSE_NEGATIVE_EXACT_COUNT.get()); 266 } 267 break; 268 269 case UPPER_BOUND: 270 countValue = ASN1Integer.decodeAsInteger(elements[0]).intValue(); 271 if (countValue <= 0) 272 { 273 throw new LDAPException(ResultCode.DECODING_ERROR, 274 ERR_MATCHING_ENTRY_COUNT_RESPONSE_NON_POSITIVE_UPPER_BOUND. 275 get()); 276 } 277 break; 278 279 case UNKNOWN: 280 default: 281 countValue = -1; 282 break; 283 } 284 285 boolean isIndexed = (countType != MatchingEntryCountType.UNKNOWN); 286 List<String> debugMessages = Collections.emptyList(); 287 for (int i=1; i < elements.length; i++) 288 { 289 switch (elements[i].getType()) 290 { 291 case TYPE_DEBUG_INFO: 292 final ASN1Element[] debugElements = 293 ASN1Sequence.decodeAsSequence(elements[i]).elements(); 294 debugMessages = new ArrayList<>(debugElements.length); 295 for (final ASN1Element e : debugElements) 296 { 297 debugMessages.add( 298 ASN1OctetString.decodeAsOctetString(e).stringValue()); 299 } 300 break; 301 302 case TYPE_SEARCH_INDEXED: 303 isIndexed = ASN1Boolean.decodeAsBoolean(elements[i]).booleanValue(); 304 break; 305 306 default: 307 throw new LDAPException(ResultCode.DECODING_ERROR, 308 ERR_MATCHING_ENTRY_COUNT_RESPONSE_UNKNOWN_ELEMENT_TYPE.get( 309 StaticUtils.toHex(elements[i].getType()))); 310 } 311 } 312 313 searchIndexed = isIndexed; 314 debugInfo = Collections.unmodifiableList(debugMessages); 315 } 316 catch (final LDAPException le) 317 { 318 Debug.debugException(le); 319 throw le; 320 } 321 catch (final Exception e) 322 { 323 Debug.debugException(e); 324 throw new LDAPException(ResultCode.DECODING_ERROR, 325 ERR_GET_BACKEND_SET_ID_RESPONSE_CANNOT_DECODE.get( 326 StaticUtils.getExceptionMessage(e)), 327 e); 328 } 329 } 330 331 332 333 /** 334 * Creates a new matching entry count response control for the case in which 335 * the exact number of matching entries is known. 336 * 337 * @param count The exact number of entries matching the associated 338 * search criteria. It must be greater than or equal to 339 * zero. 340 * @param examined Indicates whether the server examined the entries to 341 * exclude those entries that would not be returned to the 342 * client in a normal search with the same criteria. 343 * @param debugInfo An optional list of messages providing debug information 344 * about the processing performed by the server. It may be 345 * {@code null} or empty if no debug messages should be 346 * included. 347 * 348 * @return The matching entry count response control that was created. 349 */ 350 @NotNull() 351 public static MatchingEntryCountResponseControl createExactCountResponse( 352 final int count, final boolean examined, 353 @Nullable final Collection<String> debugInfo) 354 { 355 return createExactCountResponse(count, examined, true, debugInfo); 356 } 357 358 359 360 /** 361 * Creates a new matching entry count response control for the case in which 362 * the exact number of matching entries is known. 363 * 364 * @param count The exact number of entries matching the associated 365 * search criteria. It must be greater than or equal 366 * to zero. 367 * @param examined Indicates whether the server examined the entries to 368 * exclude those entries that would not be returned to 369 * the client in a normal search with the same 370 * criteria. 371 * @param searchIndexed Indicates whether the search criteria is considered 372 * at least partially indexed and could be processed 373 * more efficiently than examining all entries with a 374 * full database scan. 375 * @param debugInfo An optional list of messages providing debug 376 * information about the processing performed by the 377 * server. It may be {@code null} or empty if no debug 378 * messages should be included. 379 * 380 * @return The matching entry count response control that was created. 381 */ 382 @NotNull() 383 public static MatchingEntryCountResponseControl createExactCountResponse( 384 final int count, final boolean examined, 385 final boolean searchIndexed, 386 @Nullable final Collection<String> debugInfo) 387 { 388 Validator.ensureTrue(count >= 0); 389 390 final MatchingEntryCountType countType; 391 if (examined) 392 { 393 countType = MatchingEntryCountType.EXAMINED_COUNT; 394 } 395 else 396 { 397 countType = MatchingEntryCountType.UNEXAMINED_COUNT; 398 } 399 400 return new MatchingEntryCountResponseControl(countType, count, 401 searchIndexed, debugInfo); 402 } 403 404 405 406 /** 407 * Creates a new matching entry count response control for the case in which 408 * the exact number of matching entries is not known, but the server was able 409 * to determine an upper bound on the number of matching entries. This upper 410 * bound count may include entries that do not match the search filter, that 411 * are outside the scope of the search, and/or that match the search criteria 412 * but would not have been returned to the client in a normal search with the 413 * same criteria. 414 * 415 * @param upperBound The upper bound on the number of entries that match the 416 * associated search criteria. It must be greater than 417 * zero. 418 * @param debugInfo An optional list of messages providing debug 419 * information about the processing performed by the 420 * server. It may be {@code null} or empty if no debug 421 * messages should be included. 422 * 423 * @return The matching entry count response control that was created. 424 */ 425 @NotNull() 426 public static MatchingEntryCountResponseControl createUpperBoundResponse( 427 final int upperBound, 428 @Nullable final Collection<String> debugInfo) 429 { 430 return createUpperBoundResponse(upperBound, true, debugInfo); 431 } 432 433 434 435 /** 436 * Creates a new matching entry count response control for the case in which 437 * the exact number of matching entries is not known, but the server was able 438 * to determine an upper bound on the number of matching entries. This upper 439 * bound count may include entries that do not match the search filter, that 440 * are outside the scope of the search, and/or that match the search criteria 441 * but would not have been returned to the client in a normal search with the 442 * same criteria. 443 * 444 * @param upperBound The upper bound on the number of entries that match 445 * the associated search criteria. It must be greater 446 * than zero. 447 * @param searchIndexed Indicates whether the search criteria is considered 448 * at least partially indexed and could be processed 449 * more efficiently than examining all entries with a 450 * full database scan. 451 * @param debugInfo An optional list of messages providing debug 452 * information about the processing performed by the 453 * server. It may be {@code null} or empty if no debug 454 * messages should be included. 455 * 456 * @return The matching entry count response control that was created. 457 */ 458 @NotNull() 459 public static MatchingEntryCountResponseControl createUpperBoundResponse( 460 final int upperBound, final boolean searchIndexed, 461 @Nullable final Collection<String> debugInfo) 462 { 463 Validator.ensureTrue(upperBound > 0); 464 465 return new MatchingEntryCountResponseControl( 466 MatchingEntryCountType.UPPER_BOUND, upperBound, searchIndexed, 467 debugInfo); 468 } 469 470 471 472 /** 473 * Creates a new matching entry count response control for the case in which 474 * the server was unable to make any meaningful determination about the number 475 * of entries matching the search criteria. 476 * 477 * @param debugInfo An optional list of messages providing debug information 478 * about the processing performed by the server. It may be 479 * {@code null} or empty if no debug messages should be 480 * included. 481 * 482 * @return The matching entry count response control that was created. 483 */ 484 @NotNull() 485 public static MatchingEntryCountResponseControl createUnknownCountResponse( 486 @Nullable final Collection<String> debugInfo) 487 { 488 return new MatchingEntryCountResponseControl(MatchingEntryCountType.UNKNOWN, 489 -1, false, debugInfo); 490 } 491 492 493 494 /** 495 * Encodes a control value with the provided information. 496 * 497 * @param countType The matching entry count type. It must not be 498 * {@code null}. 499 * @param countValue The matching entry count value. It must be greater 500 * than or equal to zero for a count type of either 501 * {@code EXAMINED_COUNT} or {@code UNEXAMINED_COUNT}. 502 * It must be greater than zero for a count type of 503 * {@code UPPER_BOUND}. It must be -1 for a count type 504 * of {@code UNKNOWN}. 505 * @param searchIndexed Indicates whether the search criteria is considered 506 * at least partially indexed and could be processed 507 * more efficiently than examining all entries with a 508 * full database scan. 509 * @param debugInfo An optional list of messages providing debug 510 * information about the processing performed by the 511 * server. It may be {@code null} or empty if no debug 512 * messages should be included. 513 * 514 * @return The encoded control value. 515 */ 516 @NotNull() 517 private static ASN1OctetString encodeValue( 518 @NotNull final MatchingEntryCountType countType, 519 final int countValue, 520 final boolean searchIndexed, 521 @Nullable final Collection<String> debugInfo) 522 { 523 final ArrayList<ASN1Element> elements = new ArrayList<>(3); 524 525 switch (countType) 526 { 527 case EXAMINED_COUNT: 528 case UNEXAMINED_COUNT: 529 case UPPER_BOUND: 530 elements.add(new ASN1Integer(countType.getBERType(), countValue)); 531 break; 532 case UNKNOWN: 533 elements.add(new ASN1Null(countType.getBERType())); 534 break; 535 } 536 537 if (debugInfo != null) 538 { 539 final ArrayList<ASN1Element> debugElements = 540 new ArrayList<>(debugInfo.size()); 541 for (final String s : debugInfo) 542 { 543 debugElements.add(new ASN1OctetString(s)); 544 } 545 546 elements.add(new ASN1Sequence(TYPE_DEBUG_INFO, debugElements)); 547 } 548 549 if (! searchIndexed) 550 { 551 elements.add(new ASN1Boolean(TYPE_SEARCH_INDEXED, searchIndexed)); 552 } 553 554 return new ASN1OctetString(new ASN1Sequence(elements).encode()); 555 } 556 557 558 559 /** 560 * Retrieves the matching entry count type for the response control. 561 * 562 * @return The matching entry count type for the response control. 563 */ 564 @NotNull() 565 public MatchingEntryCountType getCountType() 566 { 567 return countType; 568 } 569 570 571 572 /** 573 * Retrieves the matching entry count value for the response control. For a 574 * count type of {@code EXAMINED_COUNT} or {@code UNEXAMINED_COUNT}, this is 575 * the exact number of matching entries. For a count type of 576 * {@code UPPER_BOUND}, this is the maximum number of entries that may match 577 * the search criteria, but it may also include entries that do not match the 578 * criteria. For a count type of {@code UNKNOWN}, this will always be -1. 579 * 580 * @return The exact count or upper bound of the number of entries in the 581 * server that may match the search criteria, or -1 if the server 582 * could not determine the number of matching entries. 583 */ 584 public int getCountValue() 585 { 586 return countValue; 587 } 588 589 590 591 /** 592 * Indicates whether the server considers the search criteria to be indexed 593 * and therefore it could be processed more efficiently than examining all 594 * entries with a full database scan. 595 * 596 * @return {@code true} if the server considers the search criteria to be 597 * indexed, or {@code false} if not. 598 */ 599 public boolean searchIndexed() 600 { 601 return searchIndexed; 602 } 603 604 605 606 /** 607 * Retrieves a list of messages with debug information about the processing 608 * performed by the server in the course of obtaining the matching entry 609 * count. These messages are intended to be human-readable rather than 610 * machine-parsable. 611 * 612 * @return A list of messages with debug information about the processing 613 * performed by the server in the course of obtaining the matching 614 * entry count, or an empty list if no debug messages were provided. 615 */ 616 @NotNull() 617 public List<String> getDebugInfo() 618 { 619 return debugInfo; 620 } 621 622 623 624 /** 625 * {@inheritDoc} 626 */ 627 @Override() 628 @NotNull() 629 public MatchingEntryCountResponseControl decodeControl( 630 @NotNull final String oid, 631 final boolean isCritical, 632 @Nullable final ASN1OctetString value) 633 throws LDAPException 634 { 635 return new MatchingEntryCountResponseControl(oid, isCritical, value); 636 } 637 638 639 640 /** 641 * Extracts a matching entry count response control from the provided search 642 * result. 643 * 644 * @param result The search result from which to retrieve the matching entry 645 * count response control. 646 * 647 * @return The matching entry count response control contained in the 648 * provided result, or {@code null} if the result did not contain a 649 * matching entry count response control. 650 * 651 * @throws LDAPException If a problem is encountered while attempting to 652 * decode the matching entry count response control 653 * contained in the provided result. 654 */ 655 @Nullable() 656 public static MatchingEntryCountResponseControl get( 657 @NotNull final SearchResult result) 658 throws LDAPException 659 { 660 final Control c = 661 result.getResponseControl(MATCHING_ENTRY_COUNT_RESPONSE_OID); 662 if (c == null) 663 { 664 return null; 665 } 666 667 if (c instanceof MatchingEntryCountResponseControl) 668 { 669 return (MatchingEntryCountResponseControl) c; 670 } 671 else 672 { 673 return new MatchingEntryCountResponseControl(c.getOID(), c.isCritical(), 674 c.getValue()); 675 } 676 } 677 678 679 680 /** 681 * {@inheritDoc} 682 */ 683 @Override() 684 @NotNull() 685 public String getControlName() 686 { 687 return INFO_CONTROL_NAME_MATCHING_ENTRY_COUNT_RESPONSE.get(); 688 } 689 690 691 692 /** 693 * {@inheritDoc} 694 */ 695 @Override() 696 public void toString(@NotNull final StringBuilder buffer) 697 { 698 buffer.append("MatchingEntryCountResponseControl(countType='"); 699 buffer.append(countType.name()); 700 buffer.append('\''); 701 702 switch (countType) 703 { 704 case EXAMINED_COUNT: 705 case UNEXAMINED_COUNT: 706 buffer.append(", count="); 707 buffer.append(countValue); 708 break; 709 710 case UPPER_BOUND: 711 buffer.append(", upperBound="); 712 buffer.append(countValue); 713 break; 714 } 715 716 buffer.append(", searchIndexed="); 717 buffer.append(searchIndexed); 718 719 if (! debugInfo.isEmpty()) 720 { 721 buffer.append(", debugInfo={"); 722 723 final Iterator<String> iterator = debugInfo.iterator(); 724 while (iterator.hasNext()) 725 { 726 buffer.append('\''); 727 buffer.append(iterator.next()); 728 buffer.append('\''); 729 730 if (iterator.hasNext()) 731 { 732 buffer.append(", "); 733 } 734 } 735 736 buffer.append('}'); 737 } 738 739 buffer.append(')'); 740 } 741}