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