B4J=true Group=Default Group ModulesStructureVersion=1 Type=Class Version=10.3 @EndOfDesignText@ ' Módulo de clase: DBHandlerJSON ' Este handler se encarga de procesar las peticiones HTTP que esperan o envían datos en formato JSON. ' Es ideal para clientes web (JavaScript, axios, etc.) o servicios que interactúan con el servidor ' mediante un API RESTful. Soporta tanto GET con JSON en un parámetro 'j' como POST con JSON ' en el cuerpo de la petición. Sub Class_Globals ' Declara una variable privada para mantener una instancia del conector RDC. ' Este objeto maneja la comunicación con la base de datos específica de la petición. Private Connector As RDCConnector End Sub ' Subrutina de inicialización de la clase. Se llama cuando se crea un objeto de esta clase. Public Sub Initialize ' No se requiere inicialización específica para esta clase en este momento. End Sub ' Este es el método principal que maneja las peticiones HTTP entrantes (req) y prepara la respuesta (resp). Sub Handle(req As ServletRequest, resp As ServletResponse) ' --- Headers CORS (Cross-Origin Resource Sharing) --- ' Estos encabezados son esenciales para permitir que aplicaciones web (clientes) ' alojadas en diferentes dominios puedan comunicarse con este servidor. resp.SetHeader("Access-Control-Allow-Origin", "*") ' Permite peticiones desde cualquier origen. resp.SetHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") ' Métodos HTTP permitidos. resp.SetHeader("Access-Control-Allow-Headers", "Content-Type") ' Encabezados permitidos. ' Las peticiones OPTIONS son pre-vuelos de CORS y no deben procesar lógica de negocio ni contadores. If req.Method = "OPTIONS" Then Return ' Salimos directamente para estas peticiones. End If Dim start As Long = DateTime.Now ' Registra el tiempo de inicio de la petición para calcular la duración. ' Declaraciones de variables con alcance en toda la subrutina para asegurar la limpieza final. Dim con As SQL ' La conexión a la BD, se inicializará más tarde. Dim queryNameForLog As String = "unknown_json_command" ' Nombre del comando para el log, con valor por defecto. 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. Dim finalDbKey As String = "DB1" ' Identificador de la base de datos, con valor por defecto "DB1". Dim requestsBeforeDecrement As Int = 0 ' Contador de peticiones activas antes de decrementar, inicializado en 0. Dim Total As Int = 0 Try ' --- INICIO: Bloque Try que envuelve la lógica principal del Handler --- Dim jsonString As String ' <<<< INICIO: Lógica para manejar peticiones POST con JSON en el cuerpo >>>> If req.Method = "POST" And req.ContentType.Contains("application/json") Then ' Si es un POST con JSON en el cuerpo, leemos directamente del InputStream. Dim Is0 As InputStream = req.InputStream Dim bytes() As Byte = Bit.InputStreamToBytes(Is0) ' Lee el cuerpo completo de la petición. jsonString = BytesToString(bytes, 0, bytes.Length, "UTF8") ' Convierte los bytes a una cadena JSON. Is0.Close ' Cierra explícitamente el InputStream para liberar recursos. Else ' De lo contrario, asumimos que el JSON viene en el parámetro 'j' de la URL (método legacy/GET). jsonString = req.GetParameter("j") End If ' <<<< FIN: Lógica para manejar peticiones POST con JSON en el cuerpo >>>> ' Validación inicial: Si no hay JSON, se envía un error 400. If jsonString = Null Or jsonString = "" Then Dim ErrorMsg As String = "Falta el parámetro 'j' en el URL o el cuerpo JSON en la petición." SendErrorResponse(resp, 400, ErrorMsg) Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log duration = DateTime.Now - start CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) Return End If Dim parser As JSONParser parser.Initialize(jsonString) ' Inicializa el parser JSON con la cadena recibida. Dim RootMap As Map = parser.NextObject ' Parsea el JSON a un objeto Map. Dim execType As String = RootMap.GetDefault("exec", "") ' Obtiene el tipo de ejecución (ej. "ExecuteQuery"). ' Obtiene el nombre de la query. Si no está en "query", busca en "exec". queryNameForLog = RootMap.GetDefault("query", "") If queryNameForLog = "" Then queryNameForLog = RootMap.GetDefault("exec", "unknown_json_command") Dim paramsList As List = RootMap.Get("params") ' Obtiene la lista de parámetros para la query. If paramsList = Null Or paramsList.IsInitialized = False Then paramsList.Initialize ' Si no hay parámetros, inicializa una lista vacía. End If ' <<<< ¡CORRECCIÓN CLAVE: RESOLVEMOS finalDbKey del JSON ANTES de usarla para los contadores! >>>> ' Esto asegura que el contador y el conector usen la DB correcta. If RootMap.Get("dbx") <> Null Then finalDbKey = RootMap.Get("dbx") ' <<<< ¡FIN DE CORRECCIÓN CLAVE! >>>> ' --- INICIO: Conteo de peticiones activas para esta finalDbKey (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. ' 1. Aseguramos que el valor inicial sea un Int y lo recuperamos como Int (usando .As(Int)). Dim currentCountFromMap As Int = GlobalParameters.ActiveRequestsCountByDB.GetDefault(finalDbKey, 0).As(Int) GlobalParameters.ActiveRequestsCountByDB.Put(finalDbKey, currentCountFromMap + 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'. requestsBeforeDecrement = currentCountFromMap + 1 ' Los logs de depuración para el incremento del contador pueden ser descomentados para una depuración profunda. ' Log($"[DEBUG] Handle Increment (JSON): dbKey=${finalDbKey}, currentCountFromMap=${currentCountFromMap}, requestsBeforeDecrement=${requestsBeforeDecrement}, Map state: ${GlobalParameters.ActiveRequestsCountByDB}"$) ' --- FIN: Conteo de peticiones activas --- ' Inicializa el Connector con la finalDbKey resuelta. Connector = Main.Connectors.Get(finalDbKey) ' Validación: Si el dbKey no es válido o no está configurado en Main.listaDeCP. If Main.listaDeCP.IndexOf(finalDbKey) = -1 Then Dim ErrorMsg As String = "Parámetro 'DB' inválido. El nombre '" & finalDbKey & "' no es válido." SendErrorResponse(resp, 400, ErrorMsg) Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log duration = DateTime.Now - start CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) Return End If con = Connector.GetConnection(finalDbKey) ' ¡La conexión a la BD se obtiene aquí del pool de conexiones! ' <<<< ¡CAPTURAMOS BUSY_CONNECTIONS INMEDIATAMENTE DESPUÉS DE OBTENER LA CONEXIÓN! >>>> ' 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! >>>> ' Obtiene la sentencia SQL correspondiente al nombre del comando desde config.properties. Dim sqlCommand As String = Connector.GetCommand(finalDbKey, queryNameForLog) ' Validación: Si el comando SQL no fue encontrado en la configuración. If sqlCommand = Null Or sqlCommand = "null" Or sqlCommand.Trim = "" Then Dim errorMessage As String = $"El comando '${queryNameForLog}' no fue encontrado en el config.properties de '${finalDbKey}'."$ Log(errorMessage) Main.LogServerError("ERROR", "DBHandlerJSON.Handle", errorMessage, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log SendErrorResponse(resp, 400, errorMessage) duration = DateTime.Now - start CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) Return End If ' --- Lógica para ejecutar diferentes tipos de comandos basados en el parámetro 'execType' --- If execType.ToLowerCase = "executequery" Then ' --- INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA --- Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryNameForLog, finalDbKey, sqlCommand, paramsList, Connector.IsParameterToleranceEnabled) If validationResult.Success = False Then SendErrorResponse(resp, 400, validationResult.ErrorMessage) duration = DateTime.Now - start CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) Return ' Salida temprana. End If Dim rs As ResultSet ' Ejecuta la consulta SQL con la lista de parámetros validada. rs = con.ExecQuery2(sqlCommand, validationResult.ParamsToExecute) ' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA --- Dim ResultList As List ResultList.Initialize ' Lista para almacenar los resultados de la consulta. Dim jrs As JavaObject = rs ' Objeto Java subyacente del ResultSet para metadatos. Dim rsmd As JavaObject = jrs.RunMethod("getMetaData", Null) ' Metadatos del ResultSet. Dim cols As Int = rsmd.RunMethod("getColumnCount", Null) ' Número de columnas. Do While rs.NextRow ' Itera sobre cada fila del resultado. Dim RowMap As Map RowMap.Initialize ' Mapa para almacenar los datos de la fila actual. For i = 1 To cols ' Itera sobre cada columna. Dim ColumnName As String = rsmd.RunMethod("getColumnName", Array(i)) ' Nombre de la columna. Dim value As Object = jrs.RunMethod("getObject", Array(i)) ' Valor de la columna. RowMap.Put(ColumnName, value) ' Añade la columna y su valor al mapa de la fila. Next ResultList.Add(RowMap) ' Añade el mapa de la fila a la lista de resultados. Loop rs.Close ' Cierra el ResultSet. SendSuccessResponse(resp, CreateMap("result": ResultList)) ' Envía la respuesta JSON de éxito. Else If execType.ToLowerCase = "executecommand" Then ' --- INICIO VALIDACIÓN DE PARÁMETROS CENTRALIZADA --- Dim validationResult As ParameterValidationResult = ParameterValidationUtils.ValidateAndAdjustParameters(queryNameForLog, finalDbKey, sqlCommand, paramsList, Connector.IsParameterToleranceEnabled) If validationResult.Success = False Then SendErrorResponse(resp, 400, validationResult.ErrorMessage) duration = DateTime.Now - start CleanupAndLog(finalDbKey, queryNameForLog, duration, req.RemoteAddress, requestsBeforeDecrement, poolBusyConnectionsForLog, con) Return ' Salida temprana. End If Dim affectedCount As Int = 1 ' Asumimos éxito (1) si ExecNonQuery2 no lanza una excepción. con.ExecNonQuery2(sqlCommand, validationResult.ParamsToExecute) ' Ejecuta un comando con la lista de parámetros validada. SendSuccessResponse(resp, CreateMap("affectedRows": affectedCount, "message": "Command executed successfully")) ' Envía confirmación de éxito. ' --- FIN VALIDACIÓN DE PARÁMETROS CENTRALIZADA --- Else Dim ErrorMsg As String = "Parámetro 'exec' inválido. '" & execType & "' no es un valor permitido." SendErrorResponse(resp, 400, ErrorMsg) Main.LogServerError("ERROR", "DBHandlerJSON.Handle", ErrorMsg, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log ' El flujo continúa hasta la limpieza final si no hay un Return explícito. End If Catch ' --- CATCH: Maneja errores generales de ejecución o de SQL/JSON --- Log(LastException) ' Registra la excepción completa en el log. Main.LogServerError("ERROR", "DBHandlerJSON.Handle", LastException.Message, finalDbKey, queryNameForLog, req.RemoteAddress) ' <-- Nuevo Log SendErrorResponse(resp, 500, LastException.Message) ' Envía un error 500 al cliente. queryNameForLog = "error_processing_json" ' Para registrar que hubo un error en el log. 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. ' Llama a la subrutina centralizada para registrar el rendimiento y limpiar recursos. CleanupAndLog(finalDbKey, queryNameForLog, 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) ' Los logs de depuración para CleanupAndLog pueden ser descomentados para una depuración profunda. ' Log($"[DEBUG] CleanupAndLog Entry (JSON): 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 (JSON): 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 (JSON): 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 de ayuda para respuestas JSON --- ' Construye y envía una respuesta JSON de éxito. ' resp: El objeto ServletResponse para enviar la respuesta. ' dataMap: Un mapa que contiene los datos a incluir en la respuesta JSON. Private Sub SendSuccessResponse(resp As ServletResponse, dataMap As Map) ' Añade el campo "success": true al mapa de datos para indicar que todo salió bien. dataMap.Put("success", True) ' Crea un generador de JSON. Dim jsonGenerator As JSONGenerator jsonGenerator.Initialize(dataMap) ' Establece el tipo de contenido de la respuesta a "application/json". resp.ContentType = "application/json" ' Escribe la cadena JSON generada en el cuerpo de la respuesta HTTP. resp.Write(jsonGenerator.ToString) End Sub ' Construye y envía una respuesta JSON de error. ' resp: El objeto ServletResponse para enviar la respuesta. ' statusCode: El código de estado HTTP (ej. 400 para error del cliente, 500 para error del servidor). ' errorMessage: El mensaje de error que se enviará al cliente. Private Sub SendErrorResponse(resp As ServletResponse, statusCode As Int, errorMessage As String) ' Personaliza el mensaje de error si es un error común de parámetros de Oracle o JDBC. If errorMessage.Contains("Índice de columnas no válido") Or errorMessage.Contains("ORA-17003") Then errorMessage = "NUMERO DE PARAMETROS EQUIVOCADO: " & errorMessage End If ' Crea un mapa con el estado de error y el mensaje. Dim resMap As Map = CreateMap("success": False, "error": errorMessage) ' Genera la cadena JSON a partir del mapa. Dim jsonGenerator As JSONGenerator jsonGenerator.Initialize(resMap) ' Establece el código de estado HTTP (ej. 400 para error del cliente, 500 para error del servidor). resp.Status = statusCode ' Establece el tipo de contenido y escribe la respuesta de error. resp.ContentType = "application/json" resp.Write(jsonGenerator.ToString) End Sub