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