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") + ")";
      }
   }
}