001 /*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License"). You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at
010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012 * See the License for the specific language governing permissions
013 * and limitations under the License.
014 *
015 * When distributing Covered Code, include this CDDL HEADER in each
016 * file and include the License file at
017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
018 * add the following below this CDDL HEADER, with the fields enclosed
019 * by brackets "[]" replaced with your own identifying information:
020 * Portions Copyright [yyyy] [name of copyright owner]
021 *
022 * CDDL HEADER END
023 *
024 *
025 * Copyright 2006-2008 Sun Microsystems, Inc.
026 */
027 package org.opends.server.backends.jeb;
028
029
030 import com.sleepycat.je.*;
031
032 import org.opends.messages.Message;
033 import org.opends.server.core.DirectoryServer;
034 import org.opends.server.core.SearchOperation;
035 import org.opends.server.loggers.debug.DebugTracer;
036 import org.opends.server.protocols.asn1.ASN1OctetString;
037 import org.opends.server.types.Attribute;
038 import org.opends.server.types.AttributeType;
039 import org.opends.server.types.AttributeValue;
040 import org.opends.server.types.ConditionResult;
041 import org.opends.server.types.DebugLogLevel;
042 import org.opends.server.types.DirectoryException;
043 import org.opends.server.types.DN;
044 import org.opends.server.types.Entry;
045 import org.opends.server.types.LDAPURL;
046 import org.opends.server.types.Modification;
047 import org.opends.server.types.ResultCode;
048 import org.opends.server.types.SearchResultReference;
049 import org.opends.server.types.SearchScope;
050 import org.opends.server.util.StaticUtils;
051
052 import java.io.UnsupportedEncodingException;
053 import java.util.ArrayList;
054 import java.util.Comparator;
055 import java.util.LinkedHashSet;
056 import java.util.List;
057 import java.util.Set;
058
059 import static org.opends.server.util.ServerConstants.ATTR_REFERRAL_URL;
060 import static org.opends.server.loggers.debug.DebugLogger.*;
061 import static org.opends.messages.JebMessages.
062 NOTE_JEB_REFERRAL_RESULT_MESSAGE;
063 /**
064 * This class represents the referral database which contains URIs from referral
065 * entries. The key is the DN of the referral entry and the value is that of a
066 * labeled URI in the ref attribute for that entry. Duplicate keys are permitted
067 * since a referral entry can contain multiple values of the ref attribute. Key
068 * order is the same as in the DN database so that all referrals in a subtree
069 * can be retrieved by cursoring through a range of the records.
070 */
071 public class DN2URI extends DatabaseContainer
072 {
073 /**
074 * The tracer object for the debug logger.
075 */
076 private static final DebugTracer TRACER = getTracer();
077
078 /**
079 * The key comparator used for the DN database.
080 */
081 private Comparator<byte[]> dn2uriComparator;
082
083
084 /**
085 * The standard attribute type that is used to specify the set of referral
086 * URLs in a referral entry.
087 */
088 private final AttributeType referralType =
089 DirectoryServer.getAttributeType(ATTR_REFERRAL_URL);
090
091 /**
092 * A flag that indicates whether there are any referrals contained in this
093 * database. It should only be set to {@code false} when it is known that
094 * there are no referrals.
095 */
096 private volatile ConditionResult containsReferrals =
097 ConditionResult.UNDEFINED;
098
099
100 /**
101 * Create a new object representing a referral database in a given
102 * entryContainer.
103 *
104 * @param name The name of the referral database.
105 * @param env The JE environment.
106 * @param entryContainer The entryContainer of the DN database.
107 * @throws DatabaseException If an error occurs in the JE database.
108 */
109 DN2URI(String name, Environment env,
110 EntryContainer entryContainer)
111 throws DatabaseException
112 {
113 super(name, env, entryContainer);
114
115 dn2uriComparator = new EntryContainer.KeyReverseComparator();
116 DatabaseConfig dn2uriConfig = new DatabaseConfig();
117
118 if(env.getConfig().getReadOnly())
119 {
120 dn2uriConfig.setReadOnly(true);
121 dn2uriConfig.setSortedDuplicates(true);
122 dn2uriConfig.setAllowCreate(false);
123 dn2uriConfig.setTransactional(false);
124 }
125 else if(!env.getConfig().getTransactional())
126 {
127 dn2uriConfig.setSortedDuplicates(true);
128 dn2uriConfig.setAllowCreate(true);
129 dn2uriConfig.setTransactional(false);
130 dn2uriConfig.setDeferredWrite(true);
131 }
132 else
133 {
134 dn2uriConfig.setSortedDuplicates(true);
135 dn2uriConfig.setAllowCreate(true);
136 dn2uriConfig.setTransactional(true);
137 }
138 this.dbConfig = dn2uriConfig;
139 this.dbConfig.setBtreeComparator(dn2uriComparator.getClass());
140 }
141
142 /**
143 * Insert a URI value in the referral database.
144 *
145 * @param txn A database transaction used for the update, or null if none is
146 * required.
147 * @param dn The DN of the referral entry.
148 * @param labeledURI The labeled URI value of the ref attribute.
149 * @return true if the record was inserted, false if it was not.
150 * @throws DatabaseException If an error occurs in the JE database.
151 */
152 public boolean insert(Transaction txn, DN dn, String labeledURI)
153 throws DatabaseException
154 {
155 byte[] normDN = StaticUtils.getBytes(dn.toNormalizedString());
156 byte[] URIBytes = StaticUtils.getBytes(labeledURI);
157 DatabaseEntry key = new DatabaseEntry(normDN);
158 DatabaseEntry data = new DatabaseEntry(URIBytes);
159 OperationStatus status;
160
161 // The JE insert method does not permit duplicate keys so we must use the
162 // put method.
163 status = put(txn, key, data);
164 if (status != OperationStatus.SUCCESS)
165 {
166 return false;
167 }
168 containsReferrals = ConditionResult.TRUE;
169 return true;
170 }
171
172 /**
173 * Delete URI values for a given referral entry from the referral database.
174 *
175 * @param txn A database transaction used for the update, or null if none is
176 * required.
177 * @param dn The DN of the referral entry for which URI values are to be
178 * deleted.
179 * @return true if the values were deleted, false if not.
180 * @throws DatabaseException If an error occurs in the JE database.
181 */
182 public boolean delete(Transaction txn, DN dn)
183 throws DatabaseException
184 {
185 byte[] normDN = StaticUtils.getBytes(dn.toNormalizedString());
186 DatabaseEntry key = new DatabaseEntry(normDN);
187 OperationStatus status;
188
189 status = delete(txn, key);
190 if (status != OperationStatus.SUCCESS)
191 {
192 return false;
193 }
194 containsReferrals = containsReferrals(txn);
195 return true;
196 }
197
198 /**
199 * Delete a single URI value from the referral database.
200 * @param txn A database transaction used for the update, or null if none is
201 * required.
202 * @param dn The DN of the referral entry.
203 * @param labeledURI The URI value to be deleted.
204 * @return true if the value was deleted, false if not.
205 * @throws DatabaseException If an error occurs in the JE database.
206 */
207 public boolean delete(Transaction txn, DN dn, String labeledURI)
208 throws DatabaseException
209 {
210 CursorConfig cursorConfig = null;
211 byte[] normDN = StaticUtils.getBytes(dn.toNormalizedString());
212 byte[] URIBytes = StaticUtils.getBytes(labeledURI);
213 DatabaseEntry key = new DatabaseEntry(normDN);
214 DatabaseEntry data = new DatabaseEntry(URIBytes);
215 OperationStatus status;
216
217 Cursor cursor = openCursor(txn, cursorConfig);
218 try
219 {
220 status = cursor.getSearchBoth(key, data, null);
221 if (status == OperationStatus.SUCCESS)
222 {
223 status = cursor.delete();
224 }
225 }
226 finally
227 {
228 cursor.close();
229 }
230
231 if (status != OperationStatus.SUCCESS)
232 {
233 return false;
234 }
235 containsReferrals = containsReferrals(txn);
236 return true;
237 }
238
239 /**
240 * Indicates whether the underlying database contains any referrals.
241 *
242 * @param txn The transaction to use when making the determination.
243 *
244 * @return {@code true} if it is believed that the underlying database may
245 * contain at least one referral, or {@code false} if it is certain
246 * that it doesn't.
247 */
248 private ConditionResult containsReferrals(Transaction txn)
249 {
250 try
251 {
252 Cursor cursor = openCursor(txn, null);
253 DatabaseEntry key = new DatabaseEntry();
254 DatabaseEntry data = new DatabaseEntry();
255
256 OperationStatus status = cursor.getFirst(key, data, null);
257 cursor.close();
258
259 if (status == OperationStatus.SUCCESS)
260 {
261 return ConditionResult.TRUE;
262 }
263 else if (status == OperationStatus.NOTFOUND)
264 {
265 return ConditionResult.FALSE;
266 }
267 else
268 {
269 return ConditionResult.UNDEFINED;
270 }
271 }
272 catch (Exception e)
273 {
274 if (debugEnabled())
275 {
276 TRACER.debugCaught(DebugLogLevel.ERROR, e);
277 }
278
279 return ConditionResult.UNDEFINED;
280 }
281 }
282
283 /**
284 * Update the referral database for an entry that has been modified. Does
285 * not do anything unless the entry before the modification or the entry after
286 * the modification is a referral entry.
287 *
288 * @param txn A database transaction used for the update, or null if none is
289 * required.
290 * @param before The entry before the modifications have been applied.
291 * @param after The entry after the modifications have been applied.
292 * @param mods The sequence of modifications made to the entry.
293 * @throws DatabaseException If an error occurs in the JE database.
294 */
295 public void modifyEntry(Transaction txn, Entry before, Entry after,
296 List<Modification> mods)
297 throws DatabaseException
298 {
299 DN entryDN = before.getDN();
300 for (Modification mod : mods)
301 {
302 Attribute modAttr = mod.getAttribute();
303 AttributeType modAttrType = modAttr.getAttributeType();
304 if (modAttrType.equals(referralType))
305 {
306 Attribute a = mod.getAttribute();
307 switch (mod.getModificationType())
308 {
309 case ADD:
310 if (a != null)
311 {
312 for (AttributeValue v : a.getValues())
313 {
314 insert(txn, entryDN, v.getStringValue());
315 }
316 }
317 break;
318
319 case DELETE:
320 if (a == null || !a.hasValue())
321 {
322 delete(txn, entryDN);
323 }
324 else
325 {
326 for (AttributeValue v : a.getValues())
327 {
328 delete(txn, entryDN, v.getStringValue());
329 }
330 }
331 break;
332
333 case INCREMENT:
334 // Nonsensical.
335 break;
336
337 case REPLACE:
338 delete(txn, entryDN);
339 if (a != null)
340 {
341 for (AttributeValue v : a.getValues())
342 {
343 insert(txn, entryDN, v.getStringValue());
344 }
345 }
346 break;
347 }
348 }
349 }
350 }
351
352 /**
353 * Update the referral database for an entry that has been replaced. Does
354 * not do anything unless the entry before it was replaced or the entry after
355 * it was replaced is a referral entry.
356 *
357 * @param txn A database transaction used for the update, or null if none is
358 * required.
359 * @param before The entry before it was replaced.
360 * @param after The entry after it was replaced.
361 * @throws DatabaseException If an error occurs in the JE database.
362 */
363 public void replaceEntry(Transaction txn, Entry before, Entry after)
364 throws DatabaseException
365 {
366 deleteEntry(txn, before);
367 addEntry(txn, after);
368 }
369
370 /**
371 * Update the referral database for a new entry. Does nothing if the entry
372 * is not a referral entry.
373 * @param txn A database transaction used for the update, or null if none is
374 * required.
375 * @param entry The entry to be added.
376 * @return True if the entry was added successfully or False otherwise.
377 * @throws DatabaseException If an error occurs in the JE database.
378 */
379 public boolean addEntry(Transaction txn, Entry entry)
380 throws DatabaseException
381 {
382 boolean success = true;
383 Set<String> labeledURIs = entry.getReferralURLs();
384 if (labeledURIs != null)
385 {
386 DN dn = entry.getDN();
387 for (String labeledURI : labeledURIs)
388 {
389 if(!insert(txn, dn, labeledURI))
390 {
391 success = false;
392 }
393 }
394 }
395 return success;
396 }
397
398 /**
399 * Update the referral database for a deleted entry. Does nothing if the entry
400 * was not a referral entry.
401 * @param txn A database transaction used for the update, or null if none is
402 * required.
403 * @param entry The entry to be deleted.
404 * @throws DatabaseException If an error occurs in the JE database.
405 */
406 public void deleteEntry(Transaction txn, Entry entry)
407 throws DatabaseException
408 {
409 Set<String> labeledURIs = entry.getReferralURLs();
410 if (labeledURIs != null)
411 {
412 delete(txn, entry.getDN());
413 }
414 }
415
416 /**
417 * Checks whether the target of an operation is a referral entry and throws
418 * a Directory referral exception if it is.
419 * @param entry The target entry of the operation, or the base entry of a
420 * search operation.
421 * @param searchScope The scope of the search operation, or null if the
422 * operation is not a search operation.
423 * @throws DirectoryException If a referral is found at or above the target
424 * DN. The referral URLs will be set appropriately for the references found
425 * in the referral entry.
426 */
427 public void checkTargetForReferral(Entry entry, SearchScope searchScope)
428 throws DirectoryException
429 {
430 Set<String> referralURLs = entry.getReferralURLs();
431 if (referralURLs != null)
432 {
433 throwReferralException(entry.getDN(), entry.getDN(), referralURLs,
434 searchScope);
435 }
436 }
437
438 /**
439 * Throws a Directory referral exception for the case where a referral entry
440 * exists at or above the target DN of an operation.
441 * @param targetDN The target DN of the operation, or the base object of a
442 * search operation.
443 * @param referralDN The DN of the referral entry.
444 * @param labeledURIs The set of labeled URIs in the referral entry.
445 * @param searchScope The scope of the search operation, or null if the
446 * operation is not a search operation.
447 * @throws DirectoryException If a referral is found at or above the target
448 * DN. The referral URLs will be set appropriately for the references found
449 * in the referral entry.
450 */
451 public void throwReferralException(DN targetDN, DN referralDN,
452 Set<String> labeledURIs,
453 SearchScope searchScope)
454 throws DirectoryException
455 {
456 ArrayList<String> URIList = new ArrayList<String>(labeledURIs.size());
457 for (String labeledURI : labeledURIs)
458 {
459 // Remove the label part of the labeled URI if there is a label.
460 String uri = labeledURI;
461 int i = labeledURI.indexOf(' ');
462 if (i != -1)
463 {
464 uri = labeledURI.substring(0, i);
465 }
466
467 try
468 {
469 LDAPURL ldapurl = LDAPURL.decode(uri, false);
470
471 if (ldapurl.getScheme().equalsIgnoreCase("ldap"))
472 {
473 DN urlBaseDN = targetDN;
474 if (!referralDN.equals(ldapurl.getBaseDN()))
475 {
476 urlBaseDN =
477 EntryContainer.modDN(targetDN,
478 referralDN.getNumComponents(),
479 ldapurl.getBaseDN());
480 }
481 ldapurl.setBaseDN(urlBaseDN);
482 if (searchScope == null)
483 {
484 // RFC 3296, 5.2. Target Object Considerations:
485 // In cases where the URI to be returned is a LDAP URL, the server
486 // SHOULD trim any present scope, filter, or attribute list from the
487 // URI before returning it. Critical extensions MUST NOT be trimmed
488 // or modified.
489 StringBuilder builder = new StringBuilder(uri.length());
490 ldapurl.toString(builder, true);
491 uri = builder.toString();
492 }
493 else
494 {
495 // RFC 3296, 5.3. Base Object Considerations:
496 // In cases where the URI to be returned is a LDAP URL, the server
497 // MUST provide an explicit scope specifier from the LDAP URL prior
498 // to returning it.
499 ldapurl.getAttributes().clear();
500 ldapurl.setScope(searchScope);
501 ldapurl.setFilter(null);
502 uri = ldapurl.toString();
503 }
504 }
505 }
506 catch (DirectoryException e)
507 {
508 if (debugEnabled())
509 {
510 TRACER.debugCaught(DebugLogLevel.ERROR, e);
511 }
512 // Return the non-LDAP URI as is.
513 }
514
515 URIList.add(uri);
516 }
517
518 // Throw a directory referral exception containing the URIs.
519 Message msg =
520 NOTE_JEB_REFERRAL_RESULT_MESSAGE.get(String.valueOf(referralDN));
521 throw new DirectoryException(
522 ResultCode.REFERRAL, msg, referralDN, URIList, null);
523 }
524
525 /**
526 * Process referral entries that are above the target DN of an operation.
527 * @param targetDN The target DN of the operation, or the base object of a
528 * search operation.
529 * @param searchScope The scope of the search operation, or null if the
530 * operation is not a search operation.
531 * @throws DirectoryException If a referral is found at or above the target
532 * DN. The referral URLs will be set appropriately for the references found
533 * in the referral entry.
534 */
535 public void targetEntryReferrals(DN targetDN, SearchScope searchScope)
536 throws DirectoryException
537 {
538 if (containsReferrals == ConditionResult.UNDEFINED)
539 {
540 containsReferrals = containsReferrals(null);
541 }
542
543 if (containsReferrals == ConditionResult.FALSE)
544 {
545 return;
546 }
547
548 Transaction txn = null;
549 CursorConfig cursorConfig = null;
550
551 try
552 {
553 Cursor cursor = openCursor(txn, cursorConfig);
554 try
555 {
556 DatabaseEntry key = new DatabaseEntry();
557 DatabaseEntry data = new DatabaseEntry();
558
559 // Go up through the DIT hierarchy until we find a referral.
560 for (DN dn = entryContainer.getParentWithinBase(targetDN); dn != null;
561 dn = entryContainer.getParentWithinBase(dn))
562 {
563 // Look for a record whose key matches the current DN.
564 String normDN = dn.toNormalizedString();
565 key.setData(StaticUtils.getBytes(normDN));
566 OperationStatus status =
567 cursor.getSearchKey(key, data, LockMode.DEFAULT);
568 if (status == OperationStatus.SUCCESS)
569 {
570 // Construct a set of all the labeled URIs in the referral.
571 Set<String> labeledURIs =
572 new LinkedHashSet<String>(cursor.count());
573 do
574 {
575 String labeledURI = new String(data.getData(), "UTF-8");
576 labeledURIs.add(labeledURI);
577 status = cursor.getNextDup(key, data, LockMode.DEFAULT);
578 } while (status == OperationStatus.SUCCESS);
579
580 throwReferralException(targetDN, dn, labeledURIs, searchScope);
581 }
582 }
583 }
584 finally
585 {
586 cursor.close();
587 }
588 }
589 catch (DatabaseException e)
590 {
591 if (debugEnabled())
592 {
593 TRACER.debugCaught(DebugLogLevel.ERROR, e);
594 }
595 }
596 catch (UnsupportedEncodingException e)
597 {
598 if (debugEnabled())
599 {
600 TRACER.debugCaught(DebugLogLevel.ERROR, e);
601 }
602 }
603 }
604
605 /**
606 * Return search result references for a search operation using the referral
607 * database to find all referral entries within scope of the search.
608 * @param searchOp The search operation for which search result references
609 * should be returned.
610 * @return <CODE>true</CODE> if the caller should continue processing the
611 * search request and sending additional entries and references, or
612 * <CODE>false</CODE> if not for some reason (e.g., the size limit
613 * has been reached or the search has been abandoned).
614 * @throws DirectoryException If a Directory Server error occurs.
615 */
616 public boolean returnSearchReferences(SearchOperation searchOp)
617 throws DirectoryException
618 {
619 if (containsReferrals == ConditionResult.UNDEFINED)
620 {
621 containsReferrals = containsReferrals(null);
622 }
623
624 if (containsReferrals == ConditionResult.FALSE)
625 {
626 return true;
627 }
628
629 Transaction txn = null;
630 CursorConfig cursorConfig = null;
631
632 /*
633 * We will iterate forwards through a range of the keys to
634 * find subordinates of the base entry from the top of the tree
635 * downwards.
636 */
637 DN baseDN = searchOp.getBaseDN();
638 String normBaseDN = baseDN.toNormalizedString();
639 byte[] suffix = StaticUtils.getBytes("," + normBaseDN);
640
641 /*
642 * Set the ending value to a value of equal length but slightly
643 * greater than the suffix. Since keys are compared in
644 * reverse order we must set the first byte (the comma).
645 * No possibility of overflow here.
646 */
647 byte[] end = null;
648
649 DatabaseEntry data = new DatabaseEntry();
650 DatabaseEntry key = new DatabaseEntry(suffix);
651
652 try
653 {
654 Cursor cursor = openCursor(txn, cursorConfig);
655 try
656 {
657 // Initialize the cursor very close to the starting value then
658 // step forward until we pass the ending value.
659 for (OperationStatus status =
660 cursor.getSearchKeyRange(key, data, LockMode.DEFAULT);
661 status == OperationStatus.SUCCESS;
662 status = cursor.getNextNoDup(key, data, LockMode.DEFAULT))
663 {
664 if (end == null)
665 {
666 end = suffix.clone();
667 end[0] = (byte) (end[0] + 1);
668 }
669
670 int cmp = dn2uriComparator.compare(key.getData(), end);
671 if (cmp >= 0)
672 {
673 // We have gone past the ending value.
674 break;
675 }
676
677 // We have found a subordinate referral.
678 DN dn = DN.decode(new ASN1OctetString(key.getData()));
679
680 // Make sure the referral is within scope.
681 if (searchOp.getScope() == SearchScope.SINGLE_LEVEL)
682 {
683 if ((dn.getNumComponents() !=
684 baseDN.getNumComponents() + 1))
685 {
686 continue;
687 }
688 }
689
690 // Construct a list of all the URIs in the referral.
691 ArrayList<String> URIList = new ArrayList<String>(cursor.count());
692 do
693 {
694 // Remove the label part of the labeled URI if there is a label.
695 String labeledURI = new String(data.getData(), "UTF-8");
696 String uri = labeledURI;
697 int i = labeledURI.indexOf(' ');
698 if (i != -1)
699 {
700 uri = labeledURI.substring(0, i);
701 }
702
703 // From RFC 3296 section 5.4:
704 // If the URI component is not a LDAP URL, it should be returned as
705 // is. If the LDAP URL's DN part is absent or empty, the DN part
706 // must be modified to contain the DN of the referral object. If
707 // the URI component is a LDAP URL, the URI SHOULD be modified to
708 // add an explicit scope specifier.
709 try
710 {
711 LDAPURL ldapurl = LDAPURL.decode(uri, false);
712
713 if (ldapurl.getScheme().equalsIgnoreCase("ldap"))
714 {
715 if (ldapurl.getBaseDN().isNullDN())
716 {
717 ldapurl.setBaseDN(dn);
718 }
719 ldapurl.getAttributes().clear();
720 if (searchOp.getScope() == SearchScope.SINGLE_LEVEL)
721 {
722 ldapurl.setScope(SearchScope.BASE_OBJECT);
723 }
724 else
725 {
726 ldapurl.setScope(SearchScope.WHOLE_SUBTREE);
727 }
728 ldapurl.setFilter(null);
729 uri = ldapurl.toString();
730 }
731 }
732 catch (DirectoryException e)
733 {
734 if (debugEnabled())
735 {
736 TRACER.debugCaught(DebugLogLevel.ERROR, e);
737 }
738 // Return the non-LDAP URI as is.
739 }
740
741 URIList.add(uri);
742 status = cursor.getNextDup(key, data, LockMode.DEFAULT);
743 } while (status == OperationStatus.SUCCESS);
744
745 SearchResultReference reference = new SearchResultReference(URIList);
746 if (!searchOp.returnReference(dn, reference))
747 {
748 return false;
749 }
750 }
751 }
752 finally
753 {
754 cursor.close();
755 }
756 }
757 catch (DatabaseException e)
758 {
759 if (debugEnabled())
760 {
761 TRACER.debugCaught(DebugLogLevel.ERROR, e);
762 }
763 }
764 catch (UnsupportedEncodingException e)
765 {
766 if (debugEnabled())
767 {
768 TRACER.debugCaught(DebugLogLevel.ERROR, e);
769 }
770 }
771
772 return true;
773 }
774
775 /**
776 * Gets the comparator for records stored in this database.
777 *
778 * @return The comparator used for records stored in this database.
779 */
780 public Comparator<byte[]> getComparator()
781 {
782 return dn2uriComparator;
783 }
784 }