| opends/resource/schema/02-config.ldif | ●●●●● patch | view | raw | blame | history | |
| opends/src/admin/defn/org/opends/server/admin/std/LDIFBackendConfiguration.xml | ●●●●● patch | view | raw | blame | history | |
| opends/src/messages/messages/backend.properties | ●●●●● patch | view | raw | blame | history | |
| opends/src/server/org/opends/server/backends/LDIFBackend.java | ●●●●● patch | view | raw | blame | history | |
| opends/src/server/org/opends/server/types/DN.java | ●●●●● patch | view | raw | blame | history | |
| opends/src/server/org/opends/server/util/ServerConstants.java | ●●●●● patch | view | raw | blame | history | |
| opends/tests/unit-tests-testng/resource/config-changes.ldif | ●●●●● patch | view | raw | blame | history | |
| opends/tests/unit-tests-testng/src/server/org/opends/server/backends/LDIFBackendTestCase.java | ●●●●● patch | view | raw | blame | history |
opends/resource/schema/02-config.ldif
@@ -1678,6 +1678,9 @@ NAME 'ds-cfg-invoke-for-internal-operations' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' ) attributeTypes: ( 1.3.6.1.4.1.26027.1.1.496 NAME 'ds-cfg-ldif-file' SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE X-ORIGIN 'OpenDS Directory Server' ) objectClasses: ( 1.3.6.1.4.1.26027.1.2.1 NAME 'ds-cfg-access-control-handler' SUP top STRUCTURAL MUST ( cn $ ds-cfg-acl-handler-class $ ds-cfg-acl-handler-enabled ) @@ -2512,4 +2515,7 @@ objectClasses: ( 1.3.6.1.4.1.26027.1.2.174 NAME 'ds-cfg-memory-usage-monitor-provider' SUP ds-cfg-monitor-provider STRUCTURAL X-ORIGIN 'OpenDS Directory Server' ) objectClasses: ( 1.3.6.1.4.1.26027.1.2.175 NAME 'ds-cfg-ldif-backend' SUP ds-cfg-backend STRUCTURAL MUST ds-cfg-ldif-file X-ORIGIN 'OpenDS Directory Server' ) opends/src/admin/defn/org/opends/server/admin/std/LDIFBackendConfiguration.xml
New file @@ -0,0 +1,84 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- ! 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 ! trunk/opends/resource/legal-notices/OpenDS.LICENSE ! or https://OpenDS.dev.java.net/OpenDS.LICENSE. ! 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 ! trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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 ! ! ! Portions Copyright 2007 Sun Microsystems, Inc. ! --> <adm:managed-object name="ldif-backend" plural-name="ldif-backends" package="org.opends.server.admin.std" extends="backend" xmlns:adm="http://www.opends.org/admin" xmlns:ldap="http://www.opends.org/admin-ldap"> <adm:synopsis> The LDIF backend provides a mechanism for interacting with data stored in an LDIF file. All basic LDAP operations are supported in the LDIF backend, although it has minimal support for custom controls. </adm:synopsis> <adm:profile name="ldap"> <ldap:object-class> <ldap:oid>1.3.6.1.4.1.26027.1.2.175</ldap:oid> <ldap:name>ds-cfg-ldif-backend</ldap:name> <ldap:superior>ds-cfg-backend</ldap:superior> </ldap:object-class> </adm:profile> <adm:property-override name="backend-class"> <adm:requires-admin-action> <adm:component-restart /> </adm:requires-admin-action> <adm:default-behavior> <adm:defined> <adm:value> org.opends.server.backends.LDIFBackend </adm:value> </adm:defined> </adm:default-behavior> </adm:property-override> <adm:property name="ldif-file" mandatory="true" multi-valued="false"> <adm:synopsis> This specifies the path to the LDIF file containing the data for this backend. </adm:synopsis> <adm:requires-admin-action> <adm:component-restart /> </adm:requires-admin-action> <adm:syntax> <adm:string /> </adm:syntax> <adm:profile name="ldap"> <ldap:attribute> <ldap:oid>1.3.6.1.4.1.26027.1.1.496</ldap:oid> <ldap:name>ds-cfg-ldif-file</ldap:name> </ldap:attribute> </adm:profile> </adm:property> </adm:managed-object> opends/src/messages/messages/backend.properties
@@ -964,3 +964,69 @@ certificate %s from the trust store file %s: %s SEVERE_ERR_TRUSTSTORE_CERTIFICATE_NOT_FOUND_338=Unable to retrieve entry %s \ from the trust store backend because the certificate %s does not exist SEVERE_ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS_339=The LDIF backend defined in \ configuration entry %s only supports a single base DN, but was configured \ for use with multiple base DNs SEVERE_ERR_LDIF_BACKEND_ERROR_OPENING_FILE_340=An error occurred while \ attempting to open LDIF file %s for use by the LDIF backend defined in \ configuration entry %s: %s SEVERE_ERR_LDIF_BACKEND_ERROR_READING_ENTRY_341=An error occurred while \ attempting to read data from LDIF file %s into the LDIF backend defined in \ configuration entry %s: %s MILD_ERR_LDIF_BACKEND_DUPLICATE_ENTRY_342=LDIF file %s configured for use \ with the LDIF backend defined in configuration entry %s has multiple entries \ with a DN of %s MILD_ERR_LDIF_BACKEND_ENTRY_OUT_OF_SCOPE_343=LDIF file %s configured for use \ with the LDIF backend defined in configuration entry %s includes entry %s \ which is not below the base DN defined for that backend MILD_ERR_LDIF_BACKEND_MISSING_PARENT_344=LDIF file %s configured for use with \ the LDIF backend defined in configuration entry %s contains entry %s but \ its parent entry has not yet been read SEVERE_ERR_LDIF_BACKEND_ERROR_CREATING_FILE_345=An error occurred while \ trying to create file %s to write an updated version of the data for the \ LDIF backend defined in configuration entry %s: %s SEVERE_ERR_LDIF_BACKEND_ERROR_WRITING_FILE_346=An error occurred while \ trying to write updated data to file %s for the LDIF backend defined in \ configuration entry %s: %s SEVERE_ERR_LDIF_BACKEND_ERROR_RENAMING_FILE_347=An error occurred while \ attempting to rename file %s to %s while writing updated data for the LDIF \ backend defined in configuration entry %s: %s MILD_ERR_LDIF_BACKEND_ADD_ALREADY_EXISTS_348=Entry %s already exists in the \ LDIF backend MILD_ERR_LDIF_BACKEND_ADD_MISSING_PARENT_349=The parent for entry %s does not \ exist MILD_ERR_LDIF_BACKEND_DELETE_NO_SUCH_ENTRY_350=Entry %s does not exist MILD_ERR_LDIF_BACKEND_DELETE_NONLEAF_351=Entry %s has one or more subordinate \ entries and cannot be deleted until all of its subordinate entries are \ removed first MILD_ERR_LDIF_BACKEND_MODIFY_NO_SUCH_ENTRY_352=Entry %s does not exist MILD_ERR_LDIF_BACKEND_MODDN_NO_SUCH_SOURCE_ENTRY_353=Source entry %s does not \ exist MILD_ERR_LDIF_BACKEND_MODDN_TARGET_ENTRY_ALREADY_EXISTS_354=Target entry %s \ already exists MILD_ERR_LDIF_BACKEND_MODDN_NEW_PARENT_DOESNT_EXIST_355=The new parent DN %s \ does not exist MILD_ERR_LDIF_BACKEND_SEARCH_NO_SUCH_BASE_356=Entry %s specified as the \ search base DN does not exist SEVERE_ERR_LDIF_BACKEND_CANNOT_CREATE_LDIF_WRITER_357=An error occurred while \ trying to create the writer for the LDIF export operation: %s SEVERE_ERR_LDIF_BACKEND_CANNOT_WRITE_ENTRY_TO_LDIF_358=An error occurred \ while trying to write entry %s during the LDIF export: %s SEVERE_ERR_LDIF_BACKEND_CANNOT_CREATE_LDIF_READER_359=An error occurred while \ trying to create the reader for the LDIF import operation: %s SEVERE_ERR_LDIF_BACKEND_ERROR_READING_LDIF_360=An unrecoverable error \ occurred while attempting to read data from the import file: %s. The LDIF \ import cannot continue MILD_ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED_361=The LDIF backend \ currently does not provide a backup or restore mechanism. Use LDIF import \ and export operations instead MILD_ERR_LDIF_BACKEND_LDIF_DOESNT_EXIST_362=LDIF file %s referenced in LDIF \ backend configuration entry %s does not exist INFO_LDIF_BACKEND_LDIF_FILE_CHANGED_363=The change to the LDIF file path \ will not take effect until the backend is disabled and re-enabled INFO_LDIF_BACKEND_BASE_DN_CHANGED_364=The change to the LDIF backend base DN \ will not take effect until the backend is disabled and re-enabled MILD_ERR_LDIF_BACKEND_HAS_SUBORDINATES_NO_SUCH_ENTRY_365=The target entry %s \ does not exist MILD_ERR_LDIF_BACKEND_NUM_SUBORDINATES_NO_SUCH_ENTRY_366=The target entry %s \ does not exist opends/src/server/org/opends/server/backends/LDIFBackend.java
New file @@ -0,0 +1,1525 @@ /* * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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 * * * Portions Copyright 2007 Sun Microsystems, Inc. */ package org.opends.server.backends; import java.io.File; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.opends.messages.Message; import org.opends.server.admin.Configuration; 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.config.ConfigException; import org.opends.server.core.AddOperation; import org.opends.server.core.DeleteOperation; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyOperation; import org.opends.server.core.ModifyDNOperation; import org.opends.server.core.SearchOperation; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.types.BackupConfig; import org.opends.server.types.BackupDirectory; import org.opends.server.types.ConditionResult; import org.opends.server.types.ConfigChangeResult; import org.opends.server.types.Control; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.ExistingFileBehavior; import org.opends.server.types.InitializationException; import org.opends.server.types.LDIFExportConfig; import org.opends.server.types.LDIFImportConfig; import org.opends.server.types.LDIFImportResult; import org.opends.server.types.RestoreConfig; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchFilter; import org.opends.server.types.SearchScope; import org.opends.server.util.LDIFException; import org.opends.server.util.LDIFReader; import org.opends.server.util.LDIFWriter; import org.opends.server.util.Validator; import static org.opends.messages.BackendMessages.*; import static org.opends.server.loggers.ErrorLogger.*; import static org.opends.server.loggers.debug.DebugLogger.*; import static org.opends.server.util.ServerConstants.*; import static 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 implements ConfigurationChangeListener<LDIFBackendCfg>, AlertGenerator { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); // The base DNs for this backend. private DN[] baseDNs; // The mapping between parent DNs and their immediate children. private HashMap<DN,HashSet<DN>> childDNs; // The base DNs for this backend, in a hash set. private HashSet<DN> baseDNSet; // The set of supported controls for this backend. private HashSet<String> supportedControls; // The set of supported features for this backend. private HashSet<String> supportedFeatures; // The current configuration for this backend. private LDIFBackendCfg currentConfig; // The mapping between entry DNs and the corresponding entries. private LinkedHashMap<DN,Entry> entryMap; // A read-write lock used to protect access to this backend. private ReentrantReadWriteLock backendLock; // 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() { super(); entryMap = new LinkedHashMap<DN,Entry>(); childDNs = new HashMap<DN,HashSet<DN>>(); boolean useFairLocking = DirectoryServer.getEnvironmentConfig().getLockManagerFairOrdering(); backendLock = new ReentrantReadWriteLock(useFairLocking); } /** * {@inheritDoc} */ @Override() public void initializeBackend() 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)) { Message message = ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get( currentConfig.dn().toString()); throw new ConfigException(message); } for (DN dn : baseDNs) { try { DirectoryServer.registerBaseDN(dn, this, false, false); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_BACKEND_CANNOT_REGISTER_BASEDN.get( dn.toString(), 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 (debugEnabled()) { TRACER.debugInfo("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) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message m = ERR_LDIF_BACKEND_ERROR_CREATING_FILE.get( tempFile.getAbsolutePath(), currentConfig.dn().toString(), 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) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } try { writer.close(); } catch (Exception e2) {} Message m = ERR_LDIF_BACKEND_ERROR_WRITING_FILE.get( tempFile.getAbsolutePath(), currentConfig.dn().toString(), stackTraceToSingleLineString(e)); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), m, e); } } try { writer.close(); } catch (Exception e) {} // Rename the existing "live" file out of the way and move the new file // into place. try { if (oldFile.exists()) { oldFile.delete(); } } catch (Exception e) {} try { if (ldifFile.exists()) { ldifFile.renameTo(oldFile); } } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } try { tempFile.renameTo(ldifFile); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message m = ERR_LDIF_BACKEND_ERROR_RENAMING_FILE.get( tempFile.getAbsolutePath(), ldifFile.getAbsolutePath(), currentConfig.dn().toString(), stackTraceToSingleLineString(e)); DirectoryServer.sendAlertNotification(this, ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, m); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), m, e); } } /** * {@inheritDoc} */ @Override() public void finalizeBackend() { backendLock.writeLock().lock(); try { currentConfig.removeLDIFChangeListener(this); DirectoryServer.deregisterAlertGenerator(this); for (DN dn : baseDNs) { try { DirectoryServer.deregisterBaseDN(dn, false); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, 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 isLocal() { return true; } /** * {@inheritDoc} */ @Override() public ConditionResult hasSubordinates(DN entryDN) throws DirectoryException { backendLock.readLock().lock(); try { HashSet<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 { Message m = ERR_LDIF_BACKEND_HAS_SUBORDINATES_NO_SUCH_ENTRY.get( String.valueOf(entryDN)); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m); } } else { return ConditionResult.TRUE; } } finally { backendLock.readLock().unlock(); } } /** * {@inheritDoc} */ @Override() public long numSubordinates(DN entryDN) throws DirectoryException { backendLock.readLock().lock(); try { HashSet<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; } else { Message m = ERR_LDIF_BACKEND_NUM_SUBORDINATES_NO_SUCH_ENTRY.get( String.valueOf(entryDN)); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m); } } else { return childDNSet.size(); } } 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.getDN(); if (entryMap.containsKey(entryDN)) { Message m = ERR_LDIF_BACKEND_ADD_ALREADY_EXISTS.get(entryDN.toString()); 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)); HashSet<DN> childDNSet = childDNs.get(parentDN); if (childDNSet == null) { childDNSet = new HashSet<DN>(); childDNs.put(parentDN, childDNSet); } childDNSet.add(entryDN); writeLDIF(); return; } else { DN matchedDN = null; while (true) { parentDN = parentDN.getParentDNInSuffix(); if (parentDN == null) { break; } if (entryMap.containsKey(parentDN)) { matchedDN = parentDN; break; } } Message m = ERR_LDIF_BACKEND_ADD_MISSING_PARENT.get(entryDN.toString()); 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(); } Message m = ERR_LDIF_BACKEND_DELETE_NO_SUCH_ENTRY.get(entryDN.toString()); 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). HashSet<DN> childDNSet = childDNs.get(entryDN); if ((childDNSet == null) || childDNSet.isEmpty()) { entryMap.remove(entryDN); childDNs.remove(entryDN); if (parentDN != null) { HashSet<DN> parentChildren = childDNs.get(parentDN); if (parentChildren != null) { parentChildren.remove(entryDN); if (parentChildren.isEmpty()) { childDNs.remove(parentDN); } } } writeLDIF(); return; } else { boolean subtreeDelete = false; for (Control c : deleteOperation.getRequestControls()) { if (c.getOID().equals(OID_SUBTREE_DELETE_CONTROL)) { subtreeDelete = true; break; } } if (! subtreeDelete) { Message m = ERR_LDIF_BACKEND_DELETE_NONLEAF.get(entryDN.toString()); throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF, m); } entryMap.remove(entryDN); childDNs.remove(entryDN); if (parentDN != null) { HashSet<DN> parentChildren = childDNs.get(parentDN); if (parentChildren != null) { parentChildren.remove(entryDN); if (parentChildren.isEmpty()) { childDNs.remove(parentDN); } } } for (DN childDN : childDNSet) { subtreeDelete(childDN); } writeLDIF(); return; } } 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); HashSet<DN> childDNSet = childDNs.remove(entryDN); if (childDNSet != null) { for (DN childDN : childDNSet) { subtreeDelete(childDN); } } } /** * {@inheritDoc} */ @Override() public void replaceEntry(Entry entry, ModifyOperation modifyOperation) throws DirectoryException { backendLock.writeLock().lock(); try { // Make sure that the target entry exists. If not, then fail. DN entryDN = entry.getDN(); if (! entryMap.containsKey(entryDN)) { DN matchedDN = null; DN parentDN = entryDN.getParentDNInSuffix(); while (parentDN != null) { if (entryMap.containsKey(parentDN)) { matchedDN = parentDN; break; } parentDN = parentDN.getParentDNInSuffix(); } Message m = ERR_LDIF_BACKEND_MODIFY_NO_SUCH_ENTRY.get(entryDN.toString()); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null); } entryMap.put(entryDN, entry.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.getDN(); if (! entryMap.containsKey(currentDN)) { DN matchedDN = null; DN parentDN = currentDN.getParentDNInSuffix(); while (parentDN != null) { if (entryMap.containsKey(parentDN)) { matchedDN = parentDN; break; } parentDN = parentDN.getParentDNInSuffix(); } Message m = ERR_LDIF_BACKEND_MODDN_NO_SUCH_SOURCE_ENTRY.get( currentDN.toString()); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m, matchedDN, null); } if (entryMap.containsKey(newDN)) { Message m = ERR_LDIF_BACKEND_MODDN_TARGET_ENTRY_ALREADY_EXISTS.get( newDN.toString()); throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, m); } DN parentDN = newDN.getParentDNInSuffix(); if (! entryMap.containsKey(parentDN)) { Message m = ERR_LDIF_BACKEND_MODDN_NEW_PARENT_DOESNT_EXIST.get( String.valueOf(parentDN)); throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, m); } // 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); if ((childDNSet == null) || childDNSet.isEmpty()) { entryMap.remove(currentDN); entryMap.put(newDN, entry.duplicate(false)); writeLDIF(); return; } else { entryMap.remove(currentDN); entryMap.put(newDN, entry.duplicate(false)); for (DN childDN : childDNSet) { subtreeRename(childDN, newDN); } writeLDIF(); return; } } 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.getRDN(), newParentDN); Entry oldEntry = entryMap.remove(entryDN); if (oldEntry == null) { // This should never happen. if (debugEnabled()) { TRACER.debugWarning("Subtree rename encountered entry DN " + entryDN.toString() + " for nonexistent entry."); } return; } Entry newEntry = oldEntry.duplicate(false); newEntry.setDN(newEntryDN); entryMap.put(newEntryDN, newEntry); HashSet<DN> parentChildren = childDNs.get(newParentDN); if (parentChildren == null) { parentChildren = new HashSet<DN>(); 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(); } Message m = ERR_LDIF_BACKEND_SEARCH_NO_SUCH_BASE.get( String.valueOf(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 HashSet<String> getSupportedControls() { return supportedControls; } /** * {@inheritDoc} */ @Override() public HashSet<String> getSupportedFeatures() { return supportedFeatures; } /** * {@inheritDoc} */ @Override() public boolean supportsLDIFExport() { return true; } /** * {@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) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message 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.getDN(); ldifWriter.writeEntry(entry); } } catch (Exception e) { Message m = ERR_LDIF_BACKEND_CANNOT_WRITE_ENTRY_TO_LDIF.get( String.valueOf(entryDN), stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), m, e); } finally { try { ldifWriter.close(); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } } } finally { backendLock.readLock().unlock(); } } /** * {@inheritDoc} */ @Override() public boolean supportsLDIFImport() { return true; } /** * {@inheritDoc} */ @Override() public LDIFImportResult importLDIF(LDIFImportConfig importConfig) 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) { Message 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()) { Message 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.getDN(); if (entryMap.containsKey(entryDN)) { Message m = ERR_LDIF_BACKEND_DUPLICATE_ENTRY.get(ldifFilePath, currentConfig.dn().toString(), entryDN.toString()); logError(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) { Message m = ERR_LDIF_BACKEND_ENTRY_OUT_OF_SCOPE.get(ldifFilePath, currentConfig.dn().toString(), entryDN.toString()); logError(m); reader.rejectLastEntry(m); continue; } DN parentDN = entryDN.getParentDNInSuffix(); if ((parentDN == null) || (! entryMap.containsKey(parentDN))) { Message m = ERR_LDIF_BACKEND_MISSING_PARENT.get(ldifFilePath, currentConfig.dn().toString(), entryDN.toString()); logError(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); HashSet<DN> childDNSet = childDNs.get(parentDN); if (childDNSet == null) { childDNSet = new HashSet<DN>(); 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) { Message m = ERR_LDIF_BACKEND_ERROR_READING_LDIF.get( stackTraceToSingleLineString(e)); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), m, e); } finally { reader.close(); } } finally { backendLock.writeLock().unlock(); } } /** * {@inheritDoc} */ @Override() public boolean supportsBackup() { // This backend does not provide a backup/restore mechanism. return false; } /** * {@inheritDoc} */ @Override() public boolean supportsBackup(BackupConfig backupConfig, StringBuilder unsupportedReason) { // This backend does not provide a backup/restore mechanism. return false; } /** * {@inheritDoc} */ @Override() public void createBackup(BackupConfig backupConfig) throws DirectoryException { Message 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 { Message message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get(); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } /** * {@inheritDoc} */ @Override() public boolean supportsRestore() { // This backend does not provide a backup/restore mechanism. return false; } /** * {@inheritDoc} */ @Override() public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException { Message message = ERR_LDIF_BACKEND_BACKUP_RESTORE_NOT_SUPPORTED.get(); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message); } /** * {@inheritDoc} */ @Override() public void configureBackend(Configuration config) throws ConfigException { if (config != null) { Validator.ensureTrue(config instanceof LDIFBackendCfg); currentConfig = (LDIFBackendCfg) config; currentConfig.addLDIFChangeListener(this); baseDNs = new DN[currentConfig.getBackendBaseDN().size()]; currentConfig.getBackendBaseDN().toArray(baseDNs); if (baseDNs.length != 1) { throw new ConfigException(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get( currentConfig.dn().toString())); } baseDNSet = new HashSet<DN>(); for (DN dn : baseDNs) { baseDNSet.add(dn); } supportedControls = new HashSet<String>(1); supportedControls.add(OID_SUBTREE_DELETE_CONTROL); supportedFeatures = new HashSet<String>(0); ldifFilePath = currentConfig.getLDIFFile(); } } /** * {@inheritDoc} */ public boolean isConfigurationChangeAcceptable(LDIFBackendCfg configuration, List<Message> unacceptableReasons) { boolean configAcceptable = true; // Make sure that there is only a single base DN. if (configuration.getBackendBaseDN().size() != 1) { unacceptableReasons.add(ERR_LDIF_BACKEND_MULTIPLE_BASE_DNS.get( configuration.dn().toString())); configAcceptable = false; } return configAcceptable; } /** * {@inheritDoc} */ 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. boolean adminActionRequired = false; LinkedList<Message> messages = new LinkedList<Message>(); if (ldifFilePath != null) { File currentLDIF = getFileForPath(ldifFilePath); File newLDIF = getFileForPath(configuration.getLDIFFile()); if (! currentLDIF.equals(newLDIF)) { messages.add(INFO_LDIF_BACKEND_LDIF_FILE_CHANGED.get()); adminActionRequired = true; } } if (baseDNSet != null) { if (! baseDNSet.equals(configuration.getBackendBaseDN())) { messages.add(INFO_LDIF_BACKEND_BASE_DN_CHANGED.get()); adminActionRequired = true; } } currentConfig = configuration; return new ConfigChangeResult(ResultCode.SUCCESS, adminActionRequired, messages); } /** * {@inheritDoc} */ public DN getComponentEntryDN() { return currentConfig.dn(); } /** * {@inheritDoc} */ public String getClassName() { return LDIFBackend.class.getName(); } /** * {@inheritDoc} */ public LinkedHashMap<String,String> getAlerts() { LinkedHashMap<String,String> alerts = new LinkedHashMap<String,String>(); alerts.put(ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE, ALERT_DESCRIPTION_LDIF_BACKEND_CANNOT_WRITE_UPDATE); return alerts; } } opends/src/server/org/opends/server/types/DN.java
@@ -41,6 +41,7 @@ import static org.opends.server.config.ConfigConstants.*; import static org.opends.server.loggers.debug.DebugLogger.*; import static org.opends.server.util.StaticUtils.*; import static org.opends.server.util.Validator.*; @@ -91,17 +92,17 @@ // The number of RDN components that comprise this DN. private int numComponents; private final int numComponents; // The set of RDN components that comprise this DN, arranged with // the suffix as the last element. private RDN[] rdnComponents; private final RDN[] rdnComponents; // The string representation of this DN. private String dnString; // The normalized string representation of this DN. private String normalizedDN; private final String normalizedDN; @@ -136,7 +137,7 @@ numComponents = this.rdnComponents.length; dnString = null; normalizedDN = toNormalizedString(); normalizedDN = normalize(this.rdnComponents); } @@ -162,7 +163,37 @@ numComponents = this.rdnComponents.length; dnString = null; normalizedDN = toNormalizedString(); normalizedDN = normalize(this.rdnComponents); } /** * Creates a new DN with the given RDN below the specified parent. * * @param rdn The RDN to use for the new DN. It must not be * {@code null}. * @param parentDN The DN of the entry below which the new DN * should exist. It must not be {@code null}. */ public DN(RDN rdn, DN parentDN) { ensureNotNull(rdn, parentDN); if (parentDN.isNullDN()) { rdnComponents = new RDN[] { rdn }; } else { rdnComponents = new RDN[parentDN.numComponents + 1]; rdnComponents[0] = rdn; System.arraycopy(parentDN.rdnComponents, 0, rdnComponents, 1, parentDN.numComponents); } numComponents = this.rdnComponents.length; dnString = null; normalizedDN = normalize(this.rdnComponents); } @@ -2808,33 +2839,43 @@ /** * Retrieves a normalized representation of the DN with the provided * components. * * @param rdnComponents The RDN components for which to obtain the * normalized string representation. * * @return The normalized string representation of the provided RDN * components. */ private static final String normalize(RDN[] rdnComponents) { if (rdnComponents.length == 0) { return ""; } StringBuilder buffer = new StringBuilder(); rdnComponents[0].toNormalizedString(buffer); for (int i=1; i < rdnComponents.length; i++) { buffer.append(','); rdnComponents[i].toNormalizedString(buffer); } return buffer.toString(); } /** * Retrieves a normalized string representation of this DN. * * @return A normalized string representation of this DN. */ public String toNormalizedString() { if (normalizedDN == null) { if (numComponents == 0) { normalizedDN = ""; } else { StringBuilder buffer = new StringBuilder(); rdnComponents[0].toNormalizedString(buffer); for (int i=1; i < numComponents; i++) { buffer.append(','); rdnComponents[i].toNormalizedString(buffer); } normalizedDN = buffer.toString(); } } return normalizedDN; } opends/src/server/org/opends/server/util/ServerConstants.java
@@ -1676,6 +1676,28 @@ /** * The description for the alert type that will be used for the alert * notification generated when the LDIF backend cannot write an updated LDIF * file. */ public static final String ALERT_DESCRIPTION_LDIF_BACKEND_CANNOT_WRITE_UPDATE = "This alert type will be used to provide notification that an " + "LDIF backend was unable to store an updated copy of the LDIF " + "file after processing a write operation."; /** * The alert type string that will be used for the alert notification * generated when the LDIF backend cannot write an updated LDIF file. */ public static final String ALERT_TYPE_LDIF_BACKEND_CANNOT_WRITE_UPDATE = "org.opends.server.LDIFBackendCannotWriteUupdate"; /** * The description for the alert type that will be used for the alert * notification generated when the LDIF connection handler is unable to * process the contents of a file as valid LDIF. */ opends/tests/unit-tests-testng/resource/config-changes.ldif
@@ -855,3 +855,15 @@ ds-cfg-password-storage-scheme-class: org.opends.server.extensions.RC4PasswordStorageScheme ds-cfg-password-storage-scheme-enabled: true dn: ds-cfg-backend-id=ldifRoot,cn=Backends,cn=config changetype: add objectClass: top objectClass: ds-cfg-backend objectClass: ds-cfg-ldif-backend ds-cfg-backend-id: ldifRoot ds-cfg-backend-enabled: true ds-cfg-backend-class: org.opends.server.backends.LDIFBackend ds-cfg-backend-writability-mode: enabled ds-cfg-backend-base-dn: o=ldif ds-cfg-ldif-file: config/ldif-backend.ldif opends/tests/unit-tests-testng/src/server/org/opends/server/backends/LDIFBackendTestCase.java
New file @@ -0,0 +1,814 @@ /* * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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 * * * Portions Copyright 2007 Sun Microsystems, Inc. */ package org.opends.server.backends; import java.io.File; import java.util.UUID; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.opends.server.TestCaseUtils; import org.opends.server.api.Backend; import org.opends.server.backends.LDIFBackend; import org.opends.server.backends.task.Task; import org.opends.server.backends.task.TaskState; import org.opends.server.core.AddOperation; import org.opends.server.core.CompareOperation; import org.opends.server.core.DeleteOperation; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyDNOperation; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.protocols.internal.InternalSearchOperation; import org.opends.server.tasks.LdifFileWriter; import org.opends.server.tasks.TasksTestCase; import org.opends.server.tools.LDAPModify; import org.opends.server.tools.LDAPSearch; import org.opends.server.tools.LDIFDiff; import org.opends.server.types.ConditionResult; import org.opends.server.types.DirectoryException; import org.opends.server.types.DN; import org.opends.server.types.Entry; import org.opends.server.types.LDIFImportConfig; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchScope; import static org.testng.Assert.*; import static org.opends.server.util.StaticUtils.*; /** * A set of test cases for the LDIF backend. */ public class LDIFBackendTestCase extends BackendTestCase { /** * Ensures that the Directory Server is running and that the LDIF backend * is populated with sample data. * * @throws Exception If an unexpected problem occurs. */ @BeforeClass() public void setUp() throws Exception { TestCaseUtils.startServer(); String templateFilePath = TestCaseUtils.createTempFile( "define suffix=o=ldif", "define numusers=25", "", "branch: [suffix]", "", "branch: ou=People,[suffix]", "subordinateTemplate: person:[numusers]", "", "template: person", "rdnAttr: uid", "objectClass: top", "objectClass: person", "objectClass: organizationalPerson", "objectClass: inetOrgPerson", "givenName: <random:alpha:6>", "sn: <random:alpha:6>", "cn: {givenName} {sn}", "uid: user.<sequential:1>", "userPassword: password"); // Create a temporary test LDIF file. File ldifFile = File.createTempFile("import-test", ".ldif"); String resourcePath = DirectoryServer.getServerRoot() + File.separator + "config" + File.separator + "MakeLDIF"; LdifFileWriter.makeLdif(ldifFile.getPath(), resourcePath, templateFilePath); String taskDN = "ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks"; TestCaseUtils.addEntry( "dn: " + taskDN, "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-backend-id: ldifRoot", "ds-task-import-ldif-file: " + ldifFile.getAbsolutePath()); Task t = TasksTestCase.getCompletedTask(DN.decode(taskDN)); assertNotNull(t); assertEquals(t.getTaskState(), TaskState.COMPLETED_SUCCESSFULLY); } /** * Tests to ensure that add and delete operations (including subtree delete) * work as expected. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testAddAndDelete() throws Exception { // Add a number of entries to the server. int resultCode = TestCaseUtils.applyModifications( "dn: ou=dummy,o=ldif", "changetype: add", "objectClass: top", "objectClass: organizationalUnit", "ou: dummy", "", "dn: ou=sub1,ou=dummy,o=ldif", "changetype: add", "objectClass: top", "objectClass: organizationalUnit", "ou: sub1", "", "dn: ou=sub2,ou=dummy,o=ldif", "changetype: add", "objectClass: top", "objectClass: organizationalUnit", "ou: sub2", "", "dn: ou=sub3,ou=dummy,o=ldif", "changetype: add", "objectClass: top", "objectClass: organizationalUnit", "ou: sub3", "", "dn: ou=sub4,ou=dummy,o=ldif", "changetype: add", "objectClass: top", "objectClass: organizationalUnit", "ou: sub4", "", "dn: ou=sub5,ou=dummy,o=ldif", "changetype: add", "objectClass: top", "objectClass: organizationalUnit", "ou: sub5"); assertEquals(resultCode, 0); // Verify that we can delete a single leaf entry. resultCode = TestCaseUtils.applyModifications( "dn: ou=sub5,ou=dummy,o=ldif", "changetype: delete"); assertEquals(resultCode, 0); // Verify that a default attempt to delete a non-leaf entry will fail. String subtreeDeletePath = TestCaseUtils.createTempFile( "dn: ou=dummy,o=ldif", "changetype: delete"); String[] args = { "-h", "127.0.0.1", "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), "-D", "cn=Directory Manager", "-w", "password", "-f", subtreeDeletePath }; resultCode = LDAPModify.mainModify(args, false, System.out, System.err); assertEquals(resultCode, ResultCode.NOT_ALLOWED_ON_NONLEAF.getIntValue()); // Verify that the subtree delete will succeed if we include the subtree // delete control in the request. args = new String[] { "-h", "127.0.0.1", "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), "-D", "cn=Directory Manager", "-w", "password", "-J", "subtreeDelete", "-f", subtreeDeletePath }; resultCode = LDAPModify.mainModify(args, false, System.out, System.err); assertEquals(resultCode, 0); } /** * Tests an attempt to add an entry to the LDIF backend when an entry with * the same DN already exists. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testAddAlreadyExists() throws Exception { Entry e = TestCaseUtils.makeEntry( "dn: ou=People,o=ldif", "objectClass: top", "objectClass: organizationalUnit", "ou: People"); InternalClientConnection conn = InternalClientConnection.getRootConnection(); AddOperation addOperation = conn.processAdd(e); assertEquals(addOperation.getResultCode(), ResultCode.ENTRY_ALREADY_EXISTS); } /** * Tests an attempt to add an entry to the LDIF backend when the parent for * the new entry doesn't exist. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testAddNoParent() throws Exception { Entry e = TestCaseUtils.makeEntry( "dn: ou=test,ou=doesntexist,o=ldif", "objectClass: top", "objectClass: organizationalUnit", "ou: test"); InternalClientConnection conn = InternalClientConnection.getRootConnection(); AddOperation addOperation = conn.processAdd(e); assertEquals(addOperation.getResultCode(), ResultCode.NO_SUCH_OBJECT); assertEquals(addOperation.getMatchedDN(), DN.decode("o=ldif")); } /** * Tests the ability to add the base entry to the backend. This will first * perform a subtree delete to get rid of everything, then add the base entry, * and then restore the original content. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testAddBaseEntry() throws Exception { assertTrue(DirectoryServer.entryExists(DN.decode("o=ldif"))); assertTrue(DirectoryServer.entryExists( DN.decode("uid=user.1,ou=People,o=ldif"))); String path = TestCaseUtils.createTempFile( "dn: o=ldif", "changetype: delete"); String[] args = { "-h", "127.0.0.1", "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), "-D", "cn=Directory Manager", "-w", "password", "-J", "subtreeDelete", "-f", path }; assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), 0); assertFalse(DirectoryServer.entryExists(DN.decode("o=ldif"))); assertFalse(DirectoryServer.entryExists( DN.decode("uid=user.1,ou=People,o=ldif"))); Entry e = TestCaseUtils.makeEntry( "dn: o=ldif", "objectClass: top", "objectClass: organization", "o: ldif"); InternalClientConnection conn = InternalClientConnection.getRootConnection(); AddOperation addOperation = conn.processAdd(e); assertEquals(addOperation.getResultCode(), ResultCode.SUCCESS); assertTrue(DirectoryServer.entryExists(DN.decode("o=ldif"))); assertFalse(DirectoryServer.entryExists( DN.decode("uid=user.1,ou=People,o=ldif"))); setUp(); assertTrue(DirectoryServer.entryExists(DN.decode("o=ldif"))); assertTrue(DirectoryServer.entryExists( DN.decode("uid=user.1,ou=People,o=ldif"))); } /** * Tests to ensure that we can bind as a user contained in an LDIF backend. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testBind() throws Exception { String[] args = { "-h", "127.0.0.1", "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), "-D", "uid=user.1,ou=People,o=ldif", "-w", "password", "-b", "o=ldif", "-s", "base", "(objectClass=*)" }; assertEquals(LDAPSearch.mainSearch(args, false, System.out, System.err), 0); } /** * Tests to ensure that we can perform a compare against entries in an LDIF * backend. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testCompare() throws Exception { InternalClientConnection conn = InternalClientConnection.getRootConnection(); CompareOperation compareOperation = conn.processCompare("uid=user.1,ou=People,o=ldif", "uid", "user.1"); assertEquals(compareOperation.getResultCode(), ResultCode.COMPARE_TRUE); } /** * Tests to ensure that we can modify entries in the LDIF backend. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testModify() throws Exception { String path = TestCaseUtils.createTempFile( "dn: o=ldif", "changetype: modify", "replace: description", "description: foo"); String[] args = { "-h", "127.0.0.1", "-p", String.valueOf(TestCaseUtils.getServerLdapPort()), "-D", "cn=Directory Manager", "-w", "password", "-f", path }; assertEquals(LDAPModify.mainModify(args, false, System.out, System.err), 0); } /** * Tests a simple modify DN operation that targets a single leaf entry. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testSimpleModifyDN() throws Exception { TestCaseUtils.addEntry( "dn: ou=leaf before,o=ldif", "objectClass: top", "objectClass: organizationalUnit", "ou: leaf before"); DN beforeDN = DN.decode("ou=leaf before,o=ldif"); DN afterDN = DN.decode("ou=leaf after,o=ldif"); assertTrue(DirectoryServer.entryExists(beforeDN)); assertFalse(DirectoryServer.entryExists(afterDN)); InternalClientConnection conn = InternalClientConnection.getRootConnection(); ModifyDNOperation modifyDNOperation = conn.processModifyDN("ou=leaf before,o=ldif", "ou=leaf after", true); assertEquals(modifyDNOperation.getResultCode(), ResultCode.SUCCESS); assertFalse(DirectoryServer.entryExists(beforeDN)); assertTrue(DirectoryServer.entryExists(afterDN)); DeleteOperation deleteOperation = conn.processDelete(afterDN); assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS); assertFalse(DirectoryServer.entryExists(afterDN)); } /** * Tests a modify DN operation in which the target entry already exists. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testModifyDNTargetAlreadyExists() throws Exception { TestCaseUtils.addEntry( "dn: ou=new entry,o=ldif", "objectClass: top", "objectClass: organizationalUnit", "ou: new entry"); assertTrue(DirectoryServer.entryExists(DN.decode("ou=new entry,o=ldif"))); InternalClientConnection conn = InternalClientConnection.getRootConnection(); ModifyDNOperation modifyDNOperation = conn.processModifyDN("ou=new entry,o=ldif", "ou=People", true); assertEquals(modifyDNOperation.getResultCode(), ResultCode.ENTRY_ALREADY_EXISTS); assertTrue(DirectoryServer.entryExists(DN.decode("ou=new entry,o=ldif"))); DeleteOperation deleteOperation = conn.processDelete("ou=new entry,o=ldif"); assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS); assertFalse(DirectoryServer.entryExists(DN.decode("ou=new entry,o=ldif"))); } /** * Tests a modify DN operation that targets a single leaf entry and provides a * new superior DN. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testModifyDNWithNewSuperior() throws Exception { TestCaseUtils.addEntry( "dn: ou=leaf before,o=ldif", "objectClass: top", "objectClass: organizationalUnit", "ou: leaf before"); DN beforeDN = DN.decode("ou=leaf before,o=ldif"); DN afterDN = DN.decode("ou=leaf after,ou=People,o=ldif"); assertTrue(DirectoryServer.entryExists(beforeDN)); assertFalse(DirectoryServer.entryExists(afterDN)); InternalClientConnection conn = InternalClientConnection.getRootConnection(); ModifyDNOperation modifyDNOperation = conn.processModifyDN("ou=leaf before,o=ldif", "ou=leaf after", true, "ou=People,o=ldif"); assertEquals(modifyDNOperation.getResultCode(), ResultCode.SUCCESS); assertFalse(DirectoryServer.entryExists(beforeDN)); assertTrue(DirectoryServer.entryExists(afterDN)); DeleteOperation deleteOperation = conn.processDelete(afterDN); assertEquals(deleteOperation.getResultCode(), ResultCode.SUCCESS); assertFalse(DirectoryServer.entryExists(afterDN)); } /** * Tests a modify DN operation that involves a subtree rename operation. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testModifyDNSubtreeRename() throws Exception { DN beforeDN = DN.decode("ou=People,o=ldif"); DN afterDN = DN.decode("ou=Users,o=ldif"); DN childBeforeDN = DN.decode("uid=user.1,ou=People,o=ldif"); DN childAfterDN = DN.decode("uid=user.1,ou=Users,o=ldif"); assertTrue(DirectoryServer.entryExists(beforeDN)); assertFalse(DirectoryServer.entryExists(afterDN)); assertTrue(DirectoryServer.entryExists(childBeforeDN)); assertFalse(DirectoryServer.entryExists(childAfterDN)); InternalClientConnection conn = InternalClientConnection.getRootConnection(); ModifyDNOperation modifyDNOperation = conn.processModifyDN("ou=People,o=ldif", "ou=Users", true); assertEquals(modifyDNOperation.getResultCode(), ResultCode.SUCCESS); assertFalse(DirectoryServer.entryExists(beforeDN)); assertTrue(DirectoryServer.entryExists(afterDN)); assertFalse(DirectoryServer.entryExists(childBeforeDN)); assertTrue(DirectoryServer.entryExists(childAfterDN)); modifyDNOperation = conn.processModifyDN("ou=Users,o=ldif", "ou=People", true); assertTrue(DirectoryServer.entryExists(beforeDN)); assertFalse(DirectoryServer.entryExists(afterDN)); assertTrue(DirectoryServer.entryExists(childBeforeDN)); assertFalse(DirectoryServer.entryExists(childAfterDN)); } /** * Tests to ensure that a base-level search works as expected. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testBaseSearch() throws Exception { InternalClientConnection conn = InternalClientConnection.getRootConnection(); InternalSearchOperation searchOperation = conn.processSearch("o=ldif", SearchScope.BASE_OBJECT, "(objectClass=*)"); assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS); assertEquals(searchOperation.getSearchEntries().size(), 1); } /** * Tests to ensure that a base-level search works as expected when the filter * doesn't match the target entry. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testBaseSearchNonMatchingFilter() throws Exception { InternalClientConnection conn = InternalClientConnection.getRootConnection(); InternalSearchOperation searchOperation = conn.processSearch("o=ldif", SearchScope.BASE_OBJECT, "(o=not ldif)"); assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS); assertEquals(searchOperation.getSearchEntries().size(), 0); } /** * Tests to ensure that a base-level search works as expected when the target * entry does not exist. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testBaseSearchNoSuchEntry() throws Exception { InternalClientConnection conn = InternalClientConnection.getRootConnection(); InternalSearchOperation searchOperation = conn.processSearch("o=nonexistent2,o=nonexistent1,o=ldif", SearchScope.BASE_OBJECT, "(objectClass=*)"); assertEquals(searchOperation.getResultCode(), ResultCode.NO_SUCH_OBJECT); assertEquals(searchOperation.getMatchedDN(), DN.decode("o=ldif")); } /** * Tests to ensure that a single-level search works as expected. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testSingleLevelSearch() throws Exception { InternalClientConnection conn = InternalClientConnection.getRootConnection(); InternalSearchOperation searchOperation = conn.processSearch("o=ldif", SearchScope.SINGLE_LEVEL, "(objectClass=*)"); assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS); assertEquals(searchOperation.getSearchEntries().size(), 1); } /** * Tests to ensure that subtree search operations work as expected. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testSubtreeSearch() throws Exception { InternalClientConnection conn = InternalClientConnection.getRootConnection(); InternalSearchOperation searchOperation = conn.processSearch("o=ldif", SearchScope.WHOLE_SUBTREE, "(uid=user.1)"); assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS); assertEquals(searchOperation.getSearchEntries().size(), 1); } /** * Tests to ensure that subordinate subtree search operations work as * expected. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testSubordinateSubtreeSearch() throws Exception { InternalClientConnection conn = InternalClientConnection.getRootConnection(); InternalSearchOperation searchOperation = conn.processSearch("o=ldif", SearchScope.SUBORDINATE_SUBTREE, "(uid=user.1)"); assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS); assertEquals(searchOperation.getSearchEntries().size(), 1); } /** * Tests the {@code hasSubordinates} method. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testHasSubordinates() throws Exception { Backend b = DirectoryServer.getBackend("ldifRoot"); assertNotNull(b); assertTrue(b instanceof LDIFBackend); assertEquals(b.hasSubordinates(DN.decode("o=ldif")), ConditionResult.TRUE); assertEquals(b.hasSubordinates(DN.decode("uid=user.1,ou=People,o=ldif")), ConditionResult.FALSE); try { b.hasSubordinates(DN.decode("ou=nonexistent,o=ldif")); fail("Expected an exception when calling hasSubordinates on a " + "non-existent entry"); } catch (DirectoryException de) { assertEquals(de.getResultCode(), ResultCode.NO_SUCH_OBJECT); } } /** * Tests the {@code numSubordinates} method. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testNumSubordinates() throws Exception { Backend b = DirectoryServer.getBackend("ldifRoot"); assertNotNull(b); assertTrue(b instanceof LDIFBackend); assertEquals(b.numSubordinates(DN.decode("o=ldif")), 1); assertEquals(b.numSubordinates(DN.decode("uid=user.1,ou=People,o=ldif")), 0); try { b.numSubordinates(DN.decode("ou=nonexistent,o=ldif")); fail("Expected an exception when calling numSubordinates on a " + "non-existent entry"); } catch (DirectoryException de) { assertEquals(de.getResultCode(), ResultCode.NO_SUCH_OBJECT); } } /** * Tests LDIF export functionality. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testLDIFExport() throws Exception { Backend b = DirectoryServer.getBackend("ldifRoot"); assertNotNull(b); assertTrue(b instanceof LDIFBackend); assertTrue(b.supportsLDIFExport()); String tempFilePath = TestCaseUtils.createTempFile(); String taskDN = "ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks"; TestCaseUtils.addEntry( "dn: " + taskDN, "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-export", "ds-task-class-name: org.opends.server.tasks.ExportTask", "ds-task-export-backend-id: ldifRoot", "ds-task-export-ldif-file: " + tempFilePath); Task t = TasksTestCase.getCompletedTask(DN.decode(taskDN)); assertNotNull(t); assertEquals(t.getTaskState(), TaskState.COMPLETED_SUCCESSFULLY); } /** * Tests a number of miscellaneous backend methods. * * @throws Exception If an unexpected problem occurs. */ @Test() public void testMiscellaneousBackendMethods() throws Exception { Backend b = DirectoryServer.getBackend("ldifRoot"); assertNotNull(b); assertTrue(b instanceof LDIFBackend); assertTrue(b.getEntryCount() > 0); assertTrue(b.isLocal()); assertFalse(b.supportsBackup()); assertFalse(b.supportsBackup(null, null)); try { b.createBackup(null); fail("Expected an exception when calling createBackup"); } catch (DirectoryException de) {} try { b.removeBackup(null, null); fail("Expected an exception when calling removeBackup"); } catch (DirectoryException de) {} assertFalse(b.supportsRestore()); try { b.restoreBackup(null); fail("Expected an exception when calling restoreBackup"); } catch (DirectoryException de) {} LDIFBackend ldifBackend = (LDIFBackend) b; assertNotNull(ldifBackend.getClassName()); assertNotNull(ldifBackend.getAlerts()); assertFalse(ldifBackend.getAlerts().isEmpty()); } }