SQLExecutor.java

/*
** Module   : SQLExecutor.java
** Abstract : Execute SQLs and potentially log them
**
** Copyright (c) 2023-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 RAA 20230607 First revision with basic usage.
**     RAA 20230608 Integrated configuration for each database.
**     RAA 20230609 Replaced Configuration with SQLStatementLoggerConfiguration.
** 002 RAA 20230615 Refactored so that all execute methods receive the SQL.
**     RAA 20230724 Added some specific types of Statement to avoid the direct use of ConsumerWithException.
**                  Added LambaLoggerType.
** 003 RAA 20231011 Refactored cases that allow the use of sensitive data.
** 004 ICP 20241114 Added calls to update the query counter JMX.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
**
**   0. Attribution Requirement.
**
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
**
**   1. No License To Use Trademarks.
**
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
**
**   2. No Misrepresentation of Affiliation.
**
**     You may not represent yourself as Golden Code Development Corporation or FWD.
**
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
**
**   3. No Misrepresentation of Source or Origin.
**
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist.orm;

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.function.*;
import java.util.stream.Collectors;

import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.persist.Database;
import com.goldencode.p2j.persist.DatabaseManager;

/**
 * Class responsible with executing SQLs.
 * If logging is intended, then the execution job will be passed to {@link SQLStatementLogger}.
 */
public class SQLExecutor
{
   /** The static singleton instance, initially null for lazy initialization. */
   private static SQLExecutor instance = null;

   /** Singleton instance of {@link QueryCounterJMX} for managing query counting operations. */
   private static final QueryCounterJMX SIMPLE_QUERY_COUNTER =
         QueryCounterJMX.getInstance(FwdServerJMX.QueryCounterEnum.QueryCounter);

   /** Usually, SQLs are executed one by one. There are methods that take care of
    *  SQLs executed in proper batches. 
    */
   private final int defaultBatchSize = 1;

   /**
    * The private default constructor.
    */
   private SQLExecutor()
   {
      SIMPLE_QUERY_COUNTER.updateSupplier(SQLQueryCounterConfiguration::getConfiguration);
      List<String> uniqueDatabases = DatabaseManager.getAllDatabases()
                                                    .stream()
                                                    .map(Database::getName)
                                                    .distinct()
                                                    .collect(Collectors.toList());
      SIMPLE_QUERY_COUNTER.bootstrap(uniqueDatabases);
   }
   
   /**
    * Static accessor to singleton with lazy initialization.
    * 
    * @return  The {@code SQLExecutor} singleton instance.
    */
   public static synchronized SQLExecutor getInstance()
   {
      if (instance == null)
      {
         instance = new SQLExecutor();
      }
      return instance;
   }
   
   /**
    * Logs a statement for a database. If the actions for this {@code Database} are disabled the method will
    * return without printing anything.
    * 
    * @param   <T>
    *          The statement type.
    * @param   database
    *          The reference {@code Database}.
    * @param   sql
    *          String representing the SQL used in the statement.
    * @param   r
    *          A {@link ConsumerWithException} that handles the execution of a statement.
    * @param   receiver
    *          Receiver denoting the statement that will be executed.
    */
   public <T> void execute(Database database, String sql, ConsumerWithException<T> r, T receiver)
   throws SQLException
   {
      execute(database, sql, r, receiver, null, defaultBatchSize, LambdaLoggerType.CONSUMER);
   }
   
   /**
    * Logs a statement for a database. If the actions for this {@code Database} are disabled the method will
    * return without printing anything.
    * 
    * @param   <T>
    *          The statement type.
    * @param   database
    *          The reference {@code Database}.
    * @param   sql
    *          String representing the SQL used in the statement.
    * @param   r
    *          A {@link ConsumerWithException} that handles the execution of a statement.
    * @param   receiver
    *          Receiver denoting the statement that will be executed.
    * @param   batchSize
    *          How many SQLs are executed in this batch.
    */
   public <T> void execute(Database database, 
                           String sql,
                           ConsumerWithException<T> r, 
                           T receiver, 
                           int batchSize)
   throws SQLException
   {
      execute(database, sql, r, receiver, null, batchSize, LambdaLoggerType.CONSUMER);
   }
   
