JsonImport.java
/*
** Module : JsonImport.java
** Abstract : Read JSON content into a temp-table.
**
** Copyright (c) 2017-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20190123 Created initial version.
** 002 OM 20190327 Renamed DataSource to avoid conflicts with DataSet source.
** 003 OM 20190818 Improve reading and validation support.
** 004 ECF 20200906 New ORM implementation.
** CA 20200906 Batch error fix.
** 005 OM 20210108 Improve reading compatibility with ABL JSON files.
** CA 20210510 Fixed JSON and XML serialization when XML-NODE-NAME/SERIALIZE-NAME field options are set.
** When reading a temp-table from a JSON file, the JSON may include other fields which do not
** map to a temp-table column - these are silently ignored.
** Fixed reading a temp-table with extent fields (they were assumed to be a child temp-table).
** CA 20210709 JSON parse must use a pristine byte stream and use the reader to decode it.
** OM 20210825 Added support methods for READ-JSON methods.
** OM 20210922 Fixed default values for XML-NODE-NAME and SERIALIZE-NAME.
** OM 20211020 Added _DATASOURCE_ROWID property.
** CA 20220218 Ensure the group and decimal separators are ',' and '.' during record read.
** OM 20220328 Stop and return false if validation errors occur while reading a table.
** OM 20220420 Rewrite the read in REPLACE mode.
** CA 20220420 Reading from JSON allows a table field to not exist in the JSON, which will default to its
** initial value.
** CA 20221012 Fixed JSON import of tables with extent fields, when the source is a JsonObject.
** TJD 20220504 Upgrade do Java 11 minor changes
** OM 20220524 Fixed reading a dataset/table with explicit serialize-name attribute.
** OM 20220525 Fixed case-sensitivity issues related to fields, tables, and datasets.
** CA 20220526 Fixed JSON deserialization of decimal values.
** OM 20220914 Use MAX-WIDTH to generate custom sized varchar columns.
** SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
** performance.
** CA 20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
** 006 IAS 20230325 Fixed source validation. Implemented support for dynamic TEMP-TABLE right
** after CREATE TEMP-TABLE.
** IAS 20230327 Fixed READ-JSON for static DATA-SET with NESTED/FOREIGN-KEY-HIDDEN relations.
** IAS 20230414 Added support for inferring empty dynamic DATA-SET from JSON data.
** IAS 20230420 Added support for inferring empty dynamic DATA-SET from JSON data w/o root element.
** 007 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 008 RAA 20230525 Creating a new BaseDataType instance is now done using BaseDataTypeFactory.
** 009 IAS 20230526 Extracted common logic to the 'Importer'.
** 010 CA 20230724 Further reduce context-local usage.
** 011 OM 20231027 Nested child tables are not required to be found after all fields from parent table.
** The nested table lookup are done by serialize name.
** 012 EXT 20231122 JSON input must always be UTF-8, regardless of cpinternal (refs #7944) (by Anton)
** 013 CA 20240320 Avoid BDTs within internal FWD runtime. Replaced iterators with Java 'for', where it
** applies.
** 014 OM 20240502 When reading a table, both the name and the serialization name will match it.
** 015 TJD 20240123 Java 17 compatibility updates and compiler warnings fix
** 016 LS 20240711 Fixed READ-JSON, so that the comparison between a table field and a key from JsonObject
** is case-insensitive.
** 017 AP 20240918 Fixed READ-JSON for JsonObjects with SERIALIZE-HIDDEN
** 018 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** Implementation of [setMultiTenantAlternative] method.
** 019 AS 20250305 Added dataset/table name matching for reading operations.
*/
/*
** 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.math.*;
import java.nio.charset.*;
import java.util.*;
import java.util.function.*;
import com.goldencode.p2j.util.logging.*;
import org.apache.commons.lang3.tuple.*;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.oo.json.*;
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 static com.fasterxml.jackson.core.JsonToken.*;
import static com.goldencode.p2j.persist.serial.SerializeOptions.*;
/**
* An implementation of the 4GL READ-JSON method, which parses a JSON stream and reads the content
* into a temp-table.
* <p>
* Runtime support is partial at this time; not all modes and features are supported. PRODATASET
* is not supported at this time.
*/
public final class JsonImport
extends Importer
implements TableReader
{
/** Logger. */
private static final CentralLogger LOG = CentralLogger.get(JsonImport.class);
/** The primary index which drives the duplicate resolution, in case the read mode is REPLACE. */
private P2JIndex primaryIndex = null;
/** JSON parser */
private JsonParser parser = null;
/**
* Constructor which immediately performs the import of a DATASET or a TEMP-TABLE.
*
* @param source
* Data source which normalizes access of various media to an input stream.
* @param mode
* Read mode to determine how records are stored and how non-unique records are
* handled. Not currently honored, except temp-table records are deleted if mode
* is EMPTY.
*/
public JsonImport(SourceData source, String mode)
{
super(source, mode);
}
/**
* Validates the {@code source} and create the dataset structure by building all temp-tables according to
* source JsonObject.
*
* @param ds
* The {@code Dataset} to be populated with fields and finally prepared.
* @param source
* The data source.
*
* @return {@code true} if operation is successful and {@code false} otherwise.
*/
public static boolean createDatasetStructure(DataSet ds, SourceData source)
{
String sourceType = source.getType();
BaseDataType bdtSrc = source.getSource();
if (!(bdtSrc instanceof object))
{
ErrorManager.recordOrShowError(19079, sourceType, "READ-JSON");
// <argument> argument for <method-name> is not valid. (19079)
return false;
}
_BaseObject_ baseObj = ((object<?>) bdtSrc).ref();
if (!(baseObj instanceof JsonObject))
{
ErrorManager.recordOrShowError(19079, sourceType, "READ-JSON");
// <argument> argument for <method-name> is not valid. (19079)
return false;
}
Iterator<Map.Entry<String, Object>> it = ((JsonObject) baseObj).getProperties().iterator();
if (!it.hasNext())
{
return true; // no dataset stored here
}
Map.Entry<String, Object> next = it.next();
String dsName = next.getKey();
Object val = next.getValue();
if (!(val instanceof JsonObject))
{
ErrorManager.recordOrShowError(15360, "bracket", "");
// Error parsing JSON: unexpected token: <token>. (15360)
return false;
}
if (it.hasNext())
{
ErrorManager.recordOrShowError(15374, "", "");
// Unable to infer Temp-Table or dataset schema from JSON Data. (15374)
return false;
}
ds.name(dsName);
for (Map.Entry<String, Object> tableDef : ((JsonObject) val).getProperties())
{
String tableName = tableDef.getKey();
handle tth = ds.bufferHandle(tableName);
if (tth != null && !tth.isUnknown() && tth._isValid())
{
// skip it, this table is already defined
continue;
}
Object defValue = tableDef.getValue(); // must be an JsonArray which stores all table's records
if (!(defValue instanceof JsonArray))
{
ErrorManager.recordOrShowError(15374, "", "");
// Unable to infer Temp-Table or dataset schema from JSON Data. (15374)
return false;
}
JsonArray jsonArray = (JsonArray) defValue;
int firstElementType = jsonArray.getType(new integer(1)).intValue();
if (firstElementType != JsonBackend.JSON_OBJECT)
{
ErrorManager.recordOrShowError(15358, "bracket", JsonBackend.getJsonDataTypeName(firstElementType));
// Error parsing JSON: expected <token>, but found <token>. (15358)
return false;
}
object<? extends JsonObject> jsonObjectHolder = jsonArray.getJsonObject(new integer(1));
JsonObject jsonObject = jsonObjectHolder.ref();
tth = new handle(false);
TempTableBuilder.create(tth);
TempTableBuilder builder = (TempTableBuilder) tth.getResource();
Iterable<Map.Entry<String, Object>> fields = jsonObject.getProperties();
for (Map.Entry<String, Object> entry : fields)
{
P2JField field = inferFieldType(entry.getKey(), entry.getValue());
if (field == null)
{
ErrorManager.recordOrShowError(15360, "bracket", "");
// Error parsing JSON: unexpected token: <token>. (15360)
ErrorManager.recordOrShowError(15374, "", "");
// Unable to infer Temp-Table or dataset schema from JSON Data. (15374)
return false;
}
builder.addField(field);
}
if (!builder.tempTablePrepare(tableName).booleanValue())
{
ErrorManager.recordOrShowError(15374, "", "");
// Unable to infer Temp-Table or dataset schema from JSON Data. (15374)
return false;
}
// table buffer ready to be added to dataset
ds.addBuffer(builder.defaultBufferHandleNative());
}
return true;
}
/**
* Validates the {@code source} and create the table structure for {@code builder} temp-table.
*
* @param builder
* The {@code TempTableBuilder} to be populated with fields and finally prepared.
* @param source
* The data source.
*
* @return {@code true} if operation is successful, {@code false} on failure, or {@code null}
* if source is not JSONOBJECT or JSONARRAY.
*/
public static Boolean createTableStructure(TempTableBuilder builder, SourceData source)
{
String sourceType = source.getType();
switch (sourceType.toUpperCase())
{
case "JSONOBJECT":
{
BaseDataType bdtSrc = source.getSource();
if (!(bdtSrc instanceof object))
{
ErrorManager.recordOrShowError(19079, sourceType, "READ-JSON");
// <argument> argument for <method-name> is not valid. (19079)
return false;
}
_BaseObject_ baseObj = ((object<?>) bdtSrc).ref();
if (!(baseObj instanceof JsonObject))
{
ErrorManager.recordOrShowError(19079, sourceType, "READ-JSON");
// <argument> argument for <method-name> is not valid. (19079)
return false;
}
JsonObject jsonObject = (JsonObject) baseObj;
Iterable<Map.Entry<String, Object>> it = jsonObject.getProperties();
for (Map.Entry<String, Object> entry : it)
{
P2JField field = inferFieldType(entry.getKey(), entry.getValue());
if (field == null)
{
ErrorManager.recordOrShowError(15360, "bracket", "");
// Error parsing JSON: unexpected token: <token>. (15360)
ErrorManager.recordOrShowError(15374, "", "");
// Unable to infer Temp-Table or dataset schema from JSON Data. (15374)
return false;
}
builder.addField(field);
}
return builder.tempTablePrepare("NewTable").booleanValue();
}
case "JSONARRAY":
{
BaseDataType bdtSrc = source.getSource();
if (!(bdtSrc instanceof object))
{
ErrorManager.recordOrShowError(19079, sourceType, "READ-JSON");
// <argument> argument for <method-name> is not valid. (19079)
return false;
}
_BaseObject_ baseObj = ((object<?>) bdtSrc).ref();
if (!(baseObj instanceof JsonArray))
{
ErrorManager.recordOrShowError(19079, sourceType, "READ-JSON");
// <argument> argument for <method-name> is not valid. (19079)
return false;
}
JsonArray jsonArray = (JsonArray) baseObj;
int firstElementType = jsonArray.getType(new integer(1)).intValue();
if (firstElementType != JsonBackend.JSON_OBJECT)
{
ErrorManager.recordOrShowError(15358, "bracket", JsonBackend.getJsonDataTypeName(firstElementType));
// Error parsing JSON: expected <token>, but found <token>. (15358)
return false;
}
object<? extends JsonObject> jsonObjectHolder = jsonArray.getJsonObject(new integer(1));
JsonObject jsonObject = jsonObjectHolder.ref();
Iterable<Map.Entry<String, Object>> it = jsonObject.getProperties();
for (Map.Entry<String, Object> entry : it)
{
P2JField field = inferFieldType(entry.getKey(), entry.getValue());
if (field == null)
{
ErrorManager.recordOrShowError(15360, "bracket", "");
// Error parsing JSON: unexpected token: <token>. (15360)
ErrorManager.recordOrShowError(15374, "", "");
// Unable to infer Temp-Table or dataset schema from JSON Data. (15374)
return false;
}
builder.addField(field);
}
return builder.tempTablePrepare("NewTable").booleanValue();
}
}
return null;
// ErrorManager.recordOrShowError(19079, sourceType, "READ-JSON");
// // <argument> argument for <method-name> is not valid. (19079)
// return false;
}
/** TBA */
private static P2JField inferFieldType(String name, Object value)
{
String typeName = "character";
if (value instanceof Boolean)
{
typeName = "logical";
}
else if (value instanceof BigInteger || value instanceof BigDecimal ||
value instanceof Integer || value instanceof Long)
{
typeName = "decimal";
}
else if (value instanceof _BaseObject_)
{
return null;
}
return new P2JField(name, TempTableBuilder.validateDatatype(typeName), 0, null, null, null, null,
false, null, null, false, null, null, null, null, false, 0, 0);
}
/**
* Read JSON data from the input source and store it in the just created empty TEMP-TABLE.
* @param table
* TEMP-TABLE to be loaded.
* @param loader
* A {@code BiConsumer} which helps loading a {@code Record} into the buffer.
*
* @return {@code true} on success.
*
* @throws PersistenceException
* if there is any error reading or storing JSON data.
*/
public boolean readTable(TempTableBuilder table, BiConsumer<RecordBuffer, Record> loader)
throws PersistenceException
{
this.ctx.tt = table;
char groupSep = decimal.getGroupSeparator();
char decSep = decimal.getDecimalSeparator();
ObjectMapper mapper = new ObjectMapper();
JsonFactory factory = mapper.getFactory();
try (Reader reader = new InputStreamReader(source.getStream(), StandardCharsets.UTF_8);
JsonParser parser = factory.createParser(reader))
{
this.parser = parser;
JsonToken tok = parser.nextToken();
if (tok != START_OBJECT)
{
// TODO: proper message and error reporting
return false;
}
this.ctx.ttName = parser.nextFieldName();
if (ctx.ttName == null || ctx.ttName.length() > 32 || ctx.ttName.trim().isEmpty())
{
ErrorManager.recordOrShowError(
new int[] {1700, 9044, 15370, 15374},
new String[] {
"Identifier was left blank or is more than 32 characters",
"Unable to prepare TEMP-TABLE fields",
String.format("Unable to create table '%s' from JSON Data", ctx.ttName),
"Unable to infer Temp-Table or dataset schema from JSON Data"
},
false, // modal
true, // prefix
false, // isError
false, // asMsg
true // addDot
);
return false;
}
boolean array = false;
while ((tok = parser.nextToken()) != null)
{
switch (tok)
{
case START_ARRAY:
array = true;
break;
case START_OBJECT:
if (!(ctx.tt._prepared() ? readRecord(false) : parseRecord(false)))
{
return false;
}
break;
case END_ARRAY:
return true;
case END_OBJECT:
if (!array)
{
return true;
}
}
}
if (parser.nextToken() != END_OBJECT)
{
return false;
}
}
catch (IOException exc)
{
throw new PersistenceException(exc);
}
finally
{
decimal.setNumericFormat(decSep, groupSep);
}
return false;
}
/**
* Read JSON data from the input source and store it in the just created empty TEMP-TABLE
* which is a part of a DATA-SET.
* @param tableName
* table name
* @param table
* TEMP-TABLE to be loaded.
* @param loader
* A {@code BiConsumer} which helps loading a {@code Record} into the buffer.
*
* @return {@code true} on success.
*
* @throws PersistenceException
* if there is any error reading or storing JSON data.
*/
@Override
public boolean readTable(String tableName, TempTableBuilder table, BiConsumer<RecordBuffer, Record> loader)
throws PersistenceException
{
pushContext();
this.ctx.tt = table;
this.ctx.ttName = tableName;
this.tables.put(tableName, table);
char groupSep = decimal.getGroupSeparator();
char decSep = decimal.getDecimalSeparator();
try
{
if (ctx.ttName == null || ctx.ttName.length() > 32 || ctx.ttName.trim().isEmpty())
{
ErrorManager.recordOrShowError(
new int[] {1700, 9044, 15370, 15374},
new String[] {
"Identifier was left blank or is more than 32 characters",
"Unable to prepare TEMP-TABLE fields",
String.format("Unable to create table '%s' from JSON Data", ctx.ttName),
"Unable to infer Temp-Table or dataset schema from JSON Data"
},
false, // modal
true, // prefix
false, // isError
false, // asMsg
true // addDot
);
return false;
}
boolean array = false;
JsonToken tok = parser.getCurrentToken();
do
{
switch (tok)
{
case START_ARRAY:
array = true;
break;
case START_OBJECT:
if (!(ctx.tt._prepared() ? readRecord(true) : parseRecord(true)))
{
return false;
}
break;
case END_ARRAY:
return true;
case END_OBJECT:
if (!array)
{
return true;
}
}
}
while ((tok = parser.nextToken()) != null);
if (parser.nextToken() != END_OBJECT)
{
return false;
}
}
catch (IOException exc)
{
throw new PersistenceException(exc);
}
finally
{
decimal.setNumericFormat(decSep, groupSep);
popContext();
}
return false;
}
/**
* Read JSON data from the input source and store it in the temp-table.
*
* @param buffer
* A buffer of a table in which data is stored.
* @param loader
* A {@code BiConsumer} which helps loading a {@code Record} into the buffer.
*
* @return {@code true} on success.
*
* @throws PersistenceException
* if there is any error reading or storing JSON data.
*/
public boolean readTable(TemporaryBuffer buffer,
BiConsumer<RecordBuffer, Record> loader)
throws PersistenceException
{
this.loader = loader;
ctx.setBuffer(buffer, true);
if (readMode == Read.EMPTY)
{
ctx.proxy.deleteAll();
}
// JsonObject and JsonArray sources does not have a [stream] in [source]
if ("JsonObject".equalsIgnoreCase(source.getType()))
{
return readRowFromJsonObject(ctx.schema, ((object<JsonObject>) source.getSource()).ref());
}
else if ("JsonArray".equalsIgnoreCase(source.getType()))
{
return readTableFromJsonArray(ctx.schema, ((object<JsonArray>) source.getSource()).ref());
}
ObjectMapper mapper = new ObjectMapper();
JsonFactory factory = mapper.getFactory();
String tableName = ctx.proxy.name().toJavaType();
String serializeName = ctx.proxy.getSerializeName().toJavaType();
try (Reader reader = new InputStreamReader(source.getStream(), StandardCharsets.UTF_8);
JsonParser parser = factory.createParser(reader))
{
this.parser = parser;
JsonToken tok;
String name;
while ((tok = parser.nextToken()) != null)
{
name = null;
switch (tok)
{
case START_ARRAY:
name = serializeName;
case START_OBJECT:
name = name == null ? parser.nextFieldName() : name;
if (serializeName == null ||
(!serializeName.equalsIgnoreCase(name) && !tableName.equalsIgnoreCase(name)))
{
ErrorManager.recordOrShowError(15376, name, serializeName);
// Temp-table name '<name>' in JSON does not match '<name>'. (15376)
return false;
}
if (!readTableContent())
{
return false;
}
break;
case END_OBJECT:
return true;
}
}
}
catch (IOException exc)
{
throw new PersistenceException(exc);
}
return true;
}
/**
* Read JSON data from the input source and store it in each temp-table of the dataset.
*
* @param ds
* The {@code DataSet} in which data is stored.
*
* @return {@code true} on success.
*
* @throws PersistenceException
* if there is any error reading or storing JSON data.
*/
public boolean readDataset(DataSet ds)
throws PersistenceException
{
if (!source.configureSupportedSources(SourceData.SD_READ_JSON, LegacyResource.DATASET + " widget"))
{
return false;
}
processHiddenFk(ds);
ObjectMapper mapper = new ObjectMapper();
JsonFactory factory = mapper.getFactory();
if ("JsonArray".equalsIgnoreCase(source.getType()))
{
ErrorManager.recordOrShowError(19080, "", "");
// JsonArray is not a valid source-type for READ-JSON on a ProDataset. (19080)
return false;
}
else if ("JsonObject".equalsIgnoreCase(source.getType()))
{
if (/* use 4GL solution */ false)
{
longchar target = new longchar();
JsonConstruct jsonObject = (JsonConstruct) ((object<?>) source.getSource()).ref();
jsonObject.write(target, new logical(false));
JsonImport jsonImport = new JsonImport(new SourceData("longchar", target), readMode.toString());
return jsonImport.readDataset(ds);
}
else
{
if (ds._dynamic())
{
if (!createDatasetStructure(ds, source))
{
return false;
}
}
this.ds = ds;
return readDatasetFromJsonObject(((object<JsonObject>) source.getSource()).ref());
}
}
try (Reader reader = new InputStreamReader(source.getStream(), StandardCharsets.UTF_8);
JsonParser parser = factory.createParser(reader))
{
this.ds = ds;
this.parser = parser;
this.peerMapping = new HashMap<>();
JsonToken tok = parser.nextToken();
if (tok != START_OBJECT)
{
// TODO: proper message and error reporting
return false;
}
String name = parser.nextFieldName();
String dsSerializeName = ds.getSerializeName().toJavaType();
String dsName = ds._name();
if (!dsName.isEmpty() &&
!ds._dynamic() &&
!(dsName.equalsIgnoreCase(name) ||
dsSerializeName.equalsIgnoreCase(name)))
{
ErrorManager.recordOrShowError(15375, name, dsName);
// Dataset name '<name>' in JSON does not match '<name>'.
return false;
}
if (ds.getBuffers().isEmpty())
{
return readDataSetContent(name);
}
if (!readDataSetContent())
{
return false;
}
if (parser.nextToken() != END_OBJECT)
{
return false;
}
}
catch (FileNotFoundException exc)
{
ErrorManager.recordOrShowError(293, source.getFileName());
return false;
}
catch (IOException exc)
{
throw new PersistenceException(exc);
}
finally
{
this.ds = null;
this.peerMapping = null;
}
return true;
}
/**
* Reads the content of an empty dynamic dataset, by inferring its structure and
* populating its whole set of tables.
*
* @param name
* dataset name.
* @return {@code true} on success.
*
* @throws IOException
* If an IO error occurred while getting the data.
* @throws PersistenceException
* If an error occurred while persisting the read data.
*/
private boolean readDataSetContent(String name)
throws IOException,
PersistenceException
{
String tableName = null;
JsonToken tok = parser.nextToken();
if (tok == JsonToken.START_ARRAY)
{
if ((tok = parser.nextToken()) != START_OBJECT)
{
return false;
}
tableName = name;
name = null;
handle tt = TypeFactory.handle();
TempTableBuilder.create(tt);
TempTableBuilder ttb = (TempTableBuilder)tt.unwrapJsonData();
if (!ttb.readTable(tableName, this))
{
return false;
}
}
if (tok != START_OBJECT)
{
return false;
}
process:
while ((tok = parser.nextToken()) != null)
{
switch (tok)
{
case FIELD_NAME:
tableName = parser.currentName();
if ("prods:hasChanges".equals(tableName))
{
String hasChanges = parser.nextValue().asString(); // TODO: maybe remember this?
tableName = null;
}
else if ("prods:before".equals(tableName))
{
readingBefore = true;
if (parser.nextToken() != START_OBJECT)
{
return false;
}
tableName = null;
}
break;
case END_OBJECT:
if (readingBefore)
{
readingBefore = false;
}
else
{
break process;
}
break;
}
if (tableName != null)
{
handle tt = TypeFactory.handle();
TempTableBuilder.create(tt);
TempTableBuilder ttb = (TempTableBuilder)tt.unwrapJsonData();
if (!ttb.readTable(tableName, this))
{
return false;
}
}
}
setupDataset(name);
return true;
}
/**
* Reads the content of a dataset, by populating its whole set of tables.
*
* @return {@code true} on success.
*
* @throws IOException
* If an IO error occurred while getting the data.
* @throws PersistenceException
* If an error occurred while persisting the read data.
*/
private boolean readDataSetContent()
throws IOException,
PersistenceException
{
if (parser.nextToken() != START_OBJECT)
{
return false;
}
if (readMode == Read.EMPTY)
{
ds.emptyDataset();
}
String tableName = null;
JsonToken tok;
while ((tok = parser.nextToken()) != null)
{
switch (tok)
{
case FIELD_NAME:
tableName = parser.currentName();
if ("prods:hasChanges".equals(tableName))
{
String hasChanges = parser.nextValue().asString(); // TODO: maybe remember this?
tableName = null;
}
else if ("prods:before".equals(tableName))
{
readingBefore = true;
if (parser.nextToken() != START_OBJECT)
{
return false;
}
tableName = null;
}
break;
case END_OBJECT:
if (readingBefore)
{
readingBefore = false;
}
else
{
return true;
}
break;
}
if (tableName != null)
{
BufferImpl after = null;
List<BufferImpl> dsBuffers = ds.getBuffers();
for (BufferImpl buf : dsBuffers)
{
if (tableName.equalsIgnoreCase(buf.getSerializeName().toJavaType()) ||
tableName.equalsIgnoreCase(buf._name()))
{
after = buf;
break;
}
}
if (after == null)
{
return false;
}
BufferImpl buffer = after;
if (readingBefore)
{
buffer = after.beforeBufferNative();
if (buffer == null)
{
return false;
}
buffer.setUpBeforeBuffer(true);
}
try
{
ctx.setBuffer(buffer.buffer(), true);
this.primaryIndex = null;
if (readMode == Read.REPLACE && !readingBefore)
{
// the REPLACE mode needs primary unique index declared for the after table
this.primaryIndex = buffer.buffer().getDmoInfo().getPrimaryIndex(true);
if (this.primaryIndex == null)
{
ErrorManager.recordOrShowError(13063, ctx.schema.getTableName());
// REPLACE mode requires a unique primary index in the target table <table>.
return false;
}
}
readTableContent();
}
finally
{
if (readingBefore)
{
buffer.setUpBeforeBuffer(false);
}
}
tableName = null;
}
}
return true;
}
/**
* Read JSON content from reader and import records into the temp-table.
* <p>
* This version ensures that the group and decimal separators are ',' and '.' for the duration of the
* deserialization.
*
* @throws IOException
* if there is an error reading JSON content from the stream.
* @throws PersistenceException
* if there is an error storing data in the temp-table.
*/
private boolean readTableContent()
throws IOException,
PersistenceException
{
char groupSep = decimal.getGroupSeparator();
char decSep = decimal.getDecimalSeparator();
try
{
decimal.setNumericFormat('.', ',');
JsonToken tok;
while ((tok = parser.nextToken()) != null)
{
switch (tok)
{
case START_ARRAY:
break;
case START_OBJECT:
if (!readRecord(false))
{
return false;
}
break;
case END_ARRAY:
case END_OBJECT:
return true;
}
}
throw new IOException("Unexpected end of JSON data");
}
finally
{
decimal.setNumericFormat(decSep, groupSep);
}
}
/**
* Parse JSON record to retrieve fields' data, prepare table and populate the first record
*
* @param addLinkToParent
* flag indicating that a link to the parent must be added
* @return <code>true</code> on success.
*
* @throws IOException
* on I/O error
* @throws PersistenceException
* on error.
*/
private boolean parseRecord(boolean addLinkToParent)
throws IOException,
PersistenceException
{
FieldMaker maker = this::addField;
Map<String, Object> readValues = new LinkedHashMap<>();
String fname = null;
List<Object> datum = new ArrayList<>();
boolean extent = false;
boolean nested = false;
boolean persisted = false;
JsonToken tok;
objectLoop: while ((tok = parser.nextToken()) != null)
{
// need a look-ahead(1) to decide whether the FIELD_NAME is a field or a NESTED table
if (tok == FIELD_NAME)
{
fname = parser.currentName();
tok = parser.nextToken();
}
switch (tok)
{
case START_OBJECT:
if (!extent)
{
// TODO: error? Should 'never happen' unless the source JSON is badly broken.
return false;
}
extent = false;
if (!datum.isEmpty())
{
// TODO: error? should 'never happen' unless the source JSON is badly broken.
return false;
}
nested = true;
AbstractTempTable att = tables.get(fname);
if (att == null)
{
handle tt = TypeFactory.handle();
TempTableBuilder.create(tt);
att = (TempTableBuilder)tt.unwrapJsonData();
inferredRelations.add(Pair.of(ctx.ttName, fname));
}
if (!readValues.isEmpty() && !persist(maker, readValues, addLinkToParent))
{
return false;
}
persisted = true;
if (!att.readTable(fname, this))
{
return false;
}
break;
case START_ARRAY:
extent = true;
break;
case END_ARRAY:
if (extent && datum.isEmpty())
{
ErrorManager.recordOrShowError(
new int[] {15368, 15374},
new String[] {
"More than one Temp-Table inferred from JSON Data",
"Unable to infer Temp-Table or dataset schema from JSON Data"
},
false, // modal
true, // prefix
false, // isError
false, // asMsg
true // addDot
);
return false;
}
extent = false;
break;
case VALUE_NULL:
datum.add(null);
break;
case VALUE_STRING:
datum.add(parser.getText());
break;
case VALUE_NUMBER_INT:
datum.add(parser.getLongValue());
break;
case VALUE_NUMBER_FLOAT:
datum.add(parser.getDecimalValue());
break;
case VALUE_TRUE:
case VALUE_FALSE:
datum.add(parser.getBooleanValue());
break;
case END_OBJECT:
break objectLoop;
}
if (!extent && !nested)
{
Object value = datum.size() == 1 ? datum.get(0) : new ArrayList<>(datum);
datum.clear();
if (fname == null)
{
throw new PersistenceException("No name for field: '" + value + "'");
}
readValues.put(fname, value);
fname = null;
}
}
return persisted || persist(maker, readValues, addLinkToParent);
}
/**
* Get an inferred type of the value.
*
* @param val
* The value to be analyzed.
*
* @return the inferred type of the value.
*
* @throws PersistenceException
* when the type cannot be inferred from the provided value.
*/
@Override
protected ParmType typeOf(Object val)
throws PersistenceException
{
if (val == null)
{
return ParmType.CHAR;
}
if (val instanceof Boolean)
{
return ParmType.LOG;
}
if (val instanceof BigDecimal || val instanceof Long)
{
return ParmType.DEC;
}
if (val instanceof String)
{
return ParmType.CHAR;
}
if (val instanceof List)
{
for(Object o: (List<Object>) val)
{
if (o != null)
{
return typeOf(o);
}
}
return ParmType.CHAR;
}
throw new PersistenceException("Unexpected value type:" + val.getClass().getName());
}
/**
* Read JSON content from the stream corresponding with a single data record and store it in
* the temp-table.
*
* @param addLinkToParent
* flag indicating that a link to the parent must be added.
*
* @throws IOException
* if there is an error reading JSON content from the stream.
* @throws PersistenceException
* if there is an error storing data in the temp-table.
*/
private boolean readRecord(boolean addLinkToParent)
throws IOException,
PersistenceException
{
if (readMode != Read.REPLACE)
{
try
{
ctx.proxy.create();
}
catch (ErrorConditionException ex)
{
ErrorManager.recordOrShowError(13050, ctx.proxy.name().toStringMessage());
// Unable to create record for ''<table> during READ-XML/READ-JSON.
return false;
}
}
String name = null;
boolean batchError = true;
boolean returnSuccess = true;
RecordBuffer recBuffer = ((BufferImpl) ctx.proxy).buffer();
Map<String, String> readValues = (readMode == Read.REPLACE) ? new HashMap<>() : null;
try
{
if (readMode != Read.REPLACE)
{
RecordBuffer.startBatch(bufMan, true);
}
fillFromParent();
TempTableSchema.Column column = null;
String value = null;
Object datum = null;
boolean extent = false;
JsonToken tok;
objectLoop: while ((tok = parser.nextToken()) != null)
{
// need a look-ahead(1) to decide whether the FIELD_NAME is a field or a NESTED table
if (tok == FIELD_NAME)
{
name = parser.currentName();
tok = parser.nextToken();
column = getColumn(name);
// if the column was not found in the columns map,
// there is a chance that the column key was it's serialize name but, we provided it's name,
// so we should search iterativelly through all the columns in the schema and check for both
// the serialize name and column name.
if (column == null)
{
for (TempTableSchema.Column col : ctx.schema.columns())
{
if (col.getFieldName().equalsIgnoreCase(name) || col.getSerializeName().equalsIgnoreCase(name))
{
column = col;
break;
}
}
}
if (column == null && tok == START_ARRAY)
{
// if there is no column with this name, then try to read a nested child DataSet table
if (!readNested(name))
{
return false;
}
continue; // next token
}
}
switch (tok)
{
case START_ARRAY:
extent = true;
break;
case END_ARRAY:
if (extent)
{
extent = false;
break;
}
// pass through
case END_OBJECT:
batchError = false;
break objectLoop;
case VALUE_NULL:
if (column != null)
{
if (!column.isNillable())
{
ErrorManager.recordOrThrowError(0,
"Column not nillable: " + column.getFieldName(),
false);
}
if (readMode == Read.REPLACE)
{
// in REPLACE mode, store the read values temporarily
readValues.put(extent ? name + " " + ctx.extentTracker.getNext(name) : name, null);
}
else
{
setField(column, name, null, extent);
}
}
break;
case VALUE_STRING:
case VALUE_NUMBER_INT:
case VALUE_NUMBER_FLOAT:
case VALUE_TRUE:
case VALUE_FALSE:
// TODO: this approach seems inefficient in that the parser already has parsed to a useful
// data type, then we convert to a string, but it's necessitated by the use of the
// complex Stream.assignDatum method downstream; we would have to duplicate much of
// the functionality of that method here to do this more efficiently
value = null;
switch (tok)
{
case VALUE_STRING:
value = parser.getText();
break;
case VALUE_NUMBER_INT:
datum = parser.getLongValue();
break;
case VALUE_NUMBER_FLOAT:
datum = parser.getDecimalValue();
// ensure exponent is not used
datum = ((BigDecimal) datum).toPlainString();
break;
case VALUE_TRUE:
case VALUE_FALSE:
datum = parser.getBooleanValue();
break;
}
if (value == null)
{
value = datum == null ? null : datum.toString();
}
if (column != null)
{
if (readMode == Read.REPLACE)
{
// in REPLACE mode, store the read values temporarily
readValues.put(extent ? name + " " + ctx.extentTracker.getNext(name) : name, value);
}
else
{
setField(column, name, value, extent);
}
}
break;
}
}
batchError = false;
}
finally
{
ctx.extentTracker.reset();
if (readMode == Read.REPLACE)
{
boolean batched = false;
try
{
Boolean foundConflict = null;
DmoMeta dmoInfo = recBuffer.getDmoInfo();
primaryIndex = dmoInfo.getPrimaryIndex(true);
if (primaryIndex != null && primaryIndex.isUnique())
{
List<Object> params = new ArrayList<>();
StringBuilder sb = new StringBuilder();
sb.append("from ").append(dmoInfo.getDmoImplInterface().getName()).append(" where ");
if (recBuffer.isTemporary()) // always, right?
{
sb.append("_multiplex=?");
params.add(recBuffer.getMultiplexID()); // not null if temp-table
}
Set<String> indexComps = new HashSet<>();
ArrayList<P2JIndexComponent> comps = primaryIndex.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent comp = comps.get(i);
String fieldName = comp.getLegacyName();
if (!readValues.containsKey(fieldName)) // this will not match an extent field
{
foundConflict = false; // not full field-set for the primary unique index
break;
}
if (!params.isEmpty())
{
sb.append(" and ");
}
sb.append(comp.getPropertyName()).append("=?");
params.add(readValues.get(fieldName));
indexComps.add(fieldName);
}
if (foundConflict == null)
{
Query uq = Session.createQuery(sb.toString());
for (int i = 0; i < params.size(); i++)
{
uq.setParameter(i, params.get(i));
}
Object dmo = uq.uniqueResult(recBuffer.getPersistence().getSession(!dmoInfo.multiTenant));
foundConflict = (dmo != null);
// check if we have the full index field-set but not a matching record
if (foundConflict)
{
loader.accept(recBuffer, (Record) dmo); // loads the record into the buffer
RecordBuffer.startBatch(bufMan, true);
batched = true;
for (Map.Entry<String, String> entry : readValues.entrySet())
{
String keyName = entry.getKey();
if (!indexComps.contains(keyName))// do not overwrite the key/index component
{
int k = keyName.indexOf(' ');
int extIndex = -1;
if (k > 0)
{
extIndex = Integer.parseInt(keyName.substring(k + 1));
keyName = keyName.substring(0, k);
}
setField(getColumn(keyName), keyName, entry.getValue(), extIndex);
}
}
}
}
}
if (foundConflict == null)
{
throw new RuntimeException("Internal error");
}
else if (!foundConflict)
{
ctx.proxy.create();
RecordBuffer.startBatch(bufMan, true);
batched = true;
for (Map.Entry<String, String> entry : readValues.entrySet())
{
String keyName = entry.getKey();
int k = keyName.indexOf(' ');
int extIndex = -1;
if (k > 0)
{
extIndex = Integer.parseInt(keyName.substring(k + 1));
keyName = keyName.substring(0, k - 1);
}
setField(getColumn(keyName), keyName, entry.getValue(), extIndex);
}
}
}
finally
{
if (batched)
{
ErrorManager.ErrorHelper eh = ErrorManager.getErrorHelper();
boolean wm = eh.isWarningMode();
if (!wm)
{
// this is mostly for APPEND mode, the other ones will not encounter issues here
eh.setWarningMode(true);
}
int validationErrors = RecordBuffer.endBatch(bufMan, batchError);
if (!wm)
{
eh.setWarningMode(false);
}
// if (ErrorManager.error().toJavaType())
if (ErrorManager.isPendingError() || validationErrors != 0)
{
((BufferImpl) ctx.proxy).drop();
ErrorManager.recordOrShowError(15366, true, ctx.schema.getName(), "");
// Unable to update indexes for table '<table>'. (15366)
returnSuccess = false;
}
}
// else some exception occurred preventing the RecordBuffer to start the batch
}
}
else
{
if (readMode == Read.MERGE)
{
boolean validated = false;
try
{
validated = recBuffer.validate(false);
}
catch (ValidationException e)
{
// ignore, no 'expected' exception will be thrown
}
// was there an unique conflict with this record?
if (!validated)
{
// merge mode: ignore it, just skip to next record
ctx.proxy.release(); // TODO: not enough
}
}
ErrorManager.ErrorHelper eh = ErrorManager.getErrorHelper();
boolean wm = eh.isWarningMode();
if (!wm)
{
// this is mostly for APPEND mode, the other ones will not encounter issues here
eh.setWarningMode(true);
}
int validationErrors = RecordBuffer.endBatch(bufMan, batchError);
if (!wm)
{
eh.setWarningMode(false);
}
// if (ErrorManager.error().toJavaType())
if (ErrorManager.isPendingError() || validationErrors != 0)
{
((BufferImpl) ctx.proxy).drop();
ErrorManager.recordOrShowError(15366, true, ctx.schema.getName(), "");
// Unable to update indexes for table '<table>'. (15366)
returnSuccess = false;
}
}
}
return returnSuccess; // if batchError is on, the returnSuccess was also set to false
}
/**
* Read nested table.
*
* @param name
* The name of the table to be read.
*
* @return {@code true} on success and {@code false} on fail.
*
* @throws PersistenceException
* on database error.
* @throws IOException
* on file I/O error.
*/
private boolean readNested(String name)
throws PersistenceException,
IOException
{
boolean rc = true;
pushContext();
try
{
if (!setParentValues(name))
{
return false;
}
BufferImpl buffer = null;
if (ds.getBuffers().isEmpty()) // empty dynamic dataset
{
AbstractTempTable att = tables.get(name);
if (att == null)
{
handle tt = TypeFactory.handle();
TempTableBuilder.create(tt);
att = (TempTableBuilder)tt.getResource();
inferredRelations.add(Pair.of(ctx.ttName, name));
if (!att.readTable(name, this))
{
return false;
}
}
else
{
buffer = (BufferImpl) att.defaultBufferHandleNative();
}
}
else
{
buffer = ds.getBufferBySerializeName(name);
}
if (buffer != null)
{
ctx.setBuffer(buffer.buffer(), true);
rc = readTableContent();
}
}
finally
{
// restore configuration for next row
popContext();
}
return rc;
}
/**
* Iterates the {@code JsonObject} source and populates the tables matching the properties.
*
* @param source
* The source which contains the {@code JsonObject} source.
*
* @return {@code true} on success and {@code false} otherwise.
*
* @throws PersistenceException
* on unexpected events related to persistence.
*/
private boolean readDatasetFromJsonObject(JsonObject source)
throws PersistenceException
{
JsonObject jsonObject = source;
if (!ds.getSerializeHidden().booleanValue())
{
Map.Entry<String, Object> dsEntry = source.getProperties().iterator().next();
// assert dsEntry.getKey() == ds.name()
jsonObject = (JsonObject) dsEntry.getValue();
}
for (Map.Entry<String, Object> entry : jsonObject.getProperties())
{
String tableName = entry.getKey();
handle bufferHandle = ds.bufferHandle(tableName);
if (bufferHandle == null || bufferHandle.isUnknown() || !bufferHandle._isValid())
{
BufferImpl buffer = ds.getBufferBySerializeName(tableName);
if (buffer == null)
{
// there is no such table in this dataset
continue;
}
bufferHandle = new handle(buffer);
}
RecordBuffer buffer = ((BufferImpl) bufferHandle.getResource()).buffer();
ctx.setBuffer(buffer, true);
if (readMode == Read.EMPTY)
{
ctx.proxy.deleteAll();
}
if (!readTableFromJsonArray(ctx.schema, (JsonArray) entry.getValue()))
{
return false;
}
}
// all fine
return true;
}
/**
* Iterates the {@code JsonArray} source and for each element creates a new row in the target temp-table.
*
* @param schema
* The schema of the temp-table.
* @param jsonArray
* The source which contains the {@code JsonArray} source.
*
* @return {@code true} on success and {@code false} otherwise.
*
* @throws PersistenceException
* on unexpected events related to persistence.
*/
private boolean readTableFromJsonArray(TempTableSchema schema, JsonArray jsonArray)
throws PersistenceException
{
int length = jsonArray.getLength().intValue();
for (int i = 0; i < length; i++)
{
int elementType = jsonArray.getType(new integer(i + 1)).intValue();
if (elementType != JsonBackend.JSON_OBJECT)
{
ErrorManager.recordOrShowError(15358, "bracket", JsonBackend.getJsonDataTypeName(elementType));
// Error parsing JSON: expected <token>, but found <token>. (15358)
return false;
}
if (!readRowFromJsonObject(schema, jsonArray.getJsonObject(new integer(i + 1)).ref()))
{
return false;
}
}
// all OK
return true;
}
/**
* Add field to the temp-table.
*
* @param ttb
* The destination temp-table builder.
* @param name
* The name of the field to be added.
* @param val
* The value of field. It is used for inferring the type of the new field.
*
* @throws PersistenceException
* If the type of the field cannot be inferred from the provided value.
*/
private void addField(TempTableBuilder ttb, String name, Object val)
throws PersistenceException
{
String type = typeOf(val).toString();
if (val instanceof List)
{
int ext = ((List<Object>) val).size();
ttb.addNewField(new character(name), new character(type), new integer(ext));
}
else
{
ttb.addNewField(new character(name), new character(type));
}
}
/**
* Creates a new record in target temp-table, iterates the {@code JsonObject} source and populates the new
* records with data extracted from the source {@code JsonObject}.
*
* @param schema
* The schema of the temp-table.
* @param jsonObject
* The source which contains the {@code JsonObject} source.
*
* @return {@code true} on success and {@code false} otherwise.
*
* @throws PersistenceException
* on unexpected events related to persistence.
*/
private boolean readRowFromJsonObject(TempTableSchema schema, JsonObject jsonObject)
throws PersistenceException
{
try
{
ctx.proxy.create();
}
catch (ErrorConditionException ex)
{
ErrorManager.recordOrShowError(13050, ctx.proxy.name().toStringMessage());
// Unable to create record for ''<table> during READ-XML/READ-JSON.
return false;
}
boolean res = true;
for (Map.Entry<String, Object> property : jsonObject.getProperties())
{
String key = property.getKey();
try
{
String value = null;
ArrayList<TempTableSchema.Column> columns = schema.columns();
TempTableSchema.Column column = null;
for (int k = 0; k < columns.size(); k++)
{
TempTableSchema.Column item = columns.get(k);
if (key.equalsIgnoreCase(item.getSerializeName()))
{
column = item;
break;
}
}
character fieldName = new character(key);
boolean hasProp = column != null;
if (hasProp)
{
integer type = jsonObject.getType(fieldName);
if (type.intValue() == JsonBackend.JSON_ARRAY)
{
String fname = column.getFieldName();
JsonArray arr = jsonObject.getJsonArray(fieldName).ref();
Integer extent = column.getExtent();
if (extent == null || extent == 0)
{
ErrorManager.recordOrShowError(new int[] { 15364 },
new String[] { ErrorManager.replaceTokens(15364, fname) },
false, // modal
false, // no prefix
false, // error
false, // not as message
true); // with dot
res = false;
break;
}
if (arr.getLength().intValue() > extent)
{
ErrorManager.recordOrShowError(new int[] { 13054 },
new String[] { ErrorManager.replaceTokens(13054, fname, schema.getName()) },
false, // modal
false, // no prefix
false, // error
false, // not as message
true); // with dot
res = false;
break;
}
Iterator<Object> iter = arr.getElements().iterator();
integer idx = new integer(1);
while (iter.hasNext() && idx.intValue() <= extent)
{
iter.next(); // we don't get the value directly
type = arr.getType(idx);
value = null;
switch (type.intValue())
{
case JsonBackend.JSON_STRING:
value = arr.getCharacter(idx).getValue();
break;
case JsonBackend.JSON_NUMBER:
value = arr.getDecimal(idx).toStringMessage();
break;
case JsonBackend.JSON_BOOLEAN:
value = arr.getLogical(idx).toStringMessage();
break;
case JsonBackend.JSON_OBJECT:
value = arr.getJsonObject(idx).toStringMessage();
break;
}
setField(column, fname, value, idx.intValue() - 1);
idx.assign(idx.intValue() + 1);
}
}
else
{
switch (type.intValue())
{
case JsonBackend.JSON_STRING:
value = jsonObject.getCharacter(fieldName).getValue();
break;
case JsonBackend.JSON_NUMBER:
value = jsonObject.getDecimal(fieldName).toStringMessage();
break;
case JsonBackend.JSON_BOOLEAN:
value = jsonObject.getLogical(fieldName).toStringMessage();
break;
case JsonBackend.JSON_OBJECT:
value = jsonObject.getJsonObject(fieldName).toStringMessage();
break;
}
BaseDataType datum = BaseDataTypeFactory.instantiate(column.getType());
if (value == null)
{
datum.setUnknown();
}
else
{
Stream.assignDatum(datum, value, true);
}
column.getSetter().invoke(ctx.proxy, datum);
}
}
}
catch (Exception exc)
{
ctx.proxy.deleteRecord();
throw new PersistenceException(exc);
}
}
if (!res)
{
ctx.proxy.deleteRecord();
}
else
{
ctx.proxy.release();
}
return res;
}
}