PooledDataSourceProvider.java

/*
** Module   : PooledDataSourceProvider.java
** Abstract : Implementation of DataSourceProvider that offers a pooled DataSource.
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------------Description----------------------------------------
** 001 ECF 20191001 Created initial version with basic runtime support.
** 002 AIL 20201016 Set c3p0 options through setters instead of system properties.
** 003 IAS 20210807 Added c3p0 extensions processing and support for useJavaUDFs flag.
**     ECF 20210910 Minor cleanup.
**     IAS 20220816 Do not use ConnectionCustomizer for non-PostgreSQL databases.
**     TW  20221024 Implement support for driver_properties.
** 004 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 005 IAS 20230208 Dialect-specific error handler support.
** 006 RAA 20240423 getDataSource now includes the database configuration.
**     RAA 20240426 Added null check in setDatabaseConnectionSettingsTenant.
**     RAA 20240627 Added database parameter to setDatabaseConnectionSettingsTenant function.
**     RAA 20240702 Replaced DBCredentials occurrences with TenantDatabaseDescriptor.
**     RAA 20240715 Clarified the type of database name occurrences.
**     RAA 20240726 Updated parameters of the getDatabaseCredentials() call.
** 007 OM  20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
**                  based on [sharedDb] parameter.
** 008 OM  20250211 New parameter for TenantManager.getTenant().
** 009 OM  20250331 Code maintenance.
*/

/*
** 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.lang.reflect.*;
import java.util.*;
import java.util.logging.*;
import javax.sql.DataSource;

import com.goldencode.p2j.util.logging.*;
import com.mchange.v2.c3p0.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.orm.TenantManager.*;
import com.goldencode.p2j.persist.orm.types.*;

/**
 * Implementation of DataSourceProvider that offers a pooled DataSource.
 */
