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}