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