001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2006-2010 Sun Microsystems, Inc. 015 * Portions Copyright 2011-2016 ForgeRock AS. 016 * Portions copyright 2013 Manuel Gaupp 017 */ 018package org.opends.server.backends.pluggable; 019 020import static org.forgerock.util.Utils.*; 021import static org.opends.messages.BackendMessages.*; 022import static org.opends.server.backends.pluggable.DnKeyFormat.*; 023import static org.opends.server.backends.pluggable.IndexFilter.*; 024import static org.opends.server.backends.pluggable.VLVIndex.*; 025import static org.opends.server.core.DirectoryServer.*; 026import static org.opends.server.protocols.ldap.LDAPResultCode.*; 027import static org.opends.server.types.AdditionalLogItem.*; 028import static org.opends.server.util.StaticUtils.*; 029 030import java.util.ArrayList; 031import java.util.Arrays; 032import java.util.Collection; 033import java.util.Collections; 034import java.util.HashMap; 035import java.util.List; 036import java.util.Map; 037import java.util.NoSuchElementException; 038import java.util.Objects; 039import java.util.TreeMap; 040import java.util.concurrent.locks.Lock; 041import java.util.concurrent.locks.ReentrantReadWriteLock; 042 043import org.forgerock.i18n.LocalizableMessage; 044import org.forgerock.i18n.LocalizableMessageBuilder; 045import org.forgerock.i18n.slf4j.LocalizedLogger; 046import org.forgerock.opendj.config.server.ConfigChangeResult; 047import org.forgerock.opendj.config.server.ConfigException; 048import org.forgerock.opendj.ldap.ByteSequence; 049import org.forgerock.opendj.ldap.ByteString; 050import org.forgerock.opendj.ldap.ByteStringBuilder; 051import org.forgerock.opendj.ldap.DN; 052import org.forgerock.opendj.ldap.ResultCode; 053import org.forgerock.opendj.ldap.SearchScope; 054import org.forgerock.opendj.ldap.schema.AttributeType; 055import org.forgerock.util.Pair; 056import org.opends.messages.CoreMessages; 057import org.opends.server.admin.server.ConfigurationAddListener; 058import org.opends.server.admin.server.ConfigurationChangeListener; 059import org.opends.server.admin.server.ConfigurationDeleteListener; 060import org.opends.server.admin.std.server.BackendIndexCfg; 061import org.opends.server.admin.std.server.BackendVLVIndexCfg; 062import org.opends.server.admin.std.server.PluggableBackendCfg; 063import org.opends.server.api.ClientConnection; 064import org.opends.server.api.EntryCache; 065import org.opends.server.api.VirtualAttributeProvider; 066import org.opends.server.api.plugin.PluginResult.SubordinateDelete; 067import org.opends.server.api.plugin.PluginResult.SubordinateModifyDN; 068import org.opends.server.backends.pluggable.spi.AccessMode; 069import org.opends.server.backends.pluggable.spi.Cursor; 070import org.opends.server.backends.pluggable.spi.ReadOperation; 071import org.opends.server.backends.pluggable.spi.ReadableTransaction; 072import org.opends.server.backends.pluggable.spi.SequentialCursor; 073import org.opends.server.backends.pluggable.spi.Storage; 074import org.opends.server.backends.pluggable.spi.StorageRuntimeException; 075import org.opends.server.backends.pluggable.spi.TreeName; 076import org.opends.server.backends.pluggable.spi.WriteOperation; 077import org.opends.server.backends.pluggable.spi.WriteableTransaction; 078import org.opends.server.controls.PagedResultsControl; 079import org.opends.server.controls.ServerSideSortRequestControl; 080import org.opends.server.controls.ServerSideSortResponseControl; 081import org.opends.server.controls.SubtreeDeleteControl; 082import org.opends.server.controls.VLVRequestControl; 083import org.opends.server.controls.VLVResponseControl; 084import org.opends.server.core.AddOperation; 085import org.opends.server.core.DeleteOperation; 086import org.opends.server.core.DirectoryServer; 087import org.opends.server.core.ModifyDNOperation; 088import org.opends.server.core.ModifyOperation; 089import org.opends.server.core.SearchOperation; 090import org.opends.server.types.Attribute; 091import org.opends.server.types.Attributes; 092import org.opends.server.types.CanceledOperationException; 093import org.opends.server.types.Control; 094import org.opends.server.types.DirectoryException; 095import org.opends.server.types.Entry; 096import org.opends.server.types.Modification; 097import org.opends.server.types.Operation; 098import org.opends.server.types.Privilege; 099import org.opends.server.types.SearchFilter; 100import org.opends.server.types.SortOrder; 101import org.opends.server.types.VirtualAttributeRule; 102import org.opends.server.util.ServerConstants; 103import org.opends.server.util.StaticUtils; 104 105/** 106 * Storage container for LDAP entries. Each base DN of a backend is given 107 * its own entry container. The entry container is the object that implements 108 * the guts of the backend API methods for LDAP operations. 109 */ 110public class EntryContainer 111 implements SuffixContainer, ConfigurationChangeListener<PluggableBackendCfg> 112{ 113 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 114 115 /** The name of the entry tree. */ 116 private static final String ID2ENTRY_TREE_NAME = ID2ENTRY_INDEX_NAME; 117 /** The name of the DN tree. */ 118 private static final String DN2ID_TREE_NAME = DN2ID_INDEX_NAME; 119 /** The name of the children index tree. */ 120 private static final String ID2CHILDREN_COUNT_TREE_NAME = ID2CHILDREN_COUNT_NAME; 121 /** The name of the referral tree. */ 122 private static final String REFERRAL_TREE_NAME = REFERRAL_INDEX_NAME; 123 /** The name of the state tree. */ 124 private static final String STATE_TREE_NAME = STATE_INDEX_NAME; 125 126 /** The attribute index configuration manager. */ 127 private final AttributeIndexCfgManager attributeIndexCfgManager; 128 /** The vlv index configuration manager. */ 129 private final VLVIndexCfgManager vlvIndexCfgManager; 130 131 /** The backend configuration. */ 132 private PluggableBackendCfg config; 133 /** ID of the backend to which this entry container belongs. */ 134 private final String backendID; 135 /** The baseDN this entry container is responsible for. */ 136 private final DN baseDN; 137 /** The root container in which this entryContainer belongs. */ 138 private final RootContainer rootContainer; 139 /** The tree storage. */ 140 private final Storage storage; 141 142 /** The DN tree maps a normalized DN string to an entry ID (8 bytes). */ 143 private final DN2ID dn2id; 144 /** The entry tree maps an entry ID (8 bytes) to a complete encoded entry. */ 145 private ID2Entry id2entry; 146 /** Store the number of children for each entry. */ 147 private final ID2ChildrenCount id2childrenCount; 148 /** The referral tree maps a normalized DN string to labeled URIs. */ 149 private final DN2URI dn2uri; 150 /** The state tree maps a config DN to config entries. */ 151 private final State state; 152 153 /** The set of attribute indexes. */ 154 private final Map<AttributeType, AttributeIndex> attrIndexMap = new HashMap<>(); 155 /** The set of VLV (Virtual List View) indexes. */ 156 private final Map<String, VLVIndex> vlvIndexMap = new HashMap<>(); 157 158 /** 159 * Prevents name clashes for common indexes (like id2entry) across multiple suffixes. 160 * For example when a root container contains multiple suffixes. 161 */ 162 private final String treePrefix; 163 164 /** 165 * This class is responsible for managing the configuration for attribute 166 * indexes used within this entry container. 167 */ 168 private class AttributeIndexCfgManager implements 169 ConfigurationAddListener<BackendIndexCfg>, 170 ConfigurationDeleteListener<BackendIndexCfg> 171 { 172 @Override 173 public boolean isConfigurationAddAcceptable(final BackendIndexCfg cfg, List<LocalizableMessage> unacceptableReasons) 174 { 175 try 176 { 177 new AttributeIndex(cfg, state, EntryContainer.this); 178 return true; 179 } 180 catch(Exception e) 181 { 182 unacceptableReasons.add(LocalizableMessage.raw(e.getLocalizedMessage())); 183 return false; 184 } 185 } 186 187 @Override 188 public ConfigChangeResult applyConfigurationAdd(final BackendIndexCfg cfg) 189 { 190 final ConfigChangeResult ccr = new ConfigChangeResult(); 191 try 192 { 193 final AttributeIndex index = new AttributeIndex(cfg, state, EntryContainer.this); 194 storage.write(new WriteOperation() 195 { 196 @Override 197 public void run(WriteableTransaction txn) throws Exception 198 { 199 index.open(txn, true); 200 if (!index.isTrusted()) 201 { 202 ccr.setAdminActionRequired(true); 203 ccr.addMessage(NOTE_INDEX_ADD_REQUIRES_REBUILD.get(cfg.getAttribute().getNameOrOID())); 204 } 205 attrIndexMap.put(cfg.getAttribute(), index); 206 } 207 }); 208 } 209 catch(Exception e) 210 { 211 ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); 212 ccr.addMessage(LocalizableMessage.raw(e.getLocalizedMessage())); 213 } 214 return ccr; 215 } 216 217 @Override 218 public boolean isConfigurationDeleteAcceptable( 219 BackendIndexCfg cfg, List<LocalizableMessage> unacceptableReasons) 220 { 221 // TODO: validate more before returning true? 222 return true; 223 } 224 225 @Override 226 public ConfigChangeResult applyConfigurationDelete(final BackendIndexCfg cfg) 227 { 228 final ConfigChangeResult ccr = new ConfigChangeResult(); 229 230 exclusiveLock.lock(); 231 try 232 { 233 storage.write(new WriteOperation() 234 { 235 @Override 236 public void run(WriteableTransaction txn) throws Exception 237 { 238 attrIndexMap.remove(cfg.getAttribute()).closeAndDelete(txn); 239 } 240 }); 241 } 242 catch (Exception de) 243 { 244 ccr.setResultCode(getServerErrorResultCode()); 245 ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(de))); 246 } 247 finally 248 { 249 exclusiveLock.unlock(); 250 } 251 252 return ccr; 253 } 254 } 255 256 /** 257 * This class is responsible for managing the configuration for VLV indexes 258 * used within this entry container. 259 */ 260 private class VLVIndexCfgManager implements 261 ConfigurationAddListener<BackendVLVIndexCfg>, 262 ConfigurationDeleteListener<BackendVLVIndexCfg> 263 { 264 @Override 265 public boolean isConfigurationAddAcceptable(BackendVLVIndexCfg cfg, List<LocalizableMessage> unacceptableReasons) 266 { 267 return VLVIndex.isConfigurationAddAcceptable(cfg, unacceptableReasons); 268 } 269 270 @Override 271 public ConfigChangeResult applyConfigurationAdd(final BackendVLVIndexCfg cfg) 272 { 273 final ConfigChangeResult ccr = new ConfigChangeResult(); 274 try 275 { 276 storage.write(new WriteOperation() 277 { 278 @Override 279 public void run(WriteableTransaction txn) throws Exception 280 { 281 VLVIndex vlvIndex = new VLVIndex(cfg, state, storage, EntryContainer.this, txn); 282 vlvIndex.open(txn, true); 283 if(!vlvIndex.isTrusted()) 284 { 285 ccr.setAdminActionRequired(true); 286 ccr.addMessage(NOTE_INDEX_ADD_REQUIRES_REBUILD.get(cfg.getName())); 287 } 288 vlvIndexMap.put(cfg.getName().toLowerCase(), vlvIndex); 289 } 290 }); 291 } 292 catch(Exception e) 293 { 294 ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); 295 ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(e))); 296 } 297 return ccr; 298 } 299 300 @Override 301 public boolean isConfigurationDeleteAcceptable(BackendVLVIndexCfg cfg, List<LocalizableMessage> unacceptableReasons) 302 { 303 // TODO: validate more before returning true? 304 return true; 305 } 306 307 @Override 308 public ConfigChangeResult applyConfigurationDelete(final BackendVLVIndexCfg cfg) 309 { 310 final ConfigChangeResult ccr = new ConfigChangeResult(); 311 exclusiveLock.lock(); 312 try 313 { 314 storage.write(new WriteOperation() 315 { 316 @Override 317 public void run(WriteableTransaction txn) throws Exception 318 { 319 vlvIndexMap.remove(cfg.getName().toLowerCase()).closeAndDelete(txn); 320 } 321 }); 322 } 323 catch (Exception e) 324 { 325 ccr.setResultCode(getServerErrorResultCode()); 326 ccr.addMessage(LocalizableMessage.raw(StaticUtils.stackTraceToSingleLineString(e))); 327 } 328 finally 329 { 330 exclusiveLock.unlock(); 331 } 332 return ccr; 333 } 334 } 335 336 /** A read write lock to handle schema changes and bulk changes. */ 337 private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 338 final Lock sharedLock = lock.readLock(); 339 final Lock exclusiveLock = lock.writeLock(); 340 341 /** 342 * Create a new entry container object. 343 * 344 * @param baseDN The baseDN this entry container will be responsible for 345 * storing on disk. 346 * @param backendID ID of the backend that is creating this entry container. 347 * It is needed by the Directory Server entry cache methods. 348 * @param config The configuration of the backend. 349 * @param storage The storage for this entryContainer. 350 * @param rootContainer The root container this entry container is in. 351 * @throws ConfigException if a configuration related error occurs. 352 */ 353 EntryContainer(DN baseDN, String backendID, PluggableBackendCfg config, Storage storage, 354 RootContainer rootContainer) throws ConfigException 355 { 356 this.backendID = backendID; 357 this.baseDN = baseDN; 358 this.config = config; 359 this.storage = storage; 360 this.rootContainer = rootContainer; 361 this.treePrefix = baseDN.toNormalizedUrlSafeString(); 362 this.id2childrenCount = new ID2ChildrenCount(getIndexName(ID2CHILDREN_COUNT_TREE_NAME)); 363 this.dn2id = new DN2ID(getIndexName(DN2ID_TREE_NAME), baseDN); 364 this.dn2uri = new DN2URI(getIndexName(REFERRAL_TREE_NAME), this); 365 this.state = new State(getIndexName(STATE_TREE_NAME)); 366 367 config.addPluggableChangeListener(this); 368 369 attributeIndexCfgManager = new AttributeIndexCfgManager(); 370 config.addBackendIndexAddListener(attributeIndexCfgManager); 371 config.addBackendIndexDeleteListener(attributeIndexCfgManager); 372 373 vlvIndexCfgManager = new VLVIndexCfgManager(); 374 config.addBackendVLVIndexAddListener(vlvIndexCfgManager); 375 config.addBackendVLVIndexDeleteListener(vlvIndexCfgManager); 376 } 377 378 private TreeName getIndexName(String indexId) 379 { 380 return new TreeName(treePrefix, indexId); 381 } 382 383 /** 384 * Opens the entryContainer for reading and writing. 385 * 386 * @param txn a non null transaction 387 * @param accessMode specifies how the container has to be opened (read-write or read-only) 388 * @throws StorageRuntimeException If an error occurs in the storage. 389 * @throws ConfigException if a configuration related error occurs. 390 */ 391 void open(WriteableTransaction txn, AccessMode accessMode) throws StorageRuntimeException, ConfigException 392 { 393 boolean shouldCreate = accessMode.isWriteable(); 394 try 395 { 396 DataConfig entryDataConfig = new DataConfig( 397 config.isEntriesCompressed(), config.isCompactEncoding(), rootContainer.getCompressedSchema()); 398 399 id2entry = new ID2Entry(getIndexName(ID2ENTRY_TREE_NAME), entryDataConfig); 400 id2entry.open(txn, shouldCreate); 401 id2childrenCount.open(txn, shouldCreate); 402 dn2id.open(txn, shouldCreate); 403 state.open(txn, shouldCreate); 404 dn2uri.open(txn, shouldCreate); 405 406 for (String idx : config.listBackendIndexes()) 407 { 408 BackendIndexCfg indexCfg = config.getBackendIndex(idx); 409 410 final AttributeIndex index = new AttributeIndex(indexCfg, state, this); 411 index.open(txn, shouldCreate); 412 if(!index.isTrusted()) 413 { 414 logger.info(NOTE_INDEX_ADD_REQUIRES_REBUILD, index.getName()); 415 } 416 attrIndexMap.put(indexCfg.getAttribute(), index); 417 } 418 419 for (String idx : config.listBackendVLVIndexes()) 420 { 421 BackendVLVIndexCfg vlvIndexCfg = config.getBackendVLVIndex(idx); 422 423 VLVIndex vlvIndex = new VLVIndex(vlvIndexCfg, state, storage, this, txn); 424 vlvIndex.open(txn, shouldCreate); 425 if(!vlvIndex.isTrusted()) 426 { 427 logger.info(NOTE_INDEX_ADD_REQUIRES_REBUILD, vlvIndex.getName()); 428 } 429 430 vlvIndexMap.put(vlvIndexCfg.getName().toLowerCase(), vlvIndex); 431 } 432 } 433 catch (StorageRuntimeException de) 434 { 435 logger.traceException(de); 436 close(); 437 throw de; 438 } 439 } 440 441 /** 442 * Closes the entry container. 443 * 444 * @throws StorageRuntimeException If an error occurs in the storage. 445 */ 446 @Override 447 public void close() throws StorageRuntimeException 448 { 449 closeSilently(attrIndexMap.values()); 450 closeSilently(vlvIndexMap.values()); 451 452 // Deregister any listeners. 453 config.removePluggableChangeListener(this); 454 config.removeBackendIndexAddListener(attributeIndexCfgManager); 455 config.removeBackendIndexDeleteListener(attributeIndexCfgManager); 456 config.removeBackendVLVIndexAddListener(vlvIndexCfgManager); 457 config.removeBackendVLVIndexDeleteListener(vlvIndexCfgManager); 458 } 459 460 /** 461 * Retrieves a reference to the root container in which this entry container 462 * exists. 463 * 464 * @return A reference to the root container in which this entry container 465 * exists. 466 */ 467 RootContainer getRootContainer() 468 { 469 return rootContainer; 470 } 471 472 /** 473 * Get the DN tree used by this entry container. 474 * The entryContainer must have been opened. 475 * 476 * @return The DN tree. 477 */ 478 DN2ID getDN2ID() 479 { 480 return dn2id; 481 } 482 483 /** 484 * Get the entry tree used by this entry container. 485 * The entryContainer must have been opened. 486 * 487 * @return The entry tree. 488 */ 489 ID2Entry getID2Entry() 490 { 491 return id2entry; 492 } 493 494 /** 495 * Get the referral tree used by this entry container. 496 * The entryContainer must have been opened. 497 * 498 * @return The referral tree. 499 */ 500 DN2URI getDN2URI() 501 { 502 return dn2uri; 503 } 504 505 /** 506 * Get the children tree used by this entry container. 507 * The entryContainer must have been opened. 508 * 509 * @return The children tree. 510 */ 511 ID2ChildrenCount getID2ChildrenCount() 512 { 513 return id2childrenCount; 514 } 515 516 /** 517 * Look for an attribute index for the given attribute type. 518 * 519 * @param attrType The attribute type for which an attribute index is needed. 520 * @return The attribute index or null if there is none for that type. 521 */ 522 AttributeIndex getAttributeIndex(AttributeType attrType) 523 { 524 return attrIndexMap.get(attrType); 525 } 526 527 /** 528 * Look for a VLV index for the given index name. 529 * 530 * @param vlvIndexName The vlv index name for which an vlv index is needed. 531 * @return The VLV index or null if there is none with that name. 532 */ 533 VLVIndex getVLVIndex(String vlvIndexName) 534 { 535 return vlvIndexMap.get(vlvIndexName); 536 } 537 538 /** 539 * Retrieve all attribute indexes. 540 * 541 * @return All attribute indexes defined in this entry container. 542 */ 543 Collection<AttributeIndex> getAttributeIndexes() 544 { 545 return attrIndexMap.values(); 546 } 547 548 /** 549 * Retrieve all VLV indexes. 550 * 551 * @return The collection of VLV indexes defined in this entry container. 552 */ 553 Collection<VLVIndex> getVLVIndexes() 554 { 555 return vlvIndexMap.values(); 556 } 557 558 /** 559 * Determine the highest entryID in the entryContainer. 560 * The entryContainer must already be open. 561 * 562 * @param txn a non null transaction 563 * @return The highest entry ID. 564 * @throws StorageRuntimeException If an error occurs in the storage. 565 */ 566 EntryID getHighestEntryID(ReadableTransaction txn) throws StorageRuntimeException 567 { 568 try (Cursor<ByteString, ByteString> cursor = txn.openCursor(id2entry.getName())) 569 { 570 // Position a cursor on the last data item, and the key should give the highest ID. 571 if (cursor.positionToLastKey()) 572 { 573 return new EntryID(cursor.getKey()); 574 } 575 return new EntryID(0); 576 } 577 } 578 579 boolean hasSubordinates(final DN dn) 580 { 581 try 582 { 583 return storage.read(new ReadOperation<Boolean>() 584 { 585 @Override 586 public Boolean run(final ReadableTransaction txn) throws Exception 587 { 588 try (final SequentialCursor<?, ?> cursor = dn2id.openChildrenCursor(txn, dn)) 589 { 590 return cursor.next(); 591 } 592 } 593 }); 594 } 595 catch (Exception e) 596 { 597 throw new StorageRuntimeException(e); 598 } 599 } 600 601 /** 602 * Determine the number of children entries for a given entry. 603 * 604 * @param entryDN The distinguished name of the entry. 605 * @return The number of children entries for the given entry or -1 if 606 * the entry does not exist. 607 * @throws StorageRuntimeException If an error occurs in the storage. 608 */ 609 long getNumberOfChildren(final DN entryDN) throws StorageRuntimeException 610 { 611 try 612 { 613 return storage.read(new ReadOperation<Long>() 614 { 615 @Override 616 public Long run(ReadableTransaction txn) throws Exception 617 { 618 final EntryID entryID = dn2id.get(txn, entryDN); 619 return entryID != null ? id2childrenCount.getCount(txn, entryID) : -1; 620 } 621 }); 622 } 623 catch (Exception e) 624 { 625 throw new StorageRuntimeException(e); 626 } 627 } 628 629 /** 630 * Processes the specified search in this entryContainer. 631 * Matching entries should be provided back to the core server using the 632 * <CODE>SearchOperation.returnEntry</CODE> method. 633 * 634 * @param searchOperation The search operation to be processed. 635 * @throws DirectoryException 636 * If a problem occurs while processing the 637 * search. 638 * @throws StorageRuntimeException If an error occurs in the storage. 639 * @throws CanceledOperationException if this operation should be cancelled. 640 */ 641 void search(final SearchOperation searchOperation) 642 throws DirectoryException, StorageRuntimeException, CanceledOperationException 643 { 644 try 645 { 646 storage.read(new ReadOperation<Void>() 647 { 648 @Override 649 public Void run(final ReadableTransaction txn) throws Exception 650 { 651 DN aBaseDN = searchOperation.getBaseDN(); 652 SearchScope searchScope = searchOperation.getScope(); 653 654 PagedResultsControl pageRequest = searchOperation.getRequestControl(PagedResultsControl.DECODER); 655 ServerSideSortRequestControl sortRequest = 656 searchOperation.getRequestControl(ServerSideSortRequestControl.DECODER); 657 if (sortRequest != null && !sortRequest.containsSortKeys() && sortRequest.isCritical()) 658 { 659 /* 660 * If the control's criticality field is true then the server SHOULD 661 * do the following: return unavailableCriticalExtension as a return 662 * code in the searchResultDone message; include the 663 * sortKeyResponseControl in the searchResultDone message, and not 664 * send back any search result entries. 665 */ 666 addServerSideSortControl(searchOperation, NO_SUCH_ATTRIBUTE); 667 searchOperation.setResultCode(ResultCode.UNAVAILABLE_CRITICAL_EXTENSION); 668 return null; 669 } 670 671 VLVRequestControl vlvRequest = searchOperation.getRequestControl(VLVRequestControl.DECODER); 672 if (vlvRequest != null && pageRequest != null) 673 { 674 throw new DirectoryException( 675 ResultCode.CONSTRAINT_VIOLATION, ERR_SEARCH_CANNOT_MIX_PAGEDRESULTS_AND_VLV.get()); 676 } 677 678 // Handle client abandon of paged results. 679 if (pageRequest != null) 680 { 681 if (pageRequest.getSize() == 0) 682 { 683 addPagedResultsControl(searchOperation, pageRequest, null); 684 return null; 685 } 686 if (searchOperation.getSizeLimit() > 0 && pageRequest.getSize() >= searchOperation.getSizeLimit()) 687 { 688 // The RFC says : "If the page size is greater than or equal to the 689 // sizeLimit value, the server should ignore the control as the 690 // request can be satisfied in a single page" 691 pageRequest = null; 692 } 693 } 694 695 // Handle base-object search first. 696 if (searchScope == SearchScope.BASE_OBJECT) 697 { 698 searchBaseObject(txn, searchOperation, pageRequest); 699 return null; 700 } 701 702 // Check whether the client requested debug information about the 703 // contribution of the indexes to the search. 704 StringBuilder debugBuffer = null; 705 if (searchOperation.getAttributes().contains(ATTR_DEBUG_SEARCH_INDEX)) 706 { 707 debugBuffer = new StringBuilder(); 708 } 709 710 EntryIDSet candidateEntryIDs = null; 711 boolean candidatesAreInScope = false; 712 if (sortRequest != null) 713 { 714 for (VLVIndex vlvIndex : vlvIndexMap.values()) 715 { 716 try 717 { 718 candidateEntryIDs = vlvIndex.evaluate(txn, searchOperation, sortRequest, vlvRequest, debugBuffer); 719 if (candidateEntryIDs != null) 720 { 721 addServerSideSortControl(searchOperation, SUCCESS); 722 candidatesAreInScope = true; 723 break; 724 } 725 } 726 catch (DirectoryException de) 727 { 728 serverSideSortControlError(searchOperation, sortRequest, de); 729 } 730 } 731 } 732 733 // Combining server-side sort with paged result controls 734 // requires us to use an entryIDSet where the entryIDs are ordered 735 // so further paging can restart where it previously stopped 736 long[] reorderedCandidateEntryIDs; 737 if (candidateEntryIDs == null) 738 { 739 if (processSearchWithVirtualAttributeRule(searchOperation, true)) 740 { 741 return null; 742 } 743 744 // Create an index filter to get the search result candidate entries 745 IndexFilter indexFilter = new IndexFilter( 746 EntryContainer.this, txn, searchOperation, debugBuffer, rootContainer.getMonitorProvider()); 747 748 // Evaluate the filter against the attribute indexes. 749 candidateEntryIDs = indexFilter.evaluate(); 750 if (!isBelowFilterThreshold(candidateEntryIDs)) 751 { 752 final int idSetLimit = getEntryIDSetLimit(searchOperation); 753 final EntryIDSet scopeSet = getIDSetFromScope(txn, aBaseDN, searchScope, idSetLimit); 754 candidateEntryIDs.retainAll(scopeSet); 755 if (debugBuffer != null) 756 { 757 debugBuffer.append(" scope=").append(searchScope); 758 scopeSet.toString(debugBuffer); 759 } 760 if (scopeSet.isDefined()) 761 { 762 // In this case we know that every candidate is in scope. 763 candidatesAreInScope = true; 764 } 765 } 766 767 if (sortRequest != null) 768 { 769 // If the sort key is not present, the sorting will generate the 770 // default ordering. VLV search request goes through as if 771 // this sort key was not found in the user entry. 772 try 773 { 774 SortOrder sortOrder = sortRequest.getSortOrder(); 775 reorderedCandidateEntryIDs = sort(txn, candidateEntryIDs, searchOperation, sortOrder, vlvRequest); 776 } 777 catch (DirectoryException de) 778 { 779 reorderedCandidateEntryIDs = candidateEntryIDs.toLongArray(); 780 serverSideSortControlError(searchOperation, sortRequest, de); 781 } 782 try 783 { 784 if (sortRequest.containsSortKeys()) 785 { 786 addServerSideSortControl(searchOperation, SUCCESS); 787 } 788 else 789 { 790 /* 791 * There is no sort key associated with the sort control. 792 * Since it came here it means that the criticality is false 793 * so let the server return all search results unsorted and 794 * include the sortKeyResponseControl in the searchResultDone 795 * message. 796 */ 797 addServerSideSortControl(searchOperation, NO_SUCH_ATTRIBUTE); 798 } 799 } 800 catch (DirectoryException de) 801 { 802 serverSideSortControlError(searchOperation, sortRequest, de); 803 } 804 } 805 else 806 { 807 reorderedCandidateEntryIDs = candidateEntryIDs.toLongArray(); 808 } 809 } 810 else 811 { 812 reorderedCandidateEntryIDs = candidateEntryIDs.toLongArray(); 813 } 814 815 // If requested, construct and return a fictitious entry containing 816 // debug information, and no other entries. 817 if (debugBuffer != null) 818 { 819 debugBuffer.append(" final="); 820 candidateEntryIDs.toString(debugBuffer); 821 822 Entry debugEntry = buildDebugSearchIndexEntry(debugBuffer); 823 searchOperation.returnEntry(debugEntry, null); 824 return null; 825 } 826 827 if (reorderedCandidateEntryIDs != null) 828 { 829 rootContainer.getMonitorProvider().incrementIndexedSearchCount(); 830 searchIndexed(txn, reorderedCandidateEntryIDs, candidatesAreInScope, searchOperation, pageRequest); 831 } 832 else 833 { 834 rootContainer.getMonitorProvider().incrementUnindexedSearchCount(); 835 836 searchOperation.addAdditionalLogItem(keyOnly(getClass(), "unindexed")); 837 838 if (processSearchWithVirtualAttributeRule(searchOperation, false)) 839 { 840 return null; 841 } 842 843 ClientConnection clientConnection = searchOperation.getClientConnection(); 844 if (!clientConnection.hasPrivilege(Privilege.UNINDEXED_SEARCH, searchOperation)) 845 { 846 throw new DirectoryException( 847 ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_SEARCH_UNINDEXED_INSUFFICIENT_PRIVILEGES.get()); 848 } 849 850 if (sortRequest != null) 851 { 852 // FIXME OPENDJ-2628: Add support for sorting unindexed searches using indexes like DSEE currently does 853 addServerSideSortControl(searchOperation, UNWILLING_TO_PERFORM); 854 if (sortRequest.isCritical()) 855 { 856 throw new DirectoryException( 857 ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_SEARCH_CANNOT_SORT_UNINDEXED.get()); 858 } 859 } 860 861 searchNotIndexed(txn, searchOperation, pageRequest); 862 } 863 return null; 864 } 865 866 private int getEntryIDSetLimit(final SearchOperation searchOperation) 867 { 868 final int lookThroughLimit = searchOperation.getClientConnection().getLookthroughLimit(); 869 final int indexLimit = config.getIndexEntryLimit() == 0 ? CURSOR_ENTRY_LIMIT : config.getIndexEntryLimit(); 870 return lookThroughLimit > 0 ? Math.min(indexLimit, lookThroughLimit) : indexLimit; 871 } 872 873 private void searchBaseObject(ReadableTransaction txn, SearchOperation searchOperation, 874 PagedResultsControl pageRequest) throws DirectoryException 875 { 876 final Entry baseEntry = fetchBaseEntry(txn, searchOperation.getBaseDN(), searchOperation.getScope()); 877 if (!isManageDsaITOperation(searchOperation)) 878 { 879 dn2uri.checkTargetForReferral(baseEntry, searchOperation.getScope()); 880 } 881 882 if (searchOperation.getFilter().matchesEntry(baseEntry)) 883 { 884 searchOperation.returnEntry(baseEntry, null); 885 } 886 887 // Indicate no more pages. 888 addPagedResultsControl(searchOperation, pageRequest, null); 889 } 890 891 private void serverSideSortControlError(final SearchOperation searchOperation, 892 ServerSideSortRequestControl sortRequest, DirectoryException de) throws DirectoryException 893 { 894 addServerSideSortControl(searchOperation, de.getResultCode().intValue()); 895 if (sortRequest.isCritical()) 896 { 897 throw de; 898 } 899 } 900 901 private void addServerSideSortControl(SearchOperation searchOp, int resultCode) 902 { 903 searchOp.addResponseControl(new ServerSideSortResponseControl(resultCode, null)); 904 } 905 906 private EntryIDSet getIDSetFromScope(final ReadableTransaction txn, DN aBaseDN, SearchScope searchScope, 907 int idSetLimit) throws DirectoryException 908 { 909 final EntryIDSet scopeSet; 910 try 911 { 912 switch (searchScope.asEnum()) 913 { 914 case BASE_OBJECT: 915 try (final SequentialCursor<?, EntryID> scopeCursor = dn2id.openCursor(txn, aBaseDN)) 916 { 917 scopeSet = EntryIDSet.newDefinedSet(scopeCursor.getValue().longValue()); 918 } 919 break; 920 case SINGLE_LEVEL: 921 try (final SequentialCursor<?, EntryID> scopeCursor = dn2id.openChildrenCursor(txn, aBaseDN)) 922 { 923 scopeSet = newIDSetFromCursor(scopeCursor, false, idSetLimit); 924 } 925 break; 926 case SUBORDINATES: 927 case WHOLE_SUBTREE: 928 try (final SequentialCursor<?, EntryID> scopeCursor = dn2id.openSubordinatesCursor(txn, aBaseDN)) 929 { 930 scopeSet = newIDSetFromCursor(scopeCursor, searchScope.equals(SearchScope.WHOLE_SUBTREE), idSetLimit); 931 } 932 break; 933 default: 934 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 935 CoreMessages.INFO_ERROR_SEARCH_SCOPE_NOT_ALLOWED.get()); 936 } 937 } 938 catch (NoSuchElementException e) 939 { 940 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_SEARCH_NO_SUCH_OBJECT.get(aBaseDN), 941 getMatchedDN(txn, aBaseDN), e); 942 } 943 return scopeSet; 944 } 945 }); 946 } 947 catch (Exception e) 948 { 949 throwAllowedExceptionTypes(e, DirectoryException.class, CanceledOperationException.class); 950 } 951 } 952 953 private static EntryIDSet newIDSetFromCursor(SequentialCursor<?, EntryID> cursor, boolean includeCurrent, 954 int idSetLimit) 955 { 956 long entryIDs[] = new long[idSetLimit]; 957 int offset = 0; 958 if (includeCurrent) 959 { 960 entryIDs[offset++] = cursor.getValue().longValue(); 961 } 962 963 while(offset < idSetLimit && cursor.next()) 964 { 965 entryIDs[offset++] = cursor.getValue().longValue(); 966 } 967 968 if (offset == idSetLimit && cursor.next()) 969 { 970 return EntryIDSet.newUndefinedSet(); 971 } 972 else if (offset != idSetLimit) 973 { 974 entryIDs = Arrays.copyOf(entryIDs, offset); 975 } 976 Arrays.sort(entryIDs); 977 978 return EntryIDSet.newDefinedSet(entryIDs); 979 } 980 981 private <E1 extends Exception, E2 extends Exception> 982 void throwAllowedExceptionTypes(Exception e, Class<E1> clazz1, Class<E2> clazz2) 983 throws E1, E2 984 { 985 throwIfPossible(e, clazz1, clazz2); 986 if (e.getCause() != null) 987 { 988 throwIfPossible(e.getCause(), clazz1, clazz2); 989 } 990 else if (e instanceof StorageRuntimeException) 991 { 992 throw (StorageRuntimeException) e; 993 } 994 throw new StorageRuntimeException(e); 995 } 996 997 private static <E1 extends Exception, E2 extends Exception> void throwIfPossible(final Throwable cause, 998 Class<E1> clazz1, Class<E2> clazz2) throws E1, E2 999 { 1000 if (clazz1.isAssignableFrom(cause.getClass())) 1001 { 1002 throw clazz1.cast(cause); 1003 } 1004 else if (clazz2.isAssignableFrom(cause.getClass())) 1005 { 1006 throw clazz2.cast(cause); 1007 } 1008 } 1009 1010 private static boolean processSearchWithVirtualAttributeRule(final SearchOperation searchOperation, 1011 boolean isPreIndexed) 1012 { 1013 for (VirtualAttributeRule rule : DirectoryServer.getVirtualAttributes()) 1014 { 1015 VirtualAttributeProvider<?> provider = rule.getProvider(); 1016 if (provider.isSearchable(rule, searchOperation, isPreIndexed)) 1017 { 1018 provider.processSearch(rule, searchOperation); 1019 return true; 1020 } 1021 } 1022 return false; 1023 } 1024 1025 private static Entry buildDebugSearchIndexEntry(StringBuilder debugBuffer) throws DirectoryException 1026 { 1027 Attribute attr = Attributes.create(ATTR_DEBUG_SEARCH_INDEX, debugBuffer.toString()); 1028 Entry entry = new Entry(DN.valueOf("cn=debugsearch"), null, null, null); 1029 entry.addAttribute(attr, new ArrayList<ByteString>()); 1030 return entry; 1031 } 1032 1033 /** 1034 * We were not able to obtain a set of candidate entry IDs for the 1035 * search from the indexes. 1036 * <p> 1037 * Here we are relying on the DN key order to ensure children are 1038 * returned after their parents. 1039 * <ul> 1040 * <li>iterate through a subtree range of the DN tree 1041 * <li>discard non-children DNs if the search scope is single level 1042 * <li>fetch the entry by ID from the entry cache or the entry tree 1043 * <li>return the entry if it matches the filter 1044 * </ul> 1045 * 1046 * @param searchOperation The search operation. 1047 * @param pageRequest A Paged Results control, or null if none. 1048 * @throws DirectoryException If an error prevented the search from being 1049 * processed. 1050 */ 1051 private void searchNotIndexed(ReadableTransaction txn, SearchOperation searchOperation, 1052 PagedResultsControl pageRequest) throws DirectoryException, CanceledOperationException 1053 { 1054 DN aBaseDN = searchOperation.getBaseDN(); 1055 SearchScope searchScope = searchOperation.getScope(); 1056 boolean manageDsaIT = isManageDsaITOperation(searchOperation); 1057 1058 // The base entry must already have been processed if this is 1059 // a request for the next page in paged results. So we skip 1060 // the base entry processing if the cookie is set. 1061 if (pageRequest == null || pageRequest.getCookie().length() == 0) 1062 { 1063 final Entry baseEntry = fetchBaseEntry(txn, aBaseDN, searchScope); 1064 if (!manageDsaIT) 1065 { 1066 dn2uri.checkTargetForReferral(baseEntry, searchScope); 1067 } 1068 1069 /* The base entry is only included for whole subtree search. */ 1070 if (searchScope == SearchScope.WHOLE_SUBTREE 1071 && searchOperation.getFilter().matchesEntry(baseEntry)) 1072 { 1073 searchOperation.returnEntry(baseEntry, null); 1074 } 1075 1076 if (!manageDsaIT && !dn2uri.returnSearchReferences(txn, searchOperation)) 1077 { 1078 // Indicate no more pages. 1079 addPagedResultsControl(searchOperation, pageRequest, null); 1080 } 1081 } 1082 1083 /* 1084 * We will iterate forwards through a range of the dn2id keys to 1085 * find subordinates of the target entry from the top of the tree 1086 * downwards. For example, any subordinates of dn "dc=example,dc=com" appear 1087 * in dn2id with a dn ending in ",dc=example,dc=com". The dn 1088 * "cn=joe,ou=people,dc=example,dc=com" will appear after the dn 1089 * "ou=people,dc=example,dc=com". 1090 */ 1091 ByteString baseDNKey = dnToDNKey(aBaseDN, this.baseDN.size()); 1092 ByteStringBuilder beforeFirstChild = beforeFirstChildOf(baseDNKey); 1093 ByteStringBuilder afterLastChild = afterLastChildOf(baseDNKey); 1094 1095 // Set the starting value. 1096 ByteSequence begin; 1097 if (pageRequest != null && pageRequest.getCookie().length() != 0) 1098 { 1099 // The cookie contains the DN of the next entry to be returned. 1100 try 1101 { 1102 begin = ByteString.wrap(pageRequest.getCookie().toByteArray()); 1103 } 1104 catch (Exception e) 1105 { 1106 logger.traceException(e); 1107 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 1108 ERR_INVALID_PAGED_RESULTS_COOKIE.get(pageRequest.getCookie().toHexString()), e); 1109 } 1110 } 1111 else 1112 { 1113 // Set the starting value to the suffix. 1114 begin = beforeFirstChild; 1115 } 1116 1117 int lookthroughCount = 0; 1118 int lookthroughLimit = searchOperation.getClientConnection().getLookthroughLimit(); 1119 1120 try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(dn2id.getName())) 1121 { 1122 // Initialize the cursor very close to the starting value. 1123 boolean success = cursor.positionToKeyOrNext(begin); 1124 1125 // Step forward until we pass the ending value. 1126 while (success && cursor.getKey().compareTo(afterLastChild) < 0) 1127 { 1128 if (lookthroughLimit > 0 && lookthroughCount > lookthroughLimit) 1129 { 1130 // Lookthrough limit exceeded 1131 searchOperation.setResultCode(ResultCode.ADMIN_LIMIT_EXCEEDED); 1132 searchOperation.appendErrorMessage(NOTE_LOOKTHROUGH_LIMIT_EXCEEDED.get(lookthroughLimit)); 1133 return; 1134 } 1135 1136 // We have found a subordinate entry. 1137 EntryID entryID = new EntryID(cursor.getValue()); 1138 boolean isInScope = 1139 searchScope != SearchScope.SINGLE_LEVEL 1140 // Check if this entry is an immediate child. 1141 || findDNKeyParent(cursor.getKey()) == baseDNKey.length(); 1142 if (isInScope) 1143 { 1144 // Process the candidate entry. 1145 final Entry entry = getEntry(txn, entryID); 1146 if (entry != null) 1147 { 1148 lookthroughCount++; 1149 1150 if ((manageDsaIT || entry.getReferralURLs() == null) 1151 && searchOperation.getFilter().matchesEntry(entry)) 1152 { 1153 if (isPageFull(searchOperation, pageRequest)) 1154 { 1155 // Set the cookie to remember where we were. 1156 addPagedResultsControl(searchOperation, pageRequest, cursor.getKey()); 1157 return; 1158 } 1159 1160 if (!searchOperation.returnEntry(entry, null)) 1161 { 1162 // We have been told to discontinue processing of the search. 1163 // This could be due to size limit exceeded or operation cancelled 1164 return; 1165 } 1166 } 1167 } 1168 } 1169 1170 searchOperation.checkIfCanceled(false); 1171 1172 // Move to the next record. 1173 success = cursor.next(); 1174 } 1175 } 1176 catch (StorageRuntimeException e) 1177 { 1178 logger.traceException(e); 1179 } 1180 1181 // Indicate no more pages. 1182 addPagedResultsControl(searchOperation, pageRequest, null); 1183 } 1184 1185 private boolean isPageFull(SearchOperation searchOperation, PagedResultsControl pageRequest) 1186 { 1187 return pageRequest != null && searchOperation.getEntriesSent() == pageRequest.getSize(); 1188 } 1189 1190 private void addPagedResultsControl(SearchOperation searchOp, PagedResultsControl pageRequest, ByteString cookie) 1191 { 1192 if (pageRequest != null) 1193 { 1194 searchOp.addResponseControl(new PagedResultsControl(pageRequest.isCritical(), 0, cookie)); 1195 } 1196 } 1197 1198 /** 1199 * Returns the entry corresponding to the provided entryID. 1200 * 1201 * @param txn a non null transaction 1202 * @param entryID 1203 * the id of the entry to retrieve 1204 * @return the entry corresponding to the provided entryID 1205 * @throws DirectoryException 1206 * If an error occurs retrieving the entry 1207 */ 1208 private Entry getEntry(ReadableTransaction txn, EntryID entryID) throws DirectoryException 1209 { 1210 // Try the entry cache first. 1211 final EntryCache<?> entryCache = getEntryCache(); 1212 final Entry cacheEntry = entryCache.getEntry(backendID, entryID.longValue()); 1213 if (cacheEntry != null) 1214 { 1215 return cacheEntry; 1216 } 1217 1218 final Entry entry = id2entry.get(txn, entryID); 1219 if (entry != null) 1220 { 1221 // Put the entry in the cache making sure not to overwrite a newer copy 1222 // that may have been inserted since the time we read the cache. 1223 entryCache.putEntryIfAbsent(entry, backendID, entryID.longValue()); 1224 } 1225 return entry; 1226 } 1227 1228 /** 1229 * We were able to obtain a set of candidate entry IDs for the search from the indexes. 1230 * <p> 1231 * Here we are relying on ID order to ensure children are returned after their parents. 1232 * <ul> 1233 * <li>Iterate through the candidate IDs 1234 * <li>fetch entry by ID from cache or id2entry 1235 * <li>put the entry in the cache if not present 1236 * <li>discard entries that are not in scope 1237 * <li>return entry if it matches the filter 1238 * </ul> 1239 * 1240 * @param entryIDReorderedSet 1241 * The candidate entry IDs. 1242 * @param candidatesAreInScope 1243 * true if it is certain that every candidate entry is in the search scope. 1244 * @param searchOperation 1245 * The search operation. 1246 * @param pageRequest 1247 * A Paged Results control, or null if none. 1248 * @throws DirectoryException 1249 * If an error prevented the search from being processed. 1250 */ 1251 private void searchIndexed(ReadableTransaction txn, long[] entryIDReorderedSet, boolean candidatesAreInScope, 1252 SearchOperation searchOperation, PagedResultsControl pageRequest) throws DirectoryException, 1253 CanceledOperationException 1254 { 1255 SearchScope searchScope = searchOperation.getScope(); 1256 DN aBaseDN = searchOperation.getBaseDN(); 1257 boolean manageDsaIT = isManageDsaITOperation(searchOperation); 1258 boolean continueSearch = true; 1259 1260 // Set the starting value. 1261 Long beginEntryID = null; 1262 if (pageRequest != null && pageRequest.getCookie().length() != 0) 1263 { 1264 // The cookie contains the ID of the next entry to be returned. 1265 try 1266 { 1267 beginEntryID = pageRequest.getCookie().toLong(); 1268 } 1269 catch (Exception e) 1270 { 1271 logger.traceException(e); 1272 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 1273 ERR_INVALID_PAGED_RESULTS_COOKIE.get(pageRequest.getCookie().toHexString()), e); 1274 } 1275 } 1276 else if (!manageDsaIT) 1277 { 1278 continueSearch = dn2uri.returnSearchReferences(txn, searchOperation); 1279 } 1280 1281 // Make sure the candidate list is smaller than the lookthrough limit 1282 int lookthroughLimit = 1283 searchOperation.getClientConnection().getLookthroughLimit(); 1284 if (lookthroughLimit > 0 && entryIDReorderedSet.length > lookthroughLimit) 1285 { 1286 //Lookthrough limit exceeded 1287 searchOperation.setResultCode(ResultCode.ADMIN_LIMIT_EXCEEDED); 1288 searchOperation.appendErrorMessage(NOTE_LOOKTHROUGH_LIMIT_EXCEEDED.get(lookthroughLimit)); 1289 continueSearch = false; 1290 } 1291 1292 // Iterate through the index candidates. 1293 if (continueSearch) 1294 { 1295 final SearchFilter filter = searchOperation.getFilter(); 1296 for (int i = findStartIndex(beginEntryID, entryIDReorderedSet); i < entryIDReorderedSet.length; i++) 1297 { 1298 EntryID entryID = new EntryID(entryIDReorderedSet[i]); 1299 Entry entry; 1300 try 1301 { 1302 entry = getEntry(txn, entryID); 1303 } 1304 catch (Exception e) 1305 { 1306 logger.traceException(e); 1307 continue; 1308 } 1309 1310 // Process the candidate entry. 1311 if (entry != null 1312 && isInScope(candidatesAreInScope, searchScope, aBaseDN, entry) 1313 && (manageDsaIT || entry.getReferralURLs() == null) 1314 && filter.matchesEntry(entry)) 1315 { 1316 if (isPageFull(searchOperation, pageRequest)) 1317 { 1318 // Set the cookie to remember where we were. 1319 addPagedResultsControl(searchOperation, pageRequest, entryID.toByteString()); 1320 return; 1321 } 1322 1323 if (!searchOperation.returnEntry(entry, null)) 1324 { 1325 // We have been told to discontinue processing of the search. 1326 // This could be due to size limit exceeded or operation cancelled 1327 break; 1328 } 1329 } 1330 } 1331 searchOperation.checkIfCanceled(false); 1332 } 1333 1334 // Before we return success from the search we must ensure the base entry 1335 // exists. However, if we have returned at least one entry or subordinate 1336 // reference it implies the base does exist, so we can omit the check. 1337 if (searchOperation.getEntriesSent() == 0 1338 && searchOperation.getReferencesSent() == 0) 1339 { 1340 final Entry baseEntry = fetchBaseEntry(txn, aBaseDN, searchScope); 1341 if (!manageDsaIT) 1342 { 1343 dn2uri.checkTargetForReferral(baseEntry, searchScope); 1344 } 1345 } 1346 1347 // Indicate no more pages. 1348 addPagedResultsControl(searchOperation, pageRequest, null); 1349 } 1350 1351 private int findStartIndex(Long beginEntryID, long[] entryIDReorderedSet) 1352 { 1353 if (beginEntryID == null) 1354 { 1355 return 0; 1356 } 1357 final long begin = beginEntryID.longValue(); 1358 for (int i = 0; i < entryIDReorderedSet.length; i++) 1359 { 1360 if (entryIDReorderedSet[i] == begin) 1361 { 1362 return i; 1363 } 1364 } 1365 return 0; 1366 } 1367 1368 private boolean isInScope(boolean candidatesAreInScope, SearchScope searchScope, DN aBaseDN, Entry entry) 1369 { 1370 DN entryDN = entry.getName(); 1371 1372 if (candidatesAreInScope) 1373 { 1374 return true; 1375 } 1376 else if (searchScope == SearchScope.SINGLE_LEVEL) 1377 { 1378 // Check if this entry is an immediate child. 1379 if (entryDN.size() == aBaseDN.size() + 1 1380 && entryDN.isSubordinateOrEqualTo(aBaseDN)) 1381 { 1382 return true; 1383 } 1384 } 1385 else if (searchScope == SearchScope.WHOLE_SUBTREE) 1386 { 1387 if (entryDN.isSubordinateOrEqualTo(aBaseDN)) 1388 { 1389 return true; 1390 } 1391 } 1392 else if (searchScope == SearchScope.SUBORDINATES 1393 && entryDN.size() > aBaseDN.size() 1394 && entryDN.isSubordinateOrEqualTo(aBaseDN)) 1395 { 1396 return true; 1397 } 1398 return false; 1399 } 1400 1401 /** 1402 * Adds the provided entry to this tree. This method must ensure that the 1403 * entry is appropriate for the tree and that no entry already exists with 1404 * the same DN. The caller must hold a write lock on the DN of the provided 1405 * entry. 1406 * 1407 * @param entry The entry to add to this tree. 1408 * @param addOperation The add operation with which the new entry is 1409 * associated. This may be <CODE>null</CODE> for adds 1410 * performed internally. 1411 * @throws DirectoryException If a problem occurs while trying to add the 1412 * entry. 1413 * @throws StorageRuntimeException If an error occurs in the storage. 1414 * @throws CanceledOperationException if this operation should be cancelled. 1415 */ 1416 void addEntry(final Entry entry, final AddOperation addOperation) 1417 throws StorageRuntimeException, DirectoryException, CanceledOperationException 1418 { 1419 final DN parentDN = getParentWithinBase(entry.getName()); 1420 final EntryID entryID = rootContainer.getNextEntryID(); 1421 1422 // Insert into the indexes, in index configuration order. 1423 final IndexBuffer indexBuffer = new IndexBuffer(); 1424 insertEntryIntoIndexes(indexBuffer, entry, entryID); 1425 1426 final ByteString encodedEntry = id2entry.encode(entry); 1427 1428 try 1429 { 1430 storage.write(new WriteOperation() 1431 { 1432 @Override 1433 public void run(WriteableTransaction txn) throws Exception 1434 { 1435 // No need to call indexBuffer.reset() since IndexBuffer content will be the same for each retry attempt. 1436 try 1437 { 1438 // Check whether the entry already exists. 1439 if (dn2id.get(txn, entry.getName()) != null) 1440 { 1441 throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, 1442 ERR_ADD_ENTRY_ALREADY_EXISTS.get(entry.getName())); 1443 } 1444 // Check that the parent entry exists. 1445 EntryID parentID = null; 1446 if (parentDN != null) 1447 { 1448 // Check for referral entries above the target. 1449 dn2uri.targetEntryReferrals(txn, entry.getName(), null); 1450 1451 parentID = dn2id.get(txn, parentDN); 1452 if (parentID == null) 1453 { 1454 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 1455 ERR_ADD_NO_SUCH_OBJECT.get(entry.getName()), 1456 getMatchedDN(txn, parentDN), 1457 null); 1458 } 1459 } 1460 1461 // Ensure same access ordering as deleteEntry. 1462 dn2id.put(txn, entry.getName(), entryID); 1463 id2childrenCount.updateCount(txn, parentID, 1); 1464 id2entry.put(txn, entryID, encodedEntry); 1465 dn2uri.addEntry(txn, entry); 1466 id2childrenCount.updateTotalCount(txn, 1); 1467 indexBuffer.flush(txn); 1468 // One last check before committing 1469 addOperation.checkIfCanceled(true); 1470 } 1471 catch (StorageRuntimeException | DirectoryException | CanceledOperationException e) 1472 { 1473 throw e; 1474 } 1475 catch (Exception e) 1476 { 1477 String msg = e.getMessage(); 1478 if (msg == null) 1479 { 1480 msg = stackTraceToSingleLineString(e); 1481 } 1482 throw new DirectoryException( 1483 DirectoryServer.getServerErrorResultCode(), ERR_UNCHECKED_EXCEPTION.get(msg), e); 1484 } 1485 } 1486 }); 1487 } 1488 catch (Exception e) 1489 { 1490 writeTrustState(indexBuffer); 1491 throwAllowedExceptionTypes(e, DirectoryException.class, CanceledOperationException.class); 1492 } 1493 1494 final EntryCache<?> entryCache = DirectoryServer.getEntryCache(); 1495 if (entryCache != null) 1496 { 1497 entryCache.putEntry(entry, backendID, entryID.longValue()); 1498 } 1499 } 1500 1501 private void writeTrustState(final IndexBuffer indexBuffer) 1502 { 1503 // Transaction modifying the index has been rolled back. 1504 // Ensure that the index trusted state is persisted. 1505 try 1506 { 1507 storage.write(new WriteOperation() 1508 { 1509 @Override 1510 public void run(WriteableTransaction txn) throws Exception 1511 { 1512 indexBuffer.writeTrustState(txn); 1513 } 1514 }); 1515 } 1516 catch (Exception e) 1517 { 1518 // Cannot throw because this method is used in a catch block and we do not want to hide the real exception. 1519 logger.traceException(e); 1520 } 1521 } 1522 1523 void importEntry(WriteableTransaction txn, EntryID entryID, Entry entry) throws DirectoryException, 1524 StorageRuntimeException 1525 { 1526 final IndexBuffer indexBuffer = IndexBuffer.newImportIndexBuffer(txn, entryID); 1527 insertEntryIntoIndexes(indexBuffer, entry, entryID); 1528 dn2id.put(txn, entry.getName(), entryID); 1529 id2entry.put(txn, entryID, id2entry.encode(entry)); 1530 dn2uri.addEntry(txn, entry); 1531 indexBuffer.flush(txn); 1532 } 1533 1534 /** 1535 * Removes the specified entry from this tree. This method must ensure 1536 * that the entry exists and that it does not have any subordinate entries 1537 * (unless the storage supports a subtree delete operation and the client 1538 * included the appropriate information in the request). The caller must hold 1539 * a write lock on the provided entry DN. 1540 * 1541 * @param entryDN The DN of the entry to remove from this tree. 1542 * @param deleteOperation The delete operation with which this action is 1543 * associated. This may be <CODE>null</CODE> for 1544 * deletes performed internally. 1545 * @throws DirectoryException If a problem occurs while trying to remove the 1546 * entry. 1547 * @throws StorageRuntimeException If an error occurs in the storage. 1548 * @throws CanceledOperationException if this operation should be cancelled. 1549 */ 1550 void deleteEntry(final DN entryDN, final DeleteOperation deleteOperation) 1551 throws DirectoryException, StorageRuntimeException, CanceledOperationException 1552 { 1553 final IndexBuffer indexBuffer = new IndexBuffer(); 1554 try 1555 { 1556 storage.write(new WriteOperation() 1557 { 1558 @Override 1559 public void run(WriteableTransaction txn) throws Exception 1560 { 1561 indexBuffer.reset(); 1562 try 1563 { 1564 // Check for referral entries above the target entry. 1565 dn2uri.targetEntryReferrals(txn, entryDN, null); 1566 1567 // We'll need the parent ID when we update the id2childrenCount. Fetch it now so that accesses to dn2id 1568 // are ordered. 1569 final DN parentDN = getParentWithinBase(entryDN); 1570 EntryID parentID = null; 1571 if (parentDN != null) 1572 { 1573 parentID = dn2id.get(txn, parentDN); 1574 if (parentID == null) 1575 { 1576 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 1577 ERR_DELETE_NO_SUCH_OBJECT.get(entryDN), 1578 getMatchedDN(txn, parentDN), 1579 null); 1580 } 1581 } 1582 1583 // Delete the subordinate entries in dn2id if requested. 1584 final boolean isSubtreeDelete = deleteOperation.getRequestControl(SubtreeDeleteControl.DECODER) != null; 1585 1586 /* draft-armijo-ldap-treedelete, 4.1 Tree Delete Semantics: The server MUST NOT chase referrals stored in 1587 * the tree. If information about referrals is stored in this section of the tree, this pointer will be 1588 * deleted. 1589 */ 1590 final boolean isManageDsaIT = isSubtreeDelete || isManageDsaITOperation(deleteOperation); 1591 1592 /* Ensure that all index updates are done in the correct order to avoid deadlocks. First iterate over 1593 * dn2id collecting all the IDs of the entries to be deleted. Then update dn2uri, id2entry, 1594 * id2childrenCount, and finally the attribute indexes. 1595 */ 1596 final List<Long> entriesToBeDeleted = new ArrayList<>(); 1597 try (final SequentialCursor<Void, EntryID> cursor = dn2id.openSubordinatesCursor(txn, entryDN)) 1598 { 1599 // Delete the target entry in dn2id. 1600 if (!cursor.isDefined()) 1601 { 1602 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 1603 ERR_DELETE_NO_SUCH_OBJECT.get(entryDN), 1604 getMatchedDN(txn, entryDN), 1605 null); 1606 } 1607 entriesToBeDeleted.add(cursor.getValue().longValue()); 1608 cursor.delete(); 1609 1610 // Now delete the subordinate entries in dn2id. 1611 while (cursor.next()) 1612 { 1613 if (!isSubtreeDelete) 1614 { 1615 throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF, 1616 ERR_DELETE_NOT_ALLOWED_ON_NONLEAF.get(entryDN)); 1617 } 1618 entriesToBeDeleted.add(cursor.getValue().longValue()); 1619 cursor.delete(); 1620 deleteOperation.checkIfCanceled(false); 1621 } 1622 } 1623 // The target entry will have the lowest entryID so it will remain the first element. 1624 Collections.sort(entriesToBeDeleted); 1625 1626 // Now update id2entry, dn2uri, and id2childrenCount in key order. 1627 id2childrenCount.updateCount(txn, parentID, -1); 1628 final EntryCache<?> entryCache = DirectoryServer.getEntryCache(); 1629 boolean isBaseEntry = true; 1630 try (final Cursor<EntryID, Entry> cursor = id2entry.openCursor(txn)) 1631 { 1632 for (Long entryIDLong : entriesToBeDeleted) 1633 { 1634 final EntryID entryID = new EntryID(entryIDLong); 1635 if (!cursor.positionToKey(entryID.toByteString())) 1636 { 1637 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1638 ERR_MISSING_ID2ENTRY_RECORD.get(entryID)); 1639 } 1640 final Entry entry = cursor.getValue(); 1641 if (isBaseEntry && !isManageDsaIT) 1642 { 1643 dn2uri.checkTargetForReferral(entry, null); 1644 } 1645 cursor.delete(); 1646 dn2uri.deleteEntry(txn, entry); 1647 id2childrenCount.removeCount(txn, entryID); 1648 removeEntryFromIndexes(indexBuffer, entry, entryID); 1649 if (!isBaseEntry) 1650 { 1651 invokeSubordinateDeletePlugins(entry); 1652 } 1653 if (entryCache != null) 1654 { 1655 entryCache.removeEntry(entry.getName()); 1656 } 1657 isBaseEntry = false; 1658 deleteOperation.checkIfCanceled(false); 1659 } 1660 } 1661 id2childrenCount.updateTotalCount(txn, -entriesToBeDeleted.size()); 1662 indexBuffer.flush(txn); 1663 deleteOperation.checkIfCanceled(true); 1664 if (isSubtreeDelete) 1665 { 1666 deleteOperation.addAdditionalLogItem(unquotedKeyValue(getClass(), "deletedEntries", 1667 entriesToBeDeleted.size())); 1668 } 1669 } 1670 catch (StorageRuntimeException | DirectoryException | CanceledOperationException e) 1671 { 1672 throw e; 1673 } 1674 catch (Exception e) 1675 { 1676 String msg = e.getMessage(); 1677 if (msg == null) 1678 { 1679 msg = stackTraceToSingleLineString(e); 1680 } 1681 throw new DirectoryException( 1682 DirectoryServer.getServerErrorResultCode(), ERR_UNCHECKED_EXCEPTION.get(msg), e); 1683 } 1684 } 1685 1686 private void invokeSubordinateDeletePlugins(final Entry entry) throws DirectoryException 1687 { 1688 if (!deleteOperation.isSynchronizationOperation()) 1689 { 1690 SubordinateDelete pluginResult = 1691 getPluginConfigManager().invokeSubordinateDeletePlugins(deleteOperation, entry); 1692 if (!pluginResult.continueProcessing()) 1693 { 1694 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 1695 ERR_DELETE_ABORTED_BY_SUBORDINATE_PLUGIN.get(entry.getName())); 1696 } 1697 } 1698 } 1699 }); 1700 } 1701 catch (Exception e) 1702 { 1703 writeTrustState(indexBuffer); 1704 throwAllowedExceptionTypes(e, DirectoryException.class, CanceledOperationException.class); 1705 } 1706 } 1707 1708 /** 1709 * Indicates whether an entry with the specified DN exists. 1710 * 1711 * @param entryDN The DN of the entry for which to determine existence. 1712 * 1713 * @return <CODE>true</CODE> if the specified entry exists, 1714 * or <CODE>false</CODE> if it does not. 1715 */ 1716 private boolean entryExists(ReadableTransaction txn, final DN entryDN) 1717 { 1718 // Try the entry cache first. 1719 EntryCache<?> entryCache = DirectoryServer.getEntryCache(); 1720 return (entryCache != null && entryCache.containsEntry(entryDN)) 1721 || dn2id.get(txn, entryDN) != null; 1722 } 1723 1724 1725 boolean entryExists(final DN entryDN) throws StorageRuntimeException 1726 { 1727 final EntryCache<?> entryCache = DirectoryServer.getEntryCache(); 1728 if (entryCache != null && entryCache.containsEntry(entryDN)) 1729 { 1730 return true; 1731 } 1732 1733 try 1734 { 1735 return storage.read(new ReadOperation<Boolean>() 1736 { 1737 @Override 1738 public Boolean run(ReadableTransaction txn) throws Exception 1739 { 1740 return dn2id.get(txn, entryDN) != null; 1741 } 1742 }); 1743 } 1744 catch (Exception e) 1745 { 1746 throw new StorageRuntimeException(e); 1747 } 1748 } 1749 1750 /** 1751 * Fetch an entry by DN, trying the entry cache first, then the tree. 1752 * Retrieves the requested entry, trying the entry cache first, 1753 * then the tree. 1754 * 1755 * @param entryDN The distinguished name of the entry to retrieve. 1756 * @return The requested entry, or <CODE>null</CODE> if the entry does not 1757 * exist. 1758 * @throws DirectoryException If a problem occurs while trying to retrieve 1759 * the entry. 1760 * @throws StorageRuntimeException An error occurred during a storage operation. 1761 */ 1762 Entry getEntry(final DN entryDN) throws StorageRuntimeException, DirectoryException 1763 { 1764 try 1765 { 1766 return storage.read(new ReadOperation<Entry>() 1767 { 1768 @Override 1769 public Entry run(ReadableTransaction txn) throws Exception 1770 { 1771 Entry entry = getEntry0(txn, entryDN); 1772 if (entry == null) 1773 { 1774 // The entryDN does not exist. Check for referral entries above the target entry. 1775 dn2uri.targetEntryReferrals(txn, entryDN, null); 1776 } 1777 return entry; 1778 } 1779 }); 1780 } 1781 catch (Exception e) 1782 { 1783 // it is not very clean to specify twice the same exception but it saves me some code for now 1784 throwAllowedExceptionTypes(e, DirectoryException.class, DirectoryException.class); 1785 return null; // it can never happen 1786 } 1787 } 1788 1789 private Entry getEntry0(ReadableTransaction txn, final DN entryDN) throws StorageRuntimeException, DirectoryException 1790 { 1791 final EntryCache<?> entryCache = DirectoryServer.getEntryCache(); 1792 if (entryCache != null) 1793 { 1794 final Entry entry = entryCache.getEntry(entryDN); 1795 if (entry != null) 1796 { 1797 return entry; 1798 } 1799 } 1800 1801 final EntryID entryID = dn2id.get(txn, entryDN); 1802 if (entryID == null) 1803 { 1804 return null; 1805 } 1806 1807 final Entry entry = id2entry.get(txn, entryID); 1808 if (entry != null && entryCache != null) 1809 { 1810 /* 1811 * Put the entry in the cache making sure not to overwrite a newer copy that may have been 1812 * inserted since the time we read the cache. 1813 */ 1814 entryCache.putEntryIfAbsent(entry, backendID, entryID.longValue()); 1815 } 1816 return entry; 1817 } 1818 1819 /** 1820 * The simplest case of replacing an entry in which the entry DN has 1821 * not changed. 1822 * 1823 * @param oldEntry The old contents of the entry 1824 * @param newEntry The new contents of the entry 1825 * @param modifyOperation The modify operation with which this action is 1826 * associated. This may be <CODE>null</CODE> for 1827 * modifications performed internally. 1828 * @throws StorageRuntimeException If an error occurs in the storage. 1829 * @throws DirectoryException If a Directory Server error occurs. 1830 * @throws CanceledOperationException if this operation should be cancelled. 1831 */ 1832 void replaceEntry(final Entry oldEntry, final Entry newEntry, final ModifyOperation modifyOperation) 1833 throws StorageRuntimeException, DirectoryException, CanceledOperationException 1834 { 1835 final IndexBuffer indexBuffer = new IndexBuffer(); 1836 final ByteString encodedNewEntry = id2entry.encode(newEntry); 1837 try 1838 { 1839 storage.write(new WriteOperation() 1840 { 1841 @Override 1842 public void run(WriteableTransaction txn) throws Exception 1843 { 1844 indexBuffer.reset(); 1845 try 1846 { 1847 EntryID entryID = dn2id.get(txn, newEntry.getName()); 1848 if (entryID == null) 1849 { 1850 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 1851 ERR_MODIFY_NO_SUCH_OBJECT.get(newEntry.getName()), 1852 getMatchedDN(txn, newEntry.getName()), 1853 null); 1854 } 1855 1856 if (!isManageDsaITOperation(modifyOperation)) 1857 { 1858 // Check if the entry is a referral entry. 1859 dn2uri.checkTargetForReferral(oldEntry, null); 1860 } 1861 1862 // Ensure same ordering as deleteEntry: id2entry, dn2uri, then indexes. 1863 id2entry.put(txn, entryID, encodedNewEntry); 1864 1865 // Update the referral tree and indexes 1866 dn2uri.modifyEntry(txn, oldEntry, newEntry, modifyOperation.getModifications()); 1867 indexModifications(indexBuffer, oldEntry, newEntry, entryID, modifyOperation.getModifications()); 1868 1869 indexBuffer.flush(txn); 1870 1871 // One last check before committing 1872 modifyOperation.checkIfCanceled(true); 1873 1874 // Update the entry cache. 1875 EntryCache<?> entryCache = DirectoryServer.getEntryCache(); 1876 if (entryCache != null) 1877 { 1878 entryCache.putEntry(newEntry, backendID, entryID.longValue()); 1879 } 1880 } 1881 catch (StorageRuntimeException | DirectoryException | CanceledOperationException e) 1882 { 1883 throw e; 1884 } 1885 catch (Exception e) 1886 { 1887 String msg = e.getMessage(); 1888 if (msg == null) 1889 { 1890 msg = stackTraceToSingleLineString(e); 1891 } 1892 throw new DirectoryException( 1893 DirectoryServer.getServerErrorResultCode(), ERR_UNCHECKED_EXCEPTION.get(msg), e); 1894 } 1895 } 1896 }); 1897 } 1898 catch (Exception e) 1899 { 1900 writeTrustState(indexBuffer); 1901 throwAllowedExceptionTypes(e, DirectoryException.class, CanceledOperationException.class); 1902 } 1903 } 1904 1905 /** 1906 * Moves and/or renames the provided entry in this backend, altering any 1907 * subordinate entries as necessary. This must ensure that an entry already 1908 * exists with the provided current DN, and that no entry exists with the 1909 * target DN of the provided entry. The caller must hold write locks on both 1910 * the current DN and the new DN for the entry. 1911 * 1912 * @param oldTargetDN The current DN of the entry to be renamed. 1913 * @param newTargetEntry The new content to use for the entry. 1914 * @param modifyDNOperation The modify DN operation with which this action 1915 * is associated. This may be <CODE>null</CODE> 1916 * for modify DN operations performed internally. 1917 * @throws DirectoryException 1918 * If a problem occurs while trying to perform the rename. 1919 * @throws CanceledOperationException 1920 * If this backend noticed and reacted 1921 * to a request to cancel or abandon the 1922 * modify DN operation. 1923 * @throws StorageRuntimeException If an error occurs in the storage. 1924 */ 1925 void renameEntry(final DN oldTargetDN, final Entry newTargetEntry, final ModifyDNOperation modifyDNOperation) 1926 throws StorageRuntimeException, DirectoryException, CanceledOperationException 1927 { 1928 final IndexBuffer indexBuffer = new IndexBuffer(); 1929 try 1930 { 1931 storage.write(new WriteOperation() 1932 { 1933 @Override 1934 public void run(WriteableTransaction txn) throws Exception 1935 { 1936 indexBuffer.reset(); 1937 try 1938 { 1939 // Validate the request. 1940 final DN newTargetDN = newTargetEntry.getName(); 1941 final DN oldSuperiorDN = getParentWithinBase(oldTargetDN); 1942 final DN newSuperiorDN = getParentWithinBase(newTargetDN); 1943 1944 final EntryID oldSuperiorID = oldSuperiorDN != null ? dn2id.get(txn, oldSuperiorDN) : null; 1945 final EntryID oldTargetID = dn2id.get(txn, oldTargetDN); 1946 if ((oldSuperiorDN != null && oldSuperiorID == null) || oldTargetID == null) 1947 { 1948 // Check for referral entries above the target entry. 1949 dn2uri.targetEntryReferrals(txn, oldTargetDN, null); 1950 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 1951 ERR_MODIFYDN_NO_SUCH_OBJECT.get(oldTargetDN), 1952 getMatchedDN(txn, oldTargetDN), 1953 null); 1954 } 1955 1956 final EntryID newSuperiorID = newSuperiorDN != null ? dn2id.get(txn, newSuperiorDN) : null; 1957 if (newSuperiorDN != null && newSuperiorID == null) 1958 { 1959 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 1960 ERR_NEW_SUPERIOR_NO_SUCH_OBJECT.get(newSuperiorDN), 1961 getMatchedDN(txn, newSuperiorDN), 1962 null); 1963 } 1964 1965 // Check that an entry with the new name does not already exist, but take care to handle the case where 1966 // the user is renaming the entry with an equivalent name, e.g. "cn=matt" to "cn=Matt". 1967 if (!oldTargetDN.equals(newTargetDN) && dn2id.get(txn, newTargetDN) != null) 1968 { 1969 throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, 1970 ERR_MODIFYDN_ALREADY_EXISTS.get(newTargetDN)); 1971 } 1972 1973 /* We want to preserve the invariant that the ID of an entry is greater than its parent, since search 1974 * results are returned in ID order. Note: if the superior has changed then oldSuperiorDN and 1975 * newSuperiorDN will be non-null. 1976 */ 1977 final boolean superiorHasChanged = !Objects.equals(oldSuperiorDN, newSuperiorDN); 1978 final boolean renumberEntryIDs = superiorHasChanged && newSuperiorID.compareTo(oldSuperiorID) > 0; 1979 1980 /* Ensure that all index updates are done in the correct order to avoid deadlocks. First iterate over 1981 * dn2id collecting all the IDs of the entries to be renamed. Then update dn2uri, id2entry, 1982 * id2childrenCount, and finally the attribute indexes. 1983 */ 1984 final List<Pair<Long, Long>> renamedEntryIDs = dn2id.renameSubtree(txn, 1985 oldTargetDN, 1986 newTargetDN, 1987 rootContainer, 1988 renumberEntryIDs, 1989 modifyDNOperation); 1990 1991 // The target entry will have the lowest entryID so it will remain the first element. 1992 Collections.sort(renamedEntryIDs, Pair.<Long, Long>getPairComparator()); 1993 1994 // Now update id2entry, dn2uri, and id2childrenCount in key order. 1995 if (superiorHasChanged) 1996 { 1997 id2childrenCount.updateCount(txn, oldSuperiorID, -1); 1998 id2childrenCount.updateCount(txn, newSuperiorID, 1); 1999 } 2000 boolean isBaseEntry = true; 2001 try (final Cursor<EntryID, Entry> cursor = id2entry.openCursor(txn)) 2002 { 2003 for (Pair<Long, Long> renamedEntryID : renamedEntryIDs) 2004 { 2005 renameSingleEntry(txn, renamedEntryID, cursor, indexBuffer, newTargetDN, renumberEntryIDs, isBaseEntry); 2006 isBaseEntry = false; 2007 modifyDNOperation.checkIfCanceled(false); 2008 } 2009 2010 } 2011 indexBuffer.flush(txn); 2012 modifyDNOperation.checkIfCanceled(true); 2013 } 2014 catch (StorageRuntimeException | DirectoryException | CanceledOperationException e) 2015 { 2016 throw e; 2017 } 2018 catch (Exception e) 2019 { 2020 String msg = e.getMessage(); 2021 if (msg == null) 2022 { 2023 msg = stackTraceToSingleLineString(e); 2024 } 2025 throw new DirectoryException( 2026 DirectoryServer.getServerErrorResultCode(), ERR_UNCHECKED_EXCEPTION.get(msg), e); 2027 } 2028 } 2029 2030 private void renameSingleEntry( 2031 final WriteableTransaction txn, 2032 final Pair<Long, Long> renamedEntryID, 2033 final Cursor<EntryID, Entry> cursor, 2034 final IndexBuffer indexBuffer, 2035 final DN newTargetDN, 2036 final boolean renumberEntryIDs, 2037 final boolean isBaseEntry) throws DirectoryException 2038 { 2039 final EntryID oldEntryID = new EntryID(renamedEntryID.getFirst()); 2040 final EntryID newEntryID = new EntryID(renamedEntryID.getSecond()); 2041 if (!cursor.positionToKey(oldEntryID.toByteString())) 2042 { 2043 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 2044 ERR_MISSING_ID2ENTRY_RECORD.get(oldEntryID)); 2045 } 2046 2047 final Entry oldEntry = cursor.getValue(); 2048 final Entry newEntry; 2049 final List<Modification> modifications; 2050 if (isBaseEntry) 2051 { 2052 if (!isManageDsaITOperation(modifyDNOperation)) 2053 { 2054 dn2uri.checkTargetForReferral(oldEntry, null); 2055 } 2056 newEntry = newTargetEntry; 2057 modifications = modifyDNOperation.getModifications(); 2058 } 2059 else 2060 { 2061 final DN newDN = oldEntry.getName().rename(oldTargetDN, newTargetDN); 2062 newEntry = oldEntry.duplicate(false); 2063 newEntry.setDN(newDN); 2064 modifications = invokeSubordinateModifyDNPlugins(oldEntry, newEntry); 2065 } 2066 2067 if (renumberEntryIDs) 2068 { 2069 cursor.delete(); 2070 } 2071 id2entry.put(txn, newEntryID, newEntry); 2072 dn2uri.deleteEntry(txn, oldEntry); 2073 dn2uri.addEntry(txn, newEntry); 2074 if (renumberEntryIDs) 2075 { 2076 // In-order: new entryID is guaranteed to be greater than old entryID. 2077 final long count = id2childrenCount.removeCount(txn, oldEntryID); 2078 id2childrenCount.updateCount(txn, newEntryID, count); 2079 } 2080 2081 if (renumberEntryIDs || modifications == null) 2082 { 2083 // Slow path: the entry has been renumbered so we need to fully re-index. 2084 removeEntryFromIndexes(indexBuffer, oldEntry, oldEntryID); 2085 insertEntryIntoIndexes(indexBuffer, newEntry, newEntryID); 2086 } 2087 else if (!modifications.isEmpty()) 2088 { 2089 // Fast-path: the entryID has not changed so we only need to re-index the mods. 2090 indexModifications(indexBuffer, oldEntry, newEntry, oldEntryID, modifications); 2091 } 2092 2093 final EntryCache<?> entryCache = DirectoryServer.getEntryCache(); 2094 if (entryCache != null) 2095 { 2096 entryCache.removeEntry(oldEntry.getName()); 2097 } 2098 } 2099 2100 private List<Modification> invokeSubordinateModifyDNPlugins( 2101 final Entry oldEntry, final Entry newEntry) throws DirectoryException 2102 { 2103 final List<Modification> modifications = Collections.unmodifiableList(new ArrayList<Modification>(0)); 2104 2105 // Create a new entry that is a copy of the old entry but with the new DN. 2106 // Also invoke any subordinate modify DN plugins on the entry. 2107 // FIXME -- At the present time, we don't support subordinate modify DN 2108 // plugins that make changes to subordinate entries and therefore 2109 // provide an unmodifiable list for the modifications element. 2110 // FIXME -- This will need to be updated appropriately if we decided that 2111 // these plugins should be invoked for synchronization operations. 2112 if (!modifyDNOperation.isSynchronizationOperation()) 2113 { 2114 SubordinateModifyDN pluginResult = getPluginConfigManager().invokeSubordinateModifyDNPlugins( 2115 modifyDNOperation, oldEntry, newEntry, modifications); 2116 2117 if (!pluginResult.continueProcessing()) 2118 { 2119 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 2120 ERR_MODIFYDN_ABORTED_BY_SUBORDINATE_PLUGIN.get(oldEntry.getName(), 2121 newEntry.getName())); 2122 } 2123 2124 if (!modifications.isEmpty()) 2125 { 2126 LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder(); 2127 if (!newEntry.conformsToSchema(null, false, false, false, invalidReason)) 2128 { 2129 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), 2130 ERR_MODIFYDN_ABORTED_BY_SUBORDINATE_SCHEMA_ERROR.get(oldEntry.getName(), 2131 newEntry.getName(), 2132 invalidReason)); 2133 } 2134 } 2135 } 2136 return modifications; 2137 } 2138 }); 2139 } 2140 catch (Exception e) 2141 { 2142 writeTrustState(indexBuffer); 2143 throwAllowedExceptionTypes(e, DirectoryException.class, CanceledOperationException.class); 2144 } 2145 } 2146 2147 /** 2148 * Insert a new entry into the attribute indexes. 2149 * 2150 * @param buffer The index buffer used to buffer up the index changes. 2151 * @param entry The entry to be inserted into the indexes. 2152 * @param entryID The ID of the entry to be inserted into the indexes. 2153 * @throws StorageRuntimeException If an error occurs in the storage. 2154 * @throws DirectoryException If a Directory Server error occurs. 2155 */ 2156 private void insertEntryIntoIndexes(IndexBuffer buffer, Entry entry, EntryID entryID) 2157 throws StorageRuntimeException, DirectoryException 2158 { 2159 for (AttributeIndex index : attrIndexMap.values()) 2160 { 2161 index.addEntry(buffer, entryID, entry); 2162 } 2163 2164 for (VLVIndex vlvIndex : vlvIndexMap.values()) 2165 { 2166 vlvIndex.addEntry(buffer, entryID, entry); 2167 } 2168 } 2169 2170 /** 2171 * Remove an entry from the attribute indexes. 2172 * 2173 * @param buffer The index buffer used to buffer up the index changes. 2174 * @param entry The entry to be removed from the indexes. 2175 * @param entryID The ID of the entry to be removed from the indexes. 2176 * @throws StorageRuntimeException If an error occurs in the storage. 2177 * @throws DirectoryException If a Directory Server error occurs. 2178 */ 2179 private void removeEntryFromIndexes(IndexBuffer buffer, Entry entry, EntryID entryID) 2180 throws StorageRuntimeException, DirectoryException 2181 { 2182 for (AttributeIndex index : attrIndexMap.values()) 2183 { 2184 index.removeEntry(buffer, entryID, entry); 2185 } 2186 2187 for (VLVIndex vlvIndex : vlvIndexMap.values()) 2188 { 2189 vlvIndex.removeEntry(buffer, entryID, entry); 2190 } 2191 } 2192 2193 /** 2194 * Update the attribute indexes to reflect the changes to the 2195 * attributes of an entry resulting from a sequence of modifications. 2196 * 2197 * @param buffer The index buffer used to buffer up the index changes. 2198 * @param oldEntry The contents of the entry before the change. 2199 * @param newEntry The contents of the entry after the change. 2200 * @param entryID The ID of the entry that was changed. 2201 * @param mods The sequence of modifications made to the entry. 2202 * @throws StorageRuntimeException If an error occurs in the storage. 2203 * @throws DirectoryException If a Directory Server error occurs. 2204 */ 2205 private void indexModifications(IndexBuffer buffer, Entry oldEntry, Entry newEntry, 2206 EntryID entryID, List<Modification> mods) 2207 throws StorageRuntimeException, DirectoryException 2208 { 2209 // Process in index configuration order. 2210 for (AttributeIndex index : attrIndexMap.values()) 2211 { 2212 // Check whether any modifications apply to this indexed attribute. 2213 if (isAttributeModified(index, mods)) 2214 { 2215 index.modifyEntry(buffer, entryID, oldEntry, newEntry); 2216 } 2217 } 2218 2219 for(VLVIndex vlvIndex : vlvIndexMap.values()) 2220 { 2221 vlvIndex.modifyEntry(buffer, entryID, oldEntry, newEntry, mods); 2222 } 2223 } 2224 2225 /** 2226 * Get a count of the number of entries stored in this entry container including the baseDN 2227 * 2228 * @return The number of entries stored in this entry container including the baseDN. 2229 * @throws StorageRuntimeException 2230 * If an error occurs in the storage. 2231 */ 2232 long getNumberOfEntriesInBaseDN() throws StorageRuntimeException 2233 { 2234 try 2235 { 2236 return storage.read(new ReadOperation<Long>() 2237 { 2238 @Override 2239 public Long run(ReadableTransaction txn) throws Exception 2240 { 2241 return getNumberOfEntriesInBaseDN0(txn); 2242 } 2243 }); 2244 } 2245 catch (Exception e) 2246 { 2247 throw new StorageRuntimeException(e); 2248 } 2249 } 2250 2251 long getNumberOfEntriesInBaseDN0(ReadableTransaction txn) 2252 { 2253 return id2childrenCount.getTotalCount(txn); 2254 } 2255 2256 /** 2257 * Determine whether the provided operation has the ManageDsaIT request control. 2258 * @param operation The operation for which the determination is to be made. 2259 * @return true if the operation has the ManageDsaIT request control, or false if not. 2260 */ 2261 private static boolean isManageDsaITOperation(Operation operation) 2262 { 2263 for (Control control : operation.getRequestControls()) 2264 { 2265 if (ServerConstants.OID_MANAGE_DSAIT_CONTROL.equals(control.getOID())) 2266 { 2267 return true; 2268 } 2269 } 2270 return false; 2271 } 2272 2273 /** 2274 * Delete this entry container from disk. The entry container should be 2275 * closed before calling this method. 2276 * 2277 * @param txn a non null transaction 2278 * @throws StorageRuntimeException If an error occurs while removing the entry container. 2279 */ 2280 void delete(WriteableTransaction txn) throws StorageRuntimeException 2281 { 2282 for (Tree tree : listTrees()) 2283 { 2284 tree.delete(txn); 2285 } 2286 } 2287 2288 /** 2289 * Remove a tree from disk. 2290 * 2291 * @param txn a non null transaction 2292 * @param tree The tree container to remove. 2293 * @throws StorageRuntimeException If an error occurs while attempting to delete the tree. 2294 */ 2295 void deleteTree(WriteableTransaction txn, Tree tree) throws StorageRuntimeException 2296 { 2297 if(tree == state) 2298 { 2299 // The state tree cannot be removed individually. 2300 return; 2301 } 2302 2303 tree.delete(txn); 2304 if(tree instanceof Index) 2305 { 2306 state.deleteRecord(txn, tree.getName()); 2307 } 2308 } 2309 2310 /** 2311 * This method constructs a container name from a base DN. Only alphanumeric 2312 * characters are preserved, all other characters are replaced with an 2313 * underscore. 2314 * 2315 * @return The container name for the base DN. 2316 */ 2317 String getTreePrefix() 2318 { 2319 return treePrefix; 2320 } 2321 2322 @Override 2323 public DN getBaseDN() 2324 { 2325 return baseDN; 2326 } 2327 2328 /** 2329 * Get the parent of a DN in the scope of the base DN. 2330 * 2331 * @param dn A DN which is in the scope of the base DN. 2332 * @return The parent DN, or null if the given DN is the base DN. 2333 */ 2334 DN getParentWithinBase(DN dn) 2335 { 2336 if (dn.equals(baseDN)) 2337 { 2338 return null; 2339 } 2340 return dn.parent(); 2341 } 2342 2343 @Override 2344 public boolean isConfigurationChangeAcceptable( 2345 PluggableBackendCfg cfg, List<LocalizableMessage> unacceptableReasons) 2346 { 2347 // This is always true because only all config attributes used 2348 // by the entry container should be validated by the admin framework. 2349 return true; 2350 } 2351 2352 @Override 2353 public ConfigChangeResult applyConfigurationChange(final PluggableBackendCfg cfg) 2354 { 2355 final ConfigChangeResult ccr = new ConfigChangeResult(); 2356 2357 exclusiveLock.lock(); 2358 try 2359 { 2360 storage.write(new WriteOperation() 2361 { 2362 @Override 2363 public void run(WriteableTransaction txn) throws Exception 2364 { 2365 DataConfig entryDataConfig = new DataConfig(cfg.isEntriesCompressed(), 2366 cfg.isCompactEncoding(), rootContainer.getCompressedSchema()); 2367 id2entry.setDataConfig(entryDataConfig); 2368 2369 EntryContainer.this.config = cfg; 2370 } 2371 }); 2372 } 2373 catch (Exception e) 2374 { 2375 ccr.setResultCode(DirectoryServer.getServerErrorResultCode()); 2376 ccr.addMessage(LocalizableMessage.raw(stackTraceToSingleLineString(e))); 2377 } 2378 finally 2379 { 2380 exclusiveLock.unlock(); 2381 } 2382 2383 return ccr; 2384 } 2385 2386 /** 2387 * Clear the contents of this entry container. 2388 * 2389 * @throws StorageRuntimeException If an error occurs while removing the entry 2390 * container. 2391 */ 2392 public void clear() throws StorageRuntimeException 2393 { 2394 try 2395 { 2396 storage.write(new WriteOperation() 2397 { 2398 @Override 2399 public void run(WriteableTransaction txn) throws Exception 2400 { 2401 for (Tree tree : listTrees()) 2402 { 2403 tree.delete(txn); 2404 } 2405 } 2406 }); 2407 } 2408 catch (Exception e) 2409 { 2410 throw new StorageRuntimeException(e); 2411 } 2412 } 2413 2414 List<Tree> listTrees() 2415 { 2416 final List<Tree> allTrees = new ArrayList<>(); 2417 allTrees.add(dn2id); 2418 allTrees.add(id2entry); 2419 allTrees.add(dn2uri); 2420 allTrees.add(id2childrenCount); 2421 allTrees.add(state); 2422 2423 for (AttributeIndex index : attrIndexMap.values()) 2424 { 2425 allTrees.addAll(index.getNameToIndexes().values()); 2426 } 2427 2428 allTrees.addAll(vlvIndexMap.values()); 2429 return allTrees; 2430 } 2431 2432 /** 2433 * Finds an existing entry whose DN is the closest ancestor of a given baseDN. 2434 * 2435 * @param targetDN the DN for which we are searching a matched DN. 2436 * @return the DN of the closest ancestor of the baseDN. 2437 * @throws DirectoryException If an error prevented the check of an 2438 * existing entry from being performed. 2439 */ 2440 private DN getMatchedDN(ReadableTransaction txn, DN targetDN) throws DirectoryException 2441 { 2442 DN parentDN = DirectoryServer.getParentDNInSuffix(targetDN); 2443 while (parentDN != null && parentDN.isSubordinateOrEqualTo(baseDN)) 2444 { 2445 if (entryExists(txn, parentDN)) 2446 { 2447 return parentDN; 2448 } 2449 parentDN = DirectoryServer.getParentDNInSuffix(parentDN); 2450 } 2451 return null; 2452 } 2453 2454 /** 2455 * Checks if any modifications apply to this indexed attribute. 2456 * @param index the indexed attributes. 2457 * @param mods the modifications to check for. 2458 * @return true if any apply, false otherwise. 2459 */ 2460 private static boolean isAttributeModified(AttributeIndex index, List<Modification> mods) 2461 { 2462 AttributeType indexAttributeType = index.getAttributeType(); 2463 List<AttributeType> subTypes = 2464 DirectoryServer.getSchema().getSubTypes(indexAttributeType); 2465 2466 for (Modification mod : mods) 2467 { 2468 Attribute modAttr = mod.getAttribute(); 2469 AttributeType modAttrType = modAttr.getAttributeDescription().getAttributeType(); 2470 if (modAttrType.equals(indexAttributeType) 2471 || subTypes.contains(modAttrType)) 2472 { 2473 return true; 2474 } 2475 } 2476 return false; 2477 } 2478 2479 /** 2480 * Fetch the base Entry of the EntryContainer. 2481 * @param searchBaseDN the DN for the base entry 2482 * @param searchScope the scope under which this is fetched. 2483 * Scope is used for referral processing. 2484 * @return the Entry matching the baseDN. 2485 * @throws DirectoryException if the baseDN doesn't exist. 2486 */ 2487 private Entry fetchBaseEntry(ReadableTransaction txn, DN searchBaseDN, SearchScope searchScope) 2488 throws DirectoryException 2489 { 2490 Entry baseEntry = getEntry0(txn, searchBaseDN); 2491 if (baseEntry == null) 2492 { 2493 // Check for referral entries above the base entry. 2494 dn2uri.targetEntryReferrals(txn, searchBaseDN, searchScope); 2495 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 2496 ERR_SEARCH_NO_SUCH_OBJECT.get(searchBaseDN), getMatchedDN(txn, searchBaseDN), null); 2497 } 2498 return baseEntry; 2499 } 2500 2501 private long[] sort(ReadableTransaction txn, EntryIDSet entryIDSet, SearchOperation searchOperation, 2502 SortOrder sortOrder, VLVRequestControl vlvRequest) throws DirectoryException 2503 { 2504 if (!entryIDSet.isDefined()) 2505 { 2506 return null; 2507 } 2508 2509 final DN baseDN = searchOperation.getBaseDN(); 2510 final SearchScope scope = searchOperation.getScope(); 2511 final SearchFilter filter = searchOperation.getFilter(); 2512 2513 final TreeMap<ByteString, EntryID> sortMap = new TreeMap<>(); 2514 for (EntryID id : entryIDSet) 2515 { 2516 try 2517 { 2518 Entry e = getEntry(txn, id); 2519 if (e.matchesBaseAndScope(baseDN, scope) && filter.matchesEntry(e)) 2520 { 2521 sortMap.put(encodeVLVKey(sortOrder, e, id.longValue()), id); 2522 } 2523 } 2524 catch (Exception e) 2525 { 2526 LocalizableMessage message = ERR_ENTRYIDSORTER_CANNOT_EXAMINE_ENTRY.get(id, getExceptionMessage(e)); 2527 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 2528 } 2529 } 2530 2531 // See if there is a VLV request to further pare down the set of results, and if there is where it should be 2532 // processed by offset or assertion value. 2533 if (vlvRequest == null) 2534 { 2535 return toArray(sortMap.values()); 2536 } 2537 2538 if (vlvRequest.getTargetType() == VLVRequestControl.TYPE_TARGET_BYOFFSET) 2539 { 2540 return sortByOffset(searchOperation, vlvRequest, sortMap); 2541 } 2542 return sortByGreaterThanOrEqualAssertion(searchOperation, vlvRequest, sortOrder, sortMap); 2543 } 2544 2545 private static final long[] toArray(Collection<EntryID> entryIDs) 2546 { 2547 final long[] array = new long[entryIDs.size()]; 2548 int i = 0; 2549 for (EntryID entryID : entryIDs) 2550 { 2551 array[i++] = entryID.longValue(); 2552 } 2553 return array; 2554 } 2555 2556 private static final long[] sortByGreaterThanOrEqualAssertion(SearchOperation searchOperation, 2557 VLVRequestControl vlvRequest, SortOrder sortOrder, final TreeMap<ByteString, EntryID> sortMap) 2558 throws DirectoryException 2559 { 2560 ByteString assertionValue = vlvRequest.getGreaterThanOrEqualAssertion(); 2561 ByteSequence encodedTargetAssertion = 2562 encodeTargetAssertion(sortOrder, assertionValue, searchOperation, sortMap.size()); 2563 2564 boolean targetFound = false; 2565 int index = 0; 2566 int targetIndex = 0; 2567 int startIndex = 0; 2568 int includedAfterCount = 0; 2569 long[] idSet = new long[sortMap.size()]; 2570 for (Map.Entry<ByteString, EntryID> entry : sortMap.entrySet()) 2571 { 2572 ByteString vlvKey = entry.getKey(); 2573 EntryID id = entry.getValue(); 2574 idSet[index++] = id.longValue(); 2575 2576 if (targetFound) 2577 { 2578 includedAfterCount++; 2579 if (includedAfterCount >= vlvRequest.getAfterCount()) 2580 { 2581 break; 2582 } 2583 } 2584 else 2585 { 2586 targetFound = vlvKey.compareTo(encodedTargetAssertion) >= 0; 2587 if (targetFound) 2588 { 2589 startIndex = Math.max(0, targetIndex - vlvRequest.getBeforeCount()); 2590 } 2591 targetIndex++; 2592 } 2593 } 2594 2595 final long[] result; 2596 if (targetFound) 2597 { 2598 final long[] array = new long[index - startIndex]; 2599 System.arraycopy(idSet, startIndex, array, 0, array.length); 2600 result = array; 2601 } 2602 else 2603 { 2604 /* 2605 * No entry was found to be greater than or equal to the sort key, so the target offset will 2606 * be one greater than the content count. 2607 */ 2608 targetIndex = sortMap.size() + 1; 2609 result = new long[0]; 2610 } 2611 addVLVResponseControl(searchOperation, targetIndex, sortMap.size(), SUCCESS); 2612 return result; 2613 } 2614 2615 private static final long[] sortByOffset(SearchOperation searchOperation, VLVRequestControl vlvRequest, 2616 TreeMap<ByteString, EntryID> sortMap) throws DirectoryException 2617 { 2618 int targetOffset = vlvRequest.getOffset(); 2619 if (targetOffset < 0) 2620 { 2621 // The client specified a negative target offset. This should never be allowed. 2622 addVLVResponseControl(searchOperation, targetOffset, sortMap.size(), OFFSET_RANGE_ERROR); 2623 2624 LocalizableMessage message = ERR_ENTRYIDSORTER_NEGATIVE_START_POS.get(); 2625 throw new DirectoryException(ResultCode.VIRTUAL_LIST_VIEW_ERROR, message); 2626 } 2627 2628 // This is an easy mistake to make, since VLV offsets start at 1 instead of 0. We'll assume the client meant 2629 // to use 1. 2630 targetOffset = (targetOffset == 0) ? 1 : targetOffset; 2631 2632 int beforeCount = vlvRequest.getBeforeCount(); 2633 int afterCount = vlvRequest.getAfterCount(); 2634 int listOffset = targetOffset - 1; // VLV offsets start at 1, not 0. 2635 int startPos = listOffset - beforeCount; 2636 if (startPos < 0) 2637 { 2638 // This can happen if beforeCount >= offset, and in this case we'll just adjust the start position to ignore 2639 // the range of beforeCount that doesn't exist. 2640 startPos = 0; 2641 beforeCount = listOffset; 2642 } 2643 else if (startPos >= sortMap.size()) 2644 { 2645 // The start position is beyond the end of the list. In this case, we'll assume that the start position was 2646 // one greater than the size of the list and will only return the beforeCount entries. 2647 targetOffset = sortMap.size() + 1; 2648 listOffset = sortMap.size(); 2649 startPos = listOffset - beforeCount; 2650 afterCount = 0; 2651 } 2652 2653 int count = 1 + beforeCount + afterCount; 2654 long[] sortedIDs = new long[count]; 2655 int treePos = 0; 2656 int arrayPos = 0; 2657 for (EntryID id : sortMap.values()) 2658 { 2659 if (treePos++ < startPos) 2660 { 2661 continue; 2662 } 2663 2664 sortedIDs[arrayPos++] = id.longValue(); 2665 if (arrayPos >= count) 2666 { 2667 break; 2668 } 2669 } 2670 2671 if (arrayPos < count) 2672 { 2673 // We don't have enough entries in the set to meet the requested page size, so we'll need to shorten the array. 2674 sortedIDs = Arrays.copyOf(sortedIDs, arrayPos); 2675 } 2676 2677 addVLVResponseControl(searchOperation, targetOffset, sortMap.size(), SUCCESS); 2678 return sortedIDs; 2679 } 2680 2681 private static void addVLVResponseControl(SearchOperation searchOp, int targetPosition, int contentCount, 2682 int vlvResultCode) 2683 { 2684 searchOp.addResponseControl(new VLVResponseControl(targetPosition, contentCount, vlvResultCode)); 2685 } 2686 2687 /** Get the exclusive lock. */ 2688 void lock() 2689 { 2690 exclusiveLock.lock(); 2691 } 2692 2693 /** Unlock the exclusive lock. */ 2694 void unlock() 2695 { 2696 exclusiveLock.unlock(); 2697 } 2698 2699 @Override 2700 public String toString() { 2701 return treePrefix; 2702 } 2703}