Files
jRDC-Multi/DBHandlerB4X.bas
Jose Alberto Guerra Ugalde 8b876e5095 - VERSION 5.09.19
- feat(sqlite): Implementa optimización de SQLite (WAL e Índices)
- fix(manager): Extiende el comando 'test' para verificar todos los pools de conexión configurados.

- Mejoras al subsistema de logs y diagnóstico del servidor jRDC2-Multi.
- Cambios principales:
1. Optimización del Rendimiento de SQLite (users.db):
*   Habilitación de WAL: Se implementó PRAGMA journal_mode=WAL y PRAGMA synchronous=NORMAL en `InitializeSQLiteDatabase`. Esto reduce la contención de disco y mejora el rendimiento de I/O en las escrituras transaccionales de logs por lotes.
*   Índices de logs: Se agregaron índices a las columnas `timestamp` y `duration_ms` en `query_logs`, y a `timestamp` en `errores`. Esto acelera drásticamente las operaciones de limpieza periódica (`borraArribaDe15000Logs`) y la generación de reportes de consultas lentas (`slowqueries`).

2. Mejora del Comando de Diagnóstico 'test':
*   Se corrigió el comando `manager?command=test` para que no solo pruebe la conexión de `DB1`, sino que itere sobre `Main.listaDeCP` y fuerce la adquisición y liberación de una conexión (`GetConnection`) en *todos* los `RDCConnector` configurados (DB1, DB2, DB3, etc.).
*   La nueva lógica garantiza una prueba de vida rigurosa de cada pool C3P0, devolviendo un mensaje detallado del estado de conectividad y registrando un error crítico vía `LogServerError` si algún pool no responde.
2025-09-27 20:34:12 -06:00

781 lines
35 KiB
QBasic

