ConnectTableUpdater.java

/*
** Module   : ConnectTableUpdater.java
** Abstract : Manages metadata connect records for a particular, primary database
**
** Copyright (c) 2015-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------------Description----------------------------------------
** 001 ECF 20150111 Created initial version.
** 002 ECF 20171030 Implemented _connect user and time, _myconnection table and fields.
** 003 OM  20181213 Ignore database connection events during DatabaseManager initialization.
** 004 OM  20190311 Fixed _connect record info when an user explicitly connects.
** 005 ECF 20200906 New ORM implementation.
** 006 IAS 20200819 Added support for additional _Connect table fields.
** 007 CA  20200924 Replaced Method.invoke with ReflectASM.
** 008 IAS 20210401 Do not update MyConnection on server startup.
**     IAS 20210414 Added _UserTableStat cleanup
** 009 ECF 20210908 Removed synchronization from connect/disconnect hooks. Replaced use of persistence APIs
**                  which have context-local side effects from context cleanup (disconnect) code.
**     ECF 20220630 Fixed Minimal* getter/setter names to match conversion of historical names in _Connect
**                  and _MyConnection tables. Fixed hard-coded SQL to reflect corresponding changes in column
**                  names.
**     TJD 20220504 Upgrade do Java 11 minor changes
** 010 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 011 GBB 20230825 SecurityManager session methods calls updated.
** 012 TJD 20240123 Java 17 compatibility updates
** 013 OM  20240909 Improved Database API.
** 014 OM  20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
**                  based on [sharedDb] parameter.
*/

/*
** 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.meta;

import java.lang.reflect.*;
import java.sql.*;
import java.util.*;
import java.util.logging.*;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.orm.Session;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.proxy.*;

/**
 * A {@link com.goldencode.p2j.persist.meta.ConnectionListener database connection listener implementation}
 * which receives notifications of database connection events (at a 4GL level, not the JDBC level) and
 * updates the metadata connect and myconnection tables accordingly. User table statistics are also gathered
 * for disconnect events, if the {@code _Usertablestat} metadata table used by the application.
 * <p>
 * This involves the creation, lookup, and deletion of metadata connect and myconnection records.
 * Since such events are minimized in real applications (it is a relatively expensive operation),
 * this work is done inline (as opposed to offloaded into a worker thread).
 * <p>
 * One instance of this class is created per database, per user session.
 */
