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