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 &lt; and &amp;, 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
   {
      
   }
}