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 Alcatel-Lucent 8661
059 *   server products.  These classes provide support for proprietary
060 *   functionality or for external specifications that are not considered stable
061 *   or mature enough to be guaranteed to work in an interoperable way with
062 *   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<String>(
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<String>(
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<String>(field));
305    this.values =
306         Collections.unmodifiableList(new ArrayList<JSONValue>(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<String>(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<JSONValue>(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 =
426         new ArrayList<JSONValue>(values.length);
427    for (final String s : values)
428    {
429      valueList.add(new JSONString(s));
430    }
431    return valueList;
432  }
433
434
435
436  /**
437   * Indicates whether string matching should be performed in a case-sensitive
438   * manner.
439   *
440   * @return  {@code true} if string matching should be case sensitive, or
441   *          {@code false} if not.
442   */
443  public boolean caseSensitive()
444  {
445    return caseSensitive;
446  }
447
448
449
450  /**
451   * Specifies whether string matching should be performed in a case-sensitive
452   * manner.
453   *
454   * @param  caseSensitive  Indicates whether string matching should be
455   *                        case sensitive.
456   */
457  public void setCaseSensitive(final boolean caseSensitive)
458  {
459    this.caseSensitive = caseSensitive;
460  }
461
462
463
464  /**
465   * {@inheritDoc}
466   */
467  @Override()
468  public String getFilterType()
469  {
470    return FILTER_TYPE;
471  }
472
473
474
475  /**
476   * {@inheritDoc}
477   */
478  @Override()
479  protected Set<String> getRequiredFieldNames()
480  {
481    return REQUIRED_FIELD_NAMES;
482  }
483
484
485
486  /**
487   * {@inheritDoc}
488   */
489  @Override()
490  protected Set<String> getOptionalFieldNames()
491  {
492    return OPTIONAL_FIELD_NAMES;
493  }
494
495
496
497  /**
498   * {@inheritDoc}
499   */
500  @Override()
501  public boolean matchesJSONObject(final JSONObject o)
502  {
503    final List<JSONValue> candidates = getValues(o, field);
504    if (candidates.isEmpty())
505    {
506      return false;
507    }
508
509    for (final JSONValue objectValue : candidates)
510    {
511      for (final JSONValue filterValue : values)
512      {
513        if (filterValue.equals(objectValue, false, (! caseSensitive), false))
514        {
515          return true;
516        }
517      }
518
519      if (objectValue instanceof JSONArray)
520      {
521        final JSONArray a = (JSONArray) objectValue;
522        for (final JSONValue filterValue : values)
523        {
524          if (a.contains(filterValue, false, (!caseSensitive), false, false))
525          {
526            return true;
527          }
528        }
529      }
530    }
531
532    return false;
533  }
534
535
536
537  /**
538   * {@inheritDoc}
539   */
540  @Override()
541  public JSONObject toJSONObject()
542  {
543    final LinkedHashMap<String,JSONValue> fields =
544         new LinkedHashMap<String,JSONValue>(4);
545
546    fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE));
547
548    if (field.size() == 1)
549    {
550      fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0)));
551    }
552    else
553    {
554      final ArrayList<JSONValue> fieldNameValues =
555           new ArrayList<JSONValue>(field.size());
556      for (final String s : field)
557      {
558        fieldNameValues.add(new JSONString(s));
559      }
560      fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues));
561    }
562
563    fields.put(FIELD_VALUES, new JSONArray(values));
564
565    if (caseSensitive)
566    {
567      fields.put(FIELD_CASE_SENSITIVE, JSONBoolean.TRUE);
568    }
569
570    return new JSONObject(fields);
571  }
572
573
574
575  /**
576   * {@inheritDoc}
577   */
578  @Override()
579  protected EqualsAnyJSONObjectFilter decodeFilter(
580                                           final JSONObject filterObject)
581            throws JSONException
582  {
583    final List<String> fieldPath =
584         getStrings(filterObject, FIELD_FIELD_PATH, false, null);
585
586    final boolean isCaseSensitive = getBoolean(filterObject,
587         FIELD_CASE_SENSITIVE, false);
588
589    final JSONValue arrayValue = filterObject.getField(FIELD_VALUES);
590    if (arrayValue instanceof JSONArray)
591    {
592      return new EqualsAnyJSONObjectFilter(fieldPath,
593           ((JSONArray) arrayValue).getValues(), isCaseSensitive);
594    }
595    else
596    {
597      throw new JSONException(ERR_OBJECT_FILTER_VALUE_NOT_ARRAY.get(
598           String.valueOf(filterObject), FILTER_TYPE, FIELD_VALUES));
599    }
600  }
601}