DsTableDefinition.java
/*
** Module : DsTableDefinition.java
** Abstract : A container for a DataSet Table metadata.
**
** Copyright (c) 2019-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM 20130708 Created initial version.
** OM 20190716 Updated Javadocs.
** OM 20190721 Added new table properties to transport protocol.
** CA 20190812 Added toString().
** 002 CA 20191119 Added support for before-table.
** 003 CA 20200427 BLOB fields must be serialized via MemoryBuffer.
** CA 20200514 In some cases, BLOB fields are already MemoryBuffer.
** 004 OM 20200906 New ORM implementation.
** 005 IAS 20200908 Rework (de)serialization.
** 006 IAS 20200922 Get rid of possible NPE on serialization.
** OM 20201120 Added implementations for XML-NODE-NAME, ERROR-STRING and SCHEMA-MARSHAL attributes.
** CA 20210213 Added 'isSimpleTable'.
** OM 20210309 Do not use DmoMeta as key in TableMapper because temp-tables share the same DmoMeta.
** SVL 20210614 Added copyRows parameter.
** SVL 20210701 Added clearRows.
** IAS 20221108 Fixed support for SCHEMA-MARSHAL == NONE. Re-worked (de)serialization.
** IAS 20221111 Fixed 'schemaMarshalLevel', 'xmlNodeName', 'nsURI', and 'nsPrefix' support.
** CA 20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
** 007 CA 20230215 Use PayloadSerializer for serialization. Fixed blob and object field serialization.
** 008 OM 20230315 Better debugging support (improved toString() method).
** 009 AS 20250416 For this.name use the buffer's parent name, insted of the buffer name.
*/
/*
** 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 java.io.*;
import java.util.*;
import java.util.stream.*;
import com.goldencode.p2j.oo.lang.*;
import com.goldencode.p2j.util.*;
import com.goldencode.util.*;
import static com.goldencode.p2j.persist.AbstractTempTable.*;
import static com.goldencode.util.NativeTypeSerializer.*;
/**
* Container for a temp-table. Used to send the temp-table to a remote side, when remote
* appserver calls are involved.
*/
public class DsTableDefinition
implements Externalizable
{
/** Flag indicating that table schema was received */
boolean receivedSchema = false;
/** The name of this table. */
private String name;
/** The result set to be sent to the remote side. */
private TableResultSet resultSet = null;
/** The list of read properties. */
private List<PropertyDefinition> properties;
/** The read data. */
private List<Object[]> rows;
/** The read row metadata. */
private List<Object[]> rowsMeta;
/** The table BEFORE/AFTER type. */
private int tableType = TempTable.BeforeType.SIMPLE.ordinal();
/** The peer table. For an AFTER-TABLE, the peer-table is the BEFORE-TABLE. And vice-versa. */
private DsTableDefinition peerDef = null;
/** The number of indexes in this table definition. */
private int numIndexes = 0;
/** The XML namespace for this table definition. */
private String xmlns = null;
/** The XML prefix for this table definition. */
private String xmlPrefix = null;
/** The XML-NODE-NAME attribute for this table definition. */
private String xmlNodeName = null;
/** The ERROR-STRING attribute for this table definition. */
private String errorString = null;
/** List of indexes' descriptors */
private List<IndexDefinition> indexDefs = null;
/**
* A string with all indexes of this table definition encoded as a {@code String}.
*
* See multiIxCols syntax on
* https://documentation.progress.com/output/ua/OpenEdge_latest/dvjav/Constructor_2.html
*/
private String indexes = null;
/**
* The SCHEMA-MARSHAL level for the serialized temp-table. It is never {@code SM_DEFAULT} when object is to
* be serialized.
*/
private int schemaMarshalLevel;
/**
* Default c'tor, explicitly added to allow instances of this class to be created on
* deserialization. Not for public use.
*/
public DsTableDefinition()
{
schemaMarshalLevel = SM_DEFAULT;
}
/**
* Wrap a custom result set in a class known to P2J, so the remote side can deserialize it.
*
* @param resultSet
* The result set, which is a subclass of {@link TableResultSet}.
*/
public DsTableDefinition(TableResultSet resultSet)
{
schemaMarshalLevel = SM_DEFAULT;
this.resultSet = resultSet;
}
/**
* The "serialization" constructor. Takes a {@code BufferImpl} and prepare for serialization.
* If it belongs to an AFTER-TABLE, the associated BEFORE-TABLE will also be serialized.
*
* @param target
* The {@code BufferImpl} to be serialized.
* @param copyRows
* <code>true</code>> to transfer rows in INPUT direction. <code>false</code>> to transfer only
* table definitions.
*/
public DsTableDefinition(BufferImpl target, boolean copyRows)
{
this.name = target.buffer().getParentTable()._name();
this.tableType = (target.isAfterBuffer() ? TempTable.BeforeType.AFTER :
target.isBeforeBuffer() ? TempTable.BeforeType.BEFORE :
TempTable.BeforeType.SIMPLE).ordinal();
Buffer defBuff = (Buffer) target.defaultBufferHandleNative();
AbstractTempTable parentTable = (AbstractTempTable) target.buffer().getParentTable();
int sml = parentTable.getSchemaMarshalLevel();
this.schemaMarshalLevel = (sml == SM_DEFAULT) ? SM_FULL : sml;
switch (sml)
{
case SM_FULL:
case SM_MIN:
this.errorString = defBuff.errorString().toStringMessage();
// no break, fall through
case SM_DEFAULT:
// full table schema is serialized with table parameter
character tmp = parentTable.namespaceURI();
this.xmlns = tmp.isUnknown() ? "" : tmp.toJavaType();
this.xmlPrefix = parentTable.namespacePrefix().toJavaType();
this.xmlNodeName = parentTable.namespacePrefix().toJavaType();
this.indexes = TableMapper.getLegacyIndexInfo(parentTable);
this.numIndexes = this.indexes.split("\\.").length;
this.indexDefs = TableMapper.getLegacyIndexes(parentTable).map(lif -> new IndexDefinition(lif)).
collect(Collectors.toList());
// no break, fall through
case SM_NONE:
default:
// nothing is marshaled. The protocol is the same but null values are sent instead of data.
// The receiver peer must have all information readily available.
break;
}
if (this.tableType == TempTable.BeforeType.AFTER.ordinal())
{
this.peerDef = new DsTableDefinition((BufferImpl) target.beforeBufferNative(), copyRows);
this.peerDef.peerDef = this;
}
this.resultSet = new TempTableResultSet((Temporary) target, true, true, true, copyRows);
}
/**
* Obtain the peer table definition. For an AFTER-TABLE, the peer-table is its BEFORE-TABLE.
* And vice-versa: for a BEFORE-TABLE, the peer-table is its AFTER-TABLE.
* <p>
* Usage of this method is not encouraged. This is because the caller is not aware of what
* exactly he will receive and the method logic does not have the means to check whether the
* returned object is exactly what was desired. Specialized method should be used (namely
* {@code getBeforeDef()} and {@code getAfterDef()}).
*
* @return the peer {@code DsTableDefinition} as explained above.
*/
public DsTableDefinition getPeerDef()
{
return peerDef;
}
/**
* Configures the peer table definition.
* <p>
* Usage of this method is not encouraged. This is because the caller is not aware of what
* exactly he will set and the method logic does not have the means to check whether the
* returned object is exactly what was desired. Specialized method should be used (namely
* {@code setBeforeDef()} and {@code setAfterDef()}). Moreover, the method automatically sets
* this as an AFTER-BUFFER definition.
*
* @param peerDef
* The new peer table definition.
*/
public void setPeerDef(DsTableDefinition peerDef)
{
this.peerDef = peerDef;
this.tableType = TempTable.BeforeType.AFTER.ordinal();
this.peerDef.tableType = TempTable.BeforeType.BEFORE.ordinal();
}
/**
* Gets the name of this table.
*
* @return the name of this table.
*/
public String getName()
{
return name;
}
/**
* The SCHEMA-MARSHAL level for the serialized temp-table.
*
* @return The {@link #schemaMarshalLevel}.
*/
public int getSchemaMarshalLevel()
{
return schemaMarshalLevel;
}
/**
* Set the SCHEMA-MARSHAL level for the serialized temp-table.
*
* @param schemaMarshalLevel
* The {@link #schemaMarshalLevel}.
*/
public void setSchemaMarshalLevel(int schemaMarshalLevel)
{
this.schemaMarshalLevel = schemaMarshalLevel;
}
/**
* Sets the name of this table.
*
* @param name
* The (new) name of this table.
*/
public void setName(String name)
{
this.name = name;
}
/**
* Check that table schema was received.
*
* @return <code>true</code> if table schema was received.
*/
public boolean isReceivedSchema()
{
return receivedSchema;
}
/**
* Check whether this is a definition of an AFTER-TABLE.
*
* @return {@code true} if this is an AFTER-TABLE definition.
*/
public boolean isAfterTable()
{
return tableType == TempTable.BeforeType.AFTER.ordinal();
}
/**
* Check whether this is a definition of a BEFORE-TABLE.
*
* @return {@code true} if this is an BEFORE-TABLE definition.
*/
public boolean isBeforeTable()
{
return tableType == TempTable.BeforeType.BEFORE.ordinal();
}
/**
* Check whether this is a definition of a SIMPLE temp-table.
*
* @return {@code true} if this is an SIMPLE temp-table definition.
*/
public boolean isSimpleTable()
{
return tableType == TempTable.BeforeType.SIMPLE.ordinal();
}
/**
* Obtain the number of fields in this table.
* <p>
* NOTE: the result of this method is valid only after {@code properties} was set.
* <p>
* TODO: test and confirm whether this method returns the number of legacy fields or this
* number includes the meta-fields (ROW-STATE, BEFORE-ROWID, etc)
*
*
* @return the number of fields in this table.
*/
public int getNumFields()
{
return properties.size();
}
/**
* Returns the number of indexes defined for this table.
*
* @return the number of indexes defined for this table.
*/
public int getNumIndexes()
{
return numIndexes;
}
/**
* Obtain the XML namespace for this table definition.
*
* @return the XML namespace for this table definition.
*/
public String getXmlns()
{
return xmlns;
}
/**
* Obtain the XML prefix for this table definition.
*
* @return the XML prefix for this table definition.
*/
public String getXmlPrefix()
{
return xmlPrefix;
}
/**
* Obtain the XML node name for this table definition.
*
* @return the XML node name for this table definition.
*/
public String getXmlNodeName()
{
return xmlNodeName;
}
/**
* Obtain the ERROR-STRING attribute for this table definition.
*
* @return the ERROR-STRING attribute for this table definition.
*/
public String getErrorString()
{
return errorString;
}
/**
* Obtain the set of indexes of this table definition.
*
* @return the set of indexes of this table definition encoded as a single string.
*
* @see #indexes
*/
public String getIndexes()
{
return indexes;
}
/**
* Get List of indexes' descriptors.
* @return the List of indexes' descriptors.
*/
public List<IndexDefinition> getIndexDefs()
{
return indexDefs;
}
/**
* Sets the XML namespace for this table definition.
*
* @param xmlns
* The (new) XML namespace for this table definition.
*/
public void setXmlns(String xmlns)
{
this.xmlns = xmlns;
}
/**
* Sets the XML prefix for this table definition.
*
* @param xmlPrefix
* XML prefix for this table definition.
*/
public void setXmlPrefix(String xmlPrefix)
{
this.xmlPrefix = xmlPrefix;
}
/**
* Sets the XML node name for this table definition.
*
* @param xmlNodeName
* XML prefix for this table definition.
*/
public void setXmlNodeName(String xmlNodeName)
{
this.xmlNodeName = xmlNodeName;
}
/**
* Sets the ERROR-STRING attribute for this table definition.
*
* @param err
* ERROR-STRING attribute for this table definition.
*/
public void setErrorString(String err)
{
this.errorString = err;
}
/**
* Configures the index3es for this table.
*
* @param numIndexes
* The number of indexes in this table definition.
* @param indexes
* A string with all indexes of this table definition encoded as a {@code String}.
*/
public void setIndexes(int numIndexes, String indexes)
{
this.numIndexes = numIndexes;
this.indexes = indexes;
}
/**
* Provides an iterator over the read {@link #properties}. This is used to access the data
* sent by a remote side, via {@link #readExternal}. Must not be called when {@link #resultSet}
* is set.
*
* @return See above.
*/
public Iterator<PropertyDefinition> propertyIterator()
{
if (resultSet != null)
{
throw new IllegalStateException(
"A custom result set must not be specified when calling this method!");
}
if (properties == null)
{
return Collections.EMPTY_LIST.iterator();
}
return properties.iterator();
}
/**
* Provides an iterator over the read {@link #rows}. This is used to access the data sent by a
* remote side, via {@link #readExternal}. Must not be called when {@link #resultSet} is set.
*
* @return See above.
*/
public Iterator<Object[]> rowIterator()
{
if (resultSet != null)
{
throw new IllegalStateException(
"A custom result set must not be specified when calling this method!");
}
if (rows == null)
{
return Collections.EMPTY_LIST.iterator();
}
return rows.iterator();
}
/**
* Get the list of read {@link #properties}. This is used to access the data sent by a remote
* side, via {@link #readExternal}. Must not be called when {@link #resultSet} is set.
*
* @return See above.
*/
public List<PropertyDefinition> getProperties()
{
if (resultSet != null)
{
throw new IllegalStateException(
"A custom result set must not be specified when calling this method!");
}
return properties != null ? new ArrayList<>(properties) : Collections.emptyList();
}
/**
* Get the list of read {@link #rows}. This is used to access the data sent by a remote side,
* via {@link #readExternal}. Must not be called when {@link #resultSet} is set.
*
* @return See above.
*/
public List<Object[]> getRows()
{
if (resultSet != null)
{
throw new IllegalStateException(
"A custom result set must not be specified when calling this method!");
}
return new ArrayList<>(rows);
}
/**
* Get the meta info for each row (the {@link TempRecord} fields).
*
* @return The meta info for each row.
*
* @see TempRecord#toArray()
*/
public List<Object[]> getRowsMeta()
{
if (resultSet != null)
{
throw new IllegalStateException(
"A custom result set must not be specified when calling this method!");
}
return new ArrayList<>(rowsMeta);
}
/**
* Obtain the {@code TableResultSet} of this table.
*
* @return the {@code TableResultSet} of this table.
*/
public TableResultSet getResultSet()
{
return resultSet;
}
/**
* Send the table definition to the specified output destination.
*
* @param out
* The output destination to which the table definition will be sent.
*
* @throws IOException
* In case of I/O errors.
*/
public final void writeExternal(ObjectOutput out)
throws IOException
{
boolean writeSchema = schemaMarshalLevel != SM_NONE;
out.writeByte(schemaMarshalLevel);
writeString(out, name);
out.writeByte(tableType);
if (writeSchema)
{
out.writeByte(numIndexes);
writeString(out,indexes);
writeList(out, indexDefs);
writeString(out,xmlns);
writeString(out,xmlPrefix);
}
// write properties
Set<Integer> blobProps = null;
Set<Integer> objectProps = null;
if (resultSet != null)
{
int idx = 0;
while (resultSet.hasMoreProperties())
{
PropertyDefinition property = resultSet.nextProperty();
if (writeSchema)
{
property.setMarshalLevel(schemaMarshalLevel);
out.writeByte(1);
property.writeExternal(out);
}
if (property.getType() == blob.class)
{
if (blobProps == null)
{
blobProps = new HashSet<>();
}
blobProps.add(idx);
}
else if (object.class.isAssignableFrom(property.getType()))
{
if (objectProps == null)
{
objectProps = new HashSet<>();
}
objectProps.add(idx);
}
idx = idx + 1;
}
}
if (writeSchema)
{
out.writeByte(0);
}
writeIntSet(out, blobProps);
writeIntSet(out, objectProps);
// write data
if (resultSet != null)
{
while (resultSet.hasMoreRows())
{
Object[] row = resultSet.nextRow();
if (blobProps != null)
{
Object[] row2 = new Object[row.length];
System.arraycopy(row, 0, row2, 0, row2.length);
for (int idx : blobProps)
{
if (row2[idx] instanceof blob)
{
row2[idx] = new MemoryBuffer(((blob) row2[idx]).getByteArray());
}
}
row = row2;
}
if (objectProps != null)
{
Object[] row2 = new Object[row.length];
System.arraycopy(row, 0, row2, 0, row2.length);
for (int idx : objectProps)
{
if (row2[idx] instanceof object)
{
row2[idx] = new LegacyObject((object<?>) row2[idx]);
}
}
row = row2;
}
PayloadSerializer.writePayload(out, row);
Object[] rowMeta = resultSet.nextRowMeta();
PayloadSerializer.writePayload(out, rowMeta);
}
}
// end data with a [null]
PayloadSerializer.writePayload(out, null);
// last thing to do:
if (isAfterTable())
{
peerDef.writeExternal(out);
}
}
/**
* Read the table definition from the specified input source.
*
* @param in
* The input source from which the table definition will be read.
*
* @throws IOException
* In case of I/O errors.
* @throws ClassNotFoundException
* If the read {@code type} of a property can't be resolved to a valid class.
*/
public final void readExternal(ObjectInput in)
throws IOException,
ClassNotFoundException
{
schemaMarshalLevel = in.readByte();
receivedSchema = schemaMarshalLevel != SM_NONE;
name = readString(in);
tableType = in.readByte();
if (receivedSchema)
{
numIndexes = in.readByte();
indexes = readString(in);
indexDefs = readList(in, () -> new IndexDefinition());
xmlns = readString(in);
xmlPrefix = readString(in);
properties = new ArrayList<>();
while (in.readByte() != 0)
{
PropertyDefinition pd = new PropertyDefinition();
pd.readExternal(in);
properties.add(pd);
}
}
Set<Integer> blobProps = readIntSet(in);
Set<Integer> objectProps = readIntSet(in);
// read data
rows = new ArrayList<>();
rowsMeta = new ArrayList<>();
Object next = PayloadSerializer.readPayload(in);
while (next != null)
{
Object[] row = (Object[]) next;
rows.add(row);
if (blobProps != null)
{
for (Integer i : blobProps)
{
row[i] = new blob(((MemoryBuffer) row[i]).getValue());
}
}
if (objectProps != null)
{
objectProps.stream().forEach(i -> {
LegacyObject ref = (LegacyObject) row[i];
ObjectVar<? extends _BaseObject_> v = new ObjectVar<>(ref.getType());
v.assign(ref.restore());
row[i] = v;
});
}
next = PayloadSerializer.readPayload(in);
rowsMeta.add((Object[]) next);
// prepare the next object, stop at [null]
next = PayloadSerializer.readPayload(in);
}
// last thing to do:
if (isAfterTable())
{
// read the before-table
peerDef = new DsTableDefinition();
peerDef.readExternal(in);
peerDef.peerDef = this;
}
}
/**
* Delete row data.
*/
public void clearRows()
{
rows = null;
rowsMeta = null;
if (resultSet != null)
{
resultSet.clearRows();
}
}
/**
* Get a string representation of this table.
*
* @return See above.
*/
@Override
public String toString()
{
StringBuilder sb = new StringBuilder("TABLE[").append(getName()).append("]\n");
sb.append("PROPERTIES:\n");
Iterator<PropertyDefinition> iter = (properties == null) ? null : properties.iterator();
if (iter != null)
{
while (iter.hasNext())
{
sb.append(iter.next().toString()).append("\n");
}
sb.append("\n");
}
else
{
sb.append("/none/\n");
}
sb.append("ROWS:\n");
if (resultSet == null || rows == null)
{
sb.append("/none/\n");
}
else
{
Iterator<Object[]> rowIter = rows.iterator();
while (rowIter.hasNext())
{
Object[] data = rowIter.next();
for (Object d : data)
{
String sd = "";
if (d instanceof BaseDataType)
{
sd = ((BaseDataType) d).toStringMessage();
}
else
{
sd = d.toString();
}
sb.append("[").append(sd).append("]; ");
}
sb.append("\n");
}
}
if (peerDef != null && isAfterTable())
{
sb.append("BEFORE-").append(peerDef.toString());
}
return sb.toString();
}
}