# JsonExport.java

    /*
    ** Module   : JsonExport.java
    ** Abstract : Write temp-table and dataset data into a JSON stream.
    **
    ** Copyright (c) 2019-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 ECF 20190107 Created initial version.
    ** 002 OM  20190327 Renamed DataSource to avoid conflicts with DataSet source.
    ** 003 OM  20190626 Added serialization support for DATASETs.
    **     HC  20190707 Implemented read(dataSet) and read(tempTable) methods of JsonObject legacy class.
    ** 004 CA  20190720 Default encoding is UTF-8, if not specified.
    **     CA  20190728 If the value is unknown, write a null.
    **     CA  20190811 Fixed problems with DATASET export.
    ** 005 ECF 20200906 New ORM implementation.
    ** 006 CA  20200914 Added blob, raw, rowid and handle support.
    ** 007 CA  20200927 Use IdentityHashMap instead of plain map when the key is a Class.
    ** 008 OM  20201120 Improved compatibility with P4GL.
    **     ME  20210130 Added serializeTempTable method into JsonArray.
    **         20210319 Refactor to reuse code for JsonArray/JsonObject Read methods.
    **     ME  20210324 Add method to serialize 4GL error as Json Object.
    **     CA  20210510 Fixed JSON and XML serialization when XML-NODE-NAME/SERIALIZE-NAME field options are set.
    **     CA  20210511 Fixed a XmlTempTableSchema usage.
    **     OM  20210809 Completed implementation of SERIALIZE-ROW|WRITE-JSON}(JsonObject|JsonArray).
    **     CA  20220120 Do not use TypeFactory.object for internal usages, use ObjectVar if the reference must be
    **                  tracked.
    **     OM  20220212 Use ReservedProperty static method for testing reserved properties names.
    **     CA  20220923 Variable definitions (including associated with parameters) must be done always outside of
    **                  the BlockManager API
    **     CA  20221006 Do not access tableHandle() in the FWD runtime, get the TempTable instance directly.
    **                  Refs #6826
    **     CA  20221017 Honor DATASET:SERIALIZE-HIDDEN at JsonObject serialization.
    **     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 20230303 Fixed <code>decimal</code> serialization
    **         20230315 Added support for the DATA-REALATION:FOREIGN-KEY-HIDDEN attribute.
    ** 010 IAS 20230415 Fixed support for PARENT-ID-RELATION.
    ** 011 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 012 CA  20231026 Do not use ObjectVar instances within FWD runtime.
    ** 013 CA  20240227 Fixed SERIALIZE-ROW to not include a root buffer name key when exporting to JsonObject.
    ** 014 CA  20240320 Performance improvement for WRITE-JSON:
    **                  Avoid BDTs within internal FWD runtime.
    **                  Replaced iterators with Java 'for', where it applies.
    **                  Read the temp-table fields directly from the Record.data.
    **                  Directly set the record in the parent buffer instead of using a findUnique
    ** 015 TJD 20240123 Java 17 compatibility updates
    ** 016 SP  20240627 Error 4065 must not set ERROR-STATUS:ERROR flag.
    ** 017 AP  20240910 Improved support for WRITE-JSON to JsonObject by using the same serialization
    **                  implementation for all output types.
    ** 018 RNC 20241101 Added stripping of BigDecimal's trailing zeros in writeDatum().
    ** 019 ES  20250327 Use seralizationName of the buffer, instead of temp-table. In case of TEMP-TABLE:writeJson
    **                  use seralizationName of the defaultBuffer.
     */

    /*
    ** 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.sql.Timestamp;
    import java.time.OffsetDateTime;
    import java.util.*;

    import com.fasterxml.jackson.core.*;
    import com.goldencode.p2j.oo.core.Assert;
    import com.goldencode.p2j.oo.json.objectmodel.*;
    import com.goldencode.p2j.oo.lang.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.Record;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.ErrorManager;
    import com.goldencode.p2j.util.logging.*;

    /**
     * An implementation of the 4GL WRITE-JSON method, which writes the records of a temp-table into
     * a JSON stream.
     * <p>
     * Runtime support is partial at this time; not all modes and features are supported.
     */
    public final class JsonExport
    {
       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(JsonExport.class.getName());

       /** Map of data types to functions which write JSON values */
       private final Map<Class<? extends BaseDataType>, ThrowingConsumer> fnMap = new IdentityHashMap<>();

       /**
        * Child fields for NESTED relations with FOREIGN-KEY-HIDDEN = true per child table name.
        * These fields are skipped on serialization.
        *
        */
       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();

       /** Cache the {@link TempTableSchema} structures. */
       private Map<TemporaryBuffer, TempTableSchema> tableSchemas = new IdentityHashMap<>();

       /** Encoding for JSON string */
       public static JsonEncoding parseEncoding(character encoding)
       {
          if (encoding == null || "UTF-8".equalsIgnoreCase(encoding.toStringMessage()))
          {
             return JsonEncoding.UTF8;
          }

          JsonEncoding jenc = (encoding.isUnknown() || "".equals(encoding.toStringMessage()))
                              ? JsonEncoding.UTF8
                              : null;

          if (jenc == null)
          {
             try
             {
                jenc = JsonEncoding.valueOf(encoding.toStringMessage());
             }
             catch (IllegalArgumentException exc)
             {
                // TODO: proper error reporting
                ErrorManager.noRecordOrThrowError(exc.getMessage());
             }
          }

          return jenc;
       }

       /**
        * Constructor specific to DATASET serialization. It will immediately performs the export.
        *
        * @param   dataSet
        *          The target object to be serialized.
        * @param   target
        *          Data target.
        * @param   pretty
        *          {@code True} to format the JSON to make it more readable; {@code false} to write it
        *          as unformatted text.
        * @param   encoding
        *          The encoding to use.
        * @param   noInit
        *          {@code True} to omit data for fields whose values match their initial values.
        * @param   noOuter
        *          {@code True} to omit data for the outermost object from the output.
        * @param   beforeImage
        *          {@code True} to write before-image data and error information, else {@code false}.
        *
        * @return {@code true} on success and {@code false} otherwise.
        *
        * @throws  PersistenceException
        *          On persistence issues.
        */
       public boolean exportDataSet(DataSet dataSet,
                                    TargetData target,
                                    boolean pretty,
                                    JsonEncoding encoding,
                                    boolean noInit,
                                    boolean noOuter,
                                    boolean beforeImage)
       throws PersistenceException
       {
          dataSet.getRelations().filter(r -> r.getForeignKeyHidden().booleanValue() ||
                                             r.isParentIdRelation().booleanValue()).
          forEach(r -> {
                childFieldsByChild.put(r.getChildTable(),
                         new HashSet<>(Arrays.asList(r.getRelationFields(false))));
          });

          if (!target.configureSupportedTargets(TargetData.TD_WRITE_JSON,
                                                TargetData.TD_WRITE_JSON,
                                                LegacyResource.DATASET + " widget"))
          {
             return false;
          }

          try
          {
             if (target.isJson())
             {
                return writeDatasetToJson(dataSet, target, noInit, noOuter, beforeImage);
             }
             else
             {
                try (OutputStream out = target.getStream())
                {
                   StreamJsonSerializer sjs = new StreamJsonSerializer(out, encoding, pretty);
                   exportDataSet(dataSet, noInit, noOuter, beforeImage, sjs, false);
                   sjs.closeStream();
                }
             }
             return true;
          }
          catch (IOException exc)
          {
             throw new PersistenceException("Error exporting JSON data", exc);
          }
          catch (RuntimeException exc)
          {
             throw new PersistenceException(exc.getMessage(), exc);
          }
       }

       /**
        * Constructor specific to DATASET serialization. It will immediately performs the export.
        *
        * @param   dataSet
        *          The target object to be serialized.
        * @param   noInit
        *          {@code True} to omit data for fields whose values match their initial values.
        * @param   noOuter
        *          {@code True} to omit data for the outermost object from the output.
        * @param   beforeImage
        *          {@code True} to write before-image data and error information, else {@code false}.
        * @param   json
        *          This is the target for the deserialized json data.
        * @param   skipStartAndEndObject
        *          Flag used to skip the creation and removing of the first JSON node as these will be performed
        *          somewhere else.
        *
        * @throws  PersistenceException
        *          On persistence issues.
        */
       public void exportDataSet(DataSet dataSet,
                                 boolean noInit,
                                 boolean noOuter,
                                 boolean beforeImage,
                                 JsonStructureCallback json,
                                 boolean skipStartAndEndObject)
       throws PersistenceException
       {
          try
          {
             if (!skipStartAndEndObject)
             {
                json.startObject();
             }

             serializeDataSet(json, dataSet, noInit, noOuter, beforeImage);

             if (!skipStartAndEndObject)
             {
                json.endObject();
             }
          }
          catch (IOException exc)
          {
             throw new PersistenceException("Error exporting JSON data", exc);
          }
          catch (RuntimeException exc)
          {
             throw new PersistenceException(exc.getMessage(), exc);
          }
       }

       /**
        * Serializes TEMP-TABLE to json.
        *
        * @param   buffer
        *          Temp-table buffer in which data is stored.
        * @param   target
        *          Data target.
        * @param   formatted
        *          {@code True} to format the JSON to make it more readable; {@code false} to write it
        *          as unformatted text. Unknown value is treated as {@code false}.
        * @param   encoding
        *          The JSON encoding to use.
        * @param   omitInitialValues
        *          {@code True} to omit data for fields whose values match their initial values.
        * @param   omitOuterObject
        *          {@code True} to omit data for the outermost object from the output.
        *
        * @return  {@code true} on success.
        *
        * @throws  PersistenceException
        *          On persistence issues.
        */
       public boolean exportTempBuffer(TemporaryBuffer buffer,
                                       TargetData target,
                                       logical formatted,
                                       JsonEncoding encoding,
                                       logical omitInitialValues,
                                       logical omitOuterObject)
       throws PersistenceException
       {
          if (!target.configureSupportedTargets(TargetData.TD_WRITE_JSON,
                                                TargetData.TD_WRITE_JSON,
                                                LegacyResource.TEMP_TABLE + " widget"))
          {
             return false;
          }

          if (target.isJson())
          {
             return writeTableToJson(buffer, target);
          }

          try (OutputStream out = target.getStream())
          {
             boolean pretty = formatted != null && !formatted.isUnknown() && formatted.booleanValue();
             StreamJsonSerializer sjs = new StreamJsonSerializer(out, encoding, pretty);
             exportTempBuffer(buffer, omitInitialValues, omitOuterObject, sjs);
             sjs.closeStream();
             return true;
          }
          catch (IOException exc)
          {
             throw new PersistenceException("Error exporting JSON data", exc);
          }
          catch (RuntimeException exc)
          {
             throw new PersistenceException(exc.getMessage(), exc);
          }
       }

       /**
        * Serializes TEMP-TABLE to json.
        *
        * @param   buffer
        *          Temp-table buffer in which data is stored.
        * @param   omitInitialValues
        *          {@code True} to omit data for fields whose values match their initial values.
        * @param   omitOuterObject
        *          {@code True} to omit data for the outermost object from the output.
        * @param   json
        *          This is the target for the deserialized json data.
        *
        * @throws  PersistenceException
        *          On persistence issues.
        */
       public void exportTempBuffer(TemporaryBuffer buffer,
                                    logical omitInitialValues,
                                    logical omitOuterObject,
                                    JsonStructureCallback json)
       throws PersistenceException
       {
          boolean omitOuter = !omitOuterObject.isUnknown() && omitOuterObject.booleanValue();
          boolean noInit = !omitInitialValues.isUnknown() && omitInitialValues.getValue();
          boolean beforeFlags = ((BufferImpl) buffer.getDMOProxy()).isBeforeBuffer();

          try
          {
             serializeTempTable(json, null, buffer, null, noInit, omitOuter, beforeFlags, false, false);
          }
          catch (IOException exc)
          {
             throw new PersistenceException("Error exporting JSON data", exc);
          }
          catch (RuntimeException exc)
          {
             throw new PersistenceException(exc.getMessage(), exc);
          }
       }

       /**
        * Exports a single record in JSON format.
        *
        * @param   bufferImpl
        *          The buffer holding the record to be exported.
        * @param   targetData
        *          The output structure.
        * @param   formatted
        *          Use {@code true} to have a pretty output. Otherwise, the output is generated on a single line.
        * @param   jsonEncoding
        *          The encoding to be used.
        * @param   omitInitial
        *          If {@code true} the initial/default values are skipped.
        * @param   omitOuter
        *          Omit outer values.
        *
        * @return  The buffer holding the record to be exported.
        */
       public boolean exportRecord(BufferImpl bufferImpl,
                                   TargetData targetData,
                                   boolean formatted,
                                   JsonEncoding jsonEncoding,
                                   boolean omitInitial,
                                   boolean omitOuter)
       {
          if (!targetData.configureSupportedTargets(TargetData.TD_SERIALIZE_ROW,
                                                    TargetData.TD_WRITE_JSON,
                                                    LegacyResource.BUFFER + " widget"))
          {
             return false;
          }

          TemporaryBuffer tempBuffer = (TemporaryBuffer) bufferImpl.buffer();
          TempTableSchema schema = new TempTableSchema(tempBuffer, true);
    //      if (checkSupportedTypes(schema))
    //      {
    //         return false;
    //      }

          if (targetData.isJson())
          {
             return writeRecordToJson(bufferImpl, targetData, schema);
          }

          try (OutputStream out = targetData.getStream())
          {
             StreamJsonSerializer json = new StreamJsonSerializer(out, jsonEncoding, formatted);
             if (!omitOuter)
             {
                json.startObject();
                json.fieldName(bufferImpl.doGetName());
             }
             writeRecord(json, tempBuffer, tempBuffer.getCurrentRecord(), schema, bufferImpl.doGetName(),
                         true, false, false, false, omitInitial);
             if (!omitOuter)
             {
                json.endObject();
             }
             json.closeStream();
          }
          catch (IOException e)
          {
             return false;
          }

          return true;
       }

       /**
        * Populate the JSON object target with data from the buffer.
        *
        * @param   buffer
        *          The buffer containing data.
        * @param   target
        *          The object containing JSON target.
        * @param   schema
        *          The schema used for operation.
        *
        * @return  {@code true} if the operation is successful.
        */
       private boolean writeRecordToJson(BufferImpl buffer, TargetData target, TempTableSchema schema)
       {
          String opType = target.getType();
          if (!Serializator.TYPE_JSON_OBJECT.equalsIgnoreCase(opType))
          {
             ErrorManager.recordOrShowError(15344, opType, "SERIALIZE-ROW");
             // Invalid mode '<mode>' for <method>. (15344)
             return false;
          }

          Object ufo = target.getDestination();
          if (!(ufo instanceof ObjectVar) || !(((ObjectVar<?>) ufo).ref() instanceof JsonObject))
          {
             ErrorManager.recordOrShowError(19079, "JsonObject", "SERIALIZE-ROW");
             // <argument> argument for <method-name> is not valid. (19079)
             ErrorManager.recordOrShowError(4065, false, "SERIALIZE-ROW", "BUFFER widget");
             // **The <attribute> attribute on the <widget id> has invalid arguments. (4065)
             return false;
          }

          // good to go
          object<? extends JsonObject> root = asJsonObject((TempRecord) buffer.buffer().getCurrentRecord(),
                                                           schema);
          if (root == null)
          {
             return false;
          }

          JsonObject json = (JsonObject) (((ObjectVar<?>) ufo).ref());
          clearObject(json);
          json.copy(root.ref());

          return true; // all good!
       }

       /**
        * Resets a {@code JsonConstruct} object by dropping all its properties or elements.
        * <p>
        * TODO: optimize this by providing a method to clear the [properties]/[elements] member of the [json]
        *
        * @param   json
        *          The object to be reset.
        */
       private void clearObject(JsonConstruct json)
       {
          if (json instanceof JsonObject)
          {
             JsonObject jo = (JsonObject) json;
             for (Map.Entry<String, Object> property : jo.getProperties())
             {
                jo.remove(new character(property.getKey()));
             }
          }
          else if (json instanceof JsonArray)
          {
             ((JsonArray) json).setLength(new int64(0));
          }
          else
          {
             throw new RuntimeException("Is there some other kind of JsonConstruct?");
          }
       }

       /**
        * Write the content of the whole table to the destination JSON object found in target.
        *
        * @param   buffer
        *          The buffer whose table is to be saved to JSON object.
        * @param   target
        *          The target which contains the destination object.
        *
        * @return  {@code true} on success.
        */
       private boolean writeTableToJson(TemporaryBuffer buffer, TargetData target)
       {
          String opType = target.getType();
          boolean isJOTarget = Serializator.TYPE_JSON_OBJECT.equalsIgnoreCase(opType);
          boolean isJATarget = Serializator.TYPE_JSON_ARRAY.equalsIgnoreCase(opType);
          if (!isJOTarget && !isJATarget)
          {
             ErrorManager.recordOrShowError(15344, opType, "SERIALIZE-ROW");
             // Invalid mode '<mode>' for <method>. (15344)
             return false;
          }

          Object ufo = target.getDestination();
          TempTableSchema schema = new TempTableSchema(buffer, true);

          if (isJOTarget)
          {
             if (!(ufo instanceof ObjectVar) || !(((ObjectVar<?>) ufo).ref() instanceof JsonObject))
             {
                ErrorManager.recordOrShowError(19079, "JsonObject", "WRITE-JSON");
                // <argument> argument for <method-name> is not valid. (19079)
                ErrorManager.recordOrShowError(4065, false, "WRITE-JSON", "BUFFER widget");
                // **The <attribute> attribute on the <widget id> has invalid arguments. (4065)
                return false;
             }

             if (!((BufferImpl) buffer.getDMOProxy()).bufferRelease().booleanValue())
             {
                return false;
             }

             // good to go
             final object<? extends JsonArray> root = new object<>(JsonArray.class);
             root.assign(ObjectOps.newInstance(JsonArray.class));

             buffer.readAllRows((dmo) -> {
                object<? extends JsonObject> node = asJsonObject(dmo, schema);
                if (node == null)
                {
                   return;
                }

                root.ref().add_2(node);
             });

             JsonObject json = (JsonObject) (((ObjectVar<?>) ufo).ref());
             clearObject(json);
             json.add(((BufferImpl) buffer.getDMOProxy()).getSerializeName(), root);
          }
          else //if (isJATarget)
          {
             if (!(ufo instanceof ObjectVar) || !(((ObjectVar<?>) ufo).ref() instanceof JsonArray))
             {
                ErrorManager.recordOrShowError(19079, "JsonArray", "WRITE-JSON");
                // <argument> argument for <method-name> is not valid. (19079)
                ErrorManager.recordOrShowError(4065, false, "WRITE-JSON", "BUFFER widget");
                // **The <attribute> attribute on the <widget id> has invalid arguments. (4065)
                return false;
             }

             if (!((BufferImpl) buffer.getDMOProxy()).bufferRelease().booleanValue())
             {
                return false;
             }

             JsonArray json = (JsonArray) (((ObjectVar<?>) ufo).ref());
             clearObject(json);

             // good to go
             buffer.readAllRows((dmo) -> {
                object<? extends JsonObject> node = asJsonObject(dmo, schema);
                if (node == null)
                {
                   return;
                }

                json.add_2(node);
             });
          }

          return true;
       }

       /**
        * Creates a new JSON Object starting from the information which can be found in the buffer sent as
        * parameter. Each field is converted to a property in the JSON Object.
        *
        * @param   dmo
        *          The record which contains the data to be used for creating the object to be returned.
        * @param   schema
        *          Metadata information about the structure of the record.
        *
        * @return  The JSON object constructed as described above. Or {@code null} on error.
        */
       private object<? extends JsonObject> asJsonObject(TempRecord dmo, TempTableSchema schema)
       {
          final object<? extends JsonObject> root = new object<>(JsonObject.class);
          root.assign(ObjectOps.newInstance(JsonObject.class));

          ArrayList<TempTableSchema.Column> columns = schema.columns();
          for (int k = 0; k < columns.size(); k++)
          {
             TempTableSchema.Column column = columns.get(k);

             String name = column.getSerializeName();
             if (ReservedProperty.isReservedProperty(name))
             {
                // some fields which start with "__" are hidden fields (specific to BEFORE tables)
                continue;
             }

             Integer extent = column.getExtent();
             if (extent != null && extent > 0)
             {
                final object<? extends JsonArray> jsonExtent = new object<>(JsonArray.class);
                root.assign(ObjectOps.newInstance(JsonArray.class));

                for (int i = 0; i < extent; i++)
                {
                   BaseDataType datum = null;
                   try
                   {
                      datum = (BaseDataType) column.getGetter().invoke(dmo, i);
                   }
                   catch (IllegalAccessException | InvocationTargetException e)
                   {
                      ErrorManager.recordOrThrowError(-3, "", null, null);
                      return null;
                   }
                   addPropertyToJsonArray(jsonExtent.ref(), column, datum);
                }

                root.ref().add(new character(column.getSerializeName()), jsonExtent);
             }
             else // plain property
             {
                BaseDataType datum = null;
                try
                {
                   datum = (BaseDataType) column.getGetter().invoke(dmo);
                }
                catch (IllegalAccessException | InvocationTargetException e)
                {
                   ErrorManager.recordOrThrowError(-4, "", null, null);
                   return null;
                }
                addPropertyToJson(root.ref(), column, datum);
             }
          }
          return root;
       }

       /**
        * Adds a value as a new property to a JSON object. Only for scalar values.
        *
        * @param   dest
        *          The destination JSON object.
        * @param   column
        *          The column which provides the type and name of the property.
        * @param   datum
        *          The actual data to be set as property's value.
        */
       private void addPropertyToJson(JsonObject dest, TempTableSchema.Column column, BaseDataType datum)
       {
          switch (column.getType().getSimpleName().toLowerCase())
          {
             case "character": dest.add(new character(column.getSerializeName()), (character) datum); break;
             case "comhandle": dest.add(new character(column.getSerializeName()), (comhandle) datum); break;
             case "date": dest.add(new character(column.getSerializeName()), (date) datum); break;
             case "datetime": dest.add(new character(column.getSerializeName()), (datetime) datum); break;
             case "datetimetz": dest.add(new character(column.getSerializeName()), (datetimetz) datum); break;
             case "decimal": dest.add(new character(column.getSerializeName()), (decimal) datum); break;
             case "handle": dest.add(new character(column.getSerializeName()), (handle) datum); break;
             case "int64": dest.add(new character(column.getSerializeName()), (int64) datum); break;
             case "integer": dest.add(new character(column.getSerializeName()), (integer) datum); break;
             case "logical": dest.add(new character(column.getSerializeName()), (logical) datum); break;
             case "longchar": dest.add(new character(column.getSerializeName()), (longchar) datum); break;
             case "memptr": dest.add(new character(column.getSerializeName()), (memptr) datum); break;
             case "raw": dest.add(new character(column.getSerializeName()), (raw) datum); break;
             case "recid": dest.add(new character(column.getSerializeName()), (recid) datum); break;
             case "rowid": dest.add(new character(column.getSerializeName()), (rowid) datum); break;
             case "object": dest.add_1(new character(column.getSerializeName()), (object) datum); break;
             case "blob": dest.add(new character(column.getSerializeName()), (blob) datum); break;
             case "clob": dest.add(new character(column.getSerializeName()), (clob) datum); break;
          }
       }

       /**
        * Adds a value as a new indexed property in JSON array object.
        *
        * @param   dest
        *          The destination JSON array object.
        * @param   column
        *          The column which provides the type of the property.
        * @param   datum
        *          The actual data to be set as property's value.
        */
       private void addPropertyToJsonArray(JsonArray dest, TempTableSchema.Column column, BaseDataType datum)
       {
          switch (column.getType().getSimpleName().toLowerCase())
          {
             case "character": dest.add((character) datum); break;
             case "comhandle": dest.add((comhandle) datum); break;
             case "date": dest.add((date) datum); break;
             case "datetime": dest.add((datetime) datum); break;
             case "datetimetz": dest.add((datetimetz) datum); break;
             case "decimal": dest.add((decimal) datum); break;
             case "handle": dest.add((handle) datum); break;
             case "int64": dest.add((int64) datum); break;
             case "integer": dest.add((integer) datum); break;
             case "logical": dest.add((logical) datum); break;
             case "longchar": dest.add((longchar) datum); break;
             case "memptr": dest.add((memptr) datum); break;
             case "raw": dest.add((raw) datum); break;
             case "recid": dest.add((recid) datum); break;
             case "rowid": dest.add((rowid) datum); break;
             case "object": dest.add_2((object) datum); break;
             case "blob": dest.add((blob) datum); break;
             case "clob": dest.add((clob) datum); break;
          }
       }

       /**
        * Write the content of the whole dataset to the destination JSON object found in target.
        *
        * @param   dataSet
        *          The buffer whose table is to be saved to JSON object.
        * @param   target
        *          The target which contains the destination object.
        * @param   noInit
        *          {@code true} to omit data for fields whose values match their initial values.
        * @param   noOuter
        *          {@code true} to omit data for the outermost object from the output.
        * @param   beforeImage
        *          {@code true} to write before-image data and error information, else {@code false}.
        *
        * @return  {@code true} on success.
        *
        * @throws  PersistenceException
        *          On persistence issues.
        */
       private boolean writeDatasetToJson(DataSet dataSet,
                                          TargetData target,
                                          boolean noInit,
                                          boolean noOuter,
                                          boolean beforeImage)
       throws PersistenceException
       {
          String opType = target.getType();
          boolean isJOTarget = Serializator.TYPE_JSON_OBJECT.equalsIgnoreCase(opType);
          if (!isJOTarget)
          {
             ErrorManager.recordOrShowError(19043, "WRITE-JSON");
             // JsonArray is not a valid target-type for <method> method. (19043)
             ErrorManager.recordOrShowError(4065, false, "WRITE-JSON", "DATASET widget");
             // **The <attribute> attribute on the <widget id> has invalid arguments. (4065)
             return false;
          }

          Object ufo = target.getDestination();
          if (!(ufo instanceof ObjectVar) || !(((ObjectVar<?>) ufo).ref() instanceof JsonObject))
          {
             ErrorManager.recordOrShowError(19079, "JsonObject", "WRITE-JSON");
             // <argument> argument for <method-name> is not valid. (19079)
             ErrorManager.recordOrShowError(4065, false, "WRITE-JSON", "DATASET widget");
             // **The <attribute> attribute on the <widget id> has invalid arguments. (4065)
             return false;
          }
          JsonObject json = (JsonObject) (((ObjectVar<?>) ufo).ref());
          clearObject(json);

          JsonConstruct.ObjectBuilder jsonExportOutput =
             new JsonConstruct.ObjectBuilder(json);

          exportDataSet(dataSet, noInit, noOuter, beforeImage, jsonExportOutput, true);

          // this call should be performed as the first Json node is added through the ObjectBuilder constructor
          jsonExportOutput.endObject();
          return true;
       }

       /**
        * Actual implementation of a table serialization. This is called for both for TEMP-TABLEs
        * (once) and for DATASETs (one time for each member buffer, and supplementary for any eventual
        * before-buffer)
        *
        * @param   json
        *          The target of the deserialized json data.
        * @param   relation
        *          The relation on which this buffer is a child of.
        * @param   buffer
        *          A buffer of the temp-table to be serialized.
        * @param   overrideBeforeName
        *          Overrides the name of the table. The BEFORE-TABLE data is serialized under the name
        *          of the main (AFTER) buffer.
        * @param   noInit
        *          Skip the initial values.
        * @param   omitOuter
        *          {@code True} to omit data for the outermost object from the output.
        * @param   hiddenFields
        *          {@code true} to add the internal flags specific to BEFORE-BUFFER records.
        * @param   dsFlags
        *          {@code true} to add the internal flags specific to DataSet TempTable records.
        * @param   dsBeforeImage
        *          {@code true} to write the before-image if available.
        *
        * @return  {@code true} if everything went fine, and {@code false} if a non-fatal error was encountered
        *
        * @throws  IOException
        *          If exceptions occur during the serialization.
        */
       private boolean serializeTempTable(JsonStructureCallback json,
                                          DataRelation relation,
                                          TemporaryBuffer buffer,
                                          String overrideBeforeName,
                                          boolean noInit,
                                          boolean omitOuter,
                                          boolean hiddenFields,
                                          boolean dsFlags,
                                          boolean dsBeforeImage)
       throws IOException
       {
          TempTableSchema schema = tableSchemas.computeIfAbsent(buffer, (k) -> new TempTableSchema(buffer, true));
          boolean isBefore = (overrideBeforeName != null);
          Buffer currentBuffer = (Buffer) buffer.getDMOProxy();

          String serializeName = currentBuffer.getSerializeName().toJavaType();
          String tableName = isBefore
                                ? overrideBeforeName
                                : serializeName != null && !serializeName.isEmpty()
                                    ? serializeName
                                    : schema.getTableName();

    //      if (checkSupportedTypes(schema))
    //      {
    //         return false;
    //      }

          if (relation == null)
          {
             // reads the records in order of primary index, if available, else in ascending primary key order
             if (!omitOuter)
             {
                json.startObject();
             }
             if (dsFlags || !omitOuter)
             {
                json.fieldName(tableName);
             }
             json.startArray();

             buffer.readAllRows(
                   (dmo) -> writeRecord(json,
                                        buffer,
                                        dmo,
                                        schema,
                                        tableName,
                                        hiddenFields,
                                        dsFlags,
                                        dsFlags && isBefore,
                                        dsBeforeImage,
                                        noInit));

             json.endArray();
             if (!omitOuter)
             {
                json.endObject();
             }

             return true;
          }

          // build a query depending on the relation, and add each record from the relation
          QueryWrapper query = (QueryWrapper) relation.getQuery();
          query.hintFullResults();
          query.queryOpen();
          query.first();
          boolean hasRecords = !query._isOffEnd();
          if (hasRecords)
          {
             json.fieldName(tableName);
             json.startArray();
          }
          while (!query._isOffEnd())
          {
             writeRecord(json,
                         buffer,
                         buffer.getCurrentRecord(),
                         schema,
                         tableName,
                         hiddenFields,
                         dsFlags,
                         dsFlags && isBefore,
                         dsBeforeImage,
                         noInit);

             query._getNext();
          }
          query.close();

          if (hasRecords)
          {
             json.endArray();
          }

          return true;
       }

       /**
        * Actual implementation of a table serialization into a JsonArray (only rows).
        *
        * @param   json
        *          The target of the deserialized json data.
        * @param   buffer
        *          A buffer of the temp-table to be serialized.
        * @param   omitOuter
        *          {@code True} to omit data for the outermost object from the output.
        * @param   noInit
        *          Skip the initial values.
        *
        * @throws  IOException
        *          If exceptions occur during the serialization.
        */
       public boolean serializeTempTable(JsonStructureCallback json,
                                         TemporaryBuffer buffer,
                                         boolean omitOuter,
                                         boolean noInit)
       throws IOException
       {
          TempTableSchema schema = new TempTableSchema(buffer, true);
          TempTable tempTable = buffer.getParentTable();
          String serializeName = tempTable.getSerializeName().toJavaType();
          String tableName = serializeName != null && !serializeName.isEmpty() ? serializeName
                   : schema.getTableName();

          if (!omitOuter)
          {
             json.fieldName(tableName);
             json.startArray();
          }

          buffer.readAllRows((dmo) -> writeRecord(json, buffer, dmo, schema, tableName, false, false,
                   false, false, noInit));

          if (!omitOuter)
          {
             json.endArray();
          }

          return true;
       }

       /**
        * Actual implementation of a temp-table buffer serialization into a JsonArray (only current row).
        *
        * @param   json
        *          The target of the deserialized json data.
        * @param   buffer
        *          A buffer record to be serialized.
        * @param   noInit
        *          Skip the initial values.
        *
        * @throws  IOException
        *          If exceptions occur during the serialization.
        */
       public boolean serializeRecord(JsonStructureCallback json, TemporaryBuffer buffer, boolean noInit)
       throws IOException
       {
          if (json != null && buffer != null)
          {
             try
             {
                writeRecord(json, buffer.getCurrentRecord(), new TempTableSchema(buffer, true), false, noInit);

                return true;
             }
             catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException exc)
             {
                throw new RuntimeException("Error exporting JSON data", exc);
             }
          }

          return false;
       }

       /**
        * Constructor specific to DATASET serialization in a JSON Object.
        *
        * @param   json
        *          This is the target for the deserialized json data.
        * @param   dataSet
        *          The target object to be serialized.
        * @param   noInit
        *          {@code True} to omit data for fields whose values match their initial values.
        * @param   noOuter
        *          {@code True} to omit data for the outermost object from the output.
        * @param   beforeImage
        *          {@code True} to write before-image data and error information, else {@code false}.
        *
        * @throws  IOException
        *          On persistence issues.
        */
       public void serializeDataSet(JsonStructureCallback json,
                                    DataSet dataSet,
                                    boolean noInit,
                                    boolean noOuter,
                                    boolean beforeImage)
       throws IOException
       {
          boolean writeDataset = !noOuter && !dataSet.getSerializeHidden().booleanValue();
          if (writeDataset)
          {
             json.fieldName(dataSet.getSerializeName().toStringMessage());
             json.startObject();
          }

          int ttCount = dataSet.numBuffers().intValue();
          boolean hasChanges = false;
          for (int k = 0; k < ttCount; k++)
          {
             BufferImpl after = dataSet.getBufferByIndex(k + 1);
             if (!after.isAfterBuffer())
             {
                continue; // SIMPLE buffers don't have changes
             }
             BufferImpl before = (BufferImpl) after.beforeBufferNative();
             TemporaryBuffer buffer = (TemporaryBuffer) before.buffer();
             if (buffer.count() > 0)
             {
                hasChanges = true;
                break;
             }
          }

          if (beforeImage /*&&  hasChanges */)
          {
             json.fieldName("prods:hasChanges");
             json.booleanValue(true);
          }

          nextBuffer: for (int k = 0; k < ttCount; k++)
          {
             BufferImpl after = (BufferImpl) dataSet.getBufferByIndex(k + 1);
             List<DataRelation> rels = dataSet.getRelations(after, false, true, true);
             for (DataRelation rel : rels)
             {
                if (rel.isNested().booleanValue() || rel.isParentIdRelation().booleanValue())
                {
                   // if [after] is a child buffer in a active nested relation, its data was (or
                   // will be serialized with the parent)
                   continue nextBuffer;
                }
             }

             // if there are no active relation with [after] as child buffer, serialize it now
             TemporaryBuffer buffer = (TemporaryBuffer) after.buffer();
             if (buffer.count() > 0)
             {
                serializeTempTable(
                         json, null, buffer, null, noInit, true, false, true, beforeImage);
             }
          }

          if (beforeImage)
          {
             json.fieldName("prods:before");
             json.startObject();

             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 nameOverride = after.name().toStringMessage();
                serializeTempTable(json, null, buffer, nameOverride, noInit, true, false, true, beforeImage);
             }

             json.endObject();
          }

          if (writeDataset)
          {
             json.endObject();
          }
       }

       /**
        * Serialize a single record to the JSON output.
        *
        * @param   json
        *          Target of the deserialized json data.
        * @param   buffer
        *          The buffer to which the DMO belongs.
        * @param   dmo
        *          DMO representing the record.
        * @param   schema
        *          The temp-table schema.
        * @param   hiddenFields
        *          Write the hidden fields ({@code __ERROR_FLAG__}, {@code __ORIGIN_ROWID__},
        *          {@code __ERROR_STRING__}, {@code __AFTER_ROWID__}, {@code __ROW_STATE__}).
        * @param   dsFlags
        *          {@code true} to add the internal flags specific to DataSet TempTable records.
        * @param   skipCreateFields
        *          {@code true} to skip CREATED records in before table.
        * @param   dsBeforeImage
        *          {@code true} to write the before-image if available.
        * @param   noInit
        *          Flag indicating the initial values are to be omitted.
        */
       private void writeRecord(JsonStructureCallback json,
                                TemporaryBuffer buffer,
                                TempRecord dmo,
                                TempTableSchema schema,
                                String rowName,
                                boolean hiddenFields,
                                boolean dsFlags,
                                boolean skipCreateFields,
                                boolean dsBeforeImage,
                                boolean noInit)
       {
          if (dmo == null)
          {
             return;
          }

          Integer rowState = dmo._rowState();
          if (rowState == null)
          {
             rowState = Buffer.ROW_UNMODIFIED;
          }
          if (skipCreateFields && rowState == Buffer.ROW_CREATED)
          {
             // NOTE: it seems like the CREATED records are NOT serialized at all for BEFORE TEMP-TABLEs, so these
             //       values should not reach past this point, so the records are cut here
             return;
          }

          try
          {
             json.startObject();

             if (dsFlags && dsBeforeImage && rowState != Buffer.ROW_UNMODIFIED)
             {
                json.fieldName("prods:id");
                // 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
                   json.stringValue(rowName + dmo.primaryKey());
                }
                else
                {
                   // CREATED or MODIFIED before images
                   json.stringValue(rowName + dmo._peerRowid());
                }

                json.fieldName("prods:rowState");
                json.stringValue(Util.getRowStateAsString(rowState));
             }

             writeRecord(json, dmo, schema, hiddenFields, noInit);

             if (dsFlags)
             {
                BufferImpl buf = (BufferImpl) buffer.getDMOProxy();

                DataSet ds = buf._dataSet();
                if (ds != null)
                {
                   // position the buffer on this DMO
                   buf.buffer().loadRecord(dmo);

                   ArrayList<DataRelation> childRels = ds.getRelations(buf, true, false, true);

                   for (int i = 0; i < childRels.size(); i++)
                   {
                      DataRelation dr = childRels.get(i);

                      if (dr.isNested().booleanValue() || dr.isParentIdRelation().booleanValue())
                      {
                         BufferImpl childBuffer = (BufferImpl) dr.getChildBufferNative();
                         TemporaryBuffer child = (TemporaryBuffer) childBuffer.buffer();

                         Set<String> cFields = this.childFields;
                         childFields = childFieldsByChild.getOrDefault(childBuffer._name(),
                                  Collections.emptySet());
                         try
                         {
                            serializeTempTable(json, dr, child, null, noInit, true, false, true, dsBeforeImage);
                         }
                         finally
                         {
                            this.childFields = cFields;
                         }
                      }
                   }
                }
             }

             json.endObject();
          }
          catch (IllegalAccessException |
                 IllegalArgumentException |
                 InvocationTargetException |
                 IOException exc)
          {
             throw new RuntimeException("Error exporting JSON data", exc);
          }
       }

       /**
        * Serialize a single buffer record to the JSON output.
        *
        * @param   json
        *          Target of the deserialized json data.
        * @param   dmo
        *          DMO representing the record.
        * @param   schema
        *          The temp-table schema.
        * @param   hiddenFields
        *          Write the hidden fields ({@code __ERROR_FLAG__}, {@code __ORIGIN_ROWID__},
        *          {@code __ERROR_STRING__}, {@code __AFTER_ROWID__}, {@code __ROW_STATE__}).
        * @param   noInit
        *          Flag indicating the initial values are to be omitted.
        */
       private boolean writeRecord(JsonStructureCallback json,
                                   TempRecord dmo,
                                   TempTableSchema schema,
                                   boolean hiddenFields,
                                   boolean noInit)
       throws IOException,
              IllegalAccessException,
              IllegalArgumentException,
              InvocationTargetException
       {
          if (json != null && dmo != null)
          {
            ArrayList<TempTableSchema.Column> columns = schema.columns();
            for (int k = 0; k < columns.size(); k++)
            {
               TempTableSchema.Column column = columns.get(k);
               String name = column.getSerializeName();
               if (!hiddenFields && ReservedProperty.isReservedProperty(name) ||
                        childFields.contains(column.getFieldName()) || column.isSerializeHidden())
               {
                  // fields which start with "__" are hidden fields (specific to BEFORE tables)
                  continue;
               }

               Integer extent = column.getExtent();
               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
                     }
                  }

                  json.fieldName(name);
                  json.startArray();

                  for (int i = 0; i < extent; i++)
                  {
                     writeDatum(json, column, dmo, i);
                  }

                  json.endArray();
               }
               else
               {
                  if (noInit)
                  {
                     if (!column.isChanged((BaseDataType) column.getGetter().invoke(dmo)))
                     {
                        continue; // skip to next column
                     }
                  }
                  writeDatum(json, column, dmo, null);
               }
            }

            return true;
          }

          return false;
       }

       /**
        * Checks supported data types. The evaluation is delegated to {@code Util.checkSupportedTypes()}.
        * The method returns {@code true} and prints an error messages is unsupported types are detected.
        *
        * @return  {@code true} if a type problem was discovered.
        */
       private boolean checkSupportedTypes(TempTableSchema schema)
       {
          String unsupportedType = Util.checkSupportedTypes(schema);
          if (unsupportedType != null)
          {
             // NOTE: this test is executed for each serialized row! Error 15391 is printed multiple
             //       times in message line, one for each record.
             ErrorManager.recordOrShowError(15391, unsupportedType);
             // Unsupported data type for JSON serialization: <datatype>.
             return true;
          }
          return false;
       }

       /**
        * Write a single data value to the JSON generator's output stream, according to the
        * instructions stored in the temp-table schema.
        *
        * @param   json
        *          Target of the deserialized json data.
        * @param   column
        *          Temp-table column schema information.
        * @param   dmo
        *          DMO containing datum associated with the specified column.
        * @param   index
        *          Zero-based index of an extent field element. Should be {@code null} for a scalar field.
        *
        * @return  {@code false} on errors. The error was already reported through {@code ErrorManager}.
        *
        * @throws  IOException
        *          if the JSON generator cannot write data.
        * @throws  InvocationTargetException
        *          if the method invoked to get the data from the DMO fails.
        * @throws  IllegalArgumentException
        *          if an argument passed to a DMO getter is invalid.
        * @throws  IllegalAccessException
        *          if there is an access/security problem.
        */
       private boolean writeDatum(JsonStructureCallback json,
                                  TempTableSchema.Column column,
                                  Record dmo,
                                  Integer index)
       throws IOException,
              IllegalAccessException,
              IllegalArgumentException,
              InvocationTargetException
       {
          boolean extent = index != null;
          Object datum = OrmUtils.getField(dmo, column.offset + (index == null ? 0 : index));

          if (!extent)
          {
             json.fieldName(column.getSerializeName());
          }

          if (datum == null)
          {
             json.nullValue();
             return true;
          }
          else if (!Util.checkSupportedType(column))
          {
             ErrorManager.recordOrThrowError(17586, Util.getLegacyType(column.getType()));
             // Unsupported data type for JSON serialization: <datatype>.
             return false;
          }

          // lookup JSON writer function by data type; we cache lambdas to minimize use of
          // isAssignableFrom native method call
          Class<? extends BaseDataType> type = column.getType();
          ThrowingConsumer fn = fnMap.get(type);

          if (fn == null)
          {
             if (int64.class.isAssignableFrom(type))
             {
                fn = (d) -> json.numberValue(((Number) d).longValue());
             }
             else if (Text.class.isAssignableFrom(type))
             {
                fn = (d) -> json.stringValue((String) d);
             }
             else if (logical.class.isAssignableFrom(type))
             {
                fn = (d) -> json.booleanValue((Boolean) d);
             }
             else if (decimal.class.isAssignableFrom(type))
             {
                // strip trailing zeros, but assure at least one
                fn = (d) ->
                {
                   BigDecimal value = ((BigDecimal) d).setScale(((BigDecimal) d).scale(), RoundingMode.HALF_UP)
                                                      .stripTrailingZeros();
                   d = value.scale() <= 0 ? value.setScale(1) : value;
                   json.numberValue((BigDecimal) d);
                };
             }
             else if (datetimetz.class.isAssignableFrom(type))
             {
                fn = (d) ->
                {
                   date p2jDate = new datetimetz((OffsetDateTime) d);
                   if (p2jDate.isUnknown())
                   {
                      json.nullValue();
                   }
                   else
                   {
                      json.stringValue(p2jDate.getIsoDate());
                   }
                };
             }
             else if (datetime.class.isAssignableFrom(type))
             {
                fn = (d) ->
                {
                   date p2jDate = new datetime((Timestamp) d);
                   if (p2jDate.isUnknown())
                   {
                      json.nullValue();
                   }
                   else
                   {
                      json.stringValue(p2jDate.getIsoDate());
                   }
                };
             }
             else if (date.class.isAssignableFrom(type))
             {
                fn = (d) ->
                {
                   date p2jDate = new date((Date) d);
                   if (p2jDate.isUnknown())
                   {
                      json.nullValue();
                   }
                   else
                   {
                      json.stringValue(p2jDate.getIsoDate());
                   }
                };
             }
             else if (rowid.class.isAssignableFrom(type))
             {
                fn = (d) ->
                {
                   Long r = (Long) d;
                   json.stringValue(Util.encodeBase64(r));
                };
             }
             else if (blob.class.isAssignableFrom(type) || raw.class.isAssignableFrom(type))
             {
                fn = (d) ->
                {
                   byte[] b = (byte[]) d;
                   Base64.Encoder enc = Base64.getEncoder();
                   json.stringValue(enc.encodeToString(b));
                };
             }
             else if (handle.class.isAssignableFrom(type))
             {
                fn = (d) ->
                {
                   Long h = (Long) d;
                   json.numberValue(h);
                };
             }
             else
             {
                ErrorManager.recordOrShowError(15391, Util.getLegacyType(type));
                // Unsupported data type for JSON serialization: <datatype>.
                return false;
             }

             // TODO: other data types ?

             if (fn != null)
             {
                fnMap.put(type, fn);
             }
          }

          if (fn == null)
          {
             ErrorManager.recordOrShowError(15391, Util.getLegacyType(type));
             // Unsupported data type for JSON serialization: <datatype>.
             return false;
          }

          fn.accept(datum);
          return true;
       }

       /**
        * Write a single data value of type {@code rowid} passed as a long value, along with its field
        * name.
        *
        * @param   json
        *          Target of the deserialized json data.
        * @param   name
        *          The name of the field.
        * @param   rowidAsLong
        *          The {@code rowid} value as {@code Long}.
        */
       private void writeRowid(JsonStructureCallback json, String name, Long rowidAsLong)
       throws IOException
       {
          json.fieldName(name);

          if (rowidAsLong == null)
          {
             json.nullValue();
             return;
          }

          json.stringValue(Util.encodeBase64(rowidAsLong));
       }

       /**
        * Serialize 4GL legacy error as Json object.
        *
        * Used by Json entity/body writers in .net package, might be part of some new class in .json package.
        * @param poError The 4GL legacy error.
        *
        * @return The JSON serialization of the error as a Json object.
        */
       public static object<? extends JsonObject> serializeError(object<? extends LegacyError> poError)
       {
          final object<? extends JsonObject> oResponse = new object<>(JsonObject.class);
          final object<? extends JsonObject> oError = new object<>(JsonObject.class);
          final object<? extends JsonArray> oErrorList = new object<>(JsonArray.class);
          integer iLoop = TypeFactory.integer();

          Assert.notNull(poError, new character("Error"));
          oResponse.assign(ObjectOps.newInstance(JsonObject.class));
          oErrorList.assign(ObjectOps.newInstance(JsonArray.class));

          if ((ObjectOps.typeOf(poError, AppError.class)).booleanValue())
          {
             oResponse.ref().add(new character("_retVal"), ObjectOps.cast(poError, AppError.class).ref().getReturnValue());
          }

          oResponse.ref().add(new character("_errors"), oErrorList);

          for (int i = 0; i < poError.ref().getNumMessages().intValue(); i++)
          {
             iLoop.assign(i + 1);

             oError.assign(ObjectOps.newInstance(JsonObject.class));
             oErrorList.ref().add_2(oError);
             oError.ref().add(new character("_errorMsg"), poError.ref().getMessage(iLoop));
             oError.ref().add(new character("_errorNum"), poError.ref().getMessageNum(iLoop));
          }

          if ((SessionUtils.isDebugAlert()).booleanValue())
          {
             oResponse.ref().add(new character("_type"), poError.ref().getLegacyClass().ref().getTypeName());

             if (!poError.ref().getCallStack().isUnknown())
          {
             oErrorList.assign(ObjectOps.newInstance(JsonArray.class));
             oResponse.ref().add(new character("_stack"), oErrorList);

                String[] callStack = TextOps.entries(poError.ref().getCallStack().getValue(), "\\n");

                for (int i = 0; i < callStack.length; i++)
             {
                   oErrorList.ref().add(new character(callStack[i]));
                }
             }

          }

    // TODO: accessing any method in LegacyClass throws NPE
    // ProcedureManager.CalleeInfoImpl.push #5351
    //      oProp.assign(poError.ref().getLegacyClass().ref().getProperty(new character("InnerError")));
    //
    //      if (_and(and(oProp.isValid(), () -> isEqual(oProp.ref().getDataType(), DataType.object_)), () -> LegacyClass.getLegacyClass(oProp.ref().getDataTypeName()).ref().isA(ObjectOps.getLegacyClass(LegacyError.class))))
    //      {
    //         oInner.assign(oProp.ref().get(poError));
    //
    //         if ((oInner.isValid()).booleanValue())
    //         {
    //            this.writeError(oInner);
    //         }
    //      }
          return oResponse;
       };

       /**
        * A consumer which accepts one value and throws IOException, to avoid having to handle this
        * exception in every defined lambda expression.
        */
       @FunctionalInterface
       private interface ThrowingConsumer
       {
          /**
           * Accept and process a data value.
           *
           * @param   datum
           *          Data value to be processed.
           *
           * @throws  IOException
           *          if an I/O error occurs.
           */
          public void accept(Object datum)
          throws IOException;
       }
    }