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})"$ 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). 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) 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})"$ 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