/* * 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 * * * Copyright 2008-2009 Sun Microsystems, Inc. */ package org.opends.server.backends.ndb; import com.mysql.cluster.ndbj.NdbApiException; import com.mysql.cluster.ndbj.NdbOperation; import java.io.IOException; import org.opends.messages.Message; import java.util.concurrent.atomic.AtomicInteger; import java.util.*; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.concurrent.ConcurrentHashMap; import org.opends.server.api.Backend; import org.opends.server.api.MonitorProvider; import org.opends.server.api.AlertGenerator; 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.util.Validator; import static org.opends.server.util.StaticUtils.*; import static org.opends.messages.BackendMessages.*; import static org.opends.messages.NdbMessages.*; import static org.opends.server.loggers.ErrorLogger.logError; import static org.opends.server.loggers.debug.DebugLogger.*; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.types.*; import static org.opends.server.util.ServerConstants.*; import org.opends.server.admin.Configuration; import org.opends.server.admin.server.ConfigurationChangeListener; import org.opends.server.admin.std.meta.GlobalCfgDefn.WorkflowConfigurationMode; import org.opends.server.admin.std.server.NdbBackendCfg; import org.opends.server.admin.std.server.NdbIndexCfg; import org.opends.server.backends.SchemaBackend; import org.opends.server.backends.ndb.importLDIF.Importer; import org.opends.server.core.Workflow; import org.opends.server.core.WorkflowImpl; import org.opends.server.core.networkgroups.NetworkGroup; import org.opends.server.types.DN; import org.opends.server.util.LDIFException; import org.opends.server.workflowelement.WorkflowElement; import org.opends.server.workflowelement.ndb.NDBWorkflowElement; /** * This is an implementation of a Directory Server Backend which stores * entries in MySQL Cluster NDB database engine. */ public class BackendImpl extends Backend implements ConfigurationChangeListener, AlertGenerator { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** * The fully-qualified name of this class. */ private static final String CLASS_NAME = "org.opends.server.backends.ndb.BackendImpl"; /** * The configuration of this NDB backend. */ private NdbBackendCfg cfg; /** * The root container to use for this backend. */ private RootContainer rootContainer; /** * A count of the total operation threads currently in the backend. */ private AtomicInteger threadTotalCount = new AtomicInteger(0); /** * A count of the write operation threads currently in the backend. */ private AtomicInteger threadWriteCount = new AtomicInteger(0); /** * A list of monitor providers created for this backend instance. */ private ArrayList> monitorProviders = new ArrayList>(); /** * The base DNs defined for this backend instance. */ private DN[] baseDNs; /** * The mysqld connection object. */ private Connection sqlConn; /** * The controls supported by this backend. */ private static HashSet supportedControls; /** * Database name. */ protected static String DATABASE_NAME; /** * Attribute column length. */ private static int ATTRLEN; /** * Attribute column length string. */ private static String ATTRLEN_STRING; /** * NDB Max Row Size known. */ private static final int NDB_MAXROWSIZE = 8052; /** * Number of rDN components supported. */ protected static final int DN2ID_DN_NC = 16; /** * Number of times to retry NDB transaction. */ protected static int TXN_RETRY_LIMIT = 0; /** * DN2ID table. */ protected static final String DN2ID_TABLE = "DS_dn2id"; /** * NEXTID autoincrement table. */ protected static final String NEXTID_TABLE = "DS_nextid"; /** * Operational Attributes table. */ protected static final String OPATTRS_TABLE = "DS_opattrs"; /** * Operational Attributes table. */ protected static final String TAGS_TABLE = "DS_tags"; /** * Index table prefix. */ protected static final String IDX_TABLE_PREFIX = "DS_idx_"; /** * Referrals table. */ protected static final String REFERRALS_TABLE = "referral"; /** * Name prefix for server specific objectclasses. */ private static final String DSOBJ_NAME_PREFIX = "ds-"; /** * EID column name. */ protected static final String EID = "eid"; /** * MID column name. */ protected static final String MID = "mid"; /** * DN column name prefix. */ protected static final String DN2ID_DN = "a"; /** * OC column name. */ protected static final String DN2ID_OC = "object_classes"; /** * Extensible OC column name. */ protected static final String DN2ID_XOC = "x_object_classes"; /** * Attribute column name. */ protected static final String TAG_ATTR = "attr"; /** * Tags column name. */ protected static final String TAG_TAGS = "tags"; /** * Value column name. */ protected static final String IDX_VAL = "value"; /** * Attribute name to lowercase name map. */ protected static Map attrName2LC; /** * Set of blob attribute names. */ protected static Set blobAttributes; /** * List of operational attribute names. */ protected static List operationalAttributes; /** * List of index names. */ protected static List indexes; /** * Attribute name to ObjectClass name/s map. */ protected static Map attr2Oc; /** * Entry DN to Transaction Map to track entry locking. */ protected static Map lockMap; /** * The features supported by this backend. */ private static HashSet supportedFeatures; static { // Set our supported controls. supportedControls = new HashSet(); supportedControls.add(OID_MANAGE_DSAIT_CONTROL); supportedControls.add(OID_SUBTREE_DELETE_CONTROL); attrName2LC = new HashMap(); operationalAttributes = new ArrayList(); blobAttributes = new HashSet(); indexes = new ArrayList(); attr2Oc = new HashMap(); lockMap = new ConcurrentHashMap(); // Set supported features. supportedFeatures = new HashSet(); } /** * Begin a Backend API method that reads the database. */ private void readerBegin() { threadTotalCount.getAndIncrement(); } /** * End a Backend API method that reads the database. */ private void readerEnd() { threadTotalCount.getAndDecrement(); } /** * Begin a Backend API method that writes the database. */ private void writerBegin() { threadTotalCount.getAndIncrement(); threadWriteCount.getAndIncrement(); } /** * End a Backend API method that writes the database. */ private void writerEnd() { threadWriteCount.getAndDecrement(); threadTotalCount.getAndDecrement(); } /** * Wait until there are no more threads accessing the database. It is assumed * that new threads have been prevented from entering the database at the time * this method is called. */ private void waitUntilQuiescent() { while (threadTotalCount.get() > 0) { // Still have threads in the database so sleep a little try { Thread.sleep(500); } catch (InterruptedException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } } } /** * Get suggested minimum upper bound for given attribute type. * @param attrType attribute type * @return suggested upper bound * or 0 if none suggested. */ private int getAttributeBound(AttributeType attrType) { // HACK: This should be done by Directory Server // Schema parser and available in AttributeSyntax. String attrDefinition = attrType.getDefinition(); try { int boundOpenIndex = attrDefinition.indexOf("{"); if (boundOpenIndex == -1) { return 0; } int boundCloseIndex = attrDefinition.indexOf("}"); if (boundCloseIndex == -1) { return 0; } String boundString = attrDefinition.substring( boundOpenIndex + 1, boundCloseIndex); return Integer.parseInt(boundString); } catch (Exception ex) { return 0; } } /** * {@inheritDoc} */ public void configureBackend(Configuration cfg) throws ConfigException { Validator.ensureNotNull(cfg); Validator.ensureTrue(cfg instanceof NdbBackendCfg); this.cfg = (NdbBackendCfg)cfg; Set dnSet = this.cfg.getBaseDN(); baseDNs = new DN[dnSet.size()]; dnSet.toArray(baseDNs); this.DATABASE_NAME = this.cfg.getNdbDbname(); this.ATTRLEN = this.cfg.getNdbAttrLen(); this.ATTRLEN_STRING = Integer.toString(ATTRLEN); this.TXN_RETRY_LIMIT = this.cfg.getDeadlockRetryLimit(); for (String attrName : this.cfg.getNdbAttrBlob()) { this.blobAttributes.add(attrName); } } /** * {@inheritDoc} */ @Override() public void initializeBackend() throws ConfigException, InitializationException { // Checksum this db environment and register its offline state id/checksum. DirectoryServer.registerOfflineBackendStateID(this.getBackendID(), 0); // Load MySQL JDBC driver. try { // The newInstance() call is a work around for some // broken Java implementations Class.forName("com.mysql.jdbc.Driver").newInstance(); } catch (Exception ex) { throw new InitializationException( ERR_NDB_DATABASE_EXCEPTION.get(ex.getMessage())); } // Get MySQL connection. try { sqlConn = DriverManager.getConnection("jdbc:mysql://" + cfg.getSqlConnectString(), cfg.getSqlUser(), cfg.getSqlPasswd()); } catch (SQLException ex) { throw new InitializationException( ERR_NDB_DATABASE_EXCEPTION.get(ex.getMessage())); } // Initialize the database. Statement stmt = null; try { stmt = sqlConn.createStatement(); stmt.execute("CREATE DATABASE IF NOT EXISTS " + DATABASE_NAME); } catch (SQLException ex) { throw new InitializationException( ERR_NDB_DATABASE_EXCEPTION.get("CREATE DATABASE failed: " + ex.getMessage())); } finally { // release resources. if (stmt != null) { try { stmt.close(); } catch (SQLException ex) {} stmt = null; } } try { stmt = sqlConn.createStatement(); stmt.execute("USE " + DATABASE_NAME); } catch (SQLException ex) { throw new InitializationException( ERR_NDB_DATABASE_EXCEPTION.get("USE failed: " + ex.getMessage())); } finally { // release resources. if (stmt != null) { try { stmt.close(); } catch (SQLException ex) {} stmt = null; } } // Log a message indicating that database init // and schema bootstraping are about to start. logError(NOTE_NDB_BOOTSTRAP_SCHEMA.get()); // Initialize dn2id table. try { stmt = sqlConn.createStatement(); stmt.execute("CREATE TABLE IF NOT EXISTS " + DN2ID_TABLE + "(" + "eid bigint unsigned NOT NULL, " + "object_classes VARCHAR(1024) NOT NULL, " + "x_object_classes VARCHAR(1024) NOT NULL, " + "a0 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a1 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a2 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a3 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a4 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a5 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a6 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a7 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a8 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a9 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a10 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a11 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a12 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a13 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a14 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "a15 VARCHAR(" + ATTRLEN_STRING + ") NOT NULL DEFAULT '', " + "PRIMARY KEY (a0, a1, a2, a3, a4, a5, a6, " + "a7, a8, a9, a10, a11, a12, a13, a14, a15), " + "UNIQUE KEY eid (eid)" + ") ENGINE=ndb"); } catch (SQLException ex) { throw new InitializationException( ERR_NDB_DATABASE_EXCEPTION.get("CREATE TABLE " + DN2ID_TABLE + " failed: " + ex.getMessage())); } finally { // release resources. if (stmt != null) { try { stmt.close(); } catch (SQLException ex) {} stmt = null; } } // Initialize nextid autoincrement table. try { stmt = sqlConn.createStatement(); stmt.execute("CREATE TABLE IF NOT EXISTS " + NEXTID_TABLE + "(" + "a bigint unsigned AUTO_INCREMENT PRIMARY KEY" + ") ENGINE=ndb"); } catch (SQLException ex) { throw new InitializationException( ERR_NDB_DATABASE_EXCEPTION.get("CREATE TABLE " + NEXTID_TABLE + " failed: " + ex.getMessage())); } finally { // release resources. if (stmt != null) { try { stmt.close(); } catch (SQLException ex) {} stmt = null; } } // Set schema read only. SchemaBackend schemaBackend = (SchemaBackend) DirectoryServer.getBackend("schema"); if (schemaBackend != null) { schemaBackend.setWritabilityMode(WritabilityMode.DISABLED); } // Set defined attributes. Map attrTypesMap = DirectoryServer.getSchema().getAttributeTypes(); for (AttributeType attrType : attrTypesMap.values()) { String attrName = attrType.getNameOrOID(); attrName2LC.put(attrName, attrName.toLowerCase()); // Skip over server specific object classes. // FIXME: this is not clean. if (attrName.startsWith(DSOBJ_NAME_PREFIX)) { continue; } if (attrType.getUsage() == AttributeUsage.DIRECTORY_OPERATION) { if (operationalAttributes.contains(attrName)) { continue; } operationalAttributes.add(attrName); } } // Strip virtual attributes. for (VirtualAttributeRule rule : DirectoryServer.getVirtualAttributes()) { String attrName = rule.getAttributeType().getNameOrOID(); if (operationalAttributes.contains(attrName)) { operationalAttributes.remove(attrName); } } // Initialize objectClass tables. // TODO: dynamic schema validation and adjustement. Map objectClasses = DirectoryServer.getSchema().getObjectClasses(); Set> ocKeySet = objectClasses.entrySet(); for (Map.Entry ocEntry : ocKeySet) { ObjectClass oc = ocEntry.getValue(); String ocName = oc.getNameOrOID(); if (oc.getObjectClassType() == ObjectClassType.ABSTRACT) { continue; } // Skip over server specific object classes. // FIXME: this is not clean. if (ocName.startsWith(DSOBJ_NAME_PREFIX)) { continue; } int nColumns = 0; StringBuilder attrsBuffer = new StringBuilder(); Set reqAttrs = oc.getRequiredAttributes(); for (AttributeType attrType : reqAttrs) { String attrName = attrType.getNameOrOID(); if (nColumns > 0) { attrsBuffer.append(", "); } attrsBuffer.append("`"); attrsBuffer.append(attrName); attrsBuffer.append("`"); if (blobAttributes.contains(attrName)) { attrsBuffer.append(" BLOB"); } else { attrsBuffer.append(" VARCHAR("); int attrBound = getAttributeBound(attrType); if ((attrBound > 0) && (attrBound < NDB_MAXROWSIZE)) { attrsBuffer.append(Integer.toString(attrBound)); } else { attrsBuffer.append(ATTRLEN_STRING); } attrsBuffer.append(")"); } if (!attr2Oc.containsKey(attrName)) { attr2Oc.put(attrName, ocName); } nColumns++; } Set optAttrs = oc.getOptionalAttributes(); for (AttributeType attrType : optAttrs) { String attrName = attrType.getNameOrOID(); if (nColumns > 0) { attrsBuffer.append(", "); } attrsBuffer.append("`"); attrsBuffer.append(attrName); attrsBuffer.append("`"); if (blobAttributes.contains(attrName)) { attrsBuffer.append(" BLOB"); } else { attrsBuffer.append(" VARCHAR("); int attrBound = getAttributeBound(attrType); if ((attrBound > 0) && (attrBound < NDB_MAXROWSIZE)) { attrsBuffer.append(Integer.toString(attrBound)); } else { attrsBuffer.append(ATTRLEN_STRING); } attrsBuffer.append(")"); } if (!attr2Oc.containsKey(attrName)) { attr2Oc.put(attrName, ocName); } nColumns++; } if (attrsBuffer.toString().length() != 0) { attrsBuffer.append(", PRIMARY KEY(eid, mid))"); } String attrsString = attrsBuffer.toString(); try { stmt = sqlConn.createStatement(); stmt.execute("CREATE TABLE IF NOT EXISTS " + "`" + ocName + "`" + " (" + "eid bigint unsigned NOT NULL, " + "mid int unsigned NOT NULL" + (attrsString.length() != 0 ? ", " + attrsString : ", PRIMARY KEY(eid, mid))") + " ENGINE=ndb PARTITION BY KEY(eid)"); } catch (SQLException ex) { throw new InitializationException( ERR_NDB_DATABASE_EXCEPTION.get("CREATE TABLE " + ocName + " failed: " + ex.getMessage())); } finally { // release resources. if (stmt != null) { try { stmt.close(); } catch (SQLException ex) { } stmt = null; } } } // Initialize operational attributes table. int nColumns = 0; StringBuilder attrsBuffer = new StringBuilder(); for (String attrName : operationalAttributes) { if (nColumns > 0) { attrsBuffer.append(", "); } attrsBuffer.append("`"); attrsBuffer.append(attrName); attrsBuffer.append("`"); attrsBuffer.append(" VARCHAR("); attrsBuffer.append(ATTRLEN_STRING); attrsBuffer.append(")"); nColumns++; } if (attrsBuffer.toString().length() != 0) { attrsBuffer.append(", PRIMARY KEY(eid))"); } String attrsString = attrsBuffer.toString(); try { stmt = sqlConn.createStatement(); stmt.execute("CREATE TABLE IF NOT EXISTS " + "`" + OPATTRS_TABLE + "`" + " (" + "eid bigint unsigned NOT NULL" + (attrsString.length() != 0 ? ", " + attrsString : ", PRIMARY KEY(eid))") + " ENGINE=ndb PARTITION BY KEY(eid)"); } catch (SQLException ex) { throw new InitializationException( ERR_NDB_DATABASE_EXCEPTION.get("CREATE TABLE " + OPATTRS_TABLE + " failed: " + ex.getMessage())); } finally { // release resources. if (stmt != null) { try { stmt.close(); } catch (SQLException ex) { } stmt = null; } } // Initialize attribute options table. try { stmt = sqlConn.createStatement(); stmt.execute("CREATE TABLE IF NOT EXISTS " + TAGS_TABLE + "(" + "eid bigint unsigned NOT NULL, " + "attr VARCHAR(" + ATTRLEN_STRING + "), " + "mid int unsigned NOT NULL, " + "tags VARCHAR(" + ATTRLEN_STRING + "), " + "PRIMARY KEY (eid, attr, mid))" + " ENGINE=ndb PARTITION BY KEY(eid)"); } catch (SQLException ex) { throw new InitializationException( ERR_NDB_DATABASE_EXCEPTION.get("CREATE TABLE " + TAGS_TABLE + " failed: " + ex.getMessage())); } finally { // release resources. if (stmt != null) { try { stmt.close(); } catch (SQLException ex) {} stmt = null; } } // Initialize configured indexes. for (String idx : cfg.listNdbIndexes()) { NdbIndexCfg indexCfg = cfg.getNdbIndex(idx); // TODO: Substring indexes. AttributeType attrType = indexCfg.getAttribute(); String attrName = attrType.getNameOrOID(); indexes.add(attrName); try { stmt = sqlConn.createStatement(); stmt.execute("CREATE TABLE IF NOT EXISTS " + IDX_TABLE_PREFIX + attrName + "(" + "eid bigint unsigned NOT NULL, " + "mid int unsigned NOT NULL, " + "value VARCHAR(" + ATTRLEN_STRING + "), " + "PRIMARY KEY (eid, mid), " + "KEY value (value)" + ") ENGINE=ndb PARTITION BY KEY(eid)"); } catch (SQLException ex) { throw new InitializationException( ERR_NDB_DATABASE_EXCEPTION.get("CREATE TABLE " + IDX_TABLE_PREFIX + attrName + " failed: " + ex.getMessage())); } finally { // release resources. if (stmt != null) { try { stmt.close(); } catch (SQLException ex) { } stmt = null; } } } // Open Root Container. if (rootContainer == null) { rootContainer = initializeRootContainer(); } try { // Log an informational message about the number of entries. Message message = NOTE_NDB_BACKEND_STARTED.get( cfg.getBackendId(), rootContainer.getEntryCount()); logError(message); } catch(NdbApiException ex) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, ex); } Message message = WARN_NDB_GET_ENTRY_COUNT_FAILED.get(ex.getMessage()); throw new InitializationException(message, ex); } WorkflowConfigurationMode workflowConfigMode = DirectoryServer.getWorkflowConfigurationMode(); DirectoryServer.setWorkflowConfigurationMode( WorkflowConfigurationMode.MANUAL); for (DN dn : cfg.getBaseDN()) { try { DirectoryServer.registerBaseDN(dn, this, false); WorkflowImpl workflowImpl = createWorkflow(dn); registerWorkflowWithDefaultNetworkGroup(workflowImpl); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_BACKEND_CANNOT_REGISTER_BASEDN.get( String.valueOf(dn), String.valueOf(e)); throw new InitializationException(message, e); } } DirectoryServer.setWorkflowConfigurationMode(workflowConfigMode); // Register as an AlertGenerator. DirectoryServer.registerAlertGenerator(this); // Register this backend as a change listener. cfg.addNdbChangeListener(this); } /** * {@inheritDoc} */ @Override() public void finalizeBackend() { // Deregister as a change listener. cfg.removeNdbChangeListener(this); WorkflowConfigurationMode workflowConfigMode = DirectoryServer.getWorkflowConfigurationMode(); DirectoryServer.setWorkflowConfigurationMode( WorkflowConfigurationMode.MANUAL); // Deregister our base DNs. for (DN dn : rootContainer.getBaseDNs()) { try { DirectoryServer.deregisterBaseDN(dn); deregisterWorkflowWithDefaultNetworkGroup(dn); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } } DirectoryServer.setWorkflowConfigurationMode(workflowConfigMode); // Deregister our monitor providers. for (MonitorProvider monitor : monitorProviders) { DirectoryServer.deregisterMonitorProvider( monitor.getMonitorInstanceName().toLowerCase()); } monitorProviders = new ArrayList>(); // We presume the server will prevent more operations coming into this // backend, but there may be existing operations already in the // backend. We need to wait for them to finish. waitUntilQuiescent(); // Close the database. try { rootContainer.close(); rootContainer = null; } catch (NdbApiException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_NDB_DATABASE_EXCEPTION.get(e.getMessage()); logError(message); } try { if (sqlConn != null) { sqlConn.close(); } } catch (SQLException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_NDB_DATABASE_EXCEPTION.get(e.getMessage()); logError(message); } // Checksum this db environment and register its offline state id/checksum. DirectoryServer.registerOfflineBackendStateID(this.getBackendID(), 0); //Deregister the alert generator. DirectoryServer.deregisterAlertGenerator(this); // Make sure the thread counts are zero for next initialization. threadTotalCount.set(0); threadWriteCount.set(0); // Log an informational message. Message message = NOTE_BACKEND_OFFLINE.get(cfg.getBackendId()); logError(message); } /** * {@inheritDoc} */ @Override() public boolean isLocal() { return true; } /** * {@inheritDoc} */ @Override() public boolean isIndexed(AttributeType attributeType, IndexType indexType) { // Substring indexing NYI. if (indexType == IndexType.SUBSTRING) { return false; } String attrName = attributeType.getNameOrOID(); if (indexes.contains(attrName)) { return true; } return false; } /** * {@inheritDoc} */ @Override() public boolean supportsLDIFExport() { return true; } /** * {@inheritDoc} */ @Override() public boolean supportsLDIFImport() { return true; } /** * {@inheritDoc} */ @Override() public boolean supportsBackup() { return false; } /** * {@inheritDoc} */ @Override() public boolean supportsBackup(BackupConfig backupConfig, StringBuilder unsupportedReason) { return false; } /** * {@inheritDoc} */ @Override() public boolean supportsRestore() { return false; } /** * {@inheritDoc} */ @Override() public HashSet getSupportedFeatures() { return supportedFeatures; } /** * {@inheritDoc} */ @Override() public HashSet getSupportedControls() { return supportedControls; } /** * {@inheritDoc} */ @Override() public DN[] getBaseDNs() { return baseDNs; } /** * {@inheritDoc} */ @Override() public long getEntryCount() { if (rootContainer != null) { try { return rootContainer.getEntryCount(); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } } return -1; } /** * {@inheritDoc} */ @Override() public ConditionResult hasSubordinates(DN entryDN) throws DirectoryException { long ret = numSubordinates(entryDN, false); if(ret < 0) { return ConditionResult.UNDEFINED; } else if(ret == 0) { return ConditionResult.FALSE; } else { return ConditionResult.TRUE; } } /** * {@inheritDoc} */ @Override() public long numSubordinates(DN entryDN, boolean subtree) throws DirectoryException { // NYI. return -1; } /** * {@inheritDoc} */ @Override() public Entry getEntry(DN entryDN) throws DirectoryException { readerBegin(); EntryContainer ec; if (rootContainer != null) { ec = rootContainer.getEntryContainer(entryDN); } else { Message message = ERR_ROOT_CONTAINER_NOT_INITIALIZED.get(getBackendID()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } ec.sharedLock.lock(); Entry entry; try { entry = ec.getEntry(entryDN); } catch (NdbApiException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw createDirectoryException(e); } catch (NDBException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), e.getMessageObject()); } finally { ec.sharedLock.unlock(); readerEnd(); } return entry; } /** * Retrieves the requested entry from this backend. Note that the * caller must hold a read or write lock on the specified DN. Note * that the lock is held after this method has completed execution. * * @param entryDN The distinguished name of the entry to retrieve. * @param txn Abstarct transaction for this operation. * @param lockMode Lock mode for this operation. * * @return The requested entry, or {@code null} if the entry does * not exist. * * @throws DirectoryException If a problem occurs while trying to * retrieve the entry. */ public Entry getEntryNoCommit(DN entryDN, AbstractTransaction txn, NdbOperation.LockMode lockMode) throws DirectoryException { readerBegin(); EntryContainer ec; if (rootContainer != null) { ec = rootContainer.getEntryContainer(entryDN); } else { Message message = ERR_ROOT_CONTAINER_NOT_INITIALIZED.get(getBackendID()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } ec.sharedLock.lock(); Entry entry; try { entry = ec.getEntryNoCommit(entryDN, txn, lockMode); } catch (NdbApiException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw createDirectoryException(e); } catch (NDBException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), e.getMessageObject()); } finally { ec.sharedLock.unlock(); readerEnd(); } return entry; } /** * {@inheritDoc} */ @Override() public void addEntry(Entry entry, AddOperation addOperation) throws DirectoryException, CanceledOperationException { throw createDirectoryException(new UnsupportedOperationException()); } /** * Adds the provided entry to this backend. This method must ensure * that the entry is appropriate for the backend and that no entry * already exists with the same DN. The caller must hold a write * lock on the DN of the provided entry. * * @param entry The entry to add to this backend. * @param addOperation The add operation with which the new entry * is associated. This may be {@code null} * for adds performed internally. * @param txn Abstract transaction for this operation. * * @throws DirectoryException If a problem occurs while trying to * add the entry. * * @throws CanceledOperationException If this backend noticed and * reacted to a request to * cancel or abandon the add * operation. */ public void addEntry(Entry entry, AddOperation addOperation, AbstractTransaction txn) throws DirectoryException, CanceledOperationException { writerBegin(); DN entryDN = entry.getDN(); EntryContainer ec; if (rootContainer != null) { ec = rootContainer.getEntryContainer(entryDN); } else { Message message = ERR_ROOT_CONTAINER_NOT_INITIALIZED.get(getBackendID()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } ec.sharedLock.lock(); try { ec.addEntry(entry, addOperation, txn); } catch (NdbApiException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw createDirectoryException(e); } catch (NDBException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), e.getMessageObject()); } finally { ec.sharedLock.unlock(); writerEnd(); } } /** * {@inheritDoc} */ @Override() public void deleteEntry(DN entryDN, DeleteOperation deleteOperation) throws DirectoryException, CanceledOperationException { throw createDirectoryException(new UnsupportedOperationException()); } /** * Removes the specified entry from this backend. This method must * ensure that the entry exists and that it does not have any * subordinate entries (unless the backend supports a subtree delete * operation and the client included the appropriate information in * the request). The caller must hold a write lock on the provided * entry DN. * * @param entryDN The DN of the entry to remove from this * backend. * @param entry The entry to delete. * @param deleteOperation The delete operation with which this * action is associated. This may be * {@code null} for deletes performed * internally. * @param txn Abstract transaction for this operation. * * @throws DirectoryException If a problem occurs while trying to * remove the entry. * * @throws CanceledOperationException If this backend noticed and * reacted to a request to * cancel or abandon the * delete operation. */ public void deleteEntry(DN entryDN, Entry entry, DeleteOperation deleteOperation, AbstractTransaction txn) throws DirectoryException, CanceledOperationException { writerBegin(); EntryContainer ec; if (rootContainer != null) { ec = rootContainer.getEntryContainer(entryDN); } else { Message message = ERR_ROOT_CONTAINER_NOT_INITIALIZED.get(getBackendID()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } ec.sharedLock.lock(); try { ec.deleteEntry(entryDN, entry, deleteOperation, txn); } catch (NdbApiException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw createDirectoryException(e); } catch (NDBException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), e.getMessageObject()); } finally { ec.sharedLock.unlock(); writerEnd(); } } /** * {@inheritDoc} */ @Override() public void replaceEntry(Entry oldEntry, Entry newEntry, ModifyOperation modifyOperation) throws DirectoryException, CanceledOperationException { throw createDirectoryException(new UnsupportedOperationException()); } /** * Replaces the specified entry with the provided entry in this * backend. The backend must ensure that an entry already exists * with the same DN as the provided entry. The caller must hold a * write lock on the DN of the provided entry. * * @param oldEntry * The original entry that is being replaced. * @param newEntry * The new entry to use in place of the existing entry with * the same DN. * @param modifyOperation * The modify operation with which this action is * associated. This may be {@code null} for modifications * performed internally. * @param txn * Abstract transaction for this operation. * @throws DirectoryException * If a problem occurs while trying to replace the entry. * @throws CanceledOperationException * If this backend noticed and reacted to a request to * cancel or abandon the modify operation. */ public void replaceEntry(Entry oldEntry, Entry newEntry, ModifyOperation modifyOperation, AbstractTransaction txn) throws DirectoryException, CanceledOperationException { writerBegin(); DN entryDN = newEntry.getDN(); EntryContainer ec; if (rootContainer != null) { ec = rootContainer.getEntryContainer(entryDN); } else { Message message = ERR_ROOT_CONTAINER_NOT_INITIALIZED.get(getBackendID()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } ec.sharedLock.lock(); try { ec.replaceEntry(oldEntry, newEntry, modifyOperation, txn); } catch (NdbApiException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw createDirectoryException(e); } catch (NDBException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), e.getMessageObject()); } finally { ec.sharedLock.unlock(); writerEnd(); } } /** * {@inheritDoc} */ @Override() public void renameEntry(DN currentDN, Entry entry, ModifyDNOperation modifyDNOperation) throws DirectoryException, CanceledOperationException { throw createDirectoryException(new UnsupportedOperationException()); } /** * Moves and/or renames the provided entry in this backend, altering * any subordinate entries as necessary. This must ensure that an * entry already exists with the provided current DN, and that no * entry exists with the target DN of the provided entry. The caller * must hold write locks on both the current DN and the new DN for * the entry. * * @param currentDN * The current DN of the entry to be replaced. * @param entry * The new content to use for the entry. * @param modifyDNOperation * The modify DN operation with which this action is * associated. This may be {@code null} for modify DN * operations performed internally. * @param txn * Abstract transaction for this operation. * @throws DirectoryException * If a problem occurs while trying to perform the rename. * @throws CanceledOperationException * If this backend noticed and reacted to a request to * cancel or abandon the modify DN operation. */ public void renameEntry(DN currentDN, Entry entry, ModifyDNOperation modifyDNOperation, AbstractTransaction txn) throws DirectoryException, CanceledOperationException { writerBegin(); EntryContainer currentContainer; if (rootContainer != null) { currentContainer = rootContainer.getEntryContainer(currentDN); } else { Message message = ERR_ROOT_CONTAINER_NOT_INITIALIZED.get(getBackendID()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } EntryContainer container = rootContainer.getEntryContainer(entry.getDN()); if (currentContainer != container) { // FIXME: No reason why we cannot implement a move between containers // since the containers share the same database environment. Message msg = WARN_NDB_FUNCTION_NOT_SUPPORTED.get(); throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, msg); } try { currentContainer.sharedLock.lock(); currentContainer.renameEntry(currentDN, entry, modifyDNOperation, txn); } catch (NdbApiException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw createDirectoryException(e); } catch (NDBException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), e.getMessageObject()); } finally { currentContainer.sharedLock.unlock(); writerEnd(); } } /** * {@inheritDoc} */ @Override() public void search(SearchOperation searchOperation) throws DirectoryException, CanceledOperationException { // Abort any Group or ACI bulk search operations. List requestControls = searchOperation.getRequestControls(); if (requestControls != null) { for (Control c : requestControls) { if (c.getOID().equals(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE)) { return; } } } EntryContainer ec; if (rootContainer != null) { ec = rootContainer.getEntryContainer(searchOperation.getBaseDN()); } else { Message message = ERR_ROOT_CONTAINER_NOT_INITIALIZED.get(getBackendID()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } ec.sharedLock.lock(); readerBegin(); try { ec.search(searchOperation); } catch (NdbApiException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw createDirectoryException(e); } catch (NDBException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_NDB_DATABASE_EXCEPTION.get(e.getMessage()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } finally { ec.sharedLock.unlock(); readerEnd(); } } /** * {@inheritDoc} */ @Override() public void exportLDIF(LDIFExportConfig exportConfig) throws DirectoryException { if (!DirectoryServer.isRunning()) { // No offline export for now. Message message = ERR_NDB_EXPORT_OFFLINE_NOT_SUPPORTED.get(); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } try { if (rootContainer == null) { rootContainer = initializeRootContainer(); } ExportJob exportJob = new ExportJob(exportConfig); exportJob.exportLDIF(rootContainer); } catch (IOException ioe) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, ioe); } Message message = ERR_NDB_IO_ERROR.get(ioe.getMessage()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } catch (NdbApiException nae) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, nae); } throw createDirectoryException(nae); } catch (LDIFException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), e.getMessageObject()); } catch (InitializationException ie) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, ie); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ie.getMessageObject()); } catch (ConfigException ce) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, ce); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ce.getMessageObject()); } finally { // leave the backend in the same state. if (rootContainer != null) { try { rootContainer.close(); rootContainer = null; } catch (NdbApiException e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } } } } /** * {@inheritDoc} */ @Override() public LDIFImportResult importLDIF(LDIFImportConfig importConfig) throws DirectoryException { if (!DirectoryServer.isRunning()) { // No offline import for now. Message message = ERR_NDB_IMPORT_OFFLINE_NOT_SUPPORTED.get(); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } try { Importer importer = new Importer(importConfig); if (rootContainer == null) { rootContainer = initializeRootContainer(); } return importer.processImport(rootContainer); } catch (IOException ioe) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, ioe); } Message message = ERR_NDB_IO_ERROR.get(ioe.getMessage()); throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message); } catch (NDBException ne) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, ne); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ne.getMessageObject()); } catch (InitializationException ie) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, ie); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ie.getMessageObject()); } catch (ConfigException ce) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, ce); } throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), ce.getMessageObject()); } finally { // leave the backend in the same state. try { if (rootContainer != null) { rootContainer.close(); rootContainer = null; } } catch (NdbApiException nae) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, nae); } } } } /** * {@inheritDoc} */ @Override() public void createBackup(BackupConfig backupConfig) throws DirectoryException { // Not supported, do nothing. } /** * {@inheritDoc} */ @Override() public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException { // Not supported, do nothing. } /** * {@inheritDoc} */ @Override() public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException { // Not supported, do nothing. } /** * {@inheritDoc} */ @Override() public boolean isConfigurationAcceptable(Configuration configuration, List unacceptableReasons) { NdbBackendCfg config = (NdbBackendCfg) configuration; return isConfigurationChangeAcceptable(config, unacceptableReasons); } /** * {@inheritDoc} */ public boolean isConfigurationChangeAcceptable( NdbBackendCfg cfg, List unacceptableReasons) { return true; } /** * {@inheritDoc} */ public ConfigChangeResult applyConfigurationChange(NdbBackendCfg newCfg) { ConfigChangeResult ccr; ResultCode resultCode = ResultCode.SUCCESS; ArrayList messages = new ArrayList(); ccr = new ConfigChangeResult(resultCode, false, messages); return ccr; } /** * Returns a handle to the root container currently used by this backend. * The rootContainer could be NULL if the backend is not initialized. * * @return The RootContainer object currently used by this backend. */ public RootContainer getRootContainer() { return rootContainer; } /** * Creates a customized DirectoryException from the NdbApiException * thrown by NDB backend. * * @param e The NdbApiException to be converted. * @return DirectoryException created from exception. */ DirectoryException createDirectoryException(Exception e) { ResultCode resultCode = DirectoryServer.getServerErrorResultCode(); Message message = null; String jeMessage = e.getMessage(); if (jeMessage == null) { jeMessage = stackTraceToSingleLineString(e); } message = ERR_NDB_DATABASE_EXCEPTION.get(jeMessage); return new DirectoryException(resultCode, message, e); } /** * {@inheritDoc} */ public String getClassName() { return CLASS_NAME; } /** * {@inheritDoc} */ public LinkedHashMap getAlerts() { LinkedHashMap alerts = new LinkedHashMap(); alerts.put(ALERT_TYPE_BACKEND_ENVIRONMENT_UNUSABLE, ALERT_DESCRIPTION_BACKEND_ENVIRONMENT_UNUSABLE); return alerts; } /** * {@inheritDoc} */ public DN getComponentEntryDN() { return cfg.dn(); } private RootContainer initializeRootContainer() throws ConfigException, InitializationException { // Open the database environment try { RootContainer rc = new RootContainer(this, cfg); rc.open(); return rc; } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_NDB_OPEN_ENV_FAIL.get(e.getMessage()); throw new InitializationException(message, e); } } /** * {@inheritDoc} */ public void preloadEntryCache() throws UnsupportedOperationException { throw new UnsupportedOperationException("Operation not supported"); } /** * Creates one workflow for a given base DN in a backend. * * @param baseDN the base DN of the workflow to create * @param backend the backend handled by the workflow * * @return the newly created workflow * * @throws DirectoryException If the workflow ID for the provided * workflow conflicts with the workflow * ID of an existing workflow. */ private WorkflowImpl createWorkflow(DN baseDN) throws DirectoryException { String backendID = this.getBackendID(); // Create a root workflow element to encapsulate the backend NDBWorkflowElement rootWE = NDBWorkflowElement.createAndRegister(backendID, this); // The workflow ID is "backendID + baseDN". // We cannot use backendID as workflow identifier because a backend // may handle several base DNs. We cannot use baseDN either because // we might want to configure several workflows handling the same // baseDN through different network groups. So a mix of both // backendID and baseDN should be ok. String workflowID = backendID + "#" + baseDN.toString(); // Create the worklfow for the base DN and register the workflow with // the server. WorkflowImpl workflowImpl = new WorkflowImpl(workflowID, baseDN, rootWE.getWorkflowElementID(), (WorkflowElement) rootWE); workflowImpl.register(); return workflowImpl; } /** * Registers a workflow with the default network group. * * @param workflowImpl The workflow to register with the * default network group * * @throws DirectoryException If the workflow is already registered with * the default network group */ private void registerWorkflowWithDefaultNetworkGroup( WorkflowImpl workflowImpl ) throws DirectoryException { NetworkGroup defaultNetworkGroup = NetworkGroup.getDefaultNetworkGroup(); defaultNetworkGroup.registerWorkflow(workflowImpl); } /** * Deregisters a workflow with the default network group and * deregisters the workflow with the server. This method is * intended to be called when workflow configuration mode is * auto. * * @param baseDN the DN of the workflow to deregister */ private void deregisterWorkflowWithDefaultNetworkGroup( DN baseDN ) { String backendID = this.getBackendID(); // Get the default network group and deregister all the workflows // being configured for the backend (there is one worklfow per // backend base DN). NetworkGroup defaultNetworkGroup = NetworkGroup.getDefaultNetworkGroup(); Workflow workflow = defaultNetworkGroup.deregisterWorkflow(baseDN); WorkflowImpl workflowImpl = (WorkflowImpl) workflow; // The workflow ID is "backendID + baseDN". // We cannot use backendID as workflow identifier because a backend // may handle several base DNs. We cannot use baseDN either because // we might want to configure several workflows handling the same // baseDN through different network groups. So a mix of both // backendID and baseDN should be ok. String workflowID = backendID + "#" + baseDN.toString(); NDBWorkflowElement.remove(backendID); workflowImpl.deregister(workflowID); } }