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