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 }