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