mirror of
https://github.com/KeymonSoft/jRDC-Multi.git
synced 2026-04-17 21:06:24 +00:00
- feat(logs): Implementación de Cacheo y Escritura Transaccional en Lotes - Implementa la funcionalidad de cacheo de logs en memoria y escritura transaccional para reducir el overhead de E/S de disco en SQLite [1, 2]. - Cambios principales: 1. Refactorización de LogQueryPerformance y LogServerError para que solo almacenen logs en las cachés globales (QueryLogCache y ErrorLogCache) [3]. 2. Introducción de WriteQueryLogsBatch y WriteErrorLogsBatch, que vacían las cachés y realizan la inserción a SQLite dentro de una única transacción atómica (`BeginTransaction`/`TransactionSuccessful`), disparada por umbral (`LOG_CACHE_THRESHOLD`) o periódicamente por `TimerLogs_Tick` [4-7]. 3. Corrección del manejo de objetos List en las rutinas de lote (Write*LogsBatch): Se implementó la copia explícita de contenido (`List.AddAll`) dentro del bloqueo (`MainConnectorsLock`) para asegurar que el lote mantenga sus registros, resolviendo el problema de tamaño cero causado por la asignación de referencias.
736 lines
34 KiB
QBasic
736 lines
34 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!
|
|
|
|
' <<<< ¡BUSY_CONNECTIONS YA SE CAPTURABA BIEN! >>>>
|
|
' 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
|
|
' <<<< ¡CORRECCIÓN CLAVE: Aseguramos que el valor sea Int! >>>>
|
|
poolBusyConnectionsForLog = poolStats.Get("BusyConnections").As(Int) ' Capturamos el valor.
|
|
End If
|
|
End If
|
|
' <<<< ¡FIN DE CAPTURA! >>>>
|
|
|
|
' 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 ---
|
|
Log(LastException) ' Registra la excepción completa en el log.
|
|
Main.LogServerError("ERROR", "DBHandlerB4X.Handle", LastException.Message, dbKey, q, req.RemoteAddress) ' <-- Nuevo Log
|
|
SendPlainTextError(resp, 500, LastException.Message) ' 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")
|
|
|
|
' 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 (N/A)": 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.
|
|
' <<< FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA DENTRO DEL BATCH >>>
|
|
Next
|
|
|
|
res.Rows.Add(Array As Object(0)) ' 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
|
|
' 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 = ""
|
|
|
|
Try
|
|
con.BeginTransaction
|
|
' Itera para procesar cada comando del lote.
|
|
For i = 0 To numberOfStatements - 1
|
|
' 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)
|
|
If numberOfStatements = 1 Then
|
|
singleQueryName = queryName 'Capturamos el nombre del query.
|
|
End If
|
|
Dim sqlCommand As String = Connector.GetCommand(DB, queryName)
|
|
|
|
' <<< 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
|
|
|
|
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) >>>
|
|
Next
|
|
|
|
con.TransactionSuccessful ' Confirma la transacción.
|
|
|
|
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 res
|
|
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 |