B4J=true
Group=Default Group
ModulesStructureVersion=1
Type=Class
Version=10.3
@EndOfDesignText@
' Módulo de clase: DBHandlerB4X
' Este handler genérico se encarga de procesar las peticiones HTTP provenientes
' de clientes B4A/B4i (que utilizan la librería DBRequestManager).
' La base de datos a utilizar (DB1, DB2, etc.) se determina dinámicamente
' a partir de la URL de la petición.
' Esta versión incluye validaciones de parámetros y manejo de errores.
Sub Class_Globals
' --- Variables globales de la clase ---
' La siguiente sección de constantes y utilidades se compila condicionalmente
' solo si la directiva #if VERSION1 está activa. Esto es para dar soporte
' a una versión antigua del protocolo de comunicación de DBRequestManager.
' #if VERSION1
' Constantes para identificar los tipos de datos en la serialización personalizada (protocolo V1).
Private const T_NULL = 0, T_STRING = 1, T_SHORT = 2, T_INT = 3, T_LONG = 4, T_FLOAT = 5 _
,T_DOUBLE = 6, T_BOOLEAN = 7, T_BLOB = 8 As Byte
' Utilidades para convertir entre tipos de datos y arrays de bytes.
Private bc As ByteConverter
' Utilidad para comprimir/descomprimir streams de datos (usado en V1).
Private cs As CompressedStreams
' #end if
' Mapa para convertir tipos de columna JDBC de fecha/hora a los nombres de métodos de Java
' para obtener los valores correctos de ResultSet.
Private DateTimeMethods As Map
' Objeto que gestiona las conexiones al pool de una base de datos específica.
' Esta instancia de RDCConnector será asignada en el método Handle según la dbKey de la petición.
Private Connector As RDCConnector
End Sub
' Se ejecuta una vez cuando se crea una instancia de esta clase por el servidor HTTP.
Public Sub Initialize
' Inicializa el mapa que asocia los códigos de tipo de columna de fecha/hora de JDBC
' con los nombres de los métodos correspondientes para leerlos correctamente desde un ResultSet.
DateTimeMethods = CreateMap(91: "getDate", 92: "getTime", 93: "getTimestamp")
End Sub
' Método principal que maneja cada petición HTTP que llega a este handler.
' req: El objeto ServletRequest que contiene la información de la petición entrante.
' resp: El objeto ServletResponse para construir y enviar la respuesta al cliente.
Sub Handle(req As ServletRequest, resp As ServletResponse)
' === INICIO DE LA LÓGICA DINÁMICA: Extracción de dbKey de la URL ===
' Esta sección analiza la URL de la petición para determinar a qué base de datos
' (DB1, DB2, etc.) se dirige la solicitud. Por ejemplo, si la URL es "/DB2/query",
' el 'dbKey' extraído será "DB2".
Dim URI As String = req.RequestURI
Dim dbKey As String ' Variable para almacenar el identificador de la base de datos.
If URI.Length > 1 And URI.StartsWith("/") Then
dbKey = URI.Substring(1) ' Elimina el '/' inicial.
If dbKey.Contains("/") Then
' Si la URL tiene más segmentos (ej. "/DB2/alguna_ruta"), toma solo el primer segmento como dbKey.
dbKey = dbKey.SubString2(0, dbKey.IndexOf("/"))
End If
Else
' Si la URL es solo "/", por defecto se usa "DB1".
dbKey = "DB1"
End If
dbKey = dbKey.ToUpperCase ' Normaliza el dbKey a mayúsculas para consistencia.
' Verifica si el dbKey extraído corresponde a una base de datos configurada y cargada en Main.
If Main.Connectors.ContainsKey(dbKey) = False Then
Dim ErrorMsg As String = $"Invalid DB key specified in URL: '${dbKey}'. Valid keys are: ${Main.listaDeCP}"$
Log(ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerB4X.Handle", ErrorMsg, dbKey, Null, req.RemoteAddress) ' <-- Nuevo Log
SendPlainTextError(resp, 400, ErrorMsg)
Return
End If
' === FIN DE LA LÓGICA DINÁMICA ===
' Log("********************* " & dbKey & " ********************") ' Log de depuración para identificar la base de datos.
Dim start As Long = DateTime.Now ' Registra el tiempo de inicio de la petición para calcular la duración.
' --- INICIO: Conteo de peticiones activas para esta dbKey (Incrementar) ---
' Este bloque incrementa un contador global que rastrea cuántas peticiones están
' activas para una base de datos específica en un momento dado.
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor inicial sea un Int y lo recuperamos como Int! >>>>
Dim currentActiveRequests As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0).As(Int)
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentActiveRequests + 1)
' requestsBeforeDecrement es el valor del contador justo después de que esta petición lo incrementa.
' Este es el valor que se registrará en la tabla 'query_logs'.
Dim requestsBeforeDecrement As Int = currentActiveRequests + 1
' Log($"[DEBUG] Handle Increment (B4X): dbKey=${dbKey}, currentCountFromMap=${currentActiveRequests}, requestsBeforeDecrement=${requestsBeforeDecrement}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
' --- FIN: Conteo de peticiones activas ---
' Declaraciones de variables con alcance en toda la subrutina para asegurar la limpieza final.
Dim q As String = "unknown_b4x_command" ' Nombre del comando para el log, con valor por defecto.
Dim con As SQL ' La conexión a la BD, se inicializará más tarde.
Dim duration As Long ' La duración total de la petición, calculada antes del log.
Dim poolBusyConnectionsForLog As Int = 0 ' Contiene el número de conexiones ocupadas del pool.
Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler ---
Dim in As InputStream = req.InputStream ' Obtiene el stream de entrada de la petición HTTP.
Dim method As String = req.GetParameter("method") ' Obtiene el parámetro 'method' de la URL (ej. "query2", "batch2").
Connector = Main.Connectors.Get(dbKey) ' Asigna la instancia de RDCConnector para esta dbKey.
con = Connector.GetConnection(dbKey) ' ¡La conexión a la BD se obtiene aquí del pool de conexiones!
' Este bloque captura el número de conexiones actualmente ocupadas en el pool
' *después* de que esta petición ha obtenido la suya.
If Connector.IsInitialized Then
Dim poolStats As Map = Connector.GetPoolStats
If poolStats.ContainsKey("BusyConnections") Then
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor.
Log($">>>>>>>>>> ${poolStats.Get("BusyConnections")} "$)
End If
End If
Dim cachedStatsB4X As Map = Main.LatestPoolStats.Get(dbKey).As(Map)
If cachedStatsB4X.IsInitialized Then
' 1. Actualizar Busy Connections y Active Requests
cachedStatsB4X.Put("BusyConnections", poolBusyConnectionsForLog)
cachedStatsB4X.Put("HandlerActiveRequests", requestsBeforeDecrement)
' 2. Capturar TotalConnections y IdleConnections (ya disponibles en poolStats)
If poolStats.ContainsKey("TotalConnections") Then
cachedStatsB4X.Put("TotalConnections", poolStats.Get("TotalConnections"))
End If
If poolStats.ContainsKey("IdleConnections") Then
cachedStatsB4X.Put("IdleConnections", poolStats.Get("IdleConnections"))
End If
' 3. Re-escribir el mapa en el cache global (es Thread-Safe)
Main.LatestPoolStats.Put(dbKey, cachedStatsB4X)
End If
' Log("Metodo: " & method) ' Log de depuración para identificar el método de la petición.
' --- Lógica para ejecutar diferentes tipos de comandos basados en el parámetro 'method' ---
If method = "query2" Then
' Ejecuta una consulta única utilizando el protocolo V2 (B4XSerializator).
q = ExecuteQuery2(dbKey, con, in, resp)
If q = "error" Then ' Si ExecuteQuery2 devolvió un error de validación.
duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana si hay un error.
End If
' #if VERSION1
' Estas ramas se compilan solo si #if VERSION1 está activo (para protocolo antiguo).
Else if method = "query" Then
in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1.
q = ExecuteQuery(dbKey, con, in, resp)
If q = "error" Then
duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
Else if method = "batch" Then
in = cs.WrapInputStream(in, "gzip") ' Descomprime el stream de entrada si es protocolo V1.
q = ExecuteBatch(dbKey, con, in, resp)
If q = "error" Then
duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
' #end if
Else if method = "batch2" Then
' Ejecuta un lote de comandos (INSERT, UPDATE, DELETE) utilizando el protocolo V2.
q = ExecuteBatch2(dbKey, con, in, resp)
If q = "error" Then
duration = DateTime.Now - start
CleanupAndLog(dbKey, "error_in_" & method, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return ' Salida temprana si hay un error.
End If
Else
Dim ErrorMsg As String = "Unknown method: " & method
Log(ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerB4X.Handle", ErrorMsg, dbKey, method, req.RemoteAddress) ' <-- Nuevo Log
SendPlainTextError(resp, 500, "unknown method")
q = "unknown_method_handler"
duration = DateTime.Now - start
CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
Return
End If
Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL ---
Dim errorMessage As String = LastException.Message
If errorMessage.Contains("ORA-01002") Or errorMessage.Contains("recuperación fuera de secuencia") Then
errorMessage = "SE USA EXECUTEQUERY EN LUGAR DE EXECUTECOMMAND: " & errorMessage
else If errorMessage.Contains("ORA-17003") Or errorMessage.Contains("Índice de columnas no válido") Then
errorMessage = "NUMERO DE PARAMETROS EQUIVOCADO: " & errorMessage
End If
Log(errorMessage) ' Registra la excepción completa en el log.
Main.LogServerError("ERROR", "DBHandlerB4X.Handle", errorMessage, dbKey, q, req.RemoteAddress) ' <-- Nuevo Log
SendPlainTextError(resp, 500, errorMessage) ' Envía un error 500 al cliente.
q = "error_in_b4x_handler" ' Aseguramos un valor para 'q' en caso de excepción.
End Try ' --- FIN: Bloque Try principal ---
' --- Lógica de logging y limpieza final (para rutas de ejecución normal o después de Catch) ---
' Este bloque se asegura de que, independientemente de cómo termine la petición (éxito o error),
' la duración se calcule y se llamen las subrutinas de limpieza y logging.
duration = DateTime.Now - start ' Calcula la duración total de la petición.
Log($"${dbKey} - Command: ${q}, took: ${duration}ms, client=${req.RemoteAddress}"$) ' Logea el comando y la duración.
' Llama a la subrutina centralizada para registrar el rendimiento y limpiar recursos.
CleanupAndLog(dbKey, q, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con)
End Sub
' --- NUEVA SUBRUTINA: Centraliza el logging de rendimiento y la limpieza de recursos ---
' Esta subrutina es llamada por Handle en todos los puntos de salida, asegurando
' que los contadores se decrementen y las conexiones se cierren de forma consistente.
Private Sub CleanupAndLog(dbKey As String, qName As String, durMs As Long, clientIp As String, handlerReqs As Int, poolBusyConns As Int, conn As SQL)
' Log($"[DEBUG] CleanupAndLog Entry (B4X): dbKey=${dbKey}, handlerReqs=${handlerReqs}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
' 1. Llama a la subrutina centralizada en Main para registrar el rendimiento en SQLite.
Main.LogQueryPerformance(qName, durMs, dbKey, clientIp, handlerReqs, poolBusyConns)
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que currentCount sea Int al obtenerlo del mapa! >>>>
' 2. Decrementa el contador de peticiones activas para esta dbKey de forma robusta.
Dim currentCount As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey, 0).As(Int)
' Log($"[DEBUG] CleanupAndLog Before Decrement (B4X): dbKey=${dbKey}, currentCount (as Int)=${currentCount}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
If currentCount > 0 Then
' Si el contador es positivo, lo decrementamos.
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, currentCount - 1)
Else
' Si el contador ya está en 0 o negativo (lo cual no debería ocurrir con la lógica actual,
' pero se maneja para robustez), registramos una advertencia y lo aseguramos en 0.
' Log($"ADVERTENCIA: Intento de decrementar ActiveRequestsCountByDB para ${dbKey} que ya estaba en ${currentCount}. Asegurando a 0."$)
GlobalParameters.ActiveRequestsCountByDB.Put(dbKey, 0)
End If
' Log($"[DEBUG] CleanupAndLog After Decrement (B4X): dbKey=${dbKey}, New count (as Int)=${GlobalParameters.ActiveRequestsCountByDB.GetDefault(dbKey,0).As(Int)}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$)
' <<<< ¡FIN DE CORRECCIÓN CLAVE! >>>>
' 3. Asegura que la conexión a la BD siempre se cierre y se devuelva al pool de conexiones.
If conn <> Null And conn.IsInitialized Then conn.Close
End Sub
' --- Subrutinas para manejar la ejecución de queries y batches (Protocolo V2) ---
' Ejecuta una consulta única usando el protocolo V2 (B4XSerializator).
' DB: Identificador de la base de datos.
' con: La conexión SQL obtenida del pool.
' in: InputStream de la petición.
' resp: ServletResponse para enviar la respuesta.
' Retorna el nombre del comando ejecutado o "error" si falló.
Private Sub ExecuteQuery2 (DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
Dim ser As B4XSerializator ' Objeto para deserializar los datos enviados desde el cliente.
' Convierte el stream de entrada a un array de bytes y luego a un objeto Mapa.
Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in))
' Extrae el objeto DBCommand (nombre de la query y sus parámetros) del mapa.
Dim cmd As DBCommand = m.Get("command")
' Extrae el límite de filas a devolver (para paginación).
Dim limit As Int = m.Get("limit")
' Obtiene la sentencia SQL correspondiente al nombre del comando desde config.properties.
Dim sqlCommand As String = Connector.GetCommand(DB, cmd.Name)
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE >>>
' Comprueba si el comando no fue encontrado en el archivo de configuración.
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
Dim errorMessage As String = $"El comando '${cmd.Name}' no fue encontrado en el config.properties de '${DB}'."$
Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteQuery2", errorMessage, DB, cmd.Name, Null)
' Envía un error 400 (Bad Request) al cliente informando del problema.
SendPlainTextError(resp, 400, errorMessage)
Return "error" ' Retorna un texto para el log.
End If
' <<< FIN NUEVA VALIDACIÓN >>>
' <<< INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA >>>
' Convertimos el array de Object() de cmd.Parameters a una List para la utilidad de validación.
Dim paramsAsList As List
paramsAsList.Initialize
If cmd.Parameters <> Null Then
For Each p As Object In cmd.Parameters
paramsAsList.Add(p)
Next
End If
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(cmd.Name, DB, sqlCommand, paramsAsList, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
SendPlainTextError(resp, 400, validationResult.ErrorMessage)
Return "error" ' Salida temprana si la validación falla.
End If
' Ejecuta la consulta SQL con la lista de parámetros validada.
Dim rs As ResultSet = con.ExecQuery2(sqlCommand, validationResult.ParamsToExecute)
' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA >>>
' Si el límite es 0 o negativo, lo establece a un valor muy alto (máximo entero).
If limit <= 0 Then limit = 0x7fffffff 'max int
' Obtiene el objeto Java subyacente del ResultSet para acceder a métodos adicionales.
Dim jrs As JavaObject = rs
' Obtiene los metadatos del ResultSet (información sobre las columnas).
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null)
' Obtiene el número de columnas del resultado.
Dim cols As Int = rs.ColumnCount
Dim res As DBResult ' Crea un objeto DBResult para empaquetar la respuesta.
res.Initialize
res.columns.Initialize
res.Tag = Null
' Llena el mapa de columnas con el nombre de cada columna y su índice.
For i = 0 To cols - 1
res.columns.Put(rs.GetColumnName(i), i)
Next
' Inicializa la lista de filas.
res.Rows.Initialize
' Itera sobre cada fila del ResultSet, hasta llegar al límite.
Do While rs.NextRow And limit > 0
Dim row(cols) As Object
' Itera sobre cada columna de la fila actual.
For i = 0 To cols - 1
' Obtiene el tipo de dato de la columna según JDBC.
Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1))
' Maneja diferentes tipos de datos para leerlos de la forma correcta.
If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then ' Tipos BLOB/binarios
row(i) = rs.GetBlob2(i)
Else If ct = 2005 Then ' Tipo CLOB (texto largo)
row(i) = rs.GetString2(i)
Else if ct = 2 Or ct = 3 Then ' Tipos numéricos que pueden tener decimales
row(i) = rs.GetDouble2(i)
Else If DateTimeMethods.ContainsKey(ct) Then ' Tipos de Fecha/Hora
' Obtiene el objeto de tiempo/fecha de Java.
Dim SQLTime As JavaObject = jrs.RunMethodJO(DateTimeMethods.Get(ct), Array(i + 1))
If SQLTime.IsInitialized Then
' Lo convierte a milisegundos (Long) para B4X.
row(i) = SQLTime.RunMethod("getTime", Null)
Else
row(i) = Null
End If
Else ' Para todos los demás tipos de datos
' Usa getObject que funciona para la mayoría de los tipos estándar.
row(i) = jrs.RunMethod("getObject", Array(i + 1))
End If
Next
' Añade la fila completa a la lista de resultados.
res.Rows.Add(row)
limit = limit - 1
Loop
' Cierra el ResultSet para liberar recursos.
rs.Close
' Serializa el objeto DBResult completo a un array de bytes.
Dim data() As Byte = ser.ConvertObjectToBytes(res)
' Escribe los datos serializados en el stream de respuesta.
resp.OutputStream.WriteBytes(data, 0, data.Length)
' Devuelve el nombre del comando para el log.
Return "query: " & cmd.Name
End Sub
' Ejecuta un lote de comandos (INSERT, UPDATE, DELETE) usando el protocolo V2.
' DB: Identificador de la base de datos.
' con: La conexión SQL obtenida del pool.
' in: InputStream de la petición.
' resp: ServletResponse para enviar la respuesta.
' Retorna un resumen del lote para el log, o "error" si falló.
Private Sub ExecuteBatch2(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
Dim ser As B4XSerializator
' Deserializa el mapa que contiene la lista de comandos.
Dim m As Map = ser.ConvertBytesToObject(Bit.InputStreamToBytes(in))
' Obtiene la lista de objetos DBCommand.
Dim commands As List = m.Get("commands")
Dim totalAffectedRows As Int = 0 ' Contador para acumular el total de filas afectadas.
' Prepara un objeto DBResult para la respuesta (aunque para batch no devuelve datos, solo confirmación).
Dim res As DBResult
res.Initialize
res.columns = CreateMap("AffectedRows": 0) ' Columna simbólica.
res.Rows.Initialize
res.Tag = Null
Try
' Inicia una transacción. Todos los comandos del lote se ejecutarán como una unidad.
con.BeginTransaction
' Itera sobre cada comando en la lista.
For Each cmd As DBCommand In commands
' Obtiene la sentencia SQL para el comando actual.
Dim sqlCommand As String = Connector.GetCommand(DB, cmd.Name)
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE DENTRO DEL BATCH >>>
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
con.Rollback ' Deshace la transacción si un comando es inválido.
Dim errorMessage As String = $"El comando '${cmd.Name}' no fue encontrado en el config.properties de '${DB}'."$
Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch2", errorMessage, DB, cmd.Name, Null)
SendPlainTextError(resp, 400, errorMessage)
Return "error"
End If
' <<< FIN NUEVA VALIDACIÓN >>>
' <<< INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH >>>
' Convertimos el array de Object() de cmd.Parameters a una List para la utilidad de validación.
Dim paramsAsList As List
paramsAsList.Initialize
If cmd.Parameters <> Null Then
For Each p As Object In cmd.Parameters
paramsAsList.Add(p)
Next
End If
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(cmd.Name, DB, sqlCommand, paramsAsList, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
con.Rollback ' ¡Importante hacer rollback si la validación falla dentro de una transacción!
SendPlainTextError(resp, 400, validationResult.ErrorMessage)
Return "error" ' Salida temprana si la validación falla.
End If
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta el comando con la lista de parámetros validada.
totalAffectedRows = totalAffectedRows + 1 ' Acumulamos 1 por cada comando ejecutado sin error.
' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH >>>
Next
res.Rows.Add(Array As Object(totalAffectedRows)) ' Añade una fila simbólica al resultado para indicar éxito.
con.TransactionSuccessful ' Si todos los comandos se ejecutaron sin error, confirma la transacción.
Catch
' Si cualquier comando falla, se captura el error.
con.Rollback ' Se deshacen todos los cambios hechos en la transacción.
Log(LastException) ' Registra la excepción.
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch2", LastException.Message, DB, "batch_execution_error", Null)
SendPlainTextError(resp, 500, LastException.Message) ' Envía un error 500 al cliente.
End Try
' Serializa y envía la respuesta al cliente.
Dim data() As Byte = ser.ConvertObjectToBytes(res)
resp.OutputStream.WriteBytes(data, 0, data.Length)
' Devuelve un resumen para el log.
' Return $"batch (size=${commands.Size})"$
' Devuelve un resumen para el log, incluyendo el nombre de la query si es un lote de tamaño 1.
If commands.Size = 1 Then
' Obtenemos el único comando en el lote.
Dim cmd As DBCommand = commands.Get(0)
Return $"batch (size=1) - query: ${cmd.Name}"$
Else
' Si el lote es de tamaño > 1, mantenemos el resumen por tamaño.
Return $"batch (size=${commands.Size})"$
End If
End Sub
' --- Subrutinas para manejar la ejecución de queries y batches (Protocolo V1 - Compilación Condicional) ---
' Este código se compila solo si #if VERSION1 está activo, para mantener compatibilidad con clientes antiguos.
'#if VERSION1
' Ejecuta un lote de comandos usando el protocolo V1.
Private Sub ExecuteBatch(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
' Log($"ExecuteBatch ${DB}"$)
' Lee y descarta la versión del cliente.
Dim clientVersion As Float = ReadObject(in) 'ignore
' Lee cuántos comandos vienen en el lote.
Dim numberOfStatements As Int = ReadInt(in)
Dim res(numberOfStatements) As Int ' Array para resultados (aunque no se usa).
Dim singleQueryName As String = ""
Dim affectedCounts As List
Dim totalAffectedRows As Int
affectedCounts.Initialize
Try
con.BeginTransaction
' Itera para procesar cada comando del lote.
' Log(numberOfStatements)
For i = 0 To numberOfStatements - 1
' Log($"i: ${i}"$)
' Lee el nombre del comando y la lista de parámetros usando el deserializador V1.
Dim queryName As String = ReadObject(in)
Dim params As List = ReadList(in)
' Log(params)
If numberOfStatements = 1 Then
singleQueryName = queryName 'Capturamos el nombre del query.
End If
Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
' Log(sqlCommand)
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>>
If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then
con.Rollback ' Deshace la transacción si un comando es inválido.
Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$
Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch (V1)", errorMessage, DB, queryName, Null)
SendPlainTextError(resp, 400, errorMessage)
Return "error"
End If
' <<< FIN NUEVA VALIDACIÓN >>>
' <<< INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH (V1) >>>
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryName, DB, sqlCommand, params, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
con.Rollback ' ¡Importante hacer rollback si la validación falla dentro de una transacción!
SendPlainTextError(resp, 400, validationResult.ErrorMessage)
Return "error" ' Salida temprana si la validación falla.
End If
' Log(validationResult.ParamsToExecute)
Dim affectedCount As Int = 1 ' Asumimos éxito (1) ya que la llamada directa es la única que ejecuta el SQL sin fallar en runtime.
con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta el comando con la lista de parámetros validada.
' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH (V1) >>>
affectedCounts.Add(affectedCount) ' Añadimos el resultado (1) a la lista de respuesta V1
totalAffectedRows = totalAffectedRows + affectedCount ' Acumulamos el total para el log (aunque sea 1 simbólico)
Next
con.TransactionSuccessful ' Confirma la transacción.
' Log("Transaction succesfull")
Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime la salida antes de enviarla.
' Escribe la respuesta usando el serializador V1.
WriteObject(Main.VERSION, out)
WriteObject("batch", out)
WriteInt(res.Length, out)
For Each r As Int In affectedCounts
WriteInt(r, out)
Next
out.Close
Catch
con.Rollback
Log(LastException)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteBatch (V1)", LastException.Message, DB, "batch_execution_error_v1", Null)
SendPlainTextError(resp, 500, LastException.Message)
End Try
' Return $"batch (size=${numberOfStatements})"$
If numberOfStatements = 1 And singleQueryName <> "" Then
Return $"batch (size=1) - query: ${singleQueryName}"$
Else
Return $"batch (size=${numberOfStatements})"$
End If
End Sub
' Ejecuta una consulta única usando el protocolo V1.
Private Sub ExecuteQuery(DB As String, con As SQL, in As InputStream, resp As ServletResponse) As String
' Log("====================== ExecuteQuery =====================")
' Deserializa los datos de la petición usando el protocolo V1.
Dim clientVersion As Float = ReadObject(in) 'ignore
Dim queryName As String = ReadObject(in)
Dim limit As Int = ReadInt(in)
Dim params As List = ReadList(in)
' Obtiene la sentencia SQL.
Dim theSql As String = Connector.GetCommand(DB, queryName)
' <<< INICIO NUEVA VALIDACIÓN: VERIFICAR SI EL COMANDO EXISTE (V1) >>>
If theSql = Null Or theSql ="null" Or theSql.Trim = "" Then
Dim errorMessage As String = $"El comando '${queryName}' no fue encontrado en el config.properties de '${DB}'."$
Log(errorMessage)
Main.LogServerError("ERROR", "DBHandlerB4X.ExecuteQuery (V1)", errorMessage, DB, queryName, Null)
SendPlainTextError(resp, 400, errorMessage)
Return "error"
End If
' <<< FIN NUEVA VALIDACIÓN >>>
' <<< INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA (V1) >>>
Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryName, DB, theSql, params, Connector.IsParameterToleranceEnabled)
If validationResult.Success = False Then
SendPlainTextError(resp, 400, validationResult.ErrorMessage)
Return "error" ' Salida temprana si la validación falla.
End If
' Ejecuta la consulta con la lista de parámetros validada.
Dim rs As ResultSet = con.ExecQuery2(theSql, validationResult.ParamsToExecute)
' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA (V1) >>>
If limit <= 0 Then limit = 0x7fffffff 'max int
Dim jrs As JavaObject = rs
Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null)
Dim cols As Int = rs.ColumnCount
Dim out As OutputStream = cs.WrapOutputStream(resp.OutputStream, "gzip") ' Comprime el stream de salida.
' Escribe la cabecera de la respuesta V1.
WriteObject(Main.VERSION, out)
WriteObject("query", out)
WriteInt(rs.ColumnCount, out)
' Escribe los nombres de las columnas.
For i = 0 To cols - 1
WriteObject(rs.GetColumnName(i), out)
Next
' Itera sobre las filas del resultado.
Do While rs.NextRow And limit > 0
WriteByte(1, out) ' Escribe un byte '1' para indicar que viene una fila.
' Itera sobre las columnas de la fila.
For i = 0 To cols - 1
Dim ct As Int = rsmd.RunMethod("getColumnType", Array(i + 1))
' Maneja los tipos de datos binarios de forma especial.
If ct = -2 Or ct = 2004 Or ct = -3 Or ct = -4 Then
WriteObject(rs.GetBlob2(i), out)
Else
' Escribe el valor de la columna.
WriteObject(jrs.RunMethod("getObject", Array(i + 1)), out)
End If
Next
limit = limit - 1
Loop
' Escribe un byte '0' para indicar el fin de las filas.
WriteByte(0, out)
out.Close
rs.Close
Return "query: " & queryName
End Sub
' Escribe un único byte en el stream de salida.
Private Sub WriteByte(value As Byte, out As OutputStream)
out.WriteBytes(Array As Byte(value), 0, 1)
End Sub
' Serializador principal para el protocolo V1. Escribe un objeto al stream.
Private Sub WriteObject(o As Object, out As OutputStream)
Dim data() As Byte
' Escribe un byte de tipo seguido de los datos.
If o = Null Then
out.WriteBytes(Array As Byte(T_NULL), 0, 1)
Else If o Is Short Then
out.WriteBytes(Array As Byte(T_SHORT), 0, 1)
data = bc.ShortsToBytes(Array As Short(o))
Else If o Is Int Then
out.WriteBytes(Array As Byte(T_INT), 0, 1)
data = bc.IntsToBytes(Array As Int(o))
Else If o Is Float Then
out.WriteBytes(Array As Byte(T_FLOAT), 0, 1)
data = bc.FloatsToBytes(Array As Float(o))
Else If o Is Double Then
out.WriteBytes(Array As Byte(T_DOUBLE), 0, 1)
data = bc.DoublesToBytes(Array As Double(o))
Else If o Is Long Then
out.WriteBytes(Array As Byte(T_LONG), 0, 1)
data = bc.LongsToBytes(Array As Long(o))
Else If o Is Boolean Then
out.WriteBytes(Array As Byte(T_BOOLEAN), 0, 1)
Dim b As Boolean = o
Dim data(1) As Byte
If b Then data(0) = 1 Else data(0) = 0
Else If GetType(o) = "[B" Then ' Si el objeto es un array de bytes (BLOB)
data = o
out.WriteBytes(Array As Byte(T_BLOB), 0, 1)
' Escribe la longitud de los datos antes de los datos mismos.
WriteInt(data.Length, out)
Else ' Trata todo lo demás como un String
out.WriteBytes(Array As Byte(T_STRING), 0, 1)
data = bc.StringToBytes(o, "UTF8")
' Escribe la longitud del string antes del string.
WriteInt(data.Length, out)
End If
' Escribe los bytes del dato.
If data.Length > 0 Then out.WriteBytes(data, 0, data.Length)
End Sub
' Deserializador principal para el protocolo V1. Lee un objeto del stream.
Private Sub ReadObject(In As InputStream) As Object
' Lee el primer byte para determinar el tipo de dato.
Dim data(1) As Byte
In.ReadBytes(data, 0, 1)
Select data(0)
Case T_NULL
Return Null
Case T_SHORT
Dim data(2) As Byte
Return bc.ShortsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_INT
Dim data(4) As Byte
Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_LONG
Dim data(8) As Byte
Return bc.LongsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_FLOAT
Dim data(4) As Byte
Return bc.FloatsFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_DOUBLE
Dim data(8) As Byte
Return bc.DoublesFromBytes(ReadBytesFully(In, data, data.Length))(0)
Case T_BOOLEAN
Dim b As Byte = ReadByte(In)
Return b = 1
Case T_BLOB
' Lee la longitud, luego lee esa cantidad de bytes.
Dim len As Int = ReadInt(In)
Dim data(len) As Byte
Return ReadBytesFully(In, data, data.Length)
Case Else ' T_STRING
' Lee la longitud, luego lee esa cantidad de bytes y los convierte a string.
Dim len As Int = ReadInt(In)
Dim data(len) As Byte
ReadBytesFully(In, data, data.Length)
Return BytesToString(data, 0, data.Length, "UTF8")
End Select
End Sub
' Se asegura de leer exactamente la cantidad de bytes solicitada del stream.
Private Sub ReadBytesFully(In As InputStream, Data() As Byte, Len As Int) As Byte()
Dim count = 0, Read As Int
' Sigue leyendo en un bucle hasta llenar el buffer, por si los datos llegan en partes.
Do While count < Len And Read > -1
Read = In.ReadBytes(Data, count, Len - count)
count = count + Read
Loop
Return Data
End Sub
' Escribe un entero (4 bytes) en el stream.
Private Sub WriteInt(i As Int, out As OutputStream)
Dim data() As Byte
data = bc.IntsToBytes(Array As Int(i))
out.WriteBytes(data, 0, data.Length)
End Sub
' Lee un entero (4 bytes) del stream.
Private Sub ReadInt(In As InputStream) As Int
Dim data(4) As Byte
Return bc.IntsFromBytes(ReadBytesFully(In, data, data.Length))(0)
End Sub
' Lee un solo byte del stream.
Private Sub ReadByte(In As InputStream) As Byte
Dim data(1) As Byte
In.ReadBytes(data, 0, 1)
Return data(0)
End Sub
' Lee una lista de objetos del stream (protocolo V1).
Private Sub ReadList(in As InputStream) As List
' Primero lee la cantidad de elementos en la lista.
Dim len As Int = ReadInt(in)
Dim l1 As List
l1.Initialize
' Luego lee cada objeto uno por uno y lo añade a la lista.
For i = 0 To len - 1
l1.Add(ReadObject(in))
Next
Return l1
End Sub
'#end If ' Fin del bloque de compilación condicional para VERSION1
' Envía una respuesta de error en formato de texto plano.
' Esto evita la página de error HTML por defecto que genera resp.SendError.
' resp: El objeto ServletResponse para enviar la respuesta.
' statusCode: El código de estado HTTP (ej. 400 para Bad Request, 500 para Internal Server Error).
' errorMessage: El mensaje de error que se enviará al cliente.
' En los clientes de B4X, una respuesta en HTML o JSON no es lo ideal, el IDE muestra todo el texto del error y texto plano es mucho mas facil de leer que HTML o JSON.
Private Sub SendPlainTextError(resp As ServletResponse, statusCode As Int, errorMessage As String)
Try
' Establece el código de estado HTTP (ej. 400, 500).
resp.Status = statusCode
' Define el tipo de contenido como texto plano, con codificación UTF-8 para soportar acentos.
resp.ContentType = "text/plain; charset=utf-8"
' Obtiene el OutputStream de la respuesta para escribir los datos directamente.
Dim out As OutputStream = resp.OutputStream
' Convierte el mensaje de error a un array de bytes usando UTF-8.
Dim data() As Byte = errorMessage.GetBytes("UTF8")
' Escribe los bytes en el stream de salida.
out.WriteBytes(data, 0, data.Length)
' Cierra el stream para asegurar que todos los datos se envíen correctamente.
out.Close
Catch
' Si algo falla al intentar enviar la respuesta de error, lo registra en el log
' para que no se pierda la causa original del problema.
Dim ErrorMsg As String = "Error sending plain text error response: " & LastException
Log(ErrorMsg)
Main.LogServerError("ERROR", "DBHandlerB4X.SendPlainTextError", ErrorMsg, Null, Null, Null)
End Try
End Sub