AbstractParameter.java

/*
** Module   : AbstractParameter.java
** Abstract : Common logic for assigns [input-]output parameter values (database fields and normal
**            variables) upon exit from procedure/function
**
** Copyright (c) 2015-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 OM  20150702 First version based on extracted code form AbstractSimpleParameter and
**                  AbstractExtentParameter for which this is the base class.
** 002 OM  20150723 IdentityHashMap replaces HashMap to keep references to simple parameters.
** 003 OM  20161011 Made getCurrentScope() static. Added Scope.isParameter().
** 004 CA  20181105 Fixed processing order of OUTPUT/INPUT-OUTPUT parameters, in cases when there
**                  are mixed extent and non-extent arguments.
** 005 CA  20190710 Made scope-related APIs public.
** 006 CA  20200506 Fixed runtime for character vs longchar OO method calls.
** 007 CA  20210223 If a dynamic array parameter is resized by the top-level block code, then the reference
**                  in the parameter instance must be replaced.
**     CA  20210609 Reworked INPUT/INPUT-OUTPUT parameters to a new approach, where they are explicitly 
**                  initialized at the method's execution, and not at the caller's arguments.
**     CA  20220901 Refactored scope notification support: ScopeableFactory was removed, and the registration 
**                  is now specific to each type of scopeable.  For each case, the block will be registered 
**                  for scope support (for that particular scopeable) only when the scopeable is 'active' 
**                  (i.e. unnamed streams or accumulators are used).  This allows a lazy registration of 
**                  scopeables, to avoid the unnecessary overhead of processing all the scopeables for each
**                  and every block.
**     CA  20220906 Parameter scope notifications is required for all OUTPUT parameters, not just fields.
**     CA  20220609 Release all the callee's resources for this parameter, after assignment has taken place.
**     CA  20220613 Do not release memptr variables, as these are passed 'by pointer'.
**     CA  20220630 Fixed a memory leak for object references.
**     SVL 20230108 Improved performance by replacing some "for-each" loops with indexed "for" loops.
**     CA  20230104 Track if an output parameter is associated with a table field, so batch processing is done
**                  only in these cases, for output parameter assignment.
**     CA  20230116 Avoid fromTypeName when the ref type is valid and known.
** 008 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 009 CA  20230724 Further reduce context-local usage.
** 010 CA  20231031 Added 'BlockDefinition' parameter to 'scopeStart'.
*/
/*
** 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.util;

import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.util.logging.*;

import java.lang.reflect.*;
import java.util.*;
import java.util.logging.*;

/**
 * Base class for all types of parameters. The hierarchy looks like this:
 * {@link AbstractParameter}
 * <ul>
 *    <li>{@link AbstractSimpleParameter}
 *       <ul>
 *          <li>{@link VariableAssigner}
 *          <li>{@link FieldAssigner}
 *       </ul>
 *    <li>{@link AbstractExtentParameter}
 *       <ul>
 *          <li>{@link OutputExtentParameter}
 *             <ul>
 *                <li>{@code ExtentExprN1} (generated-code)
 *                <li>{@link OutputExtentField}
 *             </ul>
 *          <li>{@link InputOutputExtentParameter}
 *             <ul>
 *                <li>{@code ExtentExprN2} (generated-code)
 *                <li>{@link InputOutputExtentField}
 *             </ul>
 *       </ul>
 * </ul>
 * The class does the managements of scopes of for called procedures for both simple and extent
 * parameters, using a {@link ContextLocal} {@code WorkArea}.
 */
abstract public class AbstractParameter
{
   /** Logger */
   protected static final CentralLogger LOG = CentralLogger.get(AbstractParameter.class.getName());
   
   /** Object which manages all instances of this class by context. */
   protected static final ContextLocal<WorkArea> context = new ContextLocal<WorkArea>()
   {
      /**
       * Get the weight of this context-local var.
       *
       * @return  Always {@link WeightFactor#LEVEL_5}.
       */
      @Override
      public WeightFactor getWeight()
      {
         return WeightFactor.LEVEL_5;
      }
      
      /** Retrieve the initial value of this context local variable. */
      @Override
      protected WorkArea initialValue()
      {
         return new WorkArea();
      }
   };

   /**
    * Release all the callee's resources for this parameter, after assignment has taken place.
    */
   protected abstract void release();

   /**
    * Default constructor - register the {@link WorkArea scopeable} instance as pending for the next started
    * block.
    */
   public AbstractParameter()
   {
      TransactionManager.registerPendingScopeable(getScopeable());
   }
   
   /**
    * Retrieve a {@code Scopeable} for the current context. This allows an external object to
    * notify this class' current context of scope open and close events.
    *
    * @return  Context local {@code Scopeable}.
    */
   public static Scopeable getScopeable()
   {
      return context.get();
   }
   
