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.Collection; 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.JSONObject; 059import com.unboundid.util.json.JSONString; 060import com.unboundid.util.json.JSONValue; 061 062import static com.unboundid.ldap.sdk.unboundidds.jsonfilter.JFMessages.*; 063 064 065 066/** 067 * This class provides an implementation of a JSON object filter that can be 068 * used to identify JSON objects that have a specified field whose value matches 069 * one of specified set of values. 070 * <BR> 071 * <BLOCKQUOTE> 072 * <B>NOTE:</B> This class, and other classes within the 073 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 074 * supported for use against Ping Identity, UnboundID, and 075 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 076 * for proprietary functionality or for external specifications that are not 077 * considered stable or mature enough to be guaranteed to work in an 078 * interoperable way with other types of LDAP servers. 079 * </BLOCKQUOTE> 080 * <BR> 081 * The fields that are required to be included in an "equals any" filter are: 082 * <UL> 083 * <LI> 084 * {@code field} -- A field path specifier for the JSON field for which to 085 * make the determination. This may be either a single string or an array 086 * of strings as described in the "Targeting Fields in JSON Objects" section 087 * of the class-level documentation for {@link JSONObjectFilter}. 088 * </LI> 089 * <LI> 090 * {@code values} -- The set of values that should be used to match. This 091 * should be an array, but the elements of the array may be of any type. In 092 * order for a JSON object ot match this "equals any" filter, either the 093 * value of the target field must have the same type and value as one of the 094 * values in this array, or the value of the target field must be an array 095 * containing at least one element with the same type and value as one of 096 * the values in this array. 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>Example</H2> 110 * The following is an example of an "equals any" filter that will match any 111 * JSON object that includes a top-level field of "userType" with a value of 112 * either "employee", "partner", or "contractor": 113 * value: 114 * <PRE> 115 * { "filterType" : "equalsAny", 116 * "field" : "userType", 117 * "values" : [ "employee", "partner", "contractor" ] } 118 * </PRE> 119 * The above filter can be created with the code: 120 * <PRE> 121 * EqualsAnyJSONObjectFilter filter = new EqualsAnyJSONObjectFilter( 122 * "userType", "employee", "partner", "contractor"); 123 * </PRE> 124 */ 125@Mutable() 126@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 127public final class EqualsAnyJSONObjectFilter 128 extends JSONObjectFilter 129{ 130 /** 131 * The value that should be used for the filterType element of the JSON object 132 * that represents an "equals any" filter. 133 */ 134 @NotNull public static final String FILTER_TYPE = "equalsAny"; 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 @NotNull 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 values to use for 148 * the matching. 149 */ 150 @NotNull public static final String FIELD_VALUES = "values"; 151 152 153 154 /** 155 * The name of the JSON field that is used to indicate whether string matching 156 * should be case-sensitive. 157 */ 158 @NotNull public static final String FIELD_CASE_SENSITIVE = "caseSensitive"; 159 160 161 162 /** 163 * The pre-allocated set of required field names. 164 */ 165 @NotNull private static final Set<String> REQUIRED_FIELD_NAMES = 166 Collections.unmodifiableSet(new HashSet<>( 167 Arrays.asList(FIELD_FIELD_PATH, FIELD_VALUES))); 168 169 170 171 /** 172 * The pre-allocated set of optional field names. 173 */ 174 @NotNull private static final Set<String> OPTIONAL_FIELD_NAMES = 175 Collections.unmodifiableSet(new HashSet<>( 176 Collections.singletonList(FIELD_CASE_SENSITIVE))); 177 178 179 180 /** 181 * The serial version UID for this serializable class. 182 */ 183 private static final long serialVersionUID = -7441807169198186996L; 184 185 186 187 // Indicates whether string matching should be case-sensitive. 188 private volatile boolean caseSensitive; 189 190 // The set of expected values for the target field. 191 @NotNull private volatile List<JSONValue> values; 192 193 // The field path specifier for the target field. 194 @NotNull private volatile List<String> field; 195 196 197 198 /** 199 * Creates an instance of this filter type that can only be used for decoding 200 * JSON objects as "equals any" filters. It cannot be used as a regular 201 * "equals any" filter. 202 */ 203 EqualsAnyJSONObjectFilter() 204 { 205 field = null; 206 values = null; 207 caseSensitive = false; 208 } 209 210 211 212 /** 213 * Creates a new instance of this filter type with the provided information. 214 * 215 * @param field The field path specifier for the target field. 216 * @param values The set of expected values for the target field. 217 * @param caseSensitive Indicates whether string matching should be 218 * case sensitive. 219 */ 220 private EqualsAnyJSONObjectFilter(@NotNull final List<String> field, 221 @NotNull final List<JSONValue> values, 222 final boolean caseSensitive) 223 { 224 this.field = field; 225 this.values = values; 226 this.caseSensitive = caseSensitive; 227 } 228 229 230 231 /** 232 * Creates a new instance of this filter type with the provided information. 233 * 234 * @param field The name of the top-level field to target with this filter. 235 * It must not be {@code null} . See the class-level 236 * documentation for the {@link JSONObjectFilter} class for 237 * information about field path specifiers. 238 * @param values The set of expected string values for the target field. 239 * This filter will match an object in which the target field 240 * has the same type and value as any of the values in this 241 * set, or in which the target field is an array containing an 242 * element with the same type and value as any of the values 243 * in this set. It must not be {@code null} or empty. 244 */ 245 public EqualsAnyJSONObjectFilter(@NotNull final String field, 246 @NotNull final String... values) 247 { 248 this(Collections.singletonList(field), toJSONValues(values)); 249 } 250 251 252 253 /** 254 * Creates a new instance of this filter type with the provided information. 255 * 256 * @param field The name of the top-level field to target with this filter. 257 * It must not be {@code null} . See the class-level 258 * documentation for the {@link JSONObjectFilter} class for 259 * information about field path specifiers. 260 * @param values The set of expected string values for the target field. 261 * This filter will match an object in which the target field 262 * has the same type and value as any of the values in this 263 * set, or in which the target field is an array containing an 264 * element with the same type and value as any of the values 265 * in this set. It must not be {@code null} or empty. 266 */ 267 public EqualsAnyJSONObjectFilter(@NotNull final String field, 268 @NotNull final JSONValue... values) 269 { 270 this(Collections.singletonList(field), StaticUtils.toList(values)); 271 } 272 273 274 275 /** 276 * Creates a new instance of this filter type with the provided information. 277 * 278 * @param field The name of the top-level field to target with this filter. 279 * It must not be {@code null} . See the class-level 280 * documentation for the {@link JSONObjectFilter} class for 281 * information about field path specifiers. 282 * @param values The set of expected string values for the target field. 283 * This filter will match an object in which the target field 284 * has the same type and value as any of the values in this 285 * set, or in which the target field is an array containing an 286 * element with the same type and value as any of the values 287 * in this set. It must not be {@code null} or empty. 288 */ 289 public EqualsAnyJSONObjectFilter(@NotNull final String field, 290 @NotNull final Collection<JSONValue> values) 291 { 292 this(Collections.singletonList(field), values); 293 } 294 295 296 297 /** 298 * Creates a new instance of this filter type with the provided information. 299 * 300 * @param field The field path specifier for this filter. It must not be 301 * {@code null} or empty. See the class-level documentation 302 * for the {@link JSONObjectFilter} class for information 303 * about field path specifiers. 304 * @param values The set of expected string values for the target field. 305 * This filter will match an object in which the target field 306 * has the same type and value as any of the values in this 307 * set, or in which the target field is an array containing an 308 * element with the same type and value as any of the values 309 * in this set. It must not be {@code null} or empty. 310 */ 311 public EqualsAnyJSONObjectFilter(@NotNull final List<String> field, 312 @NotNull final Collection<JSONValue> values) 313 { 314 Validator.ensureNotNull(field); 315 Validator.ensureFalse(field.isEmpty()); 316 317 Validator.ensureNotNull(values); 318 Validator.ensureFalse(values.isEmpty()); 319 320 this.field= Collections.unmodifiableList(new ArrayList<>(field)); 321 this.values = 322 Collections.unmodifiableList(new ArrayList<>(values)); 323 324 caseSensitive = false; 325 } 326 327 328 329 /** 330 * Retrieves the field path specifier for this filter. 331 * 332 * @return The field path specifier for this filter. 333 */ 334 @NotNull() 335 public List<String> getField() 336 { 337 return field; 338 } 339 340 341 342 /** 343 * Sets the field path specifier for this filter. 344 * 345 * @param field The field path specifier for this filter. It must not be 346 * {@code null} or empty. See the class-level documentation 347 * for the {@link JSONObjectFilter} class for information about 348 * field path specifiers. 349 */ 350 public void setField(@NotNull final String... field) 351 { 352 setField(StaticUtils.toList(field)); 353 } 354 355 356 357 /** 358 * Sets the field path specifier for this filter. 359 * 360 * @param field The field path specifier for this filter. It must not be 361 * {@code null} or empty. See the class-level documentation 362 * for the {@link JSONObjectFilter} class for information about 363 * field path specifiers. 364 */ 365 public void setField(@NotNull final List<String> field) 366 { 367 Validator.ensureNotNull(field); 368 Validator.ensureFalse(field.isEmpty()); 369 370 this.field = Collections.unmodifiableList(new ArrayList<>(field)); 371 } 372 373 374 375 /** 376 * Retrieves the set of target values for this filter. A JSON object will 377 * only match this filter if it includes the target field with a value 378 * contained in this set. 379 * 380 * @return The set of target values for this filter. 381 */ 382 @NotNull() 383 public List<JSONValue> getValues() 384 { 385 return values; 386 } 387 388 389 390 /** 391 * Specifies the set of target values for this filter. 392 * 393 * @param values The set of target string values for this filter. It must 394 * not be {@code null} or empty. 395 */ 396 public void setValues(@NotNull final String... values) 397 { 398 setValues(toJSONValues(values)); 399 } 400 401 402 403 /** 404 * Specifies the set of target values for this filter. 405 * 406 * @param values The set of target values for this filter. It must not be 407 * {@code null} or empty. 408 */ 409 public void setValues(@NotNull final JSONValue... values) 410 { 411 setValues(StaticUtils.toList(values)); 412 } 413 414 415 416 /** 417 * Specifies the set of target values for this filter. 418 * 419 * @param values The set of target values for this filter. It must not be 420 * {@code null} or empty. 421 */ 422 public void setValues(@NotNull final Collection<JSONValue> values) 423 { 424 Validator.ensureNotNull(values); 425 Validator.ensureFalse(values.isEmpty()); 426 427 this.values = 428 Collections.unmodifiableList(new ArrayList<>(values)); 429 } 430 431 432 433 /** 434 * Converts the provided set of string values to a list of {@code JSONString} 435 * values. 436 * 437 * @param values The string values to be converted. 438 * 439 * @return The corresponding list of {@code JSONString} values. 440 */ 441 @NotNull() 442 private static List<JSONValue> toJSONValues(@NotNull final String... values) 443 { 444 final ArrayList<JSONValue> valueList = new ArrayList<>(values.length); 445 for (final String s : values) 446 { 447 valueList.add(new JSONString(s)); 448 } 449 return valueList; 450 } 451 452 453 454 /** 455 * Indicates whether string matching should be performed in a case-sensitive 456 * manner. 457 * 458 * @return {@code true} if string matching should be case sensitive, or 459 * {@code false} if not. 460 */ 461 public boolean caseSensitive() 462 { 463 return caseSensitive; 464 } 465 466 467 468 /** 469 * Specifies whether string matching should be performed in a case-sensitive 470 * manner. 471 * 472 * @param caseSensitive Indicates whether string matching should be 473 * case sensitive. 474 */ 475 public void setCaseSensitive(final boolean caseSensitive) 476 { 477 this.caseSensitive = caseSensitive; 478 } 479 480 481 482 /** 483 * {@inheritDoc} 484 */ 485 @Override() 486 @NotNull() 487 public String getFilterType() 488 { 489 return FILTER_TYPE; 490 } 491 492 493 494 /** 495 * {@inheritDoc} 496 */ 497 @Override() 498 @NotNull() 499 protected Set<String> getRequiredFieldNames() 500 { 501 return REQUIRED_FIELD_NAMES; 502 } 503 504 505 506 /** 507 * {@inheritDoc} 508 */ 509 @Override() 510 @NotNull() 511 protected Set<String> getOptionalFieldNames() 512 { 513 return OPTIONAL_FIELD_NAMES; 514 } 515 516 517 518 /** 519 * {@inheritDoc} 520 */ 521 @Override() 522 public boolean matchesJSONObject(@NotNull final JSONObject o) 523 { 524 final List<JSONValue> candidates = getValues(o, field); 525 if (candidates.isEmpty()) 526 { 527 return false; 528 } 529 530 for (final JSONValue objectValue : candidates) 531 { 532 for (final JSONValue filterValue : values) 533 { 534 if (filterValue.equals(objectValue, false, (! caseSensitive), false)) 535 { 536 return true; 537 } 538 } 539 540 if (objectValue instanceof JSONArray) 541 { 542 final JSONArray a = (JSONArray) objectValue; 543 for (final JSONValue filterValue : values) 544 { 545 if (a.contains(filterValue, false, (!caseSensitive), false, false)) 546 { 547 return true; 548 } 549 } 550 } 551 } 552 553 return false; 554 } 555 556 557 558 /** 559 * {@inheritDoc} 560 */ 561 @Override() 562 @NotNull() 563 public JSONObject toJSONObject() 564 { 565 final LinkedHashMap<String,JSONValue> fields = 566 new LinkedHashMap<>(StaticUtils.computeMapCapacity(4)); 567 568 fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE)); 569 570 if (field.size() == 1) 571 { 572 fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0))); 573 } 574 else 575 { 576 final ArrayList<JSONValue> fieldNameValues = 577 new ArrayList<>(field.size()); 578 for (final String s : field) 579 { 580 fieldNameValues.add(new JSONString(s)); 581 } 582 fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues)); 583 } 584 585 fields.put(FIELD_VALUES, new JSONArray(values)); 586 587 if (caseSensitive) 588 { 589 fields.put(FIELD_CASE_SENSITIVE, JSONBoolean.TRUE); 590 } 591 592 return new JSONObject(fields); 593 } 594 595 596 597 /** 598 * {@inheritDoc} 599 */ 600 @Override() 601 @NotNull() 602 public JSONObject toNormalizedJSONObject() 603 { 604 final LinkedHashMap<String,JSONValue> fields = 605 new LinkedHashMap<>(StaticUtils.computeMapCapacity(4)); 606 607 fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE)); 608 609 if (field.size() == 1) 610 { 611 fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0))); 612 } 613 else 614 { 615 final ArrayList<JSONValue> fieldNameValues = 616 new ArrayList<>(field.size()); 617 for (final String s : field) 618 { 619 fieldNameValues.add(new JSONString(s)); 620 } 621 fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues)); 622 } 623 624 if (caseSensitive) 625 { 626 fields.put(FIELD_VALUES, new JSONArray(values)); 627 fields.put(FIELD_CASE_SENSITIVE, JSONBoolean.TRUE); 628 } 629 else 630 { 631 final List<JSONValue> normalizedValues = new ArrayList<>(values.size()); 632 for (final JSONValue v : values) 633 { 634 normalizedValues.add( 635 v.toNormalizedValue(false, (! caseSensitive), false)); 636 } 637 638 fields.put(FIELD_VALUES, new JSONArray(normalizedValues)); 639 } 640 641 return new JSONObject(fields); 642 } 643 644 645 646 /** 647 * {@inheritDoc} 648 */ 649 @Override() 650 @NotNull() 651 protected EqualsAnyJSONObjectFilter decodeFilter( 652 @NotNull final JSONObject filterObject) 653 throws JSONException 654 { 655 final List<String> fieldPath = 656 getStrings(filterObject, FIELD_FIELD_PATH, false, null); 657 658 final boolean isCaseSensitive = getBoolean(filterObject, 659 FIELD_CASE_SENSITIVE, false); 660 661 final JSONValue arrayValue = filterObject.getField(FIELD_VALUES); 662 if (arrayValue instanceof JSONArray) 663 { 664 return new EqualsAnyJSONObjectFilter(fieldPath, 665 ((JSONArray) arrayValue).getValues(), isCaseSensitive); 666 } 667 else 668 { 669 throw new JSONException(ERR_OBJECT_FILTER_VALUE_NOT_ARRAY.get( 670 String.valueOf(filterObject), FILTER_TYPE, FIELD_VALUES)); 671 } 672 } 673}