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);
}
}