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