/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2006-2010 Sun Microsystems, Inc.
* Portions Copyright 2011-2015 ForgeRock AS
*/
package org.opends.server.backends.pluggable;
import static org.opends.messages.BackendMessages.*;
import static org.opends.server.util.StaticUtils.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.PluggableBackendCfg;
import org.opends.server.api.CompressedSchema;
import org.opends.server.backends.pluggable.spi.AccessMode;
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.StorageRuntimeException;
import org.opends.server.backends.pluggable.spi.StorageStatus;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.core.SearchOperation;
import org.opends.server.types.DN;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.InitializationException;
import org.opends.server.types.Operation;
import org.opends.server.types.Privilege;
/**
* Wrapper class for a backend "container". Root container holds all the entry
* containers for each base DN. It also maintains all the openings and closings
* of the entry containers.
*/
public class RootContainer implements ConfigurationChangeListener<PluggableBackendCfg>
{
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
/** The tree storage. */
private final Storage storage;
/** The ID of the backend to which this entry root container belongs. */
private final String backendId;
/** The backend configuration. */
private final PluggableBackendCfg config;
/** The monitor for this backend. */
private BackendMonitor monitor;
/** The base DNs contained in this root container. */
private final ConcurrentMap<DN, EntryContainer> entryContainers = new ConcurrentHashMap<>();
/** Value of the next entryID to be assigned. */
private AtomicLong nextEntryID = new AtomicLong(1);
/** The compressed schema manager for this backend. */
private PersistentCompressedSchema compressedSchema;
/**
* Creates a new RootContainer object representing a storage.
*
* @param config
* The configuration of the backend.
* @param backendID
* A reference to the backend that is creating this root
* container.
*/
RootContainer(String backendID, Storage storage, PluggableBackendCfg config)
{
this.backendId = backendID;
this.storage = storage;
this.config = config;
getMonitorProvider().enableFilterUseStats(config.isIndexFilterAnalyzerEnabled());
getMonitorProvider().setMaxEntries(config.getIndexFilterAnalyzerMaxFilters());
config.addPluggableChangeListener(this);
}
/**
* Returns the underlying storage engine.
*
* @return the underlying storage engine
*/
Storage getStorage()
{
return storage;
}
/**
* Opens the root container.
*
* @param accessMode specifies how the container has to be opened (read-write or read-only)
*
* @throws StorageRuntimeException
* If an error occurs when opening the storage.
* @throws ConfigException
* If an configuration error occurs while opening the storage.
*/
void open(final AccessMode accessMode) throws StorageRuntimeException, ConfigException
{
try
{
storage.open(accessMode);
storage.write(new WriteOperation()
{
@Override
public void run(WriteableTransaction txn) throws Exception
{
compressedSchema = new PersistentCompressedSchema(storage, txn, accessMode);
openAndRegisterEntryContainers(txn, config.getBaseDN(), accessMode);
}
});
}
catch(StorageRuntimeException e)
{
throw e;
}
catch (Exception e)
{
throw new StorageRuntimeException(e);
}
}
/**
* Opens the entry container for a base DN. If the entry container does not
* exist for the base DN, it will be created. The entry container will be
* opened with the same mode as the root container. Any entry containers
* opened in a read only root container will also be read only. Any entry
* containers opened in a non transactional root container will also be non
* transactional.
*
* @param baseDN
* The base DN of the entry container to open.
* @param txn
* The transaction
* @param accessMode specifies how the container has to be opened (read-write or read-only)
* @return The opened entry container.
* @throws StorageRuntimeException
* If an error occurs while opening the entry container.
* @throws ConfigException
* If an configuration error occurs while opening the entry container.
*/
EntryContainer openEntryContainer(DN baseDN, WriteableTransaction txn, AccessMode accessMode)
throws StorageRuntimeException, ConfigException
{
EntryContainer ec = new EntryContainer(baseDN, backendId, config, storage, this);
ec.open(txn, accessMode);
return ec;
}
/**
* Registers the entry container for a base DN.
*
* @param baseDN
* The base DN of the entry container to close.
* @param entryContainer
* The entry container to register for the baseDN.
* @throws InitializationException
* If an error occurs while opening the entry container.
*/
void registerEntryContainer(DN baseDN, EntryContainer entryContainer) throws InitializationException
{
EntryContainer ec = this.entryContainers.get(baseDN);
if (ec != null)
{
throw new InitializationException(ERR_ENTRY_CONTAINER_ALREADY_REGISTERED.get(ec.getTreePrefix(), baseDN));
}
this.entryContainers.put(baseDN, entryContainer);
}
/**
* Opens the entry containers for multiple base DNs.
*
* @param baseDNs
* The base DNs of the entry containers to open.
* @param accessMode specifies how the containers have to be opened (read-write or read-only)
*
* @throws StorageRuntimeException
* If an error occurs while opening the entry container.
* @throws InitializationException
* If an initialization error occurs while opening the entry
* container.
* @throws ConfigException
* If a configuration error occurs while opening the entry
* container.
*/
private void openAndRegisterEntryContainers(WriteableTransaction txn, Set<DN> baseDNs, AccessMode accessMode)
throws StorageRuntimeException, InitializationException, ConfigException
{
EntryID highestID = null;
for (DN baseDN : baseDNs)
{
EntryContainer ec = openEntryContainer(baseDN, txn, accessMode);
EntryID id = ec.getHighestEntryID(txn);
registerEntryContainer(baseDN, ec);
if (highestID == null || id.compareTo(highestID) > 0)
{
highestID = id;
}
}
nextEntryID = new AtomicLong(highestID.longValue() + 1);
}
/**
* Unregisters the entry container for a base DN.
*
* @param baseDN
* The base DN of the entry container to close.
* @return The entry container that was unregistered or NULL if a entry
* container for the base DN was not registered.
*/
EntryContainer unregisterEntryContainer(DN baseDN)
{
return entryContainers.remove(baseDN);
}
/**
* Retrieves the compressed schema manager for this backend.
*
* @return The compressed schema manager for this backend.
*/
CompressedSchema getCompressedSchema()
{
return compressedSchema;
}
/**
* Get the BackendMonitor object used by this root container.
*
* @return The BackendMonitor object.
*/
BackendMonitor getMonitorProvider()
{
if (monitor == null)
{
monitor = new BackendMonitor(backendId + " Storage", this);
}
return monitor;
}
/**
* Preload the tree cache. There is no preload if the configured preload
* time limit is zero.
*
* @param timeLimit
* The time limit for the preload process.
*/
void preload(long timeLimit)
{
if (timeLimit > 0)
{
// Get a list of all the tree used by the backend.
final List<Tree> trees = new ArrayList<>();
for (EntryContainer ec : entryContainers.values())
{
ec.sharedLock.lock();
try
{
trees.addAll(ec.listTrees());
}
finally
{
ec.sharedLock.unlock();
}
}
// Sort the list in order of priority.
Collections.sort(trees, new TreePreloadComparator());
// Preload each tree until we reach the time limit or the cache is filled.
try
{
throw new UnsupportedOperationException("Not implemented exception");
}
catch (StorageRuntimeException e)
{
logger.error(ERR_CACHE_PRELOAD, backendId,
stackTraceToSingleLineString(e.getCause() != null ? e.getCause() : e));
}
}
}
/**
* Closes this root container.
*
* @throws StorageRuntimeException
* If an error occurs while attempting to close the root container.
*/
void close() throws StorageRuntimeException
{
for (DN baseDN : entryContainers.keySet())
{
EntryContainer ec = unregisterEntryContainer(baseDN);
ec.exclusiveLock.lock();
try
{
ec.close();
}
finally
{
ec.exclusiveLock.unlock();
}
}
config.removePluggableChangeListener(this);
if (storage != null)
{
storage.close();
}
}
/**
* Return all the entry containers in this root container.
*
* @return The entry containers in this root container.
*/
public Collection<EntryContainer> getEntryContainers()
{
return entryContainers.values();
}
/**
* Returns all the baseDNs this root container stores.
*
* @return The set of DNs this root container stores.
*/
Set<DN> getBaseDNs()
{
return entryContainers.keySet();
}
/**
* Return the entry container for a specific base DN.
*
* @param baseDN
* The base DN of the entry container to retrieve.
* @return The entry container for the base DN.
*/
EntryContainer getEntryContainer(DN baseDN)
{
EntryContainer ec = null;
DN nodeDN = baseDN;
while (ec == null && nodeDN != null)
{
ec = entryContainers.get(nodeDN);
if (ec == null)
{
nodeDN = nodeDN.getParentDNInSuffix();
}
}
return ec;
}
/**
* Get the total number of entries in this root container.
*
* @return The number of entries in this root container
* @throws StorageRuntimeException
* If an error occurs while retrieving the entry count.
*/
long getEntryCount() throws StorageRuntimeException
{
try
{
return storage.read(new ReadOperation<Long>()
{
@Override
public Long run(ReadableTransaction txn) throws Exception
{
long entryCount = 0;
for (EntryContainer ec : entryContainers.values())
{
ec.sharedLock.lock();
try
{
entryCount += ec.getNumberOfEntriesInBaseDN0(txn);
}
finally
{
ec.sharedLock.unlock();
}
}
return entryCount;
}
});
}
catch (Exception e)
{
throw new StorageRuntimeException(e);
}
}
/**
* Assign the next entry ID.
*
* @return The assigned entry ID.
*/
EntryID getNextEntryID()
{
return new EntryID(nextEntryID.getAndIncrement());
}
/** Resets the next entry ID counter to zero. This should only be used after clearing all trees. */
public void resetNextEntryID()
{
nextEntryID.set(1);
}
@Override
public boolean isConfigurationChangeAcceptable(PluggableBackendCfg configuration,
List<LocalizableMessage> unacceptableReasons)
{
// Storage has also registered a change listener, delegate to it.
return true;
}
@Override
public ConfigChangeResult applyConfigurationChange(PluggableBackendCfg configuration)
{
getMonitorProvider().enableFilterUseStats(configuration.isIndexFilterAnalyzerEnabled());
getMonitorProvider().setMaxEntries(configuration.getIndexFilterAnalyzerMaxFilters());
return new ConfigChangeResult();
}
/**
* Checks the storage has enough resources for an operation.
*
* @param operation the current operation
* @throws DirectoryException if resources are in short supply
*/
void checkForEnoughResources(Operation operation) throws DirectoryException
{
StorageStatus status = storage.getStorageStatus();
if (status.isUnusable()
|| (status.isLockedDown() && hasBypassLockdownPrivileges(operation)))
{
throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, status.getReason());
}
}
private boolean hasBypassLockdownPrivileges(Operation operation)
{
return operation != null
// Read operations are always allowed in lock down mode
&& !(operation instanceof SearchOperation)
&& !operation.getClientConnection().hasPrivilege(
Privilege.BYPASS_LOCKDOWN, operation);
}
}