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