# TransactionTableUpdater.java

    /*
    ** Module   : TransactionTableUpdater.java
    ** Abstract : Synchronizes metadata transaction records for a particular, primary database.
    **
    ** Copyright (c) 2017-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 ECF 20171030 Created initial version.
    ** 002 ECF 20180819 Optimized common case of updates at the expense of reads.
    ** 003 OM  20190122 Rewrote the class to work with new TransactionManager.
    ** 004 ECF 20200906 New ORM implementation.
    ** 005 CA  20200924 Replaced Method.invoke with ReflectASM.
    **     OM  20201012 Force use locally cached meta information instead of map lookup.
    **     ECF 20220630 Fixed MinimalTrans getter/setter names to match conversion of historical names in _Trans
    **                  table.
    **     TJD 20220504 Upgrade do Java 11 minor changes
    **     RAA 20230109 Changed inline statement(s) to prepared statement(s).
    ** 006 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 007 TJD 20240123 Java 17 compatibility updates
    ** 008 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.util.*;
    import java.util.logging.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.Record;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;
    import com.goldencode.proxy.*;

    /**
     * An utility class which receives notifications from the {@link TransactionManager} when a flush
     * of {@code _Trans} metadata VST is needed. This involves dropping the expired records and
     * creation of new transaction records.
     */
    public final class TransactionTableUpdater
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(TransactionTableUpdater.class);

       /** Metadata transaction table name */
       private static final String TRANS_TABLE = "meta_trans";

       /** MetaTrans DMO implementation class */
       private static final Class<? extends Record> dmoClass;

       /** Map of {@code MinimalTrans} methods to {@code MetaTransImpl} methods */
       private static final Map<Method, Method> methodMap = new HashMap<>();

       /*
        * Before this static block is executed the MetadataManager.initialize() is processed. At that
        * time, if the _Trans meta table is present configured in configuration file, the specialized
        * DMO interface - dmoName is already registered with DmoMetadataManager.
        */
       static
       {
          Class<? extends Record> 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
          String dmoName = DmoMetadataManager.getDmoBasePackage() + "._meta.MetaTrans";
          try
          {
             DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(Class.forName(dmoName));
             implementingClass = dmoInfo.getImplementationClass();

             // map MinimalTrans methods to corresponding dmoClass methods; if we try to use
             // MinimalTrans methods with a MetaTransImpl object, we will raise
             // IllegalArgumentException during Method.invoke(), because our object will not be an
             // instance of MinimalTrans
             if (implementingClass != null)
             {
                for (Method ifcMeth : MinimalTrans.class.getMethods())
                {
                   Method dmoMeth = implementingClass.getMethod(ifcMeth.getName(),
                                                                ifcMeth.getParameterTypes());
                   methodMap.put(ifcMeth, dmoMeth);
                }
             }
          }
          catch (ClassNotFoundException | IllegalArgumentException exc)
          {
             // not a fatal error; application does not use _trans table
             implementingClass = null;
          }
          catch (NoSuchMethodException exc)
          {
             // our MinimalTrans does not match the full _Trans DMO from conversion time
             throw new RuntimeException("Error preparing MetaTrans implementation class", exc);
          }
          dmoClass = implementingClass;
       }

       /** Proxy object for the current metadata transaction DMO */
       private final MinimalTrans proxy;

       /** Invocation handler which maps proxy method calls to underlying DMO methods */
       private final Handler handler = new Handler();

       /** Dummy empty class to be used as the super class for proxies. */
       public static class Empty {}

       /**
        * Constructor which stores a persistence object for the associated metadata database, and
        * initializes a proxy for the application-specific DMO.
        */
       public TransactionTableUpdater()
       {
          this.proxy = ProxyFactory.getProxy(Empty.class,
                                             new Class<?>[] { MinimalTrans.class },
                                             handler);
       }

       /**
        * Check whether the project was configured with support for {@code _Trans}.
        *
        * @return  {@code true} when the project was configured with support for {@code _Trans}.
        */
       public static boolean isEnabled()
       {
          return dmoClass != null;
       }

       /**
        * Synchronize metadata table with map of transaction IDs to session IDs, creating and
        * persisting a DMO for each record not already in the table.
        *
        * @param   p
        *          The {@link Persistence} to be used when flushing data to database.
        * @param   transactions
        *          The set of active transactions from the current database, mapped by the users
        *          (connection ID) that opened them.
        */
       public void flush(Persistence p, Map<Integer, Integer> transactions)
       {
          if (!isEnabled())
          {
             return;
          }

          String dbName = p.getDatabase(Persistence.META_CTX).getId().toLowerCase();
          boolean commit = false;
          try
          {
             commit = p.beginTransaction(Persistence.META_CTX);

             // drop all records that are no more valid
             StringBuilder buf = new StringBuilder(
                   "delete from " + TRANS_TABLE + " where not trans_num in (");
             List<Object> args = new ArrayList<>();
             Iterator<Integer> it = transactions.values().iterator();
             for (int i = 0; it.hasNext(); i++)
             {
                if (i > 0)
                {
                   buf.append(',');
                }

                args.add(it.next());
                buf.append('?');
             }
             buf.append(')');
             int delCounter = p.executeSQL(buf.toString(), Persistence.META_CTX, args.toArray());
             if (LOG.isLoggable(Level.FINE))
             {
                LOG.log(Level.FINE, "Dropped " + delCounter + " old _trans records from " + dbName);
             }

             // retrieve all records that are still in database and will be skipped:
             Set<Long> savedIds = new HashSet<>();
             ScrollableResults<Long> sr = p.executeSQLQuery("select trans_num from " + TRANS_TABLE,
                                                            Persistence.META_CTX,
                                                            null);
             while (sr.next())
             {
                Long xid = sr.get(0, Long.class);
                savedIds.add(xid);
             }

             // save/update the records
             for (Map.Entry<Integer, Integer> entry : transactions.entrySet())
             {
                Integer trans_num = entry.getValue();
                if (savedIds.contains(trans_num.longValue()))
                {
                   // this is already in database, leave it as it is
                   continue;
                }

                // create new DMO instance and back the proxy with it
                Record dmo;
                try
                {
                   dmo = dmoClass.getDeclaredConstructor().newInstance();
                   dmo.initialize(null, true);
                }
                catch (ReflectiveOperationException e)
                {
                   return;// failed to instantiate smo
                }
                handler.setDelegate(dmo);

                // assign primary key
                Long key = p.nextPrimaryKey(TRANS_TABLE, true);
                dmo.primaryKey(key);

                // assign transaction ID (required by unique index)
                proxy.setTransId(new int64(key));

                // assign fields with known values
                proxy.setTransNum(new integer(trans_num));
                proxy.setTransUsrnum(new integer(entry.getKey()));
                proxy.setTransFlags(new character("FWD"));
                // NOTE: all possibilities:
                //    FWD - regular forward (normal) process
                //    UNDO - rollback of the transaction is underway
                //    2PC, INDOUBT, COORD and FORCE - unknown
                proxy.setTransState(new character("BEGIN"));
                // NOTE: other possibilities:
                //    ALLOCATED - entered on long lasting transaction. When entering this state
                //                the start-time field is also filled-in
                //    BEGIN - the initial state of the transaction
                // more on: https://documentation.progress.com/output/ua/OpenEdge_latest/index.html#page/dmadm%2Fpromon-record-locking-table-option.html%23wwID0EE5XW

                // assign other fields which are mandatory and default to unknown
                proxy.setTransCoord(new character("?"));
                proxy.setTransCoordTx(new integer(0));
                proxy.setTransDuration(new integer(0));
                proxy.setTransTxtime(new character("?"));
                proxy.setTransTenantId(new integer(0));
                proxy.setTransCounter(new integer(0));
                proxy.setTransMisc(new integer(0));

                // save the new record
                p.save(dmo, key);
             }

             // flush explicitly at the end of batch
             p.flush(Persistence.META_CTX);

             if (commit)
             {
                commit = false;   // to avoid double-rollback in catch block if commit fails
                p.commit(Persistence.META_CTX);
             }
          }
          catch (PersistenceException exc)
          {
             try
             {
                if (commit)
                {
                   p.rollback(Persistence.META_CTX);
                }
             }
             catch (PersistenceException pe)
             {
                // error already logged in finally block
             }
             finally
             {
                if (LOG.isLoggable(Level.SEVERE))
                {
                   LOG.log(Level.SEVERE, "Error updating " + dbName + "._Trans meta table.", exc);
                }
             }
          }
       }

       /**
        * Invocation handler which delegates calls to the {@link MinimalTrans} proxy 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 MinimalTrans} proxy by delegating them to the
           * 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>MetaTrans</code>
        * interface. This interface defines the proxy used to create and manipulate metadata
        * transaction DMO instances. The DMO's methods cannot be invoked directly from code here,
        * since the <code>MetaTrans</code> interface resides in an application-specific package,
        * which is not known at compile time.
        */
       public interface MinimalTrans
       {
          /**
           * Setter: transId
           *
           * @param   transId
           *          transId
           */
          public void setTransId(NumberType transId);

          /**
           * Setter: Trans State
           *
           * @param   transState
           *          Trans State
           */
          public void setTransState(Text transState);

          /**
           * Setter: Trans Flags
           *
           * @param   transFlags
           *          Trans Flags
           */
          public void setTransFlags(Text transFlags);

          /**
           * Setter: Trans Usernum
           *
           * @param   transUsrnum
           *          Trans Usernum
           */
          public void setTransUsrnum(NumberType transUsrnum);

          /**
           * Setter: Trans Num
           *
           * @param   transNum
           *          Trans Num
           */
          public void setTransNum(NumberType transNum);

          /**
           * Setter: Trans rl counter
           *
           * @param   transCounter
           *          Trans rl counter
           */
          public void setTransCounter(NumberType transCounter);

          /**
           * Setter: Trans Start Time
           *
           * @param   transTxtime
           *          Trans Start Time
           */
          public void setTransTxtime(Text transTxtime);

          /**
           * Setter: Coord Name
           *
           * @param   transCoord
           *          Coord Name
           */
          public void setTransCoord(Text transCoord);

          /**
           * Setter: Coordinator Transaction
           *
           * @param   transCoordTx
           *          Coordinator Transaction
           */
          public void setTransCoordTx(NumberType transCoordTx);

          /**
           * Setter: Tx Duration (in secs)
           *
           * @param   transDuration
           *          Tx Duration (in secs)
           */
          public void setTransDuration(NumberType transDuration);

          /**
           * Setter: Trans Misc
           *
           * @param   transMisc
           *          Trans Misc
           */
          public void setTransMisc(NumberType transMisc);

          /**
           * Setter: Tenant Id
           *
           * @param   transTenantId
           *          Tenant Id
           */
          public void setTransTenantId(NumberType transTenantId);
       }
    }