class PooledDataSourceProvider
implements DataSourceProvider
{
   /** A collection of setters for a ComboPooledDataSource */
   private static final Map<String, Method> cpdsSetters = new HashMap<>();
   
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(PooledDataSourceProvider.class.getName());
   
   /** The PooledDataSourceProvider instance. */
   private static final PooledDataSourceProvider pdsp = new PooledDataSourceProvider();
   
   /** Initialize the cpdsSetters collection */
   static
   {
      Method[] methods = ComboPooledDataSource.class.getMethods();
      for (Method m : methods)
      {
         if (m.getName().startsWith("set"))
         {
            cpdsSetters.put(m.getName().substring("set".length()).toLowerCase(), m);
         }
      }
   }
   
   /**
    * Default constructor.
    */
   private PooledDataSourceProvider()
   {
   }
   
   /**
    * Return the instance of {@code PooledDataSourceProvider}.
    * 
    * @return  The instance to work with.
    */
   public static PooledDataSourceProvider getInstance()
   {
      return pdsp;
   }
   
   /**
    * Obtain the {@code DataSource} for a specific {@code Database}.
    * 
    * @param   database
    *          The FWD {@code database} that needs a connection to backing database.
    * @param   cfg
    *          The configuration for the given database.
    * 
    * @return  The {@code DataSource} for the specified {@code database}.
    * 
    * @throws  PersistenceException
    *          If any error occurs in the process.
    */
   @Override
   public DataSource getDataSource(Database database, DatabaseConfig cfg)
   throws PersistenceException
   {
      return getDataSource(database, cfg, null);
   }
   
   /**
    * Obtain the {@code DataSource} for a specific {@code Database}.
    * 
    * @param   database
    *          The FWD {@code database} that needs a connection to backing database.
    * @param   cfg
    *          The configuration for the given database.
    * @param   tenantName
    *          The name of the tenant.
    * 
    * @return  The {@code DataSource} for the specified {@code database}.
    * 
    * @throws  PersistenceException
    *          If any error occurs in the process.
    */
   public DataSource getDataSource(Database database, DatabaseConfig cfg, String tenantName)
   throws PersistenceException
   {
      try
      {
         if (cfg == null)
         {
            cfg = DatabaseManager.getConfiguration(database);
         }
         
         ComboPooledDataSource ds = new ComboPooledDataSource();
         Settings settings = cfg.getOrmSettings();
         Dialect dialect = Dialect.getDialect(settings);
         String driver = dialect.getDriverClassName();
         
         // set (optional) driver properties first, so they won't get overwritten
         Properties props = new Properties();
         Map<String,Object> driver_properties = settings.getAllForCategory("connection.driver_properties");
         for (String key : driver_properties.keySet())
         {
            String shortkey=key.substring("connection.driver_properties.".length()); 
            props.setProperty(shortkey, (String)driver_properties.get(key));
         }
         ds.setProperties(props);
         ds.setDriverClass(driver);
         
         String url;
         if (tenantName != null && !tenantName.equalsIgnoreCase(TenantManager.DEFAULT_TENANT_NAME))
         {
            url = setDatabaseConnectionSettingsTenant(ds, tenantName, database.getName());
         }
         else
         {
            // basic connection settings
            url = settings.getString(Settings.URL, null);
            ds.setJdbcUrl(url);
            String user = settings.getString(Settings.USER, null);
            ds.setUser(user);
            String pass = settings.getString(Settings.PASSWORD, null);
            ds.setPassword(pass);   
         }
         
         Map<String, Object> extensions = new HashMap<>();
         
         // additional configuration
         for (Map.Entry<String, Object> next : settings.getAllForCategory("c3p0").entrySet())
         {
            String key = next.getKey();
            Object value = next.getValue();
            if (key.startsWith("c3p0.extensions."))
            {
               extensions.put(key.substring("c3p0.extensions.".length()), value);
               continue;
            }
            try
            {
               Method setter = cpdsSetters.get(key.substring("c3p0.".length()).toLowerCase());
               if (setter == null)
               {
                  if (LOG.isLoggable(Level.WARNING))
                  {
                     LOG.log(Level.WARNING, "Can't find a setter for the specified c3p0 parameter " + key);
                  }
               }
               else
               {
                  setter.invoke(ds, value);
               }
            }
            catch (IllegalArgumentException e)
            {
               if (LOG.isLoggable(Level.WARNING))
               {
                  LOG.log(Level.WARNING, "The provided value is not suitable for the c3p0 parameter " + key);
               }
            }
            catch (IllegalAccessException | InvocationTargetException e)
            {
               if (LOG.isLoggable(Level.SEVERE))
               {
                  LOG.log(Level.SEVERE, "Failed to set a value for the c3p0 parameter " + key);
               }
            }
         }
         Class<? extends AbstractConnectionCustomizer> customizer = 
                  DatabaseManager.getDialect(database).connectionCustomizer(cfg);
         if (ds.getConnectionCustomizerClassName() == null && customizer != null)
         {
            ds.setConnectionCustomizerClassName(customizer.getName());
         }
         if (!cfg.isUseJavaUDFs())
         {
            extensions.putIfAbsent("useJavaUdfs", "n");
         }
         if (!extensions.isEmpty())
         {
            ds.setExtensions(extensions);
         }
         
         // setup the blob and clob creators
         TypeManager.addBlobCreator(url, dialect::blobCreator);
         TypeManager.addClobCreator(url, dialect::clobCreator);
         
         return ds;
      }
      catch (PropertyVetoException exc)
      {
         String msg = "Error initializing C3P0 connection pool for database " + database;
         
         throw new PersistenceException(msg, exc);
      }
   }
   
   /**
    * Given a {@code DataSource}, set its URL, username and password, based on the given tenant
    * and database combination.
    * 
    * @param   ds
    *          The given data source.
    * @param   tenantName
    *          The name of the tenant.
    * @param   pdbName
    *          The physical database name.
    *          
    * @return  The URL that has been set to the data source.
    */
   private String setDatabaseConnectionSettingsTenant(ComboPooledDataSource ds, 
                                                      String tenantName, 
                                                      String pdbName)
   {
      Tenant tenant = TenantManager.getTenant(tenantName, null);
      if (tenant == null)
      {
         LOG.log(Level.WARNING, "Tenant is not configured in the landlord database");
         return null;
      }
      
      TenantDatabaseDescriptor dbCredentials = tenant.getDatabaseCredentials(pdbName, true);
      String url = dbCredentials.getUrl();
      ds.setJdbcUrl(url);
      ds.setUser(dbCredentials.getUsername());
      ds.setPassword(dbCredentials.getPassword()); 
      
      return url;
   }
}