001/* 002 * Copyright 2020-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2020-2023 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) 2020-2023 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.listener; 037 038 039 040import java.io.Serializable; 041import java.util.ArrayList; 042import java.util.Collections; 043import java.util.HashMap; 044import java.util.List; 045import java.util.Map; 046 047import com.unboundid.ldap.sdk.Attribute; 048import com.unboundid.ldap.sdk.Entry; 049import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition; 050import com.unboundid.ldap.sdk.schema.ObjectClassDefinition; 051import com.unboundid.ldap.sdk.schema.Schema; 052import com.unboundid.util.NotNull; 053import com.unboundid.util.Nullable; 054import com.unboundid.util.ObjectPair; 055import com.unboundid.util.StaticUtils; 056import com.unboundid.util.ThreadSafety; 057import com.unboundid.util.ThreadSafetyLevel; 058 059 060 061/** 062 * This class provides support methods for paring search result entries based 063 * on a given set of requested attributes. 064 */ 065@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 066public final class SearchEntryParer 067 implements Serializable 068{ 069 /** 070 * The serial version UID for this serializable class. 071 */ 072 private static final long serialVersionUID = -3249960583816464391L; 073 074 075 076 // Indicates whether to include all operational attributes. 077 private final boolean allOperationalAttributes; 078 079 // Indicates whether to include all user attributes. 080 private final boolean allUserAttributes; 081 082 // The list of requested attributes for use when paring entries. 083 @NotNull private final List<String> requestedAttributes; 084 085 // A map of specific attribute types to be returned. The keys of the map will 086 // be the lowercase OIDs and names of each attribute types, and the values 087 // will be a list of option sets for the associated attribute type. 088 @NotNull private final Map<String,List<List<String>>> attributeTypesToReturn; 089 090 // The schema to use in processing. 091 @Nullable private final Schema schema; 092 093 094 095 /** 096 * Creates a new search entry parer for the provided set of requested 097 * attributes. 098 * 099 * @param requestedAttributes The list of requested attributes for use when 100 * paring entries. It must not be {@code null}, 101 * but may be empty. 102 * @param schema The schema to use when paring entries. It may 103 * be {@code null} if no schema is available. 104 */ 105 public SearchEntryParer(@NotNull final List<String> requestedAttributes, 106 @Nullable final Schema schema) 107 { 108 this.schema = schema; 109 this.requestedAttributes = 110 Collections.unmodifiableList(new ArrayList<>(requestedAttributes)); 111 112 if (requestedAttributes.isEmpty()) 113 { 114 allUserAttributes = true; 115 allOperationalAttributes = false; 116 attributeTypesToReturn = Collections.emptyMap(); 117 return; 118 } 119 120 boolean allUserAttrs = false; 121 boolean allOpAttrs = false; 122 final Map<String,List<List<String>>> m = new HashMap<>( 123 StaticUtils.computeMapCapacity(requestedAttributes.size())); 124 for (final String s : requestedAttributes) 125 { 126 if (s.equals("*")) 127 { 128 allUserAttrs = true; 129 } 130 else if (s.equals("+")) 131 { 132 allOpAttrs = true; 133 } 134 else if (s.startsWith("@")) 135 { 136 // Return attributes by object class. This can only be supported if a 137 // schema has been defined. 138 if (schema != null) 139 { 140 final String ocName = s.substring(1); 141 final ObjectClassDefinition oc = schema.getObjectClass(ocName); 142 if (oc != null) 143 { 144 for (final AttributeTypeDefinition at : 145 oc.getRequiredAttributes(schema, true)) 146 { 147 addAttributeOIDAndNames(at, m, Collections.<String>emptyList(), 148 schema); 149 } 150 for (final AttributeTypeDefinition at : 151 oc.getOptionalAttributes(schema, true)) 152 { 153 addAttributeOIDAndNames(at, m, Collections.<String>emptyList(), 154 schema); 155 } 156 } 157 } 158 } 159 else 160 { 161 final ObjectPair<String,List<String>> nameWithOptions = 162 getNameWithOptions(s); 163 if (nameWithOptions == null) 164 { 165 continue; 166 } 167 168 final String name = nameWithOptions.getFirst(); 169 final List<String> options = nameWithOptions.getSecond(); 170 171 if (schema == null) 172 { 173 // Just use the name as provided. 174 List<List<String>> optionLists = m.get(name); 175 if (optionLists == null) 176 { 177 optionLists = new ArrayList<>(1); 178 m.put(name, optionLists); 179 } 180 optionLists.add(options); 181 } 182 else 183 { 184 // If the attribute type is defined in the schema, then use it to get 185 // all names and the OID. Otherwise, just use the name as provided. 186 final AttributeTypeDefinition at = schema.getAttributeType(name); 187 if (at == null) 188 { 189 List<List<String>> optionLists = m.get(name); 190 if (optionLists == null) 191 { 192 optionLists = new ArrayList<>(1); 193 m.put(name, optionLists); 194 } 195 optionLists.add(options); 196 } 197 else 198 { 199 addAttributeOIDAndNames(at, m, options, schema); 200 } 201 } 202 } 203 } 204 205 allUserAttributes = allUserAttrs; 206 allOperationalAttributes = allOpAttrs; 207 attributeTypesToReturn = Collections.unmodifiableMap(m); 208 } 209 210 211 212 /** 213 * Parses the provided string into an attribute type and set of options. 214 * 215 * @param s The string to be parsed. 216 * 217 * @return An {@code ObjectPair} in which the first element is the attribute 218 * type name and the second is the list of options (or an empty 219 * list if there are no options). Alternately, a value of 220 * {@code null} may be returned if the provided string does not 221 * represent a valid attribute type description. 222 */ 223 @NotNull() 224 private static ObjectPair<String,List<String>> getNameWithOptions( 225 @NotNull final String s) 226 { 227 if (! Attribute.nameIsValid(s, true)) 228 { 229 return null; 230 } 231 232 final String l = StaticUtils.toLowerCase(s); 233 234 int semicolonPos = l.indexOf(';'); 235 if (semicolonPos < 0) 236 { 237 return new ObjectPair<>(l, Collections.<String>emptyList()); 238 } 239 240 final String name = l.substring(0, semicolonPos); 241 final ArrayList<String> optionList = new ArrayList<>(1); 242 while (true) 243 { 244 final int nextSemicolonPos = l.indexOf(';', semicolonPos+1); 245 if (nextSemicolonPos < 0) 246 { 247 optionList.add(l.substring(semicolonPos+1)); 248 break; 249 } 250 else 251 { 252 optionList.add(l.substring(semicolonPos+1, nextSemicolonPos)); 253 semicolonPos = nextSemicolonPos; 254 } 255 } 256 257 return new ObjectPair<String,List<String>>(name, optionList); 258 } 259 260 261 262 /** 263 * Adds all-lowercase versions of the OID and all names for the provided 264 * attribute type definition to the given map with the given options. 265 * 266 * @param d The attribute type definition to process. 267 * @param m The map to which the OID and names should be added. 268 * @param o The array of attribute options to use in the map. It should be 269 * empty if no options are needed, and must not be {@code null}. 270 * @param s The schema to use when processing. 271 */ 272 private static void addAttributeOIDAndNames( 273 @Nullable final AttributeTypeDefinition d, 274 @NotNull final Map<String,List<List<String>>> m, 275 @NotNull final List<String> o, 276 @Nullable final Schema s) 277 { 278 if (d == null) 279 { 280 return; 281 } 282 283 final String lowerOID = StaticUtils.toLowerCase(d.getOID()); 284 if (lowerOID != null) 285 { 286 List<List<String>> l = m.get(lowerOID); 287 if (l == null) 288 { 289 l = new ArrayList<>(1); 290 m.put(lowerOID, l); 291 } 292 293 l.add(o); 294 } 295 296 for (final String name : d.getNames()) 297 { 298 final String lowerName = StaticUtils.toLowerCase(name); 299 List<List<String>> l = m.get(lowerName); 300 if (l == null) 301 { 302 l = new ArrayList<>(1); 303 m.put(lowerName, l); 304 } 305 306 l.add(o); 307 } 308 309 // If a schema is available, then see if the attribute type has any 310 // subordinate types. If so, then add them. 311 if (s != null) 312 { 313 for (final AttributeTypeDefinition subordinateType : 314 s.getSubordinateAttributeTypes(d)) 315 { 316 addAttributeOIDAndNames(subordinateType, m, o, s); 317 } 318 } 319 } 320 321 322 323 /** 324 * Retrieves the set of requested attributes used to create this search entry 325 * parer. 326 * 327 * @return The set of requested attributes used to create this search entry 328 * parer. 329 */ 330 @NotNull() 331 public List<String> getRequestedAttributes() 332 { 333 return requestedAttributes; 334 } 335 336 337 338 /** 339 * Retrieves a copy of the provided entry that includes only the appropriate 340 * set of requested attributes. 341 * 342 * @param entry The entry to be pared. 343 * 344 * @return A copy of the provided entry that includes only the appropriate 345 * set of requested attributes. 346 */ 347 @NotNull() 348 public Entry pareEntry(@NotNull final Entry entry) 349 { 350 // See if we can return the entry without paring it down. 351 if (allUserAttributes) 352 { 353 if (allOperationalAttributes || (schema == null)) 354 { 355 return entry.duplicate(); 356 } 357 } 358 359 360 // If we've gotten here, then we may only need to return a partial entry. 361 final Entry copy = new Entry(entry.getDN(), schema); 362 363 for (final Attribute a : entry.getAttributes()) 364 { 365 final ObjectPair<String,List<String>> nameWithOptions = 366 getNameWithOptions(a.getName()); 367 final String name = nameWithOptions.getFirst(); 368 final List<String> options = nameWithOptions.getSecond(); 369 370 // If there is a schema, then see if it is an operational attribute, since 371 // that needs to be handled in a manner different from user attributes 372 if (schema != null) 373 { 374 final AttributeTypeDefinition at = schema.getAttributeType(name); 375 if ((at != null) && at.isOperational()) 376 { 377 if (allOperationalAttributes) 378 { 379 copy.addAttribute(a); 380 continue; 381 } 382 383 final List<List<String>> optionLists = 384 attributeTypesToReturn.get(name); 385 if (optionLists == null) 386 { 387 continue; 388 } 389 390 for (final List<String> optionList : optionLists) 391 { 392 boolean matchAll = true; 393 for (final String option : optionList) 394 { 395 if (! options.contains(option)) 396 { 397 matchAll = false; 398 break; 399 } 400 } 401 402 if (matchAll) 403 { 404 copy.addAttribute(a); 405 break; 406 } 407 } 408 continue; 409 } 410 } 411 412 // We'll assume that it's a user attribute, and we'll look for an exact 413 // match on the base name. 414 if (allUserAttributes) 415 { 416 copy.addAttribute(a); 417 continue; 418 } 419 420 final List<List<String>> optionLists = attributeTypesToReturn.get(name); 421 if (optionLists == null) 422 { 423 continue; 424 } 425 426 for (final List<String> optionList : optionLists) 427 { 428 boolean matchAll = true; 429 for (final String option : optionList) 430 { 431 if (! options.contains(option)) 432 { 433 matchAll = false; 434 break; 435 } 436 } 437 438 if (matchAll) 439 { 440 copy.addAttribute(a); 441 break; 442 } 443 } 444 } 445 446 return copy; 447 } 448}