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