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