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