public final class ConnectTableUpdater
implements ConnectionListener
{
   /** Logger */
   private static final CentralLogger log = CentralLogger.get(ConnectTableUpdater.class);
   
   /** Metadata connect primary table name */
   private static String CONNECT_TABLE;
   
   /** Metadata myconnection table name */
   private static String MYCONNECTION_TABLE;
   
   /** MetaConnectImpl DMO implementation class */
   private static final Class<? extends Record> dmoConnectClass;
   
   /** MetaMyconnectionImpl DMO implementation class */
   private static final Class<? extends Record> dmoMyconnClass;
   
   /** Map of {@code MinimalConnect} methods to {@code MetaConnectImpl} methods */
   private static final Map<Method, Method> methodMap = new HashMap<>();
   
   /** Dummy empty class to be used as the super class for creating proxies */
   public static class Empty {}
   
   /*
    * Before this static block is executed the MetadataManager.initialize() is processed. At that
    * time, if the _Connect and _MyConnection meta tables are present configured in the application's
    * configuration, the specialized DMO interfaces are already registered with DmoMetadataManager.
    */
   static
   {
      Class<? extends Record> implementingClass;
      String dmoName = DmoMetadataManager.getDmoBasePackage() + "._meta.MetaConnect";
      try
      {
         DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoName, null);
         implementingClass = dmoInfo.getImplementationClass();
         CONNECT_TABLE = dmoInfo.getSqlTableName();
         
         // map MinimalConnect methods to corresponding dmoConnectClass methods; if we try to use
         // MinimalConnect methods with a MetaConnectImpl object, we will raise IllegalArgumentException
         // during Method.invoke(), because our object will not be an instance of MinimalConnect
         for (Method ifcMeth : MinimalConnect.class.getMethods())
         {
            Method dmoMeth = implementingClass.getMethod(ifcMeth.getName(), ifcMeth.getParameterTypes());
            methodMap.put(ifcMeth, dmoMeth);
         }
      }
      catch (IllegalArgumentException exc)
      {
         // not a fatal error; application does not use _connect table
         implementingClass = null;
      }
      catch (NoSuchMethodException exc)
      {
         // our MinimalConnect does not match the full _Connect DMO from conversion time
         throw new RuntimeException("Error preparing MetaConnect implementation class", exc);
      }
      dmoConnectClass = implementingClass;
      
      // TODO: this assumes a particular DMO interface name conversion, which can change with configuration;
      // we should only allow a single, deterministic name conversion for the metaschema tables
      dmoName = DmoMetadataManager.getDmoBasePackage() + "._meta.MetaMyconnection";
      try
      {
         DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoName, null);
         implementingClass = dmoInfo.getImplementationClass();
         MYCONNECTION_TABLE = dmoInfo.getSqlTableName();
         
         // map MinimalConnect methods to corresponding dmoMyconnClass methods; if we try to use
         // MinimalMyconnection methods with a MetaConnect DMO implementation object, we will raise
         // IllegalArgumentException during Method.invoke(), because our object will not be an instance of
         // MinimalMyconnection
         for (Method ifcMeth : MinimalMyconnection.class.getMethods())
         {
            Method dmoMeth = implementingClass.getMethod(ifcMeth.getName(), ifcMeth.getParameterTypes());
            methodMap.put(ifcMeth, dmoMeth);
         }
      }
      catch (IllegalArgumentException exc)
      {
         // not a fatal error; application does not use _MyConnection table
         implementingClass = null;
      }
      catch (NoSuchMethodException exc)
      {
         // our MinimalMyconnection does not match the full _MyConnection DMO from conversion time
         throw new RuntimeException("Error preparing MetaMyconnection implementation class", exc);
      }
      dmoMyconnClass = implementingClass;
   }
   
   /** Persistence helper object for metadata database */
   private final Persistence persistence;
   
   /** Security manager */
   private final SecurityManager securityManager;
   
   /** Proxy object for the current metadata connect DMO */
   private final MinimalConnect connProxy;
   
   /** Proxy object for the current metadata myconnection DMO */
   private final MinimalMyconnection myconnProxy;
   
   /** Invocation handler which maps proxy method calls to underlying DMO methods */
   private final Handler handler = new Handler();
   
   /** Optional object which updates user table stats metadata. May be {@code null}. */
   private final UserTableStatUpdater userTableStatUpdater;
   
   /**
    * Constructor which stores a persistence object for the associated metadata database, and
    * initializes proxies for the application-specific DMOs.
    * 
    * @param   database
    *          Primary database.
    */
   public ConnectTableUpdater(Database database)
   {
      this.persistence = PersistenceFactory.getInstance(database.toType(Database.Type.META));
      this.securityManager = SecurityManager.getInstance();
      this.connProxy = ProxyFactory.getProxy(Empty.class,
                                             new Class<?>[] { MinimalConnect.class },
                                             handler);
      this.myconnProxy = ProxyFactory.getProxy(Empty.class,
                                               new Class<?>[] { MinimalMyconnection.class },
                                               handler);
      this.userTableStatUpdater = UserTableStatUpdater.get(database);
   }
   
   /**
    * Update the metadata database to reflect a legacy, database connection event.
    * 
    * @param   connectID
    *          Unique identifier for the database connection.
    */
   public void databaseConnected(int64 connectID)
   {
      if (dmoMyconnClass == null || dmoConnectClass == null || !Persistence.isActive())
      {
         return;
      }
      
      // get current session id as the user number
      Integer sid = securityManager.sessionSm.getSessionId();
      character now = new character(datetime.now().toStringMessage());
      
      // we don't do any pessimistic locking here because the records affected are specific to the current
      // user session, thus no other session touches them
      Session session = null;
      try
      {
         session = new Session(persistence.getDatabase(Persistence.META_CTX));
         session.beginTransaction();
         
         // create new MetaConnectImpl DMO instance and back the proxy with it
         Record dmo = dmoConnectClass.getDeclaredConstructor().newInstance();
         dmo.initialize(null, true);
         handler.setDelegate(dmo);
         
         // assign primary key
         Long key = persistence.nextPrimaryKey(CONNECT_TABLE, true);
         dmo.primaryKey(key);
         
         // assign connect ID (required by unique index)
         connProxy.setConnectId(connectID);
         
         // assign fields with values retrieved from client parameters
         ClientParameters parms = StandardServer.getClientParameters();
         if (parms == null)
         {
            // in case of a permanent connection (database connected at server startup, before
            // the actual client connections) set all to unknown
            connProxy.setConnectName(new character(""));
            connProxy.setConnectDevice(new character("CON:"));
            connProxy.setConnectPid(new integer(-1));
         }
         else
         {
            // if the database was connected by a user, use his details:
            String name = parms.osUserName != null ? parms.osUserName : "";
            connProxy.setConnectName(new character(name));
            String device = parms.terminalName != null ? parms.terminalName : "CON:";
            connProxy.setConnectDevice(new character(device));
            connProxy.setConnectPid(new integer(parms.pid));
         }
         
         // assign other known values
         connProxy.setConnectTime(now);
         connProxy.setConnectUsr(new integer(sid));
         connProxy.setConnectType(new character((sid == null || sid == 0) ? "BROK" : "SELF"));
         
         // assign other fields which are mandatory and default to unknown
         character emptyString = new character("");
         connProxy.setConnectBatch(emptyString);
         
         // save the new record
         session.save(dmo, true);
         
         // create new MetaMyconnectionImpl DMO instance and back the proxy with it
         dmo = dmoMyconnClass.getDeclaredConstructor().newInstance();
         dmo.initialize(null, true);
         handler.setDelegate(dmo);
         
         // assign primary key
         key = persistence.nextPrimaryKey(MYCONNECTION_TABLE, true);
         dmo.primaryKey(key);
         
         // assign connect ID (required by unique index)
         myconnProxy.setMyConnId(connectID);
         
         // assign fields with values retrieved from client parameters
         myconnProxy.setMyConnPid(new integer(parms == null ? -1 : parms.pid));
         
         // assign other known values
         myconnProxy.setMyConnUserId(new integer(sid));
         
         // assign other fields
         myconnProxy.setMyConnNumSeqBuffers(new integer(0));
         myconnProxy.setMyConnUsedSeqBuffers(new integer(0));
         myconnProxy.setMyConnTenantId(new integer(0));
         
         // save the new record
         session.save(dmo, true);
         session.commit();
      }
      catch (Exception exc)
      {
         try
         {
            // the main block failed so the session was not committed, revert now any possible changes
            if (session != null)
            {
               session.rollback();
            }
         }
         catch (PersistenceException pe)
         {
            // error already logged in finally block
         }
         finally
         {
            if (log.isLoggable(Level.SEVERE))
            {
               String msg = String.format(
                  "Error inserting into _connect/_myconnection table for %s (%s)",
                  persistence.getDatabase(Persistence.META_CTX).toString(),
                  connectID.toStringMessage());
               log.log(Level.SEVERE, msg, exc);
            }
         }
      }
      finally
      {
         if (session != null)
         {
            try
            {
               session.close();
            }
            catch (PersistenceException e)
            {
               // no-op
            }
         }
      }
   }
   
   /**
    * Update the metadata database to reflect a legacy, database disconnection event.
    * 
    * @param   connectID
    *          Unique identifier for the database disconnection.
    */
   public void databaseDisconnected(int64 connectID)
   {
      if (dmoMyconnClass == null || dmoConnectClass == null)
      {
         return;
      }
      
      // we don't do any pessimistic locking here because the records affected are specific to the current
      // user session, thus no other session touches them
      try (Session session = new Session(persistence.getDatabase(Persistence.META_CTX)))
      {
         try
         {
            Query query = Session.createQuery("delete from MetaConnect where connectId = ?0");
            query.setParameter(0, connectID);
            int res = query.executeUpdate(session);
            
            if (res != 1 && log.isLoggable(Level.INFO))
            {
               log.log(Level.INFO, "Failed to delete current record from meta _Connect");
            }
         }
         catch (PersistenceException e)
         {
            if (log.isLoggable(Level.WARNING))
            {
               log.log(Level.WARNING, "Failed to delete current record from meta _Connect", e);
            }
         }
         
         try
         {
            Integer myConnUserId = null;
            Connection conn = session.getConnection();
            PreparedStatement pstmt = conn.prepareStatement(
               "select my_conn_user_id from meta_myconnection_ where my_conn_id = ?"
            );
            pstmt.setInt(1, connectID.intValue());
            ResultSet rs = pstmt.executeQuery();
            if (rs.next())
            {
               myConnUserId = rs.getInt(1);
            }
            Query query = Session.createQuery("delete from MetaMyconnection where myConnId = ?0");
            query.setParameter(0, connectID);
            int res = query.executeUpdate(session);
            
            if (res != 1 && log.isLoggable(Level.INFO))
            {
               log.log(Level.INFO, "Failed to delete current record from meta _MyConnection");
            }
            else if (myConnUserId != null && userTableStatUpdater != null)
            {
               userTableStatUpdater.disconnected(session, myConnUserId);
            }
         }
         catch (PersistenceException | SQLException e)
         {
            if (log.isLoggable(Level.WARNING))
            {
               log.log(Level.WARNING, "Failed to delete current record from meta _MyConnection", e);
            }
         }
      }
      catch (PersistenceException e)
      {
         if (log.isLoggable(Level.WARNING))
         {
            log.log(Level.WARNING, "Failed to acquire connection to SQL for connection clean up.", e);
         }
      }
   }
   
   /**
    * Invocation handler which delegates calls to the {@link MinimalConnect} connProxy methods to
    * the backing DMO instance.
    */
   private static class Handler
   implements InvocationHandler
   {
      /** DMO delegate whose methods ultimately are invoked */
      private Object delegate = null;
      
      /**
       * Process method invocations on the {@link MinimalConnect} and {@link MinimalMyconnection}
       * proxies by delegating them to the corresponding, backing DMO instance.
       * 
       * @param   proxy
       *          Proxy on which the enclosing class invokes methods.
       * @param   method
       *          Method which was invoked.
       * @param   args
       *          Method arguments.
       * 
       * @throws  Throwable
       *          if the delegated method call throws an exception.
       */
      @Override
      public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable
      {
         Method dmoMethod = methodMap.get(method);
         
         return Utils.invoke(dmoMethod, delegate, args);
      }
      
      /**
       * Set the backing DMO delegate instance.
       * 
       * @param   delegate
       *          DMO whose methods will be invoked.
       */
      void setDelegate(Object delegate)
      {
         this.delegate = delegate;
      }
   }
   
   /**
    * Interface which defines the minimally required methods of the <code>MetaConnect</code>
    * interface. This interface defines the proxy used to create and manipulate metadata connect
    * DMO instances. The DMO's methods cannot be invoked directly from code here, since the
    * <code>MetaConnect</code> interface resides in an application-specific package, which is not
    * known at compile time.
    */
   public interface MinimalConnect
   {
      /**
       * Setter: connectId
       * 
       * @param   connectId
       *          connectId
       */
      public void setConnectId(NumberType connectId);
      
      /**
       * Setter: User-Id
       * 
       * @param   connectUsr
       *          User-Id
       */
      public void setConnectUsr(NumberType connectUsr);
      
      /**
       * Setter: Type
       *
       * @param   connectType
       *          Type
       */
      public void setConnectType(Text connectType);
      
      /**
       * Setter: Name
       * 
       * @param   connectName
       *          Name
       */
      public void setConnectName(Text connectName);
      
      /**
       * Setter: Device
       * 
       * @param   connectDevice
       *          Device
       */
      public void setConnectDevice(Text connectDevice);
      
      /**
       * Setter: Login time
       * 
       * @param   connectTime
       *          Login time
       */
      public void setConnectTime(Text connectTime);
      
      /**
       * Setter: Pid
       * 
       * @param   connectPid
       *          Pid
       */
      public void setConnectPid(NumberType connectPid);
      
      /**
       * Setter: Batch User
       * 
       * @param   connectBatch
       *          Batch User
       */
      public void setConnectBatch(Text connectBatch);
      
      /**
       * Setter: Trans id
       * 
       * @param   connectTransId
       *          Trans id
       */
      public void setConnectTransId(NumberType connectTransId);
      
      /**
       * Setter: Disconnect
       * 
       * @param   connectDisconnect
       *          Disconnect
       */
      public void setConnectDisconnect(NumberType connectDisconnect);
      
      /**
       * Setter: Srv
       * 
       * @param   connectServer
       *          Srv
       */
      public void setConnectServer(NumberType connectServer);
      
      /**
       * Setter: IP Address
       * 
       * @param   connectIpaddress
       *          IP Address
       */
      public void setConnectIpaddress(Text connectIpaddress);
      
      /**
       * Setter: Client Type
       * 
       * @param   connectClientType
       *          Client Type
       */
      public void setConnectClientType(Text connectClientType);
      
      /**
       * Setter: Tenant ID
       * 
       * @param   connectTenantId
       *          Tenant ID
       */
      public void setConnectTenantId(NumberType connectTenantId);
   }
   
   /**
    * Interface which defines the minimally required methods of the <code>MetaMyconnection</code>
    * interface. This interface defines the proxy used to create and manipulate metadata
    * myconnection DMO instances. The DMO's methods cannot be invoked directly from code here,
    * since the <code>MetaMyconnection</code> interface resides in an application-specific
    * package, which is not known at compile time.
    */
   public interface MinimalMyconnection
   {
      /**
       * Setter: myConnId
       * 
       * @param   myConnId
       *          myConnId
       */
      public void setMyConnId(NumberType myConnId);
      
      /**
       * Setter: MyConn-UserId
       * 
       * @param   myConnUserId
       *          MyConn-UserId
       */
      public void setMyConnUserId(NumberType myConnUserId);
      
      /**
       * Setter: MyConn-Pid
       * 
       * @param   myConnPid
       *          MyConn-Pid
       */
      public void setMyConnPid(NumberType myConnPid);
      
      /**
       * Setter: MyConn-NumSeqBuffers
       * 
       * @param   myConnNumSeqBuffers
       *          MyConn-NumSeqBuffers
       */
      public void setMyConnNumSeqBuffers(NumberType myConnNumSeqBuffers);
      
      /**
       * Setter: MyConn-UsedSeqBuffers
       * 
       * @param   myConnUsedSeqBuffers
       *          MyConn-UsedSeqBuffers
       */
      public void setMyConnUsedSeqBuffers(NumberType myConnUsedSeqBuffers);
      
      /**
       * Setter: Tenant Id
       * 
       * @param   myConnTenantId
       *          Tenant Id
       */
      public void setMyConnTenantId(NumberType myConnTenantId);
   }
}