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.Collection; 042import java.util.Collections; 043import java.util.EnumSet; 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.Nullable; 052import com.unboundid.util.StaticUtils; 053import com.unboundid.util.ThreadSafety; 054import com.unboundid.util.ThreadSafetyLevel; 055import com.unboundid.util.Validator; 056import com.unboundid.util.json.JSONArray; 057import com.unboundid.util.json.JSONBoolean; 058import com.unboundid.util.json.JSONException; 059import com.unboundid.util.json.JSONNull; 060import com.unboundid.util.json.JSONNumber; 061import com.unboundid.util.json.JSONObject; 062import com.unboundid.util.json.JSONString; 063import com.unboundid.util.json.JSONValue; 064 065import static com.unboundid.ldap.sdk.unboundidds.jsonfilter.JFMessages.*; 066 067 068 069/** 070 * This class provides an implementation of a JSON object filter that can be 071 * used to identify JSON objects containing a specified field, optionally 072 * restricting it by the data type of the value. 073 * <BR> 074 * <BLOCKQUOTE> 075 * <B>NOTE:</B> This class, and other classes within the 076 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 077 * supported for use against Ping Identity, UnboundID, and 078 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 079 * for proprietary functionality or for external specifications that are not 080 * considered stable or mature enough to be guaranteed to work in an 081 * interoperable way with other types of LDAP servers. 082 * </BLOCKQUOTE> 083 * <BR> 084 * The fields that are required to be included in a "contains field" filter are: 085 * <UL> 086 * <LI> 087 * {@code field} -- A field path specifier for the JSON field for which to 088 * make the determination. This may be either a single string or an 089 * array of strings as described in the "Targeting Fields in JSON Objects" 090 * section of the class-level documentation for {@link JSONObjectFilter}. 091 * </LI> 092 * </UL> 093 * The fields that may optionally be included in a "contains field" filter are: 094 * <UL> 095 * <LI> 096 * {@code expectedType} -- Specifies the expected data type for the value of 097 * the target field. If this is not specified, then any data type will be 098 * permitted. If this is specified, then the filter will only match a JSON 099 * object that contains the specified {@code fieldName} if its value has the 100 * expected data type. The value of the {@code expectedType} field must be 101 * either a single string or an array of strings, and the only values 102 * allowed will be: 103 * <UL> 104 * <LI> 105 * {@code boolean} -- Indicates that the value may be a Boolean value of 106 * {@code true} or {@code false}. 107 * </LI> 108 * <LI> 109 * {@code empty-array} -- Indicates that the value may be an empty 110 * array. 111 * </LI> 112 * <LI> 113 * {@code non-empty-array} -- Indicates that the value may be an array 114 * that contains at least one element. There will not be any 115 * constraints placed on the values inside of the array. 116 * </LI> 117 * <LI> 118 * {@code null} -- Indicates that the value may be {@code null}. 119 * </LI> 120 * <LI> 121 * {@code number} -- Indicates that the value may be a number. 122 * </LI> 123 * <LI> 124 * {@code object} -- Indicates that the value may be a JSON object. 125 * </LI> 126 * <LI> 127 * {@code string} -- Indicates that the value may be a string. 128 * </LI> 129 * </UL> 130 * </LI> 131 * </UL> 132 * <H2>Examples</H2> 133 * The following is an example of a "contains field" filter that will match any 134 * JSON object that includes a top-level field of "department" with any kind of 135 * value: 136 * <PRE> 137 * { "filterType" : "containsField", 138 * "field" : "department" } 139 * </PRE> 140 * The above filter can be created with the code: 141 * <PRE> 142 * ContainsFieldJSONObjectFilter filter = 143 * new ContainsFieldJSONObjectFilter("department"); 144 * </PRE> 145 * <BR><BR> 146 * The following is an example of a "contains field" filter that will match any 147 * JSON object with a top-level field of "first" whose value is a JSON object 148 * (or an array containing a JSON object) with a field named "second" whose 149 * value is a Boolean of either {@code true} or {@code false}. 150 * <PRE> 151 * { "filterType" : "containsField", 152 * "field" : [ "first", "second" ], 153 * "expectedType" : "boolean" } 154 * </PRE> 155 * The above filter can be created with the code: 156 * <PRE> 157 * ContainsFieldJSONObjectFilter filter = new ContainsFieldJSONObjectFilter( 158 * Arrays.asList("first", "second"), 159 * EnumSet.of(ExpectedValueType.BOOLEAN)); 160 * </PRE> 161 */ 162@Mutable() 163@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 164public final class ContainsFieldJSONObjectFilter 165 extends JSONObjectFilter 166{ 167 /** 168 * The value that should be used for the filterType element of the JSON object 169 * that represents a "contains field" filter. 170 */ 171 @NotNull public static final String FILTER_TYPE = "containsField"; 172 173 174 175 /** 176 * The name of the JSON field that is used to specify the field in the target 177 * JSON object for which to make the determination. 178 */ 179 @NotNull public static final String FIELD_FIELD_PATH = "field"; 180 181 182 183 /** 184 * The name of the JSON field that is used to specify the expected data type 185 * for the target field. 186 */ 187 @NotNull public static final String FIELD_EXPECTED_TYPE = "expectedType"; 188 189 190 191 /** 192 * The pre-allocated set of required field names. 193 */ 194 @NotNull private static final Set<String> REQUIRED_FIELD_NAMES = 195 Collections.unmodifiableSet(new HashSet<>( 196 Collections.singletonList(FIELD_FIELD_PATH))); 197 198 199 200 /** 201 * The pre-allocated set of optional field names. 202 */ 203 @NotNull private static final Set<String> OPTIONAL_FIELD_NAMES = 204 Collections.unmodifiableSet(new HashSet<>( 205 Collections.singletonList(FIELD_EXPECTED_TYPE))); 206 207 208 209 /** 210 * A pre-allocated set containing all expected value type values. 211 */ 212 @NotNull private static final Set<ExpectedValueType> 213 ALL_EXPECTED_VALUE_TYPES = 214 Collections.unmodifiableSet(EnumSet.allOf(ExpectedValueType.class)); 215 216 217 218 /** 219 * The serial version UID for this serializable class. 220 */ 221 private static final long serialVersionUID = -2922149221350606755L; 222 223 224 225 // The field path specifier for the target field. 226 @NotNull private volatile List<String> field; 227 228 // The expected value types for the target field. 229 @NotNull private volatile Set<ExpectedValueType> expectedValueTypes; 230 231 232 233 /** 234 * Creates an instance of this filter type that can only be used for decoding 235 * JSON objects as "contains field" filters. It cannot be used as a regular 236 * "contains field" filter. 237 */ 238 ContainsFieldJSONObjectFilter() 239 { 240 field = null; 241 expectedValueTypes = null; 242 } 243 244 245 246 /** 247 * Creates a new instance of this filter type with the provided information. 248 * 249 * @param field The field path specifier for the target field. 250 * @param expectedValueTypes The expected value types for the target field. 251 */ 252 private ContainsFieldJSONObjectFilter(@NotNull final List<String> field, 253 @NotNull final Set<ExpectedValueType> expectedValueTypes) 254 { 255 this.field = field; 256 this.expectedValueTypes = expectedValueTypes; 257 } 258 259 260 261 /** 262 * Creates a new "contains field" filter that targets the specified field. 263 * 264 * @param field The field path specifier for this filter. It must not be 265 * {@code null} or empty. See the class-level documentation 266 * for the {@link JSONObjectFilter} class for information about 267 * field path specifiers. 268 */ 269 public ContainsFieldJSONObjectFilter(@NotNull final String... field) 270 { 271 this(StaticUtils.toList(field)); 272 } 273 274 275 276 /** 277 * Creates a new "contains field" filter that targets the specified field. 278 * 279 * @param field The field path specifier for this filter. It must not be 280 * {@code null} or empty. See the class-level documentation 281 * for the {@link JSONObjectFilter} class for information about 282 * field path specifiers. 283 */ 284 public ContainsFieldJSONObjectFilter(@NotNull final List<String> field) 285 { 286 Validator.ensureNotNull(field); 287 Validator.ensureFalse(field.isEmpty()); 288 289 this.field = Collections.unmodifiableList(new ArrayList<>(field)); 290 291 expectedValueTypes = ALL_EXPECTED_VALUE_TYPES; 292 } 293 294 295 296 /** 297 * Retrieves the field path specifier for this filter. 298 * 299 * @return The field path specifier for this filter. 300 */ 301 @NotNull() 302 public List<String> getField() 303 { 304 return field; 305 } 306 307 308 309 /** 310 * Sets the field path specifier for this filter. 311 * 312 * @param field The field path specifier for this filter. It must not be 313 * {@code null} or empty. See the class-level documentation 314 * for the {@link JSONObjectFilter} class for information about 315 * field path specifiers. 316 */ 317 public void setField(@NotNull final String... field) 318 { 319 setField(StaticUtils.toList(field)); 320 } 321 322 323 324 /** 325 * Sets the field path specifier for this filter. 326 * 327 * @param field The field path specifier for this filter. It must not be 328 * {@code null} or empty. See the class-level documentation 329 * for the {@link JSONObjectFilter} class for information about 330 * field path specifiers. 331 */ 332 public void setField(@NotNull final List<String> field) 333 { 334 Validator.ensureNotNull(field); 335 Validator.ensureFalse(field.isEmpty()); 336 337 this.field = Collections.unmodifiableList(new ArrayList<>(field)); 338 } 339 340 341 342 /** 343 * Retrieves the set of acceptable value types for the specified field. 344 * 345 * @return The set of acceptable value types for the specified field. 346 */ 347 @NotNull() 348 public Set<ExpectedValueType> getExpectedType() 349 { 350 return expectedValueTypes; 351 } 352 353 354 355 /** 356 * Specifies the set of acceptable value types for the specified field. 357 * 358 * @param expectedTypes The set of acceptable value types for the specified 359 * field. It may be {@code null} or empty if the field 360 * may have a value of any type. 361 */ 362 public void setExpectedType( 363 @Nullable final ExpectedValueType... expectedTypes) 364 { 365 setExpectedType(StaticUtils.toList(expectedTypes)); 366 } 367 368 369 370 /** 371 * Specifies the set of acceptable value types for the specified field. 372 * 373 * @param expectedTypes The set of acceptable value types for the specified 374 * field. It may be {@code null} or empty if the field 375 * may have a value of any type. 376 */ 377 public void setExpectedType( 378 @Nullable final Collection<ExpectedValueType> expectedTypes) 379 { 380 if ((expectedTypes == null) || expectedTypes.isEmpty()) 381 { 382 expectedValueTypes = ALL_EXPECTED_VALUE_TYPES; 383 } 384 else 385 { 386 final EnumSet<ExpectedValueType> s = 387 EnumSet.noneOf(ExpectedValueType.class); 388 s.addAll(expectedTypes); 389 expectedValueTypes = Collections.unmodifiableSet(s); 390 } 391 } 392 393 394 395 /** 396 * {@inheritDoc} 397 */ 398 @Override() 399 @NotNull() 400 public String getFilterType() 401 { 402 return FILTER_TYPE; 403 } 404 405 406 407 /** 408 * {@inheritDoc} 409 */ 410 @Override() 411 @NotNull() 412 protected Set<String> getRequiredFieldNames() 413 { 414 return REQUIRED_FIELD_NAMES; 415 } 416 417 418 419 /** 420 * {@inheritDoc} 421 */ 422 @Override() 423 @NotNull() 424 protected Set<String> getOptionalFieldNames() 425 { 426 return OPTIONAL_FIELD_NAMES; 427 } 428 429 430 431 /** 432 * {@inheritDoc} 433 */ 434 @Override() 435 public boolean matchesJSONObject(@NotNull final JSONObject o) 436 { 437 final List<JSONValue> candidates = getValues(o, field); 438 if (candidates.isEmpty()) 439 { 440 return false; 441 } 442 443 for (final JSONValue v : candidates) 444 { 445 if (v instanceof JSONArray) 446 { 447 final JSONArray a = (JSONArray) v; 448 if (a.isEmpty()) 449 { 450 if (expectedValueTypes.contains(ExpectedValueType.EMPTY_ARRAY)) 451 { 452 return true; 453 } 454 } 455 else 456 { 457 if (expectedValueTypes.contains(ExpectedValueType.NON_EMPTY_ARRAY)) 458 { 459 return true; 460 } 461 } 462 } 463 else if (v instanceof JSONBoolean) 464 { 465 if (expectedValueTypes.contains(ExpectedValueType.BOOLEAN)) 466 { 467 return true; 468 } 469 } 470 else if (v instanceof JSONNull) 471 { 472 if (expectedValueTypes.contains(ExpectedValueType.NULL)) 473 { 474 return true; 475 } 476 } 477 else if (v instanceof JSONNumber) 478 { 479 if (expectedValueTypes.contains(ExpectedValueType.NUMBER)) 480 { 481 return true; 482 } 483 } 484 else if (v instanceof JSONObject) 485 { 486 if (expectedValueTypes.contains(ExpectedValueType.OBJECT)) 487 { 488 return true; 489 } 490 } 491 else if (v instanceof JSONString) 492 { 493 if (expectedValueTypes.contains(ExpectedValueType.STRING)) 494 { 495 return true; 496 } 497 } 498 } 499 500 return false; 501 } 502 503 504 505 /** 506 * {@inheritDoc} 507 */ 508 @Override() 509 @NotNull() 510 public JSONObject toJSONObject() 511 { 512 final LinkedHashMap<String,JSONValue> fields = 513 new LinkedHashMap<>(StaticUtils.computeMapCapacity(3)); 514 515 fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE)); 516 517 if (field.size() == 1) 518 { 519 fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0))); 520 } 521 else 522 { 523 final ArrayList<JSONValue> fieldNameValues = 524 new ArrayList<>(field.size()); 525 for (final String s : field) 526 { 527 fieldNameValues.add(new JSONString(s)); 528 } 529 fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues)); 530 } 531 532 if (! expectedValueTypes.equals(ALL_EXPECTED_VALUE_TYPES)) 533 { 534 if (expectedValueTypes.size() == 1) 535 { 536 fields.put(FIELD_EXPECTED_TYPE, new 537 JSONString(expectedValueTypes.iterator().next().toString())); 538 } 539 else 540 { 541 final ArrayList<JSONValue> expectedTypeValues = 542 new ArrayList<>(expectedValueTypes.size()); 543 for (final ExpectedValueType t : expectedValueTypes) 544 { 545 expectedTypeValues.add(new JSONString(t.toString())); 546 } 547 fields.put(FIELD_EXPECTED_TYPE, new JSONArray(expectedTypeValues)); 548 } 549 } 550 551 return new JSONObject(fields); 552 } 553 554 555 556 /** 557 * {@inheritDoc} 558 */ 559 @Override() 560 @NotNull() 561 public JSONObject toNormalizedJSONObject() 562 { 563 return toJSONObject(); 564 } 565 566 567 568 /** 569 * {@inheritDoc} 570 */ 571 @Override() 572 @NotNull() 573 protected ContainsFieldJSONObjectFilter decodeFilter( 574 @NotNull final JSONObject filterObject) 575 throws JSONException 576 { 577 final List<String> fieldPath = 578 getStrings(filterObject, FIELD_FIELD_PATH, false, null); 579 580 final Set<ExpectedValueType> expectedTypes; 581 final List<String> valueTypeNames = getStrings(filterObject, 582 FIELD_EXPECTED_TYPE, false, Collections.<String>emptyList()); 583 if (valueTypeNames.isEmpty()) 584 { 585 expectedTypes = ALL_EXPECTED_VALUE_TYPES; 586 } 587 else 588 { 589 final EnumSet<ExpectedValueType> valueTypes = 590 EnumSet.noneOf(ExpectedValueType.class); 591 for (final String s : valueTypeNames) 592 { 593 final ExpectedValueType t = ExpectedValueType.forName(s); 594 if (t == null) 595 { 596 throw new JSONException( 597 ERR_CONTAINS_FIELD_FILTER_UNRECOGNIZED_EXPECTED_TYPE.get( 598 String.valueOf(filterObject), FILTER_TYPE, s, 599 FIELD_EXPECTED_TYPE)); 600 } 601 else 602 { 603 valueTypes.add(t); 604 } 605 } 606 expectedTypes = valueTypes; 607 } 608 609 return new ContainsFieldJSONObjectFilter(fieldPath, expectedTypes); 610 } 611}