/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2026 3A Systems, LLC. */ package org.opends.server.backends.pluggable; import static org.assertj.core.api.Assertions.assertThat; import static org.forgerock.opendj.config.ConfigurationMock.mockCfg; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.opends.server.backends.pluggable.IndexFilter.CURSOR_ENTRY_LIMIT; import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection; import static org.opends.server.protocols.internal.Requests.newSearchRequest; import static org.opends.server.protocols.ldap.LDAPResultCode.SUCCESS; import static org.opends.server.util.CollectionUtils.newTreeSet; import static org.opends.server.util.ServerConstants.OID_SERVER_SIDE_SORT_RESPONSE_CONTROL; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.ldap.ByteSequence; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.SearchScope; import org.forgerock.opendj.server.config.meta.BackendIndexCfgDefn.IndexType; import org.forgerock.opendj.server.config.meta.BackendVLVIndexCfgDefn.Scope; import org.forgerock.opendj.server.config.server.BackendIndexCfg; import org.forgerock.opendj.server.config.server.BackendVLVIndexCfg; import org.forgerock.opendj.server.config.server.PDBBackendCfg; import org.opends.server.DirectoryServerTestCase; import org.opends.server.TestCaseUtils; import org.opends.server.api.ClientConnection; import org.opends.server.backends.pdb.PDBStorage; import org.opends.server.backends.pluggable.spi.AccessMode; import org.opends.server.backends.pluggable.spi.Cursor; import org.opends.server.backends.pluggable.spi.Importer; import org.opends.server.backends.pluggable.spi.ReadOperation; import org.opends.server.backends.pluggable.spi.ReadableTransaction; import org.opends.server.backends.pluggable.spi.Storage; import org.opends.server.backends.pluggable.spi.StorageStatus; import org.opends.server.backends.pluggable.spi.TreeName; import org.opends.server.backends.pluggable.spi.WriteOperation; import org.opends.server.controls.ServerSideSortRequestControl; import org.opends.server.controls.ServerSideSortResponseControl; import org.opends.server.controls.VLVRequestControl; import org.opends.server.core.AddOperation; import org.opends.server.core.ServerContext; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.protocols.internal.InternalSearchOperation; import org.opends.server.protocols.internal.SearchRequest; import org.opends.server.types.AuthenticationInfo; import org.opends.server.types.BackupConfig; import org.opends.server.types.BackupDirectory; import org.opends.server.types.Control; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.RestoreConfig; import org.opends.server.types.SearchResultEntry; import org.opends.server.workflowelement.localbackend.LocalBackendSearchOperation; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; /** * Regression tests for GHSA-6f6q-q9gw-4jr4: a sorted search of a caller subject to access control * takes the entries of a VLV index, then reads and sorts them through the caller's access controls. * The JE backend reads without a snapshot, so what the storage holds may change between those reads: * an entry may be deleted, and the index may stop being applicable or grow. The search must then * leave the entry out, or go on as without an index no longer applicable, rather than fail or come * back empty. An index that counts more than the most entries read from an index at once is refused, * whether it did when checked or grew over it since, and so is one that grows over the caller's * lookthrough limit between its count and the read of its entries. An index that applies and holds * no entry gives none. *

* The tests run on PDB, whose reads see a snapshot: the storage below makes each change land right * after the read it must follow, and answers the reads of the search after it as the JE backend * would. */ @SuppressWarnings("javadoc") @Test(groups = { "precommit", "pluggablebackend" }, sequential = true) public class VLVSortConcurrentChangeTest extends DirectoryServerTestCase { private static final String BACKEND_ID = "VLVSortConcurrentChangeTest"; private static final DN BASE_DN = DN.valueOf("dc=vlvsort,dc=com"); private static final DN PEOPLE_DN = DN.valueOf("ou=people,dc=vlvsort,dc=com"); /** Held in lower case, which is how an entry container keys the VLV indexes it holds. */ private static final String VLV_INDEX_NAME = "people-by-sn"; /** Added in this order, other than the order of their "sn" values: alice, bob, charlie. */ private static final DN CHARLIE_DN = DN.valueOf("uid=charlie,ou=people,dc=vlvsort,dc=com"); private static final DN ALICE_DN = DN.valueOf("uid=alice,ou=people,dc=vlvsort,dc=com"); private static final DN BOB_DN = DN.valueOf("uid=bob,ou=people,dc=vlvsort,dc=com"); /** Added only by the test that needs a fourth person. */ private static final DN DAVE_DN = DN.valueOf("uid=dave,ou=people,dc=vlvsort,dc=com"); private ServerContext serverContext; /** A user without the bypass-acl privilege, whom the global ACIs let read every entry and "sn". */ private InternalClientConnection actorConn; private SteeringBackend backend; private EntryContainer entryContainer; private VLVIndex vlvIndex; @BeforeClass public void startServer() throws Exception { TestCaseUtils.startServer(); serverContext = TestCaseUtils.getServerContext(); actorConn = new InternalClientConnection(new AuthenticationInfo(TestCaseUtils.makeEntry( "dn: uid=actor,dc=vlvsort,dc=com", "objectClass: top", "objectClass: person", "sn: Or", "cn: Act Or"), false)); } @BeforeMethod public void openBackend() throws Exception { backend = new SteeringBackend(); backend.setBackendID(BACKEND_ID); backend.configureBackend(backendCfg(), serverContext); // Start from a pristine on-disk state, whatever the previous test changed in it. backend.storage.removeStorageFiles(); backend.openBackend(); final List entries = TestCaseUtils.makeEntries( "dn: " + BASE_DN, "objectClass: top", "objectClass: domain", "dc: vlvsort", "", "dn: " + PEOPLE_DN, "objectClass: top", "objectClass: organizationalUnit", "ou: people", "", "dn: " + CHARLIE_DN, "objectClass: top", "objectClass: person", "objectClass: organizationalPerson", "objectClass: inetOrgPerson", "uid: charlie", "sn: Xavier", "cn: Charlie Xavier", "", "dn: " + ALICE_DN, "objectClass: top", "objectClass: person", "objectClass: organizationalPerson", "objectClass: inetOrgPerson", "uid: alice", "sn: Baker", "cn: Alice Baker", "", "dn: " + BOB_DN, "objectClass: top", "objectClass: person", "objectClass: organizationalPerson", "objectClass: inetOrgPerson", "uid: bob", "sn: Monroe", "cn: Bob Monroe"); for (Entry entry : entries) { backend.addEntry(entry, mock(AddOperation.class)); } entryContainer = backend.getRootContainer().getEntryContainer(BASE_DN); vlvIndex = entryContainer.getVLVIndex(VLV_INDEX_NAME); // Created with the backend empty, so trusted: otherwise no search would take it. assertThat(vlvIndex.isTrusted()).as("the VLV index trusted").isTrue(); } @AfterMethod(alwaysRun = true) public void closeBackend() { if (backend != null && backend.getRootContainer() != null) { backend.finalizeBackend(); } backend = null; try { // What a test that failed while the backend opened may have left in the server wide registry. serverContext.getBackendConfigManager().deregisterBaseDN(BASE_DN); } catch (Exception alreadyGone) { // Which is what closing the backend does. } } /** * An entry deleted once the VLV index gave the candidates, and before the sort reads it, takes no * part in the sort: the search must not fail on it. */ @Test public void sortOverVLVIndexLeavesOutEntryDeletedSinceItsCandidatesWereRead() throws Exception { backend.storage.deleteForTheNextReads(entryContainer.getID2Entry().getName(), entryIDKey(BOB_DN)); assertThat(sortedPeople(actorConn, PEOPLE_DN, SearchScope.SINGLE_LEVEL)).containsExactly(ALICE_DN, CHARLIE_DN); assertThat(backend.storage.readsOfDeleted).as("reads of the deleted entry").isPositive(); } /** * The same on the road of the attribute indexes, which every caller takes when no VLV index serves * the search. */ @Test public void sortOverAttributeIndexLeavesOutEntryDeletedSinceItsCandidatesWereRead() throws Exception { backend.storage.deleteForTheNextReads(entryContainer.getID2Entry().getName(), entryIDKey(BOB_DN)); assertThat(sortedPeople(getRootConnection(), BASE_DN, SearchScope.WHOLE_SUBTREE)) .containsExactly(ALICE_DN, CHARLIE_DN); assertThat(backend.storage.readsOfDeleted).as("reads of the deleted entry").isPositive(); } /** * A change of the definition of the VLV index between the check that it serves the search and the * read of its entries makes it no longer applicable: the search must go on as without it. */ @Test public void sortGoesOnWithoutVLVIndexRedefinedSinceItWasChecked() throws Exception { backend.storage.afterFirstRead(counterTreeName(), () -> concurrently(() -> vlvIndex.applyConfigurationChange(vlvIndexCfg("-sn")))); assertThat(sortedPeople(actorConn, PEOPLE_DN, SearchScope.SINGLE_LEVEL)) .containsExactly(ALICE_DN, BOB_DN, CHARLIE_DN); assertThat(backend.storage.changes).as("changes made during the search").isEqualTo(1); assertThat(vlvIndex.isTrusted()).as("the VLV index trusted after the change of its definition").isFalse(); } /** * A rebuild of the VLV index starting between the check and the read of its entries leaves it * untrusted and, for a read of the JE backend, empty: the search must go on as without it rather * than take the empty index for an empty result. */ @Test public void sortGoesOnWithoutVLVIndexWhoseRebuildStartedSinceItWasChecked() throws Exception { final TreeName deleted = emptyTree("vlv." + VLV_INDEX_NAME + ".deleted"); backend.storage.afterFirstRead(counterTreeName(), () -> { concurrently(() -> { backend.getRootContainer().getStorage().write(txn -> vlvIndex.setTrusted(txn, false)); return null; }); backend.storage.answer(vlvIndex.getName(), deleted); backend.storage.answer(counterTreeName(), deleted); }); assertThat(sortedPeople(actorConn, PEOPLE_DN, SearchScope.SINGLE_LEVEL)) .containsExactly(ALICE_DN, BOB_DN, CHARLIE_DN); assertThat(backend.storage.changes).as("changes made during the search").isEqualTo(1); assertThat(vlvIndex.isTrusted()).as("the VLV index trusted once its rebuild started").isFalse(); } /** * The VLV index growing over the most entries read from an index at once between its count and the * read of its entries makes it give none: the search must be refused, as it is when the index * already counts that many, rather than go on as without it and sort as many entries from the * attribute indexes. */ @Test public void sortOverVLVIndexGrownOverTheEntryLimitSinceItWasCountedIsRefused() throws Exception { final TreeName grown = counterHolding("counter.vlv." + VLV_INDEX_NAME + ".grown", CURSOR_ENTRY_LIMIT + 1); backend.storage.afterFirstRead(counterTreeName(), () -> backend.storage.answer(counterTreeName(), grown)); final InternalSearchOperation search = searchPeople(actorConn, PEOPLE_DN, SearchScope.SINGLE_LEVEL); assertThat(search.getResultCode()).isEqualTo(ResultCode.ADMIN_LIMIT_EXCEEDED); assertThat(search.getSearchEntries()).isEmpty(); assertThat(backend.storage.changes).as("changes made during the search").isEqualTo(1); } /** * A VLV index that stops being applicable is left aside whatever it counts: redefined between the * check and the read of its entries while it also grows over the most entries read from an index at * once, the search must still go on as without it rather than be refused. */ @Test public void sortGoesOnWithoutVLVIndexRedefinedSinceItWasCheckedThoughItGrewOverTheEntryLimit() throws Exception { final TreeName grown = counterHolding("counter.vlv." + VLV_INDEX_NAME + ".grown", CURSOR_ENTRY_LIMIT + 1); backend.storage.afterFirstRead(counterTreeName(), () -> { concurrently(() -> vlvIndex.applyConfigurationChange(vlvIndexCfg("-sn"))); backend.storage.answer(counterTreeName(), grown); }); assertThat(sortedPeople(actorConn, PEOPLE_DN, SearchScope.SINGLE_LEVEL)) .containsExactly(ALICE_DN, BOB_DN, CHARLIE_DN); assertThat(backend.storage.changes).as("changes made during the search").isEqualTo(1); } /** * An applicable VLV index whose tree gives none of the entries its count still holds, a state no * read should see, is neither refused nor taken for empty: the search must go on as without it. */ @Test public void sortGoesOnWithoutVLVIndexHoldingNoneOfTheEntriesItCounts() throws Exception { backend.storage.answer(vlvIndex.getName(), emptyTree("vlv." + VLV_INDEX_NAME + ".emptied")); assertThat(sortedPeople(actorConn, PEOPLE_DN, SearchScope.SINGLE_LEVEL)) .containsExactly(ALICE_DN, BOB_DN, CHARLIE_DN); } /** * Sorting the entries of a VLV index through the caller's access controls reads every one of them: * a caller without a lookthrough limit, as an internal one, may still not read more than the most * entries read from an index at once, so an index counting more is refused. */ @Test public void sortOverVLVIndexCountingOverTheEntryLimitIsRefused() throws Exception { backend.storage.answer(counterTreeName(), counterHolding("counter.vlv." + VLV_INDEX_NAME + ".over", CURSOR_ENTRY_LIMIT + 1)); final InternalSearchOperation search = searchPeople(actorConn, PEOPLE_DN, SearchScope.SINGLE_LEVEL); assertThat(search.getResultCode()).isEqualTo(ResultCode.ADMIN_LIMIT_EXCEEDED); assertThat(search.getSearchEntries()).isEmpty(); } /** * The VLV index growing between its count, checked against the caller's lookthrough limit, and the * read of its entries leaves it holding more entries than the caller may examine: the search must * be refused rather than sort them all, even though the VLV page it asks for is smaller. */ @Test public void sortOverVLVIndexGrownOverTheLookthroughLimitSinceItWasCountedIsRefused() throws Exception { // The index holds a fourth person, as if added right after its count read three. backend.addEntry(TestCaseUtils.makeEntry( "dn: " + DAVE_DN, "objectClass: top", "objectClass: person", "objectClass: organizationalPerson", "objectClass: inetOrgPerson", "uid: dave", "sn: Delaney", "cn: Dave Delaney"), mock(AddOperation.class)); backend.storage.answer(counterTreeName(), counterHolding("counter.vlv." + VLV_INDEX_NAME + ".before", 3)); backend.storage.afterFirstRead(counterTreeName(), () -> backend.storage.answer(counterTreeName(), counterTreeName())); final InternalSearchOperation search = searchPeople( actorLimitedTo(3), PEOPLE_DN, SearchScope.SINGLE_LEVEL, new VLVRequestControl(0, 0, 1, 0)); assertThat(search.getResultCode()).isEqualTo(ResultCode.ADMIN_LIMIT_EXCEEDED); assertThat(search.getSearchEntries()).isEmpty(); assertThat(backend.storage.changes).as("changes made during the search").isEqualTo(1); } /** * An applicable VLV index holding no entry answers the search with none, sorted: the search must * neither go on as without it, through the attribute indexes, which here still hold the people, nor * be refused. */ @Test public void sortOverEmptyVLVIndexGivesNoEntry() throws Exception { final TreeName empty = emptyTree("vlv." + VLV_INDEX_NAME + ".empty"); backend.storage.answer(vlvIndex.getName(), empty); backend.storage.answer(counterTreeName(), empty); final InternalSearchOperation search = searchPeople(actorConn, PEOPLE_DN, SearchScope.SINGLE_LEVEL); assertThat(search.getSearchEntries()).isEmpty(); assertThat(sortResultCode(search)).as("result code of the sort response control").isEqualTo(SUCCESS); } /** The actor, held to the provided lookthrough limit, which an internal connection never holds otherwise. */ private InternalClientConnection actorLimitedTo(int lookthroughLimit) throws Exception { final InternalClientConnection conn = new InternalClientConnection(actorConn.getAuthenticationInfo()); // An internal connection ignores the setter of the limit, which its connection keeps in a field of its own. final Field lookthroughLimitField = ClientConnection.class.getDeclaredField("lookthroughLimit"); lookthroughLimitField.setAccessible(true); lookthroughLimitField.setInt(conn, lookthroughLimit); return conn; } /** Sorts the people on "sn", with a critical sort control and the provided controls, straight through the backend. */ private InternalSearchOperation searchPeople(InternalClientConnection conn, DN baseDN, SearchScope scope, Control... controls) throws Exception { final SearchRequest request = newSearchRequest(baseDN, scope, "(sn=*)").addControl(new ServerSideSortRequestControl(true, "sn")); for (Control control : controls) { request.addControl(control); } final InternalSearchOperation search = new InternalSearchOperation(conn, -1, -1, request); backend.search(new LocalBackendSearchOperation(search)); return search; } private List sortedPeople(InternalClientConnection conn, DN baseDN, SearchScope scope) throws Exception { final List dns = new ArrayList<>(); for (SearchResultEntry entry : searchPeople(conn, baseDN, scope).getSearchEntries()) { dns.add(entry.getName()); } return dns; } /** The result code of the one sort response control of the search. */ private static int sortResultCode(InternalSearchOperation search) { final List resultCodes = new ArrayList<>(); for (Control control : search.getResponseControls()) { if (OID_SERVER_SIDE_SORT_RESPONSE_CONTROL.equals(control.getOID())) { resultCodes.add(((ServerSideSortResponseControl) control).getResultCode()); } } assertThat(resultCodes).as("result codes of the sort response controls").hasSize(1); return resultCodes.get(0); } private ByteString entryIDKey(DN dn) throws Exception { return backend.getRootContainer().getStorage().read(txn -> entryContainer.getDN2ID().get(txn, dn)).toByteString(); } /** The tree the VLV index counts its entries in, named as the VLV index names it. */ private TreeName counterTreeName() { return new TreeName(entryContainer.getTreePrefix(), "counter.vlv." + VLV_INDEX_NAME); } private TreeName emptyTree(String name) throws Exception { final TreeName tree = new TreeName(entryContainer.getTreePrefix(), name); backend.getRootContainer().getStorage().write(txn -> txn.openTree(tree, true)); return tree; } /** A tree counting entries as the VLV index counts its own, holding the provided count. */ private TreeName counterHolding(String name, long count) throws Exception { final TreeName tree = emptyTree(name); // The key the VLV index keeps its count under is its own. final Field countKeyField = VLVIndex.class.getDeclaredField("COUNT_KEY"); countKeyField.setAccessible(true); final ByteString countKey = (ByteString) countKeyField.get(null); final ShardedCounter counter = new ShardedCounter(tree); backend.getRootContainer().getStorage().write(txn -> counter.addCount(txn, countKey, count)); return tree; } /** Makes a change from another thread, as a concurrent operation does, and waits until it is done. */ private static void concurrently(Callable change) { final FutureTask task = new FutureTask<>(change); new Thread(task, "concurrent change").start(); try { task.get(30, TimeUnit.SECONDS); } catch (Exception e) { throw new AssertionError("the concurrent change failed", e); } } private PDBBackendCfg backendCfg() throws ConfigException { final PDBBackendCfg cfg = mockCfg(PDBBackendCfg.class); when(cfg.dn()).thenReturn(DN.valueOf("ds-cfg-backend-id=" + BACKEND_ID + ",cn=Backends,cn=config")); when(cfg.getBackendId()).thenReturn(BACKEND_ID); when(cfg.getDBDirectory()).thenReturn(BACKEND_ID); when(cfg.getDBDirectoryPermissions()).thenReturn("755"); when(cfg.getDBCacheSize()).thenReturn(0L); when(cfg.getDBCachePercent()).thenReturn(20); when(cfg.getBaseDN()).thenReturn(newTreeSet(BASE_DN)); // Built before they are handed over: stubbing a mock from inside a when() of another mock leaves // that when() unfinished. final BackendIndexCfg snIndexCfg = snPresenceIndexCfg(); final BackendVLVIndexCfg vlvIndexCfg = vlvIndexCfg("sn"); // The index a search goes on with, once it goes on without the VLV index. when(cfg.listBackendIndexes()).thenReturn(new String[] { "sn" }); when(cfg.getBackendIndex("sn")).thenReturn(snIndexCfg); when(cfg.listBackendVLVIndexes()).thenReturn(new String[] { VLV_INDEX_NAME }); when(cfg.getBackendVLVIndex(VLV_INDEX_NAME)).thenReturn(vlvIndexCfg); return cfg; } private BackendIndexCfg snPresenceIndexCfg() { final BackendIndexCfg cfg = mock(BackendIndexCfg.class); when(cfg.getIndexType()).thenReturn(newTreeSet(IndexType.PRESENCE)); when(cfg.getAttribute()).thenReturn(serverContext.getSchema().getAttributeType("sn")); when(cfg.getIndexEntryLimit()).thenReturn(4000); when(cfg.getSubstringLength()).thenReturn(6); return cfg; } /** The people sorted on the provided order, which a search of them sorted on "sn" matches as "sn". */ private BackendVLVIndexCfg vlvIndexCfg(String sortOrder) { final BackendVLVIndexCfg cfg = mock(BackendVLVIndexCfg.class); when(cfg.getName()).thenReturn(VLV_INDEX_NAME); when(cfg.getBaseDN()).thenReturn(PEOPLE_DN); when(cfg.getScope()).thenReturn(Scope.SINGLE_LEVEL); when(cfg.getFilter()).thenReturn("(sn=*)"); when(cfg.getSortOrder()).thenReturn(sortOrder); return cfg; } /** A PDB backend whose storage is a {@link SteeringStorage}. */ private static final class SteeringBackend extends BackendImpl { private SteeringStorage storage; @Override protected Storage configureStorage(PDBBackendCfg cfg, ServerContext serverContext) throws ConfigException { storage = new SteeringStorage(new PDBStorage(cfg, serverContext)); return storage; } } /** * Decorates a {@link Storage} so that its reads see what a concurrent operation changed after the * read began, as reads of the JE backend do: a record deleted, a tree left in another state, which * the reads then get from another tree holding that state. A change may also be set to land right * after the first cursor a read opens on a given tree. */ private static final class SteeringStorage implements Storage { private final Storage delegate; private final Map answeredFrom = new HashMap<>(); private TreeName deletedFrom; private ByteString deletedKey; private int readsOfDeleted; private TreeName changedAfter; private Runnable change; private int changes; SteeringStorage(Storage delegate) { this.delegate = delegate; } void deleteForTheNextReads(TreeName treeName, ByteString key) { deletedFrom = treeName; deletedKey = key; } /** Answers the reads of a tree from now on from another tree, which holds the state it was left in. */ void answer(TreeName treeName, TreeName from) { answeredFrom.put(treeName, from); } void afterFirstRead(TreeName treeName, Runnable change) { this.changedAfter = treeName; this.change = change; } private TreeName answering(TreeName treeName) { final TreeName from = answeredFrom.get(treeName); return from != null ? from : treeName; } @Override public T read(final ReadOperation readOperation) throws Exception { return delegate.read(txn -> readOperation.run(new ReadableTransaction() { @Override public ByteString read(TreeName treeName, ByteSequence key) { if (treeName.equals(deletedFrom) && deletedKey.equals(key)) { readsOfDeleted++; return null; } return txn.read(answering(treeName), key); } @Override public Cursor openCursor(TreeName treeName) { final Cursor cursor = txn.openCursor(answering(treeName)); if (change != null && treeName.equals(changedAfter)) { final Runnable landing = change; change = null; changes++; landing.run(); } return cursor; } @Override public long getRecordCount(TreeName treeName) { return txn.getRecordCount(answering(treeName)); } @Override public boolean treeExists(TreeName treeName) { return txn.treeExists(answering(treeName)); } })); } @Override public void write(WriteOperation writeOperation) throws Exception { delegate.write(writeOperation); } @Override public Importer startImport() throws ConfigException { return delegate.startImport(); } @Override public void open(AccessMode accessMode) throws Exception { delegate.open(accessMode); } @Override public void removeStorageFiles() { delegate.removeStorageFiles(); } @Override public StorageStatus getStorageStatus() { return delegate.getStorageStatus(); } @Override public boolean supportsBackupAndRestore() { return delegate.supportsBackupAndRestore(); } @Override public void createBackup(BackupConfig backupConfig) throws DirectoryException { delegate.createBackup(backupConfig); } @Override public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException { delegate.removeBackup(backupDirectory, backupID); } @Override public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException { delegate.restoreBackup(restoreConfig); } @Override public Set listTrees() { return delegate.listTrees(); } @Override public void close() { delegate.close(); } } }