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