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