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