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