# TenantManager.java

    /*
    ** Module   : TenantManager.java
    ** Abstract : Class that handles tenants
    **
    ** Copyright (c) 2024-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ----------------------------------------Description---------------------------------------
    ** 001 RAA 20240423 Created initial version.
    **     RAA 20240424 Added javadoc for getTenant.
    **     RAA 20240426 Fixed order of parameters in changeTenantStatus and string handling in updateTenant.
    **     RAA 20240513 Made updateTenant and deleteTenant use PreparedStatements.
    **     RAA 20240517 Moved private methods towards the end of the class. Added clearParameters call in
    **                  updateTenantImpl.
    **     RAA 20240604 Changed return type of addTenant and changeTenantStatus. Changed to a Map
    **                  parameter in updateTenant function.
    **     RAA 20240607 Added synchronization blocks.
    **     RAA 20240610 Added listTenants function.
    **     RAA 20240611 Added synchronization for initialize. Added init field for checking if initialization
    **                  has been done.
    **     RAA 20240612 Moved assignment of the init flag at the end of initialize method.
    **     RAA 20240613 Added calls for removing the stored database configuration when a tenant is changed.
    **     RAA 20240627 Re-factored multi-tenancy to allow multiple database instances per tenant.
    **     RAA 20240627 Fixed ending bracket in CREATE_DATABASES_TABLE SQL.
    **     RAA 20240627 Added tenant checks in addTenant, addDatabaseToTenant and changeTenantStatus.
    **     RAA 20240702 Replaced DBCredentials occurrences with TenantDatabaseDescriptor.
    **     RAA 20240710 Added landlordDatabase field. Added getDatabase function.
    **     RAA 20240715 Clarified the type of database name occurrences.
    **     RAA 20240717 Added support for the two tenant authentication tables. Adjusted initialization process.
    **     RAA 20240719 Added SQLs for creating the two tenant authentication tables.
    **     RAA 20240723 When deleting a tenant or one of its database instances, remove the LockTableUpdater
    **                  and the Settings for that/those instance(s).
    **     RAA 20240726 Added logical database name logic. Simplified parameters of addDatabaseToTenant. Added
    **                  TenantDatabaseDescriptorKey logic to the dbInstances map.
    **     RAA 20240807 Removed TenantDatabaseDescriptorKey occurrences. Added default tenant support.
    **     RAA 20240808 Authentication tables are no longer stored in the landlord database.
    ** 002 OM  20240815 The method getDatabase() returns a Database object of same type as the argument.
    **     OM  20240909 Concurrent multitenancy functionality implementation. Basic tenant metadata support.
    ** 003 OM  20241001 Added tenant id support. Incremental update.
    **     RAA 20241011 Added tenant description logic.
    ** 004 OM  20241022 Fixed database connection management.
    ** 005 RAA 20241106 Added tenant info field.
    **     RAA 20241108 Adjusted the tenant_name and tenant_info column sizes from 255 to 32767.
    ** 006 OM  20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
    **                  based on [sharedDb] parameter.
    ** 007 OM  20241204 The [sec_authentication_domain] and [sec_authentication_system] meta tables are accessed
    **                  from default persistent database, not from [landlord] database.
    ** 008 OM  20250116 Added support for additional dialects for landlord database.
    ** 009 OM  20250126 Synchronized landlord database with _Tenant meta table.
    **         20250129 Implemented management methods for domain administration.
    ** 010 OM  20250303 The tenant_name is fetched with the other columns of a domain.
    ** 011 OM  20250410 Added more invalidations for SecurityOps.
    ** 012 OM  20250228 Encrypted the password in [tenant_databases] using a strong symmetric algorithm.
    ** 013 OM  20250331 Added c3p0 connection parameters.
    **         20250513 Made password encryption methods public to be accessed from outside.
    ** 014 OM  20250430 Improve landlord database connection resiliency: attempts to reconnect on connection lost.
    **         20250501 Complex operations on SQL database are performed in transactions.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.persist.orm;

    import java.beans.*;
    import java.nio.charset.*;
    import java.security.*;
    import java.sql.*;
    import java.sql.Statement;
    import java.util.*;
    import java.util.function.*;
    import java.util.logging.Level;
    import com.fasterxml.jackson.databind.*;
    import com.goldencode.p2j.directory.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.dialect.*;
    import com.goldencode.p2j.persist.lock.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.security.SecurityManager;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.CentralLogger;
    import com.mchange.v2.c3p0.*;
    import org.apache.commons.codec.*;
    import org.apache.commons.codec.binary.*;
    import org.apache.commons.codec.binary.Base64;
    import javax.crypto.*;
    import static com.goldencode.p2j.util.BlockManager.*;

    /**
     * Class responsible with handling tenants.
     */
    public class TenantManager
    {
       /** The name of the default tenant. */
       public static final String DEFAULT_TENANT_NAME = "Default";

       /** The description of the default tenant. */
       public static final String DEFAULT_TENANT_DESCRIPTION = "Default tenant";

       /** The info of the default tenant. */
       public static final String DEFAULT_TENANT_INFO = "";

       /** The id of the default tenant. */
       public static final int DEFAULT_TENANT_ID = 0;

       /** The type of default tenant. */
       public static final int TYPE_DEFAULT = 0;

       /** The type of regular tenants. */
       public static final int TYPE_REGULAR = 1;

       /** The type of super tenants. */
       public static final int TYPE_SUPER = 2;

       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(TenantManager.class);

       /** The lock used to synchronize calls. */
       private static final Object lock = new Object();

       /** The maximum number of (regular and super) tenants. 16Mi far exceeds the OE capabilities. */
       private static final int MAX_TENANTS = 0xFF_FFFF;

       /** Constant for identifying the {@code _sec-authentication-system} table. */
       public static final String SEC_AUTHENTICATION_SYSTEM = "_sec-authentication-system";

       /** Constant for identifying the {@code _sec-authentication-domain} table. */
       public static final String SEC_AUTHENTICATION_DOMAIN = "_sec-authentication-domain";

       /** The JSON property name for tenant name. */
       public static final String JSON_TENANT_NAME = "tenantName";

       /** The JSON property name for tenant description. */
       public static final String JSON_TENANT_DESCRIPTION = "tenantDescription";

       /** The JSON property name for tenant info. */
       public static final String JSON_TENANT_INFO = "tenantInfo";

       /** The JSON property name for tenant's physical database name. */
       public static final String JSON_PHYSICAL_NAME = "physicalName";

       /** The JSON property name for tenant's logical database name. */
       public static final String JSON_LOGICAL_NAME = "logicalName";

       /** The column name for tenant name in both landlord tables. */
       private static final String COL_TENANT_NAME = "tenant_name";

       /** The column name for tenant description in landlord's {@code master} table. */
       private static final String COL_TENANT_DESC = "tenant_description";

       /** The column name for tenant info in landlord's {@code master} table. */
       private static final String COL_TENANT_INFO = "tenant_info";

       /** The column name for tenant's physical database in landlord's {@code tenant_databases} table. */
       private static final String COL_PHYSICAL_DB = "pdb_name";

       /** The column name for tenant's logical database in landlord's {@code tenant_databases} table. */
       private static final String COL_LOGICAL_DB = "ldb_name";

       /** The column name for tenant's connection URL in landlord's {@code tenant_databases} table. */
       private static final String COL_DB_URL = "url";

       /** The column name for tenant's authentication username in landlord's {@code tenant_databases} table. */
       private static final String COL_DB_USER = "username";

       /** The column name for tenant's authentication password in landlord's {@code tenant_databases} table. */
       private static final String COL_DB_PASS = "password";

       /** The dialect specific object responsible for providing SQL statements for tenant management. */
       private static Dialect.TenantSQL tenantSQL = null;

       /** Map for retrieving a tenant based on its name. */
       private static final Map<String, Tenant> tenants = new HashMap<>();

       /** Map for retrieving a tenant based on its id. */
       private static final Map<Integer, Tenant> tenantsById = new HashMap<>();

       /** Map of update statements for the master table based on the column name given. */
       private static final Map<String, String> updateStmts = new HashMap<>();

       /** Flag for determining if this class has been initialized or not. */
       private volatile static boolean init = false;

       /** The listeners which will be notified in case of tenant changes. */
       private static final ArrayList<TenantMetaChangeListener> metaListeners = new ArrayList<>();

       /** The Symmetric cipher parameters for en/decryption of passwords. */
       private static CryptoUtils.CipherParams cp = null;

       /** The key for en/decryption of passwords. It is initialized from directory at first use. */
       private static byte[] key = null;

       /**
        * The initialization vector for en/decryption of passwords. It is initialized from directory at first use.
        */
       private static byte[] iv = null;

       /** The data source for the pool of connections to landlord database.*/
       private static final ComboPooledDataSource ds = new ComboPooledDataSource("landlord-ds");

       /** Enable the support for multi-tenancy. This process also involves creating the default tenant. */
       public static void enable()
       {
          synchronized (lock)
          {
             init = true;
             addDefaultTenant();
          }
       }

       /**
        * Initialize multi-tenancy. This process involves:
        * <ol>
        *    <li> Creating the required tables if they don't already exist.
        *    <li> Initializing the statements that can be of type prepared.
        *    <li> Logically connecting the tenants that are stored in the master table.
        * </ol>
        *
        * @param   landlordURL
        *          The URL of the landlord database.
        * @param   landlordUser
        *          The (optional) JDBC User for access to the landlord database.
        * @param   landlordPassword
        *          The (optional) JDBC Password for access to the landlord database.
        */
       public static void initialize(String landlordURL, String landlordUser, String landlordPassword)
       {
          if (!init)
          {
             return;
          }

          synchronized (lock)
          {
             Dialect dialect = Dialect.getDialectForURL(landlordURL);
             try
             {
                ds.setDriverClass(dialect.getDriverClassName());
             }
             catch (PropertyVetoException e)
             {
                throw new RuntimeException("Failed to set the SQL driver for landlord DB " + landlordURL, e);
             }
             ds.setJdbcUrl(landlordURL);
             ds.setUser(landlordUser);
             ds.setPassword(landlordPassword);
             // ds.setProperties(props); // other properties, if needed in future

             tenantSQL = dialect.getTenantSql();
             initializeStatements(tenantSQL);

             Connection conn = null;
             boolean errorReported = false;
             try
             {
                conn = getConnection(true);
                createTables(conn);

                // check if there are tenants stored in the landlord database so we can logically connect them
                populateAllTenants(conn);
                conn.commit();
             }
             catch (SQLException e)
             {
                if (conn != null)
                {
                   try
                   {
                      conn.rollback();
                   }
                   catch (SQLException ex)
                   {
                      // will be logged/reported anyway
                   }
                }

                LOG.log(Level.WARNING, "Cannot connect to the landlord database due to " + e.getMessage(), e);
                errorReported = true;
             }
             finally
             {
                if (conn != null)
                {
                   try
                   {
                      conn.close();
                   }
                   catch (SQLException e)
                   {
                      if (!errorReported) // avoid duplicating error messages
                      {
                         LOG.log(Level.WARNING,
                                 "Failed to close connection to landlord database due to " + e.getMessage(), e);
                      }
                   }
                }
             }
          }
       }

       /**
        * Get a list with all the tenants.
        *
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  A list with the registered tenants.
        */
       public static ArrayList<Tenant> listTenants(List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return null;
          }

          return new ArrayList<>(tenants.values());
       }

       /**
        * Insert a tenant into the master table.
        *
        * @param   tenantName
        *          The name of the tenant.
        * @param   tenantDescription
        *          The description of the tenant.
        * @param   tenantInfo
        *          The info of the tenant.
        * @param   databases
        *          A list of the databases that will be used by the tenant.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operation is performed successfully, {@code false} otherwise.
        */
       public static boolean addTenant(String tenantName,
                                       String tenantDescription,
                                       String tenantInfo,
                                       List<TenantDatabaseDescriptor> databases,
                                       List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          synchronized (lock)
          {
             if (tenants.containsKey(tenantName))
             {
                errorSink.add("Tenant '" + tenantName + "' already exists in landlord database.");
                return false;
             }

             int nextId = getNextId(false);
             Tenant tenant = new Tenant(nextId, tenantName, tenantDescription, tenantInfo, databases);

             Connection conn = null;
             boolean errorReported = false;
             try
             {
                conn = getConnection(true);

                // add the tenant to landlord database
                try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getInsertTenant()))
                {
                   stmt.setObject(1, tenant.tenantId);
                   stmt.setString(2, tenant.tenantName);
                   stmt.setString(3, tenant.tenantDescription);
                   stmt.setString(4, tenant.tenantInfo);
                   stmt.setBoolean(5, false);
                   stmt.execute();
                }

                for (int i = 0; i < databases.size(); i++)
                {
                   TenantDatabaseDescriptor db = databases.get(i);

                   // add each declared physical database to the tenant
                   try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getInsertTenantDatabases()))
                   {
                      stmt.setString(1, tenant.tenantName);
                      stmt.setString(2, db.getPdbName());
                      stmt.setString(3, db.getLdbName());
                      stmt.setString(4, db.getUrl());
                      stmt.setString(5, db.getUsername());
                      stmt.setString(6, encrypt(db.getPassword()));
                      setIntValueInStatement(stmt,   7, db.getC3p0MaxStmts());
                      setIntValueInStatement(stmt,   8, db.getC3p0MinPool());
                      setIntValueInStatement(stmt,   9, db.getC3p0MaxPool());
                      setIntValueInStatement(stmt,  10, db.getC3p0AcqInc());
                      setIntValueInStatement(stmt,  11, db.getC3p0MaxIdle());
                      stmt.execute();
                   }

                   for (TenantMetaChangeListener mt : metaListeners)
                   {
                      if (mt.getDatabase().equals(db.getLdbName()))
                      {
                         runInGlobalBlock(() -> mt.addNewTenant(tenant, errorSink));
                      }
                   }
                }

                tenants.putIfAbsent(tenant.tenantName, tenant);
                tenantsById.put(Math.abs(nextId), tenant);

                conn.commit();
                return true;
             }
             catch (SQLException e)
             {
                if (conn != null)
                {
                   try
                   {
                      conn.rollback();
                   }
                   catch (SQLException ex)
                   {
                      // will be logged/reported anyway
                   }
                }

                errorSink.add("Cannot insert tenant due to " + e.getMessage());
                LOG.log(Level.WARNING, "Cannot insert tenant due to " + e.getMessage(), e);
                errorReported = true;
             }
             finally
             {
                if (conn != null)
                {
                   try
                   {
                      conn.close();
                   }
                   catch (SQLException e)
                   {
                      if (!errorReported) // avoid duplicating error messages
                      {
                         errorSink.add("Cannot insert tenant due to " + e.getMessage());
                         LOG.log(Level.WARNING, "Cannot insert tenant due to " + e.getMessage(), e);
                      }
                   }
                }
             }
          }
          return false;
       }

       /**
        * Sets an integer value to a positional parameter in a prepared statement.
        *
        * @param   prepStmt
        *          The statement. Must be already prepared.
        * @param   pos
        *          The position of the parameter.
        * @param   value
        *          The value. Can be null.
        *
        * @throws  SQLException
        *          If an SQL issue occurs.
        */
       private static void setIntValueInStatement(PreparedStatement prepStmt, int pos, Integer value)
       throws SQLException
       {
          if (value == null)
          {
             prepStmt.setNull(pos, Types.INTEGER);
          }
          else
          {
             prepStmt.setInt(pos, value);
          }
       }

       /**
        * Function for adding a database instance to a tenant.
        *
        * @param   tenantName
        *          The tenant for which to add the database instance.
        * @param   db
        *          The database information to be added.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operation is performed successfully, {@code false} otherwise.
        */
       public static boolean addDatabaseToTenant(String tenantName,
                                                 TenantDatabaseDescriptor db,
                                                 List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          if (tenantName.equals(DEFAULT_TENANT_NAME))
          {
             errorSink.add("Cannot add database instances to the default tenant.");
             LOG.log(Level.WARNING, "Cannot add database instances to the default tenant.");
             return false;
          }

          synchronized (lock)
          {
             Tenant tenant = tenants.get(tenantName);
             if (tenant == null)
             {
                errorSink.add("Cannot add a database instance to a non-existing tenant.");
                LOG.log(Level.WARNING, "Cannot add a database instance to a non-existing tenant.");
                return false;
             }

             Connection conn = null;
             boolean errorReported = false;
             try
             {
                conn = getConnection(true);

                String pdbName = db.getPdbName();
                String ldbName = db.getLdbName();
                if (tenant.getDatabaseCredentials(pdbName, true) != null &&
                    tenant.getDatabaseCredentials(ldbName, false) != null)
                {
                   // If a configuration is already set up for this (pdb/ldb) combination, abort
                   errorSink.add("A configuration is already set up for this database.");
                   return false;
                }

                try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getInsertTenantDatabases()))
                {
                   stmt.setString(1, tenantName);
                   stmt.setString(2, pdbName);
                   stmt.setString(3, ldbName);
                   stmt.setString(4, db.getUrl());
                   stmt.setString(5, db.getUsername());
                   stmt.setString(6, encrypt(db.getPassword()));
                   setIntValueInStatement(stmt,   7, db.getC3p0MaxStmts());
                   setIntValueInStatement(stmt,   8, db.getC3p0MinPool());
                   setIntValueInStatement(stmt,   9, db.getC3p0MaxPool());
                   setIntValueInStatement(stmt,  10, db.getC3p0AcqInc());
                   setIntValueInStatement(stmt,  11, db.getC3p0MaxIdle());
                   stmt.execute();
                }
                // no catch here, let the outer [try] catch it

                tenant.addDatabaseInstance(pdbName, db);
                String err = DatabaseManager.activateTenantDatabase(tenant, pdbName);
                if (err != null)
                {
                   errorSink.add("Failed to activate tenant database due to " + err);
                   // be more lenient: if the logical database was not yet connected, just warn the event:
                   LOG.log(Level.WARNING, "Failed to activate tenant database due to " + err);
                }

                for (TenantMetaChangeListener mt : metaListeners)
                {
                   if (mt.getDatabase().equals(ldbName))
                   {
                      runInGlobalBlock(() -> mt.addNewTenant(tenant, errorSink));
                   }
                }

                conn.commit();
                return true;
             }
             catch (SQLException | PersistenceException e)
             {
                if (conn != null)
                {
                   try
                   {
                      conn.rollback();
                   }
                   catch (SQLException ex)
                   {
                      // will be logged/reported anyway
                   }
                }

                errorSink.add("Cannot add database to tenant due to " + e.getMessage());
                LOG.log(Level.WARNING, "Cannot add database to tenant due to " + e.getMessage(), e);
                errorReported = true;
             }
             finally
             {
                if (conn != null)
                {
                   try
                   {
                      conn.close();
                   }
                   catch (SQLException e)
                   {
                      if (!errorReported) // avoid duplicating error messages
                      {
                         errorSink.add("Cannot add database to tenant due to " + e.getMessage());
                         LOG.log(Level.WARNING, "Cannot add database to tenant due to " + e.getMessage(), e);
                      }
                   }
                }
             }
          }
          return false;
       }

       /**
        * Add a database to the default tenant.
        *
        * @param   db
        *          The database descriptor which stores information about the database to be added.
        */
       public static void addDatabaseToDefaultTenant(TenantDatabaseDescriptor db)
       {
          if (!init)
          {
             return;
          }

          synchronized (lock)
          {
             Tenant defaultTenant = tenants.get(DEFAULT_TENANT_NAME);
             if (defaultTenant == null)
             {
                LOG.log(Level.WARNING, "Default tenant is not instantiated.");
                return;
             }

             String pdbName = db.getPdbName();
             if (defaultTenant.getDatabaseCredentials(pdbName, true) != null)
             {
                // If a configuration is already set up for this database, abort
                return;
             }

             defaultTenant.addDatabaseInstance(pdbName, db);
          }
       }

       /**
        * Get a list with all the domains of a logical database.
        *
        * @param   ldbName
        *          The logical database name whose domains are requested.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  A list with the registered domains from {@code _sec-authentication-domain} meta table of the
        *          specified logical database.
        */
       public static ArrayList<String> listDomains(String ldbName, List<String> errorSink)
       {
          return listSecAuthTable(ldbName, SEC_AUTHENTICATION_DOMAIN, "_Domain-name", errorSink);
       }

       /**
        * Get a list with all the domain types defined in a logical database.
        *
        * @param   ldbName
        *          The logical database name whose domain types are requested.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  A list with the registered domains from {@code _sec-authentication-system} meta table of the
        *          specified logical database.
        */
       public static ArrayList<String> listDomainTypes(String ldbName, List<String> errorSink)
       {
          return listSecAuthTable(ldbName, SEC_AUTHENTICATION_SYSTEM, "_Domain-type", errorSink);
       }

       /**
        * Generic implementation for extracting the list of a keys from a specified (meta-) table.
        *
        * @param   ldbName
        *          The logical database name whose domain types are requested.
        * @param   legacyName
        *          The legacy name of the meta-table.
        * @param   keyField
        *          The field which acts as a key and identifies unique records.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  A list with the registered domains from {@code _sec-authentication-*} meta table of the
        *          specified logical database.
        */
       private static ArrayList<String> listSecAuthTable(String ldbName,
                                                         String legacyName,
                                                         String keyField,
                                                         List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return null;
          }

          Persistence p;
          try
          {
             p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
          }
          catch (Exception e)
          {
             errorSink.add(e.getMessage());
             // some error happened. [ldbName] may be invalid
             return null;
          }

          if (p == null)
          {
             errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
             return null;
          }

          ArrayList<String> res = new ArrayList<>();

          runInGlobalBlock(() -> {
             @LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
             handle bh = UndoableFactory.handle();

             @LegacySignature(type = InternalEntry.Type.VARIABLE, name = "qh")
             handle qh = UndoableFactory.handle();

             doBlock(TransactionType.NONE, "listDomainsLabel", new Block((Body) () ->
             {
                try
                {
                   // create the buffer for the _Domains meta table
                   RecordBuffer.createMeta(bh, p, legacyName /*"_sec-authentication-domain/system"*/, ldbName);
                   QueryWrapper.createQuery(qh);

                   P2JQuery query = (P2JQuery) qh.getResource();
                   query.setBuffers(bh);
                   query.prepare("FOR EACH " + ldbName + "." + legacyName + " NO-LOCK");
                   query.queryOpen();
                   query.getFirst();

                   BufferImpl domain = (BufferImpl) bh.getResource();
                   while (domain.available().booleanValue())
                   {
                      res.add(domain.dereference(keyField).toStringMessage()); // it's a character
                      query.getNext();
                   }
                }
                catch (Throwable t)
                {
                   errorSink.add(t.getMessage());
                   undoLeave(); // ignore it? success[0] remains false
                }
             }));

             // cleanup:
             HandleOps.delete(bh);
             HandleOps.delete(qh);
          });

          return res;
       }

       /**
        * Get the details for a specific domain of a logical database.
        *
        * @param   ldbName
        *          The logical database name whose domain are requested.
        * @param   domainName
        *          The domain name whose details are requested.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  A map with details of a registered domain from {@code _sec-authentication-domain} meta table of
        *          the specified logical database.
        */
       public static HashMap<String, BaseDataType> getDomain(String ldbName,
                                                             String domainName,
                                                             List<String> errorSink)
       {
          return getSecAuthRecord(ldbName, domainName, SEC_AUTHENTICATION_DOMAIN, "_Domain-name", errorSink);
       }

       /**
        * Get the details for a specific domain type from a logical database.
        *
        * @param   ldbName
        *          The logical database name whose domain are requested.
        * @param   domainTypeName
        *          The name of domain type whose details are requested.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  A map with details of a registered domain from {@code _sec-authentication-domain} meta table of
        *          the specified logical database.
        */
       public static HashMap<String, BaseDataType> getDomainType(String ldbName,
                                                                 String domainTypeName,
                                                                 List<String> errorSink)
       {
          return getSecAuthRecord(ldbName, domainTypeName, SEC_AUTHENTICATION_SYSTEM, "_Domain-type", errorSink);
       }

       /**
        * Get the details for a specific domain or domain-type from the logical database.
        *
        * @param   ldbName
        *          The logical database name whose domain are requested.
        * @param   keyName
        *          The domain/domainType name whose details are requested.
        * @param   legacyName
        *          The legacy name of the meta-table.
        * @param   keyField
        *          The field which acts as a key and identifies unique records.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  A map with details of a registered domain from {@code _sec-authentication-*} meta table of
        *          the specified logical database.
        */
       private static HashMap<String, BaseDataType> getSecAuthRecord(String ldbName,
                                                                     String keyName,
                                                                     String legacyName,
                                                                     String keyField,
                                                                     List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return null;
          }

          if (!validateName(ldbName) || !validateName(keyName))
          {
             errorSink.add("Invalid parameters.");
             return new HashMap<>();
          }

          Persistence p;
          try
          {
             p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
          }
          catch (Exception e)
          {
             errorSink.add(e.getMessage());
             // some error happened. [ldbName] may be invalid
             return null;
          }

          if (p == null)
          {
             errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
             return null;
          }

          HashMap<String, BaseDataType> res = new HashMap<>();

          runInGlobalBlock(() -> {
             @LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
             handle bh = UndoableFactory.handle();

             doBlock(TransactionType.NONE, "getDomainLabel", new Block((Body) () ->
             {
                try
                {
                   // create the buffer for the _Domains meta table
                   RecordBuffer.createMeta(bh, p, legacyName /*"_sec-authentication-domain/system"*/, ldbName);
                   BufferImpl domain = (BufferImpl) bh.getResource();
                   domain.findFirst(new character("WHERE " + keyField + " = '" + keyName + "'"));

                   if (domain.available().booleanValue())
                   {
                      DmoMeta dmoInfo = domain.buffer().getDmoInfo();
                      Iterator<Property> fields = dmoInfo.getFields(false);
                      while (fields.hasNext())
                      {
                         Property prop = fields.next();
                         res.put(prop.legacy, domain.dereference(prop.legacy));
                      }
                   }
                   else
                   {
                      errorSink.add(keyField + " resource '" + keyName + "' not found.");
                   }
                }
                catch (Throwable t)
                {
                   errorSink.add(t.getMessage());
                   undoLeave(); // ignore it? success[0] remains false
                }
             }));

             // cleanup:
             HandleOps.delete(bh);
          });

          return res;
       }

       /**
        * Add a domain to the list.
        *
        * @param   ldbName
        *          The logical database name where the new domains is about to be created.
        * @param   properties
        *          The properties of the new domain, including the name
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operation is performed successfully,
        *          {@code false} if the operation is not performed successfully,
        */
       public static boolean addDomain(String ldbName, JsonNode properties, List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          Persistence p;
          try
          {
             p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
          }
          catch (Exception e)
          {
             errorSink.add(e.getMessage());
             // some error happened. [ldbName] may be invalid
             return false;
          }

          if (p == null)
          {
             errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
             return false;
          }

          Boolean[] success = new Boolean[] { false };
          runInGlobalBlock(() -> {
             @LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
             handle bh = UndoableFactory.handle();

             doBlock(TransactionType.FULL, "addDomainsLabel", new Block((Body) () ->
             {
                try
                {
                   // create the buffer for the _Domains meta table
                   RecordBuffer.createMeta(bh, p, SEC_AUTHENTICATION_DOMAIN, ldbName);
                   BufferImpl domain = (BufferImpl) bh.getResource();
                   logical ok = domain.bufferCreate();
                   if (ok.booleanValue())
                   {
                      setField(domain, properties, "_Domain-name", JsonNode::textValue);
                      setField(domain, properties, "_Domain-type", JsonNode::textValue);
                      setField(domain, properties, "_Domain-description", JsonNode::textValue);
                      setField(domain, properties, "_Domain-access-code", JsonNode::textValue);
                      setField(domain, properties, "_Auditing-context", JsonNode::textValue);
                      setField(domain, properties, "_Domain-runtime-options", JsonNode::textValue);
                      setField(domain, properties, "_Domain-enabled", JsonNode::booleanValue);
                      setField(domain, properties, "_PAM-actions", JsonNode::textValue);
                      setField(domain, properties, "_PAM-control", JsonNode::textValue);
                      setField(domain, properties, "_PAM-library-path", JsonNode::textValue);
                      setField(domain, properties, "_PAM-options", JsonNode::textValue);
                      setField(domain, properties, "_Domain-custom-detail", JsonNode::textValue);
                      setField(domain, properties, "_Tenant-Name", JsonNode::textValue);
                      setField(domain, properties, "_Domain-Id", JsonNode::longValue);
                      setField(domain, properties, "_Domain-category", JsonNode::intValue);
                      domain.release();

                      success[0] = true;
                      SecurityOps.invalidate(ldbName, SecurityOps.META_SEC_AUTHENTICATION_DOMAIN);
                   }
                   else
                   {
                      errorSink.add("Resource '" + SEC_AUTHENTICATION_DOMAIN + "' could not be created.");
                   }
                }
                catch (Throwable t)
                {
                   errorSink.add(t.getMessage());
                   undoLeave(); // ignore it? success[0] remains false
                }
             }));

             // cleanup:
             if (bh._isValid())
             {
                HandleOps.delete(bh);
             }
          });

          return success[0];
       }

       /**
        * Add a domain to the list.
        *
        * @param   ldbName
        *          The logical database name where the new domains is about to be created.
        * @param   properties
        *          The properties of the new domain, including the name
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operation is performed successfully,
        *          {@code false} if the operation is not performed successfully,
        */
       public static boolean addDomainType(String ldbName, JsonNode properties, List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          Persistence p;
          try
          {
             p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
          }
          catch (Exception e)
          {
             errorSink.add(e.getMessage());
             // some error happened. [ldbName] may be invalid
             return false;
          }

          if (p == null)
          {
             errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
             return false;
          }

          Boolean[] success = new Boolean[] { false };
          runInGlobalBlock(() -> {
             @LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
             handle bh = UndoableFactory.handle();

             doBlock(TransactionType.FULL, "addDomainsLabel", new Block((Body) () ->
             {
                try
                {
                   // create the buffer for the _Domains meta table
                   RecordBuffer.createMeta(bh, p, SEC_AUTHENTICATION_SYSTEM, ldbName);
                   BufferImpl domainType = (BufferImpl) bh.getResource();
                   logical ok = domainType.bufferCreate();
                   if (ok.booleanValue())
                   {
                      setField(domainType, properties, "_Domain-type",             JsonNode::textValue);
                      setField(domainType, properties, "_Domain-type-description", JsonNode::textValue);
                      setField(domainType, properties, "_PAM-plug-in",             JsonNode::booleanValue);
                      setField(domainType, properties, "_PAM-module-name",         JsonNode::textValue);
                      setField(domainType, properties, "_PAM-callback-procedure",  JsonNode::textValue);
                      setField(domainType, properties, "_Key-store-id",            JsonNode::textValue);
                      setField(domainType, properties, "_Custom-detail",           JsonNode::textValue);
                      setField(domainType, properties, "_PAM-library-checksum",
                               jsonNode -> new raw(Base64.decodeBase64(jsonNode.textValue())));
                      domainType.release();

                      success[0] = true;
                      SecurityOps.invalidate(ldbName, SecurityOps.META_SEC_AUTHENTICATION_SYSTEM);
                   }
                   else
                   {
                      errorSink.add("Resource '" + SEC_AUTHENTICATION_SYSTEM + "' could not be created.");
                   }
                }
                catch (Throwable t)
                {
                   errorSink.add(t.getMessage());
                   undoLeave(); // ignore it? success[0] remains false
                }
             }));

             // cleanup:
             if (bh._isValid())
             {
                HandleOps.delete(bh);
             }
          });

          return success[0];
       }

       /**
        * Update a domain from the master table.
        *
        * @param   ldbName
        *          The logical database name whose domain is updated.
        * @param   domainName
        *          The name of the domain that is targeted by the update.
        * @param   properties
        *          A Map with pairs (column name - new value) that are intended for the update.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if all the updates are performed successfully,
        *          {@code false} if there is at least an update that fails,
        *          {@code null} if the caller does not have sufficient rights.
        */
       public static Boolean updateDomain(String ldbName,
                                          String domainName,
                                          JsonNode properties,
                                          List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          if (!validateName(ldbName) || !validateName(domainName))
          {
             errorSink.add("Invalid parameters.");
             return false;
          }

          Persistence p;
          try
          {
             p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
          }
          catch (Exception e)
          {
             errorSink.add(e.getMessage());
             // some error happened. [ldbName] may be invalid
             return false;
          }

          if (p == null)
          {
             errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
             return false;
          }

          Boolean[] success = new Boolean[] { false };
          runInGlobalBlock(() -> {
             @LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
             handle bh = UndoableFactory.handle();

             doBlock(TransactionType.FULL, "addDomainsLabel", new Block((Body) () ->
             {
                try
                {
                   RecordBuffer.createMeta(bh, p, SEC_AUTHENTICATION_DOMAIN, ldbName); // create the buffer for the _Domains meta table
                   BufferImpl domain = (BufferImpl) bh.getResource();
                   logical ok = domain.findFirst(new character("WHERE _Domain-name = '" + domainName + "'"),
                                                 LockType.EXCLUSIVE);
                   if (ok.booleanValue())
                   {
                      setField(domain, properties, "_Domain-name",            JsonNode::textValue);
                      setField(domain, properties, "_Domain-type",            JsonNode::textValue);
                      setField(domain, properties, "_Domain-description",     JsonNode::textValue);
                      setField(domain, properties, "_Domain-access-code",     JsonNode::textValue);
                      setField(domain, properties, "_Auditing-context",       JsonNode::textValue);
                      setField(domain, properties, "_Domain-runtime-options", JsonNode::textValue);
                      setField(domain, properties, "_Domain-enabled",         JsonNode::booleanValue);
                      setField(domain, properties, "_PAM-actions",            JsonNode::textValue);
                      setField(domain, properties, "_PAM-control",            JsonNode::textValue);
                      setField(domain, properties, "_PAM-library-path",       JsonNode::textValue);
                      setField(domain, properties, "_PAM-options",            JsonNode::textValue);
                      setField(domain, properties, "_Domain-custom-detail",   JsonNode::textValue);
                      setField(domain, properties, "_Tenant-Name",            JsonNode::textValue);
                      setField(domain, properties, "_Domain-Id",              JsonNode::longValue);
                      setField(domain, properties, "_Domain-category",        JsonNode::intValue);
                      domain.release();

                      success[0] = true;
                      SecurityOps.invalidate(ldbName, SecurityOps.META_SEC_AUTHENTICATION_DOMAIN);
                   }
                   else
                   {
                      errorSink.add(SEC_AUTHENTICATION_DOMAIN + " resource '" + domainName + "' not found.");
                   }
                }
                catch (Throwable t)
                {
                   errorSink.add(t.getMessage());
                   undoLeave(); // ignore it? success[0] remains false
                }
             }));

             // cleanup:
             HandleOps.delete(bh);
          });

          return success[0];
       }

       /**
        * Update a domain type from the master table.
        *
        * @param   ldbName
        *          The logical database name whose domain is updated.
        * @param   domainType
        *          The domain type that is targeted by the update.
        * @param   properties
        *          A Map with pairs (column name - new value) that are intended for the update.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if all the updates are performed successfully,
        *          {@code false} if there is at least an update that fails,
        *          {@code null} if the caller does not have sufficient rights.
        */
       public static Boolean updateDomainType(String ldbName,
                                              String domainType,
                                              JsonNode properties,
                                              List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          if (!validateName(ldbName) || !validateName(domainType))
          {
             errorSink.add("Invalid parameters.");
             return false;
          }

          Persistence p;
          try
          {
             p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
          }
          catch (Exception e)
          {
             errorSink.add(e.getMessage());
             // some error happened. [ldbName] may be invalid
             return false;
          }

          if (p == null)
          {
             errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
             return false;
          }

          Boolean[] success = new Boolean[] { false };
          runInGlobalBlock(() -> {
             @LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
             handle bh = UndoableFactory.handle();

             doBlock(TransactionType.FULL, "addDomainsLabel", new Block((Body) () ->
             {
                try
                {
                   // create the buffer for the _Domains meta table
                   RecordBuffer.createMeta(bh, p, SEC_AUTHENTICATION_SYSTEM, ldbName);
                   BufferImpl domain = (BufferImpl) bh.getResource();
                   logical ok = domain.findFirst(new character("WHERE _Domain-type = '" + domainType + "'"),
                                                 LockType.EXCLUSIVE);
                   if (ok.booleanValue())
                   {
                      setField(domain, properties, "_Domain-type",             JsonNode::textValue);
                      setField(domain, properties, "_Domain-type-description", JsonNode::textValue);
                      setField(domain, properties, "_PAM-plug-in",             JsonNode::booleanValue);
                      setField(domain, properties, "_PAM-module-name",         JsonNode::textValue);
                      setField(domain, properties, "_PAM-callback-procedure",  JsonNode::textValue);
                      setField(domain, properties, "_Key-store-id",            JsonNode::textValue);
                      setField(domain, properties, "_Custom-detail",           JsonNode::textValue);
                      setField(domain, properties, "_PAM-library-checksum",
                               jsonNode -> new raw(Base64.decodeBase64(jsonNode.textValue())));
                      domain.release();

                      success[0] = true;
                      SecurityOps.invalidate(ldbName, SecurityOps.META_SEC_AUTHENTICATION_SYSTEM);
                   }
                   else
                   {
                      errorSink.add(SEC_AUTHENTICATION_SYSTEM + " resource '" + domainType + "' not found.");
                   }
                }
                catch (Throwable t)
                {
                   errorSink.add(t.getMessage());
                   undoLeave(); // ignore it? success[0] remains false
                }
             }));

             // cleanup:
             HandleOps.delete(bh);
          });

          return success[0];
       }

       /**
        * Function for deleting a domain from a logical database.
        *
        * @param   ldbName
        *          The logical database name whose domain is about to be deleted.
        * @param   domainName
        *          The name of the tenant that is to be deleted.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operation was successful, {@code false} otherwise.
        */
       public static boolean deleteDomain(String ldbName, String domainName, List<String> errorSink)
       {
          return deleteSecAuthRecord(ldbName, domainName, SEC_AUTHENTICATION_DOMAIN, "_Domain-name", errorSink);
       }

       /**
        * Function for deleting a domain from a logical database.
        *
        * @param   ldbName
        *          The logical database name whose domain is about to be deleted.
        * @param   domainName
        *          The name of the tenant that is to be deleted.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operation was successful, {@code false} otherwise.
        */
       public static boolean deleteDomainType(String ldbName, String domainName, List<String> errorSink)
       {
          return deleteSecAuthRecord(ldbName, domainName, SEC_AUTHENTICATION_SYSTEM, "_Domain-type", errorSink);
       }

       /**
        * Function for deleting a record from a sec_auth table of a logical database.
        *
        * @param   ldbName
        *          The logical database name whose domain is about to be deleted.
        * @param   recordKey
        *          The key name of the record to be deleted.
        * @param   legacyName
        *          The legacy name of the meta-table.
        * @param   keyField
        *          The field which acts as a key and identifies unique records.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operation was successful, {@code false} otherwise.
        */
       public static boolean deleteSecAuthRecord(String ldbName,
                                                 String recordKey,
                                                 String legacyName,
                                                 String keyField,
                                                 List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          if (!validateName(ldbName) || !validateName(recordKey))
          {
             errorSink.add("Invalid parameters.");
             return false;
          }

          Persistence p;
          try
          {
             p = PersistenceFactory.getInstance(new Database(ldbName, Database.Type.META));
          }
          catch (Exception e)
          {
             errorSink.add(e.getMessage());
             // some error happened. [ldbName] may be invalid
             return false;
          }

          if (p == null)
          {
             errorSink.add("Failed to acquire Persistence object for '" + ldbName + "' database.");
             return false;
          }

          Boolean[] success = new Boolean[] { false };
          runInGlobalBlock(() -> {
             @LegacySignature(type = InternalEntry.Type.VARIABLE, name = "bh")
             handle bh = UndoableFactory.handle();

             doBlock(TransactionType.FULL, "deleteDomainsLabel", new Block((Body) () ->
             {
                try
                {
                   // create the buffer for the _Domains meta table
                   RecordBuffer.createMeta(bh, p, legacyName /*"_sec-authentication-domain/system"*/, ldbName);
                   BufferImpl domain = (BufferImpl) bh.getResource();
                   logical ok = domain.findFirst(new character("WHERE " + keyField + " = '" + recordKey + "'"),
                                                 LockType.EXCLUSIVE);
                   if (ok.booleanValue())
                   {
                      domain.deleteRecord();
                      success[0] = true;
                      SecurityOps.invalidate(ldbName, legacyName);
                   }
                   else
                   {
                      errorSink.add(legacyName + " resource '" + recordKey + "' not found.");
                   }
                }
                catch (Throwable t)
                {
                   errorSink.add(t.getMessage());
                   undoLeave(); // ignore it? success[0] remains false
                }
             }));

             // cleanup:
             HandleOps.delete(bh);
          });

          return success[0];
       }

       /**
        * Update a tenant from the master table. This method allows one or multiple column values to be edited.
        * Possible {@code column} values/names are:
        * <ul>
        *    <li> tenant_name - the name of the tenant.
        *    <li> db_name - the name of a database instance that this tenant uses.
        *    <li> url - the URL of a database instance.
        *    <li> username - the username required in order to access a database instance.
        *    <li> password - the password required in order to access a database instance.
        * </ul>
        *
        * @param   tenantName
        *          The name of the tenant that is targeted by the update.
        * @param   pdbName
        *          The physical name of the database that is targeted by the update.
        * @param   values
        *          A Map with pairs (column name - new value) that are intended for the update.
        * @param   databaseUpdate
        *          Is this update targeting a database change?
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if all the updates are performed successfully,
        *          {@code false} if there is at least an update that fails.
        */
       public static boolean updateTenant(String tenantName,
                                          String pdbName,
                                          Map<String, String> values,
                                          boolean databaseUpdate,
                                          List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          if (tenantName.equals(DEFAULT_TENANT_NAME))
          {
             errorSink.add("Cannot update the default tenant.");
             LOG.log(Level.WARNING, "Cannot update the default tenant.");
             return false;
          }

          synchronized (lock)
          {
             Tenant tenant = tenants.get(tenantName);
             if (tenant == null)
             {
                errorSink.add("Cannot update a non-existing tenant.");
                LOG.log(Level.WARNING, "Cannot update a non-existing tenant.");
                return false;
             }

             if (databaseUpdate && tenant.getDatabaseCredentials(pdbName, true) == null)
             {
                errorSink.add("Cannot update a non-existing database instance.");
                LOG.log(Level.WARNING, "Cannot update a non-existing database instance.");
                return false;
             }

             Connection conn = null;
             boolean errorReported = false;
             try
             {
                conn = getConnection(true);

                boolean success = true;
                for (Map.Entry<String, String> entry : values.entrySet())
                {
                   String field = determineDatabaseFieldName(entry.getKey());
                   String value = entry.getValue();
                   if (!updateTenantImpl(conn, tenantName, pdbName, field, value, databaseUpdate))
                   {
                      success = false;
                   }
                   else
                   {
                      tenant.setField(pdbName, field, value);
                   }
                }

                if (databaseUpdate)
                {
                   DatabaseManager.removeTenantDbConfig(tenantName, pdbName);
                }

                if (success)
                {
                   for (TenantMetaChangeListener mt : metaListeners)
                   {
                      // if (mt.getDatabase().equals(db.getPdbName()))
                      {
                         runInGlobalBlock(() -> mt.tenantChanged(tenantName, tenant, errorSink));
                      }
                   }
                }

                conn.commit();
                return success;
             }
             catch (SQLException e)
             {
                if (conn != null)
                {
                   try
                   {
                      conn.rollback();
                   }
                   catch (SQLException ex)
                   {
                      // will be logged/reported anyway
                   }
                }

                LOG.log(Level.WARNING, "Could not update tenant due to " + e.getMessage(), e);
                errorSink.add("Could not update tenant due to " + e.getMessage());
                errorReported = true;
             }
             finally
             {
                if (conn != null)
                {
                   try
                   {
                      conn.close();
                   }
                   catch (SQLException e)
                   {
                      if (!errorReported) // avoid duplicating error messages
                      {
                         LOG.log(Level.WARNING, "Could not update tenant due to " + e.getMessage(), e);
                         errorSink.add("Could not update tenant due to " + e.getMessage());
                      }
                   }
                }
             }
          }
          return false;
       }

       /**
        * Function for deleting a tenant from the master table.
        *
        * @param   tenantName
        *          The name of the tenant that is targeted by the delete.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operation was successful, {@code false} otherwise.
        */
       public static boolean deleteTenant(String tenantName, List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          synchronized (lock)
          {
             Connection conn = null;
             boolean errorReported = false;
             try
             {
                conn = getConnection(true);
                if (tenantName.equals(DEFAULT_TENANT_NAME))
                {
                   errorSink.add("Cannot delete the default tenant.");
                   LOG.log(Level.WARNING, "Cannot delete the default tenant.");
                   return false;
                }

                Tenant tenant = tenants.get(tenantName);
                if (tenant == null || tenant.id == DEFAULT_TENANT_ID)
                {
                   errorSink.add("Cannot delete a non-existing tenant.");
                   LOG.log(Level.WARNING, "Cannot delete a non-existing tenant.");
                   return false;
                }

                try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getSelectTenantDatabases()))
                {
                   stmt.setString(1, tenantName);
                   try (ResultSet rs = stmt.executeQuery())
                   {
                      while (rs.next())
                      {
                         String pdbName = rs.getString(COL_PHYSICAL_DB);
                         TenantDatabaseDescriptor dbData = tenant.getDatabaseCredentials(pdbName, true);
                         Database pdb = new Database(dbData.getLdbName(), Database.Type.PRIMARY, true);
                         Database db = pdb.createTenant(dbData.getPdbName());

                         DatabaseManager.removeTenantDbFromLockTable(db);
                         Settings.removeSettings(db);
                         FastFindCache.removeTenantCache(db);
                         String ldbName = dbData.getLdbName();
                         ConnectionManager.get().removeTenant(ldbName);

                         for (TenantMetaChangeListener mt : metaListeners)
                         {
                            if (mt.getDatabase().equals(dbData.getLdbName()))
                            {
                               runInGlobalBlock(() -> mt.deleteTenant(tenant, errorSink));
                            }
                         }
                      }
                   }
                }

                // delete the databases of the tenant
                try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getDeleteTenantDatabases()))
                {
                   stmt.setString(1, tenantName);
                   stmt.execute();
                }

                // delete the tenant from landlord database
                try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getDeleteTenant()))
                {
                   stmt.setString(1, tenantName);
                   stmt.execute();
                }

                tenants.remove(tenantName);
                tenantsById.remove(Math.abs(tenant.id));
                DatabaseManager.removeTenantDbConfigs(tenantName);

                conn.commit();
                return true;
             }
             catch (SQLException e)
             {
                if (conn != null)
                {
                   try
                   {
                      conn.rollback();
                   }
                   catch (SQLException ex)
                   {
                      // will be logged/reported anyway
                   }
                }

                errorSink.add("Could not delete the tenant due to " + e.getMessage());
                LOG.log(Level.WARNING, "Could not delete the tenant due to " + e.getMessage(), e);
                errorReported = true;
             }
             finally
             {
                if (conn != null)
                {
                   try
                   {
                      conn.close();
                   }
                   catch (SQLException e)
                   {
                      if (!errorReported) // avoid duplicating error messages
                      {
                         errorSink.add("Could not delete the tenant due to " + e.getMessage());
                         LOG.log(Level.WARNING, "Could not delete the tenant due to " + e.getMessage(), e);
                      }
                   }
                }
             }
          }
          return false;
       }

       /**
        * Delete a database instance that is assigned to a tenant.
        *
        * @param   tenantName
        *          The tenant for which to remove the database instance.
        * @param   pdbName
        *          The database physical name of the database to be deleted.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operation was successful, {@code false} otherwise.
        */
       public static boolean deleteDatabaseFromTenant(String tenantName, String pdbName, List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          if (tenantName.equals(DEFAULT_TENANT_NAME))
          {
             errorSink.add("Cannot delete database instances of the default tenant.");
             LOG.log(Level.WARNING, "Cannot delete database instances of the default tenant.");
             return false;
          }

          synchronized (lock)
          {
             Tenant tenant = tenants.get(tenantName);
             if (tenant == null)
             {
                errorSink.add("Cannot delete a database of a non-existing tenant.");
                LOG.log(Level.WARNING, "Cannot delete a database of a non-existing tenant.");
                return false;
             }

             TenantDatabaseDescriptor dbDescriptor = tenant.getDatabaseCredentials(pdbName, true);
             if (dbDescriptor == null)
             {
                errorSink.add("The database to be deleted not found in the db list of the specified tenant.");
                LOG.log(Level.WARNING,
                        "The database to be deleted not found in the databases list of the specified tenant.");
                return false;
             }

             Database pdb = new Database(dbDescriptor.getLdbName(), Database.Type.PRIMARY, true);
             Database db = pdb.createTenant(dbDescriptor.getPdbName());
             FastFindCache.removeTenantCache(db);

             Connection conn = null;
             boolean errorReported = false;
             try
             {
                conn = getConnection(true);
                try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getDeleteTenantDatabase()))
                {
                   stmt.setString(1, tenantName);
                   stmt.setString(2, pdbName);
                   stmt.execute();
                }
                // no catch here, let the outer [try] catch it

                tenant.removeDatabaseInstance(pdbName);
                Settings.removeSettings(db);
                DatabaseManager.removeTenantDbConfig(tenantName, pdbName);
                DatabaseManager.removeTenantDbFromLockTable(db);
                String ldbName = dbDescriptor.getLdbName();
                ConnectionManager.get().removeTenant(ldbName);

                // delete tenant from _Tenant
                for (TenantMetaChangeListener mt : metaListeners)
                {
                   if (mt.getDatabase().equals(ldbName))
                   {
                      runInGlobalBlock(() -> mt.deleteTenant(tenant, errorSink));
                   }
                }

                conn.commit();
                return true;
             }
             catch (SQLException e)
             {
                if (conn != null)
                {
                   try
                   {
                      conn.rollback();
                   }
                   catch (SQLException ex)
                   {
                      // will be logged/reported anyway
                   }
                }

                errorSink.add("Failed to delete the database of the tenant due to " + e.getMessage());
                LOG.log(Level.WARNING, "Failed to delete the database of the tenant due to " + e.getMessage());
                errorReported = true;
             }
             finally
             {
                if (conn != null)
                {
                   try
                   {
                      conn.close();
                   }
                   catch (SQLException e)
                   {
                      if (!errorReported) // avoid duplicating error messages
                      {
                         errorSink.add("Failed to close the connection to landlord database due to " +
                                       e.getMessage());
                         LOG.log(Level.WARNING,
                                 "Failed to close the connection to landlord database due to " + e.getMessage(),
                                 e);
                      }
                   }
                }
             }
          }
          return false;
       }

       /**
        * Enable/Disable a specific tenant.
        *
        * @param   tenantName
        *          The tenant to work with.
        * @param   enable
        *          {@code true} if the intent is to enable the tenant, {@code false} otherwise.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the operation is performed successfully, {@code false} otherwise.
        */
       public static boolean changeTenantStatus(String tenantName, boolean enable, List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          if (tenantName.equals(DEFAULT_TENANT_NAME))
          {
             LOG.log(Level.WARNING, "Cannot change the status of the default tenant.");
             errorSink.add("Cannot change the status of the default tenant.");
             return false;
          }

          synchronized (lock)
          {
             if (!tenants.containsKey(tenantName))
             {
                LOG.log(Level.WARNING, "Cannot change the status of a non-existing tenant.");
                errorSink.add("Cannot change the status of a non-existing tenant.");
                return false;
             }

             try (Connection conn = getConnection(false)) // no transaction, simple setter
             {
                try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getEnableTenant()))
                {
                   stmt.setBoolean(1, enable);
                   stmt.setString(2, tenantName);
                   stmt.execute();
                }
                // no catch here, let the outer [try] catch it

                Tenant tenant = tenants.get(tenantName);
                tenant.setStatus(enable);

                for (TenantMetaChangeListener mt : metaListeners)
                {
                   // if (mt.getDatabase().equals(db.getPdbName()))
                   {
                      runInGlobalBlock(() -> mt.tenantStateChanged(tenant, errorSink));
                   }
                }

                return true;
             }
             catch (SQLException e)
             {
                LOG.log(Level.WARNING, "Cannot enable/disable this tenant.", e);
                errorSink.add("Cannot enable/disable this tenant: " + e.getMessage());
             }

             return false;
          }
       }

       /**
        * Check if a specific tenant is enabled or not.
        *
        * @param   tenantName
        *          The tenant to work with.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  {@code true} if the tenant is enabled, {@code false} otherwise.
        */
       public static boolean isTenantEnabled(String tenantName, List<String> errorSink)
       {
          if (!init)
          {
             errorSink.add("Tenant Manager not initialized.");
             return false;
          }

          if (tenantName.equals(DEFAULT_TENANT_NAME))
          {
             LOG.log(Level.WARNING, "Cannot query the status of the default tenant.");
             errorSink.add("Cannot query the status of the default tenant.");
             return false;
          }

          synchronized (lock)
          {
             if (!tenants.containsKey(tenantName))
             {
                LOG.log(Level.WARNING, "Cannot query the status of a non-existing tenant.");
                errorSink.add("Cannot query the status of a non-existing tenant.");
                return false;
             }

             try (Connection conn = getConnection(false)) // no transaction, simple getter
             {
                try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getIsTenantEnabled()))
                {
                   stmt.setString(1, tenantName);
                   try (ResultSet rs = stmt.executeQuery())
                   {
                      return rs.next() && rs.getBoolean(1);
                   }
                   // no catch here, let the outer [try] catch it
                }
                // no catch here, let the outer [try] catch it
             }
             catch (SQLException e)
             {
                LOG.log(Level.WARNING, "Failed to find the tenant because " + e.getMessage(), e);
                errorSink.add("Failed to find the tenant because " + e.getMessage());
             }

             return false;
          }
       }

       /**
        * Get a tenant from the tenants map.
        *
        * @param   tenantName
        *          The name of the tenant.
        * @param   errorSink
        *          A sink for errors encountered.
        *
        * @return  The instance of {@code Tenant} that holds other information.
        */
       public static Tenant getTenant(String tenantName, List<String> errorSink)
       {
          if (!init)
          {
             if (errorSink != null)
             {
                errorSink.add("Tenant Manager not initialized.");
             }
             return null;
          }

          synchronized (lock)
          {
             Tenant tenant = tenants.get(tenantName);
             if (tenant == null)
             {
                if (errorSink != null)
                {
                   errorSink.add("Could not find the tenant " + tenantName);
                }
                LOG.log(Level.WARNING, "Could not find the tenant " + tenantName);
                return null;
             }

             return new Tenant(tenant);
          }
       }

       /**
        * Get the database instance of a tenant based on the tenant name and the database physical name.
        *
        * @param   tenantName
        *          The name of the tenant for which to retrieve the database.
        * @param   dbName
        *          The name of the database instance to be retrieved.
        * @param   physical
        *          {@code true} if the given database name is the physical one,
        *          {@code false} if the database name is the logical one.
        *
        * @return  The database instance, if it exists.
        */
       public static Database getDatabase(String tenantName, String dbName, boolean physical, Database.Type type)
       {
          if (!init)
          {
             return null;
          }

          synchronized (lock)
          {
             Tenant tenant = tenants.get(tenantName);
             if (tenant == null)
             {
                LOG.log(Level.WARNING, "Cannot retrieve the database of a non-existing tenant.");
                return null;
             }

             TenantDatabaseDescriptor dbDescriptor = tenant.getDatabaseCredentials(dbName, physical);
             if (dbDescriptor == null)
             {
                LOG.log(Level.WARNING, "Cannot retrieve the database settings of a non-existing database.");
                return null;
             }

             Database retDb = dbDescriptor.getDatabase();

             // check if the database was connected, if not, do it now
             if (retDb == null)
             {
                connectTenantImpl(tenant);
                retDb = dbDescriptor.getDatabase();
             }

             if (retDb.getType() == type)
             {
                return retDb;
             }
             return retDb.toType(type);
          }
       }

       /**
        * Get the database settings of a specific database of a specific tenant.
        *
        * @param   tenantName
        *          The tenant to work with.
        * @param   pdbName
        *          The database physical name for which to retrieve the settings.
        *
        * @return  A list with the database settings.
        */
       public static List<Object> getDatabaseSettings(String tenantName, String pdbName)
       {
          if (!init)
          {
             return null;
          }

          if (tenantName.equals(DEFAULT_TENANT_NAME))
          {
             LOG.log(Level.WARNING, "Cannot query the database settings of the default tenant.");
             return null;
          }

          synchronized (lock)
          {
             if (!tenants.containsKey(tenantName))
             {
                LOG.log(Level.WARNING, "Cannot query the database settings of a non-existing tenant.");
                return null;
             }

             try (Connection conn = getConnection(false)) // no transaction, simple getter
             {
                try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getSelectTenantDatabase()))
                {
                   stmt.setString(1, tenantName);
                   stmt.setString(2, pdbName);
                   List<Object> result = new ArrayList<>(3);
                   try (ResultSet rs = stmt.executeQuery())
                   {
                      if (rs.next())
                      {
                         result.add(rs.getString(COL_LOGICAL_DB));
                         result.add(rs.getString(COL_DB_URL));
                         result.add(rs.getString(COL_DB_USER));
                         result.add(decrypt(rs.getString(COL_DB_PASS)));
                         result.add(getIntegerColumn(rs, Dialect.TenantSQL.C3P0_MAX_STMTS));
                         result.add(getIntegerColumn(rs, Dialect.TenantSQL.C3P0_MIN_POOL));
                         result.add(getIntegerColumn(rs, Dialect.TenantSQL.C3P0_MAX_POOL));
                         result.add(getIntegerColumn(rs, Dialect.TenantSQL.C3P0_ACQ_INC));
                         result.add(getIntegerColumn(rs, Dialect.TenantSQL.C3P0_MAX_IDLE));
                      }
                   }
                   // no catch here, let the outer [try] catch it

                   return result;
                }
                // no catch here, let the outer [try] catch it
             }
             catch (SQLException e)
             {
                LOG.log(Level.WARNING,
                        "Failed to retrieve the tenant database settings because " + e.getMessage(), e);
             }

             return null;
          }
       }

       /**
        * Method that reads the domains from the authentication tables and adds them into the given context.
        *
        * @param   ldbName
        *          The logical database name used when adding a domain.
        * @param   wa
        *          The context in which to add the domains.
        */
       public static void readDomains(String ldbName, SecurityOps.WorkArea wa)
       {
          if (!init)
          {
             return;
          }

          synchronized (lock)
          {
             Tenant defaultTenant = tenants.get(DEFAULT_TENANT_NAME);
             if (defaultTenant == null)
             {
                LOG.log(Level.WARNING, "Default tenant is not instantiated.");
                return;
             }

             TenantDatabaseDescriptor dbDesc = defaultTenant.dbLogicalInstances.get(ldbName);
             if (dbDesc == null)
             {
                LOG.log(Level.WARNING, "Default tenant does not have access to database " + ldbName);
                return;
             }

             try (Connection conn = DriverManager.getConnection(
                             dbDesc.getUrl(), dbDesc.getUsername(), dbDesc.getPassword()))
             {
                // First add the domain types
                try (ResultSet rs = conn.createStatement().executeQuery(tenantSQL.getSelectDomainTypes()))
                {
                   while (rs.next())
                   {
                      wa.addDomainType(ldbName, rs.getString(1), rs.getString(2), true);
                   }
                }

                // Then add the domains
                try (ResultSet rs = conn.createStatement().executeQuery(tenantSQL.getSelectDomains()))
                {
                   while (rs.next())
                   {
                      wa.addDomain(ldbName,
                                   rs.getString(1),
                                   rs.getString(2),
                                   rs.getString(3),
                                   SymmetricEncryption.decrypt(rs.getString(4)),
                                   rs.getBoolean(5),
                                   rs.getString(6));
                   }
                }
             }
             catch (SQLException e)
             {
                LOG.log(Level.WARNING, "Couldn't add the domains due to " + e.getMessage(), e);
             }
          }
       }

       /**
        * Retrieve the tenant name from the given domain.
        *
        * @param   domainName
        *          The name of the domain from which to retrieve the tenant name.
        * @param   ldbName
        *          The logical name of the database.
        *
        * @return  The name of the tenant that was assigned to the given domain.
        */
       public static String getTenantName(String domainName, String ldbName)
       {
          if (!init)
          {
             return null;
          }

          synchronized (lock)
          {
             Tenant defaultTenant = tenants.get(DEFAULT_TENANT_NAME);
             if (defaultTenant == null)
             {
                LOG.log(Level.WARNING, "Default tenant is not instantiated.");
                return null;
             }

             TenantDatabaseDescriptor dbDesc = defaultTenant.dbLogicalInstances.get(ldbName);
             if (dbDesc == null)
             {
                LOG.log(Level.WARNING, "Default tenant does not have access to database " + ldbName);
                return null;
             }

             SecurityOps.Domain domain = SecurityOps.findDomain(ldbName, domainName);
             if (domain != null)
             {
                return domain.getTenantName();
             }
          }

          return null;
       }

       /**
        * Registers a new {@code TenantMetaChangeListener} for receiving tenant definition changes.
        *
        * @param   tmcl
        *          The {@code TenantMetaChangeListener} to receive the notifications.
        *
        * @return  {@code true} if the operation is successful.
        */
       public static boolean registerMetaListener(TenantMetaChangeListener tmcl)
       {
          synchronized (lock)
          {
             return metaListeners.add(tmcl);
          }
       }

       /**
        * Deregister an existing {@code TenantMetaChangeListener} so it won't receive tenant definition changes.
        *
        * @param   tmcl
        *          The {@code TenantMetaChangeListener} to receive the notifications.
        *
        * @return  {@code true} if the operation is successful, if {@code false} the listener was not registered
        *          before.
        */
       public static boolean deregisterMetaListener(TenantMetaChangeListener tmcl)
       {
          synchronized (lock)
          {
             return metaListeners.remove(tmcl);
          }
       }

       /**
        * Encrypt a plain password. There are a few steps executed: <ol>
        *   <li>a random-length prefix is added to the plain text to add extra entropy;
        *   <li>the resulting string is encoded to a byte array using configured algorithm and key;
        *   <li>the resulting byte array is converted to a hex string for easier storage in database.
        * </pl>
        *
        * @param   plainPwd
        *          The decrypted password.
        *
        * @return  The encrypted password.
        */
       public static String encrypt(String plainPwd)
       {
          initCrypto();

          try
          {
             Random rnd = new Random();
             int i = rnd.nextInt(9);
             StringBuilder salted = new StringBuilder(String.valueOf((char) (i + '0')));
             while (i > 0)
             {
                salted.append((char)(rnd.nextInt('Z' - 'A') + 'a'));
                i--;
             }
             salted.append(plainPwd);
             byte[] enc = cp.process(salted.toString().getBytes(StandardCharsets.UTF_8),
                                     Cipher.ENCRYPT_MODE, key, iv);
             return String.valueOf(Hex.encodeHex(enc));
          }
          catch (NoSuchAlgorithmException           |
                 NoSuchProviderException            |
                 NoSuchPaddingException             |
                 InvalidKeyException                |
                 InvalidAlgorithmParameterException |
                 IllegalBlockSizeException          |
                 BadPaddingException e)
          {
             LOG.warning("Failed to encrypt the password. " +
                               "The configured password, initialization vector or cypher algorithm might be invalid", e);
             // keep the original (for temporary backward compatibility)
             return plainPwd;
          }
       }

       /**
        * Decrypt an encrypted password. There are a few steps executed: <ol>
        *   <li>the provided string is decoded from the hex string;
        *   <li>the resulting byte array is decrypted using the configured algorithm and key;
        *   <li>the randomizer prefix is removed and the result returned.
        * </pl>
        *
        * @param   codePwd
        *          The encrypted password.
        *
        * @return  The decrypted password.
        */
       public static String decrypt(String codePwd)
       {
          initCrypto();

          try
          {
             byte[] dec = cp.process(Hex.decodeHex(codePwd), Cipher.DECRYPT_MODE, key, iv);
             String s = new String(dec);
             int i = s.charAt(0) - '0';
             return s.substring(i + 1);
          }
          catch (NoSuchAlgorithmException           |
                 NoSuchProviderException            |
                 NoSuchPaddingException             |
                 InvalidKeyException                |
                 InvalidAlgorithmParameterException |
                 IllegalBlockSizeException          |
                 BadPaddingException                |
                 DecoderException e)
          {
             LOG.warning("Failed to decrypt the password. " +
                               "The password, initialization vector or cypher algorithm might have changed", e);
             // keep the original (for temporary backward compatibility)
             return codePwd;
          }
       }

       /**
        * Initialize the tenant managing statements. Each entry of {@code updateStmts} has an SQL update statement
        * string mapped by its column name.
        *
        * @param   tenantSQL
        *          The dialect specific statement provider.
        */
       private static void initializeStatements(Dialect.TenantSQL tenantSQL)
       {
          // Initialize the update tenant statements and add them to the map
          updateStmts.put(COL_TENANT_NAME, tenantSQL.getUpdateTenantName());
          updateStmts.put(COL_TENANT_DESC, tenantSQL.getUpdateTenantDescription());
          updateStmts.put(COL_TENANT_INFO, tenantSQL.getUpdateTenantInfo());
          updateStmts.put(COL_PHYSICAL_DB, tenantSQL.getUpdatePdbName());
          updateStmts.put(COL_LOGICAL_DB,  tenantSQL.getUpdateLdbName());
          updateStmts.put(COL_DB_URL,      tenantSQL.getUpdateDbUrl());
          updateStmts.put(COL_DB_USER,     tenantSQL.getUpdateUsername());
          updateStmts.put(COL_DB_PASS,     tenantSQL.getUpdatePassword());
       }

       /**
        * Method used for executing SQLs in order to create the required tables in the {@code landlord} database.
        *
        * @param   conn
        *          The connection to database.
        */
       private static void createTables(Connection conn)
       throws SQLException
       {
          try (Statement stmt = conn.createStatement())
          {
             stmt.execute(tenantSQL.getCreateMasterTable());
             stmt.execute(tenantSQL.getCreateMasterIndex());
             stmt.execute(tenantSQL.getCreateDatabasesTable());
             stmt.execute(tenantSQL.getCreateTenantDbIndex());
          }
       }

       /**
        * Method used for initializing the default tenant.
        */
       private static void addDefaultTenant()
       {
          Tenant defaultTenant = new Tenant(DEFAULT_TENANT_ID,
                                            DEFAULT_TENANT_NAME,
                                            DEFAULT_TENANT_DESCRIPTION,
                                            DEFAULT_TENANT_INFO,
                                            null);
          defaultTenant.status = true;
          tenants.put(defaultTenant.tenantName, defaultTenant);
          tenantsById.put(DEFAULT_TENANT_ID, defaultTenant);
       }

       /**
        * Read all tenants defined in database and populate the {@code tenants} data structure.<p>
        * Do not connect the database at this moment.
        *
        * @param   conn
        *          The connection to database.
        */
       private static void populateAllTenants(Connection conn)
       throws SQLException
       {
          try (ResultSet rs = conn.createStatement().executeQuery(tenantSQL.getSelectAllTenants()))
          {
             while (rs.next())
             {
                String tenantName = rs.getString(COL_TENANT_NAME);
                String tenantDesc = rs.getString(COL_TENANT_DESC);
                String tenantInfo = rs.getString(COL_TENANT_INFO);

                Tenant tenant = tenants.get(tenantName);
                if (tenant != null)
                {
                   LOG.log(Level.INFO, "Duplicate tenant " + tenantName);
                   continue; // already exists. Should not happen.
                }

                List<TenantDatabaseDescriptor> databases = new ArrayList<>();

                try (PreparedStatement stmt = conn.prepareStatement(tenantSQL.getSelectTenantDatabases()))
                {
                   stmt.setString(1, tenantName);
                   try (ResultSet dbs = stmt.executeQuery())
                   {
                      while (dbs.next())
                      {
                         databases.add(new TenantDatabaseDescriptor(
                               dbs.getString(COL_PHYSICAL_DB),
                               dbs.getString(COL_LOGICAL_DB),
                               dbs.getString(COL_DB_URL),
                               dbs.getString(COL_DB_USER),
                               decrypt(dbs.getString(COL_DB_PASS)),
                               getIntegerColumn(dbs, Dialect.TenantSQL.C3P0_MAX_STMTS),
                               getIntegerColumn(dbs, Dialect.TenantSQL.C3P0_MIN_POOL),
                               getIntegerColumn(dbs, Dialect.TenantSQL.C3P0_MAX_POOL),
                               getIntegerColumn(dbs, Dialect.TenantSQL.C3P0_ACQ_INC),
                               getIntegerColumn(dbs, Dialect.TenantSQL.C3P0_MAX_IDLE),
                               null));
                      }
                   }

                   int nextId = getNextId(false);
                   tenant = new Tenant(nextId,
                                       (UUID) rs.getObject("tenant_id"),
                                       tenantName,
                                       tenantDesc,
                                       tenantInfo,
                                       databases);
                   tenant.setStatus(rs.getBoolean("status"));
                   tenants.put(tenantName, tenant);
                   tenantsById.put(Math.abs(nextId), tenant);
                }
             }
          }
       }

       /**
        * Extracts and returns an integer value of a column from a result set, taking into consideration if the
        * value is {@code null}.
        *
        * @param   rs
        *          The {@code ResultSet} to be queried.
        * @param   colName
        *          The column name.
        *
        * @return  The integer value of the specified column, possible {@code null}.
        *
        * @throws  SQLException
        *          In case an error occurs.
        */
       private static Integer getIntegerColumn(ResultSet rs, String colName)
       throws SQLException
       {
          int intVal = rs.getInt(colName);
          return rs.wasNull() ? null : intVal;
       }

       /**
        * Logically connect the tenant to its respective databases.
        *
        * @param   tenant
        *          The tenant to be connected to the database.
        */
       private static void connectTenantImpl(Tenant tenant)
       {
          try
          {
             if (!DatabaseManager.activateTenantDatabases(tenant))
             {
                LOG.log(Level.WARNING, "At least one database configuration failed.");
             }
          }
          catch (PersistenceException e)
          {
             LOG.log(Level.WARNING,
                     "Could not activate/configure this tenant databases due to " + e.getMessage(), e);
          }
       }

       /**
        * Update a tenant from the master table.
        *
        * @param   conn
        *          The connection to landlord database.
        * @param   tenantName
        *          The name of the tenant that is targeted by the update.
        * @param   pdbName
        *          The physical name of the database that is targeted by the update.
        * @param   column
        *          The column that will be updated.
        * @param   newValue
        *          The value that will replace the one from the table.
        * @param   databaseUpdate
        *          Is this update targeting a database change?
        *
        * @return  {@code true} if the operation was successful, {@code false} otherwise.
        */
       private static boolean updateTenantImpl(Connection conn,
                                               String tenantName,
                                               String pdbName,
                                               String column,
                                               String newValue,
                                               boolean databaseUpdate)
       {
          String stmtStr = updateStmts.get(column);
          if (stmtStr == null)
          {
             LOG.log(Level.WARNING, "Cannot find the column " + column + " in the master table");
             return false;
          }

          try (PreparedStatement stmt = conn.prepareStatement(stmtStr))
          {
             if (column.equals(COL_TENANT_NAME))
             {
                // We need to update both the master table and the tenant_databases table
                stmt.setString(1, newValue);
                stmt.setString(2, tenantName);
                stmt.execute();

                try (PreparedStatement dbStmt = conn.prepareStatement(tenantSQL.getUpdateTenantDbName()))
                {
                   dbStmt.setString(1, newValue);
                   dbStmt.setString(2, tenantName);
                   dbStmt.execute();
                   return true;
                }
             }

             stmt.setString(1, column.equals(COL_DB_PASS) ? encrypt(newValue) : newValue);
             stmt.setString(2, tenantName);
             if (databaseUpdate)
             {
                stmt.setString(3, pdbName);
             }

             stmt.execute();
             return true;
          }
          catch (SQLException sqle)
          {
             LOG.log(Level.WARNING, "Could not update tenant due to " + sqle.getMessage(), sqle);
             return false;
          }
       }

       /**
        * Generates a new unique id for a tenant.
        * <p>
        * This private method is synchronized externally (make sure each time it is called happens from a
        * {@code lock} synchronized block).
        *
        * @param   superTenant
        *          {@code true} if the defined entity is a super-tenant.
        *
        * @return  a new unique id for a new tenant. If this is a super-tenant, the value is negative, according
        *          to 4GL reference.
        */
       private static int getNextId(boolean superTenant)
       {
          if (tenantsById.size() == MAX_TENANTS)
          {
             throw new RuntimeException("No more than " + MAX_TENANTS + " tenant definitions are allowed.");
          }

          // pick a random number
          int newId;

          // if it is already taken, get the next free, to avoid possible infinite loop
          do
          {
             newId = 1 + (int) (Math.random() * MAX_TENANTS);
          }
          while (tenantsById.containsKey(newId));

          return superTenant ? -newId : newId;
       }

       /**
        * Given the name of a column from the tenant API, retrieve the equivalent column name from the landlord
        * database.
        *
        * @param   field
        *          The field name from the tenant API.
        *
        * @return  The correct column name from the landlord database.
        */
       private static String determineDatabaseFieldName(String field)
       {
          switch (field)
          {
             case JSON_TENANT_NAME:
                return COL_TENANT_NAME;
             case JSON_TENANT_DESCRIPTION:
                return COL_TENANT_DESC;
             case JSON_TENANT_INFO:
                return COL_TENANT_INFO;
             case JSON_PHYSICAL_NAME:
                return COL_PHYSICAL_DB;
             case JSON_LOGICAL_NAME:
                return COL_LOGICAL_DB;
          }

          // The other field names don't require changes
          return field;
       }

       /**
        * Sets a specific value to a field in a record, if that exists in the properties set. If the {@code field}
        * doesn't appear in the property set, it is not set.
        *
        * @param   domain
        *          The record to be changed.
        * @param   p
        *          The set o properties, as a {@code JsonNode} node.
        * @param   field
        *          The legacy name of the field.
        * @param   conv
        *          The conversion used for the value.
        */
       private static void setField(BufferImpl domain, JsonNode p, String field, Function<JsonNode, Object> conv)
       {
          if (!p.has(field))
          {
             return;
          }

          JsonNode jsonNode = p.get(field);
          try
          {
             domain.dereference(field, conv.apply(jsonNode));
          }
          catch (Throwable e)
          {
             throw new RuntimeException(
                   "Failed to set " + domain.doGetName() + "." + field + "' to requested value.", e);
          }
       }

       /**
        * Initializes the cryptographic data structures with values read from server's registry.
        */
       private static void initCrypto()
       {
          if (cp != null)
          {
             return;
          }

          synchronized (lock)
          {
             DirectoryService ds = DirectoryService.getInstance();
             if (!ds.bind())
             {
                throw new RuntimeException("Directory bind failed");
             }

             String algorithm = "AES_CBC_128";
             String keyStr = "tenant";
             String ivStr = "tenant-iv";
             try
             {
                algorithm = Utils.getDirectoryNodeString(ds,
                                                         "persistence/landlord_encryption_algorithm",
                                                         algorithm,
                                                         false);
                keyStr = Utils.getDirectoryNodeString(ds,
                                                      "persistence/landlord_encryption_key",
                                                      keyStr,
                                                      false);
                ivStr = Utils.getDirectoryNodeString(ds,
                                                     "persistence/landlord_encryption_iv",
                                                     ivStr,
                                                     false);
             }
             finally
             {
                ds.unbind();
             }

             cp = new CryptoUtils.CipherParams(algorithm);
             key = cp.newKey();
             byte[] keyBytes = keyStr.getBytes(StandardCharsets.UTF_8);
             System.arraycopy(keyBytes, 0, key, 0, Math.min(key.length, keyBytes.length));
             iv = cp.newIV();
             byte[] ivBytes = ivStr.getBytes(StandardCharsets.UTF_8);
             System.arraycopy(ivBytes, 0, iv, 0, Math.min(iv.length, ivBytes.length));
          }
       }

       /**
        * Basic validate of a resource name, to avoid an 4GL code injection.
        *
        * @param   name
        *          The name to be validated.
        *
        * @return  {@code true} is the name 'looks like' a valid resource identifier.
        */
       private static boolean validateName(String name)
       {
          return name != null            && // avoid basic NPE
                 name.indexOf(' ') == -1 && // single word
                 name.indexOf('.') == -1 && // no end of 4GL statement
                 name.indexOf('(') == -1;
       }

       /**
        * Get a connection to landlord database. Depending on the {@code trans} parameter, the connection can
        * be transactional or auto-commited. Before returning the object, it will be tested for validity. If it is
        * not valid (the connectivity to database was lost), an attempt to reset is performed. The method will
        * NOT do a second validity check, returning this way a possible invalid connection which will stop the
        * execution with a {@code SQLException} when it will be used.
        *
        * @param   trans
        *          If {@code true} the connection will be transactional, otherwise auto-committing.
        *
        * @return  the connection to landlord database.
        *
        * @throws  SQLException
        *          In case a valid connection cannot be obtained.
        */
       private static Connection getConnection(boolean trans)
       throws SQLException
       {
          Connection conn = ds.getConnection();
          if (!conn.isValid(1))
          {
             conn.close();
             ds.softResetAllUsers();

             // force one more
             conn = ds.getConnection();
          }

          conn.setAutoCommit(!trans);
          return conn;
       }

       /**
        * Executes a fragment of code inside a temporary global block.
        *
        * @param   code
        *          The code to be executed.
        */
       public static void runInGlobalBlock(Runnable code)
       {
          // the code is simplified version of {@code Agent.prepare()}. See that method for additional information
          try
          {
             // make sure the TM is loaded before LT
             TransactionManager.isHeadless();

             // set this as batch mode
             EnvironmentOps.setBatchMode(true);

             ProcedureManager.load();

             // push a global scope for keeping buffers states
             TransactionManager.pushScope("temporary-global-scope",
                                          TransactionManager.NO_TRANSACTION,
                                          true,
                                          true,
                                          false,
                                          false);
             DatabaseManager.autoConnect();

             code.run();
          }
          finally
          {
             // reset the context after an operation. Mandaroty to be called in pair with {@code prepare()}.
             // pop the global scope, which will be pushed again in the 'prepare' method.
             TransactionManager.popScope();

             // reset the context
             try
             {
                SecurityManager.getInstance().contextSm.resetContext();
             }
             catch (RestrictedUseException e)
             {
                // ignore
             }
          }
       }

       /**
        * Inner class responsible with storing information about a tenant.
        */
       public static class Tenant
       {
          /** The tenant ID. */
          private final int id;

          /** The tenant type. */
          private final int type;

          /** The tenant UID. */
          private final UUID tenantId; // TODO: is this requested by customer? can we drop it?

          /** The name of the tenant. */
          private String tenantName;

          /** The description of the tenant. */
          private String tenantDescription;

          /** The info of the tenant. */
          private String tenantInfo;

          /** A map that stores database settings for a specific database based on the database physical name. */
          private final Map<String, TenantDatabaseDescriptor> dbPhysicalInstances = new HashMap<>();

          /** A map that stores database settings for a specific database based on the database logical name. */
          private final Map<String, TenantDatabaseDescriptor> dbLogicalInstances = new HashMap<>();

          /** The status of the tenant (enabled/disabled). */
          private boolean status;

          /**
           * Constructor that generates a random {@code UUID} for the tenant.
           *
           * @param   id
           *          The tenant's id.
           * @param   tenantName
           *          The name of the tenant.
           * @param   tenantDescription
           *          The description of the tenant.
           * @param   tenantInfo
           *          The info of the tenant.
           * @param   databases
           *          A list of the databases that will be used by the tenant.
           */
          private Tenant(int id,
                         String tenantName,
                         String tenantDescription,
                         String tenantInfo,
                         List<TenantDatabaseDescriptor> databases)
          {
             this(id, UUID.randomUUID(), tenantName, tenantDescription, tenantInfo, databases);
          }

          /**
           * Constructor that sets all the fields required by a tenant, including the tenant id UUID.
           *
           * @param   id
           *          The tenant's id.
           * @param   tenantId
           *          The UID of the tenant.
           * @param   tenantName
           *          The name of the tenant.
           * @param   tenantDescription
           *          The description of the tenant.
           * @param   tenantInfo
           *          The info of the tenant.
           * @param   databases
           *          A list of the databases that will be used by the tenant.
           */
          private Tenant(int id,
                         UUID tenantId,
                         String tenantName,
                         String tenantDescription,
                         String tenantInfo,
                         List<TenantDatabaseDescriptor> databases)
          {
             this.id = id;
             this.type = (id == DEFAULT_TENANT_ID) ? TYPE_DEFAULT
                                                   : (id > 0 ? TYPE_REGULAR : TYPE_SUPER);
             this.tenantId = tenantId;
             this.tenantName = tenantName;
             this.tenantDescription = tenantDescription;
             this.tenantInfo = tenantInfo;
             this.status = false;
             if (databases != null)
             {
                for (int i = 0; i < databases.size(); i++)
                {
                   TenantDatabaseDescriptor db = databases.get(i);
                   dbPhysicalInstances.put(db.getPdbName(), db);
                   dbLogicalInstances.put(db.getLdbName(), db);
                }
             }
          }

          /**
           * Copy constructor.
           *
           * @param   other
           *          The other instance from which to copy the information.
           */
          private Tenant(Tenant other)
          {
             this.id = other.id;
             this.type = other.type;
             this.tenantId = other.tenantId;
             this.tenantName = other.tenantName;
             this.tenantDescription = other.tenantDescription;
             this.tenantInfo = other.tenantInfo;
             this.status = other.status;
             for (Map.Entry<String, TenantDatabaseDescriptor> entry : other.dbPhysicalInstances.entrySet())
             {
                dbPhysicalInstances.put(entry.getKey(), new TenantDatabaseDescriptor(entry.getValue()));
             }

             for (Map.Entry<String, TenantDatabaseDescriptor> entry : other.dbLogicalInstances.entrySet())
             {
                dbLogicalInstances.put(entry.getKey(), new TenantDatabaseDescriptor(entry.getValue()));
             }
          }

          /**
           * Getter for the tenant name.
           *
           * @return  The name of this tenant.
           */
          public String getTenantName()
          {
             return tenantName;
          }

          /**
           * Getter for the tenant description.
           *
           * @return  The description of this tenant.
           */
          public String getTenantDescription()
          {
             return tenantDescription;
          }

          /**
           * Getter for the tenant info.
           *
           * @return  The info of this tenant.
           */
          public String getTenantInfo()
          {
             return tenantInfo;
          }

          /**
           * Get the database settings/credentials of a given database.
           *
           * @param   dbName
           *          The name of the database for which to retrieve the information.
           * @param   physical
           *          {@code true} if the given database name is the physical one,
           *          {@code false} if the database name is the logical one.
           *
           * @return  A {@code TenantDatabaseDescriptor} that stores the information for the given database.
           */
          public TenantDatabaseDescriptor getDatabaseCredentials(String dbName, boolean physical)
          {
             return physical ? dbPhysicalInstances.get(dbName) : dbLogicalInstances.get(dbName);
          }

          /**
           * Get all the database settings/credentials assigned to this tenant.
           *
           * @return  A map with the information for all the databases.
           */
          public Map<String, TenantDatabaseDescriptor> getDatabaseCredentials()
          {
             return dbPhysicalInstances;
          }

          /**
           * Getter for the tenant ID.
           *
           * @return  The ID used by this tenant.
           */
          public int id()
          {
             return id;
          }

          /**
           * Getter for the tenant type.
           *
           * @return  The type of the tenant.
           */
          public int getType()
          {
             return type;
          }

          /**
           * Getter for the tenant UID.
           *
           * @return  The UID used by this tenant.
           */
          public UUID getId()
          {
             return tenantId;
          }

          /**
           * Getter for the status field (enabled/disabled).
           *
           * @return  {@code true} if the tenant is enabled, {@code false} otherwise.
           */
          public boolean getStatus()
          {
             return status;
          }

          /**
           * Set a specific field for this tenant.
           * Possible values for the {@code fieldName} parameter are:
           * <ul>
           *    <li> tenant_name - the name of the tenant.
           *    <li> tenant_description - the description of the tenant.
           *    <li> db_name - the name of a database that this tenant uses.
           *    <li> url - the URL to a database.
           *    <li> username - the username required in order to access a database.
           *    <li> password - the password required in order to access a database.
           * </ul>
           *
           * @param   pdbName
           *          The physical name of the database for which to perform the update.
           * @param   fieldName
           *          The name of the field to be updated.
           * @param   value
           *          The new value for the given field.
           */
          public void setField(String pdbName, String fieldName, String value)
          {
             switch (fieldName)
             {
                case COL_TENANT_NAME:
                   this.tenantName = value;
                   break;
                case COL_TENANT_DESC:
                   this.tenantDescription = value;
                   break;
                case COL_TENANT_INFO:
                   this.tenantInfo = value;
                   break;
                case COL_PHYSICAL_DB:
                   // Move the values from the old database name to the new one
                   TenantDatabaseDescriptor db = dbPhysicalInstances.remove(pdbName);
                   db.setPdbName(value);
                   this.dbPhysicalInstances.put(value, db);
                   break;
                case COL_LOGICAL_DB:
                   TenantDatabaseDescriptor dbDesc = dbPhysicalInstances.get(pdbName);
                   String oldLdbName = dbDesc.getLdbName();
                   dbDesc.setLdbName(value);
                   this.dbLogicalInstances.put(value, dbLogicalInstances.remove(oldLdbName));
                   break;
                case COL_DB_URL:
                   this.dbPhysicalInstances.get(pdbName).setUrl(value);
                   break;
                case COL_DB_USER:
                   this.dbPhysicalInstances.get(pdbName).setUsername(value);
                   break;
                case COL_DB_PASS:
                   this.dbPhysicalInstances.get(pdbName).setPassword(value);
                   break;
                default:
                   break;
             }
          }

          /**
           * Setter for the {@code status} field.
           * A value of {@code true} denotes the enabling of this tenant, while {@code false} denotes
           * disabling this tenant.
           *
           * @param   value
           *          The new value for the {@code status} field.
           */
          public void setStatus(boolean value)
          {
             this.status = value;
          }

          /**
           * Add a database configuration for this tenant.
           *
           * @param   pdbName
           *          The physical name of the database.
           * @param   db
           *          The database information to be added.
           */
          private void addDatabaseInstance(String pdbName, TenantDatabaseDescriptor db)
          {
             dbPhysicalInstances.put(pdbName, db);
             dbLogicalInstances.put(db.getLdbName(), db);
          }

          /**
           * Remove the database configuration for this tenant.
           *
           * @param   pdbName
           *          The physical name of the database for which to remove the configuration.
           */
          private void removeDatabaseInstance(String pdbName)
          {
             TenantDatabaseDescriptor db = dbPhysicalInstances.remove(pdbName);
             dbLogicalInstances.remove(db.getLdbName());
          }

          @Override
          public String toString()
          {
             return "Tenant (" + id + " = \"" + tenantName + "\", " + (status ? "" : "disabled") + ")";
          }
       }
    }