XmlExport.java
/*
** Module : XmlExport.java
** Abstract : Write temp-table data into an XML stream.
**
** 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.
** 004 OM 20190327 Renamed DataSource to avoid conflicts with DataSet source.
** 005 OM 20190711 API changes in favour of using SourceData and TargetData for all methods.
** OM 20190711 Implemented serialization of datasets and schema for tables and datasets.
** 006 OM 20190716 Fixed dataset serialized name.
** CA 20190812 Fixed problems with DATASET export.
** 007 CA 20191119 Do not explicitly XML-escape the value, as the writer will do this on its own.
** 008 ECF 20200906 New ORM implementation.
** 009 CA 20200914 Fixed raw/blob field export.
** OM 20201120 Added SERIALIZE-ROW. Improved TEMP-TABLE attribute serialization and compatibility with
** the output of P4GL.
** OM 20201218 Fixed serialization of BEFORE-BUFFER specific attributes.
** OM 20210329 Added Column API to allow extraction of legacy-name, xml-node-name and serialize-name
** independently from the precomputed name.
** CA 20210510 Fixed JSON and XML serialization when XML-NODE-NAME/SERIALIZE-NAME field options are set.
** CA 20210511 Removed XmlTempTableSchema class, as it was obsolete.
** CA 20210629 Added 'prodata:wordIndex' attribute for word indexes.
** CA 20210709 Fixed a NPE when writing a decimal unknown field.
** OM 20210809 Completed implementation of SERIALIZE-ROW|WRITE-JSON}(JsonObject|JsonArray).
** OM 20210922 Fixed default values for XML-NODE-NAME and SERIALIZE-NAME.
** OM 20211102 WRITE-XML was not using XML-NODE-NAME for the temp-table.
** CA 20211112 prodsid must be serialized only when in before-image mode.
** OM 20211020 Added _DATASOURCE_ROWID property.
** OM 20220120 Improve compatibility with P4GL. Optimizations. Fixed bugs.
** CA 20220218 Ensure the group and decimal separators are ',' and '.' during record write.
** 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.
** OM 20220525 Better encapsulation of schemaIndexes.
** CA 20220616 Unknown fields with XML-NODE-TYPE set to ATTRIBUTE or TEXT are not exported.
** IAS 20220926 Fixed WRITE-XMLSCHEMA support.
** IAS 20221013 Case-insensitive comparing of FORMAT value with a default one.
** IAS 20221014 Do not add prefix to the attribute name.
** IAS 20221110 Multiple fixes in the WRITE-XMLSCHEMA support. The most important one is the
** external schema support for DATASETs
** IAS 20221111 Fixed WRITE-XML for DATASET.
** IAS 20221123 Fixed nodes' close in WRITE-XMLSCHEMA for DATASET.
** 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.
** 009 IAS 20230223 Added support for recursive DATA-SET
** 20230224 Fixed NS Prefix value in WRITE-XML(SCHEMA)
** 20230313 Fixed WRITE-XML(SCHEMA) for multiple namespaces and NESTED relations.
** 20230314 Added WRITE-XML(SCHEMA) support for relations with FOREIGN-KEY-HIDDEN = true.
** 010 IAS 20240425 Add XML escaping. Fix "tns" prefixes' creation.
** 20240426 Fixed the indexes order.
** 20230501 Fixed support for the INITIAL-NULL pseudo-attribute.
** Added escaping for the "'"
** 011 IAS 20230415 Fixed support for PARENT-ID-RELATION.
** IAS 20230422 Fixed support for DATA-SET:SERIALIZE-HIDDEN in WRITE-XML
** IAS 20230424 Refactored WRITE-XMLSCHEMA
** IAS 20230501 Fixed support for DATA-SET:SERIALIZE-HIDDEN in WRITE-XMLSCHEMA
** 012 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 013 IAS 20230530 Fixed checks for empty nsPrefix.
** 014 CA 20230907 Added support for X-Document target. X-Noderef is not yet supported.
** 015 CA 20240308 At WRITE-XML, empty 'encoding' arg acts the same as unknown.
** 016 CA 20240320 Avoid BDTs within internal FWD runtime. Replaced iterators with Java 'for', where it
** applies.
** 017 CA 20240523 Fixed DATASET:WRITE-/READ-XML when before tables or (in)active relations are involved.
** 018 TJD 20240123 Java 17 compatibility updates
** 019 SP 20240603 Added a catch for ErrorConditionException in write().
** SP 20240627 Error 4065 must not set ERROR-STATUS:ERROR flag.
** 020 RNC 20241029 Recreate relation queries if they are already used when serializing the temp tables.
** RNC 20241030 If the serializing happens because of a SOAP request, ignore the
** FOREIGN-KEY-HIDDEN keyword from relations in the dataset.
** 021 ES 20250327 Use seralizationName, xmlNodeName, namspaceUri and namespacePrefix of the buffer,
** instead of temp-table. In case of TEMP-TABLE:readXml use attributes of the defaultBuffer.
** ES 20250328 Use the NAMESPACE-URI, NAMESPACE-PREFIX and XML-NODE-NAME attributes of the buffer in
** DATASET:WRITE-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.math.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
import java.util.stream.*;
import javax.xml.stream.*;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.commons.lang3.*;
import org.apache.commons.lang3.tuple.*;
import org.codehaus.stax2.*;
import org.codehaus.stax2.io.*;
import com.ctc.wstx.api.*;
import com.ctc.wstx.stax.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.serial.SerializeOptions.XmlNodeType;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.p2j.xml.*;
/**
* An implementation of the 4GL WRITE-XML method, which writes the records of a temp-table into
* an XML stream.
* <p>
* Runtime support is partial at this time; not all modes and features are supported.
*/
public final class XmlExport
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(XmlExport.class);
/** The 4GL output files use a 2-space indentation. */
private static final String INDENT = " ";
/** XML namespace attribute */
private static final String ATTR_NS_XSI = "xmlns:xsi";
/** Namespace URI for XML schema */
private static final String NS_URI = "http://www.w3.org/2001/XMLSchema-instance";
/** Nil attribute */
private static final String ATTR_XSI_NIL = "xsi:nil";
/** Platform-specific line separator */
// TODO: we need client's line separate, not server's
private static final String lineSep = System.lineSeparator();
/** noop XmlElementWriter */
private static final XmlElementWriter NOOP = () -> {};
/** Flag indicating whether XML output should be pretty-printed */
private final boolean format;
/** XML data target which normalizes access of various media to an output stream. */
private final TargetData target;
/** Character encoding (not currently used). */
private final String encoding;
/** Flag indicating that we're creating external schema */
private boolean external = false;
/** Flag indicating that we're creating schema for the tested table*/
private boolean nested = false;
/** XML stream writer */
private XMLStreamWriter writer = null;
/** The indentation level. */
private int indentLevel = 0;
/** The current {@code DataSet}, if one is processed. {@code null} if a table is processed. */
private DataSet ds = null;
/** The current {@code DataSet} name, if one is processed. {@code null} if a table is processed. */
private String dsName = null;
/** The current {@code DataSet} namespace, if one is processed. {@code null} if a table is processed. */
private String nsUri = "";
/** The current {@code DataSet} prefix, if one is processed. {@code null} if a table is processed. */
private String nsPrefix = null;
/** The value of the DataSet SERIALIZE-HIDDEN attribute */
private boolean serializeHidden = false;
/** Number of DataSet relations */
private int relCount;
/** Number of DataSet non PARENT-ID-RELATION relations */
private int prodataRelationCount ;
// /**
// * Flags a {@code WRITE-XMLSCHEMA} method in progress (for both {@code DataSet} and
// * {@code TempTable}). When a {@code WRITE-XML} is processed the value is {@code false}.
// */
// private boolean isSchemaOnly = false;
/** The list of indexes, in order of their buffers. */
private final Map<Integer, Pair<String, TableMapper.LegacyIndexInfo>> indexes = new TreeMap<>();
/** The map of DATA-SET tables by legacy names */
private final Map<String, TempTableSchema> tables = new LinkedHashMap<>();
/** The DATA-SET tables grouped by namespace. */
private Map<String, List<TempTableSchema>> tablesByNs = new LinkedHashMap<>();
/** External schemas by NS */
private Map<String, String> extSchemas = new LinkedHashMap<>();
/** Tables schemas already written */
private Set<String> writtenSchemas = new HashSet<>();
/** Default prefix for NS by NS */
private Map<String, String> prefixByNs = new LinkedHashMap<>();
/** Parent -> Child table names for the NESTED relations */
private final Map<String, List<String>> childByParent = new HashMap<>();
/** Child -> Parent table names for the NESTED relations */
private final Map<String, String> parentByChild = new HashMap<>();
/** Child fields for NESTED relations with FOREIGN-KEY-HIDDEN = true per child table name*/
private Map<String, Set<String>> childFieldsByChild = new HashMap<>();
/** Child fields for the current NESTED relations with FOREIGN-KEY-HIDDEN = true*/
private Set<String> childFields = Collections.emptySet();
/** The map with eventual error strings. */
private final Map<String, Pair<String, String>> prodsErrors = new LinkedHashMap<>();
/** XML schema writer*/
private XmlElementWriter writeXmlSchema = null;
/** The namespace for the currently exported temp-table, which is a root of the nested realtions tree */
private String rootNsUri = null;
/** The namespace prefix for the currently exported temp-table, which is a root of the nested realtions tree */
private String rootNsPrefix = null;
/** The namespace prefix for the currently exported temp-table, if any. */
private String ttNsPrefix = null;
/** Current table has childs */
private boolean hasChild = false;
/** Number of child records just read */
private int childRecords = 0;
/** Stack of 'hasChild' flag values */
private final Stack<Boolean> savedHasChild = new Stack<>();
/** xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" was written */
private boolean xsiWritten = false;
/** Number of dataset/tables root element written */
private int rootsWritten = 0;
/** Root table name (for DATA-SET:WRITE-XML(SCHEMA) with SERIALIZE-HIDDEN = true */
private String rootTableName = null;
/** Number of root table records written */
private int rootTableRecordsWritten = 0;
/**
* Maps the {@code LegacyIndexInfo} objects to unique names for them, if their name is not unique
* across the dataset.
*/
private final Map<TableMapper.LegacyIndexInfo, String> uidxUniqueNames = new LinkedHashMap<>();
/**
* Unique names for unique indexes map to their original {@code LegacyIndexInfo} objects. This is the
* reverse of {@code uidxUniqueNames}. All unique indexes can be found here.
*/
private final Map<String, TableMapper.LegacyIndexInfo> uidxByUniqueNames = new LinkedHashMap<>();
/**
* We store here the {@code DataRelation}s which have promoted to {@code xsd:keyref}s. Those not present
* in this set will be serialized as {@code prodata:relation}s.
*/
private final Set<DataRelation> keyrefs = new HashSet<>();
private static final TempTableSchema.Column ERROR_FLAG =
new TempTableSchema.Column(Buffer.__ERROR_FLAG__, Buffer.__ERROR_FLAG__, new integer());
private static final TempTableSchema.Column ORIGIN_ROWID =
new TempTableSchema.Column(Buffer.__ORIGIN_ROWID__, Buffer.__ORIGIN_ROWID__, new rowid());
private static final TempTableSchema.Column DATASOURCE_ROWID =
new TempTableSchema.Column(Buffer.__DATA_SOURCE_ROWID__, Buffer.__DATA_SOURCE_ROWID__, new rowid());
private static final TempTableSchema.Column ERROR_STRING =
new TempTableSchema.Column(Buffer.__ERROR_STRING__, Buffer.__ERROR_STRING__, new character());
private static final TempTableSchema.Column AFTER_ROWID =
new TempTableSchema.Column(Buffer.__AFTER_ROWID__, Buffer.__AFTER_ROWID__, new rowid());
private static final TempTableSchema.Column ROW_STATE =
new TempTableSchema.Column(Buffer.__ROW_STATE__, Buffer.__ROW_STATE__, new integer());
/**
* Constructor which saves the internal data for writing a XML/XSD stream.
*
* @param target
* XML data target which normalizes access of various media to an output stream.
* @param formatted
* {@code true} if serialized output should be pretty-printed for human-friendly
* reading; {@code false} to omit formatting.
* @param encoding
* Character encoding (not currently used).
*/
public XmlExport(TargetData target, boolean formatted, String encoding)
{
this.target = target;
this.encoding = encoding != null && encoding.isEmpty() ? null : encoding; // defaults to UTF-8
this.format = formatted;
}
/**
* Performs the export of a DATASET.
*
* @param dataSet
* The {@code DatSet} from which data is exported.
* @param schemaLocation
* Path to XML schema (not currently used).
* @param writeXmlSchema
* {@code true} to write XML schema information; {@code false} to write only data.
* If {@code true}, schema will be written to {@code schemaLocation}, if provided;
* if not provided, schema will be embedded in XML output. Not currently honored.
* @param minXmlSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}. Not
* currently honored.
* @param writeBeforeImage
* Not currently honored.
* @param omitInitialValues
* If {@code true}, record values which match default initial values will be omitted
* from serialized output, else they will be included. Not currently honored.
*
* @return {@code true} on success. Otherwise the error is already dispatched to
* {@code ErrorManager} before returning.
*/
public boolean writeDataset(DataSet dataSet,
String schemaLocation,
boolean writeXmlSchema,
boolean minXmlSchema,
boolean writeBeforeImage,
boolean omitInitialValues)
{
if (!target.configureSupportedTargets(TargetData.TD_WRITE_XML,
TargetData.TD_WRITE_XML,
LegacyResource.DATASET + " widget"))
{
return false;
}
if (target.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
{
return writeToHandle((export) -> export.writeDataset(dataSet,
schemaLocation,
writeXmlSchema,
minXmlSchema,
writeBeforeImage,
omitInitialValues));
}
if (writeXmlSchema && schemaLocation != null && !schemaLocation.isEmpty())
{
ErrorManager.recordOrShowError(13031);
// Cannot have both schema-location and write-schema arguments set.
return false;
}
this.ds = dataSet;
this.serializeHidden = this.ds.getSerializeHidden().booleanValue();
String dsName1 = dataSet.name().toStringMessage();
if (dsName1.isEmpty())
{
dsName1 = "ProDataSet";
}
this.dsName = dsName1;
character _nsUri = dataSet.namespaceURI();
character _nsPrefix = dataSet.namespacePrefix();
this.nsUri = (_nsUri.isUnknown()) ? "" : _nsUri.toJavaType();
this.nsPrefix = _nsPrefix.toJavaType();
String nsNodeName = dataSet.getXmlNodeNameInternal();
if (!tables(writeXmlSchema))
{
return false;
}
return write(() -> {
if (!serializeHidden)
{
writer.writeStartElement((isEmpty(nsPrefix) ? "" : nsPrefix + ":") + nsNodeName);
if (!nsUri.isEmpty())
{
writer.writeAttribute(
"xmlns" + (isEmpty(nsPrefix) ? "" : ":" + nsPrefix),
nsUri);
}
writer.writeAttribute(ATTR_NS_XSI, NS_URI);
xsiWritten = true;
if (schemaLocation != null)
{
if (nsUri.isEmpty())
{
writer.writeAttribute("xsi:noNamespaceSchemaLocation", schemaLocation);
}
else
{
writer.writeAttribute("xsi:schemaLocation", nsUri + " " + schemaLocation);
}
}
lineBreak();
++indentLevel;
}
if (writeXmlSchema)
{
if (serializeHidden)
{
this.writeXmlSchema = () -> {
if (!writeDatasetSchemaImpl(minXmlSchema, !omitInitialValues, nsNodeName))
{
throw new XMLStreamException("Failed to write XML schema");
}
};
}
else if (!writeDatasetSchemaImpl(minXmlSchema, !omitInitialValues, nsNodeName))
{
return false;
}
}
if (writeBeforeImage)
{
// CA: my testing shows these are NOT emitted in the normal cases; who decides that
// these are needed?
indent();
writer.writeStartElement("prods:datasetChanges");
writer.writeAttribute("xmlns:prods",
"urn:schemas-progress-com:xml-dataset-changes:0001");
writer.writeAttribute(ATTR_NS_XSI, NS_URI);
lineBreak();
++indentLevel;
indent();
writer.writeStartElement(nsNodeName == null ? dsName : nsNodeName);
if (!nsUri.isEmpty())
{
writer.writeAttribute("xmlns", nsUri);
}
writer.writeAttribute(ATTR_NS_XSI, NS_URI);
lineBreak();
++indentLevel;
}
if (!serializeDsTables(writeBeforeImage, omitInitialValues))
{
throw new XMLStreamException("Failed to serialize dataset tables");
}
if (writeBeforeImage)
{
writeEndElement(); // ProDataSet
}
if (writeBeforeImage)
{
writeElement("prods:before", NOOP, () -> {
int ttCount = ds.numBuffers().intValue();
for (int k = 0; k < ttCount; k++)
{
BufferImpl after = (BufferImpl) dataSet.getBufferByIndex(k + 1);
if (!after.isAfterBuffer())
{
continue;
}
BufferImpl before = (BufferImpl) after.beforeBufferNative();
TemporaryBuffer buffer = (TemporaryBuffer) before.buffer();
if (buffer.count() <= 0)
{
continue;
}
String bufName = after.name().toStringMessage();
ttNsPrefix = after.namespacePrefix().toJavaType();
String ttNsUri = after.namespaceURI().isUnknown() ? "" : after.namespaceURI().toJavaType();
if (ttNsUri.isEmpty() || ttNsUri.equals(nsUri))
{
ttNsPrefix = nsPrefix;
}
if (serializeTempTable(null, buffer, bufName, ttNsUri,
omitInitialValues, false, writeBeforeImage, true) < 0)
{
throw new XMLStreamException("Failed to serialize [" + bufName + "]");
}
}
});
}
writeProdErrors();
if (writeBeforeImage)
{
writeEndElement(); // prods:datasetChanges
}
if (!serializeHidden)
{
writeEndElement(); // ProDataSet
}
return true;
}, TargetData.TD_WRITE_XML,
LegacyResource.DATASET + " widget");
}
/**
* Serialize DataSet tables
*
* @param writeBeforeImage
* Not currently honored.
* @param omitInitialValues
* If {@code true}, record values which match default initial values will be omitted
* from serialized output, else they will be included. Not currently honored.
* @return <code>true</code> if at least of the tables has changes
*/
private boolean serializeDsTables(boolean writeBeforeImage, boolean omitInitialValues)
{
int ttCount = ds.numBuffers().intValue();
boolean hasChanges = false;
nextBuffer: for (int k = 0; k < ttCount; k++)
{
BufferImpl after = ds.getBufferByIndex(k + 1);
List<DataRelation> rels = ds.getRelations(after, false, true, true);
for (DataRelation rel : rels)
{
if (rel.isNested().getValue() || rel.isParentIdRelation().getValue())
{
// if [after] is a child buffer in a active nested relation, its data was (or
// will be serialized with the parent)
continue nextBuffer;
}
}
TemporaryBuffer buffer = (TemporaryBuffer) after.buffer();
ttNsPrefix = after.namespacePrefix().toJavaType();
rootNsUri = after.namespaceURI().isUnknown() ? "" : after.namespaceURI().toJavaType();
if (rootNsUri.isEmpty() || rootNsUri.equals(nsUri))
{
ttNsPrefix = nsPrefix;
}
rootNsPrefix = ttNsPrefix;
if (serializeTempTable(null, buffer, after.getXmlNodeName().toJavaType(), rootNsUri,
omitInitialValues, false, writeBeforeImage, false) < 0)
{
return false;
}
if (serializeHidden)
{
++indentLevel;
}
if (!hasChanges && after.isAfterBuffer())
{
BufferImpl before = (BufferImpl) after.beforeBufferNative();
TemporaryBuffer beforeBuffer = (TemporaryBuffer) before.buffer();
int count = beforeBuffer.count();
if (count > 0)
{
hasChanges = true;
}
}
}
return true;
}
/**
* Write errors.
* @throws XMLStreamException
* on Error
*/
private void writeProdErrors() throws XMLStreamException
{
if (!prodsErrors.isEmpty())
{
writeElement("prods:errors", NOOP, () -> {
Set<Map.Entry<String, Pair<String, String>>> errors = prodsErrors.entrySet();
for (Map.Entry<String, Pair<String, String>> error : errors)
{
writeElement(error.getValue().getLeft(), () -> {
writer.writeAttribute("prods:id", error.getKey());
writer.writeAttribute("prods:error", error.getValue().getRight());
});
}
});
prodsErrors.clear();
}
}
/**
* Constructor which immediately performs the export of a TEMP-TABLE.
*
* @param buffer
* Temp-table buffer from which data is exported.
* @param schemaLocation
* Path to XML schema (not currently used).
* @param writeXmlSchema
* {@code true} to write XML schema information; {@code false} to write only data.
* If {@code true}, schema will be written to {@code schemaLocation}, if provided;
* if not provided, schema will be embedded in XML output. Not currently honored.
* @param minXmlSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}. Not
* currently honored.
* @param writeBeforeImage
* Not currently honored.
* @param omitInitialValues
* If {@code true}, record values which match default initial values will be omitted
* from serialized output, else they will be included. Not currently honored.
*
* @return {@code true} on success. Otherwise the error is already dispatched to
* {@code ErrorManager} before returning.
*/
public boolean writeTempTable(TemporaryBuffer buffer,
String schemaLocation,
boolean writeXmlSchema,
boolean minXmlSchema,
boolean writeBeforeImage,
boolean omitInitialValues)
{
if (!target.configureSupportedTargets(TargetData.TD_WRITE_XML,
TargetData.TD_WRITE_XML,
LegacyResource.TEMP_TABLE + " widget"))
{
return false;
}
if (target.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
{
return writeToHandle((export) -> export.writeTempTable(buffer,
schemaLocation,
writeXmlSchema,
minXmlSchema,
writeBeforeImage,
omitInitialValues));
}
if (writeXmlSchema && schemaLocation != null && !schemaLocation.isEmpty())
{
ErrorManager.recordOrShowError(13031);
// Cannot have both schema-location and write-schema arguments set.
ErrorManager.recordOrShowError(4065, false, "WRITE-XML", "TEMP-TABLE widget");
// **The <attribute> attribute on the <widget id> has invalid arguments.
return false;
}
TempTableSchema schema = new TempTableSchema(buffer, false);
if (schemaLocation != null && !schemaLocation.isEmpty())
{
// check the existence of incompatible fields
String unsupportedType = Util.checkSupportedTypes(schema);
if (unsupportedType != null)
{
ErrorManager.recordOrShowError(13078, unsupportedType);
// Unsupported data type for XML Serialization: <type>.
ErrorManager.recordOrShowError(13066, schema.getTableName());
// Unable to write XML Schema for temp-table <table-name>.
ErrorManager.recordOrShowError(13097);
// Write schema failed for WRITE-XMLSCHEMA.
return false;
}
}
return write(() -> {
Buffer currentBuffer = (Buffer) buffer.getDMOProxy();
ttNsPrefix = currentBuffer.namespacePrefix().toJavaType();
String nsUri = currentBuffer.namespaceURI().isUnknown() ? "" : currentBuffer.namespaceURI().toJavaType();
String nodeName = currentBuffer.getXmlNodeName().toJavaType();
String xmlNodeName = (isEmpty(ttNsPrefix))? nodeName : (ttNsPrefix + ":" + nodeName);
writer.writeStartElement(xmlNodeName);
if (!nsUri.isEmpty())
{
writer.writeAttribute(
"xmlns" + (isEmpty(ttNsPrefix) ? "" : ":" + ttNsPrefix),
nsUri);
}
writer.writeAttribute(ATTR_NS_XSI, NS_URI);
xsiWritten = true;
if (schemaLocation != null && !schemaLocation.isEmpty())
{
if (nsUri.isEmpty())
{
writer.writeAttribute("xsi:noNamespaceSchemaLocation", schemaLocation);
}
else
{
writer.writeAttribute("xsi:schemaLocation", nsUri + " " + schemaLocation);
}
}
lineBreak();
++indentLevel;
BufferImpl dmoProxy = (BufferImpl) buffer.getDMOProxy();
if (writeXmlSchema)
{
if (!writeTableSchema(dmoProxy, schema, minXmlSchema, !omitInitialValues))
{
return false;
}
}
String rowName = xmlNodeName + "Row";
if (serializeTempTable(null, buffer, rowName, nsUri,
omitInitialValues, dmoProxy.isBeforeBuffer(), false, false) < 0)
{
return false;
}
writeEndElement();
return true;
}, TargetData.TD_WRITE_XML,
LegacyResource.TEMP_TABLE + " widget");
}
/**
* Performs the export of a DATASET schema.
*
* @param dataSet
* The {@code DatSet} to be described.
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}. Not
* currently honored.
* @param noInitial
* If {@code true}, record values which match default initial values will be omitted
* from serialized output, else they will be included. Not currently honored.
*
* @return {@code true} on success. Otherwise, the error is already dispatched to
* {@code ErrorManager} before returning.
*/
public boolean writeDatasetSchema(DataSet dataSet, boolean minSchema, boolean noInitial)
{
if (!target.configureSupportedTargets(TargetData.TD_WRITE_XMLSCHEMA,
TargetData.TD_WRITE_XMLSCHEMA,
LegacyResource.DATASET + " widget"))
{
return false;
}
if (target.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
{
return writeToHandle((export) -> export.writeDatasetSchema(dataSet, minSchema, noInitial));
}
this.ds = dataSet;
this.serializeHidden = this.ds.getSerializeHidden().booleanValue();
String dsName1 = dataSet.name().toStringMessage();
if (dsName1.isEmpty())
{
dsName1 = "ProDataSet";
}
this.dsName = dsName1;
character _nsUri = dataSet.namespaceURI();
character _nsPrefix = dataSet.namespacePrefix();
this.nsUri = (_nsUri.isUnknown()) ? "" : _nsUri.toJavaType();
this.nsPrefix = _nsPrefix.toJavaType();
String nodeNameStr = dataSet.getXmlNodeNameInternal();
if (!tables(true))
{
return false;
}
// this.isSchemaOnly = true;
return write(() -> writeDatasetSchemaImpl(minSchema, !noInitial, nodeNameStr),
TargetData.TD_WRITE_XMLSCHEMA,
LegacyResource.DATASET + " widget");
}
/**
* Extract tables's data from the DATASET
*
* @param forSchema
* Flag indicating that data is extracted for WRITE-XMLSCHEMA
* @return <code>true</code> if OK.
*/
private boolean tables(boolean forSchema)
{
ds.getBuffers().stream().
map(BufferImpl::buffer).
map(buffer -> new TempTableSchema(buffer, false)).
forEach(tts -> tables.put(tts.getTableName(), tts));
tablesByNs.put(nsUri, new ArrayList<>());
tables.values().stream().forEach(
ts -> {
String ns = ts.namespaceURI();
tablesByNs.computeIfAbsent("".equals(ns) ? nsUri : ns,
n -> new ArrayList<>()).add(ts);
});
if (!Serializator.TYPE_FILE.equals(target.getType().toUpperCase()) &&
tablesByNs.size() > 1 && forSchema)
{
ErrorManager.recordOrShowError(13095,false, false);
return false;
}
String nsPrefix = ds.namespacePrefix().getValue();
if ( nsPrefix == null || "".equals(nsPrefix))
{
nsPrefix = "".equals(nsUri) ? "" : "tns";
}
prefixByNs.put(nsUri, nsPrefix);
prefixByNs.putIfAbsent("", nsPrefix);
int n = 1;
for (Map.Entry<String, List<TempTableSchema>> e: tablesByNs.entrySet())
{
if (e.getValue().isEmpty()) // this can be for nsUri only
{
continue;
}
String tns = "tns" + String.valueOf(n++);
String prefix = e.getValue().get(0).namespacePrefix();
prefixByNs.putIfAbsent(e.getKey(), isEmpty(prefix) ? ("".equals(nsUri) ? "" : tns) : prefix);
}
ds.getRelations().
filter(r -> r.isActive().booleanValue() &&
(r.isNested().booleanValue() || r.isParentIdRelation().booleanValue())).
forEach(r -> {
String parent = r.getParentTable();
String child = r.getChildTable();
parentByChild.put(child, parent);
childByParent.computeIfAbsent(parent, p -> new ArrayList<>()).add(child);
if (r.getForeignKeyHidden().toJavaType())
{
childFieldsByChild.put(child, new HashSet<>(Arrays.asList(r.getRelationFields(false))));
}
});
return !tables.isEmpty();
}
/**
* Performs the export of a TEMP-TABLE schema.
*
* @param buffer
* A buffer of a {@code TempTable} to be described.
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}.
* @param noInitial
* If {@code true}, record values which match default initial values will be omitted
* from serialized output, else they will be included. Not currently honored.
*
* @return {@code true} on success. Otherwise, the error is already dispatched to
* {@code ErrorManager} before returning.
*/
public boolean writeTableSchema(TemporaryBuffer buffer, boolean minSchema, boolean noInitial)
{
if (!target.configureSupportedTargets(TargetData.TD_WRITE_XMLSCHEMA,
TargetData.TD_WRITE_XMLSCHEMA,
LegacyResource.TEMP_TABLE + " widget"))
{
return false;
}
if (target.getType().equalsIgnoreCase(TargetData.TYPE_HANDLE))
{
return writeToHandle((export) -> export.writeTableSchema(buffer, minSchema, noInitial));
}
// this.isSchemaOnly = true;
return write(() -> writeTableSchema(
(BufferImpl) buffer.getDMOProxy(),
new TempTableSchema(buffer, false),
minSchema,
!noInitial),
TargetData.TD_WRITE_XMLSCHEMA,
LegacyResource.TEMP_TABLE + " widget");
}
/**
* Writes a single record.
*
* @param bufferImpl
* The buffer which holds the record to be exported.
* @param omitInitial
* If {@code true} the unmodified fields (those that are equals to the declared initial value)
* are skipped in order to minimize the output.
*
* @return {@code true} if operation is successful and {@code false} if an error occurred.
*/
public boolean writeRecord(BufferImpl bufferImpl, boolean omitInitial)
{
if (!target.configureSupportedTargets(TargetData.TD_WRITE_XML,
TargetData.TD_WRITE_XML,
LegacyResource.BUFFER + " widget"))
{
return false;
}
TemporaryBuffer tempBuffer = (TemporaryBuffer) bufferImpl.buffer();
TempTableSchema schema = new TempTableSchema(tempBuffer, false);
String unsupportedType = Util.checkSupportedTypes(schema);
if (unsupportedType != null)
{
ErrorManager.recordOrShowError(13078, unsupportedType);
// Unsupported data type for XML Serialization: <type>.
ErrorManager.recordOrShowError(13093);
// Write temp-table data failed for WRITE-XML.
return false;
}
String tableName = schema.getXmlName();
TempRecord dmo = tempBuffer.getCurrentRecord();
return write(() -> {
XmlElementWriter attrWriter = () -> {
if (!xsiWritten)
{
writer.writeAttribute(ATTR_NS_XSI, NS_URI);
}
try
{
// write all columns which must be serialized as attributes first, while the start element for the
// row is open
writeColumns(dmo, schema.attributeColumns(), false, omitInitial);
}
catch (IllegalAccessException | InvocationTargetException e)
{
throw new XMLStreamException(e);
}
};
if (schema.textColumns().iterator().hasNext())
{
writeTextElement(tableName, attrWriter, () -> {
try
{
writeColumns(dmo, schema.textColumns(), false, omitInitial);
}
catch (IllegalAccessException | InvocationTargetException e)
{
throw new XMLStreamException(e);
}
});
}
else
{
writeElement(tableName, attrWriter, () -> {
try
{
// write all columns which must be serialized to elements as separate XML elements
writeColumns(dmo, schema.elementColumns(), false, omitInitial);
}
catch (IllegalAccessException | InvocationTargetException e)
{
throw new XMLStreamException(e);
}
});
}
return true;
}, TargetData.TD_WRITE_XML,
LegacyResource.BUFFER + " widget");
}
/**
* Export to a handle. Only {@link XDocument X-Document} is currently supported.
*
* @param writer
* The writer function.
*
* @return <code>true</code> if the export was successful.
*/
private boolean writeToHandle(Function<XmlExport, Boolean> writer)
{
longchar lc = new longchar("");
TargetData td = new TargetData("longchar", lc);
XmlExport export = new XmlExport(td, format, encoding);
boolean res = writer.apply(export);
if (res)
{
XDocumentImpl xdoc = (XDocumentImpl) ((handle) target.getDestination()).get();
boolean[] res2 = new boolean[1];
ErrorManager.nestedSilent(() -> res2[0] = xdoc.load("longchar", lc, false).booleanValue());
return res2[0];
}
return false;
}
/**
* Performs the export of a TEMP-TABLE schema.
*
* @param buffer
* A buffer of a {@code TempTable} to be described.
* @param schema
* The schema of the TEMP-TABLE.
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}. Not
* currently honored.
* @param defaultValues
* If {@code true}, record values which match default initial values will be omitted
* from serialized output, else they will be included.
* NOTE: actually setting [defaultValues] to [TRUE] which is the [omit-initial-values] from
* ABL will cause some extra info to be added in field description (see + attributes):
* <pre>{@code
* <xsd:element name="f11" type="xsd:string" nillable="true" +minOccurs="0" +default=""/>
* <xsd:element name="f12" type="xsd:int" nillable="true" +minOccurs="0" +default="0"/>
* <xsd:element name="f13" type="xsd:date" nillable="true" +minOccurs="0" +prodata:initial="prodata:unknown"/>
* <xsd:element name="f14" type="xsd:decimal" nillable="true" +minOccurs="0" +default="0.0"/>}
* </pre>
*
* @return {@code true} on success. Otherwise the error is already dispatched to
* {@code ErrorManager} before returning.
*
* @throws XMLStreamException
* If XML-related issued occur.
*/
private boolean writeTableSchema(BufferImpl buffer,
TempTableSchema schema,
boolean minSchema,
boolean defaultValues)
throws XMLStreamException
{
return writeTableSchema(buffer, schema, minSchema, defaultValues, false);
}
/**
* Performs the export of a TEMP-TABLE schema.
*
* @param buffer
* A buffer of a {@code TempTable} to be described.
* @param schema
* The schema of the TEMP-TABLE.
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}. Not
* currently honored.
* @param defaultValues
* If {@code true}, record values which match default initial values will be omitted
* from serialized output, else they will be included.
* NOTE: actually setting [defaultValues] to [TRUE] which is the [omit-initial-values] from
* ABL will cause some extra info to be added in field description (see + attributes):
* <pre>{@code
* <xsd:element name="f11" type="xsd:string" nillable="true" +minOccurs="0" +default=""/>
* <xsd:element name="f12" type="xsd:int" nillable="true" +minOccurs="0" +default="0"/>
* <xsd:element name="f13" type="xsd:date" nillable="true" +minOccurs="0" +prodata:initial="prodata:unknown"/>
* <xsd:element name="f14" type="xsd:decimal" nillable="true" +minOccurs="0" +default="0.0"/>}
* </pre>
* @param inDataset
* If {@code true}, this is part of a dataset SCHEMA and additional attributes will
* be written.
*
* @return {@code true} on success. Otherwise the error is already dispatched to
* {@code ErrorManager} before returning.
*
* @throws XMLStreamException
* If XML-related issued occur.
*/
private boolean writeTableSchema(BufferImpl buffer,
TempTableSchema schema,
boolean minSchema,
boolean defaultValues,
boolean inDataset)
throws XMLStreamException
{
String tableName = schema.getTableName();
String legacyName = schema.getName();
AbstractTempTable tempTable = (AbstractTempTable) buffer.buffer().getParentTable();
String xmlNodeName = tempTable.getXmlNodeName().toJavaType();
String nsPrefix = tempTable.namespacePrefix().toJavaType();
String nsUri = tempTable.namespaceURI().toJavaType();
boolean isDataSet = inDataset || buffer.isBeforeBuffer() || buffer.isAfterBuffer();
if (!inDataset && nsUri != null && !nsUri.isEmpty() && (nsPrefix == null || nsPrefix.isEmpty()))
{
nsPrefix = "tns";
}
writeSchema(nsPrefix, nsUri);
{
writeTableElement(schema, minSchema, tableName, legacyName, xmlNodeName,
tempTable.namespacePrefix().toJavaType(), isDataSet);
{
writeStartElement("xsd:complexType");
{
writeStartElement("xsd:sequence");
{
String rowName = (xmlNodeName == null ? tableName : xmlNodeName) +
(inDataset ? "" : "Row");
if (!writeTableSchemaImpl(buffer, schema, rowName,
defaultValues, minSchema, inDataset))
{
return false;
}
}
writeEndElement(); // xsd:sequenc
}
writeEndElement(); // xsd:complexType
// in case of standalone TEMP-TABLE schema serialization unique indexes go here:
if (!external && /*!minSchema &&*/ this.ds == null && !indexes.isEmpty())
{
for (Map.Entry<Integer, Pair<String, TableMapper.LegacyIndexInfo>> indexEntry :
indexes.entrySet())
{
Pair<String, TableMapper.LegacyIndexInfo> idxPair = indexEntry.getValue();
TableMapper.LegacyIndexInfo index = idxPair.getRight();
if (!index.isUnique())
{
continue; // only UNIQUE indexes here
}
indent();
writer.writeStartElement("xsd:unique");
writer.writeAttribute("name", index.getLegacyName());
if (index.isEffectivePrimary())
{
writer.writeAttribute("prodata:primaryIndex", "true");
}
if (index.isWord())
{
writer.writeAttribute("prodata:wordIndex", "true");
}
lineBreak();
++indentLevel;
{
indent();
writer.writeEmptyElement("xsd:selector");
String idxName = (isEmpty(nsPrefix))
? (".//" + idxPair.getLeft())
: (".//" + nsPrefix + ":" + idxPair.getLeft());
writer.writeAttribute("xpath", idxName);
lineBreak();
String[] comps = index.getComponents();
boolean[] dirs = index.getComponentsDirection();
for (int i = 0; i < comps.length; i++)
{
indent();
writer.writeEmptyElement("xsd:field");
String compName = (isEmpty(nsPrefix)) ? comps[i] : (nsPrefix + ":" + comps[i]);
writer.writeAttribute("xpath", compName);
if (!dirs[i])
{
writer.writeAttribute("prodata:descending", "true");
}
lineBreak();
}
}
writeEndElement(); // prodata:index
}
}
}
writeEndElement(); // xsd:element
boolean hasIndexes = !external && !minSchema && this.ds == null
/*&& !this.isSchemaOnly*/ && !indexes.isEmpty();
int idxCounter = 0;
if (hasIndexes)
{
idxCounter = indexes.size();
for (Map.Entry<Integer, Pair<String, TableMapper.LegacyIndexInfo>> pair : indexes.entrySet())
{
if (pair.getValue().getRight().isUnique())
{
// decrement counter for unique indexes. The remainder is the count of non-unique indexes
--idxCounter;
}
}
}
// in case of standalone TEMP-TABLE schema serialization non unique indexes go here:
if (hasIndexes && idxCounter > 0)
{
writeStartElement("xsd:annotation");
{
writeStartElement("xsd:appinfo");
{
for (Map.Entry<Integer, Pair<String, TableMapper.LegacyIndexInfo>> indexEntry :
indexes.entrySet())
{
Pair<String, TableMapper.LegacyIndexInfo> idxPair = indexEntry.getValue();
TableMapper.LegacyIndexInfo index = idxPair.getRight();
if (index.isUnique())
{
continue; // only NON-unique indexes here
}
indent();
writer.writeStartElement("prodata:index");
{
writer.writeAttribute("name", index.getLegacyName());
if (index.isEffectivePrimary())
{
writer.writeAttribute("prodata:primaryIndex", "true");
}
if (index.isWord())
{
writer.writeAttribute("prodata:wordIndex", "true");
}
lineBreak();
++indentLevel;
{
indent();
writer.writeEmptyElement("prodata:table");
writer.writeAttribute("name", legacyName);
lineBreak();
String[] comps = index.getComponents();
boolean[] dirs = index.getComponentsDirection();
for (int i = 0; i < comps.length; i++)
{
indent();
writer.writeEmptyElement("prodata:field");
writer.writeAttribute("name", comps[i]);
if (!dirs[i])
{
writer.writeAttribute("prodata:descending", "true");
}
lineBreak();
}
}
}
writeEndElement(); // prodata:index
}
}
writeEndElement(); // xsd:appinfo
}
writeEndElement(); // xsd:sequence
}
}
if (!external)
{
writeEndElement(); // xsd:schema
}
return true;
}
/**
* Write start of the element.
*
* @param name
* element name.
*/
private void writeStartElement(String name)
throws XMLStreamException
{
indent();
try
{
writer.writeStartElement(name);
}
catch (XMLStreamException e)
{
LOG.severe("", e);
}
lineBreak();
++indentLevel;
}
/**
* Write the element.
* @param bodyWriter
* body writer
* @param attrWriter
* attributes' writer
*/
private void writeComplexType(XmlElementWriter bodyWriter, XmlElementWriter attrWriter)
throws XMLStreamException
{
indent();
writer.writeStartElement("xsd:complexType");
lineBreak();
++indentLevel;
{
indent();
writer.writeStartElement("xsd:sequence");
lineBreak();
++indentLevel;
bodyWriter.run();
writeEndElement(); // xsd:sequence
attrWriter.run();
}
writeEndElement(); // xsd:complexType
}
/**
* Write the element.
*
* @param name
* element name.
* @param attrWriter
* attributes' writer
*/
private void writeElement(String name, XmlElementWriter attrWriter)
throws XMLStreamException
{
indent();
writer.writeStartElement(name);
attrWriter.run();
writer.writeEndElement();
lineBreak();
}
/**
* Write the element with text node only.
*
* @param name
* element name.
* @param textWriter
* attributes' writer
*/
private void writeTextElement(String name, XmlElementWriter attrWriter, XmlElementWriter textWriter)
throws XMLStreamException
{
indent();
writer.writeStartElement(name);
attrWriter.run();
textWriter.run();
writer.writeEndElement();
lineBreak();
}
/**
* Write the element.
*
* @param name
* element name.
* @param attrWriter
* attributes' writer
* @param bodyWriter
* body writer
*/
private void writeElement(String name, XmlElementWriter attrWriter, XmlElementWriter bodyWriter)
throws XMLStreamException
{
indent();
writer.writeStartElement(name);
attrWriter.run();
lineBreak();
++indentLevel;
bodyWriter.run();
--indentLevel;
indent();
writer.writeEndElement();
lineBreak();
}
/**
* Write end of the element.
*/
private void writeEndElement()
throws XMLStreamException
{
--indentLevel;
indent();
try
{
writer.writeEndElement();
}
catch (XMLStreamException e)
{
LOG.severe("", e);
throw e;
}
lineBreak();
}
/**
* Write <code>element</code> element of a table.
*
* @param schema
* table schema.
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information.
* @param tableName
* table name.
* @param legacyName
* table legacy name.
* @param xmlNodeName
* table XML node name.
* @param nsPrefix
* table XML prefix.
* @param isDataSet
* flag indicating that the table is a part of a data-set.
*
* @throws XMLStreamException
* on error
*/
private void writeTableElement(TempTableSchema schema, boolean minSchema,
String tableName,String legacyName, String xmlNodeName,
String nsPrefix, boolean isDataSet)
throws XMLStreamException
{
indent();
writer.writeStartElement("xsd:element");
writer.writeAttribute("name", xmlNodeName == null ? tableName : xmlNodeName);
if (!external)
{
writer.writeAttribute("prodata:proTempTable", "true");
// String.valueOf(isDataSet)); TODO: when is this attribute 'false' ?
}
if (isDataSet && !legacyName.equals(tableName))
{
writer.writeAttribute("prodata:tableName", legacyName);
}
if (!isEmpty(nsPrefix))
{
writer.writeAttribute("prodata:prefix", nsPrefix);
}
if (!minSchema && !schema.isNoUndo())
{
writer.writeAttribute("prodata:undo", "true"); // TODO: where this value came from ?
}
if (xmlNodeName != null && !xmlNodeName.equals(tableName))
{
writer.writeAttribute("prodata:tableName", tableName);
}
lineBreak();
++indentLevel;
}
/**
* Write <code>schema</code> element of a table/data-set.
*
* @param nsPrefix
* XML prefix of a table/data-set.
* @param nsUri
* XML namespace URI of a table/data-set.
*
* @throws XMLStreamException
* on error.
*/
private void writeSchema(String nsPrefix, String nsUri)
throws XMLStreamException
{
indent();
writer.writeStartElement("xsd:schema");
writer.writeAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
writer.writeAttribute("xmlns", nsUri == null ? "" : nsUri);
if (nsPrefix != null && !nsPrefix.isEmpty() && nsUri != null)
{
writer.writeAttribute("xmlns:" + nsPrefix, nsUri);
writer.writeAttribute("targetNamespace", nsUri);
writer.writeAttribute("elementFormDefault", "qualified");
}
writer.writeAttribute("xmlns:prodata", "urn:schemas-progress-com:xml-prodata:0001");
for (Map.Entry<String, String> e: extSchemas.entrySet())
{
String ns = e.getKey();
if (!nsUri.equals(ns) && !"".equals(ns))
{
writer.writeAttribute("xmlns:" + prefixByNs.get(ns), ns);
}
}
lineBreak();
++indentLevel;
for (Map.Entry<String, String> e: extSchemas.entrySet())
{
indent();
writer.writeStartElement("xsd:import");
writer.writeAttribute("namespace", e.getKey());
writer.writeAttribute("schemaLocation", e.getValue());
writer.writeEndElement();
lineBreak();
}
}
/**
* Writes a XML-SCHEMA to XML stream.
*
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}. Not
* currently honored.
* @param defaultValues
* If {@code true}, the initial values for the fields will be written.
* @param nodeName
* The {@code DataSet}'s node name.
*
* @return {@code true} on success. Otherwise the error is already dispatched to
* {@code ErrorManager} before returning.
*
* @throws XMLStreamException
* If XML-related issued occurs.
*/
private boolean writeDatasetSchemaImpl(boolean minSchema,
boolean defaultValues,
String nodeName)
throws XMLStreamException
{
for(String ns: tablesByNs.keySet())
{
if (nsUri.equals(ns))
{
continue;
}
if (!writeExternalSchema(ns, minSchema, defaultValues))
{
return false;
}
}
writeElement("xsd:schema", () -> {
writer.writeAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
writer.writeAttribute("xmlns", nsUri);
if (!nsUri.isEmpty())
{
writer.writeAttribute("xmlns:" + prefixByNs.get(nsUri), nsUri);
writer.writeAttribute("targetNamespace", nsUri);
writer.writeAttribute("elementFormDefault", "qualified");
}
writer.writeAttribute("xmlns:prodata", "urn:schemas-progress-com:xml-prodata:0001");
for (Map.Entry<String, String> e: prefixByNs.entrySet())
{
String ns = e.getKey();
if (!nsUri.equals(ns) && !"".equals(ns))
{
writer.writeAttribute("xmlns:" + e.getValue(), ns);
}
}
},
() -> writeDsSchemaBody(nodeName, minSchema, defaultValues)
);
return true;
}
/**
* Write DATA-SET schema body
*
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}. Not
* currently honored.
* @param defaultValues
* If {@code true}, the initial values for the fields will be written.
* @param nodeName
* The {@code DataSet}'s node name.
*
* @throws XMLStreamException
* If XML-related issued occurs.
*/
private void writeDsSchemaBody(String nodeName, boolean minSchema, boolean defaultValues)
throws XMLStreamException
{
for (Map.Entry<String, String> e: extSchemas.entrySet())
{
writeElement("xsd:import", () -> {
writer.writeAttribute("namespace", e.getKey());
writer.writeAttribute("schemaLocation", e.getValue());
});
}
if (!serializeHidden)
{
rootsWritten++;
writeElement("xsd:element", () -> {
writer.writeAttribute("name", nodeName);
writer.writeAttribute("prodata:proDataSet", "true");
if (!isEmpty(nsPrefix))
{
writer.writeAttribute("prodata:prefix", nsPrefix);
}
if (!nodeName.equals(dsName) && dsName != null && !dsName.isEmpty())
{
writer.writeAttribute("prodata:datasetName", dsName);
}
},
() -> {
writeComplexType(() -> writeDsTables(minSchema, defaultValues), NOOP);
writeDatasetIndexes();
writeDataSetKeyRefs();
});
}
else
{
writeDsTables(minSchema, defaultValues);
}
if (!minSchema || prodataRelationCount > 0 )
{
writeAnnotation(minSchema);
}
}
/**
* Write DATA-SET tables' schemas
*
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}. Not
* currently honored.
* @param defaultValues
* If {@code true}, the initial values for the fields will be written.
* @throws XMLStreamException
* If XML-related issued occurs.
*/
private void writeDsTables(boolean minSchema, boolean defaultValues)
throws XMLStreamException
{
if (ds == null)
{
return;
}
int ttCount = ds.numBuffers().intValue();
for (int k = 0; k < ttCount; k++)
{
BufferImpl after = (BufferImpl) ds.getBufferByIndex(k + 1);
writeDsTableSchema(after, minSchema, defaultValues);
}
}
/**
* Write DATA-SET table schema
*
* @param after
* table buffer.
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}. Not
* currently honored.
* @param defaultValues
* If {@code true}, the initial values for the fields will be written.
*
* @throws XMLStreamException
* If XML-related issued occurs.
*/
private void writeDsTableSchema(BufferImpl after, boolean minSchema,boolean defaultValues)
throws XMLStreamException
{
TemporaryBuffer buffer = (TemporaryBuffer) after.buffer();
TempTableSchema schema = new TempTableSchema(buffer, false);
String tname = schema.getName();
if (!writtenSchemas.add(tname))
{
return;
}
if (!tablesByNs.get(nsUri).stream().
anyMatch(tts -> tname.equals(tts.getName())))
{
// already serialized to external schema
if (!parentByChild.containsKey(tname)) // not a child of a nested relation
{
writeElement("xsd:element", () -> {
writer.writeAttribute("ref",
prefixByNs.get(schema.namespaceURI()) + ":" + tname);
writer.writeAttribute("minOccurs", "0");
writer.writeAttribute("maxOccurs", "unbounded");
});
}
}
else if (!parentByChild.containsKey(tname))
{
if (!writeTableSchemaImpl(
after, schema, schema.getTableName(), defaultValues, minSchema, true))
{
throw new XMLStreamException("Failed to write schema for table [" +
schema.getTableName() + "]");
}
}
}
/**
* Write DATA-SET schema annotation
*
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}. Not
* currently honored.
*
* @throws XMLStreamException
* If XML-related issued occurs.
*/
private void writeAnnotation(boolean minSchema) throws XMLStreamException
{
if (prodataRelationCount > 0 || (/* !this.isSchemaOnly && */ !indexes.isEmpty()))
{
indent();
writer.writeStartElement("xsd:annotation");
lineBreak();
++indentLevel;
{
indent();
writer.writeStartElement("xsd:appinfo");
lineBreak();
++indentLevel;
{
// if (!isSchemaOnly)
// {
if(!minSchema)
{
for (Map.Entry<Integer, Pair<String, TableMapper.LegacyIndexInfo>> indexEntry :
indexes.entrySet())
{
Pair<String, TableMapper.LegacyIndexInfo> idxPair = indexEntry.getValue();
TableMapper.LegacyIndexInfo index = idxPair.getRight();
if (index.isUnique())
{
continue; // only NON-unique indexes here
}
indent();
writer.writeStartElement("prodata:index");
writer.writeAttribute("name", index.getLegacyName());
if (index.isEffectivePrimary())
{
writer.writeAttribute("prodata:primaryIndex", "true");
}
if (index.isWord())
{
writer.writeAttribute("prodata:wordIndex", "true");
}
lineBreak();
++indentLevel;
{
indent();
writer.writeEmptyElement("prodata:table");
writer.writeAttribute("name", idxPair.getLeft());
lineBreak();
String[] comps = index.getComponents();
boolean[] dirs = index.getComponentsDirection();
for (int i = 0; i < comps.length; i++)
{
indent();
writer.writeEmptyElement("prodata:field");
writer.writeAttribute("name", comps[i]);
if (!dirs[i])
{
writer.writeAttribute("prodata:descending", "true");
}
lineBreak();
}
}
writeEndElement(); // prodata:index
}
}
}
for (int k = 0; k < relCount; k++)
{
DataRelation rel = (DataRelation) ds.getRelationNative(k + 1);// base-1
if (keyrefs.contains(rel))
{
continue; // this relation was serialized as a [xsd:keyref] above
}
if (rel.isParentIdRelation().getValue())
{
continue;
}
indent();
writer.writeEmptyElement("prodata:relation");
writer.writeAttribute("name", rel.name().toStringMessage());
if (rel.isNested().booleanValue())
{
writer.writeAttribute("prodata:nested", "true");
}
if (!rel.isActive().booleanValue())
{
writer.writeAttribute("prodata:notActive", "true");
}
if (rel.getForeignKeyHidden().toJavaType())
{
writer.writeAttribute("prodata:foreignKeyHidden", "true");
}
if (rel.isRecursive().booleanValue())
{
writer.writeAttribute("prodata:recursive", "true");
}
writer.writeAttribute("prodata:parent",
rel.getParentBufferNative().name().toStringMessage());
writer.writeAttribute("prodata:child",
rel.getChildBufferNative().name().toStringMessage());
writer.writeAttribute("prodata:relationFields", rel.getRelationFields().toStringMessage());
lineBreak();
}
writeEndElement(); // xsd:appinfo
}
writeEndElement(); // xsd:annotation
}
}
/**
* Write DATA-SET data relations.
*
* @throws XMLStreamException
* If XML-related issued occurs.
*/
private void writeDataSetKeyRefs() throws XMLStreamException
{
// compute and dump all KEYREFs (relations which have the parent looked up by a unique index)
relCount = ds.getNumRelations().intValue();
prodataRelationCount = 0;
for (int k = 0; k < relCount; k++)
{
DataRelation rel = (DataRelation) ds.getRelationNative(k + 1); // base-1
if (!rel.isActive().booleanValue())
{
continue;
}
String[] parentFields = rel.getRelationFields(true);
boolean isKeyref = false;
String idxName = null;
TableMapper.LegacyIndexInfo legacyIndexInfo = null;
BufferImpl pBuff = (BufferImpl) rel.getParentBufferNative();
TempTable pTempTable = (TempTable) pBuff.tableHandleNative();
int j = 0;
while (true)
{
legacyIndexInfo = TableMapper.getLegacyIndexInfo(pTempTable, j++);
if (legacyIndexInfo == null)
{
break; // no more indices
}
if (!legacyIndexInfo.isUnique())
{
continue; // this is not unique, skip to next
}
// check fields
String[] components = legacyIndexInfo.getComponents();
if (components.length != parentFields.length)
{
continue; // not same number of components, skip to next index
}
boolean match = true;
for (int i = 0; i < components.length; i++)
{
if (!components[i].equals(parentFields[i]))
{
match = false;
break;
}
}
if (!match)
{
continue; // this is unique but does not match fields, skip to next
}
// reaching this point means we have a candidate for keyref
isKeyref = true;
idxName = uidxUniqueNames.get(legacyIndexInfo);
if (idxName == null)
{
idxName = legacyIndexInfo.getLegacyName();
}
break;
}
if (isKeyref)
{
indent();
String prefix1 = prefixByNs.get(nsUri);
String prefix2 = prefixByNs.get(
tables.get(rel.getChildBufferNative().name().toJavaType()). namespaceURI()
);
if (!prefix1.isEmpty())
{
prefix1 += ":";
}
if (!prefix2.isEmpty())
{
prefix2 += ":";
}
writer.writeStartElement("xsd:keyref");
writer.writeAttribute("name", rel.name().toJavaType());
writer.writeAttribute("refer", prefix1 + idxName);
if (rel.isNested().toJavaType())
{
writer.writeAttribute("prodata:nested", "true");
}
if (rel.getForeignKeyHidden().toJavaType())
{
writer.writeAttribute("prodata:foreignKeyHidden", "true");
}
lineBreak();
++indentLevel;
{
indent();
writer.writeStartElement("xsd:selector");
writer.writeAttribute("xpath",
".//" + prefix2 + rel.getChildBufferNative().name().toJavaType());
writer.writeEndElement();
lineBreak();
for (String field : rel.getRelationFields(false))
{
indent();
writer.writeStartElement("xsd:field");
writer.writeAttribute("xpath", prefix2 + field);
writer.writeEndElement();
lineBreak();
}
}
writeEndElement(); // xsd:unique
keyrefs.add(rel);
}
else if (!rel.isParentIdRelation().getValue())
{
prodataRelationCount++;
}
}
}
/**
* Write DATA-SET indexes
*
* @throws XMLStreamException
* If XML-related issued occurs.
*/
private void writeDatasetIndexes() throws XMLStreamException
{
uidxByUniqueNames.clear();
uidxUniqueNames.clear();
// dump all UNIQUE indexes here
for (Map.Entry<Integer, Pair<String, TableMapper.LegacyIndexInfo>> indexEntry : indexes.entrySet())
{
Pair<String, TableMapper.LegacyIndexInfo> idxPair = indexEntry.getValue();
TableMapper.LegacyIndexInfo index = idxPair.getRight();
if (!index.isUnique())
{
continue; // only UNIQUE indexes here
}
indent();
String name = index.getLegacyName();
String prodataIndexName = null;
if (uidxByUniqueNames.containsKey(name))
{
prodataIndexName = name;
name = idxPair.getKey() + "_" + name;
}
uidxByUniqueNames.put(name, index);
writer.writeStartElement("xsd:unique");
writer.writeAttribute("name", name);
if (prodataIndexName != null)
{
uidxUniqueNames.put(index, name);
writer.writeAttribute("prodata:indexName", prodataIndexName);
}
if (index.isEffectivePrimary())
{
writer.writeAttribute("prodata:primaryIndex", "true");
}
if (index.isWord())
{
writer.writeAttribute("prodata:wordIndex", "true");
}
lineBreak();
++indentLevel;
{
TempTableSchema tts = tables.get(idxPair.getLeft());
String ttsNsUri = tts.namespaceURI();
String prefix = prefixByNs.get(ttsNsUri);
if (!prefix.isEmpty())
{
prefix += ":";
}
indent();
writer.writeEmptyElement("xsd:selector");
writer.writeAttribute("xpath", ".//" + prefix + idxPair.getLeft());
lineBreak();
String[] comps = index.getComponents();
boolean[] dirs = index.getComponentsDirection();
for (int i = 0; i < comps.length; i++)
{
indent();
writer.writeEmptyElement("xsd:field");
writer.writeAttribute("xpath", prefix + comps[i]);
if (!dirs[i])
{
writer.writeAttribute("prodata:descending", "true");
}
lineBreak();
}
}
writeEndElement();
}
}
/**
* Write external schema for a namespace
*
* @param ns
* namespace
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}. Not
* currently honored.
* @param defaultValues
* If {@code true}, the initial values for the fields will be written.
*
* @return name of the schema file
*
* @throws XMLStreamException
* If XML-related issued occur.
*/
private boolean writeExternalSchema(String ns,
boolean minSchema, boolean defaultValues)
throws XMLStreamException
{
LinkedBlockingQueue<String> childs = new LinkedBlockingQueue<>();
Map<String, String> imports = new LinkedHashMap<>();
tablesByNs.get(ns).stream().map(TempTableSchema::getName).forEach(childs::offer);
for (String name = childs.poll(); name != null; name = childs.poll())
{
TempTableSchema tts = tables.get(name);
String cns = tts.namespaceURI();
if (!ns.equals(cns))
{
imports.putIfAbsent(cns, extSchemaName(cns));
}
else {
List<String> clist = childByParent.get(name);
if (clist != null)
{
clist.stream().forEach(childs::offer);
}
}
}
String extSchemaName = extSchemaName(ns);
extSchemas.put(ns, extSchemaName);
Path path = Paths.get(((character) target.getDestination()).toJavaType());
String extPath = path.getParent().toString() + "/" + extSchemaName;
XmlExport export = new XmlExport(new TargetData("file", extPath), format, encoding);
export.external = true;
export.ds = ds;
export.dsName = dsName;
export.nsUri = nsUri;
export.nsPrefix = nsPrefix;
export.serializeHidden = serializeHidden;
export.tablesByNs = tablesByNs;
export.prefixByNs = prefixByNs;
export.extSchemas = imports;
export.childFieldsByChild = childFieldsByChild;
boolean ok = export.write(
() -> {
boolean rc = true;
export.writeSchema(prefixByNs.get(ns), ns);
{
for(TempTableSchema tts: tablesByNs.get(ns))
{
TemporaryBuffer tb = (TemporaryBuffer)tts.buffer();
AbstractTempTable tempTable = (AbstractTempTable) tb.buffer().getParentTable();
String tableName = tts.getTableName();
String legacyName = tts.getName();
String xmlNodeName = tempTable.getXmlNodeName().toJavaType();
String nsUri = tts.namespaceURI();
String nsPrefix = tts.namespacePrefix();
export.writeTableElement(tts, minSchema, tableName, legacyName,
xmlNodeName, nsPrefix, true);
{
rc = export.writeTableSchemaImpl((BufferImpl)tb.getDMOProxy(), tts,
tts.getTableName(), defaultValues, minSchema, true);
export.writeEndElement(); // xsd:element
if (!rc)
{
break;
}
}
}
}
export.writeEndElement(); // xsd:schema
return rc;
}, TargetData.TD_WRITE_XMLSCHEMA, LegacyResource.DATASET + " widget"
);
if (!ok)
{
return false;
}
this.indexes.putAll(export.indexes);
this.uidxByUniqueNames.putAll(export.uidxByUniqueNames);
this.uidxUniqueNames.putAll(export.uidxUniqueNames);
return true;
}
/**
* Get the name of the external schema for the namespace.
*
* @param ns
* namespace
*
* @return the name of the external schema for the namespace
*/
private String extSchemaName(String ns)
{
String ttsn = tablesByNs.get(ns).get(0).getName();
String fn = Paths.get(((character) target.getDestination()).toJavaType()).
getFileName().toString();
return fn.substring(0, fn.length() - ".xsd".length()) + "_" + ttsn + ".xsd";
}
/**
* Writes the XML-SCHEMA of a TEMP-TABLE to XML stream.
*
* @param buffer
* The buffer that identifies the TEMP-TABLE to be described.
* @param schema
* The schema of the TEMP-TABLE.
* @param tableName
* The name of the table.
* @param defaultValues
* If {@code true} the initial values are written for the fields that declared them.
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}.
* @param inDataset
* If {@code true}, this is part of a dataset SCHEMA and additional attributes will
* be written.
*
* @return {@code true} on success. Otherwise the error is already dispatched to
* {@code ErrorManager} before returning.
*
* @throws XMLStreamException
* If XML-related issued occurs.
*/
private boolean writeTableSchemaImpl(BufferImpl buffer,
TempTableSchema schema,
String tableName,
boolean defaultValues,
boolean minSchema,
boolean inDataset)
throws XMLStreamException
{
String legacyName = schema.getName();
boolean isDataset = (ds != null);
if (external)
{
writeTableColums(buffer, schema, tableName, defaultValues, minSchema, inDataset, isDataset);
}
else
{
if (serializeHidden && rootsWritten == 0)
{
rootTableName = tableName;
}
writeElement("xsd:element", () -> {
writer.writeAttribute("name", tableName);
if (serializeHidden && rootsWritten == 0)
{
String prefix = !isEmpty(ttNsPrefix)? ttNsPrefix : nsPrefix;
if (!isEmpty(prefix) )
{
writer.writeAttribute("prodata:prefix", prefix);
}
}
if (!nested && rootsWritten++ > 0 || ds == null)
{
writer.writeAttribute("minOccurs", "0");
writer.writeAttribute("maxOccurs", "unbounded");
}
if (isDataset && !legacyName.equals(tableName))
{
writer.writeAttribute("prodata:tableName", legacyName);
}
if (inDataset && !minSchema && !schema.isNoUndo())
{
writer.writeAttribute("prodata:undo", "true");
}
if (ds != null && buffer.isAfterBuffer())
{
// using [buffer.beforeBuffer().unwrap().name()] will raise a [DeferredLegacyErrorException]
String beforeName = ((BufferImpl) buffer.beforeBufferNative()).doGetName();
writer.writeAttribute("prodata:beforeTable", beforeName);
}
},
() -> {
writeTableColums(buffer, schema, tableName, defaultValues, minSchema, inDataset, isDataset);
if (serializeHidden && tableName.equals(rootTableName))
{
writeDatasetIndexes();
writeDataSetKeyRefs();
}
});
}
// save now the index list to be processed later
int keyPrefix = (this.ds == null) ? 0 : ds.getIndexOf(buffer);
int i = 0;
Iterator<TableMapper.LegacyIndexInfo> schemaIndexes = schema.getIndexes();
while (schemaIndexes.hasNext())
{
TableMapper.LegacyIndexInfo index = schemaIndexes.next();
this.indexes.put(keyPrefix * 100 + i,
new ImmutablePair<>(index.isUnique() ? tableName : legacyName, index));
i++;
}
return true;
}
/**
* Write table schema columns
*
* @param buffer
* The buffer that identifies the TEMP-TABLE to be described.
* @param schema
* The schema of the TEMP-TABLE.
* @param tableName
* The name of the table.
* @param defaultValues
* If {@code true} the initial values are written for the fields that declared them.
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}.
* @param inDataset
* If {@code true}, this is part of a dataset SCHEMA and additional attributes will
* be written.
* @param isDataset
* Flag indicating that the table is a part of a dataset.
*
* @throws XMLStreamException
* If XML-related issued occurs.
*/
private void writeTableColums(BufferImpl buffer, TempTableSchema schema, String tableName,
boolean defaultValues, boolean minSchema, boolean inDataset, boolean isDataset)
throws XMLStreamException
{
Iterable<TempTableSchema.Column> elements = schema.elementColumns();
Iterable<TempTableSchema.Column> attributes = schema.attributeColumns();
boolean hasAttrs = attributes.iterator().hasNext();
SortedSet<Integer> fields = new TreeSet<>();
if (hasAttrs)
{
ArrayList<TempTableSchema.Column> columns = schema.columns();
for (int k = 0; k < columns.size(); k++)
{
TempTableSchema.Column column = columns.get(k);
if (column.getNodeType() != XmlNodeType.HIDDEN)
{
fields.add(column.getOrder());
}
}
}
writeComplexType(() -> {
for (TempTableSchema.Column column : elements)
{
if (!writeColumnAttrs(schema, column, fields,
hasAttrs, defaultValues, minSchema, isDataset))
{
throw new XMLStreamException("Failed to table element [" + column.getFieldName() + "]");
}
lineBreak();
}
if (buffer.isBeforeBuffer() && ds == null) // only for TEMP-TABLE:WRITE-XML(SCHEMA) API
{
writeBeforeBuffer();
}
DataSet ds = buffer._dataSet();
if (inDataset && ds != null)
{
List<DataRelation> childRels = ds.getRelations(buffer, true, false, true);
for (DataRelation dr : childRels)
{
if (!dr.isNested().getValue() && !dr.isParentIdRelation().getValue())
{
continue;
}
BufferImpl childBuffer = (BufferImpl) dr.getChildBufferNative();
TemporaryBuffer child = (TemporaryBuffer) ((BufferReference) childBuffer).buffer();
TempTableSchema childSchema = new TempTableSchema(child, false);
if (schema.namespaceURI().equals(childSchema.namespaceURI()))
{
boolean ext = this.external;
Set<String> cFields = this.childFields;
this.external = false;
this.nested = ext;
String tname = childSchema.getTableName();
this.childFields = childFieldsByChild.getOrDefault(tname,
Collections.emptySet());
try
{
writeTableSchemaImpl(childBuffer,
childSchema, tname,
defaultValues, minSchema, inDataset);
}
finally
{
this.external = ext;
this.childFields = cFields;
this.nested = false;
}
}
else
{
writeElement("xsd:element", () -> {
writer.writeAttribute("ref",
prefixByNs.get(childSchema.namespaceURI()) + ":" + childSchema.getName());
writer.writeAttribute("minOccurs", "0");
writer.writeAttribute("maxOccurs", "unbounded");
});
}
}
}
if (!hasAttrs && tableName.equals(rootTableName))
{
writeDsTables(minSchema, defaultValues);
}
},
() -> {
for (TempTableSchema.Column column : attributes)
{
if (!writeColumnAttrs(schema, column, fields,
hasAttrs, defaultValues, minSchema, isDataset))
{
throw new XMLStreamException("Failed to table elementL [" + column.getFieldName() + "]");
}
lineBreak();
}
if (hasAttrs && tableName.equals(rootTableName))
{
writeDsTables(minSchema, defaultValues);
}
});
}
/**
* Write BEFORE-BUFFER data.
*
* @throws XMLStreamException
* If XML-related issued occurs.
*/
private void writeBeforeBuffer() throws XMLStreamException
{
// <xsd:element name="__error-flag__" type="xsd:int" nillable="true" prodata:initial="prodata:unknown"/>
writeElement("xsd:element", () -> {
writer.writeAttribute("name", Buffer.__ERROR_FLAG__);
writer.writeAttribute("type", "xsd:int");
writer.writeAttribute("nillable", "true");
writer.writeAttribute("prodata:initial", "prodata:unknown");
});
// <xsd:element name="__origin-rowid__" type="xsd:base64Binary" nillable="true" prodata:dataType="prodata:rowid"/>
writeElement("xsd:element", () -> {
writer.writeAttribute("name", Buffer.__ORIGIN_ROWID__);
writer.writeAttribute("type", "xsd:base64Binary");
writer.writeAttribute("nillable", "true");
writer.writeAttribute("prodata:dataType", "prodata:rowid");
});
// Buffer.__DATA_SOURCE_ROWID__ field is not serialized
// <xsd:element name="__error-string__" type="xsd:string" nillable="true" prodata:initial="prodata:unknown"/>
writeElement("xsd:element", () -> {
writer.writeAttribute("name", Buffer.__ERROR_STRING__);
writer.writeAttribute("type", "xsd:string");
writer.writeAttribute("nillable", "true");
writer.writeAttribute("prodata:initial", "prodata:unknown");
});
// <xsd:element name="__after-rowid__" type="xsd:base64Binary" nillable="true" prodata:dataType="prodata:rowid"/>
writeElement("xsd:element", () -> {
writer.writeAttribute("name", Buffer.__AFTER_ROWID__);
writer.writeAttribute("type", "xsd:base64Binary");
writer.writeAttribute("nillable", "true");
writer.writeAttribute("prodata:dataType", "prodata:rowid");
});
// <xsd:element name="__row-state__" type="xsd:int" nillable="true" prodata:initial="prodata:unknown"/>
writeElement("xsd:element", () -> {
writer.writeAttribute("name", Buffer.__ROW_STATE__);
writer.writeAttribute("type", "xsd:int");
writer.writeAttribute("nillable", "true");
writer.writeAttribute("prodata:initial", "prodata:unknown");
});
}
/**
* Writes the XML-SCHEMA element or attrubute of a TEMP-TABLE column to XML stream.
*
* @param schema
* The schema of the TEMP-TABLE.
* @param defaultValues
* If {@code true} the initial values are written for the fields that declared them.
* @param column
* The table column in question
* @param fields
* Sorted set of the taable fields ORDERs (to calculate userOrder attribute)
* @param hasAttributes
* Flag indicating that schema contains attributes
* @param minSchema
* {@code true} to write minimal schema information; {@code false} to write verbose
* schema information. Ignored if {@code writeXmlSchema} is {@code false}.
* @param isDataset
* If {@code true}, this is part of a dataset SCHEMA and additional attributes will
* be written.
* @return {@code true} on success. Otherwise the error is already dispatched to
* {@code ErrorManager} before returning.
*
* @throws XMLStreamException
* If XML-related issued occurs.
*/
private boolean writeColumnAttrs(
TempTableSchema schema,
TempTableSchema.Column column,
SortedSet<Integer> fields,
boolean hasAttributes,
boolean defaultValues,
boolean minSchema,
boolean isDataset)
throws XMLStreamException
{
XmlNodeType nodeType = column.getNodeType();
if (nodeType == XmlNodeType.HIDDEN)
{
return true;
}
boolean isAttr = nodeType == XmlNodeType.ATTRIBUTE;
Class<? extends BaseDataType> columnType = column.getType();
String type = Util.xsdMap.get(columnType);
if (type == null)
{
ErrorManager.recordOrShowError(13078, Util.getLegacyType(columnType));
// Unsupported data type for XML Serialization: <type>.
ErrorManager.recordOrShowError(13066, schema.getTableName());
// Unable to write XML Schema for temp-table <table-name>.
ErrorManager.recordOrShowError(13097);
// Write schema failed for WRITE-XMLSCHEMA.
return false;
}
String addType = Util.xsdProdataMap.get(columnType);
indent();
try
{
writer.writeEmptyElement(isAttr ? "xsd:attribute" : "xsd:element");
}
catch (XMLStreamException e)
{
// TODO Auto-generated catch block
LOG.severe("", e);
return false;
}
String name = column.getXmlNodeName();
writer.writeAttribute("name", column.getXmlNodeName());
String xmlDataType = column.getXmlDataType();
if (!StringUtils.isEmpty(xmlDataType))
{
// TODO: validate. Should be OK in a valid temp-table
type = "xsd:" + xmlDataType;
}
writer.writeAttribute("type", type);
if (!isAttr)
{
writer.writeAttribute("nillable", String.valueOf(column.isNillable()));
}
if (addType != null && defaultValues)
{
writer.writeAttribute("prodata:dataType", addType);
}
Integer extent = column.getExtent();
if (isDataset)
{
if (extent != null && extent > 0)
{
writer.writeAttribute("minOccurs", defaultValues ? String.valueOf(extent) : "0");
writer.writeAttribute("maxOccurs", String.valueOf(extent));
}
else if (!defaultValues && !isAttr || childFields.contains(column.getFieldName()))
{
writer.writeAttribute("minOccurs", "0");
}
}
else
{
if (extent != null && extent > 0)
{
writer.writeAttribute("minOccurs", defaultValues ? String.valueOf(extent) : "0");
writer.writeAttribute("maxOccurs", String.valueOf(extent));
}
else if (!defaultValues && !isAttr)
{
writer.writeAttribute("minOccurs", "0");
}
}
if (addType != null && !defaultValues)
{
writer.writeAttribute("prodata:dataType", addType);
}
if (isDataset && defaultValues)
{
// if (!defaultValues)
// {
String initial = column.getInitial();
String defVal = Util.defaultValuesMap.get(columnType);
if (initial == null && !minSchema)
{
if (column.isInitialNull() && !"?".equals(defVal))
{
writer.writeAttribute("prodata:initial", "prodata:unknown");
}
else
{
initial = defVal;
}
}
if (initial != null && !initial.equals(defVal))
{
if (type.startsWith("xsd:date") &&
("today".equalsIgnoreCase(initial) || "now".equalsIgnoreCase(initial)))
{
writer.writeAttribute("prodata:initial", "prodata:" + initial.toLowerCase());
}
else if (initial.equals("?") ||
(column.getType().equals(raw.class) && "".equals(initial)))
{
writer.writeAttribute("prodata:initial", "prodata:unknown");
}
else
{
writer.writeAttribute("default", initial);
}
}
// }
}
else
{
String initial = column.getInitial();
if (initial != null)
{
boolean initialNull = "?".equals(initial) ||
(column.getType().equals(raw.class) && "".equals(initial));
if (!defaultValues || !initial.equals(Util.defaultValuesMap.get(columnType)))
{
if (!initialNull && type.contains("decimal") && !initial.contains("."))
{
// use [decimal] property attribute instead?
initial += ".0";
}
if (type.startsWith("xsd:date") &&
("today".equalsIgnoreCase(initial) || "now".equalsIgnoreCase(initial)))
{
writer.writeAttribute("prodata:initial", "prodata:" + initial.toLowerCase());
}
else
{
writer.writeAttribute(initialNull ? "prodata:initial" : "default",
initialNull ? "prodata:unknown" : initial);
}
}
}
else if (!defaultValues)
{
initial = Util.defaultValuesMap.get(columnType);
if ("?".equals(initial))
{
writer.writeAttribute("prodata:initial", "prodata:unknown");
}
else
{
if (type.contains("decimal") && !initial.contains("."))
{
// use [decimal] property attribute instead?
initial += ".0";
}
writer.writeAttribute("default", initial);
}
}
else if (initial == null)
{
if (column.isInitialNull() && !"?".equals(Util.defaultValuesMap.get(columnType)))
{
writer.writeAttribute("prodata:initial", "prodata:unknown");
}
}
}
String fieldName = column.getFieldName();
if (!name.equals(fieldName))
{
writer.writeAttribute("prodata:fieldName", fieldName);
}
if (!minSchema && hasAttributes)
{
int userOrder = fields.headSet(column.getOrder()).size() + 1;
writer.writeAttribute("prodata:userOrder", String.valueOf(userOrder));
}
if (!minSchema && "prodata:clob".equals(addType))
{
String cp = column.getCodePage();
if (!StringUtils.isEmpty(cp))
{
writer.writeAttribute("prodata:columnCodepage", cp.toUpperCase());
}
}
if (!minSchema )
{
String fmt = column.getDefinedFormat();
BaseDataType defValue = BaseDataType.generateDefault(columnType);
if (!StringUtils.isEmpty(fmt) && (
/* defaultValues || */ !fmt.equalsIgnoreCase(defValue.defaultFormatString())))
{
writer.writeAttribute("prodata:format", fmt);
}
}
if (!minSchema)
{
String label = column.getDefinedLabel();
if (label != null)
{
writer.writeAttribute("prodata:label", label);
}
String columnLabel = column.getColumnLabel();
if (columnLabel != null)
{
writer.writeAttribute("prodata:columnLabel", columnLabel);
}
String hlp = column.getHelp();
if (hlp != null && !hlp.isEmpty())
{
writer.writeAttribute("prodata:help", hlp);
}
}
if (!minSchema && columnType == character.class && column.isCaseSensitive())
{
writer.writeAttribute("prodata:caseSensitive", "true");
}
if (!minSchema && columnType == decimal.class && column.getDecimals() > 0)
{
writer.writeAttribute("prodata:decimals", Integer.toString(column.getDecimals()));
}
return true;
}
/**
* Writes the content of a TEMP-TABLE to serializer.
*
* @param relation
* The relation on which this buffer is a child of.
* @param buffer
* The buffer to be used for accessing TEMP-TABLE records.
* @param rowName
* The name of the XML element that holds a record.
* @param xmlns
* The XML namespace uri, is set.
* @param omitInitialValues
* Flag indicating that initial values should be not serilaized
* @param includeHidden
* If {@code true}, the hidden fields will also be dumped.
* @param includeProds
* If {@code true}, the PROGRESS specific attribute will be written.
* @param before
* This is a {@code BEFORE} table. Rows written will be slightly decorated.
*
* @return number of processed records on success. Otherwise the error is already dispatched to
* {@code ErrorManager} before returning.
*/
private int serializeTempTable(DataRelation relation,
TemporaryBuffer buffer,
String rowName,
String xmlns,
boolean omitInitialValues,
boolean includeHidden,
boolean includeProds,
boolean before)
{
int nr = 0;
TempTableSchema schema = new TempTableSchema(buffer, false);
if (serializeHidden && rootTableName == null)
{
rootTableName = rowName;
}
String unsupportedType = Util.checkSupportedTypes(schema);
if (unsupportedType != null)
{
ErrorManager.recordOrShowError(13093);
// Write temp-table data failed for WRITE-XML.
ErrorManager.recordOrThrowError(13078, unsupportedType, "");
// Unsupported data type for XML Serialization: <type>.
return -1;
}
if (relation == null)
{
// reads records in order of primary index, if available, else in ascending primary key order
nr = buffer.readAllRows(
(dmo) -> writeRecord(
buffer, dmo, rowName, schema, xmlns, includeHidden, includeProds, before, omitInitialValues));
if (rowName.equals(rootTableName) && serializeHidden && hasChild && childRecords > 0)
{
try
{
lineBreak();
}
catch (XMLStreamException ignore)
{
}
}
this.hasChild = false;
return nr;
}
if (!relation.isNested().getValue() && !relation.isParentIdRelation().getValue())
{
this.hasChild = false;
return nr;
}
Set<String> cFields = this.childFields;
childFields = childFieldsByChild.getOrDefault(rowName, Collections.emptySet());
try
{
// build a query depending on the relation, and add each record from the relation
QueryWrapper query = (QueryWrapper) relation.getQuery(true);
query.hintFullResults();
query.queryOpen();
query.first();
while (!query._isOffEnd())
{
nr++;
TempRecord dmo = buffer.getCurrentRecord();
writeRecord(buffer, dmo, rowName, schema, xmlns, includeHidden, includeProds, before,
omitInitialValues);
query._getNext();
}
query.close();
}
finally
{
this.childFields = cFields;
}
this.hasChild = false;
return nr;
}
/**
* Write a temp-table record's data to the XML output stream.
*
* @param buffer
* The buffer being serialized.
* @param dmo
* Object containing temp-table record's data.
* @param rowName
* Name applied to the element or attribute associated with the row data in the XML
* output.
* @param schema
* The table schema to be used.
* @param xmlns
* The XML namespace uri, is set.
* @param hidden
* If {@code true} include the hidden fields ({@code __name__}) for each record
* written to stream.
* @param prodsId
* If {@code true} include the {@code prods:id} attribute for each record written.
* @param before
* {@code true} if a BEFORE-TABLE record is processed.
*/
private void writeRecord(TemporaryBuffer buffer,
TempRecord dmo,
String rowName,
TempTableSchema schema,
String xmlns,
boolean hidden,
boolean prodsId,
boolean before,
boolean omitInitialValues)
{
if (serializeHidden && rowName.equals(rootTableName) && rootTableRecordsWritten > 0 &&
schema.attributeColumns().iterator().hasNext())
{
attrFailure();
return;
}
if (dmo == null)
{
return;
}
// CA: who uses this? I don't see it being used for normal DATASET usage.
// prodsId = false;
// TODO: noInit!!!
try
{
Integer rowState = dmo._rowState();
if (rowState == null)
{
rowState = Buffer.ROW_UNMODIFIED;
}
if (before && rowState == Buffer.ROW_CREATED)
{
// NOTE: it seems like the CREATED records are NOT serialized at all for
// BEFORE TEMP-TABLEs, so these value should not reach past this point, those
// records are cut here.
return;
}
boolean startElementWritten = false;
if (!serializeHidden || !hasChild || schema.attributeColumns().iterator().hasNext())
{
indent();
writer.writeStartElement((isEmpty(ttNsPrefix) || rowName.contains(":") ?
"" : ttNsPrefix + ":") + rowName);
startElementWritten = true;
if (!xsiWritten)
{
if (xmlns == null || xmlns.isEmpty() )
{
xmlns = nsUri;
}
if (xmlns != null && !xmlns.isEmpty())
{
writer.writeAttribute("xmlns" + (isEmpty(ttNsPrefix) ? "" : ":" + ttNsPrefix) , xmlns);
}
writer.writeAttribute(ATTR_NS_XSI, NS_URI);
xsiWritten = true;
}
else if (xmlns != null && !xmlns.isEmpty() && (ds != null || isEmpty(ttNsPrefix) ))
{
writer.writeAttribute("xmlns" + (isEmpty(ttNsPrefix) ? "" : ":" + ttNsPrefix) , xmlns);
}
// NOTE: the next conditional is half commented out because, in some cases which we were not able to
// properly identify, the [prods:id] and [prods:rowState] are not serialized
if (prodsId /*&& before*/)
{
// this links the BEFORE and AFTER image. Because the deleted records (from
// BEFORE-TABLE) do not have a peer their rowid will be used, as an exception.
if (rowState == Buffer.ROW_DELETED || !schema.isBeforeImage())
{
// DELETED before, CREATED or MODIFIED after images
writer.writeAttribute("prods:id", rowName + dmo.primaryKey());
}
else
{
// CREATED or MODIFIED before images
writer.writeAttribute("prods:id", rowName + dmo._peerRowid());
}
if (!before)
{
if (rowState != Buffer.ROW_UNMODIFIED)
{
writer.writeAttribute("prods:rowState", Util.getRowStateAsString(rowState));
}
Integer errorFlags = dmo._errorFlags();
if (errorFlags != null && errorFlags != 0) // also only for 'changes' dataset (?)
{
writer.writeAttribute("prods:hasErrors", "true");
}
String errorString = dmo._errorString();
if (errorString != null) // also only for 'changes' dataset (?)
{
prodsErrors.put(rowName + dmo.primaryKey(), Pair.of(rowName, errorString));
}
}
}
if (!serializeHidden || !rowName.equals(rootTableName) || writeXmlSchema == null)
{
// write all columns which must be serialized as attributes first, while the start element for the
// row is open
writeColumns(dmo, schema.attributeColumns(), hidden, omitInitialValues);
// write all columns which must be serialized to text
writeColumns(dmo, schema.textColumns(), hidden, omitInitialValues);
}
}
++indentLevel;
if ( !serializeHidden || !hasChild || savedHasChild.size() > 1 || childRecords > 0)
{
lineBreak();
}
if (writeXmlSchema != null)
{
writeXmlSchema.run();
writeXmlSchema = null;
rootTableRecordsWritten++;
}
if (serializeHidden && rowName.equals(rootTableName) && rootTableRecordsWritten > 0 &&
schema.attributeColumns().iterator().hasNext())
{
attrFailure();
return;
}
// write all columns which must be serialized to elements as separate XML elements
writeColumns(dmo, schema.elementColumns(), hidden, omitInitialValues);
BufferImpl buf = (BufferImpl) buffer.getDMOProxy();
if (rowName.equals(rootTableName))
{
rootTableRecordsWritten++;
}
writeBeforeData(buf, dmo);
writeChildRecords(buf, dmo, prodsId, omitInitialValues);
--indentLevel;
indent();
boolean openTable = !serializeHidden || !hasChild;
if (openTable || savedHasChild.size() > 0)
{
writer.writeEndElement();
}
if ( openTable || savedHasChild.size() > 1 )
{
lineBreak();
}
}
catch (XMLStreamException exc)
{
ErrorManager.recordOrThrowError(0, exc.getMessage());
}
catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException exc)
{
throw new RuntimeException("Unable to access DMO data", exc);
}
}
/**
* Write before data
*
* @param buf
* The buffer being serialized.
* @param dmo
* Object containing temp-table record's data.
*
* @throws XMLStreamException
* on error
*
*/
private BufferImpl writeBeforeData(BufferImpl buf, TempRecord dmo)
throws XMLStreamException
{
if (buf.isBeforeBuffer() && ds == null)
{
writeDatum(ERROR_FLAG, new integer(dmo._errorFlags()));
writeDatum(ORIGIN_ROWID, new rowid(dmo._originRowid()));
writeDatum(DATASOURCE_ROWID, new rowid(dmo._datasourceRowid()));
writeDatum(ERROR_STRING, new character(dmo._errorString()));
writeDatum(AFTER_ROWID, new rowid(dmo._peerRowid()));
writeDatum(ROW_STATE, new integer(dmo._rowState()));
}
return buf;
}
/**
* Write child records of the current record
*
* @param buf
* The buffer being serialized.
* @param dmo
* Object containing temp-table record's data.
* @param prodsId
* If {@code true} include the {@code prods:id} attribute for each record written.
* @param omitInitialValues
* Flag indicating that initial values should be not serilaized
*/
private void writeChildRecords(BufferImpl buf, TempRecord dmo, boolean prodsId, boolean omitInitialValues)
{
DataSet ds = buf._dataSet();
if (this.ds != null && ds != null)
{
// position the buffer on this DMO
buf.findByRowID(new rowid(dmo.primaryKey()));
List<DataRelation> childRels = ds.getRelations(buf, true, false, true);
childRecords = 0;
for (DataRelation dr : childRels)
{
if (dr.isNested().booleanValue() || dr.isParentIdRelation().booleanValue())
{
hasChild = true;
Buffer childBuffer = (Buffer) dr.getChildBufferNative();
TemporaryBuffer child = (TemporaryBuffer) ((BufferReference) childBuffer).buffer();
TempTableSchema childSchema = new TempTableSchema(child, false);
String ns = childSchema.namespaceURI();
String curentPrefix = ttNsPrefix;
ttNsPrefix = ns.equals(rootNsUri) ? rootNsPrefix : childSchema.namespacePrefix();
savedHasChild.push(hasChild);
try
{
hasChild = false;
childRecords = serializeTempTable(dr, child, childSchema.getXmlName(),
ns.equals(rootNsUri) ? "" : ns,
omitInitialValues, false, prodsId, false);
}
finally
{
hasChild = savedHasChild.pop();
ttNsPrefix = curentPrefix;
}
}
}
}
}
/**
* Write data for the given columns from a single record into the XML output stream.
*
* @param dmo
* The DMO to be serialized.
* @param columns
* The set of columns for that DMO.
* @param hidden
* If {@code true} include the hidden fields ({@code __name__}) for each record
* written to stream.
*
* @throws IllegalAccessException
* if a DMO getter method cannot be accessed using reflection.
* @throws IllegalArgumentException
* if an illegal argument is passed to the DMO getter method for a column.
* @throws InvocationTargetException
* if there is a problem invoking a getter method on the given DMO.
* @throws XMLStreamException
* if there is an error writing to the XML stream.
*/
private void writeColumns(Record dmo,
Iterable<TempTableSchema.Column> columns,
boolean hidden,
boolean noInit)
throws IllegalAccessException,
IllegalArgumentException,
InvocationTargetException,
XMLStreamException
{
for (TempTableSchema.Column column : columns)
{
if ((ds == null || !ds.getSerializedBySOAP()) && childFields.contains(column.getFieldName()))
{
continue;
}
if (!hidden && column.isHidden())
{
// hidden fields are not printed now
continue;
}
Integer extent = column.getExtent();
Method getter = column.getGetter();
if (extent != null && extent > 0)
{
if (noInit)
{
boolean untouched = true;
for (int i = 0; i < extent; i++)
{
if (column.isChanged((BaseDataType) column.getGetter().invoke(dmo, i)))
{
untouched = false;
break; // drop all other tests from this extent
}
}
if (untouched)
{
continue; // skip to next column
}
}
for (int i = 0; i < extent; i++)
{
BaseDataType datum = (BaseDataType) getter.invoke(dmo, i);
writeDatum(column, datum);
}
}
else
{
if (noInit)
{
if (!column.isChanged((BaseDataType) column.getGetter().invoke(dmo)))
{
continue; // skip to next column
}
}
BaseDataType datum = (BaseDataType) getter.invoke(dmo);
writeDatum(column, datum);
}
}
}
/**
* Write a single data value to the XML output stream, according to the instructions stored
* in the temp-table schema.
* <p>
* This version ensures that the group and decimal separators are ',' and '.' for the duration of the
* serialization.
*
* @param column
* Temp-table column schema information.
* @param datum
* Datum to be written to XML output.
*
* @throws XMLStreamException
* if there is an error writing to the XML stream.
*/
private void writeDatum(TempTableSchema.Column column, BaseDataType datum)
throws XMLStreamException
{
if (datum.isUnknown() &&
(column.getNodeType() == XmlNodeType.ATTRIBUTE || column.getNodeType() == XmlNodeType.TEXT))
{
// ignore unknown values when the column is written as ATTRIBUTE or TEXt
return;
}
char groupSep = decimal.getGroupSeparator();
char decSep = decimal.getDecimalSeparator();
try
{
decimal.setNumericFormat('.', ',');
writeDatum2(column, datum);
}
finally
{
decimal.setNumericFormat(decSep, groupSep);
}
}
/**
* Write a single data value to the XML output stream, according to the instructions stored
* in the temp-table schema.
*
* @param column
* Temp-table column schema information.
* @param datum
* Datum to be written to XML output.
*
* @throws XMLStreamException
* if there is an error writing to the XML stream.
*/
private void writeDatum2(TempTableSchema.Column column, BaseDataType datum)
throws XMLStreamException
{
String name = !isEmpty(ttNsPrefix) && column.getNodeType() != XmlNodeType.ATTRIBUTE ?
(ttNsPrefix + ":" + column.getXmlNodeName()) : column.getXmlNodeName();
String value;
if (datum instanceof date)
{
// this includes datetime and datetime-tz, too
value = ((date) datum).getIsoDate();
}
else if (datum instanceof logical)
{
value = Boolean.toString(((logical) datum).booleanValue());
}
else if (datum instanceof rowid)
{
value = Util.encodeBase64(((rowid) datum).getValue());
}
else if (datum instanceof raw || datum instanceof blob)
{
if (!datum.isUnknown())
{
Base64.Encoder enc = Base64.getEncoder();
value = enc.encodeToString(((BinaryData) datum).getByteArray());
}
else
{
value = ""; // or null
}
}
else
{
value = datum.toStringMessage();
if (datum instanceof decimal && !datum.isUnknown())
{
int colDecs = column.getDecimals();
if (colDecs > 0)
{
value = ((decimal) datum).toJavaType().setScale(colDecs, RoundingMode.HALF_UP).toString();
}
else if (!value.contains("."))
{
// use [decimal] property attribute instead?
value += ".0";
}
}
}
// no need to escape, the writer will automatically escape the value
switch (column.getNodeType())
{
case TEXT:
writer.writeCharacters(value);
break;
case ATTRIBUTE:
writer.writeAttribute(name, value);
break;
case ELEMENT:
indent();
if (!datum.isUnknown() && value.isEmpty())
{
writer.writeEmptyElement(name);
}
else
{
writer.writeStartElement(name);
if (datum.isUnknown())
{
writer.writeAttribute(ATTR_XSI_NIL, "true");
}
else
{
writer.writeCharacters(value);
}
writer.writeEndElement();
}
lineBreak();
break;
}
}
/**
* Write a line break to the XML output stream, if formatting is enabled.
*
* @throws XMLStreamException
* if there is an error writing to the XML stream.
*/
private void lineBreak()
throws XMLStreamException
{
if (format)
{
writer.writeCharacters(lineSep);
}
}
/**
* Write a soft indent to the XML output stream, if formatting is enabled.
*
* @throws XMLStreamException
* if there is an error writing to the XML stream.
*/
private void indent()
throws XMLStreamException
{
if (format)
{
for (int i = 0; i < indentLevel; i++)
{
writer.writeCharacters(INDENT);
}
}
}
/**
* The framework for writing a XML file. Will open the stream specified by {@code target}
* with requested {@code encoding}, write XML header and allow the {@code code} to write
* the XML body (main element). At the end, the end of document is written and the file is
* closed.
* <p>
* In case that the {@code code} exit abruptly, the expected {@code Exception} s are catched as
* a last resort and the stack trace is printed.
*
* @param code
* The code to be executed. It should build the actual XML content.
* @param method
* The identifier for the method using the source stream.
* Only used for composing the error messages.
* @param widgetType
* The parent widget. Only used for composing the error messages.
*
* @return {@code true} on success. Otherwise the error is already dispatched to
* {@code ErrorManager} before returning.
*/
private boolean write(XmlWriter code, String method, String widgetType)
{
XMLOutputFactory factory = XMLOutputFactory.newInstance();
factory.setProperty(WstxOutputProperties.P_OUTPUT_INVALID_CHAR_HANDLER,
new InvalidCharHandler.ReplacingHandler((char) 0)
{
@Override
public char convertInvalidChar(int c) throws IOException
{
// allow all chars, as 4GL is not XML1.0 compliant!
return (char) c;
}
});
if (factory instanceof WstxOutputFactory)
{
factory.setProperty(XMLOutputFactory2.P_TEXT_ESCAPER, new Escaper());
}
try (OutputStream stream = target.getStream())
{
try
{
writer = factory.createXMLStreamWriter(stream, encoding);
}
catch (XMLStreamException ex)
{
ErrorManager.recordOrShowError(13515);
// Invalid encoding for WRITE-XML.
return false;
}
writer.writeStartDocument(encoding, "1.0");
lineBreak();
boolean result = code.run();
if (result)
{
writer.writeEndDocument();
}
writer.close();
writer = null;
return result;
}
catch (ErrorConditionException e)
{
ErrorManager.recordOrShowError(4065, false, method, widgetType);
// **The <attribute> attribute on the <widget id> has invalid arguments.
return false;
}
catch (XMLStreamException | IOException e)
{
LOG.severe("", e);
return false;
}
}
/**
* Report warnings 13065/13093, and finish export.
*
* This is how 4GL does on DATA-SET:WRITE-XML when the DATA-SET:SERIALIZE-HIDDEN =- true
* and the the first table contains fields with XML-NODE-TYPE 'attribute'.
*
*/
private void attrFailure()
{
ErrorManager.recordOrShowError(
new int [] {13065, 13093},
new String[] {
"Insert-Attribute failed for WRITE-XML. Error: 1",
"Write temp-table data failed for WRITE-XML"
},
false, // modal,
false, // prefix,
false, // isError,
false, // asMsg,
true // addDot
);
try
{
writer.flush();
}
catch (XMLStreamException ignore)
{
}
throw new Fatal();
}
/**
* Check if the string is empty.
*
* @param s
* String to be checked
* @return {@code true} if the string is empty
*/
private static boolean isEmpty(String s)
{
return s == null || s.trim().isEmpty();
}
/** Implementations of this interface actively write XML /XSD code. */
@FunctionalInterface
private static interface XmlWriter {
/**
* Similar to {@link Runnable} interface, this one allows the code to throw some specific
* {@code Exceptions} that the caller is required to catch.
*
* @return {@code true} on success. Otherwise the errors encountered were already notified
* to {@code ErrorManager}.
* @throws XMLStreamException
* on unexpected processing error.
* @throws IOException
* on I/O error
*
*/
public abstract boolean run()
throws XMLStreamException, IOException;
}
/**
* Implementation of the Interface that defines API for the factories stream writers use for creating
* "escaping writers". These factories are used when defining custom escaping of text (as well as possibly
* used by the implementations for default escaping too). Escaping in this context refers to the process of
* replacing individual text/attribute content character with pre-defined and character entities, as per
* XML specification (2.4, Appendix D).
* Typical escaping writers replace characters like < and &, as well as some additional characters
* depending on context. Custom implementations may choose to escape additional characters, for example to
* make it easier to manually view or edit resulting serialized XML document.
*/
public static class Escaper
implements EscapingWriterFactory
{
/**
* Method called to create an instance of escaping writer that will output to the specified stream,
* using the specified encoding, and escaping necessary characters (depending on both type
* [attr, element text] and encoding).
*
* @param os
* Underlying stream that the encoding writer should use for output.
* @param enc
* Encoding to use, as specified by the stream writer (based on information application has
* passed).
*
* @return escaping writer instance
*/
@Override
public Writer createEscapingWriterFor(OutputStream os, String enc)
throws UnsupportedEncodingException
{
return createEscapingWriterFor(new OutputStreamWriter(os, enc), enc);
}
/**
* Method called to create an instance of escaping writer that will output to the specified stream,
* using the specified encoding, and escaping necessary characters (depending on both type
* [attr, element text] and encoding).
*
* @param writer
* Underlying writer that the encoding writer should output.
* @param enc
* Encoding to use, as specified by the stream writer (based on information application has
* passed).
*
* @return escaping writer instance
*/
@Override
public Writer createEscapingWriterFor(final Writer writer, String enc)
throws UnsupportedEncodingException
{
return new Writer()
{
/** Closes the stream, flushing it first. */
@Override
public void close()
throws IOException
{
writer.close();
}
/** Flushes the stream. */
@Override
public void flush()
throws IOException
{
writer.flush();
}
/**
* Writes a portion of an array of characters.
*
* @param buf
* Array of characters.
* @param off
* Offset from which to start writing characters'
* @param len
* Number of characters to write.
*/
@Override
public void write(char[] buf, int off, int len)
throws IOException
{
boolean hasEscapable = false;
for (int i = 0; i < len && !hasEscapable; i++)
{
char nextChar = buf[i + off];
switch (nextChar)
{
case '<':
case '>':
case '"':
case '&':
case '\'':
hasEscapable = true;
break;
default:
break;
}
}
if (!hasEscapable)
{
writer.write(buf, off, len);
return;
}
String escaped = StringEscapeUtils.escapeXml10(new String(buf, off, len));
writer.write(escaped.toCharArray());
}
};
}
}
/** Implementations of this interface actively write XML /XSD code. */
@FunctionalInterface
private static interface XmlElementWriter {
/**
* Similar to {@link Runnable} interface, this one allows the code to throw some specific
* {@code Exceptions} that the caller is required to catch.
*
* @throws XMLStreamException
* on unexpected processing error.
*
*/
public abstract void run()
throws XMLStreamException;
}
/**
* Exception to signal immediate end of export.
*
*/
public static class Fatal extends RuntimeException
{
}
}