001/* 002 * Copyright 2020-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2020-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) 2020-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.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). 220 */ 221 @NotNull() 222 private static ObjectPair<String,List<String>> getNameWithOptions( 223 @NotNull final String s) 224 { 225 final String l = StaticUtils.toLowerCase(s); 226 227 int semicolonPos = l.indexOf(';'); 228 if (semicolonPos < 0) 229 { 230 return new ObjectPair<>(l, Collections.<String>emptyList()); 231 } 232 233 final String name = l.substring(0, semicolonPos); 234 final ArrayList<String> optionList = new ArrayList<>(1); 235 while (true) 236 { 237 final int nextSemicolonPos = l.indexOf(';', semicolonPos+1); 238 if (nextSemicolonPos < 0) 239 { 240 optionList.add(l.substring(semicolonPos+1)); 241 break; 242 } 243 else 244 { 245 optionList.add(l.substring(semicolonPos+1, nextSemicolonPos)); 246 semicolonPos = nextSemicolonPos; 247 } 248 } 249 250 return new ObjectPair<String,List<String>>(name, optionList); 251 } 252 253 254 255 /** 256 * Adds all-lowercase versions of the OID and all names for the provided 257 * attribute type definition to the given map with the given options. 258 * 259 * @param d The attribute type definition to process. 260 * @param m The map to which the OID and names should be added. 261 * @param o The array of attribute options to use in the map. It should be 262 * empty if no options are needed, and must not be {@code null}. 263 * @param s The schema to use when processing. 264 */ 265 private static void addAttributeOIDAndNames( 266 @Nullable final AttributeTypeDefinition d, 267 @NotNull final Map<String,List<List<String>>> m, 268 @NotNull final List<String> o, 269 @Nullable final Schema s) 270 { 271 if (d == null) 272 { 273 return; 274 } 275 276 final String lowerOID = StaticUtils.toLowerCase(d.getOID()); 277 if (lowerOID != null) 278 { 279 List<List<String>> l = m.get(lowerOID); 280 if (l == null) 281 { 282 l = new ArrayList<>(1); 283 m.put(lowerOID, l); 284 } 285 286 l.add(o); 287 } 288 289 for (final String name : d.getNames()) 290 { 291 final String lowerName = StaticUtils.toLowerCase(name); 292 List<List<String>> l = m.get(lowerName); 293 if (l == null) 294 { 295 l = new ArrayList<>(1); 296 m.put(lowerName, l); 297 } 298 299 l.add(o); 300 } 301 302 // If a schema is available, then see if the attribute type has any 303 // subordinate types. If so, then add them. 304 if (s != null) 305 { 306 for (final AttributeTypeDefinition subordinateType : 307 s.getSubordinateAttributeTypes(d)) 308 { 309 addAttributeOIDAndNames(subordinateType, m, o, s); 310 } 311 } 312 } 313 314 315 316 /** 317 * Retrieves the set of requested attributes used to create this search entry 318 * parer. 319 * 320 * @return The set of requested attributes used to create this search entry 321 * parer. 322 */ 323 @NotNull() 324 public List<String> getRequestedAttributes() 325 { 326 return requestedAttributes; 327 } 328 329 330 331 /** 332 * Retrieves a copy of the provided entry that includes only the appropriate 333 * set of requested attributes. 334 * 335 * @param entry The entry to be pared. 336 * 337 * @return A copy of the provided entry that includes only the appropriate 338 * set of requested attributes. 339 */ 340 @NotNull() 341 public Entry pareEntry(@NotNull final Entry entry) 342 { 343 // See if we can return the entry without paring it down. 344 if (allUserAttributes) 345 { 346 if (allOperationalAttributes || (schema == null)) 347 { 348 return entry.duplicate(); 349 } 350 } 351 352 353 // If we've gotten here, then we may only need to return a partial entry. 354 final Entry copy = new Entry(entry.getDN(), schema); 355 356 for (final Attribute a : entry.getAttributes()) 357 { 358 final ObjectPair<String,List<String>> nameWithOptions = 359 getNameWithOptions(a.getName()); 360 final String name = nameWithOptions.getFirst(); 361 final List<String> options = nameWithOptions.getSecond(); 362 363 // If there is a schema, then see if it is an operational attribute, since 364 // that needs to be handled in a manner different from user attributes 365 if (schema != null) 366 { 367 final AttributeTypeDefinition at = schema.getAttributeType(name); 368 if ((at != null) && at.isOperational()) 369 { 370 if (allOperationalAttributes) 371 { 372 copy.addAttribute(a); 373 continue; 374 } 375 376 final List<List<String>> optionLists = 377 attributeTypesToReturn.get(name); 378 if (optionLists == null) 379 { 380 continue; 381 } 382 383 for (final List<String> optionList : optionLists) 384 { 385 boolean matchAll = true; 386 for (final String option : optionList) 387 { 388 if (! options.contains(option)) 389 { 390 matchAll = false; 391 break; 392 } 393 } 394 395 if (matchAll) 396 { 397 copy.addAttribute(a); 398 break; 399 } 400 } 401 continue; 402 } 403 } 404 405 // We'll assume that it's a user attribute, and we'll look for an exact 406 // match on the base name. 407 if (allUserAttributes) 408 { 409 copy.addAttribute(a); 410 continue; 411 } 412 413 final List<List<String>> optionLists = attributeTypesToReturn.get(name); 414 if (optionLists == null) 415 { 416 continue; 417 } 418 419 for (final List<String> optionList : optionLists) 420 { 421 boolean matchAll = true; 422 for (final String option : optionList) 423 { 424 if (! options.contains(option)) 425 { 426 matchAll = false; 427 break; 428 } 429 } 430 431 if (matchAll) 432 { 433 copy.addAttribute(a); 434 break; 435 } 436 } 437 } 438 439 return copy; 440 } 441}