XmlImport.java
/*
** Module : XmlImport.java
** Abstract : Read XML content into a temp-table.
**
** Copyright (c) 2017-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20171016 Created initial version.
** 002 CA 20180511 Added XML-NODE-NAME table option support.
** 003 ECF 20190123 Replaced TempTableSchema with XmlTempTableSchema. Refactored ExtentTracker
** inner class into a separate, top-level class.
** 004 OM 20190327 Renamed DataSource to avoid conflicts with DataSet source.
** 005 OM 20190821 Added DataSet reading support.
** 006 OM 20190909 Added TempTable read and schema validation.
** 007 ECF 20200906 New ORM implementation.
** CA 20200906 Batch error fix.
** 008 OM 20201120 Added missing features: useLobs, schema location, field mapping.
** Fixed restoring table/fields attributes: case-sensitivity, decimals, xml uri, xml prefix,
** xml namespace, read-mode, etc
** 20210106 Dropped false parameter of TEMP-TABLE-PREPARE API.
** 20210108 Improve error handling when reading XML serialized files.
** OM 20210309 Do not use DmoMeta as key in TableMapper because temp-tables may share the same
** DmoMeta instance.
** CA 20210510 Fixed JSON and XML serialization when XML-NODE-NAME/SERIALIZE-NAME field options are set.
** CA 20210511 Removed XmlTempTableSchema class, as it was obsolete.
** CA 20210629 Use prodata:wordIndex when reading the table schema.
** CA 20210709 XML parse must use a pristine byte stream and use the reader to decode it (using the
** encoding as specified at the XML prolog). Also, the XML version is forced to 1.1, to
** allow ISO control characters; FWD will automatically escape them.
** OM 20210830 Fixed openReader() and readTableSchema().
** CA 20211112 Fixed BLOB deserialization and cases when the table has the XML-NODE-NAME/SERIALIZE-NAME
** set, but the schema is not included.
** OM 20220201 Added read support for elements added in XmlExport:H009/20220120.
** CA 20220218 Ensure the group and decimal separators are ',' and '.' during record read.
** OM 20220328 Stop and return false if validation errors occur while reading a table.
** OM 20220422 Rewrite the read in REPLACE mode.
** CA 20221006 Do not access tableHandle() in the FWD runtime, get the TempTable instance directly.
** Refs #6826
** TJD 20220504 Java 11 compatibility minor changes
** CA 20220520 Added support for TEXT and ATTRIBUTE XML-NODE-TYPE values.
** CA 20220608 NESTED relation fields are automatically copied from the parent to the child table. Not
** fully tested or implemented.
** OM 20220914 Use MAX-WIDTH to generate custom sized varchar columns.
** IAS 20220926 Fixed READ-XMLSCHEMA support.
** IAS 20221014 More fixes to READ-XMLSCHEMA support.
** IAS 20221018 Fixed processing of 'now' and 'today' INITIAL attribute value.
** IAS 20221116 Fixed READ-XMLSCHEMA support for DATASET.
** IAS 20221121 Replaced IllegalStateException with correct 4GL warning.
** IAS 20221123 Fixed error message for error 12135. Added names' reset on import error.
** SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
** performance.
** HC 20230116 Replaced some handle usages with the actual wrapped resources for
** performance.
** CA 20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
** HC 20230119 Removed the use of CaseInsensitiveString for CaseInsensitiveHashMap to improve
** performance.
** 009 OM 20230330 Added NPE guard for dataset:namespace-prefix.
** 010 IAS 20230330 Added support for recursive DATA-SET
** Fixed support for NESTED DATA-RELATIONs and multiple namespaces.
** Added support for relations with FOREIGN-KEY-HIDDEN = true.
** 011 IAS 20230425 Avoid NPE if the namespace is not specified.
** Skip "trivial" values of LABEL and COLUMN-LABEL.
** 20230501 Fixed support for the prodata:initial="prodata:unknown".
** 20230502 Added support for the "prodata:uniqueIndex" attribute of the "index" node.
** 012 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 013 IAS 20230519 Fixed support for DATASET:READ-XML from the XML containing embedded schema.
** 014 RAA 20230525 Creating a new BaseDataType instance is now done using BaseDataTypeFactory.
** 015 IAS 20230526 Extracted common logic to the 'Importer'.
** IAS 20230530 Added support for inferring DATASET/TEMP-TABLE structure from XML.
** 016 CA 20230620 Fixed a NPE when the XML has no namespace at the dataset.
** CA 20230630 'readTableSchema' must just verify the schema if the default buffer of the table already
** exists.
** 017 CA 20230724 Further reduce context-local usage.
** 018 CA 20230907 Added support for X-Document source. X-Noderef is not yet supported.
** 019 OM 20231027 Nested child tables are not required to be found after all fields from parent table.
** 020 OM 20230922 Added back filter of pseudo path ('.//') in the name setter of the child in a relation.
** 021 CA 20231208 Better detection of extent fields.
** 022 OM 20240118 Fixed the name of nested tables when reading dataset schema. Removed useless casts.
** 023 AI 20231221 Added support for single record syntax for READ-XML.
** AI 20240125 Added check for comments and whitespace while checking for single record syntax.
** 024 CA 20240201 If the XML row has fields which are not resolved to the current table, then silently
** ignore these fields.
** 025 DDF 20240415 Show error when importing an invalid xml file in a dataset.
** 026 CA 20240523 Fixed DATASET:WRITE-/READ-XML when before tables or (in)active relations are involved.
** 027 TJD 20240123 Java 17 compatibility updates
** 028 SP 20240627 Error 4065 must not set ERROR-STATUS:ERROR flag.
** 029 AS 20240802 Throw 13514 when dataset name and temp-table name is not found in
** the XML file in importDataset().
** 030 AS 20240801 Added external dataset schema support in importDataset().
** 031 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** 032 AS 20250204 Reduced the logging level to fine for 'unknown attribute' message.
** 033 SP 20250417 Row suffix is not mandatory for table rows when reading.
** 034 ES 20250327 Use seralizationName and xmlNodeName of the buffer, instead of temp-table.
** In case of TEMP-TABLE:readXml use attributes of the defaultBuffer.
** ES 20250328 Use the XML-NODE-NAME attribute of the buffer in DATASET:READ-XML.
*/
/*
** 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.io.*;
import java.lang.reflect.*;
import java.nio.file.*;
import java.util.*;
import java.util.Stack;
import java.util.function.*;
import java.util.logging.*;
import javax.xml.namespace.*;
import javax.xml.stream.*;
import org.apache.commons.lang3.tuple.*;
import com.ctc.wstx.api.WstxInputProperties;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.orm.Property;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.p2j.xml.*;
import com.goldencode.util.*;
import static com.goldencode.p2j.persist.serial.SerializeOptions.*;
/**
* An implementation of the 4GL READ-XML method, which parses an XML stream and reads the content
* into a temp-table.
* <p>
* Runtime support is partial at this time; not all modes and features are supported. PRODATASET
* is not supported at this time.
*/
public final class XmlImport
extends Importer
implements TableReader
{
/** Logger for objects of this type. */
private static final CentralLogger LOG = CentralLogger.get(XmlImport.class);
/** XSD schema element name */
private static final String ELEM_SCHEMA = "schema";
/** XSD import element name */
private static final String ELEM_IMPORT = "import";
/** Nil attribute name */
private static final String ATTR_NIL = "nil";
/** String constant for 'true' */
private static final String TEXT_TRUE = "true";
/** Verify schema mode. */
private final Verify verifySchemaMode;
/** Use CLOB/BLOB instead of character/raw type? (Value of {@code override-default-mapping} parameter). */
private final boolean useLobs;
/** The future namespace of the read dataset. */
private String dsNsUri = null;
/** The future namespace of the read temp-table. */
private String namespace = null;
/** The future namespace prefix of the read temp-table. */
private String ttPrefix = null;
/** The desired mapping of fields to new type, if one is defined. */
private final Map<String, String> fieldTypeMapping;
/** 'External' schema location, if one is provided. */
private final String schemaLocation;
/** Flag indicating that we're reading external schema */
private boolean external = false;
/** The mapping of the legacy table names by their (case-insensitive) XML name. */
private final Map<String, String> tablesByXmlName = new CaseInsensitiveLinkedHashMap<>();
/** Flag indicating that XML document contains embedded schema */
private boolean embeddedSchema = false;
/** The list of read relations of a dataset being read. */
private List<Relation> relations = null;
/** XML NS refix for NS by NS */
private Map<String, String> prefixByNs = new HashMap<>();
/** All unique indexes in this dataset, mapped by their unique names. */
private final Map<String, P2JIndex> uniqueIndexes = new HashMap<>();
/** The stream where the data is read from. */
private LookAheadXmlStreamReader reader = null;
/**
* The set of attributes of the current read XML element. Do NOT access it directly, instead
* use the {@code getXmlAttribute()} method.
*/
private Map<String, String> crtAttSet = null;
/**
* The location of the currently processed element. Used to check validation of the
* {@code crtAttSet}, which acts as a cache. If the position of the current XML element is
* different from {@code attrSetOffset} then {@code crtAttSet} needs to be updated.
*/
private int attrSetOffset = 0;
/**
* Flags whether the processed table has a known schema or it should be read from file.
* This is irrelevant when processing DataSets.
*/
private boolean knownTableSchema = true;
/** Flag indicating that the table/dataset structure should be inferred from the XML file. */
private boolean inferStructure = false;
/**
* Flag indicating that XML parsing will be self-checked
* (for debugging)
*/
private boolean trackXml = false;
/** Elements' stack */
private Stack<String> elements = new Stack<>();
/** Elements attributes' stack */
private Stack<Map<String, String>> attrs = new Stack<>();
/** Set of attributes and text of the field for single entry syntax. */
private Map<String, String> singleEntryAttr = new HashMap<>();
/** Flag indicating that XML contains row separators. */
private boolean singleEntry = false;
/**
* Constructor which configures the importer for an import of a Dataset or a TempTable/Buffer.
*
* @param source
* XML data source which normalizes access of various media to an input stream.
* @param readMode
* Read mode to determine how records are stored and how non-unique records are
* handled. Not currently honored, except temp-table records are deleted if mode
* is EMPTY.
* @param schemaLocation
* Path to XML schema (not currently used).
* @param overrideDefaultMapping
* Not currently used.
* @param fieldTypeMapping
* Not currently used.
* @param verifySchemaMode
* Not currently used.
*/
public XmlImport(SourceData source,
String readMode,
String schemaLocation,
boolean overrideDefaultMapping,
Map<String, String> fieldTypeMapping,
String verifySchemaMode)
{
super(source, readMode);
this.useLobs = overrideDefaultMapping;
this.verifySchemaMode = (verifySchemaMode == null) ? Verify.LOOSE
: Verify.valueOf(verifySchemaMode.toUpperCase());
this.fieldTypeMapping = fieldTypeMapping;
this.schemaLocation = schemaLocation;
}
/**
* Configures a dataset schema or validates it against the XML file. The other parameters are
* set in constructor.
*
* @param ds
* The destination for read schema.
*
* @return {@code true} on success. On failure, {@code false} is returned and the eventual
* error is already displayed.
*/
public boolean importDatasetSchema(DataSet ds)
{
if (!source.configureSupportedSources(SourceData.SD_READ_XMLSCHEMA, LegacyResource.DATASET + " widget"))
{
return false;
}
if (source.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
{
return readFromHandle((importer) -> importer.importDatasetSchema(ds));
}
this.ds = ds;
this.relations = new LinkedList<>();
processHiddenFk(ds);
try (Reader stream = openReader(source.getStream()))
{
reader = LookAheadXmlStreamReader.wrap(newXmlInputFactory().createXMLStreamReader(stream));
if (!readDatasetSchema(true))
{
ErrorManager.recordOrShowError(13033);
// Verify Temp-Table or dataset schema against XML Schema failed.
return false;
}
return true;
}
catch (IOException | XMLStreamException exc)
{
ds.resetNames();
return false;
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (XMLStreamException exc)
{
// TODO: log
}
reader = null;
}
this.ds = null;
this.relations = null;
}
}
/**
* Configures a temp-table schema or validates it against the XML file. The other parameters are set in
* the constructor.
*
* @param tt
* The destination for read schema.
*
* @return {@code true} on success. On failure, {@code false} is returned and the eventual error is
* already displayed.
*/
public boolean importTempTableSchema(AbstractTempTable tt)
{
if (!source.configureSupportedSources(SourceData.SD_READ_XMLSCHEMA,
LegacyResource.TEMP_TABLE + " widget"))
{
return false;
}
if (source.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
{
return readFromHandle((importer) -> importer.importTempTableSchema(tt));
}
this.ctx.tt = tt;
try (Reader stream = openReader(source.getStream()))
{
reader = LookAheadXmlStreamReader.wrap(newXmlInputFactory().createXMLStreamReader(stream));
if (!readTableSchema((String) null, null, null))
{
if (tt.prepared().toJavaType())
{
ErrorManager.recordOrShowError(13033);
// Verify Temp-Table or dataset schema against XML Schema failed.
}
else
{
ErrorManager.recordOrShowError(13032);
// Unable to create Temp-Table or dataset schema from XML Schema.
}
return false;
}
return true;
}
catch (IOException | XMLStreamException exc)
{
return false;
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (XMLStreamException exc)
{
// TODO: log
}
reader = null;
}
this.ctx.tt = null;
}
}
/**
* Imports a dataset. The other parameters are set in constructor.
*
* @param dataSet
* The destination for read data.
* @param loader
* A {@code BiConsumer} which helps loading a {@code Record} into the buffer.
*
* @return {@code true} on success. On failure, {@code false} is returned and the eventual
* error is already displayed.
*/
public boolean importDataset(DataSet dataSet, BiConsumer<RecordBuffer, Record> loader)
{
if (!source.configureSupportedSources(SourceData.SD_READ_XML, LegacyResource.DATASET + " widget"))
{
return false;
}
if (source.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
{
return readFromHandle((importer) -> importer.importDataset(dataSet, loader));
}
this.loader = loader;
this.ds = dataSet;
processHiddenFk(dataSet);
String dsName = null;
for (BufferImpl buf : dataSet.getBuffers())
{
if (buf.tableHandle()._isValid())
{
// this is a temp-table, populate the XML-NODE-NAME map
TempTable tt = buf.tableHandleNative();
tablesByXmlName.put(buf.getXmlNodeName().getValue(), tt.name().getValue());
}
}
this.relations = new LinkedList<>();
this.peerMapping = new HashMap<>();
if (readMode == Read.EMPTY)
{
dataSet.emptyDataset();
}
XMLInputFactory factory = newXmlInputFactory();
//If available, import schema from the schema file
if (schemaLocation != null && !schemaLocation.isEmpty())
{
try (SourceData schemaSource = new SourceData("file", schemaLocation))
{
this.embeddedSchema = true;
reader = LookAheadXmlStreamReader.wrap(factory.createXMLStreamReader(schemaSource.getStream()));
readDatasetSchema(false);
}
catch (IOException | XMLStreamException e)
{
ErrorManager.recordOrShowError(13033);
return false;
}
}
try (Reader stream = openReader(source.getStream()))
{
reader = LookAheadXmlStreamReader.wrap(factory.createXMLStreamReader(stream));
boolean isRoot = true;
String nsUri = null;
String nsPrefix = null;
boolean dataSetNameMatch = false;
while (reader.hasNext())
{
int type = reader.next();
String name = null;
switch (type)
{
case XMLStreamReader.START_ELEMENT:
name = reader.getLocalName();
switch (name)
{
case ELEM_SCHEMA:
// check "xmlns:xsd", "xmlns", and "xmlns:prodata" attributes
this.embeddedSchema = true;
prefixByNs.putAll(getXmlNamespaces());
dsNsUri = getXmlAttribute("targetNamespace");
List<BufferImpl> buffers = ds.getBuffers();
for (int nb = 0; nb < buffers.size(); nb++)
{
AbstractTempTable att = (AbstractTempTable)buffers.get(nb).tableHandleNative();
if (att == null)
{
continue;
}
String tableName = ((BufferImpl)att.defaultBufferHandleNative())._name();
tables.put(tableName, att);
String xmlNodeName = buffers.get(nb).getXmlNodeName().getValue();
tablesByXmlName.put(xmlNodeName == null ? tableName : xmlNodeName, tableName);
}
// let the schema load them
// tablesByXmlName.clear();
// the [schema] node is lost at this moment and "xmlns:xsd", "xmlns", and
// "xmlns:prodata" attributes might not be accessible
if (!readDatasetSchema(false))
{
return false;
}
break;
case "datasetChanges":
if (!readDatasetData())
{
return false;
}
break;
default:
if (isRoot)
{
isRoot = false;
dsName = name;
nsUri = reader.getNamespaceURI();
nsPrefix = reader.getPrefix();
dataSetNameMatch = ds._name().equals(dsName) ||
ds.getSerializeName().getValue().equals(dsName) ||
ds.getXmlNodeName().getValue().equals(dsName);
}
else
{
String plainName = tablesByXmlName.get(name);
// data located in root node instead of "datasetChanges" node
int numBuf = ds.numBuffers().intValue();
BufferImpl buffer = null;
boolean found = false;
for (int i = 1; i <= numBuf; i++)
{
buffer = ds.getBufferByIndex(i);
if (buffer.getXmlNodeName().toStringMessage().equalsIgnoreCase(name) ||
buffer.name().toStringMessage().equalsIgnoreCase(name))
{
found = true;
break;
}
else if (plainName != null && plainName.equals(buffer.name().toStringMessage()))
{
found = true;
break;
}
}
if (buffer != null)
{
boolean tableNameMatch = buffer._name().equals(plainName) ||
buffer.getSerializeName().getValue().equals(plainName) ||
buffer.getXmlNodeName().getValue().equals(plainName);
if (!tableNameMatch && !dataSetNameMatch)
{
ErrorManager.recordOrThrowError(13514, "Dataset", ds._name(), ds.getNsUri());
return false;
}
}
if (!found)
{
AbstractTempTable att = tables.get(name);
if (att != null)
{
found = true;
buffer = (BufferImpl)att.defaultBufferHandleNative();
}
}
if (!found)
{
if (numBuf == 0)
{
inferStructure = true;
reader.inferMode(true);
ctx.ttName = name;
ctx.tt = new TempTableBuilder();
String ttNsUri = reader.getNamespaceURI();
String ttNsPrefix = reader.getPrefix();
if (ttNsUri != null)
{
ctx.tt.namespaceURI(ttNsUri);
}
if (ttNsPrefix != null)
{
ctx.tt.namespacePrefix(ttNsPrefix);
}
if (!parseRecord(null))
{
return false;
}
break;
}
// if the table is not found in database, the record is simply skipped
if (!skipRecord(name))
{
return false;
}
break;
}
TemporaryBuffer ttBuffer = (TemporaryBuffer) buffer.buffer();
if (readMode == Read.REPLACE && !readingBefore)
{
// the REPLACE mode needs primary unique index declared for the after table
P2JIndex primaryIndex = ttBuffer.getDmoInfo().getPrimaryIndex(true);
if (primaryIndex == null)
{
ErrorManager.recordOrShowError(13063, ttBuffer.getDmoInfo().legacyTable);
// REPLACE mode requires a unique primary index in the target table <table>.
return false;
}
}
// ctx.tt = ttBuffer.getParentTable();
ctx.setBuffer(ttBuffer, false);
if (!readRecord())
{
return false;
}
}
break;
}
break;
case XMLStreamReader.END_ELEMENT:
if (inferStructure)
{
if (nsUri != null)
{
ds.namespaceURI(nsUri);
}
if (nsPrefix != null)
{
ds.namespacePrefix(nsPrefix);
}
setupDataset(dsName);
}
break; // all OK, should return but let the parser check the rest of the input
}
}
}
catch (XMLStreamException xexc)
{
ErrorManager.recordOrShowError(13035, source.getFileName(), "");
// Error reading XML file '<file>'.
int line = xexc.getLocation().getLineNumber();
int col = xexc.getLocation().getColumnNumber();
String err = "FATAL ERROR: file '" + source.getFileName() + "', line '" + line +
"', column '" + col + "', message '" + xexc.getMessage().replace('\n', ' ') + "'";
ErrorManager.recordOrShowError(13064, err, "");
// READ-XML encountered an error while parsing the XML Document: <error>. (13064)
return false;
}
catch (IOException | PersistenceException exc)
{
return false;
}
catch (Throwable exc)
{
// intercept all exceptions
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE, exc.getMessage() + reader.getLocation(), exc);
}
// the rethrow it back
throw exc;
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (XMLStreamException exc)
{
// TODO: log
}
reader = null;
}
}
return true;
}
/**
* Reads a TEMP-TABLE from the configured source.
*
* @param tempTable
* Temp-table in which data will be stored.
* @param loader
* A {@code BiConsumer} which helps loading a {@code Record} into the buffer.
*
* @return {@code true} on success.
*/
public boolean importTable(Buffer buffer,
BiConsumer<RecordBuffer, Record> loader)
{
AbstractTempTable tempTable = ((BufferReference) buffer).buffer().getParentTable();
if (!source.configureSupportedSources(SourceData.SD_READ_XML, LegacyResource.TEMP_TABLE + " widget"))
{
return false;
}
if (source.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
{
return readFromHandle((importer) -> importer.importTable(buffer, loader));
}
String nsPrefix = null;
String nsURI = null;
knownTableSchema = !tempTable._dynamic() || tempTable._prepared();
this.loader = loader;
if (knownTableSchema)
{
BufferImpl defaultBuffer = (BufferImpl) tempTable.defaultBufferHandleNative();
ctx.setBuffer(defaultBuffer.buffer(), false);
if (readMode == Read.EMPTY)
{
ctx.proxy.deleteAll();
}
}
else if (schemaLocation != null)
{
XmlImport xmlImport = new XmlImport(this.source, null, null, this.useLobs,
this.fieldTypeMapping, this.verifySchemaMode.toString());
boolean b = xmlImport.importTempTableSchema(tempTable);
if (!b)
{
return false;
}
}
XMLInputFactory factory = newXmlInputFactory();
try (Reader stream = openReader(source.getStream()))
{
reader = LookAheadXmlStreamReader.wrap(factory.createXMLStreamReader(stream));
String tableName = ctx.schema == null ? null : ctx.schema.getTableName();
boolean isRoot = true;
boolean rowSeparator = false;
while (reader.hasNext())
{
int type = reader.next();
switch (type)
{
case XMLStreamReader.START_ELEMENT:
if (isRoot)
{
nsPrefix = reader.getPrefix();
nsURI = reader.getNamespaceURI();
if (tableName == null && tempTable.getFields().isEmpty())
{
inferStructure = true;
tableName = reader.getLocalName();
tempTable.setXmlNodeName(tableName);
if (nsURI != null)
{
tempTable.namespaceURI(nsURI);
}
if (nsPrefix != null)
{
tempTable.namespacePrefix(nsPrefix);
}
}
for (int i = 0; i < reader.getAttributeCount(); i++)
{
singleEntryAttr.put(reader.getAttributeLocalName(i), reader.getAttributeValue(i));
}
isRoot = false;
break;
}
String name = reader.getLocalName();
if (ELEM_SCHEMA.equals(name))
{
if (!readTableSchema(tempTable, nsPrefix, nsURI))
{
ErrorManager.recordOrShowError(13033);
// Verify Temp-Table or dataset schema against XML Schema failed.
return false;
}
//if (tableName == null)
{
tableName = ctx.schema.getTableName();
}
break;
}
if (tableName == null)
{
ErrorManager.recordOrShowError(0, "Unsupported: Unable to read XML without schema in a dynamic unprepared TEMP-TABLE", false);
// TODO: handle this error
return false;
}
boolean comments = false;
String singleAttr = "";
while (reader.hasNext() && reader.peek() != XMLStreamReader.START_ELEMENT && reader.peek() != XMLStreamReader.END_ELEMENT)
{
type = reader.next();
if (type == XMLStreamReader.COMMENT)
{
singleAttr = "";
comments = true;
}
else if (type == XMLStreamReader.CHARACTERS && !comments)
{
singleAttr = reader.getText();
}
}
singleEntryAttr.put(name, singleAttr);
ctx.ttName = tableName;
String rowName = buffer.getXmlNodeName().toJavaType();
rowName = rowName + "Row";
if (rowName.equalsIgnoreCase(name) && reader.peek() == XMLStreamReader.START_ELEMENT)
{
rowSeparator = true;
if (!readRecord())
{
return false;
}
}
else
{
if (reader.peek() == XMLStreamReader.START_ELEMENT)
{
while (reader.hasNext())
{
rowSeparator = true;
type = reader.next();
if (type == XMLStreamReader.END_ELEMENT && name.equals(reader.getLocalName()))
{
break;
}
}
}
}
break;
case XMLStreamReader.END_ELEMENT:
{
if (reader.getLocalName().equalsIgnoreCase(tempTable.getXmlNodeName().toJavaType()))
{
if (!rowSeparator && !singleEntryAttr.isEmpty())
{
singleEntry = true;
return readRecord();
}
else
{
return true;
}
}
}
}
}
}
catch (XMLStreamException xexc)
{
ErrorManager.recordOrShowError(13035, source.getFileName(), "");
// Error reading XML file '<file>'.
int line = xexc.getLocation().getLineNumber();
int col = xexc.getLocation().getColumnNumber();
String err = "FATAL ERROR: file '" + source.getFileName() + "', line '" + line +
"', column '" + col + "', message '" + xexc.getMessage().replace('\n', ' ') + "'";
ErrorManager.recordOrShowError(13064, err, "");
// READ-XML encountered an error while parsing the XML Document: <error>. (13064)
return false;
}
catch (PersistenceException pexc)
{
// intercept all exceptions
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE, pexc.getMessage() + reader.getLocation(), pexc);
}
return false;
}
catch (IOException exc)
{
// this is a bit strange:
ErrorManager.recordOrShowError(4065, false, "READ-XML", "TEMP-TABLE widget");
// **The <attribute> attribute on the <widget id> has invalid arguments.
return false;
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (XMLStreamException exc)
{
// TODO: log
}
reader = null;
}
}
return true;
}
/**
* Read XML/JSON data from the input source and store it in the just created empty TEMP-TABLE.
*
* @param tableName
* table name
* @param table
* TEMP-TABLE to be loaded.
* @param loader
* A {@code BiConsumer} which helps loading a {@code Record} into the buffer.
*
* @return {@code true} on success.
*
* @throws PersistenceException
* if there is any error reading or storing XML/JSON data.
*/
public boolean readTable(String tableName, TempTableBuilder table, BiConsumer<RecordBuffer, Record> loader)
throws PersistenceException
{
Buffer parent = ctx.proxy;
pushContext();
this.ctx.tt = table;
this.ctx.ttName = tableName;
this.tables.put(tableName, table);
String ttNsUri = reader.getNamespaceURI();
String ttNsPrefix = reader.getPrefix();
if (ttNsUri != null)
{
ctx.tt.namespaceURI(ttNsUri);
}
if (ttNsPrefix != null)
{
ctx.tt.namespacePrefix(ttNsPrefix);
}
char groupSep = decimal.getGroupSeparator();
char decSep = decimal.getDecimalSeparator();
try
{
if (!setParentValues(tableName))
{
return false;
}
if (ctx.ttName.length() > 32 )
{
ErrorManager.recordOrShowError(
new int[] {13116, 13040},
new String[] {
"Temp-Table or dataset name is limited to 32 characters",
"Unable to infer Temp-Table or dataset schema from XML Data"
},
false, // modal
true, // prefix
false, // isError
false, // asMsg
true // addDot
);
return false;
}
if (!readRecord(parent))
{
return false;
}
}
catch (XMLStreamException exc)
{
throw new PersistenceException(exc);
}
finally
{
decimal.setNumericFormat(decSep, groupSep);
popContext();
}
return false;
}
/**
* Get an inferred type of the value.
*
* @param val
* value to be analyzed
*
* @return an inferred type of the value.
*
* @throws PersistenceException
* on invalid value type
*/
@Override
protected ParmType typeOf(Object val) throws PersistenceException
{
return ParmType.CHAR;
}
/**
* Import from a handle. Only {@link XDocument X-Document} is currently supported.
*
* @param reader
* The reader function.
*
* @return <code>true</code> if the import was successful.
*/
private boolean readFromHandle(Function<XmlImport, Boolean> reader)
{
longchar lc = new longchar("");
XDocumentImpl xdoc = (XDocumentImpl) ((handle) source.getSource()).get();
boolean[] res = new boolean[1];
ErrorManager.nestedSilent(() -> res[0] = xdoc.save("longchar", lc).booleanValue());
if (!res[0])
{
return false;
}
SourceData sd = new SourceData("longchar", lc);
XmlImport importer = new XmlImport(sd,
readMode.toString(),
schemaLocation,
useLobs,
fieldTypeMapping,
verifySchemaMode.toString());
return reader.apply(importer);
}
/**
* Create a {@link Reader} which has the encoding as specified in the XML file targeted by this stream.
* <p>
* If no encoding is specified, it defaults to UTF-8.
* <p>
* The reader is wrapped in a filter which automatically escapes ISO control characters which are not
* whitespace.
*
* @param stream
* input stream.
* @return A reader for this stream with the proper encoding.
*/
private Reader openReader(InputStream stream)
throws IOException
{
String encoding = XmlHelper.readXMLEncoding(stream);
if (encoding == null)
{
// always default to UTF-8
encoding = "UTF-8";
}
Reader reader = new InputStreamReader(stream, encoding);
return new EscapeControlCharsReader(reader);
}
/**
* Create a factory which treats XML 1.0 files as XML 1.1.
*
* @return A factory to read XML 1.0 files as XML 1.1, to allow control characters.
*/
private XMLInputFactory newXmlInputFactory()
{
XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setProperty(WstxInputProperties.P_ALLOW_XML11_ESCAPED_CHARS_IN_XML10, true);
return factory;
}
/**
* Reads and processes a {@code TEMP-TABLE} schema.
*
* @param nsPrefix
* The detected XML namespace prefix.
* @param nsUri
* The detected XML namespace URI.
*
* @return {@code true} on success and {@code false} otherwise. When {@code false} is returned
* then the error message has already been dispatched to {@code ErrorManager}.
*
* @throws XMLStreamException
* If a XML-related error occurs (like an unexpected end of stream).
*/
private boolean readTableSchema(AbstractTempTable tempTable, String nsPrefix, String nsUri)
throws XMLStreamException
{
TempTableBuilder ttb = tempTable._dynamic() ? (TempTableBuilder) tempTable : null;
String legacyTableName = null;
String xmlName = null;
String undoable = null;
List<String> readFields = new LinkedList<>();
while (reader.hasNext())
{
int type = reader.next();
String name = null;
switch (type)
{
case XMLStreamReader.START_ELEMENT:
if (verifySchemaMode == Verify.IGNORE && !tempTable._dynamic())
{
// for static temp-tables, IGNORE mode will ignore all schema nodes
break;
}
name = reader.getLocalName();
// index properties:
String indexName = null;
boolean primary = false;
boolean unique = false;
boolean word = false;
switch (name)
{
case "element":
if (Boolean.parseBoolean(getXmlAttribute("prodata:proTempTable")))
{
// this is the table definition
legacyTableName = getXmlAttribute("prodata:tableName");
xmlName = getXmlAttribute("name");
undoable = getXmlAttribute("prodata:undo");
if (ttb != null)
{
tables.put(xmlName, ttb);
tablesByXmlName.put(xmlName + "Row", xmlName);
}
break;
}
String fieldType = getXmlAttribute("type");
if (fieldType != null)
{
String fieldName = getXmlAttribute("name");
String extentMax = getXmlAttribute("maxOccurs");
String ablDataType = getXmlAttribute("prodata:dataType");
String nullable = getXmlAttribute("nullable");
String defaultStrVal = getXmlAttribute("default");
String decimals = getXmlAttribute("prodata:decimals");
String format = getXmlAttribute("prodata:format");
String cs = getXmlAttribute("prodata:caseSensitive");
String optional = getXmlAttribute("nillable");
// this a field definition
String ablType = get4GLType(
fieldType, ablDataType, nullable, useLobs, legacyTableName, fieldName);
if (ttb != null)
{
ParmType parmType = TempTableBuilder.validateDatatype(ablType);
if (parmType == null)
{
ErrorManager.recordOrShowError(13201, ablType, fieldName);
// Invalid data type override '<type>' for field '<field>'.
ErrorManager.recordOrShowError(13032);
// Unable to create Temp-Table or dataset schema from XML Schema.
return false;
}
if (format == null)
{
format = TempTableBuilder.getDefaultDataTypeFormat(ablType);
}
long extent = extentMax == null ? 0 : Long.parseLong(extentMax);
BaseDataType defVal = null;
if (BaseDataType.class.isAssignableFrom(parmType.getParamType()) &&
defaultStrVal != null)
{
try
{
Constructor<? extends BaseDataType> ctor =
(Constructor<? extends BaseDataType>)
parmType.getParamType().getConstructor(String.class);
defVal = ctor.newInstance(defaultStrVal);
}
catch (ReflectiveOperationException e)
{
return false;
}
}
ttb.addField(new P2JField(fieldName, parmType, extent, format, defVal, null, null,
Boolean.parseBoolean(cs), null, null, false, null,
null, null, null, !Boolean.parseBoolean(optional),
decimals == null ? 0 : Integer.parseInt(decimals), 0));
}
else if (verifySchemaMode != Verify.IGNORE)
{
TableMapper.LegacyFieldInfo lfi = TableMapper.getLegacyFieldInfo(
tempTable, fieldName);
if (lfi == null)
{
if (verifySchemaMode == Verify.STRICT)
{
ErrorManager.recordOrShowError(13091, legacyTableName, fieldName);
return false;
}
}
else if (!Util.checkType(lfi.getDataType(), fieldType, ablDataType) ||
!Util.checkExtent(lfi.getExtent(), extentMax) ||
!Util.checkMandatory(lfi.isMandatory(), nullable))
{
ErrorManager.recordOrShowError(13089, fieldName, legacyTableName);
return false;
}
readFields.add(fieldName);
}
}
break;
case ELEM_SCHEMA:
// check "xmlns:xsd", "xmlns", and "xmlns:prodata" attributes
break;
case "unique":
// found an unique index
indexName = getXmlAttribute("prodata:indexName");
primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, true, word, false))
{
return false;
}
break;
case "index":
// found an non-unique index
indexName = getXmlAttribute("prodata:indexName");
primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
unique = Boolean.parseBoolean(getXmlAttribute("prodata:uniqueIndex"));
word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, unique, word, true))
{
return false;
}
break;
case "complexType":
case "sequence":
case "annotation":
case "appinfo":
// tolerate them, nothing to do ta this time
break;
}
break;
case XMLStreamReader.END_ELEMENT:
name = reader.getLocalName();
if (ELEM_SCHEMA.equals(name))
{
if (legacyTableName == null || legacyTableName.isEmpty())
{
legacyTableName = xmlName;
}
if (ttb == null)
{
if (verifySchemaMode == Verify.STRICT)
{
List<TableMapper.LegacyFieldInfo> allFields =
TableMapper.getLegacyOrderedList(tempTable.defaultBufferHandleNative(), false);
for (TableMapper.LegacyFieldInfo field : allFields)
{
String fwdName = field.getLegacyName();
if (!readFields.remove(fwdName))
{
ErrorManager.recordOrShowError(13090, fwdName, legacyTableName);
// XML Schema definition for field '<field>' in table '<table>' not found.
return false;
}
}
}
return true;
}
if (!ttb._prepared())
{
ttb.setCanUndo(new logical(Boolean.parseBoolean(undoable)));
String b4table = b4tables.get(legacyTableName);
boolean ok = ttb.tempTablePrepare(legacyTableName, b4table);
if (!ok)
{
// TODO: report error? already reported?
return false;
}
ttb.setXmlNodeName(xmlName);
ttb.namespaceURI(nsUri);
ttb.namespacePrefix(nsPrefix);
BufferImpl defaultBuffer = (BufferImpl) ttb.defaultBufferHandleNative();
ctx.setBuffer(defaultBuffer.buffer(), false);
}
return true;
}
break;
}
}
throw new XMLStreamException("Unexpected EOS in readTableSchema().", reader.getLocation());
}
/**
* Reads and processes a {@code DataSet} schema. If the {@code DataSet} is in CLEAR state, the
* structure is initialized and its tables and relations are set up. Otherwise the schema is
* checked according to {@code verifySchemaMode}.
*
* @param verify
* When {@code true} the schema is only verified. Otherwise the methods attempts to create the
* read schema into the dataset.
* @return {@code true} on success and {@code false} otherwise. When {@code false} is returned
* then the error message has already been dispatched to {@code ErrorManager}.
*
* @throws XMLStreamException
* If a XML-related error occurs (like an unexpected end of stream).
*/
private boolean readDatasetSchema(boolean verify)
throws XMLStreamException
{
Relation keyrefRel = null;
String nsPrefix = null;
while (reader.hasNext())
{
int type = reader.next();
String name = null;
switch (type)
{
case XMLStreamReader.START_ELEMENT:
if (verifySchemaMode == Verify.IGNORE && !ds._dynamic())
{
// for static datasets, IGNORE mode will ignore all schema nodes
break;
}
name = reader.getLocalName();
// index properties:
String indexName = null;
boolean primary = false;
boolean unique = false;
boolean word = false;
switch (name)
{
case "element":
boolean isDS = Boolean.parseBoolean(getXmlAttribute("prodata:proDataSet"));
if (isDS)
{
String datasetName = getXmlAttribute("prodata:datasetName");
if (datasetName == null)
{
// if dedicated attribute not found, use the xml name attribute
datasetName = getXmlAttribute("name");
}
if (ds._dynamic() && (!embeddedSchema || ds.getBuffers().isEmpty()))
{
ds.name(datasetName);
nsPrefix = getXmlAttribute("prodata:prefix");
if (nsPrefix != null)
{
ds.namespacePrefix(nsPrefix);
}
if (dsNsUri != null)
{
ds.namespaceURI(dsNsUri);
}
}
else if (!ds._dynamic() &&
(!ds._name().equals(datasetName) ||
(dsNsUri != null && !ds.getNsUri().equals(dsNsUri))))
{
ErrorManager.recordOrShowWarning(13514, "Dataset", ds._name(), ds.getNsUri());
// <Temp-Table or Dataset> name '<name>' in namespace '<namespace>' not found in XML Document.
return false;
}
// else simply ignore it (?)
}
else
{
String tableName = getXmlAttribute("prodata:tableName");
AbstractTempTable att = null;
String tableRef = null;
if (tableName == null)
{
// if dedicated attribute not found, use the xml name attribute
tableName = getXmlAttribute("name");
}
if (tableName == null)
{
tableRef = getXmlAttribute("ref");
att = tables.get(removeNsPrefix(tableRef));
if (att == null)
{
ErrorManager.recordOrShowError(
new int[] {13135, 13032},
new String[] {
"Unable to find element or attribute definition for '" +
removeNsPrefix(tableRef) + "'",
"Unable to create Temp-Table or dataset schema from XML Schema"
},
// modal, prefix, isError, asMsg, addDot
false, false, false, false, true);
throw new XMLStreamException("Unknown table reference: ["
+ tableRef + "]");
}
tableName = removeNsPrefix(tableRef);
}
handle buffer = ds.bufferHandle(tableName);
if (verifySchemaMode == Verify.STRICT && verify && !buffer._isValid())
{
// TODO: 4GL will test the other way around: collects all table definitions from
// file and attempt to match dataset in that collection, throwing 10779
// errors first. Then the difference is computed and 13138 is eventually raised
ErrorManager.recordOrShowError(13138, tableName);
// Temp-table '<table>' not found in XML Schema
return false;
}
if (!readTableSchema(tableName, null))
{
return false;
}
}
break;
case ELEM_IMPORT:
if (ds != null)
{
String schemaLocation = getXmlAttribute("schemaLocation");
String extPath = !Serializator.TYPE_FILE.equalsIgnoreCase(source.getType())
? null
: Paths.get(source.getFileName()).getParent().toString() + "/" + schemaLocation;
if (extPath == null)
{
ErrorManager.recordOrShowError(
new int[] {293, 13101, 13032},
new String[] {
"** \"" + schemaLocation + "\" was not found",
"Unable to load imported or included schema: " + schemaLocation,
"Unable to create Temp-Table or dataset schema from XML Schema"
},
// modal, prefix, isError, asMsg, addDot
false, false, false, false, true);
throw new XMLStreamException("Failed to import schema " + schemaLocation);
}
String ns = getXmlAttribute("namespace");
XmlImport ext = new XmlImport(new SourceData("file", extPath),
null, schemaLocation, false, null, null);
ext.external = true;
ext.namespace = ns;
ext.ds = ds;
TempTableBuilder ttb = new TempTableBuilder();
if (!ext.importTempTableSchema(ttb))
{
return false;
}
tables.putAll(ext.tables);
tablesByXmlName.putAll(ext.tablesByXmlName);
}
break;
case ELEM_SCHEMA:
Map<String, String> attrs = getXmlAttributes();
prefixByNs.putAll(getXmlNamespaces());
// check "xmlns:xsd", "xmlns", and "xmlns:prodata" attributes
String ns = getXmlAttribute("targetNamespace");
if (ns != null)
{
ds.namespaceURI(ns);
}
break;
case "unique":
// found an unique index
indexName = getXmlAttribute("prodata:indexName");
primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, true, word, false))
{
return false;
}
break;
case "index":
// found an non-unique index
indexName = getXmlAttribute("prodata:indexName");
primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
unique = Boolean.parseBoolean(getXmlAttribute("prodata:uniqueIndex"));
word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, unique, word, true))
{
return false;
}
break;
case "keyref":
if (keyrefRel != null)
{
throw new XMLStreamException(
"Unexpected nested xsd:keyref in readDatasetSchema().", reader.getLocation());
}
// found a relation based on a unique index
indexName = getXmlAttribute("refer");
P2JIndex uidx = uniqueIndexes.get(removeNsPrefix(indexName));
if (uidx == null)
{
throw new XMLStreamException(
"Failed to locate unique index '" + indexName + "' in readDatasetSchema().",
reader.getLocation());
}
boolean nested = Boolean.parseBoolean(getXmlAttribute("prodata:nested"));
boolean fkHidden = Boolean.parseBoolean(getXmlAttribute("prodata:foreignKeyHidden"));
boolean recursive = Boolean.parseBoolean(getXmlAttribute("prodata:recursive"));
boolean active = !Boolean.parseBoolean(getXmlAttribute("prodata:notActive"));
keyrefRel = new Relation(getXmlAttribute("name"), uidx, nested, fkHidden, recursive, active);
relations.add(keyrefRel);
break;
case "relation":
// found a relation
relations.add(new Relation(
getXmlAttribute("name"),
getXmlAttribute("prodata:parent"),
getXmlAttribute("prodata:child"),
getXmlAttribute("prodata:relationFields"),
Boolean.parseBoolean(getXmlAttribute("prodata:nested")),
Boolean.parseBoolean(getXmlAttribute("prodata:foreignKeyHidden")),
Boolean.parseBoolean(getXmlAttribute("prodata:recursive")),
!Boolean.parseBoolean(getXmlAttribute("prodata:notActive"))
));
break;
case "selector":
if (keyrefRel != null)
{
keyrefRel.setChild(removeNsPrefix(getXmlAttribute("xpath")));
}
// otherwise, it is not related to unique index based relation
break;
case "field":
if (keyrefRel != null)
{
keyrefRel.addChildField(removeNsPrefix(getXmlAttribute("xpath")));
}
// otherwise, it is not related to unique index based relation
break;
case "complexType":
case "sequence":
case "annotation":
case "appinfo":
// tolerate them, nothing to do at this time
break;
}
break;
case XMLStreamReader.END_ELEMENT:
name = reader.getLocalName(); // the name of the element being closed
switch(name)
{
case ELEM_SCHEMA:
processDsBuffers();
int numTables = ds.numBuffers().intValue();
if (verifySchemaMode == Verify.STRICT)
{
// be sure all tables from ds were read from the file and that their
// fields are matching each other:
for (int k = 0; k < numTables; k++)
{
BufferImpl buff = ds.getBufferByIndex(k + 1);
TempTableSchema schema = new TempTableSchema(buff.buffer(), false);
String xmlName = schema.getTableName();
if (xmlName != null && !xmlName.equals(buff.doGetName()))
{
xmlName = tablesByXmlName.get(xmlName);
}
AbstractTempTable att = tables.get(xmlName);
if (att == null)
{
ErrorManager.recordOrShowError(13079, xmlName);
// XML Schema definition for temp-table '<table>' not found.
return false;
}
}
// check the other way around: all read tables must be present in DS:
for (Map.Entry<String, AbstractTempTable> entry : tables.entrySet())
{
String dstt = entry.getKey();
if (!ds.bufferHandle(dstt)._isValid())
{
ErrorManager.recordOrShowError(13083, dstt);
// XML Schema contains extra temp-table definition '<table>'.
return false;
}
}
// now check the relations: the problem is the relation name is irrelevant,
// we need to do a parent/child search
int dsRelCnt = ds.getNumRelations().intValue();
List<XmlImport.Relation> relationsCopy = new ArrayList<>(relations);
for (int k = 0; k < dsRelCnt; k++)
{
boolean found = false;
DataRelation dsRel = ds.getRelationNative(k + 1);
BufferImpl chBuff = dsRel.getChildBufferNative();
BufferImpl paBuff = dsRel.getParentBufferNative();
for (Relation thisRel : relations)
{
if (chBuff.doGetName().equalsIgnoreCase(thisRel.child) &&
paBuff.doGetName().equalsIgnoreCase(thisRel.parent))
{
if (!dsRel.getRelationFields().toJavaType().equalsIgnoreCase(thisRel.getFields()))
{
ErrorManager.recordOrShowError(13081, dsRel.name().toJavaType());
// XML Schema definition for data-relation between '<table>' and '<table>' not found.
return false;
}
found = true;
relationsCopy.remove(thisRel);
}
}
if (!found)
{
ErrorManager.recordOrShowError(13082, paBuff.doGetName(), chBuff.doGetName());
// XML Schema definition for data-relation between '<table>' and '<table>' not found.
return false;
}
}
// now check the other way around:
if (!relationsCopy.isEmpty())
{
ErrorManager.recordOrShowError(13086, relationsCopy.get(0).name);
// XML Schema contains extra data-relation definition '<relation>'. ()
return false;
}
}
if (!ds._dynamic())
{
// in non-STRICT mode: schema is used only for dynamic datasets in CLEAR
// state to populate them update the tablesByXmlName map:
// tablesByXmlName.clear();
for (int k = 0; k < numTables; k++)
{
BufferImpl buff = ds.getBufferByIndex(k + 1);
TempTableSchema schema = new TempTableSchema(buff.buffer(), false);
String xmlName = schema.getTableName();
if (xmlName != null && !xmlName.equals(buff.doGetName()))
{
tablesByXmlName.put(xmlName, buff.doGetName());
}
}
return true;
}
for (Map.Entry<String, AbstractTempTable> entry : tables.entrySet())
{
AbstractTempTable att = entry.getValue();
if (att instanceof StaticTempTable)
{
continue;
}
TempTableBuilder ttb = (TempTableBuilder) att;
if (ttb._prepared())
{
continue;
}
String tableName = entry.getKey();
String b4table = b4tables.get(tableName);
boolean ok = ttb.tempTablePrepare(tableName, b4table);
if (ok)
{
// set them in dataset:
BufferImpl defBuffer = (BufferImpl) ttb.defaultBufferHandleNative();
if (!ds.addBuffer(defBuffer).getValue())
{
// TODO: report error? already reported?
return false;
}
}
else
{
// TODO: report error? already reported?
return false;
}
}
if (ds._dynamic() && verifySchemaMode != Verify.STRICT) // if (!verify)
{
// add the relations, if not already added (?)
for (Relation rel : relations)
{
handle hRel = ds.addRelationImpl(
ds.bufferHandle(rel.parent), ds.bufferHandle(rel.child),
new character(rel.getFields()),
null, //reposition
new logical(rel.nested),
null, // active
new logical(rel.recursive),
new logical(rel.fkHidden),
true);
if (hRel == null)
{
continue; // duplicate relation
}
DataRelation relation = (DataRelation) hRel.getResource();
if (relation != null)
{
relation.name(rel.name);
relation.setNested(rel.nested);
relation.setActive(rel.active);
}
}
}
// we can return now, the schema is fully read
return true;
case "keyref":
if (keyrefRel == null)
{
throw new XMLStreamException(
"Unexpected end of node keyref in readDatasetSchema().", reader.getLocation());
}
keyrefRel = null;
break;
}
break;
}
}
throw new XMLStreamException("Unexpected EOS in readDatasetSchema().", reader.getLocation());
}
/**
* Remove the namespace prefix from the qualified name
*
* @param qname
* qualified name to be simplified
*
* @return name w/o prefix
*/
private String removeNsPrefix(String qname)
{
int pos = qname.indexOf(':');
return pos > 0 ? qname.substring(pos + 1) : qname;
}
/**
* Process buffers for the imported dataset tables.
*/
private void processDsBuffers()
{
if (!ds._dynamic())
{
return;
}
BufferImpl[] dsBuffers = new BufferImpl[tables.size()];
int nb = 0;
int np = 0;
for (Map.Entry<String, AbstractTempTable> e: tables.entrySet())
{
if (e.getValue()._prepared())
{
dsBuffers[nb++] = (BufferImpl)e.getValue().defaultBufferHandleNative();
}
else
{
np++;
TempTableBuilder ttb = (TempTableBuilder)e.getValue();
String tableName = e.getKey();
String b4table = b4tables.get(tableName);
ttb.tempTablePrepare(tableName, b4table);
dsBuffers[nb++] = (BufferImpl)ttb.defaultBufferHandleNative();
}
}
if (np > 0)
{
ds.setBuffers(dsBuffers);
}
}
/**
* Reads and processes an {@code AbstractTempTable} schema. If the {@code TempTable} is in CLEAR state, the
* structure is initialized. Otherwise the schema is checked according to {@code verifySchemaMode}.
*
* @param xmlNodeName
* The detected XML node name.
* @param nsPrefix
* The detected XML namespace prefix.
* @param nsUri
* The detected XML namespace URI.
*
* @return {@code true} on success and {@code false} otherwise. When {@code false} is returned
* then the error message has already been dispatched to {@code ErrorManager}.
*
* @throws XMLStreamException
* If a XML-related error occurs (like an unexpected end of stream).
*/
private boolean readTableSchema(String xmlNodeName, String nsPrefix, String nsUri)
throws XMLStreamException
{
boolean undo = false;
while (reader.hasNext())
{
int type = reader.next();
String name = null;
switch (type)
{
case XMLStreamReader.START_ELEMENT:
if (verifySchemaMode == Verify.IGNORE && !ds._dynamic())
{
// for static datasets, IGNORE mode will ignore all schema nodes
break;
}
name = reader.getLocalName();
if (trackXml)
{
elements.push(name);
attrs.push(getXmlAttributes());
}
// index properties:
String indexName = null;
boolean primary = false;
boolean unique = false;
boolean word = false;
switch (name)
{
case "element":
boolean isTT = Boolean.parseBoolean(getXmlAttribute("prodata:proTempTable"));
if (isTT || external)
{
String tableName = getXmlAttribute("prodata:tableName");
if (tableName == null)
{
// if dedicated attribute not found, use the xml name attribute
tableName = getXmlAttribute("name");
}
else if (xmlNodeName == null && !tableName.equals(getXmlAttribute("name")))
{
xmlNodeName = getXmlAttribute("name");
}
if (nsPrefix == null)
{
nsPrefix = getXmlAttribute("prodata:prefix");
}
undo = Boolean.parseBoolean(getXmlAttribute("prodata:undo"));
AbstractTempTable ctt = null;
if (ctx.ttName != null && !tableName.equals(ctx.ttName))
{
ctt = ctx.tt;
ctx.tt = null;
}
ctx.ttName = tableName;
ttPrefix = nsPrefix;
try
{
if (!readTableSchema(tableName, nsPrefix))
{
return false;
}
}
finally
{
if (ctt != null)
{
ctx.tt = ctt;
}
}
}
else
{
// what? log something?
return false;
}
break;
case ELEM_SCHEMA:
if (nsUri == null)
{
nsUri = getXmlAttribute("targetNamespace");
}
// check "xmlns:xsd", "xmlns", and "xmlns:prodata" attributes
break;
case "unique":
// found an unique index
indexName = getXmlAttribute("prodata:indexName");
primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, true, word, false))
{
return false;
}
break;
case "index":
// found an non-unique index
indexName = getXmlAttribute("prodata:indexName");
primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
unique = Boolean.parseBoolean(getXmlAttribute("prodata:uniqueIndex"));
word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, unique, word, true))
{
return false;
}
break;
case ELEM_IMPORT :
case "complexType":
case "sequence":
case "annotation":
case "appinfo":
// tolerate them, nothing to do at this time
break;
}
break;
case XMLStreamReader.END_ELEMENT:
name = reader.getLocalName();
if (trackXml)
{
if (!elements.peek().equals(name))
{
throw new XMLStreamException(
String.format("Bad XML - expected '%s': %s, got '%s'",
elements.peek(), attrs.peek().toString(),name));
}
elements.pop();
attrs.pop();
}
if (ELEM_SCHEMA.equals(name))
{
if (ctx.tt._dynamic() && !ctx.tt.prepared().booleanValue())
{
TempTableBuilder ttb = (TempTableBuilder) ctx.tt;
ttb.setCanUndo(new logical(undo));
if (xmlNodeName != null)
{
ttb.setXmlNodeName(xmlNodeName);
}
if (nsPrefix != null)
{
ttb.namespacePrefix(nsPrefix);
}
if (nsUri != null)
{
ttb.namespaceURI(nsUri);
}
String b4table = b4tables.get(ctx.ttName);
if (!external && !ttb.tempTablePrepare(ctx.ttName, b4table))
{
// TODO: report error? already reported?
return false;
}
}
}
break;
}
}
return true;
//throw new XMLStreamException("Unexpected EOS in readTableSchema().", reader.getLocation());
}
/**
* Reads and process an index from the XML stream.
*
* @param indexName
* The index name (aka XML name).
* @param originalName
* The index's original name only if the xml name was altered because of collisions.
* @param nsPrefix
* The detected XML namespace prefix.
* @param primary
* {@code true} if this is a primary index.
* @param unique
* {@code true} is this is an unique index.
* @param word
* {@code true} is this is a word index.
* @param asIndex
* {@code true} is this taken from the 'index' node.
*
* @return {@code true} on success and {@code false} otherwise. When {@code false} is returned
* then the error message has already been dispatched to {@code ErrorManager}.
*
* @throws XMLStreamException
* If a XML-related error occurs (like an unexpected end of stream).
*/
private boolean readIndex(String indexName,
String originalName,
String nsPrefix,
boolean primary,
boolean unique,
boolean word,
boolean asIndex)
throws XMLStreamException
{
if (originalName == null)
{
originalName = indexName;
}
P2JIndex uidx = null;
AbstractTempTable att = null;
while (reader.hasNext())
{
int type = reader.next();
switch (type)
{
case XMLStreamReader.START_ELEMENT:
String name = reader.getLocalName();
switch (name)
{
case "table": // in case of NON-unique indexes
att = tables.get(getXmlAttribute("name"));
if (att != null && !att._prepared())
{
att.addNewIndex(new character(originalName),
new logical(unique),
new logical(primary),
new logical(word));
}
// if the table is already prepared, the indexes in schema are not verified, not even if
// STRICT mode was specified.
break;
case "selector": // in case of unique indexes
String xpath = getXmlAttribute("xpath");
if (xpath.startsWith(".//"))
{
xpath = xpath.substring(3);
}
xpath = removeNsPrefix(xpath);
xpath = tablesByXmlName.get(xpath);
// if (xpath.endsWith("Row"))
// {
// xpath = xpath.substring(0, xpath.length() - 3);
// }
att = tables.get(xpath);
if (att != null && !att._prepared())
{
att.addNewIndex(new character(originalName),
new logical(unique),
new logical(primary),
new logical(word));
}
if (unique) // should be true, anyway
{
uidx = new P2JIndex(xpath, originalName, true);
if (uniqueIndexes.put(indexName, uidx) != null)
{
throw new XMLStreamException(
"Name collision for unique indexes in readDatasetSchema().",
reader.getLocation());
}
}
// if the table is already prepared, the indexes in schema are not verified, not even if
// STRICT mode was specified.
break;
case "field":
String field = removeNsPrefix(getXmlAttribute(unique && !asIndex ? "xpath" : "name"));
boolean desc = Boolean.parseBoolean(getXmlAttribute("prodata:descending"));
if (att != null && !att._prepared())
{
att.addFieldToIndex(new character(originalName),
new character(field),
new character(desc ? "desc" : "asc"));
}
if (uidx != null)
{
uidx.addComponent(field);
}
// if the table is already prepared, the indexes in schema are not verified, not even if
// STRICT mode was specified.
break;
}
break;
case XMLStreamReader.END_ELEMENT:
String localName = reader.getLocalName();
if ("unique".equals(localName) || "index".equals(localName))
{
return true; // all OK
}
}
}
throw new XMLStreamException("Unexpected EOS in readIndex().", reader.getLocation());
}
/**
* Reads data from XML stream and populates the tables of a {@code DataSet}.
*
* @return {@code true} on success and {@code false} otherwise. When {@code false} is returned
* then the error message has already been dispatched to {@code ErrorManager}.
*
* @throws XMLStreamException
* if a XML-related error occurs (like an unexpected end of stream).
* @throws PersistenceException
* if a persistence error occurs while saving data to database.
*/
private boolean readDatasetData()
throws XMLStreamException,
PersistenceException
{
String dsName = null;
while (reader.hasNext())
{
int type = reader.next();
String name = null;
switch (type)
{
case XMLStreamReader.START_ELEMENT:
name = reader.getLocalName();
if (dsName == null)
{
dsName = name;
}
else if ("datasetChanges".equals(name))
{
// the root-node
}
else if ("before".equals(name))
{
readingBefore = true;
}
else // must be a table name
{
BufferImpl after = ds.getBufferByName(name);
if (after == null)
{
String legacy = tablesByXmlName.get(name);
if (legacy != null)
{
after = ds.getBufferByName(legacy);
}
if (after == null)
{
// TODO: handle this error
return false;
}
}
BufferImpl buffer = after;
if (readingBefore)
{
buffer = after.beforeBufferNative();
if (buffer == null)
{
// TODO: handle this error
return false;
}
buffer.setUpBeforeBuffer(true);
}
ctx.setBuffer(buffer.buffer(), false);
if (!readRecord())
{
return false;
}
}
break;
case XMLStreamReader.END_ELEMENT:
name = reader.getLocalName();
if ("datasetChanges".equals(name))
{
return true; // all OK
}
break;
}
}
throw new XMLStreamException("Unexpected EOS in readDatasetData().", reader.getLocation());
}
/**
* The read table is automatically added to the dataset if one is processed.
*
* Note that the {@code <xsd:element>} node for this table was already processed
* (no look-ahead) in order to decide to call this method.
*
* @param tableName
* The name of the table which is being processed.
* @param nsPrefix
* The detected XML namespace prefix.
*
* @return {@code true} if the operation is successful. If the operation fails, {@code false} is returned.
*
* @throws XMLStreamException
* in the event of an exception occurred while reading the XML stream.
*/
private boolean readTableSchema(String tableName, String nsPrefix)
throws XMLStreamException
{
// detect the working mode
AbstractTempTable att = tables.get(tableName);
boolean validate = att != null && att._prepared();
DmoMeta toMatch = null; // the DMO to verify against
if (validate && ds != null)
{
BufferImpl existingBuffer = ds.getBufferByName(tableName);
if (existingBuffer != null)
{
// if the default buffer of the table already exists we need just to verify the schema of the
// static/dynamic table, not to create it from the ground up. In this case [att] is null.
toMatch = existingBuffer.buffer().getDmoInfo();
att = existingBuffer.buffer().getParentTable();
}
}
if (toMatch == null && att == null)
{
att = (ctx.tt == null) ? new TempTableBuilder() : (TempTableBuilder) ctx.tt;
}
if (!validate)
{
if (namespace != null)
{
att.namespaceURI(namespace);
}
if (nsPrefix != null)
{
att.namespacePrefix(nsPrefix);
}
this.tables.putIfAbsent(tableName, att);
String xmlName = getXmlAttribute("name");
this.tablesByXmlName.put(xmlName, tableName);
String canUndo = getXmlAttribute("prodata:undo");
att.setCanUndo(logical.of(Boolean.parseBoolean(canUndo)));
}
String b4Table = getXmlAttribute("prodata:beforeTable");
if (b4Table != null)
{
b4tables.put(tableName, b4Table);
}
Set<Property> fieldsToMatch = null;
if (verifySchemaMode == Verify.STRICT && toMatch != null)
{
fieldsToMatch = new LinkedHashSet<>();
toMatch.getFields(false).forEachRemaining(fieldsToMatch::add);
}
// allows to distinct between table-ELEMENT and field-ELEMENT
boolean moreFields = true;
// allows to add fields in a correct order if some of them are XML attributes
SortedMap<Integer, P2JField> fields = new TreeMap<>();
while (reader.hasNext())
{
int tokType = reader.next();
String name = null;
switch (tokType)
{
case XMLStreamReader.START_ELEMENT:
name = reader.getLocalName();
if (trackXml)
{
elements.push(name);
attrs.push(getXmlAttributes());
}
// index properties:
String indexName = null;
boolean primary = false;
boolean unique = false;
boolean word = false;
String xmlNodeType = null;
switch (name)
{
case "attribute":
xmlNodeType = "ATTRIBUTE";
case "element":
String userOrder = getXmlAttribute("prodata:userOrder");
String _fieldName = getXmlAttribute("name");
String _nodeName = getXmlAttribute("prodata:fieldName");
String fieldName = _nodeName != null ? _nodeName : _fieldName;
String xmlNodeName = _nodeName != null ? _fieldName : null;
String help = getXmlAttribute("prodata:help");
String codePage = getXmlAttribute("prodata:columnCodepage");
String type = getXmlAttribute("type");
String fieldLabel = getXmlAttribute("prodata:label");
if (_fieldName != null && _fieldName.equals(fieldLabel) || "".equals(fieldLabel))
{
fieldLabel = null;
}
String fieldColumnLabel = getXmlAttribute("prodata:columnLabel");
if ("".equals(fieldColumnLabel))
{
fieldColumnLabel = null;
}
String extentMax = getXmlAttribute("maxOccurs");
String extentMin = getXmlAttribute("minOccurs");
// true extent fields have [extentMax = extentMin], with strictly positive value
if (extentMax == null || !extentMax.equals(extentMin) || "0".equals(extentMax))
{
// most likely, extentMax='1' and extentMin='0'; this is a scalar field
extentMax = extentMin = null;
}
if (type == null)
{
if (getXmlAttribute("ref") == null)
{
// in this case the read [name] attribute is, in fact, the name of the nested table
String legacy = getXmlAttribute("prodata:tableName");
String prefix = getXmlAttribute("prodata:prefix");
if (!readTableSchema(legacy == null ? getXmlAttribute("name") : legacy, prefix))
{
return false;
}
}
}
else
{
String ablType = get4GLType(type,
getXmlAttribute("prodata:dataType"),
getXmlAttribute("nullable"),
useLobs && toMatch == null,
tableName,
fieldName);
boolean cs = Boolean.parseBoolean(getXmlAttribute("prodata:caseSensitive"));
int decimals;
try
{
decimals = Integer.parseInt(getXmlAttribute("prodata:decimals"), 10);
}
catch (NumberFormatException e)
{
decimals = 0;
}
String defaultStrVal = getXmlAttribute("default");
if (defaultStrVal == null)
{
defaultStrVal = getXmlAttribute("prodata:initial");
if (defaultStrVal != null && defaultStrVal.startsWith("prodata:"))
{
defaultStrVal = defaultStrVal.substring("prodata:".length());
}
}
ParmType fieldType = TempTableBuilder.validateDatatype(ablType);
if (fieldType == null)
{
ErrorManager.recordOrShowError(13201, ablType, fieldName);
// Invalid data type override '<type>' for field '<field>'.
ErrorManager.recordOrShowError(13032);
// Unable to create Temp-Table or dataset schema from XML Schema.
return false;
}
String format = getXmlAttribute("prodata:format"); //TempTableBuilder.getDefaultDataTypeFormat(ablType);
long extent = extentMax == null ? 0 : Long.parseLong(extentMax);
BaseDataType defVal = null;
Class<?> fwdType = fieldType.getParamType();
if (BaseDataType.class.isAssignableFrom(fwdType) && defaultStrVal != null)
{
if (fwdType == clob.class || fwdType == blob.class)
{
ErrorManager.recordOrShowError(13143, defaultStrVal, fieldName);
// Unable to set initial value '<value>' from XML Schema for field '<field>'.
return false;
}
if (fwdType == date.class && "today".equalsIgnoreCase(defaultStrVal))
{
defVal = new character("today");
}
else if ((fwdType == datetime.class || fwdType == datetimetz.class) &&
"now".equalsIgnoreCase(defaultStrVal))
{
defVal = new character("now");
}
else if ("unknown".equals(defaultStrVal))
{
defVal = new character();
}
else
{
try
{
Constructor<? extends BaseDataType> ctor =
(Constructor<? extends BaseDataType>) fwdType.getConstructor(String.class);
defVal = ctor.newInstance(defaultStrVal);
}
catch (NoSuchMethodException | IllegalAccessException |
InstantiationException | InvocationTargetException e)
{
ErrorManager.recordOrShowError(13143, defaultStrVal, fieldName);
// Unable to set initial value '<value>' from XML Schema for field '<field>'.
return false;
}
}
}
if (toMatch == null)
{
P2JField newField = new P2JField(
fieldName, fieldType, extent, format, defVal,
fieldLabel, fieldColumnLabel,
cs, codePage, help, false, null, null,
xmlNodeName, xmlNodeType, false, decimals, 0);
if (userOrder == null)
{
((TempTableBuilder) att).addField(newField);
}
else
{
fields.put(Integer.parseInt(userOrder), newField);
}
}
else if (verifySchemaMode != Verify.IGNORE)
{
Property prop = toMatch.byLegacyName(fieldName);
if (prop == null || !prop._ablType.equals(ablType) || prop.extent != extent)
{
if (verifySchemaMode == Verify.STRICT)
{
ErrorManager.recordOrShowError(13201, ablType, fieldName);
return false;
}
}
if (extent != 0 && useLobs &&
("character".equalsIgnoreCase(ablType) ||
"raw".equalsIgnoreCase(ablType)))
{
// LOBS (BLOB or CLOB) cannot have extents
ErrorManager.recordOrShowError(13141, fieldName);
// Invalid 'maxOccurs' attribute value set for BLOB or CLOB field '<field>'.
ErrorManager.recordOrShowError(13033);
// Verify Temp-Table or dataset schema against XML Schema failed.
return false;
}
if (prop != null && !prop._ablType.equals(ablType))
{
ErrorManager.recordOrShowError(13089, fieldName, tableName);
return false;
}
if (fieldsToMatch != null) // implies [verifySchemaMode == Verify.STRICT]
{
fieldsToMatch.remove(prop);
}
}
}
break;
case "unique":
// found an unique index
indexName = getXmlAttribute("prodata:indexName");
primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, true, word, false))
{
return false;
}
break;
case "index":
// found an non-unique index
indexName = getXmlAttribute("prodata:indexName");
primary = Boolean.parseBoolean(getXmlAttribute("prodata:primaryIndex"));
unique = Boolean.parseBoolean(getXmlAttribute("prodata:uniqueIndex"));
word = Boolean.parseBoolean(getXmlAttribute("prodata:wordIndex"));
if (!readIndex(getXmlAttribute("name"), indexName, nsPrefix, primary, unique, word, true))
{
return false;
}
break;
case "complexType":
case "sequence":
// tolerate them, nothing to do at this time
break;
}
break;
case XMLStreamReader.END_ELEMENT:
name = reader.getLocalName();
if (trackXml)
{
if (!elements.peek().equals(name))
{
throw new XMLStreamException(
String.format("Bad XML - expected '%s': %s, got '%s'",
elements.peek(), attrs.peek().toString(),name));
}
elements.pop();
attrs.pop();
}
if ("complexType".equals(name))
{
moreFields = false;
if (fieldsToMatch != null && !fieldsToMatch.isEmpty()) // implies [verifySchemaMode == Verify.STRICT]
{
Property fail = fieldsToMatch.iterator().next();
ErrorManager.recordOrShowError(13090, fail.legacy, name);
// XML Schema definition for field '<field>' in table '<table>' not found.
return false;
}
}
if (!moreFields)
{
if (validate)
{
BufferImpl buffer = (BufferImpl) att.defaultBufferHandleNative();
DmoMeta meta = buffer.buffer().getDmoInfo();
Map<String, Property> dsFields = new HashMap<>();
meta.getFields(false).forEachRemaining(p -> dsFields.put(p.legacy, p));
for(P2JField field: fields.values())
{
Property fp = dsFields.get(field.getName());
if (fp == null)
{
continue;
}
if (!fp._ablType.equals(field.getType().toString()))
{
ErrorManager.recordOrShowWarning(13089, field.getName(), tableName);
// XML Schema definition for field '%1' in table '%2' does not match Temp-Table field definition
ErrorManager.recordOrShowWarning(13033);
// Verify Temp-Table or dataset schema against XML Schema failed"
return false;
}
}
}
else
{
fields.values().stream().forEach(((TempTableBuilder) att)::addField);
}
return true;
}
break;
}
}
throw new XMLStreamException("Unexpected EOS in readTableSchema().", reader.getLocation());
}
/**
* Skip the whole subtree because the record belongs to an unknown table. Fast forward to the end tag of
* the record.
*
* @param tableName
* The table name.
*
* @return {@code true} on success. Otherwise an error is issued before returning {@code false}.
*
* @throws XMLStreamException
* if the XML stream cannot be parsed.
*/
private boolean skipRecord(String tableName)
throws XMLStreamException
{
while (reader.hasNext())
{
int type = reader.next();
if (type == XMLStreamReader.END_ELEMENT)
{
String name = reader.getLocalName();
if (tableName.equals(name))
{
return true; // all OK
}
}
}
throw new XMLStreamException("Unexpected EOS in readSchema().", reader.getLocation());
}
/**
* Read XML content from the stream corresponding with a single data record and store it in
* the temp-table.
* <p>
* This version ensures that the group and decimal separators are ',' and '.' for the duration of the
* deserialization.
*
* @throws XMLStreamException
* if there is an error reading XML content from the stream.
* @throws PersistenceException
* if there is an error storing data in the temp-table.
*/
private boolean readRecord()
throws XMLStreamException,
PersistenceException
{
return readRecord(null);
}
/**
* Read XML content from the stream corresponding with a single data record and store it in
* the temp-table.
* <p>
* This version ensures that the group and decimal separators are ',' and '.' for the duration of the
* deserialization.
*
* @param parent
* The parent buffer, if this is called for a NESTED relation fields.
*
* @throws XMLStreamException
* if there is an error reading XML content from the stream.
* @throws PersistenceException
* if there is an error storing data in the temp-table.
*/
private boolean readRecord(Buffer parent)
throws XMLStreamException,
PersistenceException
{
if (ds == null && ctx.proxy != null && ((BufferImpl) ctx.proxy).isBeforeBuffer())
{
String field = "?";
if (reader.hasNext() && reader.nextTag() == XMLStreamReader.START_ELEMENT)
{
field = reader.getLocalName();
}
ErrorManager.recordOrShowError(13053, field, ctx.proxy.name().toStringMessage());
// Unable to update field '<field>' in table '<table>'
return false;
}
char groupSep = decimal.getGroupSeparator();
char decSep = decimal.getDecimalSeparator();
try
{
decimal.setNumericFormat('.', ',');
return ctx.tt._prepared() ? readRecord2(parent) : parseRecord(parent);
}
finally
{
decimal.setNumericFormat(decSep, groupSep);
}
}
/**
* Read XML content from the stream corresponding with a single data record and store it in
* the temp-table.
*
* @param parent
* The parent buffer, if this is called for a NESTED relation fields.
*
* @throws XMLStreamException
* if there is an error reading XML content from the stream.
* @throws PersistenceException
* if there is an error storing data in the temp-table.
*/
private boolean readRecord2(Buffer parent)
throws XMLStreamException,
PersistenceException
{
if (readMode != Read.REPLACE)
{
processRelations(parent);
}
String name = null;
boolean batchError = true;
RecordBuffer recBuffer = ((BufferImpl) ctx.proxy).buffer();
Map<String, String> readValues = (readMode == Read.REPLACE) ? new HashMap<>() : null;
try
{
if (readMode != Read.REPLACE)
{
RecordBuffer.startBatch(bufMan, true);
}
fillFromParent();
if (singleEntry)
{
for (Map.Entry<String, String> entry : singleEntryAttr.entrySet())
{
String attrName = entry.getKey();
String value = entry.getValue();
// skip the surrogate fields [id], [rowState]
if ("id".equals(attrName))
{
processId(recBuffer, value);
}
else if ("rowState".equals(attrName))
{
processRowState(recBuffer, value);
}
else
{
TempTableSchema.Column column = ctx.schema.getColumn(attrName);
if (column == null)
{
// silently ignored
continue;
}
if (readMode == Read.REPLACE)
{
// in REPLACE mode, store the read values temporarily
Integer index = ctx.extentTracker.getNext(attrName);
readValues.put(index != null ?
attrName + " " + ctx.extentTracker.getNext(attrName) :
attrName, value);
}
else
{
setField(column, attrName, value);
}
}
}
batchError = false;
return true;
}
// read row attributes
int attrs = reader.isStartElement() ? reader.getAttributeCount() : 0;
for (int i = 0; i < attrs; i++)
{
String attrName = reader.getAttributeLocalName(i);
String value = reader.getAttributeValue(i);
// skip the surrogate fields [id], [rowState]
if ("id".equals(attrName))
{
processId(recBuffer, value);
}
else if ("rowState".equals(attrName))
{
processRowState(recBuffer, value);
}
else
{
TempTableSchema.Column column = ctx.schema.getColumn(attrName);
if (column == null)
{
LOG.fine("Unknown attribute " + attrName);
continue;
}
if (readMode == Read.REPLACE)
{
// in REPLACE mode, store the read values temporarily
Integer index = ctx.extentTracker.getNext(attrName);
readValues.put(index != null ?
attrName + " " + ctx.extentTracker.getNext(attrName) :
attrName, value);
}
else
{
setField(column, attrName, value);
}
}
}
// read the TEXT field
for (TempTableSchema.Column column : ctx.schema.textColumns())
{
String value = readText();
String colName = column.getXmlNodeName().toLowerCase();
if (readMode == Read.REPLACE)
{
// in REPLACE mode, store the read values temporarily
Integer index = ctx.extentTracker.getNext(colName);
readValues.put(index != null ? colName + " " + ctx.extentTracker.getNext(colName) : colName,
value);
}
else
{
setField(column, colName, value);
}
}
// read row's child elements
fields: while (reader.hasNext())
{
int type = reader.next();
switch (type)
{
case XMLStreamReader.START_ELEMENT:
name = reader.getLocalName();
if (getXmlAttribute("prods:id") != null)
{
// this is a nested DataSet table: allow batch to end and call recursively
batchError = false;
if (!processNestedRecord())
{
return false;
}
continue fields;
}
TempTableSchema.Column column = ctx.schema.getColumn(name);
if (column == null)
{
// maybe this is a record of a nested table of the dataset?
if (ds != null)
{
if (ds.bufferHandle(name)._isValid())
{
if (!processNestedRecord())
{
return false;
}
continue fields;
}
String legacy = tablesByXmlName.get(name);
if (legacy != null && ds.getBufferByName(legacy) != null)
{
if (!processNestedRecord())
{
return false;
}
continue fields;
}
AbstractTempTable att = tables.get(name);
if (att != null)
{
if (!processNestedRecord())
{
return false;
}
continue fields;
}
}
// fields not matching the current table are silently ignored; call 'readText()' as this
// advances after the END_ELEMENT token
readText();
continue fields;
}
boolean isNil = checkNil(column);
String value = readText();
if (readMode == Read.REPLACE)
{
// in REPLACE mode, store the read values temporarily
Integer index = ctx.extentTracker.getNext(name);
readValues.put(index != null ? name + " " + ctx.extentTracker.getNext(name) : name,
isNil ? null : value);
}
else
{
setField(column, name, isNil ? null : value);
}
break;
case XMLStreamReader.END_ELEMENT:
batchError = false;
return true;
}
}
batchError = false;
}
catch (XMLStreamException xmlEx)
{
ErrorManager.recordOrShowError(13035, source.getFileName(), "");
// Error reading XML file '<file>'.
return false;
}
finally
{
ctx.extentTracker.reset();
if (readMode == Read.REPLACE)
{
boolean batched = false;
try
{
Boolean foundConflict = null;
DmoMeta dmoInfo = recBuffer.getDmoInfo();
P2JIndex primaryIndex = dmoInfo.getPrimaryIndex(true);
if (primaryIndex != null && primaryIndex.isUnique())
{
List<Object> params = new ArrayList<>();
StringBuilder sb = new StringBuilder();
sb.append("from ").append(dmoInfo.getDmoImplInterface().getName()).append(" where ");
if (recBuffer.isTemporary()) // always, right?
{
sb.append("_multiplex=?");
params.add(recBuffer.getMultiplexID()); // not null if temp-table
}
Set<String> indexComps = new HashSet<>();
ArrayList<P2JIndexComponent> comps = primaryIndex.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent comp = comps.get(i);
String fieldName = comp.getLegacyName();
if (!readValues.containsKey(fieldName)) // this will not match an extent field
{
foundConflict = false; // not full field-set for the primary unique index
break;
}
if (!params.isEmpty())
{
sb.append(" and ");
}
sb.append(comp.getPropertyName()).append("=?");
params.add(readValues.get(fieldName));
indexComps.add(fieldName);
}
if (foundConflict == null)
{
Query uq = Session.createQuery(sb.toString());
for (int i = 0; i < params.size(); i++)
{
uq.setParameter(i, params.get(i));
}
Object dmo = uq.uniqueResult(recBuffer.getPersistence().getSession(!dmoInfo.multiTenant));
foundConflict = (dmo != null);
// check if we have the full index field-set but not a matching record
if (foundConflict)
{
loader.accept(recBuffer, (Record) dmo); // loads the record into the buffer
RecordBuffer.startBatch(bufMan, true);
batched = true;
for (Map.Entry<String, String> entry : readValues.entrySet())
{
String keyName = entry.getKey();
if (!indexComps.contains(keyName))// do not overwrite the key/index component
{
int k = keyName.indexOf(' ');
int extIndex = -1;
if (k > 0)
{
extIndex = Integer.parseInt(keyName.substring(k + 1));
keyName = keyName.substring(0, k);
}
setField(ctx.schema.getColumn(keyName), keyName, entry.getValue(), extIndex);
}
}
}
}
}
if (foundConflict == null)
{
throw new RuntimeException("Internal error");
}
else if (!foundConflict)
{
ctx.proxy.create();
RecordBuffer.startBatch(bufMan, true);
batched = true;
for (Map.Entry<String, String> entry : readValues.entrySet())
{
String keyName = entry.getKey();
int k = keyName.indexOf(' ');
int extIndex = -1;
if (k > 0)
{
extIndex = Integer.parseInt(keyName.substring(k + 1));
keyName = keyName.substring(0, k - 1);
}
setField(ctx.schema.getColumn(keyName), keyName, entry.getValue(), extIndex);
}
}
}
finally
{
if (batched)
{
ErrorManager.ErrorHelper eh = ErrorManager.getErrorHelper();
boolean wm = eh.isWarningMode();
if (!wm)
{
// this is mostly for APPEND mode, the other ones will not encounter issues here
eh.setWarningMode(true);
}
int validationErrors = RecordBuffer.endBatch(bufMan, batchError);
if (!wm)
{
eh.setWarningMode(false);
}
// if (ErrorManager.error().toJavaType())
if (ErrorManager.isPendingError() || validationErrors != 0)
{
((BufferImpl) ctx.proxy).drop();
ErrorManager.recordOrShowError(15366, true, ctx.schema.getName(), "");
// Unable to update indexes for table '<table>'. (15366)
return false;
}
}
// else some exception occurred preventing the RecordBuffer to start the batch
}
}
else
{
if (readMode == Read.MERGE)
{
boolean validated = false;
try
{
validated = recBuffer.validate(false);
}
catch (ValidationException e)
{
// ignore, no 'expected' exception will be thrown
}
// was there an unique conflict with this record?
if (!validated)
{
// merge mode: ignore it, just skip to next record
((BufferImpl) ctx.proxy).drop();
}
}
ErrorManager.ErrorHelper eh = ErrorManager.getErrorHelper();
boolean wm = eh.isWarningMode();
if (!wm)
{
// this is mostly for APPEND mode, the other ones will not encounter issues here
eh.setWarningMode(true);
}
int validationErrors = RecordBuffer.endBatch(bufMan, batchError);
if (!wm)
{
eh.setWarningMode(false);
}
if (ErrorManager.isPendingError() || validationErrors != 0)
{
((BufferImpl) ctx.proxy).drop();
ErrorManager.recordOrShowError(13055, true, ctx.schema.getName(), "");
// Unable to update indexes for table '<table>'. (13055)
return false;
}
}
}
// if we reached this point something went wrong
throw new XMLStreamException("Unexpected EOS in readRecord().", reader.getLocation());
}
/**
* Process a single nested record. It may have other nested records as well.
*
* @return {@code true} on success.
*
* @throws XMLStreamException
* @throws PersistenceException
*/
boolean processNestedRecord()
throws XMLStreamException, PersistenceException
{
// push current configuration and call recursively readTableContent().
// TODO: cache [schema], [ctx.proxy] and [extentTracker] for better performance?
pushContext();
String name = reader.getLocalName();
BufferImpl buffer = ds.getBufferByName(name);
if (buffer == null)
{
String legacy = tablesByXmlName.get(name);
if (legacy != null)
{
buffer = ds.getBufferByName(legacy);
}
if (buffer == null)
{
AbstractTempTable att = tables.get(name);
buffer = (BufferImpl) att.defaultBufferHandleNative();
}
if (buffer == null)
{
// TODO: handle this error
popContext();
return false;
}
}
ctx.ttName = name;
ctx.setBuffer(buffer.buffer(), false);
setParentValues(name);
if (!readRecord(peekContext().proxy))
{
popContext();
return false;
}
popContext(); // restore configuration for next row
return true;
}
/**
* Pre-process nested relations.
*
* @param parent
* The parent buffer, if this is called for a NESTED relation fields.
*/
private void processRelations(Buffer parent)
{
ctx.proxy.create();
if (parent != null && ds != null)
{
List<DataRelation> rels = this.ds.getRelations(parent, true, false, true);
// this needs to be done first, as there may be other nested records which need to 'inherit'
// these fields
for (DataRelation rel : rels)
{
if (!rel.isNested().booleanValue())
{
continue;
}
Buffer buf = rel.getChildBufferNative();
if (buf == ctx.proxy)
{
// set all relation fields from the parent buffer to the child buffer
String[] fields = rel.getRelationFieldArray();
if (fields == null)
{
// TODO: PARENT-ID and other types of relations
continue;
}
for (int i = 0; i < fields.length; i += 2)
{
String pfield = fields[i];
String chfield = fields[i + 1];
buf.dereference(chfield, parent.dereference(pfield));
}
}
}
}
}
/**
* Parse XML record to retrieve fields' data, prepare table. populate the first record, and store it in
* the temp-table.
*
* @param parent
* The parent buffer, if this is called for a NESTED relation fields.
*
* @throws XMLStreamException
* if there is an error reading XML content from the stream.
* @throws PersistenceException
* if there is an error storing data in the temp-table.
*/
private boolean parseRecord(Buffer parent)
throws XMLStreamException,
PersistenceException
{
String name = null;
boolean persisted = false;
Map<String, Object> readValues = new LinkedHashMap<>();
Set<String> attributes = new HashSet<>();
FieldMaker maker = (ttb, fname, val) -> addField(ttb, fname, val, attributes::contains);
try
{
// read row attributes
int attrs = reader.getAttributeCount();
for (int i = attrs - 1; i >= 0; i--)
{
String attributeLocalName = reader.getAttributeLocalName(i);
addValue(readValues, attributeLocalName, reader.getAttributeValue(i));
attributes.add(attributeLocalName);
}
// TODO: read the TEXT field (?)
// read row's child elements
int nest = 0;
boolean isNil = false;
StringBuilder sb = new StringBuilder();
fields: while (reader.hasNext())
{
int type = reader.peek();
switch (type)
{
case XMLStreamReader.CHARACTERS:
case XMLStreamReader.CDATA:
reader.next();
if (nest == 1)
{
sb.append(reader.getText());
}
break;
case XMLStreamReader.START_ELEMENT:
nest++;
if (ds != null && getXmlAttribute("prods:id") != null)
{
// this is a nested DataSet table: the current row is assumed complete,
// allow batch to end and call recursively
if (!persist(maker, readValues, parent != null))
{
return false;
}
persisted = true;
reader.next();
name = reader.getLocalName();
if (!readNested(name))
{
return false;
}
break;
}
if (nest > 1)
{
if (ds != null)
{
if (!persisted)
{
if (!persist(maker, readValues, parent != null))
{
return false;
}
persisted = true;
}
if (!readNested(name))
{
return false;
}
nest = 0;
break;
}
else
{
throw new PersistenceException("Unexpected nested element: " + name);
}
}
reader.next();
name = reader.getLocalName();
isNil = checkNil(null);
break;
case XMLStreamReader.END_ELEMENT:
reader.next();
if( --nest == -1)
{
return persisted ? true : persist(maker, readValues, parent != null);
}
String value = sb.toString();
sb.setLength(0);
addValue(readValues, name, isNil ? null : value);
isNil = false;
}
}
}
catch (XMLStreamException xmlEx)
{
ErrorManager.recordOrShowError(13035, source.getFileName(), "");
// Error reading XML file '<file>'.
return false;
}
return persisted ? true : persist(maker, readValues, parent != null);
}
/**
* Read nested table data.
*
* @param name
* The name of the table to be populated.
*
* @return <code>true</code> in success
*
* @throws PersistenceException
* in error
*/
private boolean readNested(String name)
throws PersistenceException
{
AbstractTempTable att = tables.get(name);
if (att == null)
{
att = new TempTableBuilder();
inferredRelations.add(Pair.of(ctx.ttName, name));
}
return !att.readTable(name, this);
}
/**
* Add named value to a map of values, with extent handling.
*
* @param readValues
* The map which colelcts the key/values pairs.
* @param name
* The key.
* @param value
* The value associated with the key. If there is already a scalar value associated with the key,
* it is replaced by a list containing the extent data. If the old value is already a list the
* {@code value} is appended to it.
*/
private void addValue(Map<String, Object> readValues, String name, String value)
{
Object other = readValues.putIfAbsent(name, value);
if (other != null)
{
if (other instanceof List)
{
((List<Object>)other).add(value);
}
else
{
List<Object> list = new ArrayList<>();
list.add(other);
list.add(value);
readValues.replace(name, list);
}
}
}
/**
* Read text content from within an element or a CDATA section.
*
* @return Text which was read.
*
* @throws XMLStreamException
* if there is an error reading XML content from the stream.
*/
private String readText()
throws XMLStreamException
{
StringBuilder bld = new StringBuilder();
while (reader.hasNext())
{
int type = reader.next();
switch (type)
{
case XMLStreamReader.CHARACTERS:
case XMLStreamReader.CDATA:
bld.append(reader.getText());
break;
case XMLStreamReader.END_ELEMENT:
return bld.toString();
}
}
return null;
}
/**
* Check whether the current temp-table value read from XML content is null. If so, check
* whether the associated column allows null values.
*
* @param column
* Schema information for the associated column.
*
* @return {@code true} if the XML content is nil; else {@code false}.
*
* @throws ErrorConditionException
* if value is nil and column does not allow null values.
*/
private boolean checkNil(TempTableSchema.Column column)
{
// read attributes for current element
int attrs = reader.getAttributeCount();
for (int i = 0; i < attrs; i++)
{
String name = reader.getAttributeLocalName(i);
String value = reader.getAttributeValue(i);
if (ATTR_NIL.equals(name))
{
if (TEXT_TRUE.equals(value))
{
if (column != null && !column.isNillable())
{
ErrorManager.recordOrThrowError(0,
"Column not nillable: " + column.getFieldName(),
false);
}
return true;
}
return false;
}
}
return false;
}
/**
* Obtain the value of an attribute for the currently processed XML element. The method uses a
* cache map in order not to process the attributes multiple times. Once they are collected,
* the attribute values are kept until a request for other element is issued. The method uses
* {@code attrSetOffset} to compare the location of the current element. If they are different,
* the cache is updated. Note that the cache is updated only on request, the current of
* {@code crtAttSet} may be stalled if the {@code getXmlAttribute()} was not yet called for the
* new element.
*
* @param fullName
* The full-name of the attribute.
*
* @return The value of the attribute if found and {@code null} if there is no such attribute
* in current element.
*/
private String getXmlAttribute(String fullName)
{
if (reader.getLocation().getCharacterOffset() != attrSetOffset)
{
attrSetOffset = reader.getLocation().getCharacterOffset();
crtAttSet = new HashMap<>();
for (int k = 0; k < reader.getAttributeCount(); k++)
{
QName qName = reader.getAttributeName(k);
crtAttSet.put(qName.getPrefix().isEmpty() ? qName.getLocalPart() :
qName.getPrefix() + ":" + qName.getLocalPart(),
reader.getAttributeValue(k));
}
}
return crtAttSet.get(fullName);
}
/**
* Get map of attributes by name.
*
* @return the map of attributes by name.
*/
private Map<String, String> getXmlAttributes()
{
if (reader.getLocation().getCharacterOffset() != attrSetOffset)
{
attrSetOffset = reader.getLocation().getCharacterOffset();
crtAttSet = new HashMap<>();
for (int k = 0; k < reader.getAttributeCount(); k++)
{
QName qName = reader.getAttributeName(k);
crtAttSet.put(qName.getPrefix().isEmpty() ? qName.getLocalPart() :
qName.getPrefix() + ":" + qName.getLocalPart(),
reader.getAttributeValue(k));
}
}
return crtAttSet;
}
/**
* Get map of NS prefixes by namespace.
*
* @return the map of NS prefixes by namespace.
*/
private Map<String, String> getXmlNamespaces()
{
Map<String, String> prefixByNs = new HashMap<>();
for (int k = 0; k < reader.getNamespaceCount(); k++)
{
prefixByNs.put(reader.getNamespaceURI(k),
reader.getNamespacePrefix(k));
}
prefixByNs.put(reader.getNamespaceURI(), "");
return prefixByNs;
}
/**
* Helper method for detecting the 4GL field type using XML data types. Since some of them
* overlap, the additional parameters are used to distinct between them.
*
* @param type
* The pure {@code xsd} type.
* @param proType
* Additional information needed to distinct the original 4GL type.
* @param nullable
* Whether the original field type was nullable.
* @param useLobs
* If {@code true}, {@code clob} and {@code blob} types will be used instead of {@code character}
* and {@code raw}, respectively.
* @param tableName
* Optional. If specified the table name is looked up in the type map instead of the type
* specified in XML file.
* @param fieldName
* Optional. If specified the field name is looked up in the type map instead of the type
* specified in XML file.
*
* @return The original 4GL type of the field. If one cannot be detected using the provided
* data, {@code null} is returned.
*
* @see com.goldencode.p2j.persist.serial.Util#xsdMap
* @see com.goldencode.p2j.persist.serial.Util#xsdProdataMap
*/
private String get4GLType(String type,
String proType,
String nullable,
boolean useLobs,
String tableName,
String fieldName)
{
if (type == null)
{
return null;
}
if (fieldTypeMapping != null && fieldName != null)
{
// TODO: is un-scoped field lookup necessary?
String prefType = fieldTypeMapping.get(fieldName);
if (prefType != null)
{
return prefType;
}
if (tableName != null)
{
prefType = fieldTypeMapping.get(tableName + "." + fieldName);
if (prefType != null)
{
return prefType;
}
}
}
switch (type)
{
case "xsd:string":
return useLobs || "prodata:clob".equals(proType) ? "clob" : "character";
case "xsd:int":
return "integer";
case "xsd:decimal":
return "decimal";
case "xsd:date":
return "date";
case "xsd:boolean":
return "logical";
case "xsd:dateTime":
return "prodata:dateTime".equals(proType) ? "datetime" : "datetime-tz";
case "xsd:base64Binary":
if ("prodata:rowid".equals(proType))
{
return "rowid";
}
return useLobs || "prodata:blob".equals(proType) ? "blob" : "raw";
case "xsd:long":
if ("prodata:recid".equals(proType))
{
return "recid";
}
else if ("prodata:handle".equals(proType))
{
return "handle";
}
else if ("prodata:comHandle".equals(proType))
{
return "comhandle";
}
else
{
return "int64";
}
}
// TOOD: log this, unknown datatype
return null;
}
/**
* Add field to the temp-table.
*
* @param ttb
* The destination temp-table builder.
* @param name
* The name of the field to be added.
* @param val
* The value of field. It is used for inferring the type of the new field.
* @param isAttr
* A predicate which allows identification of fields with {@code ATTRIBUTE} node type.
*
* @throws PersistenceException
* If the type of the field cannot be inferred from the provided value.
*/
private void addField(TempTableBuilder ttb, String name, Object val, Predicate<String> isAttr)
throws PersistenceException
{
String type = typeOf(val).toString();
if (val instanceof List)
{
int ext = ((List<Object>) val).size();
ttb.addNewField(new character(name), new character(type), new integer(ext));
}
else
{
if (isAttr.test(name))
{
P2JField field = new P2JField(name, typeOf(val),
0, null, null, null, null, false,null, null, false, null, null, null,
"ATTRIBUTE", false, 0, 0);
ttb.addField(field);
}
else
{
ttb.addNewField(new character(name), new character(type));
}
}
}
/**
* Internal structure for storing a {@code DataSet} relation until the full set of tables is
* read.
*/
private static class Relation
{
/** The relation name. */
private final String name;
/** The name of the parent buffer. */
private final String parent;
/** The name of the child buffer. */
private String child;
/** The set of fields for this relation. */
private final String fields;
/** Flags the nested relations. */
private final boolean nested;
/** Flags the FOREIHN-KEY-HIDDEN relations. */
private final boolean fkHidden;
private final boolean recursive;
/** The list of child's fields involved in this relation. */
private final List<String> childFields;
/** The lists of parent's fields involved in this relation. */
private final List<String> parentFields;
/** Flag indicating if the relation is active. */
private final boolean active;
/**
* The constructor initializes the internal data.
*
* @param name
* The relation name.
* @param parent
* The name of the parent buffer.
* @param child
* The name of the child buffer.
* @param fields
* The set of fields for this relation.
* @param nested
* {@code true} for nested relations.
* @param fkHidden
* {@code true} for foreign-key-hidden relations
* @param recursive
* {@code true} for recursive relations
* @param active
* {@code true} for active relations
*/
public Relation(String name,
String parent,
String child,
String fields,
boolean nested,
boolean fkHidden,
boolean recursive,
boolean active)
{
this.name = name;
this.parent = parent;
this.child = child;
this.fields = fields;
this.nested = nested;
this.fkHidden = fkHidden;
this.recursive = recursive;
this.childFields = null; // not used with this constructor
this.parentFields = null; // not used with this constructor
this.active = active;
}
/**
* The constructor initializes the internal data.
*
* @param name
* The relation name.
* @param parentIndex
* The name of the parent table in this relation.
* @param nested
* {@code true} for nested relations.
* @param fkHidden
* {@code true} for foreign-key-hidden relations.
* @param recursive
* {@code true} for recursive relations.
* @param active
* {@code true} for active relations
*/
public Relation(String name,
P2JIndex parentIndex,
boolean nested,
boolean fkHidden,
boolean recursive,
boolean active)
{
this.name = name;
this.parent = parentIndex.getTable();
this.nested = nested;
this.fkHidden = fkHidden;
this.recursive = recursive;
this.fields = null; // not used with this constructor
this.childFields = new ArrayList<>();
this.parentFields = new ArrayList<>();
this.active = active;
ArrayList<P2JIndexComponent> comps = parentIndex.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent comp = comps.get(i);
parentFields.add(comp.getLegacyName());
}
}
/**
* Sets the name of the child buffer.
*
* @param xpath
* The name of the child buffer, as read from XML.
*/
public void setChild(String xpath)
{
child = xpath.startsWith(".//") ? xpath.substring(3) : xpath;
}
/**
* Adds a new field to be used on child side of the relation.
*
* @param field
* The name of the field.
*/
public void addChildField(String field)
{
childFields.add(field);
}
/**
* Get the set of fields which drives this relation.
*
* @return the set of pairs or fields of this relation.
*/
public String getFields()
{
if (fields != null)
{
return fields;
}
// else compute it:
StringBuilder sb = new StringBuilder();
int size = Math.min(childFields.size(), parentFields.size()); // should be equals, actually!
for (int i = 0; i < size; i++)
{
if (sb.length() != 0)
{
sb.append(",");
}
sb.append(parentFields.get(i)).append(",").append(childFields.get(i));
}
return sb.toString();
}
}
}