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