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<Entry> 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 }