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&lt;Entry&gt; 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}