DataSetManager.java

/*
** Module   : DataSetManager.java
** Abstract : Manages all the DataSets, taking care for matching shared ones. 
**
** Copyright (c) 2019-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ----------------------------------------Description---------------------------------------
** 001 OM  20190403 Created initial version.
** 002 OM  20190624 Added runtime implementation.
** 003 CA  20190722 Allow lookup for handles registered as OUTPUT arguments.
**     OM  20190731 Added REFERENCE-ONLY support.
** 004 CA  20200110 More fixes for DATASET parameters and resource delete.
** 005 CA  20201003 Allow dictionaries with an identity map.
** 006 CA  20220524 The scope ends when the procedure returns, not on 'scopeFinished', to allow parameter
**                  processing.
**     CA  20220613 The static DATASET must be registered as 'pending' to be properly registered with the next
**                  external program call.
**     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.
** 007 AL2 20230613 Use procedure helper where possible. Allow faster gateways to the transaction manager.
** 008 CA  20231031 Added 'BlockDefinition' parameter to 'scopeStart'.
** 009 CA  20240404 Static temp-table or dataset resolution by name must be done using the currently executing
**                  type and the static temp-table/dataset's defining type.
*/

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

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

import java.util.*;

/**
 * A manager for {@code DataSet}-s:
 * <ul>
 *    <li>registers new {@code DataSet}-s and matches shared ones;
 *    <li>keeps the head and tail of the list of {@code DataSet}-s.
 * </ul>
 */
