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