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 greater 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 "greater 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 "greater 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 "greater-than or equal to" filter) or 100 * {@code false} (to indicate that it should be a strict "greater-than" 101 * filter). If this is not specified, then the default behavior will be to 102 * perform a strict "greater-than" evaluation. 103 * </LI> 104 * <LI> 105 * {@code matchAllElements} -- Indicates whether all elements of an array 106 * must be greater 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 "greater than" filter that will match any 125 * JSON object with a top-level field named "salary" with a value that is 126 * greater than or equal to 50000: 127 * <PRE> 128 * { "filterType" : "greaterThan", 129 * "field" : "salary", 130 * "value" : 50000, 131 * "allowEquals" : true } 132 * </PRE> 133 * The above filter can be created with the code: 134 * <PRE> 135 * GreaterThanJSONObjectFilter filter = 136 * new GreaterThanJSONObjectFilter("salary", 50000); 137 * filter.setAllowEquals(true); 138 * </PRE> 139 */ 140@Mutable() 141@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 142public final class GreaterThanJSONObjectFilter 143 extends JSONObjectFilter 144{ 145 /** 146 * The value that should be used for the filterType element of the JSON object 147 * that represents a "greater than" filter. 148 */ 149 public static final String FILTER_TYPE = "greaterThan"; 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 = -8397741931424599570L; 216 217 218 219 // Indicates whether to match equivalent values in addition to those that are 220 // strictly greater 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 "greater than" filters. It cannot be used as a regular 241 * "greater than" filter. 242 */ 243 GreaterThanJSONObjectFilter() 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 greater 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 GreaterThanJSONObjectFilter(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 GreaterThanJSONObjectFilter(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 GreaterThanJSONObjectFilter(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 GreaterThanJSONObjectFilter(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 GreaterThanJSONObjectFilter(final String field, 345 final JSONValue value) 346 { 347 this(Collections.singletonList(field), value); 348 } 349 350 351 352 /** 353 * Creates a new instance of this filter type with the provided information. 354 * 355 * @param field The field path specifier for this filter. It must not be 356 * {@code null} or empty. See the class-level documentation 357 * for the {@link JSONObjectFilter} class for information about 358 * field path specifiers. 359 * @param value The target value for this filter. It must not be 360 * {@code null}, and it must be either a {@link JSONNumber} or 361 * a {@link JSONString}. 362 */ 363 public GreaterThanJSONObjectFilter(final List<String> field, 364 final JSONValue value) 365 { 366 Validator.ensureNotNull(field); 367 Validator.ensureFalse(field.isEmpty()); 368 369 Validator.ensureNotNull(value); 370 Validator.ensureTrue((value instanceof JSONNumber) || 371 (value instanceof JSONString)); 372 373 this.field = Collections.unmodifiableList(new ArrayList<>(field)); 374 this.value = value; 375 376 allowEquals = false; 377 matchAllElements = false; 378 caseSensitive = false; 379 } 380 381 382 383 /** 384 * Retrieves the field path specifier for this filter. 385 * 386 * @return The field path specifier for this filter. 387 */ 388 public List<String> getField() 389 { 390 return field; 391 } 392 393 394 395 /** 396 * Sets the field path specifier for this filter. 397 * 398 * @param field The field path specifier for this filter. It must not be 399 * {@code null} or empty. See the class-level documentation 400 * for the {@link JSONObjectFilter} class for information about 401 * field path specifiers. 402 */ 403 public void setField(final String... field) 404 { 405 setField(StaticUtils.toList(field)); 406 } 407 408 409 410 /** 411 * Sets the field path specifier for this filter. 412 * 413 * @param field The field path specifier for this filter. It must not be 414 * {@code null} or empty. See the class-level documentation 415 * for the {@link JSONObjectFilter} class for information about 416 * field path specifiers. 417 */ 418 public void setField(final List<String> field) 419 { 420 Validator.ensureNotNull(field); 421 Validator.ensureFalse(field.isEmpty()); 422 423 this.field = Collections.unmodifiableList(new ArrayList<>(field)); 424 } 425 426 427 428 /** 429 * Retrieves the target value for this filter. 430 * 431 * @return The target value for this filter. 432 */ 433 public JSONValue getValue() 434 { 435 return value; 436 } 437 438 439 440 /** 441 * Specifies the target value for this filter. 442 * 443 * @param value The target value for this filter. 444 */ 445 public void setValue(final long value) 446 { 447 setValue(new JSONNumber(value)); 448 } 449 450 451 452 /** 453 * Specifies the target value for this filter. 454 * 455 * @param value The target value for this filter. 456 */ 457 public void setValue(final double value) 458 { 459 setValue(new JSONNumber(value)); 460 } 461 462 463 464 /** 465 * Specifies the target value for this filter. 466 * 467 * @param value The target value for this filter. It must not be 468 * {@code null}. 469 */ 470 public void setValue(final String value) 471 { 472 Validator.ensureNotNull(value); 473 474 setValue(new JSONString(value)); 475 } 476 477 478 479 /** 480 * Specifies the target value for this filter. 481 * 482 * @param value The target value for this filter. It must not be 483 * {@code null}, and it must be either a {@link JSONNumber} or 484 * a {@link JSONString}. 485 */ 486 public void setValue(final JSONValue value) 487 { 488 Validator.ensureNotNull(value); 489 Validator.ensureTrue((value instanceof JSONNumber) || 490 (value instanceof JSONString)); 491 492 this.value = value; 493 } 494 495 496 497 /** 498 * Indicates whether this filter will match values that are considered equal 499 * to the provided value in addition to those that are strictly greater than 500 * that value. 501 * 502 * @return {@code true} if this filter should behave like a "greater than or 503 * equal to" filter, or {@code false} if it should behave strictly 504 * like a "greater than" filter. 505 */ 506 public boolean allowEquals() 507 { 508 return allowEquals; 509 } 510 511 512 513 /** 514 * Specifies whether this filter should match values that are considered equal 515 * to the provided value in addition to those that are strictly greater than 516 * that value. 517 * 518 * @param allowEquals Indicates whether this filter should match values that 519 * are considered equal to the provided value in addition 520 * to those that are strictly greater than this value. 521 */ 522 public void setAllowEquals(final boolean allowEquals) 523 { 524 this.allowEquals = allowEquals; 525 } 526 527 528 529 /** 530 * Indicates whether, if the specified field has a value that is an array, to 531 * require all elements of that array to match the criteria for this filter 532 * rather than merely requiring at least one value to match. 533 * 534 * @return {@code true} if the criteria contained in this filter will be 535 * required to match all elements of an array, or {@code false} if 536 * merely one or more values will be required to match. 537 */ 538 public boolean matchAllElements() 539 { 540 return matchAllElements; 541 } 542 543 544 545 /** 546 * Specifies whether, if the value of the target field is an array, all 547 * elements of that array will be required to match the criteria of this 548 * filter. This will be ignored if the value of the target field is not an 549 * array. 550 * 551 * @param matchAllElements {@code true} to indicate that all elements of an 552 * array will be required to match the criteria of 553 * this filter, or {@code false} to indicate that 554 * merely one or more values will be required to 555 * match. 556 */ 557 public void setMatchAllElements(final boolean matchAllElements) 558 { 559 this.matchAllElements = matchAllElements; 560 } 561 562 563 564 /** 565 * Indicates whether string matching should be performed in a case-sensitive 566 * manner. 567 * 568 * @return {@code true} if string matching should be case sensitive, or 569 * {@code false} if not. 570 */ 571 public boolean caseSensitive() 572 { 573 return caseSensitive; 574 } 575 576 577 578 /** 579 * Specifies whether string matching should be performed in a case-sensitive 580 * manner. 581 * 582 * @param caseSensitive Indicates whether string matching should be 583 * case sensitive. 584 */ 585 public void setCaseSensitive(final boolean caseSensitive) 586 { 587 this.caseSensitive = caseSensitive; 588 } 589 590 591 592 /** 593 * {@inheritDoc} 594 */ 595 @Override() 596 public String getFilterType() 597 { 598 return FILTER_TYPE; 599 } 600 601 602 603 /** 604 * {@inheritDoc} 605 */ 606 @Override() 607 protected Set<String> getRequiredFieldNames() 608 { 609 return REQUIRED_FIELD_NAMES; 610 } 611 612 613 614 /** 615 * {@inheritDoc} 616 */ 617 @Override() 618 protected Set<String> getOptionalFieldNames() 619 { 620 return OPTIONAL_FIELD_NAMES; 621 } 622 623 624 625 /** 626 * {@inheritDoc} 627 */ 628 @Override() 629 public boolean matchesJSONObject(final JSONObject o) 630 { 631 final List<JSONValue> candidates = getValues(o, field); 632 if (candidates.isEmpty()) 633 { 634 return false; 635 } 636 637 for (final JSONValue v : candidates) 638 { 639 if (v instanceof JSONArray) 640 { 641 boolean matchOne = false; 642 boolean matchAll = true; 643 for (final JSONValue arrayValue : ((JSONArray) v).getValues()) 644 { 645 if (matches(arrayValue)) 646 { 647 if (! matchAllElements) 648 { 649 return true; 650 } 651 matchOne = true; 652 } 653 else 654 { 655 matchAll = false; 656 if (matchAllElements) 657 { 658 break; 659 } 660 } 661 } 662 663 if (matchAllElements && matchOne && matchAll) 664 { 665 return true; 666 } 667 } 668 else if (matches(v)) 669 { 670 return true; 671 } 672 } 673 674 return false; 675 } 676 677 678 679 /** 680 * Indicates whether the provided value matches the criteria of this filter. 681 * 682 * @param v The value for which to make the determination. 683 * 684 * @return {@code true} if the provided value matches the criteria of this 685 * filter, or {@code false} if not. 686 */ 687 private boolean matches(final JSONValue v) 688 { 689 if ((v instanceof JSONNumber) && (value instanceof JSONNumber)) 690 { 691 final BigDecimal targetValue = ((JSONNumber) value).getValue(); 692 final BigDecimal objectValue = ((JSONNumber) v).getValue(); 693 if (allowEquals) 694 { 695 return (objectValue.compareTo(targetValue) >= 0); 696 } 697 else 698 { 699 return (objectValue.compareTo(targetValue) > 0); 700 } 701 } 702 else if ((v instanceof JSONString) && (value instanceof JSONString)) 703 { 704 final String targetValue = ((JSONString) value).stringValue(); 705 final String objectValue = ((JSONString) v).stringValue(); 706 if (allowEquals) 707 { 708 if (caseSensitive) 709 { 710 return (objectValue.compareTo(targetValue) >= 0); 711 } 712 else 713 { 714 return (objectValue.compareToIgnoreCase(targetValue) >= 0); 715 } 716 } 717 else 718 { 719 if (caseSensitive) 720 { 721 return (objectValue.compareTo(targetValue) > 0); 722 } 723 else 724 { 725 return (objectValue.compareToIgnoreCase(targetValue) > 0); 726 } 727 } 728 } 729 else 730 { 731 return false; 732 } 733 } 734 735 736 737 /** 738 * {@inheritDoc} 739 */ 740 @Override() 741 public JSONObject toJSONObject() 742 { 743 final LinkedHashMap<String,JSONValue> fields = 744 new LinkedHashMap<>(StaticUtils.computeMapCapacity(6)); 745 746 fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE)); 747 748 if (field.size() == 1) 749 { 750 fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0))); 751 } 752 else 753 { 754 final ArrayList<JSONValue> fieldNameValues = 755 new ArrayList<>(field.size()); 756 for (final String s : field) 757 { 758 fieldNameValues.add(new JSONString(s)); 759 } 760 fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues)); 761 } 762 763 fields.put(FIELD_VALUE, value); 764 765 if (allowEquals) 766 { 767 fields.put(FIELD_ALLOW_EQUALS, JSONBoolean.TRUE); 768 } 769 770 if (matchAllElements) 771 { 772 fields.put(FIELD_MATCH_ALL_ELEMENTS, JSONBoolean.TRUE); 773 } 774 775 if (caseSensitive) 776 { 777 fields.put(FIELD_CASE_SENSITIVE, JSONBoolean.TRUE); 778 } 779 780 return new JSONObject(fields); 781 } 782 783 784 785 /** 786 * {@inheritDoc} 787 */ 788 @Override() 789 protected GreaterThanJSONObjectFilter decodeFilter( 790 final JSONObject filterObject) 791 throws JSONException 792 { 793 final List<String> fieldPath = 794 getStrings(filterObject, FIELD_FIELD_PATH, false, null); 795 796 final boolean isAllowEquals = getBoolean(filterObject, 797 FIELD_ALLOW_EQUALS, false); 798 799 final boolean isMatchAllElements = getBoolean(filterObject, 800 FIELD_MATCH_ALL_ELEMENTS, false); 801 802 final boolean isCaseSensitive = getBoolean(filterObject, 803 FIELD_CASE_SENSITIVE, false); 804 805 return new GreaterThanJSONObjectFilter(fieldPath, 806 filterObject.getField(FIELD_VALUE), isAllowEquals, isMatchAllElements, 807 isCaseSensitive); 808 } 809}