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