TempTableSchema.java
/*
** Module : TempTableSchema.java
** Abstract : Temp-table schema used for serialization, based on internal structure of the table.
**
** Copyright (c) 2017-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20171020 Created initial version.
** 002 CA 20180511 Added XML-NODE-NAME table option support.
** 003 ECF 20190120 Refactored out XML-specific features into separate XmlTempTableSchema class.
** 004 OM 20190711 Added support for BEFORE-TABLEs and default field values.
** 005 OM 20190823 Added help and format field option.
** CA 20190826 Column lookup must be case-insensitive (as 4GL is case-insensitive).
** 006 ECF 20200906 New ORM implementation.
** 007 OM 20201029 Added case sensitive column support. Added detection for changed fields.
** OM 20210127 Replaced TableMapper.getLegacyName() triple map lookup with direct access to local
** dmoMeta.legacyTable.
** OM 20210309 Do not use DmoMeta as key in TableMapper because temp-tables may share the same
** DmoMeta instance.
** OM 20210329 Added Column API to allow extraction of legacy-name, xml-node-name and serialize-name
** independently from the precomputed name.
** CA 20210510 Fixed JSON and XML serialization when XML-NODE-NAME/SERIALIZE-NAME field options are set.
** OM 20211102 WRITE-XML was not using XML-NODE-NAME for the temp-table.
** OM 20220120 Added getters for LABEL and COLUMN-LABEL attributes.
** OM 20220212 Use ReservedProperty static method for testing reserved properties names.
** CA 20221006 Refactored TableMapper for temp-tables to keep a cache of LegacyTableInfo and the mutable
** state in MutableFieldInfo, for the actual TempTable instance. Refs #6825
** CA 20220520 Added support for TEXT and ATTRIBUTE XML-NODE-TYPE values.
** OM 20220525 Fixed case-sensitivity for column lookup.
** IAS 20221006 Added ' getCodePage()', 'getDefinedFormat()', and 'getDefinedLabel()' .
** IAS 20221111 Added ' buffer', 'xmlNodeName', 'nsURI', and 'nsPrefix' fields. This is
** required for tables in the the DATASET parameters' support since they are
** processed differently from the standalone tables.
** 008 IAS 20230415 Fixed support for SERIALIZE-HIDDEN.
** 009 IAS 20230501 Fixed support for the INITIAL-NULL pseudo-attribute.
** 010 CA 20240320 Avoid BDTs within internal FWD runtime. Replaced iterators with Java 'for', where it
** applies.
*/
/*
** 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.serial;
import java.lang.reflect.*;
import java.util.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
/**
* The schema used for serialization of a temp-table, based on the internal definition of the
* table itself. Names used for serialization are legacy names as defined by the original 4GL
* code, not derived by conversion. Only legacy fields not marked SERIALIZE-HIDDEN are included
* in the schema.
*/
public class TempTableSchema
{
/* RecordBuffer on the temp-table */
private final RecordBuffer buffer;
/** Legacy name of the temp-table */
private final String name;
/** XML serialization name of the temp-table */
private final String xmlName;
/** XML node name of the temp-table */
private final String xmlNodeName;
/** XML namespace URI of the temp-table */
private final String nsURI;
/** XML prefix of the temp-table */
private final String nsPrefix;
/** Schema information needed for serialization of all columns/fields, in the DMO order. */
private final ArrayList<Column> columnList = new ArrayList<>();
/** The columns, mapped for quick case-insensitive lookup. */
private final LinkedHashMap<String, Column> columnMap = new LinkedHashMap<>();
/** The indexed of the DMO to be serialized. */
private final List<TableMapper.LegacyIndexInfo> indexes = new ArrayList<>();
/** List of column schema objects whose data is to be serialized as XML attributes */
private final List<Column> attributeColumns;
/** List of column schema objects whose data is to be serialized as XML elements */
private final List<Column> elementColumns;
/** List of column schema objects whose data is to be serialized as XML text */
private final List<Column> textColumns;
/** Flags the serialized {@code BEFORE-BUFFERS}. */
private final boolean beforeImage;
/** Flags the no-undoable temp-tables {@code NO-UNDO} option. */
private final boolean noUndo;
/**
* Constructor which gathers table and field data from a temp-table buffer.
*
* @param recBuf
* RecordBuffer on the temp-table.
* @param json
* Flag indicating if this is for JSON (when <code>true</code>) or XML (when <code>false</code>).
*/
public TempTableSchema(RecordBuffer recBuf, boolean json)
{
BufferImpl buf = (BufferImpl) recBuf.getDMOProxy();
// the table name is used for serialization, regardless of the individual buffer name
this.buffer = recBuf;
this.name = TableMapper.getLegacyName(recBuf);
this.xmlName = buf.getSerializeName().toJavaType();
Class<? extends DataModelObject> dmoIface = recBuf.getDMOInterface();
Map<String, Method> getterMap = PropertyHelper.allGettersByProperty(dmoIface);
Map<String, Method> setterMap = PropertyHelper.allSettersByProperty(dmoIface);
TempTable tt = recBuf.getParentTable();
this.xmlNodeName = tt.getXmlNodeName().toJavaType();
this.nsURI = tt.namespaceURI().isUnknown() ? "" : tt.namespaceURI().toJavaType();
this.nsPrefix = tt.namespacePrefix().toJavaType();
List<TableMapper.LegacyFieldInfo> fields = TableMapper.getAllLegacyFieldInfo(tt);
Iterator<TableMapper.LegacyFieldInfo> iter = fields.iterator();
this.noUndo = !tt.canUndo().toJavaType();
// indexes:
int k = 0;
while (true)
{
TableMapper.LegacyIndexInfo legacyIndexInfo = TableMapper.getLegacyIndexInfo(tt, k);
if (legacyIndexInfo != null)
{
indexes.add(legacyIndexInfo);
++k;
}
else
{
break;
}
}
DmoMeta meta = recBuf.getDmoInfo();
// Construct column schema data object; TableMapper.getAllLegacyFieldInfo returns field
// info objects in the correct order for serialization
Column lastCol = null;
for (int i = 0; iter.hasNext(); i++)
{
TableMapper.LegacyFieldInfo info = iter.next();
// add column to schema if it should be serialized
// if (info.getSerializeOptions(tt).isSerializeHidden())
// {
// continue;
// }
int offset = meta.byLegacyName(info.getLcLegacyName()).getMeta().getOffset();
Column col = new Column(i + 1, getterMap, setterMap, info, offset, recBuf);
String cname = json ? col.getSerializeName() : col.getXmlNodeName();
if (lastCol == null || !lastCol.legacyName.equalsIgnoreCase(col.legacyName))
{
// exclude extent column duplicates
columnList.add(col);
cname = cname.toLowerCase();
if (!columnMap.containsKey(cname))
{
columnMap.put(cname, col);
}
}
lastCol = col;
}
this.beforeImage = buf.isBeforeBuffer();
this.attributeColumns = filterColumns(columnList, SerializeOptions.XmlNodeType.ATTRIBUTE);
this.elementColumns = filterColumns(columnList, SerializeOptions.XmlNodeType.ELEMENT);
this.textColumns = filterColumns(columnList, SerializeOptions.XmlNodeType.TEXT);
}
/**
* Filter the given list of column schema objects by the given XML node type.
*
* @param columns
* Collection of column schema objects to be filtered.
* @param nodeType
* Node type which forms the filter criterion.
*
* @return A list of column schema objects whose node type matches the given node type.
* The list may be empty, but will not be {@code null}.
*/
private static List<Column> filterColumns(Collection<Column> columns,
SerializeOptions.XmlNodeType nodeType)
{
List<Column> list = null;
for (Column column : columns)
{
if (!column.isSerializeHidden() && column.getNodeType() == nodeType)
{
if (list == null)
{
list = new ArrayList<>();
}
list.add(column);
}
}
return list == null ? Collections.emptyList() : list;
}
/**
* Get the RecordBuffer on the temp-table.
*
* @return RecordBuffer on the temp-table.
*/
public RecordBuffer buffer()
{
return buffer;
}
/**
* Get the XML node name of the temp-table.
*
* @return XML node name of the temp-table.
*/
public String xmlNodeName()
{
return xmlNodeName;
}
/**
* Get the XML namespace URI of the temp-table.
*
* @return XML namespace URI of the temp-table.
*/
public String namespaceURI()
{
return nsURI;
}
/**
* Get the XML prefix of the temp-table.
*
* @return XML prefix of the temp-table.
*/
public String namespacePrefix()
{
return nsPrefix;
}
/**
* Get the legacy name of this temp-table.
*
* @return Legacy table name.
*/
public String getName()
{
return name;
}
/**
* Return the name for this table.
*
* @return See above.
*/
public String getTableName()
{
return name;
}
/**
* Return the XML serialization name for this table.
*
* @return See above.
*/
public String getXmlName()
{
return xmlName;
}
/**
* Checks whether the temp-table was defined as NO-UNDO.
*
* @return {@code true} if this is a NO-UNDO temp-table.
*/
public boolean isNoUndo()
{
return noUndo;
}
/**
* Get the column schema information associated with the given serialization name. Note: the lookup is
* performed in case-insensitive mode.
*
* @param name
* Name used in serialization output for column data.
*
* @return Column schema information.
*/
public Column getColumn(String name)
{
return columnMap.get(name.toLowerCase());
}
/**
* Obtain an iterator of the indexes of the DMO of this object.
*
* @return an iterator for the indexes of the DMO.
*/
public Iterator<TableMapper.LegacyIndexInfo> getIndexes()
{
return indexes.iterator();
}
/**
* Get an object to iterate over column schema information objects.
*
* @return An iterable object on column schema info objects.
*/
public ArrayList<Column> columns()
{
return columnList;
}
/**
* Get an iterable collection of column schema objects whose data is to be serialized as XML
* attributes.
*
* @return see above.
*/
public Iterable<Column> attributeColumns()
{
return attributeColumns;
}
/**
* Get an iterable collection of column schema objects whose data is to be serialized as XML
* elements.
*
* @return see above.
*/
public Iterable<Column> elementColumns()
{
return elementColumns;
}
/**
* Get an iterable collection of column schema objects whose data is to be serialized as XML text.
*
* @return see above.
*/
public Iterable<Column> textColumns()
{
return textColumns;
}
/**
* Check whether the target is a before-table.
*
* @return {@code true} if the target is a before-table.
*/
public boolean isBeforeImage()
{
return beforeImage;
}
/**
* Schema information needed to serialize/deserialize a field/column.
*/
static class Column
{
/** The offset in the record's data array. */
public final int offset;
/** Legacy name of field. */
private final String legacyName;
/** Extent of an array field (null for a scalar field) */
private final Integer extent;
/** Order in which column is serialized */
private final int order;
/** Name of element or attribute in serialized form (may differ from legacy field name) */
private final String precomputedName;
/** The field's XML-NODE-NAME option value. */
private final String xmlNodeName;
/** The field's SERIALIZE-NAME option value. */
private final String serializeName;
/** The field's SERIALIZE-HIDDEN option value. */
private final boolean serializeHidden;
/** Method to get field data from buffer */
private final Method getter;
/** Method to set field data into buffer */
private final Method setter;
/** Data wrapper type of field */
private final Class<? extends BaseDataType> type;
/** The initial value as an instance of the appropriate type of the INIT option for this column. */
private final BaseDataType initialValue;
/** Field info object to which we delegate all getters. */
private final TableMapper.LegacyFieldInfo lfi;
/** The {@link TempTable} instance. */
private TempTable tt;
/**
* Constructor.
*
* @param order
* Order of column in serialization output.
* @param getterMap
* Map of java property names to getter methods.
* @param setterMap
* Map of java property names to setter methods.
* @param lfi
* Field info object.
* @param offset
* The offset in the record's data array.
* @param buf
* The buffer instance.
*/
Column(int order,
Map<String, Method> getterMap,
Map<String, Method> setterMap,
TableMapper.LegacyFieldInfo lfi,
int offset,
RecordBuffer buf)
{
Class<? extends DataModelObject> dmoInterface = buf.getDMOInterface();
this.tt = buf.getParentTable();
this.offset = offset;
this.lfi = lfi;
SerializeOptions opts = lfi.getSerializeOptions(tt);
String serName = opts.getSerializeName();
if ("".equals(serName))
{
serName = null;
}
serializeName = serName;
serializeHidden = opts.isSerializeHidden();
String nodeName = opts.getXmlNodeName();
if ("".equals(nodeName))
{
nodeName = null;
}
xmlNodeName = nodeName;
this.legacyName = lfi.getLegacyName();
this.order = order;
// pre-compute the name used in serialized output is the legacy name of the field, unless overridden
// by the SERIALIZE-NAME attribute, unless overridden by the XML-NODE-NAME attribute
this.precomputedName = (nodeName != null) ? nodeName : (serName != null ? serName : legacyName);
String javaName = lfi.getJavaName();
this.extent = lfi.getExtent();
if (extent == 0 || lfi.getOriginal().isEmpty())
{
this.getter = getterMap.get(javaName);
this.setter = setterMap.get(javaName);
}
else
{
String originalName = lfi.getOriginal();
originalName = Character.toUpperCase(originalName.charAt(0)) + originalName.substring(1);
Method[] allMethods = dmoInterface.getDeclaredMethods();
Method setterMeth = null;
Method getterMeth = null;
for (Method method : allMethods)
{
if (method.getName().equals("set" + originalName) &&
method.getReturnType() == Void.TYPE &&
method.getParameterCount() == 2 &&
method.getParameterTypes()[0] == Integer.TYPE)
{
setterMeth = method;
}
else if (method.getParameterCount() == 1 &&
method.getParameterTypes()[0] == Integer.TYPE &&
(method.getName().equals("get" + originalName) ||
method.getName().equals("is" + originalName)))
{
getterMeth = method;
}
if (setterMeth != null && getterMeth != null)
{
break;
}
}
this.setter = setterMeth;
this.getter = getterMeth;
}
this.type = (Class<? extends BaseDataType>) getter.getReturnType();
this.initialValue = lfi.getInitialValue();
}
/**
* Constructor used for BEFORE TEMP-TABLE columns. It allows to set the internal values directly.
*
* @param legacyName
* The fields's name (legacy name).
* @param name
* Name of element or attribute in serialized form (may differ from legacy field name).
* @param initialValue
* The initial value as an instance of the appropriate type of the INIT option for this column.
*/
public Column(String legacyName, String name, BaseDataType initialValue)
{
this.offset = -1; // not used for reserved properties
this.lfi = null;
this.legacyName = legacyName;
this.extent = 0;
this.order = 0;
this.precomputedName = name;
this.xmlNodeName = null;
this.serializeName = null;
this.serializeHidden = false;
this.getter = null;
this.setter = null;
this.type = initialValue.getClass();
this.initialValue = initialValue;
}
/**
* Get the column's legacy field name.
*
* @return Legacy field name.
*/
public String getFieldName()
{
return legacyName;
}
/**
* Get the column's XML-NODE-NAME name, used for XML (de)serialization.
*
* @return The field name used for (de)serialization.
*/
public String getXmlNodeName()
{
if (lfi == null)
{
// BEFORE-TABLE special fields
return precomputedName;
}
// defaulting to SERIALIZE-NAME if not set, and legacy NAME if this is not set, either
return xmlNodeName == null ? serializeName == null ? legacyName : serializeName : xmlNodeName;
}
/**
* Get the column's legacy SERIALIZE-NAME, used for JSON (de)serialization.
*
* @return The field name used for (de)serialization.
*/
public String getSerializeName()
{
if (lfi == null)
{
// BEFORE-TABLE special fields
return precomputedName;
}
return serializeName == null ? legacyName : serializeName; // defaulting to legacy NAME if not set
}
/**
* Get the column's legacy SERIALIZE-HIDDEN attribute value
*
* @return The column's legacy SERIALIZE-HIDDEN attribute value.
*/
public boolean isSerializeHidden()
{
return serializeHidden;
}
/**
* Indicate whether column can be null.
*
* @return {@code true} if nillable, else {@code false}.
*/
public boolean isNillable()
{
return lfi == null || !lfi.mandatory;
}
/**
* Get extent of column, if any.
*
* @return Extent value for an array column, {@code null} for a scalar column.
*/
public Integer getExtent()
{
return extent;
}
/**
* Get the order in which this column is serialized.
*
* @return Order.
*/
public int getOrder()
{
return order;
}
/**
* Get the XML node type (element or attribute) for this column.
*
* @return Node type.
*/
public SerializeOptions.XmlNodeType getNodeType()
{
return lfi == null
? SerializeOptions.XmlNodeType.ELEMENT
: lfi.getSerializeOptions(tt).getXmlNodeType();
}
/**
* Get field codepage.
*
* @return Field codepage.
*/
public String getCodePage()
{
return lfi == null ? null : lfi.getCodePage();
}
/**
* Get the DMO getter method for this column's value.
*
* @return Getter method.
*/
public Method getGetter()
{
return getter;
}
/**
* Get the DMO setter method for this column's value.
*
* @return Setter method.
*/
public Method getSetter()
{
return setter;
}
/**
* Get the data type of the field.
*
* @return Data type.
*/
public Class<? extends BaseDataType> getType()
{
return type;
}
/**
* Get the XML data type of the field.
*
* @return XML data type.
*/
public String getXmlDataType()
{
return lfi == null ? null : lfi.getSerializeOptions(tt).getXmlDataType();
}
/**
* Obtain the initial value for this column (field).
*
* @return the initial value for this column if any is declared.
*/
public String getInitial()
{
return lfi == null ? null : lfi.getInitial();
}
/**
* Get the value of the INITIAL-NULL pseudo-attribute.
*
* @return the value of the INITIAL-NULL pseudo-attribute.
*/
public boolean isInitialNull()
{
return lfi == null ? false : lfi.isInitialNull();
}
/**
* Obtain the format for this column if one was set.
*
* @return the format for this column.
*/
public String getFormat()
{
return lfi == null ? null : lfi.getFormat(tt);
}
/**
* Obtain the defined format for this column if one was set.
*
* @return the defined format for this column.
*/
public String getDefinedFormat()
{
return lfi == null ? null : lfi.getDefinedFormat();
}
/**
* Obtain the help message for this column if one was set.
*
* @return the help for this column.
*/
public String getHelp()
{
return lfi == null ? null : lfi.getHelp(tt);
}
/**
* Test whether this is a BEFORE TEMP-TABLE hidden field. The hidden fields start with
* double underscore ({@code __}).
*
* @return {@code true} if this is a BEFORE temp-table hidden field.
*/
public boolean isHidden()
{
return ReservedProperty.isReservedProperty(legacyName);
}
/**
* Checks whether this is a case-sensitive character field.
*
* @return {@code true} only if this is a case-sensitive character field.
*/
public boolean isCaseSensitive()
{
return lfi != null && lfi.isCaseSensitive();
}
/**
* Obtain the precision for decimal fields.
*
* @return the precision for decimal fields.
*/
public int getDecimals()
{
return lfi == null ? 0 : lfi.getDecimals(tt);
}
/**
* Get the {@code COLUMN-LABEL} attribute of this column.
*
* @return The {@code COLUMN-LABEL} attribute of this column.
*/
public String getColumnLabel()
{
return lfi == null ? null : lfi.getColumnLabel(tt);
}
/**
* Get the {@code LABEL} attribute of this column.
*
* @return The {@code LABEL} attribute of this column.
*/
public String getLabel()
{
return lfi == null ? null : lfi.getLabel(tt);
}
/**
* Get the defined {@code LABEL} attribute of this column.
*
* @return The defined {@code LABEL} attribute of this column.
*/
public String getDefinedLabel()
{
return lfi == null ? null : lfi.getDefinedLabel();
}
/**
* Test whether a value is different than the initial value for this column.
*
* @param datum
* The value to be tested.
*
* @return {@code true} if the value is different.
*/
public boolean isChanged(BaseDataType datum)
{
boolean initUnknown = initialValue == null || initialValue.isUnknown();
if (datum.isUnknown() && initUnknown)
{
return false; // both unknowns
}
if (datum.isUnknown() != initUnknown)
{
return true; // only one of them is unknown
}
return !datum.equals(initialValue);
}
}
}