B4J=true Group=Default Group ModulesStructureVersion=1 Type=Class Version=4.19 @EndOfDesignText@ ' Módulo de clase: RDCConnector ' Esta clase gestiona el pool de conexiones a una base de datos específica utilizando la librería C3P0. ' Cada instancia de RDCConnector es responsable de una base de datos definida en un archivo 'config.DBx.properties'. ' Se encarga de inicializar el pool, obtener conexiones, cargar comandos SQL y proporcionar estadísticas del pool. Sub Class_Globals ' --- Variables globales de la clase --- ' Objeto principal para gestionar el pool de conexiones de la base de datos (usa C3P0 internamente). Private pool As ConnectionPool ' Bandera para activar/desactivar el modo de depuración de queries. ' Cuando está en True, los comandos SQL se recargan en cada petición (útil en desarrollo). Private DebugQueries As Boolean ' Almacena los comandos SQL específicos de esta base de datos, cargados de su archivo de configuración. Public commands As Map ' El puerto que el servidor HTTP usará. Este valor se lee del 'config.properties' de la base de datos principal (DB1). Public serverPort As Int ' Indica si se debe usar el pool de conexiones. Siempre True en este diseño, ya que C3P0 es esencial. Public usePool As Boolean = True ' Almacena la configuración completa (DriverClass, JdbcUrl, User, Password, InitialPoolSize, etc.) ' cargada de su respectivo archivo .properties. Public config As Map ' Indica si la tolerancia a parámetros de más está activa. Public IsParameterToleranceEnabled As Boolean End Sub ' Subrutina de inicialización para el conector de una base de datos específica. ' Se llama una vez por cada base de datos (DB1, DB2, DB3, DB4) al iniciar el servidor en Main.AppStart. ' DB: El identificador único de la base de datos (ej. "DB1", "DB2"). Public Sub Initialize(DB As String) ' Si el identificador es "DB1", se usa una cadena vacía para que File.ReadMap cargue "config.properties" (el archivo por defecto). If DB.EqualsIgnoreCase("DB1") Then DB = "" ' PASO 1: Cargar la configuración desde el archivo .properties correspondiente. ' Es CRUCIAL que se asigne a la variable de CLASE 'config' (sin 'Dim' local) ' para que la configuración cargada del archivo sea persistente para esta instancia del conector. config = LoadConfigMap(DB) ' Leer la configuración de tolerancia de parámetros Dim toleranceSetting As Int = config.GetDefault("parameterTolerance", 0).As(Int) ' Por defecto, 0 (estricto) IsParameterToleranceEnabled = (toleranceSetting = 1) ' La tolerancia se habilita si el valor es 1 If IsParameterToleranceEnabled Then Log($"RDCConnector.Initialize para ${DB}: Tolerancia a parámetros de más, HABILITADA."$) Else Log($"RDCConnector.Initialize para ${DB}: Tolerancia a parámetros de más, DESHABILITADA (modo estricto)."$) End If ' Bloque Try-Catch para la inicialización y configuración del pool. ' Esto es esencial para capturar cualquier error crítico que impida la conexión inicial a la base de datos. Try ' PASO 2: Inicializar el objeto B4X ConnectionPool. ' Esto crea la instancia subyacente de com.mchange.v2.c3p0.ComboPooledDataSource (la librería C3P0). ' Se le pasan los parámetros básicos para que C3P0 pueda construirse. pool.Initialize(config.Get("DriverClass"), config.Get("JdbcUrl"), config.Get("User"), config.Get("Password")) ' Obtener la referencia JavaObject para acceder a métodos de configuración avanzados de C3P0. Dim jo As JavaObject = pool ' PASO 3: Aplicar *todas* las propiedades de configuración de C3P0 INMEDIATAMENTE. ' Esto debe ocurrir *después* de 'pool.Initialize' pero *antes* de que C3P0 intente realmente adquirir conexiones. ' Esto asegura que las configuraciones sean efectivas desde el primer intento de conexión. ' Lectura de los valores desde el archivo de configuración, con valores por defecto si no se encuentran. Dim initialPoolSize As Int = config.GetDefault("InitialPoolSize", 3) Dim minPoolSize As Int = config.GetDefault("MinPoolSize", 2) Dim maxPoolSize As Int = config.GetDefault("MaxPoolSize", 5) Dim acquireIncrement As Int = config.GetDefault("AcquireIncrement", 5) ' Configuración de los parámetros del pool de conexiones C3P0: jo.RunMethod("setInitialPoolSize", Array(initialPoolSize)) ' Define el número de conexiones que se intentarán crear al iniciar el pool. jo.RunMethod("setMinPoolSize", Array(minPoolSize)) ' Fija el número mínimo de conexiones que el pool mantendrá abiertas. jo.RunMethod("setMaxPoolSize", Array(maxPoolSize)) ' Define el número máximo de conexiones simultáneas. jo.RunMethod("setAcquireIncrement", Array(acquireIncrement)) ' Cuántas conexiones nuevas se añaden en lote si el pool se queda sin disponibles. jo.RunMethod("setMaxIdleTime", Array As Object(config.GetDefault("MaxIdleTime", 300))) ' Tiempo máximo de inactividad de una conexión antes de cerrarse (segundos). jo.RunMethod("setMaxConnectionAge", Array As Object(config.GetDefault("MaxConnectionAge", 900))) ' Tiempo máximo de vida de una conexión (segundos). jo.RunMethod("setCheckoutTimeout", Array As Object(config.GetDefault("CheckoutTimeout", 60000))) ' Tiempo máximo de espera por una conexión del pool (milisegundos). ' LÍNEAS CRÍTICAS PARA FORZAR UN COMPORTAMIENTO NO SILENCIOSO DE C3P0: ' Por defecto, C3P0 puede reintentar muchas veces y no lanzar una excepción si las conexiones iniciales fallan. ' Estas líneas fuerzan a C3P0 a ser estricto y reportar errores de inmediato. jo.RunMethod("setAcquireRetryAttempts", Array As Object(2)) ' Limita los intentos iniciales de adquisición a 1. jo.RunMethod("setBreakAfterAcquireFailure", Array As Object(True)) ' ¡Forza a C3P0 a lanzar una excepción si falla al adquirir conexiones! ' PASO 4: Forzar la creación de conexiones iniciales y verificar el estado. ' Este paso es VITAL. Obliga a C3P0 a intentar establecer las conexiones iniciales (InitialPoolSize) ' *con la configuración ya establecida*. Si hay un problema de conectividad real, la excepción ' se capturará aquí y se reportará, evitando "fallos silenciosos". Dim tempCon As SQL = pool.GetConnection ' Adquiere una conexión para forzar al pool a inicializarse. If tempCon.IsInitialized Then tempCon.Close ' Devolvemos la conexión inmediatamente al pool para que esté disponible. End If ' com.mchange.v2.c3p0.ComboPooledDataSource [ ' acquireIncrement -> 3, ' acquireRetryAttempts -> 30, ' acquireRetryDelay -> 1000, ' autoCommitOnClose -> False, ' automaticTestTable -> Null, ' breakAfterAcquireFailure -> False, ' checkoutTimeout -> 20000, ' connectionCustomizerClassName -> Null, ' connectionTesterClassName -> com.mchange.v2.c3p0.impl.DefaultConnectionTester, ' contextClassLoaderSource -> caller, ' dataSourceName -> 2rvxvdb7cyxd8zlw6dyb|63021689, ' debugUnreturnedConnectionStackTraces -> False, ' description -> Null, ' driverClass -> oracle.jdbc.driver.OracleDriver, ' extensions -> {}, ' factoryClassLocation -> Null, ' forceIgnoreUnresolvedTransactions -> False, ' forceSynchronousCheckins -> False, ' forceUseNamedDriverClass -> False, ' identityToken -> 2rvxvdb7cyxd8zlw6dyb|63021689, ' idleConnectionTestPeriod -> 600, ' initialPoolSize -> 3, ' jdbcUrl -> jdbc:oracle:thin:@//10.0.0.110:1521/DBKMT, ' maxAdministrativeTaskTime -> 0, ' maxConnectionAge -> 0, ' maxIdleTime -> 1800, ' maxIdleTimeExcessConnections -> 0, ' maxPoolSize -> 5, ' maxStatements -> 150, ' maxStatementsPerConnection -> 0, ' minPoolSize -> 3, ' numHelperThreads -> 3, ' preferredTestQuery -> DBMS_SESSION.SET_IDENTIFIER('whatever'), ' privilegeSpawnedThreads -> False, ' properties -> {password=******, user=******}, ' propertyCycle -> 0, ' statementCacheNumDeferredCloseThreads -> 0, ' testConnectionOnCheckin -> False, ' testConnectionOnCheckout -> True, ' unreturnedConnectionTimeout -> 0, ' userOverrides -> {}, ' usesTraditionalReflectiveProxies -> False ' ] ' Catch ' Si ocurre un error durante la inicialización del pool o al forzar la conexión, ' este Log es CRÍTICO para el diagnóstico, especialmente en un entorno de producción. Dim ErrorMsg As String = $"RDCConnector.Initialize para ${DB}: ERROR CRÍTICO al inicializar/forzar conexión: ${LastException.Message}"$ Log(ErrorMsg) Main.LogServerError("ERROR", "RDCConnector.Initialize", ErrorMsg, DB, Null, Null) End Try ' Configuración de depuración de queries. Se activa automáticamente si el proyecto se ejecuta en modo DEBUG. #If DEBUG ' DebugQueries = True ' Descomentar para activar la recarga de comandos en cada petición en desarrollo. #Else DebugQueries = False #End If ' Se obtiene el puerto del servidor HTTP desde la configuración de esta base de datos. ' Nota: En el diseño actual, el puerto principal lo define DB1 (config.properties). serverPort = config.Get("ServerPort") ' Asegura que el identificador DB no sea una cadena vacía para la carga de comandos. ' Esto es relevante si DB era "DB1" y se convirtió a "" al inicio de esta subrutina. If DB = "" Then DB = "DB1" ' Carga los comandos SQL predefinidos de esta base de datos en el mapa global 'commandsMap' de Main. LoadSQLCommands(config, DB) End Sub ' Carga el mapa de configuración (JdbcUrl, User, Password, etc.) desde el archivo .properties correspondiente. ' DB: El identificador de la base de datos (ej. "DB1", "DB2"). ' Retorna un Mapa con la configuración leída. Private Sub LoadConfigMap(DB As String) As Map Private DBX As String = "" If DB <> "" Then DBX = "." & DB ' Construye el sufijo del nombre de archivo (ej. ".DB2"). Log($"RDCConnector.LoadConfigMap: Leemos el config${DBX}.properties"$) ' Mantenemos este log para confirmación de carga. Return File.ReadMap("./", "config" & DBX & ".properties") End Sub ' Obtiene la sentencia SQL completa para un comando dado desde el mapa de comandos cargado. ' DB: El identificador de la base de datos. ' Key: El nombre del comando SQL (ej. "select_user"). ' Retorna la sentencia SQL como String. Public Sub GetCommand(DB As String, Key As String) As String ' Obtiene los comandos de la DB específica del mapa global Main.commandsMap. commands = Main.commandsMap.Get(DB).As(Map) If commands.ContainsKey("sql." & Key) = False Then Dim ErrorMsg As String = $"RDCConnector.GetCommand: *** Comando no encontrado: '${Key}' para DB: '${DB}' ***"$ Log(ErrorMsg) Main.LogServerError("ERROR", "RDCConnector.GetCommand", ErrorMsg, DB, Key, Null) ' Log importante si un comando no se encuentra. End If Return commands.Get("sql." & Key) ' Retorna la sentencia SQL. End Sub ' Obtiene una conexión SQL funcional del pool de conexiones para la base de datos especificada. ' DB: El identificador de la base de datos. ' Retorna un objeto SQL (la conexión a la base de datos). Public Sub GetConnection(DB As String) As SQL If DB.EqualsIgnoreCase("DB1") Then DB = "" ' En modo de depuración, recarga los comandos SQL en cada petición. ' Esto permite modificar queries en config.properties sin reiniciar el servidor durante el desarrollo. If DebugQueries Then LoadSQLCommands(LoadConfigMap(DB), DB) ' <<<< Bloque de Logs de Depuración de Adquisición de Conexión (descomentar si es necesario) >>>> ' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Solicitando conexión del pool..."$) Dim conn As SQL = pool.GetConnection ' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Conexión obtenida. IsInitialized: ${conn.IsInitialized}"$) If pool.IsInitialized Then ' Doble verificación del estado del pool para logging más seguro ' Dim jo As JavaObject = pool ' Aseguramos que los valores de C3P0 sean Ints, manejando posibles retornos como Double. ' Dim busyCount As Int = jo.RunMethod("getNumBusyConnectionsAllUsers", Null).As(Object).As(Int) ' Dim totalCount As Int = jo.RunMethod("getNumConnectionsAllUsers", Null).As(Object).As(Int) ' Log($"[DEBUG - ${DB}] RDCConnector.GetConnection: Estadísticas del Pool (después de obtener): Busy=${busyCount}, Total=${totalCount}"$) End If ' <<<< Fin del bloque de Logs de Depuración >>>> Return conn ' Retorna una conexión del pool. End Sub ' Carga todos los comandos SQL del mapa de configuración en el mapa global 'commandsMap' de Main. ' config2: El mapa de configuración de la DB actual (JdbcUrl, User, Password, etc.). ' DB: El identificador de la base de datos. Private Sub LoadSQLCommands(config2 As Map, DB As String) Dim newCommands As Map newCommands.Initialize For Each k As String In config2.Keys If k.StartsWith("sql.") Then ' Busca claves que comiencen con "sql." (ej. "sql.select_user"). newCommands.Put(k, config2.Get(k)) ' Añade el comando al mapa. End If Next commands = newCommands ' Actualiza el mapa de comandos de esta instancia de RDCConnector. Main.commandsMap.Put(DB, commands) ' Almacena el mapa de comandos en el mapa global 'commandsMap' de Main. End Sub ' Nuevo: Obtiene estadísticas detalladas del pool de conexiones. ' Es utilizado por el Manager para mostrar el estado del pool. Public Sub GetPoolStats As Map Dim stats As Map stats.Initialize ' Log("--- RDCConnector.GetPoolStats llamado ---") ' Log de inicio (descomentar si es necesario) If pool.IsInitialized Then ' Log("RDCConnector.GetPoolStats: Pool está inicializado. Intentando obtener métricas.") ' Log (descomentar si es necesario) Dim jo As JavaObject = pool ' Convertimos el objeto pool a JavaObject para acceder a sus métodos internos de C3P0. Try ' --- Métricas en tiempo real del pool --- ' Se obtienen los valores y se aseguran como objetos para su posterior manejo en el mapa. Dim totalConn As Object = jo.RunMethod("getNumConnectionsAllUsers", Null) stats.Put("TotalConnections", totalConn) ' Log($"RDCConnector.GetPoolStats: TotalConnections = ${totalConn}"$) ' Log (descomentar si es necesario) Dim busyConn As Object = jo.RunMethod("getNumBusyConnectionsAllUsers", Null) stats.Put("BusyConnections", busyConn) ' Log($"RDCConnector.GetPoolStats: BusyConnections = ${busyConn}"$) ' Log (descomentar si es necesario) Dim idleConn As Object = jo.RunMethod("getNumIdleConnectionsAllUsers", Null) stats.Put("IdleConnections", idleConn) ' Log($"RDCConnector.GetPoolStats: IdleConnections = ${idleConn}"$) ' Log (descomentar si es necesario) ' --- Valores de configuración del pool (para referencia) --- ' Se obtienen y almacenan los parámetros de configuración del pool. Dim initialSize As Object = jo.RunMethod("getInitialPoolSize", Null) stats.Put("InitialPoolSize", initialSize) ' Log($"RDCConnector.GetPoolStats: InitialPoolSize = ${initialSize}"$) ' Log (descomentar si es necesario) Dim minSize As Object = jo.RunMethod("getMinPoolSize", Null) stats.Put("MinPoolSize", minSize) ' Log($"RDCConnector.GetPoolStats: MinPoolSize = ${minSize}"$) ' Log (descomentar si es necesario) Dim maxSize As Object = jo.RunMethod("getMaxPoolSize", Null) stats.Put("MaxPoolSize", maxSize) ' Log($"RDCConnector.GetPoolStats: MaxPoolSize = ${maxSize}"$) ' Log (descomentar si es necesario) Dim acquireInc As Object = jo.RunMethod("getAcquireIncrement", Null) stats.Put("AcquireIncrement", acquireInc) ' Log($"RDCConnector.GetPoolStats: AcquireIncrement = ${acquireInc}"$) ' Log (descomentar si es necesario) Dim maxIdle As Object = jo.RunMethod("getMaxIdleTime", Null) stats.Put("MaxIdleTime", maxIdle) ' Log($"RDCConnector.GetPoolStats: MaxIdleTime = ${maxIdle}"$) ' Log (descomentar si es necesario) Dim maxAge As Object = jo.RunMethod("getMaxConnectionAge", Null) stats.Put("MaxConnectionAge", maxAge) ' Log($"RDCConnector.GetPoolStats: MaxConnectionAge = ${maxAge}"$) ' Log (descomentar si es necesario) Dim checkoutTime As Object = jo.RunMethod("getCheckoutTimeout", Null) stats.Put("CheckoutTimeout", checkoutTime) ' Log($"RDCConnector.GetPoolStats: CheckoutTimeout = ${checkoutTime}"$) ' Log (descomentar si es necesario) Catch ' Si ocurre un error al obtener las estadísticas, se registra y se añade un mensaje de error al mapa. Dim ErrorMsg As String = "RDCConnector.GetPoolStats: ERROR CRÍTICO al obtener estadísticas del pool: " & LastException.Message Log(ErrorMsg) Main.LogServerError("ERROR", "RDCConnector.GetPoolStats", ErrorMsg, "Todas", Null, Null) ' <-- Nuevo Log stats.Put("Error", LastException.Message) End Try Else ' Si el pool no está inicializado, se registra una advertencia y se devuelve un mapa con un error. Dim WarningMsg As String = "RDCConnector.GetPoolStats: ADVERTENCIA: Pool NO está inicializado. Retornando mapa con error." Log(WarningMsg) Main.LogServerError("ADVERTENCIA", "RDCConnector.GetPoolStats", WarningMsg, "Todas", Null, Null) ' <-- Nuevo Log stats.Put("Error", "Pool de conexiones no inicializado para esta DB.") End If ' Se utiliza JSONGenerator para serializar el mapa de estadísticas a String para el log, ' lo que permite una visualización estructurada y fácil de leer. Dim tempJsonGen As JSONGenerator tempJsonGen.Initialize(stats) ' Log("--- RDCConnector.GetPoolStats finalizado. Retornando stats: " & tempJsonGen.ToString & " ---") ' Log de fin (descomentar si es necesario) Return stats End Sub ' *** NUEVA SUBRUTINA: Cierra el pool de conexiones de forma ordenada usando JavaObject *** ' Este método es crucial para liberar los recursos de la base de datos cuando un conector RDC ' ya no es necesario o va a ser reemplazado (por ejemplo, durante un "Hot-Swap" de configuración). Public Sub Close If pool <> Null And pool.IsInitialized Then ' Log($"RDCConnector.Close: Cerrando pool de conexiones."$) ' Log (descomentar si es necesario) ' Convertimos el objeto pool de B4X a un JavaObject para poder llamar a su método 'close()' ' que no está expuesto directamente en la envoltura de B4X, asegurando un cierre limpio de C3P0. Dim joPool As JavaObject = pool joPool.RunMethod("close", Null) ' Llamamos al método 'close()' del objeto Java subyacente de C3P0. End If End Sub