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