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