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 }