   /**
    * Obtain the current scope from the current context.
    * 
    * @return  current scope from the current context
    */
   public static Scope getCurrentScope()
   {
      return context.get().getCurrentScope();
   }
   
   /**
    * Post-process the OUTPUT parameter - if a {@link Scope#withConversionCtor conversion constructor} was
    * registered, then create a {@link VariableAssigner}.
    * <p>
    * Also, normalize the {@link AbstractSimpleParameter#outputValue} so that the called method will use a 
    * real BDT sub-type, and not an anonymous class.
    * 
    * @param    origAssigner
    *           The assigner created bdt the BDT anonymous class.
    * @param    var
    *           The variable to be passed as parameter.
    *           
    * @return   The normalized instance to be used as a parameter at the method.
    */
   protected static BaseDataType postProcessOutput(AbstractSimpleParameter origAssigner, BaseDataType var)
   {
      // the 'var' (which was passed by the caller as argument for this parameter) may have been overridden
      // by the runtime, for type mismatch cases (i.e. longchar argument for character parameter).
      
      // in this case, the runtime would have registered a 'withConversionCtor' entry - this wil contain the
      // original variable.  check that and:
      // - if the original variable has an assigner, call it
      // - otherwise, create a VariableAssigner and register it.

      Scope scope = getCurrentScope();
      Object[] other = scope.withConversionCtor.remove(var);
      AbstractSimpleParameter assigner = null;
      
      if (other != null)
      {
         BaseDataType key = (BaseDataType) other[0];
         Constructor<?> convCtor = (Constructor<?>) other[1];
         
         if (key.getAssigner() != null)
         {
            // initialize it
            assigner = (AbstractSimpleParameter) key.getAssigner().get();
         }
         else
         {
            assigner = new VariableAssigner(var, key);
         }
         
         assigner.convCtor = convCtor;
         assigner.outputValue = var;
      }
      
      if (origAssigner.outputValue.getClass().isAnonymousClass())
      {
         // do not leave behind an anonymous class - we need a real BDT implementation of a compatible legacy 
         // type, and not an anonymous class - otherwise, any usage of 'getClass()' (for map lookup, c'tors, 
         // etc) will fail

         if (origAssigner.outputValue instanceof ObjectVar)
         {
            ObjectVar newOutputValue = new ObjectVar((ObjectVar) origAssigner.outputValue);
            
            // release the reference
            origAssigner.release();
            
            origAssigner.outputValue = newOutputValue;
         }
         else
         {
            BaseDataType newOutputValue = origAssigner.outputValue.instantiateUnknown();
            newOutputValue.assign(origAssigner.outputValue);

            // release the reference
            origAssigner.release();

            origAssigner.outputValue = newOutputValue;
         }
      }
      
      return origAssigner.outputValue;
   }
   
   /**
    * Check if the {@link BaseDataType} passed as parameter is used for lookup of an 
    * {@code OUTPUT} parameter.
    *
    * @param   key
    *          The variable for lookup in the current scope.
    *
    * @return  {@code true} if the content of {@code key} will be assigned back on return with
    *          success.
    */
   protected static boolean isParameter(BaseDataType key)
   {
      return getCurrentScope().isParameter(key);
   }
   
   /**
    * Check if the given array reference was passed as a parameter, by looking into all scopes.
    * 
    * @param     key
    *            The array reference.
    *            
    * @return    See above.
    */
   protected static boolean isExtentParameter(BaseDataType[] key)
   {
      WorkArea wa = context.get();
      for (Scope scope : wa.scopes)
      {
         if (scope.isExtentParameter(key))
         {
            return true;
         }
      }
      
      return false;
   }
   
   /**
    * Replace the extent array parameter reference (not the original argument) with the new one.  This is
    * required in cases when the parameter is resized by the top-level block.
    * 
    * @param    old
    *           The previous array parameter reference.
    * @param    other
    *           The new reference.
    */
   protected static void replaceParameter(BaseDataType[] old, BaseDataType[] other)
   {
      for (Scope scope : context.get().scopes)
      {
         scope.replaceParameter(old, other);
      }
   }

   /**
    * Get the variable container used as parameter inside the procedure body. It makes sense only
    * for simple parameters.
    *
    * @return  {@code null}. Implementing classes will return the proper field. 
    */
   protected BaseDataType getLocalParam()
   {
      return null;
   }
   
   /**
    * Check if the parameter can release its resources.
    * 
    * @param    bdt
    *           The parameter instance.
    *
    * @return   <code>true</code> if the parameter can be released.
    */
   protected boolean canRelease(BaseDataType bdt)
   {
      return !(bdt instanceof memptr);
   }
   
   /**
    * Check if this parameter is associated with a table field, so a batch assign is required on output 
    * parameter processing.
    * 
    * @return   Always <code>false</code>.
    */
   protected boolean isTableField()
   {
      return false;
   }
   