   /**
    * Logs a statement for a database. If the actions for this {@code Database} are disabled the method will
    * return without printing anything.  It will use the {@code toString()} method of the {@code Statement}.
    * 
    * @param   <T>
    *          The statement type.
    * @param   <U>
    *          The argument type.
    * @param   database
    *          The reference {@code Database}.
    * @param   r
    *          A {@link BiConsumerWithException} that handles the execution of a statement with a parameter.
    * @param   receiver
    *          Receiver denoting the statement that will be executed.
    * @param   otherReceiver
    *          The parameter that will be given to the execution.
    */
   public <T, U> void execute(Database database,
                              BiConsumerWithException<T, U> r, 
                              T receiver, 
                              U otherReceiver)
   throws SQLException
   {
      execute(database, null, r, receiver, otherReceiver, defaultBatchSize, LambdaLoggerType.BICONSUMER);
   }
   
   /**
    * Logs a statement for a database. If the actions for this {@code Database} are disabled the method will
    * return without printing anything.  It will use the {@code toString()} method of the {@code Statement}.
    * 
    * @param   <T>
    *          The statement type.
    * @param   <U>
    *          The result type.
    * @param   database
    *          The reference {@code Database}.
    * @param   sql
    *          The SQL used in the statement.
    * @param   r
    *          {@link FunctionWithException} that handles the execution of a statement and returns a result.
    * @param   receiver
    *          Receiver denoting the statement that will be executed.
    *
    * @return  The result generated by the execution of the statement.
    */
   public <T, U> U execute(Database database, String sql, FunctionWithException<T, U> r, T receiver)
   throws SQLException
   {
      return execute(database, sql, r, receiver, null, defaultBatchSize, LambdaLoggerType.FUNCTION);
   }
   
   /**
    * Logs a statement for a database. If the actions for this {@code Database} are disabled the method will
    * return without printing anything.
    * 
    * @param   <T>
    *          The statement type.
    * @param   <U>
    *          The argument type or the result type, depending on {@code LambaLoggerType}.
    * @param   database
    *          The reference {@code Database}.
    * @param   sql
    *          The SQL used in the statement.
    * @param   r
    *          A {@link LambdaLoggerType} that handles the execution of a statement.
    * @param   receiver
    *          Receiver denoting the statement that will be executed.
    * @param   otherReceiver
    *          The parameter that will be given to the execution.
    * @param   batchSize
    *          How many SQLs are executed in this batch.
    * @param   type
    *          The type of the receiver.
    *          
    * @return  The result generated by the execution of the statement, if requested, or null otherwise.
    */
   private <T, U> U execute(Database database, 
                            String sql, 
                            LambdaLogger r, 
                            T receiver, 
                            U otherReceiver, 
                            int batchSize, 
                            LambdaLoggerType type) 
   throws SQLException
   {
      String databaseName = database.getName();

      SQLStatementLoggerConfiguration config = SQLStatementLoggerConfiguration.
                                               getConfiguration(databaseName);
      U result = null;
      SIMPLE_QUERY_COUNTER.updateCount(databaseName, new QueryInfo(sql, receiver));
      switch (config.getLoggingType())
      {
         case NONE:
            return executeImpl(r, receiver, otherReceiver, type);
         case LOGGING:
            try 
            {
               result = executeImpl(r, receiver, otherReceiver, type);
            }
            finally
            {
               boolean sendFullStatement = sql == null;
               SQLStatementLogger.getLogger().log(databaseName, 
                                                  sendFullStatement ? receiver.toString() : sql, 
                                                  sendFullStatement, 
                                                  config, 
                                                  batchSize, 
                                                  null);
            }
            break;
         case PROFILING:
            long start = System.nanoTime();
            try 
            {
               result = executeImpl(r, receiver, otherReceiver, type);
            }
            finally
            {
               long finish = System.nanoTime();
               String statement;
               boolean isFullStatement;
               if (config.isAllowSensitive() || sql == null)
               {
                  statement = receiver.toString();
                  isFullStatement = true;
               }
               else
               {
                  statement = sql;
                  isFullStatement = false;
               }
               
               SQLStatementLogger.getLogger().log(databaseName, 
                                                  statement, 
                                                  isFullStatement,
                                                  config, 
                                                  batchSize, 
                                                  (finish - start) / 1000000.0);
            }
            break;
      }
      
      return result;
   }
   
