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.Collections;
028    import java.util.HashSet;
029    import java.util.LinkedHashMap;
030    import java.util.List;
031    import java.util.Set;
032    
033    import com.unboundid.util.Debug;
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.JSONException;
041    import com.unboundid.util.json.JSONObject;
042    import com.unboundid.util.json.JSONString;
043    import com.unboundid.util.json.JSONValue;
044    
045    import static com.unboundid.ldap.sdk.unboundidds.jsonfilter.JFMessages.*;
046    
047    
048    
049    /**
050     * <BLOCKQUOTE>
051     *   <B>NOTE:</B>  This class is part of the Commercial Edition of the UnboundID
052     *   LDAP SDK for Java.  It is not available for use in applications that
053     *   include only the Standard Edition of the LDAP SDK, and is not supported for
054     *   use in conjunction with non-UnboundID products.
055     * </BLOCKQUOTE>
056     * This class provides an implementation of a JSON object filter that can be
057     * used to identify JSON objects that have a field whose value is a JSON object
058     * that matches a provided JSON object filter, or a field whose value is an
059     * array that contains at least one JSON object that matches the provided
060     * filter.
061     * <BR><BR>
062     * The fields that are required to be included in an "object matches" filter
063     * 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 array
068     *     of strings as described in the "Targeting Fields in JSON Objects" section
069     *     of the class-level documentation for {@link JSONObjectFilter}.  The value
070     *     of the target field is expected to either be a JSON object or an array
071     *     that contains one or more JSON objects.
072     *   </LI>
073     *   <LI>
074     *     {@code filter} -- A JSON object that represents a valid JSON object
075     *     filter to match against any JSON object(s) in the value of the target
076     *     field.  Note that field name references in this filter should be
077     *     relative to the object in the value of the target field, not to the
078     *     other JSON object that contains that field.
079     *   </LI>
080     * </UL>
081     * <H2>Example</H2>
082     * The following is an example of an "object matches" filter that will match
083     * any JSON object with a top-level field named "contact" whose value is a JSON
084     * object (or an array containing one or more JSON objects) with a "type" field
085     * with a value of "home" and a "email" field with any value:
086     * <PRE>
087     *   { "filterType" : "objectMatches",
088     *     "field" : "contact",
089     *     "filter" : {
090     *       "filterType" : "and",
091     *       "andFilters" : [
092     *         { "filterType" : "equals",
093     *           "field" : "type",
094     *           "value" : "home" },
095     *         { "filterType" : "containsField",
096     *           "field" : "email" } ] } }
097     * </PRE>
098     * The above filter can be created with the code:
099     * <PRE>
100     *   ObjectMatchesJSONObjectFilter filter = new ObjectMatchesJSONObjectFilter(
101     *        "contact",
102     *        new ANDJSONObjectFilter(
103     *             new EqualsJSONObjectFilter("type", "home"),
104     *             new ContainsFieldJSONObjectFilter("email")));
105     * </PRE>
106     */
107    @Mutable()
108    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
109    public final class ObjectMatchesJSONObjectFilter
110           extends JSONObjectFilter
111    {
112      /**
113       * The value that should be used for the filterType element of the JSON object
114       * that represents an "object matches" filter.
115       */
116      public static final String FILTER_TYPE = "objectMatches";
117    
118    
119    
120      /**
121       * The name of the JSON field that is used to specify the field in the target
122       * JSON object for which to make the determination.
123       */
124      public static final String FIELD_FIELD_PATH = "field";
125    
126    
127    
128      /**
129       * The name of the JSON field that is used to specify the filter to match
130       * against the object in the target field.
131       */
132      public static final String FIELD_FILTER = "filter";
133    
134    
135    
136      /**
137       * The pre-allocated set of required field names.
138       */
139      private static final Set<String> REQUIRED_FIELD_NAMES =
140           Collections.unmodifiableSet(new HashSet<String>(
141                Arrays.asList(FIELD_FIELD_PATH, FIELD_FILTER)));
142    
143    
144    
145      /**
146       * The pre-allocated set of optional field names.
147       */
148      private static final Set<String> OPTIONAL_FIELD_NAMES =
149           Collections.emptySet();
150    
151    
152    
153      /**
154       * The serial version UID for this serializable class.
155       */
156      private static final long serialVersionUID = 7138078723547160420L;
157    
158    
159    
160      // The filter to match against the object(s) in the target field.
161      private volatile JSONObjectFilter filter;
162    
163      // The field path specifier for the target field.
164      private volatile List<String> field;
165    
166    
167    
168      /**
169       * Creates an instance of this filter type that can only be used for decoding
170       * JSON objects as "object matches" filters.  It cannot be used as a regular
171       * "object matches" filter.
172       */
173      ObjectMatchesJSONObjectFilter()
174      {
175        field = null;
176        filter = null;
177      }
178    
179    
180    
181      /**
182       * Creates a new instance of this filter type with the provided information.
183       *
184       * @param  field   The name of the top-level field to target with this filter.
185       *                 It must not be {@code null} .  See the class-level
186       *                 documentation for the {@link JSONObjectFilter} class for
187       *                 information about field path specifiers.
188       * @param  filter  The filter that will be matched against JSON objects
189       *                 contained in the specified field.
190       */
191      public ObjectMatchesJSONObjectFilter(final String field,
192                                           final JSONObjectFilter filter)
193      {
194        this(Collections.singletonList(field), filter);
195      }
196    
197    
198    
199      /**
200       * Creates a new instance of this filter type with the provided information.
201       *
202       * @param  field   The field path specifier for this filter.  It must not be
203       *                 {@code null} or empty.  See the class-level documentation
204       *                 for the {@link JSONObjectFilter} class for information
205       *                 about field path specifiers.
206       * @param  filter  The filter that will be matched against JSON objects
207       *                 contained in the specified field.
208       */
209      public ObjectMatchesJSONObjectFilter(final List<String> field,
210                                           final JSONObjectFilter filter)
211      {
212        Validator.ensureNotNull(field);
213        Validator.ensureFalse(field.isEmpty());
214    
215        Validator.ensureNotNull(filter);
216    
217        this.field = Collections.unmodifiableList(new ArrayList<String>(field));
218        this.filter = filter;
219      }
220    
221    
222    
223      /**
224       * Retrieves the field path specifier for this filter.
225       *
226       * @return  The field path specifier for this filter.
227       */
228      public List<String> getField()
229      {
230        return field;
231      }
232    
233    
234    
235      /**
236       * Sets the field path specifier for this filter.
237       *
238       * @param  field  The field path specifier for this filter.  It must not be
239       *                {@code null} or empty.  See the class-level documentation
240       *                for the {@link JSONObjectFilter} class for information about
241       *                field path specifiers.
242       */
243      public void setField(final String... field)
244      {
245        setField(StaticUtils.toList(field));
246      }
247    
248    
249    
250      /**
251       * Sets the field path specifier for this filter.
252       *
253       * @param  field  The field path specifier for this filter.  It must not be
254       *                {@code null} or empty.  See the class-level documentation
255       *                for the {@link JSONObjectFilter} class for information about
256       *                field path specifiers.
257       */
258      public void setField(final List<String> field)
259      {
260        Validator.ensureNotNull(field);
261        Validator.ensureFalse(field.isEmpty());
262    
263        this.field = Collections.unmodifiableList(new ArrayList<String>(field));
264      }
265    
266    
267    
268      /**
269       * Retrieves the filter that will be matched against any JSON objects
270       * contained in the value of the specified field.
271       *
272       * @return  The filter that will be matched against any JSON objects contained
273       *          in the value of the specified field.
274       */
275      public JSONObjectFilter getFilter()
276      {
277        return filter;
278      }
279    
280    
281    
282      /**
283       * Specifies the filter that will be matched against any JSON objects
284       * contained in the value of the specified field.
285       *
286       * @param  filter  The filter that will be matched against any JSON objects
287       *                 contained in the value of the specified field.  It must
288       *                 not be {@code null}.
289       */
290      public void setFilter(final JSONObjectFilter filter)
291      {
292        Validator.ensureNotNull(filter);
293    
294        this.filter = filter;
295      }
296    
297    
298    
299      /**
300       * {@inheritDoc}
301       */
302      @Override()
303      public String getFilterType()
304      {
305        return FILTER_TYPE;
306      }
307    
308    
309    
310      /**
311       * {@inheritDoc}
312       */
313      @Override()
314      protected Set<String> getRequiredFieldNames()
315      {
316        return REQUIRED_FIELD_NAMES;
317      }
318    
319    
320    
321      /**
322       * {@inheritDoc}
323       */
324      @Override()
325      protected Set<String> getOptionalFieldNames()
326      {
327        return OPTIONAL_FIELD_NAMES;
328      }
329    
330    
331    
332      /**
333       * {@inheritDoc}
334       */
335      @Override()
336      public boolean matchesJSONObject(final JSONObject o)
337      {
338        final List<JSONValue> candidates = getValues(o, field);
339        if (candidates.isEmpty())
340        {
341          return false;
342        }
343    
344        for (final JSONValue v : candidates)
345        {
346          if (v instanceof JSONObject)
347          {
348            if (filter.matchesJSONObject((JSONObject) v))
349            {
350              return true;
351            }
352          }
353          else if (v instanceof JSONArray)
354          {
355            for (final JSONValue arrayValue : ((JSONArray) v).getValues())
356            {
357              if ((arrayValue instanceof JSONObject) &&
358                  filter.matchesJSONObject((JSONObject) arrayValue))
359              {
360                return true;
361              }
362            }
363          }
364        }
365    
366        return false;
367      }
368    
369    
370    
371      /**
372       * {@inheritDoc}
373       */
374      @Override()
375      public JSONObject toJSONObject()
376      {
377        final LinkedHashMap<String,JSONValue> fields =
378             new LinkedHashMap<String,JSONValue>(3);
379    
380        fields.put(FIELD_FILTER_TYPE, new JSONString(FILTER_TYPE));
381    
382        if (field.size() == 1)
383        {
384          fields.put(FIELD_FIELD_PATH, new JSONString(field.get(0)));
385        }
386        else
387        {
388          final ArrayList<JSONValue> fieldNameValues =
389               new ArrayList<JSONValue>(field.size());
390          for (final String s : field)
391          {
392            fieldNameValues.add(new JSONString(s));
393          }
394          fields.put(FIELD_FIELD_PATH, new JSONArray(fieldNameValues));
395        }
396    
397        fields.put(FIELD_FILTER, filter.toJSONObject());
398    
399        return new JSONObject(fields);
400      }
401    
402    
403    
404      /**
405       * {@inheritDoc}
406       */
407      @Override()
408      protected ObjectMatchesJSONObjectFilter decodeFilter(
409                                                   final JSONObject filterObject)
410                throws JSONException
411      {
412        final List<String> fieldPath =
413             getStrings(filterObject, FIELD_FIELD_PATH, false, null);
414    
415        final JSONValue v = filterObject.getField(FIELD_FILTER);
416        if (v == null)
417        {
418          throw new JSONException(ERR_OBJECT_FILTER_MISSING_REQUIRED_FIELD.get(
419               String.valueOf(filterObject), FILTER_TYPE, FIELD_FILTER));
420        }
421    
422        if (! (v instanceof JSONObject))
423        {
424          throw new JSONException(ERR_OBJECT_FILTER_VALUE_NOT_OBJECT.get(
425               String.valueOf(filterObject), FILTER_TYPE, FIELD_FILTER));
426        }
427    
428        try
429        {
430          return new ObjectMatchesJSONObjectFilter(fieldPath,
431               JSONObjectFilter.decode((JSONObject) v));
432        }
433        catch (final JSONException e)
434        {
435          Debug.debugException(e);
436          throw new JSONException(
437               ERR_OBJECT_FILTER_VALUE_NOT_FILTER.get(String.valueOf(filterObject),
438                    FILTER_TYPE, FIELD_FILTER, e.getMessage()),
439               e);
440        }
441      }
442    }