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