public class DataSetManager
implements Scopeable
{
   /**
    * The context-local manager.
    */
   private final static ContextLocal<DataSetManager> instance = new ContextLocal<DataSetManager>()
   {
      /**
       * This method will be invoked to retrieve the initial value of this context local variable,
       * the first time {@link #get} is invoked (but only if {@link #set} was not previously
       * invoked. The returned value will be stored as the variable's initial value.
       * <p>
       *
       * @return A new instance of {@code DataSetManager}. 
       */
      @Override
      protected DataSetManager initialValue()
      {
         return new DataSetManager();
      }
   };
   
   /** The pending datasets defined in the next external program being invoked. */
   private Set<StaticDataSet> pendingRegistry = new HashSet<>();
   
   /**
    * Scoped dictionary which contains the maps of output copiers for a procedure/function to
    * their 0-based indexes in the set of all DATASET-HANDLE parameters for the procedure/function
    * (excluding other types of parameters).
    */
   private final ScopedDictionary<OutputDataSetHandleCopier, Integer> dshOutputParams =
         new ScopedDictionary<>();
   
   /** A dictionary of BY-REFERENCE dataset parameters. */
   private final ScopedDictionary<DataSet, Integer> byReferenceParameters = new ScopedDictionary<>();

   /** The transaction helper. */
   private final TransactionManager.TransactionHelper tm;
   
   /** The procedure helper. */
   private final ProcedureManager.ProcedureHelper pm;
   
   /** The buffer helper. */
   private final BufferManager bm;
   
   /**
    * Default c'tor.  Marks dictionaries which can use identity maps.
    */
   private DataSetManager()
   {
      tm = TransactionManager.getTransactionHelper();
      pm = ProcedureManager.getProcedureHelper();
      bm = BufferManager.get();
      dshOutputParams.setIdentityKeys(true);
      byReferenceParameters.setIdentityKeys(true);
   }
   
   /**
    * Factory method. Corresponds to {@code DEFINE DATASET} statement. Creates a new builder for a
    * new {@link DataSet} and returns it for further setting up of options.
    *
    * @param   legacyName
    *          The legacy name of the {@code DataSet}. Used for lookup.
    * @param   isShared
    *          This is a shared {@code DataSet}. 
    * @param   isNew
    *          This is a new {@code DataSet}. Only in combination with {@code isShared}.
    */
   public static DataSet.Builder define(String legacyName, boolean isShared, boolean isNew)
   {
      return new DataSet.Builder(legacyName, isShared, isNew);
   }
   
   /**
    * Obtain the instance of {@code DataSetManager} (associated with this work area ?).
    * 
    * @return  the instance of {@code DataSetManager}.
    */
   public static DataSetManager instance()
   {
      return instance.get();
   }
   
   /**
    * Register the {@link DataSetManager} for scope notifications at the current block.
    */
   public static void registerScopeable()
   {
      instance()._registerScopeable();
   }
   
   /**
    * Registers a ((new) shared) dataset.
    *
    * @param   dataSet
    *          The new {@code DataSet} to be registered.
    * @param   legacyName
    *          The legacy name of the {@code DataSet}.
    * @param   isShared
    *          {@code true} if this is a SHARED DATASET.
    * @param   isNew
    *          {@code true} if this is a NEW SHARED DATASET.
    */
   public void register(DataSet dataSet, String legacyName, boolean isShared, boolean isNew)
   {
      tm.registerPendingScopeable(this);
      
      if (!dataSet._dynamic())
      {
         pendingRegistry.add((StaticDataSet) dataSet);
      }
   }
   
   /**
    * Register the {@link DataSetManager} for scope notifications at the current block.
    * This is a convenient method when the manager is already retrieved, so that there is
    * no need for a further context look-up.
    */
   public void _registerScopeable()
   {
      tm.registerBlockScopeable(this);
      
      // if there is dataset usage, then buffers are needed, too
      BufferManager.registerScopeable(bm);
   }
   
   /**
    * Convenient gateway to the transaction manager. This is used to avoid an additional
    * context look-up if the caller already has access to this manager. This is not a
    * good pattern to follow (add a method in data set manager to gateway each method
    * in transaction manager). However, this is the only case we want to have such gateway.
    * 
    * @param   dsHandle
    *          the target to be registered with the transaction manager
    */
   public void registerCurrent(handle dsHandle)
   {
      tm.registerCurrent(dsHandle);
   }
   
   /**
    * Looks up for a {@code DataSet} by its name. Only STATIC dataset are managed.
    * 
    * @param   dsName
    *          The name to search.
    *
    * @return  The {@code DataSet} statically named {@code sdName} or {@code null} is such
    *          {@code DataSet} is not accessible.
    */
   public DataSet byName(String dsName)
   {
      Object ref = pm._thisProcedure();
      Class<?> type = pm.getExecuting();

      return bm.getStaticDataSet(type, dsName, ref);
   }
   
   /**
    * Provides a notification that a new scope is about to be entered.
    * 
    * @param    block
    *           The explicit block definition which required this notification.
    */
   @Override
   public void scopeStart(BlockDefinition block)
   {
      if (block.topLevel)
      {
         dshOutputParams.addScope(null);
         byReferenceParameters.addScope(null);
         
         if (!pendingRegistry.isEmpty())
         {
            for (StaticDataSet dataset : pendingRegistry)
            {
               String name = dataset.getDefinitionName();
               Object ref = pm._thisProcedure();
               Class<?> type = dataset.getDefiningType();
               bm.registerDataSet(type, name, ref, dataset);
            }
            pendingRegistry.clear();
         }
         
         pm.executeOnReturn(() -> 
         {
            dshOutputParams.deleteScope();
            byReferenceParameters.deleteScope();
         }, WeightFactor.LAST);
      }
   }
   
   /**
    * Provides a notification that a scope is about to be exited.
    */
   @Override
   public void scopeFinished()
   {
   }
   
   /**
    * Provides a notification that an external scope is about to be deleted.
    */
   @Override
   public void scopeDeleted()
   {
      // TODO: implement~me
   }
   
   /**
    * Get the {@link ScopeId} for the instance.
    * 
    * @return   {@link ScopeId#DATA_SET_MANAGER}.
    */
   @Override
   public ScopeId getScopeId()
   {
      return ScopeId.DATA_SET_MANAGER;
   }
   
   /**
    * Check if the given dataset is passed as a BY-REFERENCE parameter.
    * 
    * @param    ds
    *           The dataset to check.
    *           
    * @return   <code>true</code> if the dataset exists in the {@link #byReferenceParameters}
    *           dictionary.
    */
   public boolean isByReferenceParameter(DataSet ds)
   {
      for (int i = 0; i < byReferenceParameters.size(); i++)
      {
         Map<DataSet, Integer> map = byReferenceParameters.getDictionaryAtScope(i, false);
         if (map == null)
         {
            continue;
         }
         
         if (map.containsKey(ds))
         {
            return true;
         }
      }
      
      return false;
   }
   
   /**
    * Register the given dataset as BY-REFERENCE, in the {@link #byReferenceParameters} dictionary.
    * 
    * @param    ds
    *           The dataset.
    */
   public void addByReferenceParameter(DataSet ds)
   {
      Map<DataSet, Integer> map = byReferenceParameters.getDictionaryAtScope(0, true);
      byReferenceParameters.addEntry(false, ds, map.size());
   }
   
   /**
    * Deregister the given dataset as BY-REFERENCE, from the {@link #byReferenceParameters} 
    * dictionary.
    * 
    * @param    ds
    *           The dataset.
    */
   public void removeByReferenceParameter(DataSet ds)
   {
      byReferenceParameters.removeEntryAtScope(ds, 0);
   }
   
   /**
    * Check if the specified dataset instance is registered as an OUTPUT argument.
    * 
    * @param    ds
    *           The dataset instance.
    *           
    * @return   <code>true</code> if the handle originates from the caller as an OUTPUT argument.
    */
   public boolean isOutputDataSetHandle(DataSet ds)
   {
      for (int i = 0; i < dshOutputParams.size(); i++)
      {
         Map<OutputDataSetHandleCopier, Integer> map = dshOutputParams.getDictionaryAtScope(i, false);
         if (map == null)
         {
            continue;
         }
         
         for (OutputDataSetHandleCopier out : map.keySet())
         {
            if (out.getCalled().get() == ds)
            {
               return true;
            }
         }
      }
      
      return false;
   }
   
   /**
    * Create and register the output table copier.
    *
    * @param   dsParam
    *          The source dataset parameter.
    * @param   dsHandle
    *          The target dataset handle..
    */
   public void registerOutputDataSetHandleCopier(DataSetParameter dsParam,
                                                 handle           dsHandle)
   {
      OutputDataSetHandleCopier copier = new OutputDataSetHandleCopier(dsParam, dsHandle);
      pm.executeOnReturn(copier::finished, copier.weight());
      
      Map<OutputDataSetHandleCopier, Integer> map = dshOutputParams.getDictionaryAtScope(0, true);
      dshOutputParams.addEntry(false, copier, map.size());
      copier.setSiblingsMap(map);
   }
   
   /**
    * Unregister the output dataset copier <b>in the current scope</b>.
    *
    * @param   copier
    *          Copier to unregister.
    */
   public void unregisterOutputDataSetHandleCopier(OutputDataSetHandleCopier copier)
   {
      dshOutputParams.removeEntryAtScope(copier, 0);
   }
}