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