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}