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