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