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