mirror of
https://github.com/KeymonSoft/jRDC-MultiDB-Hikari.git
synced 2026-04-17 21:06:23 +00:00
Commit inicial
This commit is contained in:
311
DBHandlerJSON.bas
Normal file
311
DBHandlerJSON.bas
Normal file
@@ -0,0 +1,311 @@
|
||||
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.
|
||||
' Log($">>>>>>>>>> ${poolStats.Get("BusyConnections")} "$)
|
||||
End If
|
||||
End If
|
||||
' <<<< ¡FIN DE CAPTURA! >>>>
|
||||
|
||||
Dim cachedStatsJSON As Map = Main.LatestPoolStats.Get(finalDbKey).As(Map)
|
||||
|
||||
If cachedStatsJSON.IsInitialized Then
|
||||
' Los valores ya fueron capturados: poolBusyConnectionsForLog y requestsBeforeDecrement
|
||||
cachedStatsJSON.Put("BusyConnections", poolBusyConnectionsForLog)
|
||||
cachedStatsJSON.Put("HandlerActiveRequests", requestsBeforeDecrement)
|
||||
If poolStats.ContainsKey("TotalConnections") Then
|
||||
cachedStatsJSON.Put("TotalConnections", poolStats.Get("TotalConnections"))
|
||||
End If
|
||||
If poolStats.ContainsKey("IdleConnections") Then
|
||||
cachedStatsJSON.Put("IdleConnections", poolStats.Get("IdleConnections"))
|
||||
End If
|
||||
' Re-escribir el mapa en el cache global (es Thread-Safe)
|
||||
Main.LatestPoolStats.Put(finalDbKey, cachedStatsJSON)
|
||||
' Log(Main.LatestPoolStats)
|
||||
End If
|
||||
|
||||
' Log($"Total: ${poolStats.Get("TotalConnections")}, Idle: ${poolStats.Get("IdleConnections")}, busy: ${poolBusyConnectionsForLog}, active: ${requestsBeforeDecrement}"$)
|
||||
|
||||
' 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
|
||||
Reference in New Issue
Block a user