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