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