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