DatasetWrapper.java
/*
** Module : DatasetWrapper.java
** Abstract : A wrapper for a Dataset, including all component tables, used to serialize the table
** data and send it to a remote side.
**
** Copyright (c) 2019-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM 20190708 Created initial version based on the TableWrapper.
** OM 20190716 Updated Javadoc. Fixed serialization.
** CA 20190812 Added toString().
** 002 CA 20191119 Added support for before-table.
** 003 CA 20200427 Allow a DATASET to be read from JSON argument, in case of a remote REST call.
** 004 IAS 20200908 Rework (de)serialization.
** CA 20210304 Added getResource() API.
** CA 20211112 Allow the DATASET and TABLE parameters for remote SOAP calls to be transferred via XML
** (so that relations, schema, etc is done on the server-side).
** CA 20221102 Added a static serialization ID - required when enabling AOP method tracing (see
** p2j.aspects.ltw.MethodTraceAspect).
** IAS 20221108 Fixed support for SCHEMA-MARSHAL == NONE.
** IAS 20221111 Added 'xmlNodeName', 'nsURI', and 'nsPrefix' fields and fixed
** 'schemaMarshalLevel' support.
** 005 OM 20231027 Added support for ERROR attribute.
** 006 CA 20240426 Track if this container holds a valid dataset.
*/
/*
** 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 static com.goldencode.util.NativeTypeSerializer.*;
import java.io.*;
import java.util.*;
import com.goldencode.p2j.aspects.ltw.MethodTraceAspect;
/**
* This wrapper groups multiple {@link DsTableDefinition} and {@link DsRelationDefinition} the
* components of the {@link DataSet} instance before sending them to a remote side.
* <p>
* This class is intended to be used only by FWD runtime.
*/
public class DatasetWrapper
implements Externalizable
{
/** A static serialization ID - required when enabling AOP method tracing (see {@link MethodTraceAspect}.*/
private static final long serialVersionUID = 1234567L;
/** The result set to be sent to the remote side. */
private DataSetContainer resource = null;
/** Flag indicating this dataset is sent in INPUT or INPUT-OUTPUT mode. */
private boolean input;
/** Flag indicating this dataset is sent in OUTPUT or INPUT-OUTPUT mode. */
private boolean output;
/** Flag indicating this dataset is sent in APPEND mode. */
private boolean append;
/** The tables definitions to be read for the dataset. */
private List<DsTableDefinition> tableDefs = null;
/** The relations definitions to be read for the dataset. */
private List<DsRelationDefinition> relationsDefs = null;
/**
* Determines if this wrapper wraps data from a DATASET-HANDLE parameter. Necessary because of
* quirks in error handling: calling side needs to know if it is a DATASET-HANDLE parameter
* (not a DATASET parameter) on the called side.
*/
private boolean dsHandle = false;
/** The name of the DataSet. */
private String dsName = null;
/** The XML namespace for this dataset. */
private String xmlns = null;
/** The XML prefix for this dataset. */
private String xmlPrefix = null;
/** The {@code ERROR} attribute of the dataset. */
private boolean hasError = false;
/** The XML-NODE-NAME attribute for this dataset. */
private String xmlNodeName = null;
/** Flag indicating if the source is a JSON dataset. */
private boolean fromJson;
/**
* For SOAP web services, the DATASET is sent (and received) serialized as XML, as the requester can't
* (de)serialize it properly (the relations have dependencies on executing a query to determine the
* related child-table rows). This includes the schema, also, for DATASET-HANDLE.
*/
private String xmlDataset = null;
/** Flag indicating if the transfer is made via XML. */
private boolean asXml = false;
/** Flag indicating if the before-table must be interpreted, too. */
private boolean useBeforeImage = false;
/** Flag indicating if this container holds a valid dataset. */
private boolean valid = false;
/**
* Default c'tor, explicitly added to allow instances of this class to be created on
* deserialization. Not for public use.
*/
public DatasetWrapper()
{
}
/**
* Create a new wrapper.
*
* @param input
* Flag indicating the wrapper is in INPUT mode.
* @param output
* Flag indicating the wrapper is in OUTPUT mode.
* @param dsHandle
* Flag indicating the wrapper is for a DATASET-HANDLE.
*/
public DatasetWrapper(boolean input, boolean output, boolean dsHandle)
{
this.input = input;
this.output = output;
this.dsHandle = dsHandle;
this.fromJson = true;
}
/**
* Wrap a custom result set in a class known to FWD, so the remote side can deserialize it.
*
* @param resource
* The resource, which is a (subclass of) {@link DataSet}.
*/
public DatasetWrapper(DataSetContainer resource)
{
this.valid = resource != null;
this.resource = resource;
this.xmlns = resource.namespaceURI();
this.xmlPrefix = resource.namespacePrefix();
this.xmlNodeName = resource.getXmlNodeName();
this.hasError = resource.hasError();
}
/**
* Wrap a custom result set in a class known to FWD, so the remote side can deserialize it.
*
* @param fromJson
* Flag indicating if the dataset is from a JSON source.
* @param resource
* The resource, which is a (subclass of) {@link DataSet}.
*/
public DatasetWrapper(boolean fromJson, DataSetContainer resource)
{
this(resource);
this.fromJson = fromJson;
}
/**
* Get the {@link #valid} flag.
*
* @return {@code true} if this container is for a valid dataset.
*/
public boolean isValid()
{
return valid;
}
/**
* Set the {@link #useBeforeImage} flag.
*
* @param useBeforeImage
* Flag indicating that the before-image for the dataset is used.
*/
public void setUseBeforeImage(boolean useBeforeImage)
{
this.useBeforeImage = useBeforeImage;
}
/**
* Get the {@link #useBeforeImage} flag.
*
* @return See above.
*/
public boolean isUseBeforeImage()
{
return useBeforeImage;
}
/**
* Set the {@link #xmlDataset}.
*
* @param xmlDataset
* The XML representation of the dataset.
*/
public void setXmlDataset(String xmlDataset)
{
this.xmlDataset = xmlDataset;
}
/**
* Get the {@link #xmlDataset}.
*
* @return See above.
*/
public String getXmlDataset()
{
return xmlDataset;
}
/**
* Get the {@link #asXml} flag.
*
* @return See above.
*/
public boolean isAsXml()
{
return asXml;
}
/**
* Set the {@link #asXml} flag.
*
* @param asXml
* When <code>true</code>, the dataset is transferred via its XML representation.
*/
public void setAsXml(boolean asXml)
{
this.asXml = asXml;
}
/**
* Get the dataset container resource.
*
* @return The {@link #resource}.
*/
public DataSetContainer getResource()
{
return resource;
}
/**
* Get the XML namespace of the DATASET.
* @return XML namespace of the DATASET
*/
public String namespaceURI()
{
return xmlns;
}
/**
* Get the XML prefix of the DATASET.
* @return XML prefix of the DATASET
*/
public String namespacePrefix()
{
return xmlPrefix;
}
/**
* Get the XML node name of the DATASET.
*
* @return XML node name of the DATASET
*/
public String getXmlNodeName()
{
return xmlNodeName;
}
/**
* Get the ERROR attribute of the DATASET.
*
* @return the ERROR attribute.
*/
public boolean hasError()
{
return hasError;
}
/**
* Returns the list of table definitions for the temp-tables from this {@code DataSet}.
*
* @return the list of table definitions from this {@code DataSet}.
*/
public List<DsTableDefinition> getTableDefs()
{
return tableDefs;
}
/**
* Returns the list of relations definitions between the temp-tables from this {@code DataSet}.
* All relations are returned, whether they are active or not.
*
* @return the list of relations definitions from this {@code DataSet}.
*/
public List<DsRelationDefinition> getRelationsDefs()
{
return relationsDefs;
}
/**
* Check if this dataset is in INPUT or INPUT-OUTPUT mode. This is used to access the data sent
* by a remote side, via {@link #readExternal}. Must not be called when {@link #resource} is
* set.
*
* @return The {@link #input} flag.
*/
public boolean isInput()
{
if (resource != null)
{
throw new IllegalStateException(
"A custom result set must not be specified when calling this method!");
}
return input;
}
/**
* Check if this dataset is in OUTPUT or INPUT-OUTPUT mode. This is used to access the data
* sent by a remote side, via {@link #readExternal}. Must not be called when {@link #resource}
* is set.
*
* @return The {@link #output} flag.
*/
public boolean isOutput()
{
if (resource != null)
{
throw new IllegalStateException(
"A custom result set must not be specified when calling this method!");
}
return output;
}
/**
* Check if this dataset is in APPEND mode. This is used to access the data sent by a remote
* side, via {@link #readExternal}. Must not be called when {@link #resource} is set.
*
* @return The {@link #append} flag.
*/
public boolean isAppend()
{
if (resource != null)
{
throw new IllegalStateException(
"A custom result set must not be specified when calling this method!");
}
return append;
}
/**
* Determines if this wrapper wraps data from a DATASET-HANDLE parameter.
*
* @return {@code true} if this wrapper wraps data from a DATASET-HANDLE parameter.
*/
public boolean isDatasetHandle()
{
return dsHandle;
}
/**
* Check if the source is a JSON.
*
* @return The {@link #fromJson} flag.
*/
public boolean isFromJson()
{
return fromJson;
}
/**
* Set if this wrapper wraps data from a DATASET-HANDLE parameter.
*
* @param datasetHandle
* {@code true} if this wrapper wraps data from a DATASET-HANDLE parameter.
*/
public void setDatasetHandle(boolean datasetHandle)
{
this.dsHandle = datasetHandle;
}
/**
* Obtain the dataset name.
*
* @return the dataset name.
*/
public String getDatasetName()
{
return dsName;
}
/**
* Sets the dataset name.
*
* @param dsName
* The (new) dataset name.
*/
public void setDatasetName(String dsName)
{
this.dsName = dsName;
}
/**
* Send the result set (metadata and rows) to the specified output destination.
*
* @param out
* The output destination to which the result set will be sent.
*
* @throws IOException
* In case of I/O errors.
*/
public final void writeExternal(ObjectOutput out)
throws IOException
{
out.writeByte(resource != null ? 1 : 0);
out.writeByte(fromJson ? 1 : 0);
out.writeByte(asXml ? 1 : 0);
out.writeByte(dsHandle ? 1 : 0);
out.writeByte(useBeforeImage ? 1 : 0);
out.writeByte(hasError ? 1 : 0);
// write modes
if (resource != null)
{
out.writeByte(resource.isInput() ? 1 : 0);
out.writeByte(resource.isOutput() ? 1 : 0);
out.writeByte(resource.isAppend() ? 1 : 0);
if (asXml)
{
// nothing else is sent,
writeString(out, xmlDataset);
return;
}
writeString(out, resource.getStructureName());
}
else
{
out.writeByte(input ? 1 : 0);
out.writeByte(output ? 1 : 0);
out.writeByte(append ? 1 : 0);
if (asXml)
{
// nothing else is sent,
writeString(out, xmlDataset);
return;
}
writeString(out, dsName);
}
writeString(out, xmlns);
writeString(out, xmlPrefix);
writeString(out, xmlNodeName);
// write properties
if (resource != null)
{
out.writeInt(resource.getTableCount());
if (resource.getTableCount() > 0)
{
for (DsTableDefinition table : resource.getTableDefinitions())
{
table.writeExternal(out);
}
}
}
else
{
out.writeInt(0);
}
if (resource != null)
{
out.writeInt(resource.getRelationCount());
if (resource.getRelationCount() > 0)
{
for (DsRelationDefinition rel : resource.getRelationDefinitions())
{
rel.writeExternal(out);
}
}
}
else
{
out.writeInt(0);
}
}
/**
* Read the result set (metadata and rows) from the specified input source.
*
* @param in
* The input source from which the result set will be read.
*
* @throws IOException
* In case of I/O errors.
* @throws ClassNotFoundException
* If a class load operation fails.
*/
public final void readExternal(ObjectInput in)
throws IOException,
ClassNotFoundException
{
valid = in.readByte() != 0;
fromJson = in.readByte() != 0;
asXml = in.readByte() != 0;
dsHandle = in.readByte() != 0;
useBeforeImage = in.readByte() != 0;
hasError = in.readByte() != 0;
// read modes
input = in.readByte() != 0;
output = in.readByte() != 0;
append = in.readByte() != 0;
if (asXml)
{
// nothing else is sent
xmlDataset = readString(in);
return;
}
dsName = readString(in);
xmlns = readString(in);
xmlPrefix = readString(in);
xmlNodeName = readString(in);
int n = in.readInt();
if (n != 0)
{
tableDefs = new ArrayList<>(n);
for (int k = 0; k < n; k++)
{
DsTableDefinition tDef = new DsTableDefinition();
tDef.readExternal(in);
tableDefs.add(tDef); // it will automatically read the before-table, too
}
}
n = in.readInt();
if (n != 0)
{
relationsDefs = new ArrayList<>(n);
for (int k = 0; k < n; k++)
{
DsRelationDefinition tDef = new DsRelationDefinition();
tDef.readExternal(in);
relationsDefs.add(tDef);
}
}
}
/**
* Obtain a {@code TableWrapper} for a specific table of this {@code DataSet}. The table is
* identified by its index in the buffer list.
*
* @param k
* The index of the table whose {@code TableWrapper} will be returned.
*
* @return the table wrapper for the target table.
*/
public TableWrapper getTableWrapper(int k)
{
DsTableDefinition tdef = tableDefs.get(k);
TempTableResultSet res = this.output ? new TempTableResultSet(this.output, this.append)
: new TempTableResultSet();
res.setProperties(tdef.getProperties());
res.setRows(tdef.getRows());
res.setRowsMeta(tdef.getRowsMeta());
res.setStructureName(tdef.getName());
DsTableDefinition btdef = tdef.getPeerDef();
TempTableResultSet bres = null;
if (btdef != null)
{
bres = this.output ? new TempTableResultSet(this.output, this.append)
: new TempTableResultSet();
bres.setProperties(btdef.getProperties());
bres.setRows(btdef.getRows());
bres.setRowsMeta(btdef.getRowsMeta());
bres.setStructureName(btdef.getName());
}
try
{
TableWrapper tw = new TableWrapper(fromJson, res);
tw.setTableName(tdef.getName());
tw.setSchemaMarshalLevel(tdef.getSchemaMarshalLevel());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
tw.writeExternal(oos);
oos.close();
tw = new TableWrapper();
tw.readExternal(new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())));
tw.receivedSchema = tdef.receivedSchema;
if (bres != null)
{
TableWrapper btw = new TableWrapper(bres);
baos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(baos);
btw.writeExternal(oos);
oos.close();
btw = new TableWrapper();
btw.readExternal(new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())));
tw.setPeerDef(btw);
}
return tw;
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
/**
* Get a string representation of this dataset.
*
* @return See above.
*/
@Override
public String toString()
{
StringBuilder s = new StringBuilder("DATASET[" + (dsName == null ? resource == null ? "null"
: resource.structureName
: dsName) + "]");
s.append("\n");
if (tableDefs != null)
{
for (DsTableDefinition dst : tableDefs)
{
s.append(dst.toString());
s.append("\n");
}
}
else if (resource != null)
{
for (DsTableDefinition dst : resource.getTableDefinitions())
{
s.append(dst.toString());
s.append("\n");
}
}
if (relationsDefs != null)
{
for (DsRelationDefinition dsr : relationsDefs)
{
s.append(dsr.toString());
s.append("\n");
}
}
else if (resource != null)
{
for (DsRelationDefinition dsr : resource.getRelationDefinitions())
{
s.append(dsr.toString());
s.append("\n");
}
}
return s.toString();
}
}