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