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}