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