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