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