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