StaticDataSet.java
/*
** Module : StaticDataSet.java
** Abstract : Abstracts the statically defined DATASET ABL structure.
**
** Copyright (c) 2019-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM 20190729 Created initial version.
** EVL 20190801 Commented out the message ErrorManager.recordOrShowError().
** CA 20190812 Improvements/changes to make the dataset mutable.
** 002 CA 20200921 Do not fail BIND if the previous 'bound' reference is invalid.
** CA 20200924 Replaced Method.invoke with ReflectASM.
** CA 20201003 Use an identity HashSet where possible.
** OM 20211006 Unbinding occurs on procedure return instead of TM's scope finished.
** OM 20211011 Improved debugging support for proxy objects.
** CA 20220707 Used a static 'doNotProxy' field, instead of creating the array each time.
** OM 20221206 Reworked REFERENCE-ONLY attribute.
** GES 20210501 Upgraded table-handle parameter processing to match API changes.
** CA 20230110 Cache the helper for ProcedureManager, ObjectOps and others (as needed), to reduce
** context-local lookup.
** 003 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 004 AL2 20230613 Use procedure helper instead of doing a context look-up.
** 005 CA 20240328 Save and expose the proxy for a static dataset.
** 006 CA 20240404 Static dataset resolution by name must be done using the currently executing type and
** the static dataset's defining type.
** 007 AS 20240911 Moved triggerErrorInCaller() to DataSet.isCompatibleWith().
*/
/*
** 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 com.goldencode.p2j.util.logging.*;
import com.goldencode.proxy.*;
import java.lang.reflect.*;
import java.util.*;
/**
* This class handles the statically defined {@code DataSet} objects, both having or not the
* {@code REFERENCE-ONLY} attribute. These proxy object that will redirect all calls to members
* hinted with as {@code LegacyAttribute} or {@code LegacyMethod} to a handle which will check
* whether the {@code DataSetProxy} is bound to some other {@code DatSet}. In this case the
* attribute/method of the bound is called. Otherwise, depending on whether this is a
* {@code REFERENCE-ONLY} object an error will be thrown, or the original {@code DataSet} method/
* attribute is called.
*/
public class StaticDataSet
extends DataSet
{
/** Logger. */
private static final CentralLogger LOG = CentralLogger.get(StaticDataSet.class);
/** Cache all interfaces implemented by FWD {@link DataSet} resource. */
private final static Class<?>[] DATASET_INTERFACES;
static
{
Set<Class<?>> ifcs = Collections.newSetFromMap(new IdentityHashMap<>());
Utils.collectInterfaces(DataSet.class, ifcs);
DATASET_INTERFACES = ifcs.toArray(new Class<?>[0]);
}
/** Array of methods not to be proxied */
private static final Method[] doNotProxy = StaticDataSet.class.getDeclaredMethods();
/**
* The bounded {@code DataSet}, if any. When {@code null} all member attribute and methods
* are invalid.
*/
private DataSet bound = null;
/** The original definition of the {@code DataSet}. */
private DataSet dsDef = null;
/** The defining type where this dataset is constructed. */
private Class<?> definingType = null;
/**
* The constructor should be private. Create instances using {@code create()} method.
* <p>
* Note: the cause because it is not private is because it is needed by the proxy factory.
*/
public StaticDataSet()
{
super();
}
/**
* Factory method. Creates a new proxy object and configures it according to parameters.
*
* @param target
* The original {@code DataSet}. Used to check compatibility with future binding {@code DataSet}s.
*
* @return The proxy object as defined in class definition, configured according to current
* parameters.
*/
public static StaticDataSet create(DataSet target)
{
Handler handler = new Handler();
StaticDataSet proxy = ProxyFactory.getProxy(StaticDataSet.class,
true,
DATASET_INTERFACES,
false,
doNotProxy,
null,
handler);
proxy.dsDef = target;
proxy.referenceOnly = target.referenceOnly;
proxy.bound = proxy.referenceOnly ? null : target;
target.setProxyDataSet(proxy);
// note: proxy.buffers is not initialized
return proxy;
}
/**
* Get the dataset definition name.
*
* @return The definition name from {@link #dsDef}.
*/
public String getDefinitionName()
{
return dsDef._name();
}
/**
* This resource is never dynamic.
*
* @return Always false.
*/
@Override
public boolean _dynamic()
{
return false;
}
/**
* Convert this dataset to a handle.
* <p>
* This is not part of an interface so it must be handled manually.
*
* @return See above.
*/
@Override
public handle asHandle()
{
if (bound == null)
{
Handler.throwErrors("HANDLE");
return new handle();
}
return bound.asHandle();
}
/**
* Dereference a named member from this collection. If this collection is a ProDataSet then
* the returned value is a {@link handle} to a named buffer, if the collection is a buffer
* the returned value is the field value with the respective name. In the case that this
* collection does not have a named member with the name specified {@code memberName}
* the unknown value is returned.
* <p>
* This interface member does not have a Legacy annotation so it must be handled manually.
*
* @param memberName
* The name of the member to be extracted.
*
* @return The value of the member or unknown (?) if there is no such member.
*/
@Override
public BaseDataType dereference(String memberName)
{
if (bound != null)
{
return bound.dereference(memberName);
}
ErrorManager.recordOrThrowError(12787);
// Attempt to reference uninitialized dataset.
return new unknown();
}
/**
* Dereference a named member from this collection. If this collection is a ProDataSet then
* the returned value is a {@link handle} to a named buffer, if the collection is a buffer
* the returned value is the field value with the respective name. In the case that this
* collection does not have a named member with the name specified {@code memberName}
* the unknown value is returned.
* <p>
* This interface member does not have a Legacy annotation so it must be handled manually.
*
* @param type
* The expected type of the returned value.
* @param memberName
* The name of the member to be extracted.
*
* @return The value of the member or unknown (?) if there is no such member.
*/
@Override
public <T extends BaseDataType> T dereference(Class<T> type, String memberName)
{
if (bound != null)
{
return bound.dereference(type, memberName);
}
ErrorManager.recordOrThrowError(12787);
// Attempt to reference uninitialized dataset.
return (T) BaseDataType.generateUnknown(type);
}
/**
* This overloaded method is used when the dereference is the left-side of an assignment.
* The implementation will extract the member from the collection and assign it the new value.
* <p>
* This interface member does not have a Legacy annotation, so it must be handled manually.
*
* @param memberName
* The name of the member to be extracted.
* @param value
* The new value that will be assigned to the extracted field.
*/
@Override
public void dereference(String memberName, Object value)
{
if (bound != null)
{
bound.dereference(memberName, value);
}
ErrorManager.recordOrThrowError(12787);
// Attempt to reference uninitialized dataset.
}
/**
* Reports if this object is valid for use. The {@code DataSet} is valid
*
* @return {@code true} if we are valid (can be used).
*/
@Override
public boolean valid()
{
return bound != null && bound.valid();
}
/**
* Obtains the original {@code DataSet} definition. This is for internal usage, in cases when this
* information is needed and the proxy interface will mask and make the information unavailable.
*
* @return the original {@code DataSet} definition.
*/
@Override
public DataSet ref()
{
return bound == null ? dsDef : bound;
}
/**
* Check if this resource must be processed via {@link ProcedureManager#processResource}, once
* it has been instantiated.
*
* @return Always <code>false</code>.
*/
@Override
protected boolean processResource()
{
return false;
}
/**
* Checks if this {@code REFERENCE-ONLY} {@code DataSet} was bound to a real {@code DataSet}.
*
* @return {@code true} if a real {@code DataSet} was bound to this static object.
*/
protected boolean isBound()
{
return bound != null;
}
/**
* Get the dataset defining type.
*
* @return The {@link #definingType}.
*/
Class<?> getDefiningType()
{
return this.definingType;
}
/**
* Set the defining type where this dataset is constructed.
*
* @param type
* The defining type.
*/
void setDefiningType(Class<?> type)
{
this.definingType = type;
}
/**
* Binds a real {@code DataSet} to the static {@code targetDS}. The compatibility of the parameter DataSet
* and the original defined DataSet (static define) is checked. An error is thrown in calling
* procedure if a compatibility issue is detected.
*
* @param targetDS
* The new real {@code DataSet} to bind to.
* @param input
* If {@code true}, records are copied from source tables to destination tables
* immediately upon invocation of this method, otherwise they are not.
* @param output
* When {@code true}, the records are copied from destination table back to source
* table when the current transaction or sub-transaction is committed; otherwise they
* are not.
* @param permanent
* If {@code true} the bind is permanent (due a call with BIND passing mode) and the
* link is kept even after the current method ends.
* Otherwise, the bind is temporary meaning the association is kept only during the
* execution of this method/procedure (BY-REFERENCE).
*/
boolean bind(DataSet targetDS, boolean input, boolean output, boolean permanent, boolean thisIsParam)
{
// bound is always dsDef, if in "I'm not currently bound to anything" mode
if (targetDS != bound && bound != null && bound != dsDef && bound.valid())
{
String pname = pm.getStackTrace().get(0);
String paramName = dsDef.name().toStringMessage();
// The use of ErrorManager.recordOrThrowError() is not very effective here because in FWD
// the bind is done after the called procedure has already started. The ErrorManager will
// stop only the execution of the called procedure/method. In 4GL the associate / bind
// operation is performed in caller and, in case of errors the caller is stopped before
// the invocation of the called method
TransactionManager.triggerErrorInCaller(
13011, ErrorManager.replaceTokens(13011, pname, paramName)
);
return false;
}
if (!this.dsDef.ref().isCompatibleWith(targetDS.ref()))
{
return false;
}
if (bound != null && bound != this)
{
bound.removeReference(this);
}
ParameterOption option = permanent ? ParameterOption.BIND : ParameterOption.BY_REFERENCE;
for (int i = 0; i < dsDef.getBuffers().size(); i++)
{
Temporary defBuf = (Temporary) this.dsDef.getBuffers().get(i);
Temporary targetBuf = (Temporary) targetDS.getBuffers().get(i);
if (thisIsParam)
{
TemporaryBuffer.associate(new TableParameter(defBuf, option, input, output), targetBuf, option);
}
else
{
TemporaryBuffer.associate(new TableParameter(targetBuf, option, input, output), defBuf, option);
}
}
DataSet oldBound = bound;
bound = targetDS;
targetDS.addReference(this);
if (!permanent)
{
// TODO: can a future BIND call veto any unbind from an existing call on the stack?
pm.executeOnReturn(() -> {
// unbind the {@code DataSet} at the end of the current call
StaticDataSet.this.unbind(targetDS);
if (oldBound != null)
{
bound = oldBound;
bound.addReference(StaticDataSet.this);
}
}, WeightFactor.LAST);
}
return true;
}
/**
* Unbind a dataset from this object.
*
* @param dataset
* The {@code DataSet} to unbind.
*/
void unbind(DataSet dataset)
{
if (bound == null || bound == this || bound != dataset)
{
LOG.warning("Invalid state: StaticDataSet is not bound or bound to another DataSet!");
return;
}
dataset.removeReference(this);
bound = referenceOnly ? null : dsDef;
// buffers are unbounding themselves
// NOTE: the following messages are printed apparently without any reason whatsoever. We
// keep the following lines here until we understand why these messages are printed.
// CA: there is a lot of noise in the log file caused by this message, which doesn't seem
// to be useful at all; so I've disabled it for now
/*
int bufCount = dataset.numBuffers().intValue();
for (int k = 1; k <= bufCount; k++)
{
BufferImpl buf = (BufferImpl) dataset.bufferHandle(k).getResource();
ErrorManager.recordOrShowError(12764, buf.doGetName());
// REFERENCE-ONLY temp-table is unexpectedly UNKNOWN for <name>.
}
*/
}
/**
* The proxy handle object. Will redirect all incoming invoke requests to the {@code bound}
* {@code DataSet} or will throw errors.
*/
static class Handler
implements InvocationHandler
{
/**
* Processes a method invocation on a proxy instance and returns the result. This method
* will be invoked on an invocation handler when a method is invoked on a proxy instance
* that it is associated with.
*
* @param proxy
* the proxy instance that the method was invoked on
* @param method
* the {@code Method} instance corresponding to the interface method invoked on the
* proxy instance. The declaring class of the {@code Method} object will be the
* interface that the method was declared in, which may be a superinterface of the
* proxy interface that the proxy class inherits the method through.
* @param args
* an array of objects containing the values of the arguments passed in the method
* invocation on the proxy instance, or {@code null} if interface method takes no
* arguments.
*
* @return the value to return from the method invocation on the proxy instance.
*
* @throws Throwable
* the exception to throw from the method invocation on the proxy instance.
*
* @see UndeclaredThrowableException
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
StaticDataSet dsp = (StaticDataSet) proxy;
if (dsp.bound != null)
{
return Utils.invoke(method, dsp.bound, args);
}
String[] errParam = new String[1];
LegacyAttribute legacyAttr = method.getAnnotation(LegacyAttribute.class);
if (legacyAttr != null)
{
errParam[0] = legacyAttr.name();
}
else
{
LegacyMethod legacyMeth = method.getAnnotation(LegacyMethod.class);
if (legacyMeth != null)
{
errParam[0] = legacyMeth.name();
}
else if (method.toString().equals(
"public java.lang.String com.goldencode.p2j.persist.DataSet.toString()"))
{
// this is only for debugging, no actual business code should reach this point
return toString();
}
else
{
errParam[0] = "<UNKNOWN>";
LOG.finer("DataSetProxy.invoke(" + method + ")");
// return method.invoke(dsp, args);
}
}
Handler.throwErrors(errParam);
if (BaseDataType.class.isAssignableFrom(method.getReturnType()))
{
return BaseDataType.generateUnknown(method.getReturnType());
}
else
{
return null;
}
}
/**
* Throws specific errors for a non-bound {@code DataSet} (12787 and 3140).
*
* @param errParam
* The list of positional parameters for 3140 error.
*/
private static void throwErrors(String... errParam)
{
ErrorManager.recordOrShowError(12787, (String[]) null);
// Attempt to reference uninitialized dataset.
ErrorManager.recordOrThrowError(3140, errParam);
// Cannot access the <name> attribute because the widget does not exist
}
}
}