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