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