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}