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 less 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 Alcatel-Lucent 8661
058 *   server products.  These classes provide support for proprietary
059 *   functionality or for external specifications that are not considered stable
060 *   or mature enough to be guaranteed to work in an interoperable way with
061 *   other types of LDAP servers.
062 * </BLOCKQUOTE>
063 * <BR>
064 * The fields that are required to be included in a "less 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 "less 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 "less-than or equal to" filter) or
085 *     {@code false} (to indicate that it should be a strict "less-than"
086 *     filter).  If this is not specified, then the default behavior will be to
087 *     perform a strict "less-than" evaluation.
088 *   </LI>
089 *   <LI>
090 *     {@code matchAllElements} -- Indicates whether all elements of an array
091 *     must be less 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 "less than" filter that will match any
110 * JSON object with a top-level field named "loginFailureCount" with a value
111 * that is less than or equal to 3:
112 * <PRE>
113 *   { "filterType" : "lessThan",
114 *     "field" : "loginFailureCount",
115 *     "value" : 3,
116 *     "allowEquals" : true }
117 * </PRE>
118 * The above filter can be created with the code:
119 * <PRE>
120 *   LessThanJSONObjectFilter filter =
121 *        new LessThanJSONObjectFilter("loginFailureCount", 3);
122 *   filter.setAllowEquals(true);
123 * </PRE>
124 */
125@Mutable()
126@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
127public final class LessThanJSONObjectFilter
128       extends JSONObjectFilter
129{
130  /**
131   * The value that should be used for the filterType element of the JSON object
132   * that represents a "less than" filter.
133   */
134  public static final String FILTER_TYPE = "lessThan";
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<String>(
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<String>(
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 = -6023453566718838004L;
201
202
203
204  // Indicates whether to match equivalent values in addition to those that are
205  // strictly less 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 "less than" filters.  It cannot be used as a regular
226   * "less than" filter.
227   */
228  LessThanJSONObjectFilter()
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 less 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 LessThanJSONObjectFilter(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 LessThanJSONObjectFilter(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 LessThanJSONObjectFilter(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 LessThanJSONObjectFilter(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 LessThanJSONObjectFilter(final String field, final JSONValue value)
330  {
331    this(Collections.singletonList(field), value);
332  }
333
334
335
336  /**
337   * Creates a new instance of this filter type with the provided information.
338   *
339   * @param  field  The field path specifier for this filter.  It must not be
340   *                {@code null} or empty.  See the class-level documentation
341   *                for the {@link JSONObjectFilter} class for information about
342   *                field path specifiers.
343   * @param  value  The target value for this filter.  It must not be
344   *                {@code null}, and it must be either a {@link JSONNumber} or
345   *                a {@link JSONString}.
346   */
347  public LessThanJSONObjectFilter(final List<String> field,
348                                  final JSONValue value)
349  {
350    Validator.ensureNotNull(field);
351    Validator.ensureFalse(field.isEmpty());
352
353    Validator.ensureNotNull(value);
354    Validator.ensureTrue((value instanceof JSONNumber) ||
355         (value instanceof JSONString));
356
357    this.field = Collections.unmodifiableList(new ArrayList<String>(field));
358    this.value = value;
359
360    allowEquals = false;
361    matchAllElements = false;
362    caseSensitive = false;
363  }
364
365
366
367  /**
368   * Retrieves the field path specifier for this filter.
369   *
370   * @return  The field path specifier for this filter.
371   */
372  public List<String> getField()
373  {
374    return field;
375  }
376
377
378
379  /**
380   * Sets the field path specifier for this filter.
381   *
382   * @param  field  The field path specifier for this filter.  It must not be
383   *                {@code null} or empty.  See the class-level documentation
384   *                for the {@link JSONObjectFilter} class for information about
385   *                field path specifiers.
386   */
387  public void setField(final String... field)
388  {
389    setField(StaticUtils.toList(field));
390  }
391
392
393
394  /**
395   * Sets the field path specifier for this filter.
396   *
397   * @param  field  The field path specifier for this filter.  It must not be
398   *                {@code null} or empty.  See the class-level documentation
399   *                for the {@link JSONObjectFilter} class for information about
400   *                field path specifiers.
401   */
402  public void setField(final List<String> field)
403  {
404    Validator.ensureNotNull(field);
405    Validator.ensureFalse(field.isEmpty());
406
407    this.field = Collections.unmodifiableList(new ArrayList<String>(field));
408  }
409
410
411
412  /**
413   * Retrieves the target value for this filter.
414   *
415   * @return  The target value for this filter.
416   */
417  public JSONValue getValue()
418  {
419    return value;
420  }
421
422
423
424  /**
425   * Specifies the target value for this filter.
426   *
427   * @param  value  The target value for this filter.
428   */
429  public void setValue(final long value)
430  {
431    setValue(new JSONNumber(value));
432  }
433
434
435
436  /**
437   * Specifies the target value for this filter.
438   *
439   * @param  value  The target value for this filter.
440   */
441  public void setValue(final double value)
442  {
443    setValue(new JSONNumber(value));
444  }
445
446
447
448  /**
449   * Specifies the target value for this filter.
450   *
451   * @param  value  The target value for this filter.  It must not be
452   *                {@code null}.
453   */
454  public void setValue(final String value)
455  {
456    Validator.ensureNotNull(value);
457
458    setValue(new JSONString(value));
459  }
460
461
462
463  /**
464   * Specifies the target value for this filter.
465   *
466   * @param  value  The target value for this filter.  It must not be
467   *                {@code null}, and it must be either a {@link JSONNumber} or
468   *                a {@link JSONString}.
469   */
470  public void setValue(final JSONValue value)
471  {
472    Validator.ensureNotNull(value);
473    Validator.ensureTrue((value instanceof JSONNumber) ||
474         (value instanceof JSONString));
475
476    this.value = value;
477  }
478
479
480
481  /**
482   * Indicates whether this filter will match values that are considered equal
483   * to the provided value in addition to those that are strictly less than
484   * that value.
485   *
486   * @return  {@code true} if this filter should behave like a "less than or
487   *          equal to" filter, or {@code false} if it should behave strictly
488   *          like a "less than" filter.
489   */
490  public boolean allowEquals()
491  {
492    return allowEquals;
493  }
494
495
496
497  /**
498   * Specifies whether this filter should match values that are considered equal
499   * to the provided value in addition to those that are strictly less than
500   * that value.
501   *
502   * @param  allowEquals  Indicates whether this filter should match values that
503   *                      are considered equal to the provided value in addition
504   *                      to those that are strictly less than this value.
505   */
506  public void setAllowEquals(final boolean allowEquals)
507  {
508    this.allowEquals = allowEquals;
509  }
510
511
512
513  /**
514   * Indicates whether, if the specified field has a value that is an array, to
515   * require all elements of that array to match the criteria for this filter
516   * rather than merely requiring at least one value to match.
517   *
518   * @return  {@code true} if the criteria contained in this filter will be
519   *          required to match all elements of an array, or {@code false} if
520   *          merely one or more values will be required to match.
521   */
522  public boolean matchAllElements()
523  {
524    return matchAllElements;
525  }
526
527
528
529  /**
530   * Specifies whether, if the value of the target field is an array, all
531   * elements of that array will be required to match the criteria of this
532   * filter.  This will be ignored if the value of the target field is not an
533   * array.
534   *
535   * @param  matchAllElements  {@code true} to indicate that all elements of an
536   *                           array will be required to match the criteria of
537   *                           this filter, or {@code false} to indicate that
538   *                           merely one or more values will be required to
539   *                           match.
540   */
541  public void setMatchAllElements(final boolean matchAllElements)
542  {
543    this.matchAllElements = matchAllElements;
544  }
545
546
547
548  /**
549   * Indicates whether string matching should be performed in a case-sensitive
550   * manner.
551   *
552   * @return  {@code true} if string matching should be case sensitive, or
553   *          {@code false} if not.
554   */
555  public boolean caseSensitive()
556  {
557    return caseSensitive;
558  }
559
560
561
562  /**
563   * Specifies whether string matching should be performed in a case-sensitive
564   * manner.
565   *
566   * @param  caseSensitive  Indicates whether string matching should be
567   *                        case sensitive.
568   */
569  public void setCaseSensitive(final boolean caseSensitive)
570  {
571    this.caseSensitive = caseSensitive;
572  }
573
574
575
576  /**
577   * {@inheritDoc}
578   */
579  @Override()
580  public String getFilterType()
581  {
582    return FILTER_TYPE;
583  }
584
585
586
587  /**
588   * {@inheritDoc}
589   */
590  @Override()
591  protected Set<String> getRequiredFieldNames()
592  {
593    return REQUIRED_FIELD_NAMES;
594  }
595
596
597
598  /**
599   * {@inheritDoc}
600   */
601  @Override()
602  protected Set<String> getOptionalFieldNames()
603  {
604    return OPTIONAL_FIELD_NAMES;
605  }
606
607
608
609  /**
610   * {@inheritDoc}
611   */
612  @Override()
613  public boolean matchesJSONObject(final JSONObject o)
614  {
615    final List<JSONValue> candidates = getValues(o, field);
616    if (candidates.isEmpty())
617    {
618      return false;
619    }
620
621    for (final JSONValue v : candidates)
622    {
623      if (v instanceof JSONArray)
624      {
625        boolean matchOne = false;
626        boolean matchAll = true;
627        for (final JSONValue arrayValue : ((JSONArray) v).getValues())
628        {
629          if (matches(arrayValue))
630          {
631            if (! matchAllElements)
632            {
633              return true;
634            }
635            matchOne = true;
636          }
637          else
638          {
639            matchAll = false;
640            if (matchAllElements)
641            {
642              break;
643            }
644          }
645        }
646
647        if (matchAllElements && matchOne && matchAll)
648        {
649          return true;
650        }
651      }
652      else if (matches(v))
653      {
654        return true;
655      }
656    }
657
658    return false;
659  }
660
661
662
663  /**
664   * Indicates whether the provided value matches the criteria of this filter.
665   *
666   * @param  v  The value for which to make the determination.
667   *
668   * @return  {@code true} if the provided value matches the criteria of this
669   *          filter, or {@code false} if not.
670   */
671  private boolean matches(final JSONValue v)
672  {
673    if ((v instanceof JSONNumber) && (value instanceof JSONNumber))
674    {
675      final BigDecimal targetValue = ((JSONNumber) value).getValue();
676      final BigDecimal objectValue = ((JSONNumber) v).getValue();
677      if (allowEquals)
678      {
679        return (objectValue.compareTo(targetValue) <= 0);
680      }
681      else
682      {
683        return (objectValue.compareTo(targetValue) < 0);
684      }
685    }
686    else if ((v instanceof JSONString) && (value instanceof JSONString))
687    {
688      final String targetValue = ((JSONString) value).stringValue();
689      final String objectValue = ((JSONString) v).stringValue();
690      if (allowEquals)
691      {
692        if (caseSensitive)
693        {
694          return (objectValue.compareTo(targetValue) <= 0);
695        }
696        else
697        {
698          return (objectValue.compareToIgnoreCase(targetValue) <= 0);
699        }
700      }
701      else
702      {
703        if (caseSensitive)
704        {
705          return (objectValue.compareTo(targetValue) < 0);
706        }
707        else
708        {
709          return (objectValue.compareToIgnoreCase(targetValue) < 0);
710        }
711      }
712    }
713    else
714    {
715      return false;
716    }
717  }
718
719
720
721  /**
722   * {@inheritDoc}
723   */
724  @Override()
725  public JSONObject toJSONObject()
726  {
727    final LinkedHashMap<String,JSONValue> fields =
728         new LinkedHashMap<String,JSONValue>(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<JSONValue>(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 LessThanJSONObjectFilter decodeFilter(final JSONObject filterObject)
774            throws JSONException
775  {
776    final List<String> fieldPath =
777         getStrings(filterObject, FIELD_FIELD_PATH, false, null);
778
779    final boolean isAllowEquals = getBoolean(filterObject,
780         FIELD_ALLOW_EQUALS, false);
781
782    final boolean isMatchAllElements = getBoolean(filterObject,
783         FIELD_MATCH_ALL_ELEMENTS, false);
784
785    final boolean isCaseSensitive = getBoolean(filterObject,
786         FIELD_CASE_SENSITIVE, false);
787
788    return new LessThanJSONObjectFilter(fieldPath,
789         filterObject.getField(FIELD_VALUE), isAllowEquals, isMatchAllElements,
790         isCaseSensitive);
791  }
792}