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;
}
}