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