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