   /**
    * Object which manages the context local state of field assigners. It manages a deque of
    * scopes, each of which contains zero or more {@code OutputParameterAssigner} objects. As the
    * currently executing program's block scopes open and close, these scopes are pushed and
    * popped from the deque.
    */
   protected static class WorkArea
   implements Scopeable
   {
      /** Deque containing field assigners by scope */
      private final Deque<Scope> scopes = new ArrayDeque<>();
      
      /**
       * Default constructor.
       */
      private WorkArea()
      {
         scopes.push(new Scope());
      }
      
      /**
       * Called as each block scope opens, at which time we push a scope onto the deque.
       *
       * @param    block
       *           The explicit block definition which required this notification.
       * 
       * @see com.goldencode.p2j.util.Scopeable#scopeStart
       */
      @Override
      public void scopeStart(BlockDefinition block)
      {
         scopes.push(new Scope());
      }
      
      /**
       * Called as each block scope closes, at which time we pop a scope from the deque.
       *
       * @see com.goldencode.p2j.util.Scopeable#scopeFinished()
       */
      public void scopeFinished()
      {
         // pop the param list for the inner scope
         scopes.pop();
      }
      
      /** No-op. */
      @Override
      public void scopeDeleted()
      {
         // no-op
      }
      
      /**
       * Get the {@link ScopeId} for the instance.
       * 
       * @return   {@link ScopeId#ABSTRACT_PARAMETER}.
       */
      @Override
      public ScopeId getScopeId()
      {
         return ScopeId.ABSTRACT_PARAMETER;
      }
      
      /**
       * Get the object which manages field assigners for the current scope.
       *
       * @return  Current scope.
       */
      protected Scope getCurrentScope()
      {
         return scopes.peek();
      }
   }
   
