/*
|
* 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 2007-2008 Sun Microsystems, Inc.
|
* Portions Copyright 2011-2016 ForgeRock AS
|
*/
|
package org.opends.server.backends;
|
|
import static org.forgerock.util.Reject.*;
|
import static org.opends.messages.BackendMessages.*;
|
import static org.opends.server.util.ServerConstants.*;
|
import static org.opends.server.util.StaticUtils.*;
|
|
import java.io.File;
|
import java.util.*;
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
|
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.ConditionResult;
|
import org.forgerock.opendj.ldap.ResultCode;
|
import org.forgerock.opendj.ldap.SearchScope;
|
import org.opends.server.admin.server.ConfigurationChangeListener;
|
import org.opends.server.admin.std.server.LDIFBackendCfg;
|
import org.opends.server.api.AlertGenerator;
|
import org.opends.server.api.Backend;
|
import org.opends.server.controls.SubtreeDeleteControl;
|
import org.opends.server.core.*;
|
import org.forgerock.opendj.ldap.schema.AttributeType;
|
import org.opends.server.types.*;
|
import org.opends.server.util.LDIFException;
|
import org.opends.server.util.LDIFReader;
|
import org.opends.server.util.LDIFWriter;
|
import org.opends.server.util.StaticUtils;
|
|
/**
|
* This class provides a backend implementation that stores the underlying data
|
* in an LDIF file. When the backend is initialized, the contents of the
|
* backend are read into memory and all read operations are performed purely
|
* from memory. Write operations cause the underlying LDIF file to be
|
* re-written on disk.
|
*/
|
public class LDIFBackend
|
extends Backend<LDIFBackendCfg>
|
implements ConfigurationChangeListener<LDIFBackendCfg>, AlertGenerator
|
{
|
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
|
|
|
|
/** The base DNs for this backend. */
|
private DN[] baseDNs;
|
|
/** The mapping between parent DNs and their immediate children. */
|
private final Map<DN, Set<DN>> childDNs = new HashMap<>();
|
|
/** The base DNs for this backend, in a hash set. */
|
private Set<DN> baseDNSet;
|
|
/** The set of supported controls for this backend. */
|
private final Set<String> supportedControls =
|
Collections.singleton(OID_SUBTREE_DELETE_CONTROL);
|
|
/** The current configuration for this backend. */
|
private LDIFBackendCfg currentConfig;
|
|
/** The mapping between entry DNs and the corresponding entries. */
|
private final Map<DN, Entry> entryMap = new LinkedHashMap<>();
|
|
/** A read-write lock used to protect access to this backend. */
|
private final ReentrantReadWriteLock backendLock = new ReentrantReadWriteLock();
|
|
/** The path to the LDIF file containing the data for this backend. */
|
private String ldifFilePath;
|
|
/**
|
* Creates a new backend with the provided information. All backend
|
* implementations must implement a default constructor that use
|
* <CODE>super()</CODE> to invoke this constructor.
|
*/
|
public LDIFBackend()
|
{
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void openBackend()
|
throws ConfigException, InitializationException
|
{
|
// We won't support anything other than exactly one base DN in this
|
// implementation. If we were to add such support in the future, we would
|
// likely want to separate the data for each base DN into a separate entry
|
// map.
|
if (baseDNs == null || baseDNs.length != 1)
|
{
|
throw new ConfigException(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(currentConfig.dn()));
|
}
|
|
for (DN dn : baseDNs)
|
{
|
try
|
{
|
DirectoryServer.registerBaseDN(dn, this,
|
currentConfig.isIsPrivateBackend());
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
LocalizableMessage message = ERR_BACKEND_CANNOT_REGISTER_BASEDN.get(
|
dn, getExceptionMessage(e));
|
throw new InitializationException(message, e);
|
}
|
}
|
|
DirectoryServer.registerAlertGenerator(this);
|
|
readLDIF();
|
}
|
|
|
|
/**
|
* Reads the contents of the LDIF backing file into memory.
|
*
|
* @throws InitializationException If a problem occurs while reading the
|
* LDIF file.
|
*/
|
private void readLDIF()
|
throws InitializationException
|
{
|
File ldifFile = getFileForPath(ldifFilePath);
|
if (! ldifFile.exists())
|
{
|
// This is fine. We will just start with an empty backend.
|
if (logger.isTraceEnabled())
|
{
|
logger.trace("LDIF backend starting empty because LDIF file " +
|
ldifFilePath + " does not exist");
|
}
|
|
entryMap.clear();
|
childDNs.clear();
|
return;
|
}
|
|
|
try
|
{
|
importLDIF(new LDIFImportConfig(ldifFile.getAbsolutePath()), false);
|
}
|
catch (DirectoryException de)
|
{
|
throw new InitializationException(de.getMessageObject(), de);
|
}
|
}
|
|
|
|
/**
|
* Writes the current set of entries to the target LDIF file. The new LDIF
|
* will first be created as a temporary file and then renamed into place. The
|
* caller must either hold the write lock for this backend, or must ensure
|
* that it's in some other state that guarantees exclusive access to the data.
|
*
|
* @throws DirectoryException If a problem occurs that prevents the updated
|
* LDIF from being written.
|
*/
|
private void writeLDIF()
|
throws DirectoryException
|
{
|
File ldifFile = getFileForPath(ldifFilePath);
|
File tempFile = new File(ldifFile.getAbsolutePath() + ".new");
|
File oldFile = new File(ldifFile.getAbsolutePath() + ".old");
|
|
|
// Write the new data to a temporary file.
|
LDIFWriter writer;
|
try
|
{
|
LDIFExportConfig exportConfig =
|
new LDIFExportConfig(tempFile.getAbsolutePath(),
|
ExistingFileBehavior.OVERWRITE);
|
writer = new LDIFWriter(exportConfig);
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_CREATING_FILE.get(
|
tempFile.getAbsolutePath(),
|
currentConfig.dn(),
|
stackTraceToSingleLineString(e));
|
DirectoryServer.sendAlertNotification(this,
|
ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
m, e);
|
}
|
|
|
for (Entry entry : entryMap.values())
|
{
|
try
|
{
|
writer.writeEntry(entry);
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
StaticUtils.close(writer);
|
|
LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_WRITING_FILE.get(
|
tempFile.getAbsolutePath(),
|
currentConfig.dn(),
|
stackTraceToSingleLineString(e));
|
DirectoryServer.sendAlertNotification(this,
|
ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
m, e);
|
}
|
}
|
|
// On Linux the final write() on a file can actually fail but not throw an Exception.
|
// The close() will throw an Exception in this case so we MUST check for Exceptions
|
// here.
|
try
|
{
|
writer.close();
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_CLOSING_FILE.get(
|
tempFile.getAbsolutePath(),
|
currentConfig.dn(),
|
stackTraceToSingleLineString(e));
|
DirectoryServer.sendAlertNotification(this,
|
ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
m, e);
|
}
|
|
// Extra sanity check
|
if (!entryMap.isEmpty() && tempFile.exists() && tempFile.length() == 0)
|
{
|
LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_EMPTY_FILE.get(
|
tempFile.getAbsolutePath(),
|
currentConfig.dn());
|
DirectoryServer.sendAlertNotification(this,
|
ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), m);
|
}
|
|
if (tempFile.exists())
|
{
|
// Rename the existing "live" file out of the way and move the new file
|
// into place.
|
try
|
{
|
oldFile.delete();
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
}
|
}
|
|
try
|
{
|
if (ldifFile.exists())
|
{
|
ldifFile.renameTo(oldFile);
|
}
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
}
|
|
try
|
{
|
tempFile.renameTo(ldifFile);
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_RENAMING_FILE.get(
|
tempFile.getAbsolutePath(),
|
ldifFile.getAbsolutePath(),
|
currentConfig.dn(),
|
stackTraceToSingleLineString(e));
|
DirectoryServer.sendAlertNotification(this,
|
ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m);
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
m, e);
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void closeBackend()
|
{
|
backendLock.writeLock().lock();
|
|
try
|
{
|
currentConfig.removeLDIFChangeListener(this);
|
DirectoryServer.deregisterAlertGenerator(this);
|
|
for (DN dn : baseDNs)
|
{
|
try
|
{
|
DirectoryServer.deregisterBaseDN(dn);
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
}
|
}
|
}
|
finally
|
{
|
backendLock.writeLock().unlock();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public DN[] getBaseDNs()
|
{
|
return baseDNs;
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public long getEntryCount()
|
{
|
backendLock.readLock().lock();
|
|
try
|
{
|
if (entryMap != null)
|
{
|
return entryMap.size();
|
}
|
|
return -1;
|
}
|
finally
|
{
|
backendLock.readLock().unlock();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public boolean isIndexed(AttributeType attributeType, IndexType indexType)
|
{
|
// All searches in this backend will always be considered indexed.
|
return true;
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public ConditionResult hasSubordinates(DN entryDN)
|
throws DirectoryException
|
{
|
backendLock.readLock().lock();
|
|
try
|
{
|
Set<DN> childDNSet = childDNs.get(entryDN);
|
if (childDNSet == null || childDNSet.isEmpty())
|
{
|
// It could be that the entry doesn't exist, in which case we should
|
// throw an exception.
|
if (entryMap.containsKey(entryDN))
|
{
|
return ConditionResult.FALSE;
|
}
|
else
|
{
|
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
|
ERR_LDIF_BACKEND_HAS_SUBORDINATES_NO_SUCH_ENTRY.get(entryDN));
|
}
|
}
|
else
|
{
|
return ConditionResult.TRUE;
|
}
|
}
|
finally
|
{
|
backendLock.readLock().unlock();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public long getNumberOfChildren(DN parentDN) throws DirectoryException
|
{
|
checkNotNull(parentDN, "parentDN must not be null");
|
return getNumberOfSubordinates(parentDN, false);
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException
|
{
|
checkNotNull(baseDN, "baseDN must not be null");
|
if (!Arrays.asList(baseDNs).contains(baseDN))
|
{
|
throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_LDIF_BACKEND_NUM_SUBORDINATES_NO_SUCH_ENTRY
|
.get(baseDN));
|
}
|
final int baseDNIfExists = childDNs.containsKey(baseDN) ? 1 : 0;
|
return getNumberOfSubordinates(baseDN, true) + baseDNIfExists;
|
}
|
|
private long getNumberOfSubordinates(DN entryDN, boolean includeSubtree) throws DirectoryException
|
{
|
backendLock.readLock().lock();
|
|
try
|
{
|
Set<DN> childDNSet = childDNs.get(entryDN);
|
if (childDNSet == null || childDNSet.isEmpty())
|
{
|
// It could be that the entry doesn't exist, in which case we should
|
// throw an exception.
|
if (entryMap.containsKey(entryDN))
|
{
|
return 0L;
|
}
|
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_LDIF_BACKEND_NUM_SUBORDINATES_NO_SUCH_ENTRY
|
.get(entryDN));
|
}
|
|
if (!includeSubtree)
|
{
|
return childDNSet.size();
|
}
|
|
long count = 0;
|
for (DN childDN : childDNSet)
|
{
|
count += getNumberOfSubordinates(childDN, true);
|
count++;
|
}
|
return count;
|
}
|
finally
|
{
|
backendLock.readLock().unlock();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public Entry getEntry(DN entryDN)
|
{
|
backendLock.readLock().lock();
|
|
try
|
{
|
return entryMap.get(entryDN);
|
}
|
finally
|
{
|
backendLock.readLock().unlock();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public boolean entryExists(DN entryDN)
|
{
|
backendLock.readLock().lock();
|
|
try
|
{
|
return entryMap.containsKey(entryDN);
|
}
|
finally
|
{
|
backendLock.readLock().unlock();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void addEntry(Entry entry, AddOperation addOperation)
|
throws DirectoryException
|
{
|
backendLock.writeLock().lock();
|
|
try
|
{
|
// Make sure that the target entry does not already exist, but that its
|
// parent does exist (or that the entry being added is the base DN).
|
DN entryDN = entry.getName();
|
if (entryMap.containsKey(entryDN))
|
{
|
LocalizableMessage m = ERR_LDIF_BACKEND_ADD_ALREADY_EXISTS.get(entryDN);
|
throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, m);
|
}
|
|
if (baseDNSet.contains(entryDN))
|
{
|
entryMap.put(entryDN, entry.duplicate(false));
|
writeLDIF();
|
return;
|
}
|
else
|
{
|
DN parentDN = entryDN.getParentDNInSuffix();
|
if (parentDN != null && entryMap.containsKey(parentDN))
|
{
|
entryMap.put(entryDN, entry.duplicate(false));
|
|
Set<DN> childDNSet = childDNs.get(parentDN);
|
if (childDNSet == null)
|
{
|
childDNSet = new HashSet<>();
|
childDNs.put(parentDN, childDNSet);
|
}
|
childDNSet.add(entryDN);
|
writeLDIF();
|
return;
|
}
|
else
|
{
|
DN matchedDN = null;
|
if (parentDN != null)
|
{
|
while (true)
|
{
|
parentDN = parentDN.getParentDNInSuffix();
|
if (parentDN == null)
|
{
|
break;
|
}
|
|
if (entryMap.containsKey(parentDN))
|
{
|
matchedDN = parentDN;
|
break;
|
}
|
}
|
}
|
|
LocalizableMessage m = ERR_LDIF_BACKEND_ADD_MISSING_PARENT.get(entryDN);
|
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
|
}
|
}
|
}
|
finally
|
{
|
backendLock.writeLock().unlock();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void deleteEntry(DN entryDN, DeleteOperation deleteOperation)
|
throws DirectoryException
|
{
|
backendLock.writeLock().lock();
|
|
try
|
{
|
// Get the DN of the target entry's parent, if it exists. We'll need to
|
// also remove the reference to the target entry from the parent's set of
|
// children.
|
DN parentDN = entryDN.getParentDNInSuffix();
|
|
// Make sure that the target entry exists. If not, then fail.
|
if (! entryMap.containsKey(entryDN))
|
{
|
DN matchedDN = null;
|
while (parentDN != null)
|
{
|
if (entryMap.containsKey(parentDN))
|
{
|
matchedDN = parentDN;
|
break;
|
}
|
|
parentDN = parentDN.getParentDNInSuffix();
|
}
|
|
LocalizableMessage m = ERR_LDIF_BACKEND_DELETE_NO_SUCH_ENTRY.get(entryDN);
|
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
|
}
|
|
|
// See if the target entry has any children. If so, then we'll only
|
// delete it if the request contains the subtree delete control (in
|
// which case we'll delete the entire subtree).
|
Set<DN> childDNSet = childDNs.get(entryDN);
|
if (childDNSet == null || childDNSet.isEmpty())
|
{
|
entryMap.remove(entryDN);
|
childDNs.remove(entryDN);
|
|
if (parentDN != null)
|
{
|
Set<DN> parentChildren = childDNs.get(parentDN);
|
if (parentChildren != null)
|
{
|
parentChildren.remove(entryDN);
|
if (parentChildren.isEmpty())
|
{
|
childDNs.remove(parentDN);
|
}
|
}
|
}
|
}
|
else
|
{
|
boolean subtreeDelete = deleteOperation != null
|
&& deleteOperation
|
.getRequestControl(SubtreeDeleteControl.DECODER) != null;
|
|
if (! subtreeDelete)
|
{
|
LocalizableMessage m = ERR_LDIF_BACKEND_DELETE_NONLEAF.get(entryDN);
|
throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF, m);
|
}
|
|
entryMap.remove(entryDN);
|
childDNs.remove(entryDN);
|
|
if (parentDN != null)
|
{
|
Set<DN> parentChildren = childDNs.get(parentDN);
|
if (parentChildren != null)
|
{
|
parentChildren.remove(entryDN);
|
if (parentChildren.isEmpty())
|
{
|
childDNs.remove(parentDN);
|
}
|
}
|
}
|
|
for (DN childDN : childDNSet)
|
{
|
subtreeDelete(childDN);
|
}
|
}
|
|
writeLDIF();
|
}
|
finally
|
{
|
backendLock.writeLock().unlock();
|
}
|
}
|
|
|
|
/**
|
* Removes the specified entry and any subordinates that it may have from
|
* the backend. This method assumes that the caller holds the backend write
|
* lock.
|
*
|
* @param entryDN The DN of the entry to remove, along with all of its
|
* subordinate entries.
|
*/
|
private void subtreeDelete(DN entryDN)
|
{
|
entryMap.remove(entryDN);
|
Set<DN> childDNSet = childDNs.remove(entryDN);
|
if (childDNSet != null)
|
{
|
for (DN childDN : childDNSet)
|
{
|
subtreeDelete(childDN);
|
}
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void replaceEntry(Entry oldEntry, Entry newEntry,
|
ModifyOperation modifyOperation) throws DirectoryException
|
{
|
backendLock.writeLock().lock();
|
|
try
|
{
|
// Make sure that the target entry exists. If not, then fail.
|
DN entryDN = newEntry.getName();
|
if (! entryMap.containsKey(entryDN))
|
{
|
DN matchedDN = null;
|
DN parentDN = entryDN.getParentDNInSuffix();
|
while (parentDN != null)
|
{
|
if (entryMap.containsKey(parentDN))
|
{
|
matchedDN = parentDN;
|
break;
|
}
|
|
parentDN = parentDN.getParentDNInSuffix();
|
}
|
|
LocalizableMessage m = ERR_LDIF_BACKEND_MODIFY_NO_SUCH_ENTRY.get(entryDN);
|
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
|
}
|
|
entryMap.put(entryDN, newEntry.duplicate(false));
|
writeLDIF();
|
return;
|
}
|
finally
|
{
|
backendLock.writeLock().unlock();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void renameEntry(DN currentDN, Entry entry,
|
ModifyDNOperation modifyDNOperation)
|
throws DirectoryException
|
{
|
backendLock.writeLock().lock();
|
|
try
|
{
|
// Make sure that the original entry exists and that the new entry doesn't
|
// exist but its parent does.
|
DN newDN = entry.getName();
|
if (! entryMap.containsKey(currentDN))
|
{
|
DN matchedDN = null;
|
DN parentDN = currentDN.getParentDNInSuffix();
|
while (parentDN != null)
|
{
|
if (entryMap.containsKey(parentDN))
|
{
|
matchedDN = parentDN;
|
break;
|
}
|
|
parentDN = parentDN.getParentDNInSuffix();
|
}
|
|
LocalizableMessage m = ERR_LDIF_BACKEND_MODDN_NO_SUCH_SOURCE_ENTRY.get(currentDN);
|
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
|
}
|
|
if (entryMap.containsKey(newDN))
|
{
|
LocalizableMessage m = ERR_LDIF_BACKEND_MODDN_TARGET_ENTRY_ALREADY_EXISTS.get(newDN);
|
throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, m);
|
}
|
|
DN newParentDN = newDN.getParentDNInSuffix();
|
if (! entryMap.containsKey(newParentDN))
|
{
|
throw new DirectoryException(ResultCode.NO_SUCH_OBJECT,
|
ERR_LDIF_BACKEND_MODDN_NEW_PARENT_DOESNT_EXIST.get(newParentDN));
|
}
|
|
// Remove the entry from the list of children for the old parent and
|
// add the new entry DN to the set of children for the new parent.
|
DN oldParentDN = currentDN.getParentDNInSuffix();
|
Set<DN> parentChildDNs = childDNs.get(oldParentDN);
|
if (parentChildDNs != null)
|
{
|
parentChildDNs.remove(currentDN);
|
if (parentChildDNs.isEmpty()
|
&& modifyDNOperation.getNewSuperior() != null)
|
{
|
childDNs.remove(oldParentDN);
|
}
|
}
|
|
parentChildDNs = childDNs.get(newParentDN);
|
if (parentChildDNs == null)
|
{
|
parentChildDNs = new HashSet<>();
|
childDNs.put(newParentDN, parentChildDNs);
|
}
|
parentChildDNs.add(newDN);
|
|
|
// If the entry has children, then we'll need to work on the whole
|
// subtree. Otherwise, just work on the target entry.
|
Set<DN> childDNSet = childDNs.remove(currentDN);
|
entryMap.remove(currentDN);
|
entryMap.put(newDN, entry.duplicate(false));
|
if (childDNSet != null && !childDNSet.isEmpty())
|
{
|
for (DN childDN : childDNSet)
|
{
|
subtreeRename(childDN, newDN);
|
}
|
}
|
writeLDIF();
|
}
|
finally
|
{
|
backendLock.writeLock().unlock();
|
}
|
}
|
|
|
|
/**
|
* Moves the specified entry and all of its children so that they are
|
* appropriately placed below the given new parent DN. This method assumes
|
* that the caller holds the backend write lock.
|
*
|
* @param entryDN The DN of the entry to move/rename.
|
* @param newParentDN The DN of the new parent under which the entry should
|
* be placed.
|
*/
|
private void subtreeRename(DN entryDN, DN newParentDN)
|
{
|
Set<DN> childDNSet = childDNs.remove(entryDN);
|
DN newEntryDN = new DN(entryDN.rdn(), newParentDN);
|
|
Entry oldEntry = entryMap.remove(entryDN);
|
if (oldEntry == null)
|
{
|
// This should never happen.
|
if (logger.isTraceEnabled())
|
{
|
logger.trace("Subtree rename encountered entry DN " +
|
entryDN + " for nonexistent entry.");
|
}
|
return;
|
}
|
|
Entry newEntry = oldEntry.duplicate(false);
|
newEntry.setDN(newEntryDN);
|
entryMap.put(newEntryDN, newEntry);
|
|
Set<DN> parentChildren = childDNs.get(newParentDN);
|
if (parentChildren == null)
|
{
|
parentChildren = new HashSet<>();
|
childDNs.put(newParentDN, parentChildren);
|
}
|
parentChildren.add(newEntryDN);
|
|
if (childDNSet != null)
|
{
|
for (DN childDN : childDNSet)
|
{
|
subtreeRename(childDN, newEntryDN);
|
}
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void search(SearchOperation searchOperation)
|
throws DirectoryException
|
{
|
backendLock.readLock().lock();
|
|
try
|
{
|
// Get the base DN, scope, and filter for the search.
|
DN baseDN = searchOperation.getBaseDN();
|
SearchScope scope = searchOperation.getScope();
|
SearchFilter filter = searchOperation.getFilter();
|
|
|
// Make sure the base entry exists if it's supposed to be in this backend.
|
Entry baseEntry = entryMap.get(baseDN);
|
if (baseEntry == null && handlesEntry(baseDN))
|
{
|
DN matchedDN = baseDN.getParentDNInSuffix();
|
while (matchedDN != null)
|
{
|
if (entryMap.containsKey(matchedDN))
|
{
|
break;
|
}
|
|
matchedDN = matchedDN.getParentDNInSuffix();
|
}
|
|
LocalizableMessage m = ERR_LDIF_BACKEND_SEARCH_NO_SUCH_BASE.get(baseDN);
|
throw new DirectoryException(
|
ResultCode.NO_SUCH_OBJECT, m, matchedDN, null);
|
}
|
|
if (baseEntry != null)
|
{
|
baseEntry = baseEntry.duplicate(true);
|
}
|
|
// If it's a base-level search, then just get that entry and return it if
|
// it matches the filter.
|
if (scope == SearchScope.BASE_OBJECT)
|
{
|
if (filter.matchesEntry(baseEntry))
|
{
|
searchOperation.returnEntry(baseEntry, new LinkedList<Control>());
|
}
|
}
|
else
|
{
|
// Walk through all entries and send the ones that match.
|
for (Entry e : entryMap.values())
|
{
|
e = e.duplicate(true);
|
if (e.matchesBaseAndScope(baseDN, scope) && filter.matchesEntry(e))
|
{
|
searchOperation.returnEntry(e, new LinkedList<Control>());
|
}
|
}
|
}
|
}
|
finally
|
{
|
backendLock.readLock().unlock();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public Set<String> getSupportedControls()
|
{
|
return supportedControls;
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public Set<String> getSupportedFeatures()
|
{
|
return Collections.emptySet();
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public boolean supports(BackendOperation backendOperation)
|
{
|
switch (backendOperation)
|
{
|
case LDIF_EXPORT:
|
case LDIF_IMPORT:
|
return true;
|
|
default:
|
return false;
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void exportLDIF(LDIFExportConfig exportConfig)
|
throws DirectoryException
|
{
|
backendLock.readLock().lock();
|
|
try
|
{
|
// Create the LDIF writer.
|
LDIFWriter ldifWriter;
|
try
|
{
|
ldifWriter = new LDIFWriter(exportConfig);
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
LocalizableMessage m = ERR_LDIF_BACKEND_CANNOT_CREATE_LDIF_WRITER.get(
|
stackTraceToSingleLineString(e));
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
m, e);
|
}
|
|
|
// Walk through all the entries and write them to LDIF.
|
DN entryDN = null;
|
try
|
{
|
for (Entry entry : entryMap.values())
|
{
|
entryDN = entry.getName();
|
ldifWriter.writeEntry(entry);
|
}
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage m = ERR_LDIF_BACKEND_CANNOT_WRITE_ENTRY_TO_LDIF.get(
|
entryDN, stackTraceToSingleLineString(e));
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
m, e);
|
}
|
finally
|
{
|
StaticUtils.close(ldifWriter);
|
}
|
}
|
finally
|
{
|
backendLock.readLock().unlock();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext serverContext)
|
throws DirectoryException
|
{
|
return importLDIF(importConfig, true);
|
}
|
|
/**
|
* Processes an LDIF import operation, optionally writing the resulting LDIF
|
* to disk.
|
*
|
* @param importConfig The LDIF import configuration.
|
* @param writeLDIF Indicates whether the LDIF backing file for this
|
* backend should be updated when the import is
|
* complete. This should only be {@code false} when
|
* reading the LDIF as the backend is coming online.
|
*/
|
private LDIFImportResult importLDIF(LDIFImportConfig importConfig,
|
boolean writeLDIF)
|
throws DirectoryException
|
{
|
backendLock.writeLock().lock();
|
|
try
|
{
|
LDIFReader reader;
|
try
|
{
|
reader = new LDIFReader(importConfig);
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage m = ERR_LDIF_BACKEND_CANNOT_CREATE_LDIF_READER.get(
|
stackTraceToSingleLineString(e));
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
m, e);
|
}
|
|
entryMap.clear();
|
childDNs.clear();
|
|
|
try
|
{
|
while (true)
|
{
|
Entry e = null;
|
try
|
{
|
e = reader.readEntry();
|
if (e == null)
|
{
|
break;
|
}
|
}
|
catch (LDIFException le)
|
{
|
if (! le.canContinueReading())
|
{
|
LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_READING_LDIF.get(
|
stackTraceToSingleLineString(le));
|
throw new DirectoryException(
|
DirectoryServer.getServerErrorResultCode(), m, le);
|
}
|
else
|
{
|
continue;
|
}
|
}
|
|
// Make sure that we don't already have an entry with the same DN. If
|
// a duplicate is encountered, then log a message and continue.
|
DN entryDN = e.getName();
|
if (entryMap.containsKey(entryDN))
|
{
|
LocalizableMessage m =
|
ERR_LDIF_BACKEND_DUPLICATE_ENTRY.get(ldifFilePath, currentConfig.dn(), entryDN);
|
logger.error(m);
|
reader.rejectLastEntry(m);
|
continue;
|
}
|
|
|
// If the entry DN is a base DN, then add it with no more processing.
|
if (baseDNSet.contains(entryDN))
|
{
|
entryMap.put(entryDN, e);
|
continue;
|
}
|
|
|
// Make sure that the parent exists. If not, then reject the entry.
|
boolean isBelowBaseDN = false;
|
for (DN baseDN : baseDNs)
|
{
|
if (baseDN.isAncestorOf(entryDN))
|
{
|
isBelowBaseDN = true;
|
break;
|
}
|
}
|
|
if (! isBelowBaseDN)
|
{
|
LocalizableMessage m = ERR_LDIF_BACKEND_ENTRY_OUT_OF_SCOPE.get(
|
ldifFilePath, currentConfig.dn(), entryDN);
|
logger.error(m);
|
reader.rejectLastEntry(m);
|
continue;
|
}
|
|
DN parentDN = entryDN.getParentDNInSuffix();
|
if (parentDN == null || !entryMap.containsKey(parentDN))
|
{
|
LocalizableMessage m = ERR_LDIF_BACKEND_MISSING_PARENT.get(
|
ldifFilePath, currentConfig.dn(), entryDN);
|
logger.error(m);
|
reader.rejectLastEntry(m);
|
continue;
|
}
|
|
|
// The entry does not exist but its parent does, so add it and update
|
// the set of children for the parent.
|
entryMap.put(entryDN, e);
|
|
Set<DN> childDNSet = childDNs.get(parentDN);
|
if (childDNSet == null)
|
{
|
childDNSet = new HashSet<>();
|
childDNs.put(parentDN, childDNSet);
|
}
|
|
childDNSet.add(entryDN);
|
}
|
|
|
if (writeLDIF)
|
{
|
writeLDIF();
|
}
|
|
return new LDIFImportResult(reader.getEntriesRead(),
|
reader.getEntriesRejected(),
|
reader.getEntriesIgnored());
|
}
|
catch (DirectoryException de)
|
{
|
throw de;
|
}
|
catch (Exception e)
|
{
|
LocalizableMessage m = ERR_LDIF_BACKEND_ERROR_READING_LDIF.get(
|
stackTraceToSingleLineString(e));
|
throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
|
m, e);
|
}
|
finally
|
{
|
StaticUtils.close(reader);
|
}
|
}
|
finally
|
{
|
backendLock.writeLock().unlock();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void createBackup(BackupConfig backupConfig)
|
throws DirectoryException
|
{
|
LocalizableMessage message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get();
|
throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void removeBackup(BackupDirectory backupDirectory, String backupID)
|
throws DirectoryException
|
{
|
LocalizableMessage message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get();
|
throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void restoreBackup(RestoreConfig restoreConfig)
|
throws DirectoryException
|
{
|
LocalizableMessage message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get();
|
throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public void configureBackend(LDIFBackendCfg config, ServerContext serverContext) throws ConfigException
|
{
|
if (config != null)
|
{
|
currentConfig = config;
|
currentConfig.addLDIFChangeListener(this);
|
|
baseDNs = new DN[currentConfig.getBaseDN().size()];
|
currentConfig.getBaseDN().toArray(baseDNs);
|
if (baseDNs.length != 1)
|
{
|
throw new ConfigException(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(currentConfig.dn()));
|
}
|
|
baseDNSet = new HashSet<>();
|
Collections.addAll(baseDNSet, baseDNs);
|
|
ldifFilePath = currentConfig.getLDIFFile();
|
}
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public boolean isConfigurationChangeAcceptable(LDIFBackendCfg configuration,
|
List<LocalizableMessage> unacceptableReasons)
|
{
|
boolean configAcceptable = true;
|
|
// Make sure that there is only a single base DN.
|
if (configuration.getBaseDN().size() != 1)
|
{
|
unacceptableReasons.add(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get(configuration.dn()));
|
configAcceptable = false;
|
}
|
|
return configAcceptable;
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public ConfigChangeResult applyConfigurationChange(
|
LDIFBackendCfg configuration)
|
{
|
// We don't actually need to do anything in response to this. However, if
|
// the base DNs or LDIF file are different from what we're currently using
|
// then indicate that admin action is required.
|
final ConfigChangeResult ccr = new ConfigChangeResult();
|
|
if (ldifFilePath != null)
|
{
|
File currentLDIF = getFileForPath(ldifFilePath);
|
File newLDIF = getFileForPath(configuration.getLDIFFile());
|
if (! currentLDIF.equals(newLDIF))
|
{
|
ccr.addMessage(INFO_LDIF_BACKEND_LDIF_FILE_CHANGED.get());
|
ccr.setAdminActionRequired(true);
|
}
|
}
|
|
if (baseDNSet != null && !baseDNSet.equals(configuration.getBaseDN()))
|
{
|
ccr.addMessage(INFO_LDIF_BACKEND_BASE_DN_CHANGED.get());
|
ccr.setAdminActionRequired(true);
|
}
|
|
currentConfig = configuration;
|
return ccr;
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public DN getComponentEntryDN()
|
{
|
return currentConfig.dn();
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public String getClassName()
|
{
|
return LDIFBackend.class.getName();
|
}
|
|
/** {@inheritDoc} */
|
@Override
|
public Map<String,String> getAlerts()
|
{
|
Map<String,String> alerts = new LinkedHashMap<>();
|
alerts.put(ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE,
|
ALERT_DESCRIPTION_LDIF_BACKEND_CANNOT_WRITE_UPDATE);
|
return alerts;
|
}
|
}
|