001/* 002 * Copyright 2007-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2007-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) 2007-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; 037 038 039 040import java.io.Serializable; 041import java.util.ArrayList; 042import java.util.Arrays; 043import java.util.Collection; 044import java.util.Collections; 045import java.util.Comparator; 046import java.util.Iterator; 047import java.util.List; 048import java.util.SortedSet; 049import java.util.TreeSet; 050 051import com.unboundid.asn1.ASN1OctetString; 052import com.unboundid.ldap.matchingrules.MatchingRule; 053import com.unboundid.ldap.sdk.controls.SortKey; 054import com.unboundid.ldap.sdk.schema.Schema; 055import com.unboundid.util.Debug; 056import com.unboundid.util.NotNull; 057import com.unboundid.util.Nullable; 058import com.unboundid.util.StaticUtils; 059import com.unboundid.util.ThreadSafety; 060import com.unboundid.util.ThreadSafetyLevel; 061 062 063 064/** 065 * This class provides a mechanism for client-side entry sorting. Sorting may 066 * be based on attributes contained in the entry, and may also be based on the 067 * hierarchical location of the entry in the DIT. The sorting may be applied 068 * to any collection of entries, including the entries included in a 069 * {@link SearchResult} object. 070 * <BR><BR> 071 * This class provides a client-side alternative to the use of the 072 * {@link com.unboundid.ldap.sdk.controls.ServerSideSortRequestControl}. 073 * Client-side sorting is most appropriate for small result sets, as it requires 074 * all entries to be held in memory at the same time. It is a good alternative 075 * to server-side sorting when the overhead of sorting should be distributed 076 * across client systems rather than on the server, and in cases in which the 077 * target directory server does not support the use of the server-side sort 078 * request control. 079 * <BR><BR> 080 * For best results, a {@link Schema} object may be used to provide an 081 * indication as to which matching rules should be used to perform the ordering. 082 * If no {@code Schema} object is provided, then all ordering will be performed 083 * using case-ignore string matching. 084 * <BR><BR> 085 * <H2>Example</H2> 086 * The following example may be used to obtain a sorted set of search result 087 * entries, ordered first by sn and then by givenName, without consideration for 088 * hierarchy: 089 * <PRE> 090 * SearchResult searchResult = connection.search("dc=example,dc=com", 091 * SearchScope.SUB, Filter.createEqualityFilter("sn", "Smith")); 092 * 093 * EntrySorter entrySorter = new EntrySorter(false, 094 * new SortKey("sn"), new SortKey("givenName")); 095 * SortedSet<Entry> sortedEntries = 096 * entrySorter.sort(searchResult.getSearchEntries()); 097 * </PRE> 098 */ 099@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 100public final class EntrySorter 101 implements Comparator<Entry>, Serializable 102{ 103 /** 104 * The serial version UID for this serializable class. 105 */ 106 private static final long serialVersionUID = 7606107105238612142L; 107 108 109 110 // Indicates whether entries should be sorted based on hierarchy. 111 private final boolean sortByHierarchy; 112 113 // The set of sort keys for attribute-level sorting. 114 @NotNull private final List<SortKey> sortKeys; 115 116 // The schema to use to make the comparison, if available. 117 @Nullable private final Schema schema; 118 119 120 121 /** 122 * Creates a new entry sorter that will sort entries based only on hierarchy. 123 * Superior entries (that is, entries closer to the root of the DIT) will be 124 * ordered before subordinate entries. Entries below the same parent will be 125 * sorted lexicographically based on their normalized DNs. 126 */ 127 public EntrySorter() 128 { 129 this(true, null, Collections.<SortKey>emptyList()); 130 } 131 132 133 134 /** 135 * Creates a new entry sorter with the provided information. 136 * 137 * @param sortByHierarchy Indicates whether entries should be sorted 138 * hierarchically, such that superior entries will 139 * be ordered before subordinate entries. 140 * @param sortKeys A list of sort keys that define the order in which 141 * attributes should be compared. It may be empty 142 * (but never {@code null}) if sorting should be done 143 * only based on hierarchy. 144 */ 145 public EntrySorter(final boolean sortByHierarchy, 146 @NotNull final SortKey... sortKeys) 147 { 148 this(sortByHierarchy, null, Arrays.asList(sortKeys)); 149 } 150 151 152 153 /** 154 * Creates a new entry sorter with the provided information. 155 * 156 * @param sortByHierarchy Indicates whether entries should be sorted 157 * hierarchically, such that superior entries will 158 * be ordered before subordinate entries. 159 * @param schema The schema to use to make the determination. It 160 * may be {@code null} if no schema is available. 161 * @param sortKeys A list of sort keys that define the order in which 162 * attributes should be compared. It may be empty 163 * (but never {@code null}) if sorting should be done 164 * only based on hierarchy. 165 */ 166 public EntrySorter(final boolean sortByHierarchy, 167 @Nullable final Schema schema, 168 @NotNull final SortKey... sortKeys) 169 { 170 this(sortByHierarchy, schema, Arrays.asList(sortKeys)); 171 } 172 173 174 175 /** 176 * Creates a new entry sorter with the provided information. 177 * 178 * @param sortByHierarchy Indicates whether entries should be sorted 179 * hierarchically, such that superior entries will 180 * be ordered before subordinate entries. 181 * @param sortKeys A list of sort keys that define the order in which 182 * attributes should be compared. It may be empty or 183 * {@code null} if sorting should be done only based 184 * on hierarchy. 185 */ 186 public EntrySorter(final boolean sortByHierarchy, 187 @Nullable final List<SortKey> sortKeys) 188 { 189 this(sortByHierarchy, null, sortKeys); 190 } 191 192 193 194 /** 195 * Creates a new entry sorter with the provided information. 196 * 197 * @param sortByHierarchy Indicates whether entries should be sorted 198 * hierarchically, such that superior entries will 199 * be ordered before subordinate entries. 200 * @param schema The schema to use to make the determination. It 201 * may be {@code null} if no schema is available. 202 * @param sortKeys A list of sort keys that define the order in which 203 * attributes should be compared. It may be empty or 204 * {@code null} if sorting should be done only based 205 * on hierarchy. 206 */ 207 public EntrySorter(final boolean sortByHierarchy, 208 @Nullable final Schema schema, 209 @Nullable final List<SortKey> sortKeys) 210 { 211 this.sortByHierarchy = sortByHierarchy; 212 this.schema = schema; 213 214 if (sortKeys == null) 215 { 216 this.sortKeys = Collections.emptyList(); 217 } 218 else 219 { 220 this.sortKeys = Collections.unmodifiableList(new ArrayList<>(sortKeys)); 221 } 222 } 223 224 225 226 /** 227 * Sorts the provided collection of entries according to the criteria defined 228 * in this entry sorter. 229 * 230 * @param entries The collection of entries to be sorted. 231 * 232 * @return A sorted set, ordered in accordance with this entry sorter. 233 */ 234 @NotNull() 235 public SortedSet<Entry> sort( 236 @NotNull final Collection<? extends Entry> entries) 237 { 238 final TreeSet<Entry> entrySet = new TreeSet<>(this); 239 entrySet.addAll(entries); 240 return entrySet; 241 } 242 243 244 245 /** 246 * Compares the provided entries to determine the order in which they should 247 * be placed in a sorted list. 248 * 249 * @param e1 The first entry to be compared. 250 * @param e2 The second entry to be compared. 251 * 252 * @return A negative value if the first entry should be ordered before the 253 * second, a positive value if the first entry should be ordered 254 * after the second, or zero if the entries should have an equivalent 255 * order. 256 */ 257 @Override() 258 public int compare(@NotNull final Entry e1, @NotNull final Entry e2) 259 { 260 DN parsedDN1 = null; 261 DN parsedDN2 = null; 262 263 if (sortByHierarchy) 264 { 265 try 266 { 267 parsedDN1 = e1.getParsedDN(); 268 parsedDN2 = e2.getParsedDN(); 269 270 if (parsedDN1.isAncestorOf(parsedDN2, false)) 271 { 272 return -1; 273 } 274 else if (parsedDN2.isAncestorOf(parsedDN1, false)) 275 { 276 return 1; 277 } 278 } 279 catch (final LDAPException le) 280 { 281 Debug.debugException(le); 282 } 283 } 284 285 for (final SortKey k : sortKeys) 286 { 287 final String attrName = k.getAttributeName(); 288 final Attribute a1 = e1.getAttribute(attrName); 289 final Attribute a2 = e2.getAttribute(attrName); 290 291 if ((a1 == null) || (! a1.hasValue())) 292 { 293 if ((a2 == null) || (! a2.hasValue())) 294 { 295 // Neither entry has the attribute. Continue on with the next 296 // attribute. 297 continue; 298 } 299 else 300 { 301 // The first entry does not have the attribute but the second does. 302 // The first entry should be ordered after the second. 303 return 1; 304 } 305 } 306 else 307 { 308 if ((a2 == null) || (! a2.hasValue())) 309 { 310 // The first entry has the attribute but the second does not. The 311 // first entry should be ordered before the second. 312 return -1; 313 } 314 } 315 316 317 final MatchingRule matchingRule = MatchingRule.selectOrderingMatchingRule( 318 attrName, k.getMatchingRuleID(), schema); 319 if (k.reverseOrder()) 320 { 321 // Find the largest value for each attribute, and pick the larger of the 322 // two. 323 ASN1OctetString v1 = null; 324 for (final ASN1OctetString s : a1.getRawValues()) 325 { 326 if (v1 == null) 327 { 328 v1 = s; 329 } 330 else 331 { 332 try 333 { 334 if (matchingRule.compareValues(s, v1) > 0) 335 { 336 v1 = s; 337 } 338 } 339 catch (final LDAPException le) 340 { 341 Debug.debugException(le); 342 } 343 } 344 } 345 346 ASN1OctetString v2 = null; 347 for (final ASN1OctetString s : a2.getRawValues()) 348 { 349 if (v2 == null) 350 { 351 v2 = s; 352 } 353 else 354 { 355 try 356 { 357 if (matchingRule.compareValues(s, v2) > 0) 358 { 359 v2 = s; 360 } 361 } 362 catch (final LDAPException le) 363 { 364 Debug.debugException(le); 365 } 366 } 367 } 368 369 try 370 { 371 final int value = matchingRule.compareValues(v2, v1); 372 if (value != 0) 373 { 374 return value; 375 } 376 } 377 catch (final LDAPException le) 378 { 379 Debug.debugException(le); 380 } 381 } 382 else 383 { 384 // Find the smallest value for each attribute, and pick the larger of 385 // the two. 386 ASN1OctetString v1 = null; 387 for (final ASN1OctetString s : a1.getRawValues()) 388 { 389 if (v1 == null) 390 { 391 v1 = s; 392 } 393 else 394 { 395 try 396 { 397 if (matchingRule.compareValues(s, v1) < 0) 398 { 399 v1 = s; 400 } 401 } 402 catch (final LDAPException le) 403 { 404 Debug.debugException(le); 405 } 406 } 407 } 408 409 ASN1OctetString v2 = null; 410 for (final ASN1OctetString s : a2.getRawValues()) 411 { 412 if (v2 == null) 413 { 414 v2 = s; 415 } 416 else 417 { 418 try 419 { 420 if (matchingRule.compareValues(s, v2) < 0) 421 { 422 v2 = s; 423 } 424 } 425 catch (final LDAPException le) 426 { 427 Debug.debugException(le); 428 } 429 } 430 } 431 432 try 433 { 434 final int value = matchingRule.compareValues(v1, v2); 435 if (value != 0) 436 { 437 return value; 438 } 439 } 440 catch (final LDAPException le) 441 { 442 Debug.debugException(le); 443 } 444 } 445 } 446 447 448 // If we've gotten here, then there is no difference in hierarchy or 449 // sort attributes. Compare the DNs as a last resort. 450 try 451 { 452 if (parsedDN1 == null) 453 { 454 parsedDN1 = e1.getParsedDN(); 455 } 456 457 if (parsedDN2 == null) 458 { 459 parsedDN2 = e2.getParsedDN(); 460 } 461 462 return parsedDN1.compareTo(parsedDN2); 463 } 464 catch (final LDAPException le) 465 { 466 Debug.debugException(le); 467 final String lowerDN1 = StaticUtils.toLowerCase(e1.getDN()); 468 final String lowerDN2 = StaticUtils.toLowerCase(e2.getDN()); 469 return lowerDN1.compareTo(lowerDN2); 470 } 471 } 472 473 474 475 /** 476 * Retrieves a hash code for this entry sorter. 477 * 478 * @return A hash code for this entry sorter. 479 */ 480 @Override() 481 public int hashCode() 482 { 483 int hashCode = 0; 484 485 if (sortByHierarchy) 486 { 487 hashCode++; 488 } 489 490 for (final SortKey k : sortKeys) 491 { 492 if (k.reverseOrder()) 493 { 494 hashCode *= -31; 495 } 496 else 497 { 498 hashCode *= 31; 499 } 500 501 hashCode += StaticUtils.toLowerCase(k.getAttributeName()).hashCode(); 502 } 503 504 return hashCode; 505 } 506 507 508 509 /** 510 * Indicates whether the provided object is equal to this entry sorter. 511 * 512 * @param o The object for which to make the determination. 513 * 514 * @return {@code true} if the provided object is equal to this entry sorter, 515 * or {@code false} if not. 516 */ 517 @Override() 518 public boolean equals(@Nullable final Object o) 519 { 520 if (o == null) 521 { 522 return false; 523 } 524 525 if (o == this) 526 { 527 return true; 528 } 529 530 if (! (o instanceof EntrySorter)) 531 { 532 return false; 533 } 534 535 final EntrySorter s = (EntrySorter) o; 536 if (sortByHierarchy != s.sortByHierarchy) 537 { 538 return false; 539 } 540 541 return sortKeys.equals(s.sortKeys); 542 } 543 544 545 546 /** 547 * Retrieves a string representation of this entry sorter. 548 * 549 * @return A string representation of this entry sorter. 550 */ 551 @Override() 552 @NotNull() 553 public String toString() 554 { 555 final StringBuilder buffer = new StringBuilder(); 556 toString(buffer); 557 return buffer.toString(); 558 } 559 560 561 562 /** 563 * Appends a string representation of this entry sorter to the provided 564 * buffer. 565 * 566 * @param buffer The buffer to which the string representation should be 567 * appended. 568 */ 569 public void toString(@NotNull final StringBuilder buffer) 570 { 571 buffer.append("EntrySorter(sortByHierarchy="); 572 buffer.append(sortByHierarchy); 573 buffer.append(", sortKeys={"); 574 575 final Iterator<SortKey> iterator = sortKeys.iterator(); 576 while (iterator.hasNext()) 577 { 578 iterator.next().toString(buffer); 579 if (iterator.hasNext()) 580 { 581 buffer.append(", "); 582 } 583 } 584 585 buffer.append("})"); 586 } 587}