001/*
002 * Copyright 2015-2023 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2015-2023 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) 2015-2023 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.jsonfilter;
037
038
039
040import java.math.BigDecimal;
041import java.util.ArrayList;
042import java.util.Arrays;
043import java.util.Collections;
044import java.util.HashSet;
045import java.util.LinkedHashMap;
046import java.util.List;
047import java.util.Set;
048
049import com.unboundid.util.Mutable;
050import com.unboundid.util.NotNull;
051import com.unboundid.util.StaticUtils;
052import com.unboundid.util.ThreadSafety;
053import com.unboundid.util.ThreadSafetyLevel;
054import com.unboundid.util.Validator;
055import com.unboundid.util.json.JSONArray;
056import com.unboundid.util.json.JSONBoolean;
057import com.unboundid.util.json.JSONException;
058import com.unboundid.util.json.JSONNumber;
059import com.unboundid.util.json.JSONObject;
060import com.unboundid.util.json.JSONString;
061import com.unboundid.util.json.JSONValue;
062
063
064
065/**
066 * This class provides an implementation of a JSON object filter that can be
067 * used to identify JSON objects that have at least one value for a specified
068 * field that is greater than a given value.
069 * <BR>
070 * <BLOCKQUOTE>
071 *   <B>NOTE:</B>  This class, and other classes within the
072 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
073 *   supported for use against Ping Identity, UnboundID, and
074 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
075 *   for proprietary functionality or for external specifications that are not
076 *   considered stable or mature enough to be guaranteed to work in an
077 *   interoperable way with other types of LDAP servers.
078 * </BLOCKQUOTE>
079 * <BR>
080 * The fields that are required to be included in a "greater than" filter are:
081 * <UL>
082 *   <LI>
083 *     {@code field} -- A field path specifier for the JSON field for which to
084 *     make the determination.  This may be either a single string or an array
085 *     of strings as described in the "Targeting Fields in JSON Objects" section
086 *     of the class-level documentation for {@link JSONObjectFilter}.
087 *   </LI>
088 *   <LI>
089 *     {@code value} -- The value to use in the matching.  It must be either a
090 *     string (which will be compared against other strings using lexicographic
091 *     comparison) or a number.
092 *   </LI>
093 * </UL>
094 * The fields that may optionally be included in a "greater than" filter are:
095 * <UL>
096 *   <LI>
097 *     {@code allowEquals} -- Indicates whether to match JSON objects that have
098 *     a value for the specified field that matches the provided value.  If
099 *     present, this field must have a Boolean value of either {@code true} (to
100 *     indicate that it should be a "greater-than or equal to" filter) or
101 *     {@code false} (to indicate that it should be a strict "greater-than"
102 *     filter).  If this is not specified, then the default behavior will be to
103 *     perform a strict "greater-than" evaluation.
104 *   </LI>
105 *   <LI>
106 *     {@code matchAllElements} -- Indicates whether all elements of an array
107 *     must be greater than (or possibly equal to) the specified value.  If
108 *     present, this field must have a Boolean value of {@code true} (to
109 *     indicate that all elements of the array must match the criteria for this
110 *     filter) or {@code false} (to indicate that at least one element of the
111 *     array must match the criteria for this filter).  If this is not
112 *     specified, then the default behavior will be to require only at least
113 *     one matching element.  This field will be ignored for JSON objects in
114 *     which the specified field has a value that is not an array.
115 *   </LI>
116 *   <LI>
117 *     {@code caseSensitive} -- Indicates whether string values should be
118 *     treated in a case-sensitive manner.  If present, this field must have a
119 *     Boolean value of either {@code true} or {@code false}.  If it is not
120 *     provided, then a default value of {@code false} will be assumed so that
121 *     strings are treated in a case-insensitive manner.
122 *   </LI>
123 * </UL>
124 * <H2>Example</H2>
125 * The following is an example of a "greater than" filter that will match any
126 * JSON object with a top-level field named "salary" with a value that is
127 * greater than or equal to 50000:
128 * <PRE>
129 *   { "filterType" : "greaterThan",
130 *     "field" : "salary",
131 *     "value" : 50000,
132 *     "allowEquals" : true }
133 * </PRE>
134 * The above filter can be created with the code:
135 * <PRE>
136 *   GreaterThanJSONObjectFilter filter =
137 *        new GreaterThanJSONObjectFilter("salary", 50000);
138 *   filter.setAllowEquals(true);
139 * </PRE>
140 */
141@Mutable()
142@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
143public final class GreaterThanJSONObjectFilter
144       extends JSONObjectFilter
145{
146  /**
147   * The value that should be used for the filterType element of the JSON object
148   * that represents a "greater than" filter.
149   */
150  @NotNull public static final String FILTER_TYPE = "greaterThan";
151
152
153
154  /**
155   * The name of the JSON field that is used to specify the field in the target
156   * JSON object for which to make the determination.
157   */
158  @NotNull public static final String FIELD_FIELD_PATH = "field";
159
160
161
162  /**
163   * The name of the JSON field that is used to specify the value to use for
164   * the matching.
165   */
166  @NotNull public static final String FIELD_VALUE = "value";
167
168
169
170  /**
171   * The name of the JSON field that is used to indicate whether to match JSON
172   * objects with a value that is considered equal to the provided value.
173   */
174  @NotNull public static final String FIELD_ALLOW_EQUALS = "allowEquals";
175
176
177
178  /**
179   * The name of the JSON field that is used to indicate whether to match all
180   * elements of an array rather than just one or more.
181   */
182  @NotNull public static final String FIELD_MATCH_ALL_ELEMENTS =
183       "matchAllElements";
184
185
186
187  /**
188   * The name of the JSON field that is used to indicate whether string matching
189   * should be case-sensitive.
190   */
191  @NotNull public static final String FIELD_CASE_SENSITIVE = "caseSensitive";
192
193
194
195  /**
196   * The pre-allocated set of required field names.
197   */
198  @NotNull private static final Set<String> REQUIRED_FIELD_NAMES =
199       Collections.unmodifiableSet(new HashSet<>(
200            Arrays.asList(FIELD_FIELD_PATH, FIELD_VALUE)));
201
202
203
204  /**
205   * The pre-allocated set of optional field names.
206   */
207  @NotNull private static final Set<String> OPTIONAL_FIELD_NAMES =
208       Collections.unmodifiableSet(new HashSet<>(
209            Arrays.asList(FIELD_ALLOW_EQUALS, FIELD_MATCH_ALL_ELEMENTS,
210                 FIELD_CASE_SENSITIVE)));
211
212
213
214  /**
215   * The serial version UID for this serializable class.
216   */
217  private static final long serialVersionUID = -8397741931424599570L;
218
219
220
221  // Indicates whether to match equivalent values in addition to those that are
222  // strictly greater than the target value.
223  private volatile boolean allowEquals;
224
225  // Indicates whether string matching should be case-sensitive.
226  private volatile boolean caseSensitive;
227
228  // Indicates whether to match all elements of an array rather than just one or
229  // more.
230  private volatile boolean matchAllElements;
231
232  // The expected value for the target field.
233  @NotNull private volatile JSONValue value;
234
235  // The field path specifier for the target field.
236  @NotNull private volatile List<String> field;
237
238
239
240  /**
241   * Creates an instance of this filter type that can only be used for decoding
242   * JSON objects as "greater than" filters.  It cannot be used as a regular
243   * "greater than" filter.
244   */
245  GreaterThanJSONObjectFilter()
246  {
247    field = null;
248    value = null;
249    allowEquals = false;
250    matchAllElements = false;
251    caseSensitive = false;
252  }
253
254
255
256  /**
257   * Creates a new instance of this filter type with the provided information.
258   *
259   * @param  field             The field path specifier for the target field.
260   * @param  value             The expected value for the target field.
261   * @param  allowEquals       Indicates whether to match values that are equal
262   *                           to the provided value in addition to those that
263   *                           are strictly greater than that value.
264   * @param  matchAllElements  Indicates whether, if the value of the target
265   *                           field is an array, all elements of that array
266   *                           will be required to match the criteria of this
267   *                           filter.
268   * @param  caseSensitive     Indicates whether string matching should be
269   *                           case sensitive.
270   */
271  private GreaterThanJSONObjectFilter(@NotNull final List<String> field,
272                                      @NotNull final JSONValue value,
273                                      final boolean allowEquals,
274                                      final boolean matchAllElements,
275                                      final boolean caseSensitive)
276  {
277    this.field = field;
278    this.value = value;
279    this.allowEquals = allowEquals;
280    this.matchAllElements = matchAllElements;
281    this.caseSensitive = caseSensitive;
282  }
283
284
285
286  /**
287   * Creates a new instance of this filter type with the provided information.
288   *
289   * @param  field  The name of the top-level field to target with this filter.
290   *                It must not be {@code null} .  See the class-level
291   *                documentation for the {@link JSONObjectFilter} class for
292   *                information about field path specifiers.
293   * @param  value  The target value for this filter.
294   */
295  public GreaterThanJSONObjectFilter(@NotNull final String field,
296                                     final long value)
297  {
298    this(Collections.singletonList(field), new JSONNumber(value));
299  }
300
301
302
303  /**
304   * Creates a new instance of this filter type with the provided information.
305   *
306   * @param  field  The name of the top-level field to target with this filter.
307   *                It must not be {@code null} .  See the class-level
308   *                documentation for the {@link JSONObjectFilter} class for
309   *                information about field path specifiers.
310   * @param  value  The target value for this filter.
311   */
312  public GreaterThanJSONObjectFilter(@NotNull final String field,
313                                     final double value)
314  {
315    this(Collections.singletonList(field), new JSONNumber(value));
316  }
317
318
319
320  /**
321   * Creates a new instance of this filter type with the provided information.
322   *
323   * @param  field  The name of the top-level field to target with this filter.
324   *                It must not be {@code null} .  See the class-level
325   *                documentation for the {@link JSONObjectFilter} class for
326   *                information about field path specifiers.
327   * @param  value  The target value for this filter.  It must not be
328   *                {@code null}.
329   */
330  public GreaterThanJSONObjectFilter(@NotNull final String field,
331                                     @NotNull final String value)
332  {
333    this(Collections.singletonList(field), new JSONString(value));
334  }
335
336
337
338  /**
339   * Creates a new instance of this filter type with the provided information.
340   *
341   * @param  field  The name of the top-level field to target with this filter.
342   *                It must not be {@code null} .  See the class-level
343   *                documentation for the {@link JSONObjectFilter} class for
344   *                information about field path specifiers.
345   * @param  value  The target value for this filter.  It must not be
346   *                {@code null}, and it must be either a {@link JSONNumber} or
347   *                a {@link JSONString}.
348   */
349  public GreaterThanJSONObjectFilter(@NotNull final String field,
350                                     @NotNull final JSONValue value)
351  {
352    this(Collections.singletonList(field), value);
353  }
354
355
356
357  /**
358   * Creates a new instance of this filter type with the provided information.
359   *
360   * @param  field  The field path specifier for this filter.  It must not be
361   *                {@code null} or empty.  See the class-level documentation
362   *                for the {@link JSONObjectFilter} class for information about
363   *                field path specifiers.
364   * @param  value  The target value for this filter.  It must not be
365   *                {@code null}, and it must be either a {@link JSONNumber} or
366   *                a {@link JSONString}.
367   */
368  public GreaterThanJSONObjectFilter(@NotNull final List<String> field,
369                                     @NotNull final JSONValue value)
370  {
371    Validator.ensureNotNull(field);
372    Validator.ensureFalse(field.isEmpty());
373
374    Validator.ensureNotNull(value);
375    Validator.ensureTrue((value instanceof JSONNumber) ||
376         (value instanceof JSONString));
377
378    this.field = Collections.unmodifiableList(new ArrayList<>(field));
379    this.value = value;
380
381    allowEquals = false;
382    matchAllElements = false;
383    caseSensitive = false;
384  }
385
386
387
388  /**
389   * Retrieves the field path specifier for this filter.
390   *
391   * @return  The field path specifier for this filter.
392   */
393  @NotNull()
394  public List<String> getField()
395  {
396    return field;
397  }
398
399
400
401  /**
402   * Sets the field path specifier for this filter.
403   *
404   * @param  field  The field path specifier for this filter.  It must not be
405   *                {@code null} or empty.  See the class-level documentation
406   *                for the {@link JSONObjectFilter} class for information about
407   *                field path specifiers.
408   */
409  public void setField(@NotNull final String... field)
410  {
411    setField(StaticUtils.toList(field));
412  }
413
414
415
416  /**
417   * Sets the field path specifier for this filter.
418   *
419   * @param  field  The field path specifier for this filter.  It must not be
420   *                {@code null} or empty.  See the class-level documentation
421   *                for the {@link JSONObjectFilter} class for information about
422   *                field path specifiers.
423   */
424  public void setField(@NotNull final List<String> field)
425  {
426    Validator.ensureNotNull(field);
427    Validator.ensureFalse(field.isEmpty());
428
429    this.field = Collections.unmodifiableList(new ArrayList<>(field));
430  }
431
432
433
434  /**
435   * Retrieves the target value for this filter.
436   *
437   * @return  The target value for this filter.
438   */
439  @NotNull()
440  public JSONValue getValue()
441  {
442    return value;
443  }
444
445
446
447  /**
448   * Specifies the target value for this filter.
449   *
450   * @param  value  The target value for this filter.
451   */
452  public void setValue(final long value)
453  {
454    setValue(new JSONNumber(value));
455  }
456
457
458
459  /**
460   * Specifies the target value for this filter.
461   *
462   * @param  value  The target value for this filter.
463   */
464  public void setValue(final double value)
465  {
466    setValue(new JSONNumber(value));
467  }
468
469
470
471  /**
472   * Specifies the target value for this filter.
473   *
474   * @param  value  The target value for this filter.  It must not be
475   *                {@code null}.
476   */
477  public void setValue(@NotNull final String value)
478  {
479    Validator.ensureNotNull(value);
480
481    setValue(new JSONString(value));
482  }
483
484
485
486  /**
487   * Specifies the target value for this filter.
488   *
489   * @param  value  The target value for this filter.  It must not be
490   *                {@code null}, and it must be either a {@link JSONNumber} or
491   *                a {@link JSONString}.
492   */
493  public void setValue(@NotNull final JSONValue value)
494  {
495    Validator.ensureNotNull(value);
496    Validator.ensureTrue((value instanceof JSONNumber) ||
497         (value instanceof JSONString));
498
499    this.value = value;
500  }
501
502
503
504  /**
505   * Indicates whether this filter will match values that are considered equal
506   * to the provided value in addition to those that are strictly greater than
507   * that value.
508   *
509   * @return  {@code true} if this filter should behave like a "greater than or
510   *          equal to" filter, or {@code false} if it should behave strictly
511   *          like a "greater than" filter.
512   */
513  public boolean allowEquals()
514  {
515    return allowEquals;
516  }
517
518
519
520  /**
521   * Specifies whether this filter should match values that are considered equal
522   * to the provided value in addition to those that are strictly greater than
523   * that value.
524   *
525   * @param  allowEquals  Indicates whether this filter should match values that
526   *                      are considered equal to the provided value in addition
527   *                      to those that are strictly greater than this value.
528   */
529  public void setAllowEquals(final boolean allowEquals)
530  {
531    this.allowEquals = allowEquals;
532  }
533
534
535
536  /**
537   * Indicates whether, if the specified field has a value that is an array, to
538   * require all elements of that array to match the criteria for this filter
539   * rather than merely requiring at least one value to match.
540   *
541   * @return  {@code true} if the criteria contained in this filter will be
542   *          required to match all elements of an array, or {@code false} if
543   *          merely one or more values will be required to match.
544   */
545  public boolean matchAllElements()
546  {
547    return matchAllElements;
548  }
549
550
551
552  /**
553   * Specifies whether, if the value of the target field is an array, all
554   * elements of that array will be required to match the criteria of this
555   * filter.  This will be ignored if the value of the target field is not an
556   * array.
557   *
558   * @param  matchAllElements  {@code true} to indicate that all elements of an
559   *                           array will be required to match the criteria of
560   *                           this filter, or {@code false} to indicate that
561   *                           merely one or more values will be required to
562   *                           match.
563   */
564  public void setMatchAllElements(final boolean matchAllElements)
565  {
566    this.matchAllElements = matchAllElements;
567  }
568
569
570
571  /**
572   * Indicates whether string matching should be performed in a case-sensitive
573   * manner.
574   *
575   * @return  {@code true} if string matching should be case sensitive, or
576   *          {@code false} if not.
577   */
578  public boolean caseSensitive()
579  {
580    return caseSensitive;
581  }
582
583
584
585  /**
586   * Specifies whether string matching should be performed in a case-sensitive
587   * manner.
588   *
589   * @param  caseSensitive  Indicates whether string matching should be
590   *                        case sensitive.
591   */
592  public void setCaseSensitive(final boolean caseSensitive)
593  {
594    this.caseSensitive = caseSensitive;
595  }
596
597
598
599  /**
600   * {@inheritDoc}
601   */
602  @Override()
603  @NotNull()
604  public String getFilterType()
605  {
606    return FILTER_TYPE;
607  }
608
609
610
611  /**
612   * {@inheritDoc}
613   */
614  @Override()
615  @NotNull()
616  protected Set<String> getRequiredFieldNames()
617  {
618    return REQUIRED_FIELD_NAMES;
619  }
620
621
622
623  /**
624   * {@inheritDoc}
625   */
626  @Override()
627  @NotNull()
628  protected Set<String> getOptionalFieldNames()
629  {
630    return OPTIONAL_FIELD_NAMES;
631  }
632
633
634
635  /**
636   * {@inheritDoc}
637   */
638  @Override()
639  public boolean matchesJSONObject(@NotNull final JSONObject o)
640  {
641    final List<JSONValue> candidates = getValues(o, field);
642    if (candidates.isEmpty())
643    {
644      return false;
645    }
646
647    for (final JSONValue v : candidates)
648    {
649      if (v instanceof JSONArray)
650      {
651        boolean matchOne = false;
652        boolean matchAll = true;
653        for (final JSONValue arrayValue : ((JSONArray) v).getValues())
654        {
655          if (matches(arrayValue))
656          {
657            if (! matchAllElements)
658            {
659              return true;
660            }
661            matchOne = true;
662          }
663          else
664          {
665            matchAll = false;
666            if (matchAllElements)
667            {
668              break;
669            }
670          }
671        }
672
673        if (matchAllElements && matchOne && matchAll)
674        {
675          return true;
676        }
677      }
678      else if (matches(v))
679      {
680        return true;
681      }
682    }
683
684    return false;
685  }
686
687
688
689  /**
690   * Indicates whether the provided value matches the criteria of this filter.
691   *
692   * @param  v  The value for which to make the determination.
693   *
694   * @return  {@code true} if the provided value matches the criteria of this
695   *          filter, or {@code false} if not.
696   */
697  private boolean matches(@NotNull final JSONValue v)
698  {
699    if ((v instanceof JSONNumber) && (value instanceof JSONNumber))
700    {
701      final BigDecimal targetValue = ((JSONNumber) value).getValue();
702      final BigDecimal objectValue = ((JSONNumber) v).getValue();
703      if (allowEquals)
704      {
705        return (objectValue.compareTo(targetValue) >= 0);
706      }
707      else
708      {
709        return (objectValue.compareTo(targetValue) > 0);
710      }
711    }
712    else if ((v instanceof JSONString) && (value instanceof JSONString))
713    {
714      final String targetValue = ((JSONString) value).stringValue();
715      final String objectValue = ((JSONString) v).stringValue();
716      if (allowEquals)
717      {
718        if (caseSensitive)
719        {
720          return (objectValue.compareTo(targetValue) >= 0);
721        }
722        else
723        {
724          return (objectValue.compareToIgnoreCase(targetValue) >= 0);
725        }
726      }
727      else
728      {
729        if (caseSensitive)
730        {
731          return (objectValue.compareTo(targetValue) > 0);
732        }
733        else
734        {
735          return (objectValue.compareToIgnoreCase(targetValue) > 0);
736        }
737      }
738    }
739    else
740    {
741      return false;
742    }
743  }
744
745
746
747  /**
748   * {@inheritDoc}
749   */
750  @Override()
751  @NotNull()
752  public JSONObject toJSONObject()
753  {
754    final LinkedHashMap<String,JSONValue> fields =
755         new LinkedHashMap<>(StaticUtils.computeMapCapacity(6));
756
757    fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE));
758
759    if (field.size() == 1)
760    {
761      fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0)));
762    }
763    else
764    {
765      final ArrayList<JSONValue> fieldNameValues =
766           new ArrayList<>(field.size());
767      for (final String s : field)
768      {
769        fieldNameValues.add(new JSONString(s));
770      }
771      fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues));
772    }
773
774    fields.put(FIELD_VALUE, value);
775
776    if (allowEquals)
777    {
778      fields.put(FIELD_ALLOW_EQUALS, JSONBoolean.TRUE);
779    }
780
781    if (matchAllElements)
782    {
783      fields.put(FIELD_MATCH_ALL_ELEMENTS, JSONBoolean.TRUE);
784    }
785
786    if (caseSensitive)
787    {
788      fields.put(FIELD_CASE_SENSITIVE, JSONBoolean.TRUE);
789    }
790
791    return new JSONObject(fields);
792  }
793
794
795
796  /**
797   * {@inheritDoc}
798   */
799  @Override()
800  @NotNull()
801  protected GreaterThanJSONObjectFilter decodeFilter(
802                 @NotNull final JSONObject filterObject)
803            throws JSONException
804  {
805    final List<String> fieldPath =
806         getStrings(filterObject, FIELD_FIELD_PATH, false, null);
807
808    final boolean isAllowEquals = getBoolean(filterObject,
809         FIELD_ALLOW_EQUALS, false);
810
811    final boolean isMatchAllElements = getBoolean(filterObject,
812         FIELD_MATCH_ALL_ELEMENTS, false);
813
814    final boolean isCaseSensitive = getBoolean(filterObject,
815         FIELD_CASE_SENSITIVE, false);
816
817    return new GreaterThanJSONObjectFilter(fieldPath,
818         filterObject.getField(FIELD_VALUE), isAllowEquals, isMatchAllElements,
819         isCaseSensitive);
820  }
821}