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 }