   /**
    * One instance of this class is created for each block scope which the currently executing
    * program enters.  It contains zero or more field assigners specific to that scope.
    * <p>
    * After exiting a function or procedure, the 
    * {@link OutputParameterAssigner#processAssignments(String, boolean)} method is invoked, which
    * causes each field assigner to assign the current value of its parameter value back to the
    * associated DMO property.
    */
   public static class Scope
   implements com.goldencode.p2j.util.OutputParameterAssigner
   {
      /**
       * Map of output simple parameter assigners for this scope, created lazily. The wrapper
       * outputValue variables are used as keys for quick access.
       */
      private Map<BaseDataType, AbstractSimpleParameter> simpleParameters = null;
      
      /**
       * The list with the wrapped extent parameters, created lazily.
       */
      private List<AbstractExtentParameter> extentParameters = null;
      
      /** All parameter references, in their defining order. */
      private ArrayList<AbstractParameter> allParameters = new ArrayList<>();
      
      /** 
       * Additional state computed by {@link ControlFlowOps} parameter validation, when the argument and
       * parameter type does not match.  This will be used when the {@link AbstractParameter#postProcessOutput}
       * is executed for an OUTPUT parameter.
       * <p>
       * WARNING: although this will always register via {@link #updateSimpleParameter} in the current scope,
       * it will be 'consumed' before the callee's scope opens, when the OUTPUT parameter is initialized.
       */
      private Map<BaseDataType, Object[]> withConversionCtor = new IdentityHashMap<>();

      /** Flag indicating if any parameter is associated with a table field. */
      private boolean hasTableFields = false;
      
      /**
       * Some error occurred. Discard any field assigners.
       */
      @Override
      public void abort()
      {
         clear();
      }
      
      /**
       * Cause each (if any) field assigner in the current scope to assign the current value of
       * its parameter value back to the DMO property with which it is associated.
       *
       * @param   endingRoutine
       *          The legacy name of the routine that is ending when these back assignments are
       *          performed. 
       * @param   function
       *          {@code true} if the called routine is a function.
       */
      @Override
      public void processAssignments(String endingRoutine, boolean function)
      {
         // all parameters need to be processed in order - if one fails, the remaining parameters 
         // will not be processed.
         if (allParameters == null || allParameters.isEmpty())
         {
            return;
         }
         
         boolean useBatch = hasTableFields;
         boolean batchError = true;
         BufferManager bufMan = null;
         if (useBatch)
         {
            bufMan = BufferManager.get();
            RecordBuffer.startBatch(bufMan, true);
         }
         
         try
         {
            for (int i = 0; i < allParameters.size(); i++)
            {
               AbstractParameter p = allParameters.get(i);
               try
               {
                  if (p instanceof AbstractExtentParameter)
                  {
                     ((AbstractExtentParameter) p).assign(endingRoutine, function);
                  }
                  else if (p instanceof AbstractSimpleParameter)
                  {
                     ((AbstractSimpleParameter) p).assign();
                  }
                  else
                  {
                     throw new IllegalStateException("Could not assign parameter " + p.getClass());
                  }
               }
               finally
               {
                  p.release();
               }
            }
            
            batchError = false;
         }
         finally
         {
            clear();
            if (useBatch)
            {
               RecordBuffer.endBatch(bufMan, batchError);
            }
         }
      }
      
      /**
       * Add a parameter assigner to this scope.
       *
       * @param   assigner
       *          Simple field assigner.
       */
      public void addSimpleParameter(AbstractSimpleParameter assigner)
      {
         if (simpleParameters == null)
         {
            simpleParameters = new IdentityHashMap<>(3);
            
            if (extentParameters == null)
            {
               // register only once, if extentParameters is not null, then we already registered
               TransactionManager.registerOutputParameterAssigner(this);
            }
         }
         
         hasTableFields = hasTableFields || assigner.isTableField();
         if (allParameters == null)
         {
            allParameters = new ArrayList<>();
         }
         allParameters.add(assigner);
         
         // use the procedure-local parameter as key for finding and assigning back the value at
         // the end of the procedure to initial Field
         AbstractSimpleParameter old = simpleParameters.put(assigner.getLocalParam(), assigner);
         if (old != null && old != assigner)
         {
            if (LOG.isLoggable(Level.WARNING))
            {
               LOG.log(Level.WARNING,
                     "Two AbstractOutputParameterAssigner configured with same wrapper variable");
            }
         }
      }
      
      /**
       * Check if the {@link BaseDataType} passed as parameter is used for lookup of an 
       * {@code OUTPUT} parameter.
       * 
       * @param   key
       *          The variable for lookup in the current scope.
       * 
       * @return  {@code true} if the content of {@code key} will be assigned back on return with
       *          success.
       */
      protected boolean isParameter(BaseDataType key)
      {
         return (simpleParameters != null) && simpleParameters.containsKey(key);
      }
      
      /**
       * Check if the given array reference was passed as a parameter, by looking into all scopes.
       * 
       * @param     key
       *            The array reference.
       *            
       * @return    See above.
       */
      protected boolean isExtentParameter(BaseDataType[] key)
      {
         if (extentParameters == null)
         {
            return false;
         }
         
         for (AbstractExtentParameter param : extentParameters)
         {
            if (param.getParameter() == key)
            {
               return true;
            }
         }
         
         return false;
      }
      
      /**
       * Replace the extent array parameter reference (not the original argument) with the new one.  This is
       * required in cases when the parameter is resized by the top-level block.
       * 
       * @param    old
       *           The previous array parameter reference.
       * @param    other
       *           The new reference.
       */
      protected void replaceParameter(BaseDataType[] old, BaseDataType[] other)
      {
         if (extentParameters == null)
         {
            return;
         }
         
         for (AbstractExtentParameter param : extentParameters)
         {
            if (param.getParameter() == old)
            {
               param.setParameter(other);
            }
         }
      }
      
      /**
       * Update the wrapper variable for a {@code AbstractOutputParameterAssigner}.
       *
       * @param   key
       *          The old key variable. May be the intermediary wrapper for a field or the real
       *          variable passed as *OUTPUT parameter.
       * @param   newVar
       *          The new wrapper variable that will be used inside the calling procedure. At the
       *          end of the call its value will be converted using {@code convCtor} and assigned
       *          to wrapped field or original variable.
       * @param   convCtor
       *          The conversion constructor that will cast the new variable to old variable type
       *          in order to be assigned to field.
       */
      protected void updateSimpleParameter(BaseDataType key,
                                           BaseDataType newVar,
                                           Constructor<?> convCtor)
      {
         // save the details for later
         withConversionCtor.put(newVar, new Object[] { key, convCtor });
      }
      
      /**
       * Registers an extent parameter wrapper class to be included in the scope processing.
       *
       * @param   param
       *          Extent parameter wrapper class.
       */
      public void addExtentParameter(AbstractExtentParameter<?> param)
      {
         if (extentParameters == null)
         {
            extentParameters = new LinkedList<>();
            
            if (simpleParameters == null)
            {
               // register only once, if simpleParameters is not null, then we already registered
               TransactionManager.registerOutputParameterAssigner(this);
            }
         }
         
         extentParameters.add(param);
         if (allParameters == null)
         {
            allParameters = new ArrayList<>();
         }
         allParameters.add(param);
         hasTableFields = hasTableFields || param.isTableField();
      }
      
      /**
       * Clears internal data. All information about simple and extent parameters is removed.
       */
      protected void clear()
      {
         // drop all parameters
         hasTableFields = false;
         simpleParameters = null;
         extentParameters = null;
         if (allParameters != null)
         {
            allParameters.clear();
         }
      }
   }
}