001/*
002 * Copyright 2015-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2015-2019 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 =
583         new LinkedHashMap<>(StaticUtils.computeMapCapacity(4));
584
585    fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE));
586
587    if (field.size() == 1)
588    {
589      fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0)));
590    }
591    else
592    {
593      final ArrayList<JSONValue> fieldNameValues =
594           new ArrayList<>(field.size());
595      for (final String s : field)
596      {
597        fieldNameValues.add(new JSONString(s));
598      }
599      fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues));
600    }
601
602    fields.put(FIELD_REGULAR_EXPRESSION,
603         new JSONString(regularExpression.toString()));
604
605    if (matchAllElements)
606    {
607      fields.put(FIELD_MATCH_ALL_ELEMENTS, JSONBoolean.TRUE);
608    }
609
610    return new JSONObject(fields);
611  }
612
613
614
615  /**
616   * {@inheritDoc}
617   */
618  @Override()
619  protected RegularExpressionJSONObjectFilter decodeFilter(
620                 final JSONObject filterObject)
621            throws JSONException
622  {
623    final List<String> fieldPath =
624         getStrings(filterObject, FIELD_FIELD_PATH, false, null);
625
626    final String regex = getString(filterObject, FIELD_REGULAR_EXPRESSION,
627         null, true);
628
629    final Pattern pattern;
630    try
631    {
632      pattern = Pattern.compile(regex);
633    }
634    catch (final Exception e)
635    {
636      Debug.debugException(e);
637      throw new JSONException(
638           ERR_REGEX_FILTER_DECODE_INVALID_REGEX.get(
639                String.valueOf(filterObject), FIELD_REGULAR_EXPRESSION,
640                fieldPathToName(fieldPath), StaticUtils.getExceptionMessage(e)),
641           e);
642    }
643
644    final boolean matchAll =
645         getBoolean(filterObject, FIELD_MATCH_ALL_ELEMENTS, false);
646
647    return new RegularExpressionJSONObjectFilter(fieldPath, pattern, matchAll);
648  }
649}