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.Arrays; 042import java.util.Collections; 043import java.util.HashSet; 044import java.util.LinkedHashMap; 045import java.util.List; 046import java.util.Set; 047import java.util.regex.Matcher; 048import java.util.regex.Pattern; 049 050import com.unboundid.util.Debug; 051import com.unboundid.util.Mutable; 052import com.unboundid.util.NotNull; 053import com.unboundid.util.StaticUtils; 054import com.unboundid.util.ThreadSafety; 055import com.unboundid.util.ThreadSafetyLevel; 056import com.unboundid.util.Validator; 057import com.unboundid.util.json.JSONArray; 058import com.unboundid.util.json.JSONBoolean; 059import com.unboundid.util.json.JSONException; 060import com.unboundid.util.json.JSONObject; 061import com.unboundid.util.json.JSONString; 062import com.unboundid.util.json.JSONValue; 063 064import static com.unboundid.ldap.sdk.unboundidds.jsonfilter.JFMessages.*; 065 066 067 068/** 069 * This class provides an implementation of a JSON object filter that can be 070 * used to identify JSON objects that have a particular value for a specified 071 * field. 072 * <BR> 073 * <BLOCKQUOTE> 074 * <B>NOTE:</B> This class, and other classes within the 075 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 076 * supported for use against Ping Identity, UnboundID, and 077 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 078 * for proprietary functionality or for external specifications that are not 079 * considered stable or mature enough to be guaranteed to work in an 080 * interoperable way with other types of LDAP servers. 081 * </BLOCKQUOTE> 082 * <BR> 083 * The fields that are required to be included in a "regular expression" filter 084 * 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 array 089 * of strings as described in the "Targeting Fields in JSON Objects" section 090 * of the class-level documentation for {@link JSONObjectFilter}. 091 * </LI> 092 * <LI> 093 * {@code regularExpression} -- The regular expression to use to identify 094 * matching values. It must be compatible for use with the Java 095 * {@code java.util.regex.Pattern} class. 096 * </LI> 097 * </UL> 098 * The fields that may optionally be included in a "regular expression" filter 099 * are: 100 * <UL> 101 * <LI> 102 * {@code matchAllElements} -- Indicates whether all elements of an array 103 * must match the provided regular expression. If present, this field must 104 * have a Boolean value of {@code true} (to indicate that all elements of 105 * the array must match the regular expression) or {@code false} (to 106 * indicate that at least one element of the array must match the regular 107 * expression). If this is not specified, then the default behavior will be 108 * to require only at least one matching element. This field will be 109 * ignored for JSON objects in which the specified field has a value that is 110 * not an array. 111 * </LI> 112 * </UL> 113 * <H2>Example</H2> 114 * The following is an example of a "regular expression" filter that will match 115 * any JSON object with a top-level field named "userID" with a value that 116 * starts with an ASCII letter and contains only ASCII letters and numeric 117 * digits: 118 * <PRE> 119 * { "filterType" : "regularExpression", 120 * "field" : "userID", 121 * "regularExpression" : "^[a-zA-Z][a-zA-Z0-9]*$" } 122 * </PRE> 123 * The above filter can be created with the code: 124 * <PRE> 125 * RegularExpressionJSONObjectFilter filter = 126 new RegularExpressionJSONObjectFilter("userID", 127 "^[a-zA-Z][a-zA-Z0-9]*$"); 128 * </PRE> 129 */ 130@Mutable() 131@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 132public final class RegularExpressionJSONObjectFilter 133 extends JSONObjectFilter 134{ 135 /** 136 * The value that should be used for the filterType element of the JSON object 137 * that represents a "regular expression" filter. 138 */ 139 @NotNull public static final String FILTER_TYPE = "regularExpression"; 140 141 142 143 /** 144 * The name of the JSON field that is used to specify the field in the target 145 * JSON object for which to make the determination. 146 */ 147 @NotNull public static final String FIELD_FIELD_PATH = "field"; 148 149 150 151 /** 152 * The name of the JSON field that is used to specify the regular expression 153 * that values should match. 154 */ 155 @NotNull public static final String FIELD_REGULAR_EXPRESSION = 156 "regularExpression"; 157 158 159 160 /** 161 * The name of the JSON field that is used to indicate whether all values of 162 * an array should be required to match the provided regular expression. 163 */ 164 @NotNull public static final String FIELD_MATCH_ALL_ELEMENTS = 165 "matchAllElements"; 166 167 168 169 /** 170 * The pre-allocated set of required field names. 171 */ 172 @NotNull private static final Set<String> REQUIRED_FIELD_NAMES = 173 Collections.unmodifiableSet(new HashSet<>( 174 Arrays.asList(FIELD_FIELD_PATH, FIELD_REGULAR_EXPRESSION))); 175 176 177 178 /** 179 * The pre-allocated set of optional field names. 180 */ 181 @NotNull private static final Set<String> OPTIONAL_FIELD_NAMES = 182 Collections.unmodifiableSet(new HashSet<>( 183 Collections.singletonList(FIELD_MATCH_ALL_ELEMENTS))); 184 185 186 187 /** 188 * The serial version UID for this serializable class. 189 */ 190 private static final long serialVersionUID = 7678844742777504519L; 191 192 193 194 // Indicates whether to require all elements of an array to match the 195 // regular expression 196 private volatile boolean matchAllElements; 197 198 // The field path specifier for the target field. 199 @NotNull private volatile List<String> field; 200 201 // The regular expression to match. 202 @NotNull private volatile Pattern regularExpression; 203 204 205 206 /** 207 * Creates an instance of this filter type that can only be used for decoding 208 * JSON objects as "regular expression" filters. It cannot be used as a 209 * regular "regular expression" filter. 210 */ 211 RegularExpressionJSONObjectFilter() 212 { 213 field = null; 214 regularExpression = null; 215 matchAllElements = false; 216 } 217 218 219 220 /** 221 * Creates a new instance of this filter type with the provided information. 222 * 223 * @param field The field path specifier for the target field. 224 * @param regularExpression The regular expression pattern to match. 225 * @param matchAllElements Indicates whether all elements of an array are 226 * required to match the regular expression rather 227 * than merely at least one element. 228 */ 229 private RegularExpressionJSONObjectFilter( 230 @NotNull final List<String> field, 231 @NotNull final Pattern regularExpression, 232 final boolean matchAllElements) 233 { 234 this.field = field; 235 this.regularExpression = regularExpression; 236 this.matchAllElements = matchAllElements; 237 } 238 239 240 241 /** 242 * Creates a new instance of this filter type with the provided information. 243 * 244 * @param field The name of the top-level field to target with 245 * this filter. It must not be {@code null} . See 246 * the class-level documentation for the 247 * {@link JSONObjectFilter} class for information 248 * about field path specifiers. 249 * @param regularExpression The regular expression to match. It must not 250 * be {@code null}, and it must be compatible for 251 * use with the {@code java.util.regex.Pattern} 252 * class. 253 * 254 * @throws JSONException If the provided string cannot be parsed as a valid 255 * regular expression. 256 */ 257 public RegularExpressionJSONObjectFilter(@NotNull final String field, 258 @NotNull final String regularExpression) 259 throws JSONException 260 { 261 this(Collections.singletonList(field), regularExpression); 262 } 263 264 265 266 /** 267 * Creates a new instance of this filter type with the provided information. 268 * 269 * @param field The name of the top-level field to target with 270 * this filter. It must not be {@code null} . See 271 * the class-level documentation for the 272 * {@link JSONObjectFilter} class for information 273 * about field path specifiers. 274 * @param regularExpression The regular expression pattern to match. It 275 * must not be {@code null}. 276 */ 277 public RegularExpressionJSONObjectFilter(@NotNull final String field, 278 @NotNull final Pattern regularExpression) 279 { 280 this(Collections.singletonList(field), regularExpression); 281 } 282 283 284 285 /** 286 * Creates a new instance of this filter type with the provided information. 287 * 288 * @param field The field path specifier for this filter. It 289 * must not be {@code null} or empty. See the 290 * class-level documentation for the 291 * {@link JSONObjectFilter} class for information 292 * about field path specifiers. 293 * @param regularExpression The regular expression to match. It must not 294 * be {@code null}, and it must be compatible for 295 * use with the {@code java.util.regex.Pattern} 296 * class. 297 * 298 * @throws JSONException If the provided string cannot be parsed as a valid 299 * regular expression. 300 */ 301 public RegularExpressionJSONObjectFilter(@NotNull final List<String> field, 302 @NotNull final String regularExpression) 303 throws JSONException 304 { 305 Validator.ensureNotNull(field); 306 Validator.ensureFalse(field.isEmpty()); 307 308 Validator.ensureNotNull(regularExpression); 309 310 this.field = Collections.unmodifiableList(new ArrayList<>(field)); 311 312 try 313 { 314 this.regularExpression = Pattern.compile(regularExpression); 315 } 316 catch (final Exception e) 317 { 318 Debug.debugException(e); 319 throw new JSONException( 320 ERR_REGEX_FILTER_INVALID_REGEX.get(regularExpression, 321 StaticUtils.getExceptionMessage(e)), 322 e); 323 } 324 325 matchAllElements = false; 326 } 327 328 329 330 /** 331 * Creates a new instance of this filter type with the provided information. 332 * 333 * @param field The field path specifier for this filter. It 334 * must not be {@code null} or empty. See the 335 * class-level documentation for the 336 * {@link JSONObjectFilter} class for information 337 * about field path specifiers. 338 * @param regularExpression The regular expression pattern to match. It 339 * must not be {@code null}. 340 */ 341 public RegularExpressionJSONObjectFilter(@NotNull final List<String> field, 342 @NotNull final Pattern regularExpression) 343 { 344 Validator.ensureNotNull(field); 345 Validator.ensureFalse(field.isEmpty()); 346 347 Validator.ensureNotNull(regularExpression); 348 349 this.field = Collections.unmodifiableList(new ArrayList<>(field)); 350 this.regularExpression = regularExpression; 351 352 matchAllElements = false; 353 } 354 355 356 357 /** 358 * Retrieves the field path specifier for this filter. 359 * 360 * @return The field path specifier for this filter. 361 */ 362 @NotNull() 363 public List<String> getField() 364 { 365 return field; 366 } 367 368 369 370 /** 371 * Sets the field path specifier for this filter. 372 * 373 * @param field The field path specifier for this filter. It must not be 374 * {@code null} or empty. See the class-level documentation 375 * for the {@link JSONObjectFilter} class for information about 376 * field path specifiers. 377 */ 378 public void setField(@NotNull final String... field) 379 { 380 setField(StaticUtils.toList(field)); 381 } 382 383 384 385 /** 386 * Sets the field path specifier for this filter. 387 * 388 * @param field The field path specifier for this filter. It must not be 389 * {@code null} or empty. See the class-level documentation 390 * for the {@link JSONObjectFilter} class for information about 391 * field path specifiers. 392 */ 393 public void setField(@NotNull final List<String> field) 394 { 395 Validator.ensureNotNull(field); 396 Validator.ensureFalse(field.isEmpty()); 397 398 this.field= Collections.unmodifiableList(new ArrayList<>(field)); 399 } 400 401 402 403 /** 404 * Retrieves the regular expression pattern for this filter. 405 * 406 * @return The regular expression pattern for this filter. 407 */ 408 @NotNull() 409 public Pattern getRegularExpression() 410 { 411 return regularExpression; 412 } 413 414 415 416 /** 417 * Specifies the regular expression for this filter. 418 * 419 * @param regularExpression The regular expression to match. It must not 420 * be {@code null}, and it must be compatible for 421 * use with the {@code java.util.regex.Pattern} 422 * class. 423 * 424 * @throws JSONException If the provided string cannot be parsed as a valid 425 * regular expression. 426 */ 427 public void setRegularExpression(@NotNull final String regularExpression) 428 throws JSONException 429 { 430 Validator.ensureNotNull(regularExpression); 431 432 try 433 { 434 this.regularExpression = Pattern.compile(regularExpression); 435 } 436 catch (final Exception e) 437 { 438 Debug.debugException(e); 439 throw new JSONException( 440 ERR_REGEX_FILTER_INVALID_REGEX.get(regularExpression, 441 StaticUtils.getExceptionMessage(e)), 442 e); 443 } 444 } 445 446 447 448 /** 449 * Specifies the regular expression for this filter. 450 * 451 * @param regularExpression The regular expression pattern to match. It 452 * must not be {@code null}. 453 */ 454 public void setRegularExpression(@NotNull final Pattern regularExpression) 455 { 456 Validator.ensureNotNull(regularExpression); 457 458 this.regularExpression = regularExpression; 459 } 460 461 462 463 /** 464 * Indicates whether, if the target field is an array of values, the regular 465 * expression will be required to match all elements in the array rather than 466 * at least one element. 467 * 468 * @return {@code true} if the regular expression will be required to match 469 * all elements of an array, or {@code false} if it will only be 470 * required to match at least one element. 471 */ 472 public boolean matchAllElements() 473 { 474 return matchAllElements; 475 } 476 477 478 479 /** 480 * Specifies whether the regular expression will be required to match all 481 * elements of an array rather than at least one element. 482 * 483 * @param matchAllElements Indicates whether the regular expression will be 484 * required to match all elements of an array rather 485 * than at least one element. 486 */ 487 public void setMatchAllElements(final boolean matchAllElements) 488 { 489 this.matchAllElements = matchAllElements; 490 } 491 492 493 494 /** 495 * {@inheritDoc} 496 */ 497 @Override() 498 @NotNull() 499 public String getFilterType() 500 { 501 return FILTER_TYPE; 502 } 503 504 505 506 /** 507 * {@inheritDoc} 508 */ 509 @Override() 510 @NotNull() 511 protected Set<String> getRequiredFieldNames() 512 { 513 return REQUIRED_FIELD_NAMES; 514 } 515 516 517 518 /** 519 * {@inheritDoc} 520 */ 521 @Override() 522 @NotNull() 523 protected Set<String> getOptionalFieldNames() 524 { 525 return OPTIONAL_FIELD_NAMES; 526 } 527 528 529 530 /** 531 * {@inheritDoc} 532 */ 533 @Override() 534 public boolean matchesJSONObject(@NotNull final JSONObject o) 535 { 536 final List<JSONValue> candidates = getValues(o, field); 537 if (candidates.isEmpty()) 538 { 539 return false; 540 } 541 542 for (final JSONValue v : candidates) 543 { 544 if (v instanceof JSONString) 545 { 546 final Matcher matcher = 547 regularExpression.matcher(((JSONString) v).stringValue()); 548 if (matcher.matches()) 549 { 550 return true; 551 } 552 } 553 else if (v instanceof JSONArray) 554 { 555 boolean matchOne = false; 556 boolean matchAll = true; 557 for (final JSONValue arrayValue : ((JSONArray) v).getValues()) 558 { 559 if (! (arrayValue instanceof JSONString)) 560 { 561 matchAll = false; 562 if (matchAllElements) 563 { 564 break; 565 } 566 } 567 568 final Matcher matcher = regularExpression.matcher( 569 ((JSONString) arrayValue).stringValue()); 570 if (matcher.matches()) 571 { 572 if (! matchAllElements) 573 { 574 return true; 575 } 576 matchOne = true; 577 } 578 else 579 { 580 matchAll = false; 581 if (matchAllElements) 582 { 583 break; 584 } 585 } 586 } 587 588 if (matchOne && matchAll) 589 { 590 return true; 591 } 592 } 593 } 594 595 return false; 596 } 597 598 599 600 /** 601 * {@inheritDoc} 602 */ 603 @Override() 604 @NotNull() 605 public JSONObject toJSONObject() 606 { 607 final LinkedHashMap<String,JSONValue> fields = 608 new LinkedHashMap<>(StaticUtils.computeMapCapacity(4)); 609 610 fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE)); 611 612 if (field.size() == 1) 613 { 614 fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0))); 615 } 616 else 617 { 618 final ArrayList<JSONValue> fieldNameValues = 619 new ArrayList<>(field.size()); 620 for (final String s : field) 621 { 622 fieldNameValues.add(new JSONString(s)); 623 } 624 fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues)); 625 } 626 627 fields.put(FIELD_REGULAR_EXPRESSION, 628 new JSONString(regularExpression.toString())); 629 630 if (matchAllElements) 631 { 632 fields.put(FIELD_MATCH_ALL_ELEMENTS, JSONBoolean.TRUE); 633 } 634 635 return new JSONObject(fields); 636 } 637 638 639 640 /** 641 * {@inheritDoc} 642 */ 643 @Override() 644 @NotNull() 645 public JSONObject toNormalizedJSONObject() 646 { 647 return toJSONObject(); 648 } 649 650 651 652 /** 653 * {@inheritDoc} 654 */ 655 @Override() 656 @NotNull() 657 protected RegularExpressionJSONObjectFilter decodeFilter( 658 @NotNull final JSONObject filterObject) 659 throws JSONException 660 { 661 final List<String> fieldPath = 662 getStrings(filterObject, FIELD_FIELD_PATH, false, null); 663 664 final String regex = getString(filterObject, FIELD_REGULAR_EXPRESSION, 665 null, true); 666 667 final Pattern pattern; 668 try 669 { 670 pattern = Pattern.compile(regex); 671 } 672 catch (final Exception e) 673 { 674 Debug.debugException(e); 675 throw new JSONException( 676 ERR_REGEX_FILTER_DECODE_INVALID_REGEX.get( 677 String.valueOf(filterObject), FIELD_REGULAR_EXPRESSION, 678 fieldPathToName(fieldPath), StaticUtils.getExceptionMessage(e)), 679 e); 680 } 681 682 final boolean matchAll = 683 getBoolean(filterObject, FIELD_MATCH_ALL_ELEMENTS, false); 684 685 return new RegularExpressionJSONObjectFilter(fieldPath, pattern, matchAll); 686 } 687}