001/* 002 * Copyright 2015-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2015-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2015-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk.unboundidds.jsonfilter; 037 038 039 040import java.util.ArrayList; 041import java.util.Arrays; 042import java.util.Collections; 043import java.util.HashSet; 044import java.util.LinkedHashMap; 045import java.util.List; 046import java.util.Set; 047 048import com.unboundid.util.Mutable; 049import com.unboundid.util.NotNull; 050import com.unboundid.util.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 a particular value for a specified 067 * field. 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 an "equals" filter are: 080 * <UL> 081 * <LI> 082 * {@code fieldName} -- A field path specifier for the JSON field for which 083 * to make the determination. This may be either a single string or an 084 * array of strings as described in the "Targeting Fields in JSON Objects" 085 * section of the class-level documentation for {@link JSONObjectFilter}. 086 * </LI> 087 * <LI> 088 * {@code value} -- The value to match. This value may be of any type. In 089 * order for a JSON object to match the equals filter, the value of the 090 * target field must either have the same type value as this value, or the 091 * value of the target field must be an array containing at least one 092 * element with the same type and value. If the provided value is an array, 093 * then the order, types, and values of the array must match an array 094 * contained in the target field. If the provided value is a JSON object, 095 * then the target field must contain a JSON object with exactly the same 096 * set of fields and values. 097 * </LI> 098 * </UL> 099 * The fields that may optionally be included in an "equals" filter are: 100 * <UL> 101 * <LI> 102 * {@code caseSensitive} -- Indicates whether string values should be 103 * treated in a case-sensitive manner. If present, this field must have a 104 * Boolean value of either {@code true} or {@code false}. If it is not 105 * provided, then a default value of {@code false} will be assumed so that 106 * strings are treated in a case-insensitive manner. 107 * </LI> 108 * </UL> 109 * <H2>Examples</H2> 110 * The following is an example of an "equals" filter that will match any JSON 111 * object with a top-level field named "firstName" with a value of "John": 112 * <PRE> 113 * { "filterType" : "equals", 114 * "field" : "firstName", 115 * "value" : "John" } 116 * </PRE> 117 * The above filter can be created with the code: 118 * <PRE> 119 * EqualsJSONObjectFilter filter = 120 * new EqualsJSONObjectFilter("firstName", "John"); 121 * </PRE> 122 * The following is an example of an "equals" filter that will match a JSON 123 * object with a top-level field named "contact" whose value is a JSON object 124 * (or an array containing one or more JSON objects) with a field named "type" 125 * and a value of "home": 126 * <PRE> 127 * { "filterType" : "equals", 128 * "field" : [ "contact", "type" ], 129 * "value" : "home" } 130 * </PRE> 131 * That filter can be created with the code: 132 * <PRE> 133 * EqualsJSONObjectFilter filter = 134 * new EqualsJSONObjectFilter(Arrays.asList("contact", "type"), "Home"); 135 * </PRE> 136 */ 137@Mutable() 138@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 139public final class EqualsJSONObjectFilter 140 extends JSONObjectFilter 141{ 142 /** 143 * The value that should be used for the filterType element of the JSON object 144 * that represents an "equals" filter. 145 */ 146 @NotNull public static final String FILTER_TYPE = "equals"; 147 148 149 150 /** 151 * The name of the JSON field that is used to specify the field in the target 152 * JSON object for which to make the determination. 153 */ 154 @NotNull public static final String FIELD_FIELD_PATH = "field"; 155 156 157 158 /** 159 * The name of the JSON field that is used to specify the value to use for 160 * the matching. 161 */ 162 @NotNull public static final String FIELD_VALUE = "value"; 163 164 165 166 /** 167 * The name of the JSON field that is used to indicate whether string matching 168 * should be case-sensitive. 169 */ 170 @NotNull public static final String FIELD_CASE_SENSITIVE = "caseSensitive"; 171 172 173 174 /** 175 * The pre-allocated set of required field names. 176 */ 177 @NotNull private static final Set<String> REQUIRED_FIELD_NAMES = 178 Collections.unmodifiableSet(new HashSet<>( 179 Arrays.asList(FIELD_FIELD_PATH, FIELD_VALUE))); 180 181 182 183 /** 184 * The pre-allocated set of optional field names. 185 */ 186 @NotNull private static final Set<String> OPTIONAL_FIELD_NAMES = 187 Collections.unmodifiableSet(new HashSet<>( 188 Collections.singletonList(FIELD_CASE_SENSITIVE))); 189 190 191 192 /** 193 * The serial version UID for this serializable class. 194 */ 195 private static final long serialVersionUID = 4622567662624840125L; 196 197 198 199 // Indicates whether string matching should be case-sensitive. 200 private volatile boolean caseSensitive; 201 202 // The expected value for the target field. 203 @NotNull private volatile JSONValue value; 204 205 // The path name specifier for the target field. 206 @NotNull private volatile List<String> field; 207 208 209 210 /** 211 * Creates an instance of this filter type that can only be used for decoding 212 * JSON objects as "equals" filters. It cannot be used as a regular "equals" 213 * filter. 214 */ 215 EqualsJSONObjectFilter() 216 { 217 field = null; 218 value = null; 219 caseSensitive = false; 220 } 221 222 223 224 /** 225 * Creates a new instance of this filter type with the provided information. 226 * 227 * @param field The path name specifier for the target field. 228 * @param value The expected value for the target field. 229 * @param caseSensitive Indicates whether string matching should be 230 * case sensitive. 231 */ 232 private EqualsJSONObjectFilter(@NotNull final List<String> field, 233 @NotNull final JSONValue value, 234 final boolean caseSensitive) 235 { 236 this.field = field; 237 this.value = value; 238 this.caseSensitive = caseSensitive; 239 } 240 241 242 243 /** 244 * Creates a new instance of this filter type with the provided information. 245 * 246 * @param field The name of the top-level field to target with this filter. 247 * It must not be {@code null} . See the class-level 248 * documentation for the {@link JSONObjectFilter} class for 249 * information about field path specifiers. 250 * @param value The target string value for this filter. It must not be 251 * {@code null}. 252 */ 253 public EqualsJSONObjectFilter(@NotNull final String field, 254 @NotNull final String value) 255 { 256 this(Collections.singletonList(field), new JSONString(value)); 257 } 258 259 260 261 /** 262 * Creates a new instance of this filter type with the provided information. 263 * 264 * @param field The name of the top-level field to target with this filter. 265 * It must not be {@code null} . See the class-level 266 * documentation for the {@link JSONObjectFilter} class for 267 * information about field path specifiers. 268 * @param value The target boolean value for this filter. 269 */ 270 public EqualsJSONObjectFilter(@NotNull final String field, 271 final boolean value) 272 { 273 this(Collections.singletonList(field), 274 (value ? JSONBoolean.TRUE : JSONBoolean.FALSE)); 275 } 276 277 278 279 /** 280 * Creates a new instance of this filter type with the provided information. 281 * 282 * @param field The name of the top-level field to target with this filter. 283 * It must not be {@code null} . See the class-level 284 * documentation for the {@link JSONObjectFilter} class for 285 * information about field path specifiers. 286 * @param value The target numeric value for this filter. 287 */ 288 public EqualsJSONObjectFilter(@NotNull final String field, final long value) 289 { 290 this(Collections.singletonList(field), new JSONNumber(value)); 291 } 292 293 294 295 /** 296 * Creates a new instance of this filter type with the provided information. 297 * 298 * @param field The name of the top-level field to target with this filter. 299 * It must not be {@code null} . See the class-level 300 * documentation for the {@link JSONObjectFilter} class for 301 * information about field path specifiers. 302 * @param value The target numeric value for this filter. It must not be 303 * {@code null}. 304 */ 305 public EqualsJSONObjectFilter(@NotNull final String field, 306 final double value) 307 { 308 this(Collections.singletonList(field), new JSONNumber(value)); 309 } 310 311 312 313 /** 314 * Creates a new instance of this filter type with the provided information. 315 * 316 * @param field The name of the top-level field to target with this filter. 317 * It must not be {@code null} . See the class-level 318 * documentation for the {@link JSONObjectFilter} class for 319 * information about field path specifiers. 320 * @param value The target value for this filter. It must not be 321 * {@code null}. 322 */ 323 public EqualsJSONObjectFilter(@NotNull final String field, 324 @NotNull final JSONValue value) 325 { 326 this(Collections.singletonList(field), value); 327 } 328 329 330 331 /** 332 * Creates a new instance of this filter type with the provided information. 333 * 334 * @param field The field path specifier for this filter. It must not be 335 * {@code null} or empty. See the class-level documentation 336 * for the {@link JSONObjectFilter} class for information about 337 * field path specifiers. 338 * @param value The target value for this filter. It must not be 339 * {@code null} (although it may be a {@code JSONNull}). 340 */ 341 public EqualsJSONObjectFilter(@NotNull final List<String> field, 342 @NotNull final JSONValue value) 343 { 344 Validator.ensureNotNull(field); 345 Validator.ensureFalse(field.isEmpty()); 346 347 Validator.ensureNotNull(value); 348 349 this.field = Collections.unmodifiableList(new ArrayList<>(field)); 350 this.value = value; 351 352 caseSensitive = false; 353 } 354 355 356 357 /** 358 * Retrieves the field path specifier for this filter. 359 * 360 * @return The field path specifier for this filter. 361 */ 362 @NotNull() 363 public List<String> getField() 364 { 365 return field; 366 } 367 368 369 370 /** 371 * Sets the field path specifier for this filter. 372 * 373 * @param field The field path specifier for this filter. It must not be 374 * {@code null} or empty. See the class-level documentation 375 * for the {@link JSONObjectFilter} class for information about 376 * field path specifiers. 377 */ 378 public void setField(@NotNull final String... field) 379 { 380 setField(StaticUtils.toList(field)); 381 } 382 383 384 385 /** 386 * Sets the field path specifier for this filter. 387 * 388 * @param field The field path specifier for this filter. It must not be 389 * {@code null} or empty. See the class-level documentation 390 * for the {@link JSONObjectFilter} class for information about 391 * field path specifiers. 392 */ 393 public void setField(@NotNull final List<String> field) 394 { 395 Validator.ensureNotNull(field); 396 Validator.ensureFalse(field.isEmpty()); 397 398 this.field = Collections.unmodifiableList(new ArrayList<>(field)); 399 } 400 401 402 403 /** 404 * Retrieves the target value for this filter. 405 * 406 * @return The target value for this filter. 407 */ 408 @NotNull() 409 public JSONValue getValue() 410 { 411 return value; 412 } 413 414 415 416 /** 417 * Specifies the target value for this filter. 418 * 419 * @param value The target string value for this filter. It must not be 420 * {@code null}. 421 */ 422 public void setValue(@NotNull final String value) 423 { 424 Validator.ensureNotNull(value); 425 426 this.value = new JSONString(value); 427 } 428 429 430 431 /** 432 * Specifies the target value for this filter. 433 * 434 * @param value The target Boolean value for this filter. 435 */ 436 public void setValue(final boolean value) 437 { 438 this.value = (value ? JSONBoolean.TRUE : JSONBoolean.FALSE); 439 } 440 441 442 443 /** 444 * Specifies the target value for this filter. 445 * 446 * @param value The target numeric value for this filter. 447 */ 448 public void setValue(final long value) 449 { 450 this.value = new JSONNumber(value); 451 } 452 453 454 455 /** 456 * Specifies the target value for this filter. 457 * 458 * @param value The target numeric value for this filter. 459 */ 460 public void setValue(final double value) 461 { 462 this.value = new JSONNumber(value); 463 } 464 465 466 467 /** 468 * Specifies the target value for this filter. 469 * 470 * @param value The target value for this filter. It must not be 471 * {@code null} (although it may be a {@code JSONNull}). 472 */ 473 public void setValue(@NotNull final JSONValue value) 474 { 475 Validator.ensureNotNull(value); 476 477 this.value = value; 478 } 479 480 481 482 /** 483 * Indicates whether string matching should be performed in a case-sensitive 484 * manner. 485 * 486 * @return {@code true} if string matching should be case sensitive, or 487 * {@code false} if not. 488 */ 489 public boolean caseSensitive() 490 { 491 return caseSensitive; 492 } 493 494 495 496 /** 497 * Specifies whether string matching should be performed in a case-sensitive 498 * manner. 499 * 500 * @param caseSensitive Indicates whether string matching should be 501 * case sensitive. 502 */ 503 public void setCaseSensitive(final boolean caseSensitive) 504 { 505 this.caseSensitive = caseSensitive; 506 } 507 508 509 510 /** 511 * {@inheritDoc} 512 */ 513 @Override() 514 @NotNull() 515 public String getFilterType() 516 { 517 return FILTER_TYPE; 518 } 519 520 521 522 /** 523 * {@inheritDoc} 524 */ 525 @Override() 526 @NotNull() 527 protected Set<String> getRequiredFieldNames() 528 { 529 return REQUIRED_FIELD_NAMES; 530 } 531 532 533 534 /** 535 * {@inheritDoc} 536 */ 537 @Override() 538 @NotNull() 539 protected Set<String> getOptionalFieldNames() 540 { 541 return OPTIONAL_FIELD_NAMES; 542 } 543 544 545 546 /** 547 * {@inheritDoc} 548 */ 549 @Override() 550 public boolean matchesJSONObject(@NotNull final JSONObject o) 551 { 552 final List<JSONValue> candidates = getValues(o, field); 553 if (candidates.isEmpty()) 554 { 555 return false; 556 } 557 558 for (final JSONValue v : candidates) 559 { 560 if (value.equals(v, false, (! caseSensitive), false)) 561 { 562 return true; 563 } 564 565 if (v instanceof JSONArray) 566 { 567 final JSONArray a = (JSONArray) v; 568 if (a.contains(value, false, (! caseSensitive), false, false)) 569 { 570 return true; 571 } 572 } 573 } 574 575 return false; 576 } 577 578 579 580 /** 581 * {@inheritDoc} 582 */ 583 @Override() 584 @NotNull() 585 public JSONObject toJSONObject() 586 { 587 final LinkedHashMap<String,JSONValue> fields = 588 new LinkedHashMap<>(StaticUtils.computeMapCapacity(4)); 589 590 fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE)); 591 592 if (field.size() == 1) 593 { 594 fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0))); 595 } 596 else 597 { 598 final ArrayList<JSONValue> fieldNameValues = 599 new ArrayList<>(field.size()); 600 for (final String s : field) 601 { 602 fieldNameValues.add(new JSONString(s)); 603 } 604 fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues)); 605 } 606 607 fields.put(FIELD_VALUE, value); 608 609 if (caseSensitive) 610 { 611 fields.put(FIELD_CASE_SENSITIVE, JSONBoolean.TRUE); 612 } 613 614 return new JSONObject(fields); 615 } 616 617 618 619 /** 620 * {@inheritDoc} 621 */ 622 @Override() 623 @NotNull() 624 public JSONObject toNormalizedJSONObject() 625 { 626 final LinkedHashMap<String,JSONValue> fields = 627 new LinkedHashMap<>(StaticUtils.computeMapCapacity(4)); 628 629 fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE)); 630 631 if (field.size() == 1) 632 { 633 fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0))); 634 } 635 else 636 { 637 final ArrayList<JSONValue> fieldNameValues = 638 new ArrayList<>(field.size()); 639 for (final String s : field) 640 { 641 fieldNameValues.add(new JSONString(s)); 642 } 643 fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues)); 644 } 645 646 fields.put(FIELD_VALUE, 647 value.toNormalizedValue(false, (! caseSensitive), false)); 648 649 if (caseSensitive) 650 { 651 fields.put(FIELD_CASE_SENSITIVE, JSONBoolean.TRUE); 652 } 653 654 return new JSONObject(fields); 655 } 656 657 658 659 /** 660 * {@inheritDoc} 661 */ 662 @Override() 663 @NotNull() 664 protected EqualsJSONObjectFilter decodeFilter( 665 @NotNull final JSONObject filterObject) 666 throws JSONException 667 { 668 final List<String> fieldPath = 669 getStrings(filterObject, FIELD_FIELD_PATH, false, null); 670 671 final boolean isCaseSensitive = getBoolean(filterObject, 672 FIELD_CASE_SENSITIVE, false); 673 674 return new EqualsJSONObjectFilter(fieldPath, 675 filterObject.getField(FIELD_VALUE), isCaseSensitive); 676 } 677}