   /**
    * 
    * @param   <T>
    *          The statement type.
    * @param   <U>
    *          The argument type or the result type, depending on {@code LambaLoggerType}.
    * @param   r
    *          A {@link LambdaLoggerType} that handles the execution of a statement.
    * @param   receiver
    *          Receiver denoting the statement that will be executed.
    * @param   otherReceiver
    *          The parameter that will be given to the execution.
    * @param   type
    *          The type of the receiver.
    * 
    * @return  U
    *          The result.
    */
   private <T, U> U executeImpl(LambdaLogger r, T receiver, U otherReceiver, LambdaLoggerType type) 
   throws SQLException
   {
      switch (type)
      {
         case CONSUMER:
            ((ConsumerWithException<T>) r).accept(receiver);
            break;
         case BICONSUMER:
            ((BiConsumerWithException<T, U>) r).accept(receiver, otherReceiver);
            break;
         case FUNCTION:
            return ((FunctionWithException<T, U>) r).apply(receiver);
         default:
            break;
      }
      
      return null;
   }
   
   /**
    * Basic interface that groups the functional interfaces used for logging statements.
    * For the moment, one may use only {@link ConsumerWithException}, {@link BiConsumerWithException} or
    * {@link FunctionWithException}.
    */
   public interface LambdaLogger
   {
      
   }
   
   /**
    * Enumeration used to distinguish between different {@link LambdaLogger}s. Depending on their type, 
    * the behavior varies (returning/not returning a result, using/not using a parameter, etc.).
    */
   protected enum LambdaLoggerType
   {
      /** Type that matches the {@code Consumer} behavior: uses one thing and returns nothing. */
      CONSUMER,
      
      /** Type that matches the {@code BiConsumer} behavior: uses two things and returns nothing. */
      BICONSUMER,
      
      /** Type that matches the {@code Function} behavior: uses and returns one thing. */
      FUNCTION;
   }
   
   /**
    * Functional interface, similar to {@link Consumer}, but with the capability to throw an Exception. 
    *
    * @param   <T>
    *          The type of the argument.
    */
   @FunctionalInterface
   public interface ConsumerWithException<T> 
   extends LambdaLogger
   {
      /**
       * Performs this operation on the given argument.
       *
       * @param   t 
       *          The input argument.
       */
      void accept(T t) 
      throws SQLException;
   }
   
   /**
    * A more specific {@code ConsumerWithException}, applied to {@code PreparedStatement} only.
    */
   public interface DMLStatement
   extends ConsumerWithException<PreparedStatement>
   {
      
   }
   
   /**
    * Functional interface, similar to {@link BiConsumer}, but with the capability to throw an Exception. 
    *
    * @param   <T>
    *          The type of the first argument.
    * @param   <U>
    *          The type of the second argument.
    */
   @FunctionalInterface
   public interface BiConsumerWithException<T, U> 
   extends LambdaLogger
   {
      /**
       * Performs this operation on the given arguments.
       *
       * @param   t 
       *          The first input argument.
       * @param   u 
       *          The second input argument.
       */
      void accept(T t, U u)
      throws SQLException;
   }
   
   /**
    * Functional interface, similar to {@link Function}, but with the capability to throw an Exception. 
    *
    * @param   <T>
    *          The type of the first argument.
    * @param   <R>
    *          The type of the second argument.
    */
   @FunctionalInterface
   public interface FunctionWithException<T, R> 
   extends LambdaLogger
   {
      /**
       * Applies this function to the given argument.
       *
       * @param   t 
       *          The function argument.
       *          
       * @return  The function result.
       */
      R apply(T t)
      throws SQLException;
   }
   
   /**
    * A more specific {@code FunctionWithException}, applied to {@code PreparedStatement} and 
    * {@code ResultSet} only.
    */
   public interface QueryStatement
   extends FunctionWithException<PreparedStatement, ResultSet>
   {
      
   }
   
   /**
    * A more specific {@code FunctionWithException}, applied to {@code PreparedStatement} and 
    * {@code Integer} only.
    */
   public interface UpdateStatement
   extends FunctionWithException<PreparedStatement, Integer>
   {
      
   }
   
   /**
    * A more specific {@code FunctionWithException}, applied to {@code PreparedStatement} and 
    * {@code int[]} only.
    */
   public interface UpdateBatchStatement
   extends FunctionWithException<PreparedStatement, int[]>
   {
      
   }
}