BufferImpl.java
/*
** Module : BufferImpl.java
** Abstract : Implementation of Buffer interface.
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------------Description----------------------------------------
** 001 ECF 20130118 Created initial version. Implementation of the Buffer
** interface; provides services for buffer-related, Progress
** methods, attributes, and some language statements.
** 002 VMN 20130207 Added support for handle-based methods
** BUFFER-COPY (KW_BUF_COPY)
** BUFFER-COMPARE (KW_BUF_COMP)
** BUFFER-FIELD (KW_BUF_FLD)
** BUFFER-RELEASE (KW_BUF_REL)
** FIND-BY-ROWID (KW_FIND_BR)
** FIND-UNIQUE (KW_GET_UNI)
** FIND-FIRST (KW_FIND_1ST)
** FIND-LAST (KW_FIND_LST)
** and attributes
** NUM-FIELDS (KW_NUM_FLD)
** COLUMN-LABEL (KW_COL_LAB)
** INITIAL (KW_INIT)
** AMBIGUOUS (KW_AMBIG)
** 003 ECF 20130214 Added bufferCreate and variants of bufferCopy.
** 004 SVL 20130213 Added disable*Triggers functions.
** 005 VMN 20130217 Added support for handle-based attributes:
** CURRENT-ITERATION (KW_CUR_ITER)
** ERROR (KW_ERROR)
** DBNAME (KW_DBNAME)
** TABLE (KW_TABLE)
** 006 VMN 20130219 Added support for attributes
** TABLE-HANDLE (KW_TAB_HAND)
** 007 CA 20130221 Added support for ADM-DATA, UNIQUE-ID, NAMESPACE-URI. Implemented Deletable.
** Added missing BUFFER-FIELD stubs.
** 008 GES 20130224 Added support for a DELETE statement with a validation expression.
** 009 ECF 20130224 Added limited support for embedded SQL.
** 010 OM 20130222 Added bufferField variants and dereference from Dereferenceable.
** 011 OM 20130304 Refactored isValid and isUnknown of WrappedResource to valid and unknown.
** 012 VMN 20130306 Corrected support for handle-based methods:
** FIND-UNIQUE (KW_GET_UNI)
** FIND-FIRST (KW_FIND_1ST)
** FIND-LAST (KW_FIND_LST)
** 013 VMN 20130311 Corrected support and runtime for handle-based methods:
** FIND-UNIQUE (KW_GET_UNI)
** FIND-FIRST (KW_FIND_1ST)
** FIND-LAST (KW_FIND_LST)
** FIND-BY-ROWID (KW_FIND_BR)
** BUFFER-RELEASE (KW_BUF_REL)
** BUFFER-COMPARE (KW_BUF_COMP)
** 014 ECF 20130309 Changed API for bufferCopy and bufferCompare; only wrapped data types are
** allowed for literal parameters, to prevent the explosion of the API.
** 015 VMN 20130314 Implemented runtime for handle-based methods:
** BUFFER-COMPARE (KW_BUF_COMP) also noLobs parameter type changed
** BUFFER-COPY (KW_BUF_COPY) (simple signatures)
** First parameter type changed from String to character for
** FIND-UNIQUE (KW_GET_UNI)
** FIND-FIRST (KW_FIND_1ST)
** FIND-LAST (KW_FIND_LST)
** 016 CA 20130323 Changed the APIs for KW_BUF_DEL and KW_EMPTY_TT to return logical.
** 017 ECF 20130417 Moved initial() method to FieldReference.
** 018 VMN 20130426 Implemented runtime for handle-based methods:
** BUFFER-COMPARE (KW_BUF_COMP)
** BUFFER-COPY (KW_BUF_COPY)
** 019 CA 20130603 resourceDelete must return a boolean value.
** 020 VMN 20130711 Implemented verifyTransaction() and added to handle-based methods:
** BUFFER-CREATE (KW_BUF_CREA)
** BUFFER-DELETE (KW_BUF_DELETE)
** FIND-BY-ROWID (KW_FIND_BR)
** 021 VMN 20130717 Added aggregated functions support for embedded SQL.
** 022 SVL 20130822 Added DYNAMIC attribute, implemented bufferField.
** 023 CA 20130927 Removed unknown() API, as by default the resource is known.
** 024 CA 20131021 Do not delete static buffers.
** 025 ECF 20131028 Import change.
** 026 SVL 20131028 Handled bufferField() for static tables, added doSetName.
** 027 CA 20131013 Static buffers are deleted only when the instantiating procedure gets deleted.
** 028 OM 20131128 Stub implementation for getUniqueID.
** 029 HC 20131222 Implemented ADM-DATA and UNIQUE-ID attribute.
** 030 VMN 20140110 Implemented bufferField(character).
** 031 OM 20140107 Implemented disableDumpTriggers() and disableLoadTriggers() methods and
** and removed unneeded openReadOnlyScope().
** 032 OM 20140116 Implemented attributes and methods (NUM-FIELDS, ERROR, DBNAME, TABLE,
** NAMESPACE-URI, dereference(::), BUFFER-FIELD(int), intermediary step for
** findFirst, findLast, findUnique).
** 033 OM 20140122 Added primary index selection for find-by-rowid dynamic finds. Added
** implementation for dynamic finds methods using DynamicQueryHelper.
** 034 ECF 20140305 Added defaultBufferHandle implementation, reworked bufferField(String) to
** handle invalid buffer name.
** 035 VMN 20140330 Added LockType errors processing.
** 036 VIG 20140314 Changed currentIteration() method signature.
** 037 OM 20141103 Added doGetName() method.
** 038 ECF 20141117 Added stub methods for all raw-transfer variants.
** 039 ECF 20150114 Reimplemented valid() to always return true. A buffer whose scope is not open
** is still valid for use.
** 040 OM 20150205 Queries do not need to be unregistered from DynamicQueryHelper.
** 041 OM 20150302 Fixed bufferField() behavior on invalid field name.
** 042 ECF 20150315 Replaced FieldReference with BufferFieldImpl for BUFFER-FIELD use cases.
** 043 OM 20150303 Fixed hasNext/PrevSibling(). Implemented special resource chaining for
** BUFFERs (TEMP-TABLEs in front of permanent TABLE buffers).
** 044 OM 20150506 Updated javadoc for deleteAll() and delete() methods.
** 045 CA 20151219 Implicit buffers for a defined global shared var can not be deleted. Also,
** deleted buffers must be tracked explicitly.
** 046 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 047 ECF 20160219 Added checkExtentRange and outOfBoundsError, methods which are called from
** DMO proxies. Reimplemented _numFields.
** 048 EVL 20160325 Javadoc fixes.
** 049 ECF 20160404 Javadoc fixes.
** 050 OM 20160302 Dropped errors generated inside _find() methods.
** 20160507 Partially reverted dropping of _find() errors to fix regressions.
** 051 SVL 20160510 Support for table-level delete validation.
** 052 ECF 20160517 Modified findByRowID to deal with null primary index more efficiently.
** 053 GES 20160530 Validation processing now uses lambdas instead of BufferValidator instances.
** 054 OM 20160623 Small change in error message.
** 055 SVL 20160705 Added indexInformation method.
** 056 IAS 20160714 Use legacy name instead of DMO alias.
** 057 ECF 20160829 Performance enhancement: use local BufferManager to check transaction status
** instead of TransactionManager.
** 058 IAS 20161907 Resolve P2JQuery.Parameter in delete(String, Object...)
** 059 ECF 20171016 Implemented readXml and writeXml methods.
** 060 GES 20171206 Switch to ErrorManager.silent().
** ECF 20171212 Remove redundant query substitution parameter preprocessing for bulk delete.
** 061 OM 20171216 Added delete() variant with support for external buffers for nested selects.
** CA 20171220 Cleanup the SharedVariableManager.bufferCreator on buffer delete.
** 062 GES 20180419 Added the RAW-TRANSFER() legacy method.
** 063 OM 20181114 Added NamedSerializable interface implementation.
** 064 ECF 20181208 Added stubs for recordLength, findCurrent, readJson, writeJson methods.
** 065 OM 20181206 Added QUERY readonly attribute for BUFFERs.
** 066 OM 20190117 Added CURRENT-CHANGED, FIND-CURRENT. Fixed locking in _find.
** ECF 20190118 Added basic runtime implementation of readJson, writeJson.
** OM 20190206 Added implementation of recordLength().
** OM 20190213 Added error messages in dereference().
** ECF 20190212 Added no-lobs implementation to bufferCopy, bufferCompare methods.
** OM 20190228 Removed deleted buffers from dynamic queries.
** 067 OM 20190327 Renamed DataSource to avoid conflicts with DataSet source.
** 068 CA 20190423 Added dereference(Class, String), used for chained :: operators.
** OM 20190423 Implemented copyTempTable() methods.
** 069 CA 20190511 Added API for BUFFER-FIELD(POLY).
** Added BUFFER-VALIDATE stub.
** 070 SVL 20190614 Reflected changes in TableMapper.
** 071 OM 20190426 Extracted hasField() and getHibernateField() methods to avoid code duplication.
** OM 20190510 Clarified generation of methods and attributes for variables with BUFFER /
** TEMP-TABLE ABL namespaces specified. Implemented several attributes.
** HC 20190707 Implemented read(dataSet) and read(tempTable) methods of JsonObject legacy
** class.
** 072 ECF 20190813 Change to RecordBuffer.release API.
** 073 OM 20190720 Added BATCH FILL support. FILL returns FALSE and stops if a child FILL fails.
** Fixed CamelCase Sensitivity. Fixed FILL with DATA-SOURCE with provided query.
** CA 20190728 If a callback raises an ERROR condition, invokeCallback must return false.
** Also, any FILL operation must be aborted.
** CA 20190812 Allow this class to be mutable, to allow switching between resources, in cases
** of BY-REFERENCE or REFERENCE-ONLY. WARNING: it is not allowed to access
** instance fields, if they are in another instance than 'this' reference!
** Use the 'ref()' API to get the runtime instance or add the field to an
** accessor method.
** OM 20190906 Added support for MERGE and REPLACE fill modes. Added ERROR-STRING and
** ERROR-FLAG attributes support.
** 075 CA 20191119 Fixed some issue with SAVE-ROW-CHANGES: any ERROR message prior to invocation
** must not affect the result; fixed the 'source was not changed in the database'
** check, where the source and before buffer must be checked.
** 076 AIL 20191128 Moved the validation phase in RecordBuffer.
** 077 AIL 20200107 Added name override and restore for temporarily changing name with proxy.
** AIL 20200108 Prevented direct access to record buffer members
** CA 20200110 Fixes for static buffer resource delete.
** 078 IAS 20200221 Reworked RAW-TRANSFER method support.
** 079 IAS 20200414 bufferValidate() method implementation.
** CA 20200427 Allow bulk assign support for BUFFER-VALUE and ::, in case of extent fields.
** CA 20200428 Added LAST-BATCH runtime stub.
** CA 20200503 Added BUFFER:IS-MULTI-TENANT runtime stub.
** 080 ECF 20200419 Removed Hibernate dependencies.
** CA 20200714 The rowState is available even for temp-tables not part of a dataset.
** 081 GES 20200807 Reduced ErrorManager context-local usage.
** CA 20200910 Added BUFFER:SERIALIZE-ROW() runtime stub.
** 20200911 attachDataSource allows empty include/excludeFields values.
** 20200914 attachDataSource allows ROWID function for source field.
** OM 20201016 Added implementation for LAST-BATCH, DATA-SOURCE-MODIFIED.
** OM 20201030 Invalid attribute API support for getters/setters.
** OM 20201202 Fixed persistence support for ACCEPT-CHANGES and REJECT-CHANGES methods.
** OM 20201205 Added AUTO-DELETE attribute implementation.
** OM 20201218 Fixed implementation for error and rejected attributes/hidden fields.
** OM 20210127 Replaced TableMapper.getLegacyName() triple map lookup with direct access to local
** dmoMeta.legacyTable. Fixed implementation of validate() method/statement.
** OM 20210223 Invalidate index-based cache in the event of low-level SQL operations.
** OM 20210309 Do not use DmoMeta as key in TableMapper because temp-tables might share the same
** DmoMeta instance.
** ECF 20210506 Use RecordBuffer.getDmoInfo() getter instead of direct field access to prevent
** NPE in proxy case.
** CA 20210509 Fixed 'defaultTypeMapping' implicit value for readXml - it must be the unknown value, not
** the "?" string.
** AIL 20210511 Ensure field string names are normalized before searching.
** OM 20210521 DELETE and CREATE return the proper logical value.
** CA 20210626 Do not delete the master shared temp-table until the last of its associated buffers (i.e.
** child shared temp-tables) is deleted.
** CA 20210629 Added dereference operator for indexed extent fields (rvalue and lvalue).
** OM 20210809 Completed implementation of SERIALIZE-ROW|WRITE-JSON}(JsonObject|JsonArray).
** ECF 20210820 Retrofit for query substitution parameter preprocessing fixes.
** OM 20210921 Added missing implementations for NAMESPACE-PREFIX and XML-NODE-NAME attribute.
** ECF 20210924 Minor refactoring.
** CA 20210929 Fixed ATTACH-DATA-SOURCE when rowid function is used in the pairs.
** OM 20211023 Fixed FILL for EXTENT fields.
** AL2 20211105 Character values provided by dereference are case insensitive.
** ECF 20211203 Fixed error checking in _find method.
** OM 20211206 Improve implementation of DATA-SOURCE-COMPLETE-MAP attribute. Fixed FILL assignments.
** OM 20211020 Added _DATASOURCE_ROWID property.
** CA 20211228 BUFFER:IS-MULTI-TENANT attribute must return false, even if is not implemented - unknown
** is not an acceptable returned value for this attribute.
** Replaced the UnimplementedFeature.missing from tenant-related APIs with a TODO comment.
** CA 20211204 Fixed BUFFER-FIELD behavior, they need to behave like real resources, with their lifetime
** linked with the parent BUFFER resource.
** OM 20220218 When saving DS changes, allow Session.PK to be resolved instead of dereferencing it.
** Javadoc update.
** OM 20220224 Small fixed of the error message for invalid WHERE clause.
** OM 20220422 Allow Xml/JsonImport to load records into buffer without exposing package private methods.
** OM 20220620 Added missing features in ATTACH-DATA-SOURCE method.
** OM 20220707 Shared meta-knowledge about fields and properties of a record was collected in
** DmoMeta to avoid duplicating their collection and storage.
** CA 20220707 Create the BUFFER-FIELD resources only when they are accessed.
** OM 20220801 Allow unqualified field names in attachDataSource().
** OM 20220810 Fixed check for changes in saveRowChangesImpl2() to include extent fields.
** OM 20221003 Added missing STREAM targets for JSON/XML serializations.
** CA 20220930 Refactored the callback invocation to be performed via a call-site and InvokeConfig, to
** allow caching of the resolved target.
** CA 20221006 Added JMX instrumentation for 'resourceDelete'. Refs #6814
** UNIQUE-ID and ADM-DATA are kept as Java types instead of BDT. Refs #6827
** BUFFER-FIELD's instantiating procedure must be the BUFFER's, and not when it gets lazily
** created.
** CA 20221010 Reduced 'toLowerCase' usage in 'attachDataSource'. Refs #6820
** TJD 20220504 Upgrade do Java 11 minor changes
** CA 20220426 Added PRIMARY attribute stubs.
** OM 20220526 Added KEYS and SYNCHRONIZE implementations.
** CA 20220531 If the BUFFER is in 'unknown mode' (due to an outer join not finding a record), do not
** raise 'no record is available' ERROR condition.
** CA 20220601 Added 'derefPoly', emitted when '::' is used as r-value in a dynamic extent assignment.
** OM 20220603 Fixed the order of validation for DATA-SOURCE-ROWID getter.
** OM 20220604 Improved dereferenciation support.
** OM 20220607 Added EXTENT support for SAVE-ROW-CHANGES.
** CA 20220613 Fixed RAW-TRANSFER via 'rawCopyFrom(Buffer)' - the data was being read twice from the
** stream. Also, the dest buffer creates a new record only if there isn't one already loaded.
** OM 20220617 Improve DATA-SOURCE-ROWID implementation.
** OM 20220620 Added missing features in ATTACH-DATA-SOURCE method and DATA-SOURCE-ROWID attribute.
** OM 20220609 Fixes for several 4GL attributes.
** CA 20220909 When a query gets deleted and is set at this buffer's 'lastQuery', remove the reference.
** CA 20220918 Always delete the REPLACE dynamic query at the FILL operation.
** CA 20221010 Reduced 'toLowerCase' usage in 'attachDataSource'. Refs #6820
** CA 20221121 For a dynamic TEMP-TABLE, if the NAME attribute is changed, then the default-buffer's NAME
** must be changed, too.
** SVL 20230108 Improved performance by replacing some "for-each" loops with indexed "for" loops.
** RAA 20230109 Changed inline statement(s) to prepared statement(s).
** CA 20230110 Do not create a 'callbacks' map until the application actually sets a callback.
** CA 20230110 UNIQUE-ID is generated only when is read by the application.
** Cache the helper for ProcedureManager, ObjectOps and others (as needed), to reduce
** context-local lookup.
** SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
** performance.
** SVL 20230113 Improved performance by replacing some "for-each" loops with indexed "for" loops.
** HC 20230116 Replaced some handle usages with the actual wrapped resources for
** performance.
** CA 20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
** HC 20230118 Eliminated some of the uses of String.toUpperCase and/or String.toLowerCase
** for performance.
** 082 CA 20230303 When deleting a temp-table, the delete can be vetoed if any buffer for the temp-table is
** associated with an OUTPUT dataset. Refactored to move the resource-specific code in a
** 'allowDelete' method.
** 083 CA 20230215 Improved other cases to reduce BDT usage from within FWD runtime.
** CA 20230303 CA/20190720 change in HandleOps.delete related to postponing a temp-table delete if is an
** OUTPUT argument is currently flawed, is it compares handle instances instead of the
** handle's resource. For this reason, this code was commented out.
** 084 OM 20230320 bufferFieldNative() use the original property name, not a random one from denormalized set.
** 085 IAS 20230405 Fixed FILL support for recursive DATA-RELATION
** Added support for the RESTART-ROW attribute.
** Added support for the SYNCHRONIZE method.
** 086 IAS 20230325 Added source validation to readJson().
** IAS 20230501 Added 'emergency exit' for WRITE-XML
** 087 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 088 RAA 20230525 Creating a new BaseDataType instance is now done using BaseDataTypeFactory.
** 089 CA 20230523 Performance improvement of the buffer fill operation - when possible (data type match),
** both source and destination buffers), use direct access to read and write the value.
** Extent fields are not supported yet.
** 090 CA 20230607 TRANSACTION-MODE AUTO must preserve the tx in the root appserver block - this is required
** because in modes other than State-free, requests can be executed on the same agent who had
** TRANSACTION-MODE AUTO, and this must see as active the tx initially set.
** 091 CA 20230701 In FILL, if the source buffer has no record, do nothing, and allow the record to use the
** default values.
** 092 AL2 20230704 Avoid .buffer() calls in attachDataSource, so precomupte the dmo meta for each buffer.
** 093 OM 20230712 Reworked the DATA-SOURCE-COMPLETE-MAP and the related methods and attributes.
** 094 CA 20230724 Further reduce context-local usage.
** 095 CA 20230815 Ensure a dynamic query created by the FWD runtime during a FILL operation gets deleted
** (this is the case when the data-source does not specify a query handle).
** 096 CA 20230817 A dynamic BUFFER part of a dataset can't be explicitly deleted.
** 097 EVL 20230817 Adding GET-ITERATION implementation stub.
** 098 CA 20230828 If 'parentDataSource' is invalid, allow attaching another datasource.
** 099 CA 20231114 ERROR conditions via ErrorManager API calls must be followed by a return statement.
** 100 AD 20231127 Optimized bufferFieldnative(int) to directly use DmoMeta to get name of property.
** 101 OM 20231031 Added error handling in MERGE-CHANGES and MERGE-ROW-CHANGES methods.
** 102 CA 20231129 Directly set the INSTANTIATING-PROCEDURE referent, from the parent BUFFER's, instead of
** passing it as a handle.
** CA 20231221 Lazily create fieldsByName map.
** 103 RAA 20231220 Rewrote extent value parsing in assign. Modified two loops to be index-type ones.
** 104 OM 20240110 Fixed management of implicit/explicit FILL queries.
** 105 OM 20240109 Before writing a JSON/XML all buffers of the temp-tables must be flushed. After the
** operation ends the used buffer is released.
** 106 CA 20240206 Error 11874 must not set ERROR-STATUS:ERROR flag.
** 107 HC 20240222 Enabled JMX on FWD Client.
** 108 AL2 20240307 Mark the query to retrieve the entire result set (useful for FILL operations).
** 109 CA 20240320 Avoid BDTs within internal FWD runtime. Replaced iterators with Java 'for', where it
** applies.
** 110 CA 20240323 Performance optimization to cache the FieldStructure for each legacy field, during FILL
** operations.
** CA 20240324 Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime.
** CA 20240331 Use a plain HashMap with strings for the FieldStructure cache, to avoid recomputing the
** hashCode.
** 111 OM 20240401 When attaching a data-source with explicit query, the latter is configured with the
** substitution buffers from this buffer's dataset.
** 112 CA 20240404 Errors 12360, 12780, 12781, 14275, 11935, 11906, 11909, 11912, 11913 do not set the
** ERROR-STATUS:ERROR flag.
** 113 CA 20240411 Flush is done for all buffers in 'copyChanges' so validation is no longer needed for
** 'getChanges'.
** 114 CA 20240410 SAVE-ROW-CHANGES must use only the data-source fields which are belong to the source
** buffer.
** 115 AL2 20240412 Enforce released record flushing, overruling the fact that is may be referenced by other
** buffers.
** 116 CA 20240424 The buffers on which the DATA-SOURCE is attached must be available to any FIND- queries.
** 117 CA 20240422 Allow the query to be reset to its initialization state, so the same instance can be used
** to execute child buffer FILL operations.
** 118 CA 20240525 Bulk delete can't be executed if TRACKING-CHANGES is on, FWD runtime needs to emulate an
** actual FOR EACH to delete all records.
** 119 SB 20240528 Fixed a regression where honorMultipleReferences was honored when releasing a dynamic
** buffer preventing, its flush. Refs #8760.
** 120 DDF 20240523 Added an additional parameter to displayError() call in dereference().
** DDF 20240524 Replaced displayError() with recordOrShowError() in dereference().
** DDF 20240527 Replaced recordOrShowError() with recordOrShowDatabaseError() where possible.
** DDF 20240428 Reverted changes and added only the ones tested for database behavior.
** 121 TJD 20240124 Java 17 - checkExtentRange and setQuery need to be public to enable access from generated
** code
** 122 SP 20240603 Fixed error handling when FILL fails with unique validation error in APPEND mode.
** 123 SP 20240606 Error 4065 must not set ERROR-STATUS:ERROR flag.
** 124 CA 20240627 Always register the buffer as dirty for the batch fill operation, as direct access
** bypasses the proxy call in RecordBuffer.
** 125 CA 20240723 ACCEPT-CHANGES must not prune the session cache, but update the before-fields for the
** cached records directly.
** 126 SP 20240805 Skip using RESOURCE_DEL TimeStat if JMX_DEBUG flag is not set.
** 127 CA 20240918 Removed default methods from interfaces and moved them as concrete implementations, as
** default methods cause the Java metaspace to spike one order of magnitude.
** 128 OM 20241001 Implemented tenant-related attributes.
** 129 SP 20241029 Added NPE failsafe for copyTempTable and deleteAll. Javadoc fixes.
** 130 CA 20241030 Cache the BUFFER-FIELD handles.
** 131 AI 20241107 Fixed a regression in H130 - 'bufferField' must validate the index.
** 132 ES 20241030 Changed default onPhrase in the doBlock in saveRowChangesImpl2 method.
** 133 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** 134 AS 20241203 Introduced areBDTsEqual, used to detect changes in saveRowChanges.
** 135 ICP 20250123 Used integer and logical constants to leverage caches instances.
** 136 ES 20250312 Added label methods.
** 137 ES 20250312 Added methods autoSynchronize and canRead methods.
** 138 CA 20250321 Added 'setCallback' overloads.
** 139 ES 20250508 Added runtime support for AUTO-SYNCHRONIZE buffer attribute.
** 140 ES 20250313 Moved NAMESPACEURI, NAMESPACEPREFIX, SERIALIZENAME, XMLNODENAME attributes
** to buffer instance.
*/
/*
** 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;
import java.io.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.Level;
import java.lang.reflect.*;
import com.goldencode.p2j.*;
import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.trigger.*;
import com.goldencode.p2j.persist.serial.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.BlockManager.TransactionType;
import com.goldencode.p2j.util.ErrorManager.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.p2j.util.UniqueIdGenerator.*;
import com.goldencode.p2j.persist.TempTable.BeforeType;
import com.goldencode.util.*;
import org.apache.commons.lang3.tuple.*;
import static com.goldencode.p2j.util.BlockManager.*;
/**
* This class implements services representing Progress builtin functions,
* methods, attributes, and language statements, which operate on record
* buffers.
* <p>
* Converted business logic invokes these methods on DMO proxies via a
* DMO-specific interface in the form <code><DMO interface>.Buf</code>.
* Each such interface extends both a custom interface of DMO-specific getter
* and setter methods, as well as the {@link Buffer} interface, which defines
* the API for these standard services.
* <p>
* Dynamic proxy classes for each DMO type extend this class, so that business
* logic can invoke these standard services directly as method calls on DMO
* proxy instances.
* <p>
* This class is intentionally package private, since it is not meant to be
* extended outside of this package (and within this package only by dynamic
* proxy class), nor instantiated directly.
* <p>
* This class implements {@link BufferReference} in order to grant its
* instance methods access to the {@link RecordBuffer} object associated with
* each instance of the DMO proxy class which extends this class. This access
* is necessary for these methods to do their work. Even though {@link
* #buffer()} is a public method, external packages will not be able to invoke
* this method (except through reflection). This is by design, as business
* logic is not meant to have access to <code>RecordBuffer</code> instances.
* The default implementation of this method here throws an exception, as it
* is never intended to be called, but it needs to be present in order for
* this class to compile, since it is referenced by other methods. Each DMO
* proxy class overrides this method to provide a working implementation.
*/
public abstract class BufferImpl
extends HandleChain
implements Buffer
{
/** Instrumentation for {@link #resourceDelete()}. */
private static final NanoTimer RESOURCE_DEL = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmBufferDelete);
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(BufferImpl.class.getName());
/** The call-site for the {@link Buffer#BEFORE_FILL_EVENT} event callback. */
private static final InvokeConfig BEFORE_FILL_CALL_SITE = new InvokeConfig();
/** The call-site for the {@link Buffer#AFTER_FILL_EVENT} event callback. */
private static final InvokeConfig AFTER_FILL_CALL_SITE = new InvokeConfig();
/** The call-site for the {@link Buffer#BEFORE_ROW_FILL_EVENT} event callback. */
private static final InvokeConfig BEFORE_ROW_FILL_CALL_SITE = new InvokeConfig();
/** The call-site for the {@link Buffer#AFTER_ROW_FILL_EVENT} event callback. */
private static final InvokeConfig AFTER_ROW_FILL_CALL_SITE = new InvokeConfig();
/** The call-site for the {@link Buffer#ROW_CREATE_EVENT} event callback. */
private static final InvokeConfig ROW_CREATE_CALL_SITE = new InvokeConfig();
/** The call-site for the {@link Buffer#ROW_DELETE_EVENT} event callback. */
private static final InvokeConfig ROW_DELETE_CALL_SITE = new InvokeConfig();
/** The call-site for the {@link Buffer#ROW_UPDATE_EVENT} event callback. */
private static final InvokeConfig ROW_UPDATE_CALL_SITE = new InvokeConfig();
/** The call-site for the {@link Buffer#FIND_FAILED_EVENT} event callback. */
private static final InvokeConfig FIND_FAILED_CALL_SITE = new InvokeConfig();
/** The call-site for the {@link Buffer#SYNCHRONIZE_EVENT} event callback. */
private static final InvokeConfig SYNCHRONIZE_CALL_SITE = new InvokeConfig();
/**
* Special 'pointer' used in management of BUFFER resources. This helps delimit the TEMP-tables
* from PERMANENT ones in the list of BUFFERs. The TEMP-TABLE buffers are kept always in front
* of the list.
* To be sure there are no conflicts with legacy resource types declared in
* {@link LegacyResource}, this type starts with an underscore.
*/
private static final String PERMANENT_TABLE = "_PERMANENT-TABLE";
/** Corresponds to ADM-DATA attribute. */
private String admData = null;
/** Corresponds to UNIQUE-ID attribute. */
private Long uniqueID = null;
/** Flag indicating this buffer was deleted. */
private boolean deleted;
/** Flag indicating that the deletion of this buffer is in progress. */
private boolean deleting;
/** The last query this buffer was added/set to. */
private QueryWrapper lastQuery = null;
/** The current dataset. May be {@code null}. */
private DataSet dataSet = null;
/** The attached {@code DataSource}, if any. */
private DataSource dataSource = null;
/** The current value of BATCH-SIZE attribute. Valid only when {@code dataSet} is valid. */
private int batchSize = 0;
/** Current value of the {@code DATA-SOURCE-MODIFIED} attribute. */
private boolean dataSourceModified = false;
/** The current value of LAST-BATCH attribute. Valid only when {@code dataSet} is valid. */
private boolean lastBatch = false;
/**
* The current value of the {@code FILL-MODE} attribute. The default value is {@code MERGE}. For buffers
* not part of a dataset, {@link #fillMode()} will return the unknown value.
*/
private String fillMode = FM_MERGE;
/** The current value of AUTO-DELETE attribute. */
private boolean autoDelete = true;
/**
* Flag used for bracketing internal management of BEFORE-BUFFER. ABL programmer's direct
* actions (CREATE/DELETE/ALTER buffer) are outside this bracket and will lead to errors.
*/
private boolean setUpBeforeBuffer = false;
/** Is this an AFTER-BUFFER or BEFORE-BUFFER? Cached after value is computed. */
private BeforeType beforeBufferType = BeforeType.UNKNOWN;
/** The current value of the {@code ATTACHED-PAIRLIST} attribute. */
private String attachedPairlist = null;
/** The current value of the {@code DATA-SOURCE-COMPLETE-MAP} attribute. */
private String dataSrcMappingStr = null;
/** The mapping of fields in data source. */
private List<Pair<String, DataSource.FieldReference>> dataSrcMapping = null;
/**
* The property meta for the destination buffer. The key is the legacy name. This is set when the
* data-source is assigned.
*/
private Map<String, PropertyMeta> dstFieldMeta = null;
/** A map with all known callbacks. The keys MUST be in UPPERCASE. */
private Map<String, CallbackData> callbacks = null;
/** This is not null, when a Buffer proxy overrides the name and a backup is required */
private String oldName;
/** The list of buffer fields. */
private BufferFieldImpl[] fields = null;
/** Cache of handle references to return. The assumption is they are read-only. */
private handle[] fieldHandles = null;
/** A mapping of fields by their property name. */
private HashMap<String, BufferFieldImpl> fieldsByName = null;
/**
* The cached peer buffer. For AFTER-BUFFER it is the BEFORE-BUFFER and vice-versa. The SIMPLE
* buffers do not have a peer buffer.
*/
protected BufferImpl peerBuffer = null;
/** The parent {@code DataSource}, if any. This is differnet from the attached datasource! */
private DataSource parentDataSource = null;
/** The list of keys, if the buffer is a component of a {@code DataSource}. */
private List<String> keys = null;
/** The NAMESPACE-URI of the XML node for serialized data of this {@code TEMP-TABLE}. */
private String namespaceURI = null;
/** The NAMESPACE-PREFIX of the XML node for serialized data of this {@code TEMP-TABLE}. */
private String namespacePrefix = null;
/** The current value of the {@code SERIALIZE-NAME} attribute. */
private String serializeName = null;
/** The name of the XML node for serialized data of this {@code TEMP-TABLE}. */
private String xmlNodeName = null;
/**
* The buffer's current FILL query, which is reset on each sub-fill executed for this buffer. Will be
* closed only when the complete FILL operation completes.
*/
private QueryWrapper reusableQuery;
/** The current value of AUTO-SYNCHRONIZE attribute. Valid only when {@code dataSet} is valid. */
private boolean autoSynchronize = false;
/** The mode for finding (first, last, unique) records in a dynamic call to a buffer handle. */
enum FindMode
{
FIRST, /* apply first() to a query */
LAST, /* apply last() to a query */
UNIQUE /* apply unique() to a query */
}
/**
* The default constructor. Calls the super with appropriate parameter values.
*/
protected BufferImpl()
{
super(false, false);
}
/**
* Worker static method for wrapping ErrorConditionException and reusing
* in all P2JQuery.findFirst() implementations
*
* @param findQuery
* instance of FindQuery
*
* @return <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
*/
private static logical wrapFirst(FindQuery findQuery, LockType lockType)
{
boolean success = true;
try
{
findQuery.first(lockType);
}
catch (ErrorConditionException exc)
{
success = false;
}
return logical.of(success);
}
/**
* Worker static method for wrapping ErrorConditionException and reusing
* in all P2JQuery.findLast() implementations
*
* @param findQuery
* instance of FindQuery
*
* @return <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
*/
private static logical wrapLast(FindQuery findQuery, LockType lockType)
{
boolean success = true;
try
{
findQuery.last(lockType);
}
catch (ErrorConditionException exc)
{
success = false;
}
return logical.of(success);
}
/**
* Worker static method for wrapping ErrorConditionException and reusing
* in all P2JQuery.findUnique() implementations
*
* @param findQuery
* instance of FindQuery
*
* @return <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
*/
private static logical wrapUnique(FindQuery findQuery, LockType lockType)
{
boolean success = true;
try
{
findQuery.unique(lockType);
}
catch (ErrorConditionException exc)
{
success = false;
}
return logical.of(success);
}
/**
* Checks if transaction is active otherwise records or shows error message.
*
* @param buffer
* Record buffer.
*
* @return <code>true</code> if a transaction is active else <code>false</code>.
*/
private static boolean verifyTransaction(RecordBuffer buffer)
{
if (buffer.getBufferManager().isTransaction())
{
return true;
}
else
{
String msg = "Unable to CREATE/DELETE unless a TRANSACTION is running";
ErrorManager.recordOrShowError(7345, msg, false);
return false;
}
}
/**
* Get the {@link RecordBuffer} instance associated with this object.
* <p>
* This implementation exists only to satisfy the Java compiler. This
* method is only meaningful when a proxy DMO class is created by {@link
* RecordBuffer}, which will implement this method to return an instance
* of <code>RecordBuffer</code> associated with the proxy DMO instance.
*
* @return Record buffer associated with this object.
*/
public abstract RecordBuffer buffer();
/**
* Resolve the actual resource used by the runtime.
*
* @return The instance.
*/
public BufferImpl ref()
{
return this;
}
/**
* Get the PRIMARY attribute.
*
* @return The primary index.
*/
@Override
public character primaryIndex()
{
P2JIndex primaryIndex = buffer().getDmoInfo().getPrimaryIndex(true);
if (primaryIndex == null)
{
return new character("default");
}
return new character(primaryIndex.getLegacyName());
}
/**
* Set the PRIMARY attribute.
*
* @param primary
* The primary index.
*/
@Override
public void primaryIndex(String primary)
{
if (primary == null)
{
ErrorManager.recordOrShowError(4083, "PRIMARY", LegacyResource.BUFFER + " widget");
// **Unable to assign UNKNOWN value to attribute <attribute> on <widget id>. (4083)
return;
}
ErrorManager.recordOrShowError(4052, "PRIMARY", "setable", LegacyResource.BUFFER + " widget");
// **<attribute> is not a <settable/queryable> attribute for <widget id>. (4052)
}
/**
* Set the PRIMARY attribute.
*
* @param primary
* The primary index.
*/
@Override
public void primaryIndex(Text primary)
{
primaryIndex(primary.toJavaType());
}
/**
* Get the BUFFER:LAST-BATCH attribute.
*
* @return the value of BUFFER:LAST-BATCH attribute.
*/
@Override
public logical lastBatch()
{
return (dataSet == null) ? logical.UNKNOWN : logical.of(lastBatch && batchSize > 0);
}
/**
* Set the BUFFER:LAST-BATCH attribute.
*
* @param lastBatch
* The flag state.
*/
@Override
public void lastBatch(boolean lastBatch)
{
if (dataSet == null)
{
ErrorManager.recordOrShowError(12360, false);
return;
}
// all checked, can set value now:
this.lastBatch = lastBatch;
}
/**
* Set the BUFFER:LAST-BATCH attribute.
*
* @param lastBatch
* The flag state.
*/
@Override
public void lastBatch(logical lastBatch)
{
if (lastBatch == null || lastBatch.isUnknown())
{
ErrorManager.recordOrShowError(4083, "LAST-BATCH", "BUFFER widget");
return;
}
lastBatch(lastBatch.booleanValue());
}
/**
* Get the BUFFER:AUTO-DELETE attribute.
*
* @return the value of BUFFER:AUTO-DELETE attribute.
*/
@Override
public logical autoDelete()
{
if (!_dynamic() || dataSet == null)
{
return logical.UNKNOWN;
}
return logical.of(this.autoDelete);
}
/**
* Set the BUFFER:AUTO-DELETE attribute.
*
* @param autoDelete
* The flag state.
*/
@Override
public void autoDelete(boolean autoDelete)
{
this.autoDelete = autoDelete;
}
/**
* Set the BUFFER:AUTO-DELETE attribute.
*
* @param autoDelete
* The flag state.
*/
@Override
public void autoDelete(logical autoDelete)
{
if (autoDelete == null || autoDelete.isUnknown())
{
ErrorManager.recordOrShowError(4083, "AUTO-DELETE", LegacyResource.BUFFER + " widget");
// **Unable to assign UNKNOWN value to attribute <attribute> on <widget id>.
return;
}
autoDelete(autoDelete.booleanValue());
}
/**
* Checks whether the data in the data source has been modified.
*
* @return {@code true} when the data has been modified.
*/
@Override
public logical isDataSourceModified()
{
return logical.of(dataSourceModified);
}
/**
* Manually mark the data in the target structure as modified.
*
* @param mod
* {@code true} when the data is marked as modified and {@code false} otherwise.
*/
@Override
public void setDataSourceModified(logical mod)
{
if (mod == null || mod.isUnknown())
{
unableToAssignUnknown("DATA-SOURCE-MODIFIED", "DATASET widget");
return;
}
setDataSourceModified(mod.getValue());
}
/**
* Manually mark the data in the target structure as modified.
*
* @param mod
* {@code true} when the data is marked as modified and {@code false} otherwise.
*/
@Override
public void setDataSourceModified(boolean mod)
{
this.dataSourceModified = mod;
}
/**
* Reports if this object is valid for use.
*
* @return the state of the {@link #deleted} flag. If a record buffer has been defined, it
* is valid for use until is deleted.
*/
public boolean valid()
{
return !deleted;
}
/**
* Check whether the delete operation for this buffer is in progress.
*
* @return {@code true} when the delete operation for this buffer is under way.
*/
public boolean isDeleting()
{
return deleting;
}
/**
* Obtain the handle to the buffer that was created by default for the temporary table backing
* this buffer. Note that this implementation presumes that this is a temp-table buffer. It
* should not be invoked on a permanent table buffer.
*
* @return Default buffer for the temporary table backing this buffer, or an unknown handle if
* that resource is not valid.
*/
public handle defaultBufferHandle()
{
TempTable tempTable = buffer().getParentTable();
if (tempTable == null || !tempTable.valid())
{
return new handle();
}
return tempTable.defaultBufferHandle();
}
/**
* Obtain the handle to the buffer that was created by default for the temporary table backing
* this buffer. Note that this implementation presumes that this is a temp-table buffer. It
* should not be invoked on a permanent table buffer.
*
* @return Default buffer for the temporary table backing this buffer, or an unknown handle if
* that resource is not valid.
*/
@Override
public Buffer defaultBufferHandleNative()
{
TempTable tempTable = buffer().getParentTable();
if (tempTable == null || !tempTable.valid())
{
return null;
}
return tempTable.defaultBufferHandleNative();
}
/**
* Get the record ID of the current buffer's current record, if any. P2J
* implementation of the RECID function.
* <p>
* This method will return a 32-bit recid which represents the internal
* row ID of the given record. If the actual row ID (which is implemented
* as a wider integer) cannot fit due to overflow, an error condition will
* be raised.
*
* @return Record ID of the underlying data record. If no record currently
* backs the buffer associated with <code>dmo</code>, a recid
* initialized as unknown value is returned.
*
* @throws ErrorConditionException
* if the actual record ID is too wide to fit within a 32-bit
* integer.
*/
public recid recordID()
{
return buffer().recordID();
}
/**
* Get the row ID of the current buffer's current record, if any. P2J
* implementation of the ROWID function.
*
* @return Row ID of the underlying data record. If no record currently
* backs the buffer associated with <code>dmo</code>, a
* <code>rowid</code> initialized as unknown value is returned.
*/
public rowid rowID()
{
return buffer().rowID();
}
/**
* Get the row ID of the current buffer's current record, if any.
*
* @return Row ID of the underlying data record. If no record currently backs the buffer associated with
* <code>dmo</code>, a <code>null</code> value is returned.
*/
public Long _rowID()
{
return buffer()._rowID();
}
/**
* Indicate whether a backing data record currently is available.
*
* @return <code>true</code> if data is available, <code>false</code> if
* no record backs this buffer.
*/
public logical available()
{
return (logical.of(_available()));
}
/**
* Indicate whether a backing data record currently is available.
*
* @return <code>true</code> if data is available, <code>false</code> if
* no record backs this buffer.
*/
public boolean _available()
{
return buffer().isAvailable();
}
/**
* Checks if the record in the buffer has changed in database since the
* last <code>FIND CURRENT</code> or <code>GET CURRENT</code>.
* P2J implementation of 4GL function <code>CURRENT-CHANGED</code>.
*
* @return <code>true</code> if the database record differs from the
* value in memory buffer that was queried with the last
* <code>FIND</code> and <code>false</code> otherwise
*/
@LegacyAttribute(name = "CURRENT-CHANGED")
public logical currentChanged()
{
return logical.of(_currentChanged());
}
/**
* Checks if the record in the buffer has changed in database since the
* last <code>FIND CURRENT</code> or <code>GET CURRENT</code>.
* P2J implementation of 4GL function <code>CURRENT-CHANGED</code>.
*
* @return <code>true</code> if the database record differs from the
* value in memory buffer that was queried with the last
* <code>FIND</code> and <code>false</code> otherwise
*/
public boolean _currentChanged()
{
return buffer().isRecordChanged();
}
/**
* Indicate whether the buffer associated currently contains a record, and
* whether that record is transient. A record is transient if it has been
* newly created and has not yet been saved to the database.
*
* @return <code>true</code> if a record exists and is new, else
* <code>false</code>.
*/
public logical newlyCreated()
{
return (logical.of(_newlyCreated()));
}
/**
* Indicate whether the buffer associated currently contains a record, and
* whether that record is transient. A record is transient if it has been
* newly created and has not yet been saved to the database.
*
* @return <code>true</code> if a record exists and is new, else
* <code>false</code>.
*/
public boolean _newlyCreated()
{
return buffer().isNewlyCreated();
}
/**
* Indicate whether the last record retrieval attempt was denied because
* the target record was locked.
*
* @return <code>true</code> if the record was locked; <code>false</code>
* if the attempt succeeded, or if this query has not yet
* attempted to retrieve a record.
*/
public logical locked()
{
return (logical.of(_locked()));
}
/**
* Indicate whether the last record retrieval attempt was denied because
* the target record was locked.
*
* @return <code>true</code> if the record was locked; <code>false</code>
* if the attempt succeeded, or if this query has not yet
* attempted to retrieve a record.
*/
public boolean _locked()
{
return buffer().wasLocked();
}
/**
* Indicate whether the last record retrieval attempt failed because the
* query criteria was ambiguous.
*
* @return <code>true</code> if the query was ambiguous;
* <code>false</code> if the attempt succeeded, or if no attempt
* has yet been made.
*/
public logical ambiguous()
{
return (logical.of(_ambiguous()));
}
/**
* Indicate whether the last record retrieval attempt failed because the
* query criteria was ambiguous.
*
* @return <code>true</code> if the query was ambiguous;
* <code>false</code> if the attempt succeeded, or if no attempt
* has yet been made.
*/
public boolean _ambiguous()
{
return buffer().wasAmbiguous();
}
/**
* Getter for the IS-MULTI-TENANT attribute.
*
* @return See above.
*/
@Override
public logical isMultiTenant()
{
return logical.of(buffer().getDmoInfo().multiTenant);
}
/**
* Setter for the IS-MULTI-TENANT attribute.
*
* @param l
* The flag value.
*/
@Override
public void setMultiTenant(logical l)
{
ErrorManager.recordOrShowError(4052, "IS-MULTI-TENANT", "setable" /*sic!*/, LegacyResource.BUFFER);
}
/**
* Setter for the IS-MULTI-TENANT attribute.
*
* @param l
* The flag value.
*/
@Override
public void setMultiTenant(boolean l)
{
ErrorManager.recordOrShowError(4052, "IS-MULTI-TENANT", "setable" /*sic!*/, LegacyResource.BUFFER);
}
/**
* Perform a RAW-TRANSFER operation from this buffer to the given raw variable. Serializes the
* record currently in the buffer into the specified field.
*
* @param r
* A <code>raw</code> field to which to copy the record in this buffer.
*/
public void rawCopyTo(raw r)
{
// TODO: this is just a basic implementation, it needs improvements to handle boundary
// conditions, add proper error handling is missing and the serialization is
// not binary compatible with the 4GL version
if (r == null)
{
return;
}
boolean error = false;
// obtain the signature for the buffer
ArrayList<TableMapper.LegacyFieldInfo> fields = TableMapper.getLegacyOrderedList(this, true);
TableSignature sig = new TableSignature(fields);
// collect all serialized output in an in-memory byte[]
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = null;
try
{
// collect all serialized output in an in-memory byte[]
oos = new ObjectOutputStream(baos);
// serialize the signature
sig.writeExternal(oos);
RecordBuffer buffer = buffer();
BufferReference proxy = buffer.getDMOProxy();
Map<String, Method> getters = buffer.getDmoInfo().getLegacyGetterMap();
Map<String, Integer> extents = buffer.getDmoInfo().getExtentMap();
// iterate through the field descriptors (in POSITION sequence)
for (int j = 0; j < fields.size(); j++)
{
TableMapper.LegacyFieldInfo lfi = fields.get(j);
String prop = lfi.getJavaName();
// extent or not?
Integer extent = extents.get(prop);
// obtain the getter methods for the current property
Method getter = getters.get(prop);
Externalizable result;
// read the value
if (extent == null)
{
// scalar case
result = (Externalizable) getter.invoke(proxy);
// serialize
result.writeExternal(oos);
}
else
{
// array case
for (int i = 0; i < extent; i++)
{
result = (Externalizable) getter.invoke(proxy, i);
// serialize
result.writeExternal(oos);
}
}
}
}
catch (IOException | SecurityException | IllegalAccessException | InvocationTargetException ex)
{
error = true;
ErrorManager.recordOrThrowError(new NumberedException("RAW-TRANSFER failed!", -1, ex));
}
finally
{
// the baos does not need to be closed and can be accessed even after you call close()
if (oos != null)
{
try
{
// this one should be closed
oos.close();
}
catch (IOException ex)
{
// don't care
}
}
}
// save the result in the raw variable
if (!error)
{
r.assign(baos.toByteArray());
}
}
/**
* Perform a RAW-TRANSFER operation from this buffer to the given field, which should be of
* type <code>raw</code>. Serializes the record currently in the buffer into the specified
* field.
*
* @param field
* A <code>raw</code> field to which to copy the record in this buffer.
*/
public void rawCopyTo(FieldReference field)
{
if (field == null)
{
return;
}
raw r = new raw();
rawCopyTo(r);
field.set(r);
}
/**
* Perform a RAW-TRANSFER operation from this buffer to the given buffer.
*
* @param buffer
* A buffer into which to copy the record in this buffer.
* @return <code>true</code> if successful
*/
public boolean rawCopyTo(Buffer buffer)
{
if (buffer == null)
{
return false;
}
raw r = new raw();
rawCopyTo(r);
return buffer.rawCopyFrom(r);
}
/**
* Perform a RAW-TRANSFER operation from the given raw field into this buffer. Deserializes the
* data in the specified field into this buffer.
*
* @param field
* A <code>raw</code> field from which to copy the record into this buffer.
*/
public void rawCopyFrom(FieldReference field)
{
if (field == null)
{
return;
}
rawCopyFrom((raw) field.get());
}
/**
* Perform a RAW-TRANSFER operation from the given raw field into this buffer. Deserializes the
* data in the specified field into this buffer.
*
* @param field
* A <code>raw</code> field from which to copy the record into this buffer.
* @return <code>true</code> if successful
*/
public boolean rawCopyFrom(raw field)
{
// TODO: this is just a basic implementation, it needs improvements to handle boundary
// conditions, add proper error handling is missing and the deserialization is
// not binary compatible with the 4GL version
if (field == null || field.isUnknown())
{
ErrorManager.recordOrShowError(
4952,
"Couldn't extract raw value from source field of RAW-TRANSFER",
false, false, true
);
return true;
}
byte[] data = field.getByteArray();
if (data == null || data.length == 0)
{
ErrorManager.recordOrShowError(
4952,
"Couldn't extract raw value from source field of RAW-TRANSFER",
false, false, true
);
return true;
}
// this should create a new record (causing any existing record release and firing triggers as needed
if (!_available())
{
create();
}
// obtain the signature for the buffer
ArrayList<TableMapper.LegacyFieldInfo> fields = TableMapper.getLegacyOrderedList(this, true);
TableSignature tarSig = new TableSignature(fields);
// collect all serialized output in an in-memory byte[]
ByteArrayInputStream bais = new ByteArrayInputStream(data);
try (ObjectInputStream ois = new ObjectInputStream(bais))
{
// read all serialized input from an in-memory byte[]
TableSignature srcSig = new TableSignature();
// deserialize the signature
srcSig.readExternal(ois);
if (!srcSig.equals(tarSig))
{
ErrorManager.recordOrShowError(
4955,
"Table signatures do not match in RAW-TRANSFER operation",
false, false, true
);
return true;
}
RecordBuffer buffer = buffer();
Map<String, Integer> extents = buffer.getDmoInfo().getExtentMap();
Map<String, BaseDataType[]> values = readValues(fields, extents, ois);
if (values == null)
{
return true;
}
BufferReference proxy = buffer.getDMOProxy();
Map<String, Method> setters = buffer.getDmoInfo().getLegacySetterMap();
boolean batchError = true;
RecordBuffer.startBatch(buffer.bufferManager, true);
try
{
// iterate through the field descriptors (in POSITION sequence)
for (int j = 0; j < fields.size(); j++)
{
TableMapper.LegacyFieldInfo lfi = fields.get(j);
String prop = lfi.getJavaName();
BaseDataType[] pvals = values.get(prop);
// extent or not?
Integer extent = extents.get(prop);
// obtain the setter methods for the current property
Method setter = setters.get(prop);
BaseDataType value = null;
// process the value
if (extent == null)
{
// call the default constructor
value = pvals[0];
// scalar case
setter.invoke(proxy, value);
}
else
{
// array case
for (int i = 0; i < extent; i++)
{
// call the default constructor
value = pvals[i];
// scalar case
setter.invoke(proxy, i, value);
}
}
}
batchError = false;
}
finally
{
RecordBuffer.endBatch(buffer.bufferManager, batchError);
}
return true;
}
catch (StreamCorruptedException sce)
{
ErrorManager.recordOrShowError(
4952,
"Couldn't extract raw value from source field of RAW-TRANSFER",
false, false, true
);
return true;
}
catch (IOException |
ReflectiveOperationException |
SecurityException |
IllegalArgumentException e)
{
// TODO: if something happened during the copy but after the create, do we need to put
// the original record back in?
ErrorManager.recordOrThrowError(new NumberedException("RAW-TRANSFER failed!", -1, e));
}
return false;
}
/**
* RAW-TRANSFER() method implementation that maps its processing to the same methods used
* for implementation of the RAW-TRANSFER statement. This will serialize data from this
* buffer into a RAW field or buffer OR it will deserialize data from a RAW field or buffer
* into this buffer.
*
* @param toMode
* {@code true} if the copy direction is into the handle's resource and {@code false}
* if the data is being copied into the buffer.
* @param h
* A buffer or raw buffer field.
*
* @return <code>true</code> if RAW-TRANSFER() succeeds, else <code>false</code>.
*/
@LegacyMethod(name = "RAW-TRANSFER")
public logical rawTransfer(boolean toMode, handle h)
{
if (h == null || h.isUnknown())
{
ErrorManager.recordOrShowError(
4065,
"**The RAW-TRANSFER attribute on the BUFFER widget has invalid arguments",
false, false, false
);
return logical.FALSE;
}
WrappedResource wr = (WrappedResource) h.get();
if (wr instanceof Buffer)
{
Buffer buf = (Buffer) wr;
if (toMode)
{
if (!available().booleanValue())
{
ErrorManager.recordOrShowError(
4951,
"Source element of a RAW-TRANSFER statement has no record",
false, false, true
);
return logical.FALSE;
}
return logical.of(rawCopyTo(buf));
}
else
{
if (!buf.available().booleanValue())
{
ErrorManager.recordOrShowError(
4951,
"Source element of a RAW-TRANSFER statement has no record",
false, false, true
);
return logical.FALSE;
}
return logical.of(buf.rawCopyTo(this));
}
}
else if (wr instanceof BufferField)
{
BufferField fld = (BufferField) wr;
String ftype = ((CommonField) h.getResource()).getDataType().getValue();
if (!"raw".equals(ftype))
{
String fn = h.unwrap().name().getValue();
ErrorManager.recordOrShowDatabaseError(
4824,
String.format("Field '%s' referenced in RAW-TRANSFER must be of type RAW", fn),
false, false
);
return logical.FALSE;
}
if (toMode)
{
if (!available().booleanValue())
{
ErrorManager.recordOrShowError(
4951,
"Source element of a RAW-TRANSFER statement has no record",
false, false, true
);
return logical.FALSE;
}
raw r = new raw();
rawCopyTo(r);
fld.changeValue(r);
}
else
{
if (!h.unwrapBufferField().available().booleanValue())
{
ErrorManager.recordOrShowError(
4951,
"Source element of a RAW-TRANSFER statement has no record",
false, false, true
);
return logical.FALSE;
}
BaseDataType bdt = fld.value();
// if (!(bdt instanceof raw))
// {
// // TODO: error?
// return logical.FALSE;
// }
return logical.of(rawCopyFrom((raw) bdt));
}
}
else
{
// untested at this time for 'database behavior' related to throwing structured exceptions
// - see recordOrShowDatabaseError
ErrorManager.recordOrShowError(
4067,
"The second argument for RAW-TRANSFER on the BUFFER widget should" +
" not be of type WIDGET-HANDLE",
false, true, false
);
return logical.FALSE;
}
// TODO: we probably need to know if there was a failure in the backing worker methods
// but the current signatures don't return a value
return logical.TRUE;
}
/**
* RAW-TRANSFER() method implementation that maps its processing to the same methods used
* for implementation of the RAW-TRANSFER statement. This will serialize data from this
* buffer into a RAW field or buffer OR it will deserialize data from a RAW field or buffer
* into this buffer.
*
* @param toMode
* {@code true} if the copy direction is into the handle's resource and {@code false}
* if the data is being copied into the buffer.
* @param h
* A buffer or raw buffer field.
*
* @return <code>true</code> if RAW-TRANSFER() succeeds, else <code>false</code>.
*/
@LegacyMethod(name = "RAW-TRANSFER")
public logical rawTransfer(logical toMode, handle h)
{
if (toMode == null || toMode.isUnknown())
{
// TODO: check this behavior
return logical.FALSE;
}
return rawTransfer(toMode.booleanValue(), h);
}
/**
* Create a new record in the buffer.
*
* @throws ErrorConditionException
* if there is an error retrieving the next available primary key
* from the database or locking the record.
*/
public void create()
{
buffer().create();
}
/**
* Delete the record currently in the buffer, if any.
*
* @return {@code true} if the delete was possible.
*
* @throws ErrorConditionException
* if no record currently is loaded in the buffer, or if there is
* an error deleting the current record from the database.
*/
public logical deleteRecord()
throws ErrorConditionException
{
if (isBeforeBuffer() && !setUpBeforeBuffer)
{
// the BEFORE-BUFFERS can only be queried, not altered in any way (REMOVE/CHANGE)
ErrorManager.recordOrThrowError(12374, doGetName(), "");
// You may not delete BEFORE-TABLE <name> record. Use ACCEPT-ROW-CHANGES instead.
return logical.FALSE;
}
// no validation is taken in consideration
return deleteRecord(() -> logical.TRUE, () -> new character(""));
}
/**
* Delete the record currently in the buffer, IF and ONLY IF the given validation expression
* evaluates <code>true</code>.
*
* @param valexp
* The validation expression to evaluate.
* @param valmsg
* The error message to display when validation fails.
*
* @return <code>true</code> if validation succeeds, <code>false</code> if it fails and no
* exception is raised.
*
* @throws ErrorConditionException
* If no record currently is loaded in the buffer, or if there is an error deleting
* the current record from the database or if the validation expression fails.
*/
public logical deleteRecord(Supplier<logical> valexp, String valmsg)
throws ErrorConditionException
{
return deleteRecord(valexp, () -> new character(valmsg));
}
/**
* Delete the record currently in the buffer, IF and ONLY IF the given validation expression
* evaluates <code>true</code>.
*
* @param valexp
* The validation expression to evaluate.
* @param valmsg
* The error message to display when validation fails.
*
* @return <code>true</code> if validation succeeds, <code>false</code> if it fails and no
* exception is raised.
*
* @throws ErrorConditionException
* If no record currently is loaded in the buffer, or if there is an error deleting
* the current record from the database or if the validation expression fails.
*/
public logical deleteRecord(Supplier<logical> valexp, Supplier<character> valmsg)
throws ErrorConditionException
{
RecordBuffer buffer = buffer();
if (!buffer.isTemporary())
{
if (!verifyTransaction(buffer))
{
return logical.FALSE;
}
// the validation is done at compile type, thus dynamic buffers with validation
// can't be deleted - also no delete trigger is fired
if (buffer.isDynamic())
{
Class<? extends DataModelObject> iface = buffer.getDMOInterface();
if (TableMapper.hasValidation(iface))
{
String msg = "Table " + buffer.getDmoInfo().legacyTable + " is defined with schema delete " +
"validation and may only be deleted by the DELETE statement";
ErrorManager.recordOrShowError(7347, msg, false, false);
return logical.FALSE;
}
}
}
if (dataSet != null) // implies buffer.isTemporary()
{
// checks whether tracking is active
if (buffer.getParentTable()._isTrackingChanges())
{
if (!invokeCallback(ROW_DELETE_EVENT))
{
returnError(); // veto on record delete
}
}
}
try
{
return logical.of(buffer.delete(valexp, valmsg));
}
catch (PersistenceException e)
{
return logical.FALSE;
}
}
/**
* Delete all records associated with this buffer, which match the criteria specified by the
* given where clause. Note that this where clause must not contain DMO alias qualifiers.
* <p>
* <b>NOTE:</b> the current implementation must not be invoked for a DMO which is backed by a
* permanent table; only temp-table bulk delete is supported at this time.
* <p>
* When this method returns:
* <ul>
* <li><strong>temp-table</strong>buffers became empty (Ie. {@code available} returns
* {@code false}). This is because these buffers are used in a loop and the last
* record is also deleted. P2J will detect these loops and convert it to the much
* faster bulk delete. P4GL does not allow bulk delete using SQL
* {@code DELETE FROM ... WHERE} syntax.
* <li>P2J does not support yet bulk deletion of <strong>permanent table</strong> buffers.
* In P4GL they will be empty if the predicate matches the currently stored record and
* remains unaffected otherwise.
* </ul>
*
* @param suppDMOs
* The DMOs for the external (additional) buffers that are accessed in inner
* subselect, or {@code null} in case of a simple {@code where} predicate.
* @param where
* An HQL where clause snippet which defines the restriction criteria to apply to the
* delete. All references to properties in a DMO must be unqualified.
* @param args
* Query substitution parameters required by the where clause.
*
* @throws ErrorConditionException
* if there is an error deleting the records.
* @throws UnsupportedOperationException
* if invoked on a buffer associated with a permanent table.
*/
public void delete(DataModelObject[] suppDMOs, String where, Object... args)
{
RecordBuffer buffer = buffer();
AbstractQuery.preprocessSubstitutionArguments(buffer.bufferManager, () -> false, false, args);
try
{
buffer.delete(suppDMOs, where, args);
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
/**
* Delete all records associated with this buffer, which match the criteria specified by the
* given where clause. Note that this where clause must not contain DMO alias qualifiers.
* <p>
* <b>NOTE:</b> the current implementation must not be invoked for a DMO which is backed by a
* permanent table; only temp-table bulk delete is supported at this time.
* <p>
* When this method returns:
* <ul>
* <li><strong>temp-table</strong>buffers became empty (Ie. {@code available} returns
* {@code false}). This is because these buffers are used in a loop and the last
* record is also deleted. P2J will detect these loops and convert it to the much
* faster bulk delete. P4GL does not allow bulk delete using SQL
* {@code DELETE FROM ... WHERE} syntax.
* <li>P2J does not support yet bulk deletion of <strong>permanent table</strong> buffers.
* In P4GL they will be empty if the predicate matches the currently stored record and
* remains unaffected otherwise.
* </ul>
*
* @param where
* An HQL where clause snippet which defines the restriction criteria to apply to the
* delete. All references to properties in a DMO must be unqualified.
* @param args
* Query substitution parameters required by the where clause.
*
* @throws ErrorConditionException
* if there is an error deleting the records.
* @throws UnsupportedOperationException
* if invoked on a buffer associated with a permanent table.
*/
public void delete(String where, Object ...args)
{
delete(null, where, args);
}
/**
* Bulk delete all records associated with the {@code Buffer} or {@code TempTable}. This allows bulk
* delete when TRACKING-CHANGES is on, as this emulates a {@code FOR EACH tt1. DELETE tt1. end.} block.
*
* @throws ErrorConditionException
* If an error occurs performing the bulk delete operation.
*/
@Override
public void deleteForAll()
{
if (!(this instanceof Temporary))
{
ErrorManager.recordOrShowError(7352);
// You may only invoke EMPTY-TEMP-TABLE on valid temp-tables.
return;
}
// need to update the ORIGIN-HANDLE attribute. Will defer execution through default buffer
TempTableBuilder.asTempTable((Temporary) this).deleteForAll();
}
/**
* Bulk delete all records associated with the buffer.
* <p>
* When this method returns, the buffer is empty (Ie. {@link #available} returns {@code false})
* for both temp and permanent tables.
* TODO: when is this invoked for permanent tables ?????
*
* @throws ErrorConditionException
* if an error occurs performing the bulk delete operation.
*/
@Override
public logical deleteAll()
{
if (!(this instanceof Temporary))
{
ErrorManager.recordOrShowError(7352);
// You may only invoke EMPTY-TEMP-TABLE on valid temp-tables.
return logical.FALSE;
}
TempTable tt = TempTableBuilder.asTempTable((Temporary) this);
if (tt == null)
{
LOG.log(Level.SEVERE,
"Nothing to delete, this dynamic TEMP-TABLE was not prepared yet.",
new Throwable());
return logical.FALSE;
}
// need to update the ORIGIN-HANDLE attribute. Will defer execution through default buffer
return tt.deleteAll();
}
/**
* Aggregate method which averages the values of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void average(String column, NumberType num)
{
average(column, num, null);
}
/**
* Aggregate method which averages the values of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* HQL where clause used to filter the records.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void average(String column, NumberType num, String where)
{
average(column, num, where, (Object[]) null);
}
/**
* Aggregate method which averages the values of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* HQL where clause used to filter the records.
* @param args
* Query substitution arguments.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void average(String column, NumberType num, String where, Object...args)
{
buffer().average(column, num, where, args);
}
/**
* Aggregate method which counts the rows in the table backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate. May be the wildcard
* character ("*").
* @param num
* Variable into which to store the aggregate result.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void count(String column, NumberType num)
{
count(column, num, null);
}
/**
* Aggregate method which counts the rows in the table backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate. May be the wildcard
* character ("*").
* @param num
* Variable into which to store the aggregate result.
* @param where
* HQL where clause used to filter the records.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void count(String column, NumberType num, String where)
{
count(column, num, where, (Object[]) null);
}
/**
* Aggregate method which counts the rows in the table backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate. May be the wildcard
* character ("*").
* @param num
* Variable into which to store the aggregate result.
* @param where
* HQL where clause used to filter the records.
* @param args
* Query substitution arguments.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void count(String column, NumberType num, String where, Object...args)
{
buffer().count(column, num, where, args);
}
/**
* Aggregate method which finds the minimum value of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void minimum(String column, NumberType num)
{
minimum(column, num, null);
}
/**
* Aggregate method which finds the minimum value of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* HQL where clause used to filter the records.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void minimum(String column, NumberType num, String where)
{
minimum(column, num, where, (Object[]) null);
}
/**
* Aggregate method which finds the minimum value of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* HQL where clause used to filter the records.
* @param args
* Query substitution arguments.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void minimum(String column, NumberType num, String where, Object...args)
{
buffer().minimum(column, num, where, args);
}
/**
* Aggregate method which finds the maximum value of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void maximum(String column, NumberType num)
{
maximum(column, num, null);
}
/**
* Aggregate method which finds the maximum value of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* HQL where clause used to filter the records.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void maximum(String column, NumberType num, String where)
{
maximum(column, num, where, (Object[]) null);
}
/**
* Aggregate method which finds the maximum value of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* HQL where clause used to filter the records.
* @param args
* Query substitution arguments.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void maximum(String column, NumberType num, String where, Object...args)
{
buffer().maximum(column, num, where, args);
}
/**
* Aggregate method which sums the values of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void sum(String column, NumberType num)
{
sum(column, num, null);
}
/**
* Aggregate method which sums the values of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* HQL where clause used to filter the records.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void sum(String column, NumberType num, String where)
{
sum(column, num, where, (Object[]) null);
}
/**
* Aggregate method which sums the values of a single column across rows in the table
* backing this buffer.
*
* @param column
* An expression indicating the name of the column to aggregate.
* @param num
* Variable into which to store the aggregate result.
* @param where
* HQL where clause used to filter the records.
* @param args
* Query substitution arguments.
*
* @throws ErrorConditionException
* if an error occurs performing the aggregate operation.
*/
public void sum(String column, NumberType num, String where, Object...args)
{
buffer().sum(column, num, where, args);
}
/**
* Release the record currently in the buffer, if any. No validation, no write trigger is fired. This
* method is invoked as result of an internal error condition when the content of the buffer failed
* internal validation and the buffer must be released.
*/
public void drop()
{
buffer().release(true, false);
}
/**
* Validate and release the record currently in the buffer, if any. May cause a write trigger to fire.
*
* @throws ErrorConditionException
* if the buffer fails validation.
*/
@Override
public void release()
{
// the converted RELEASE statement is forcefully flushing the record, so the fact that the underlying
// record is loaded in multiple buffers in the same time shouldn't be honored.
release(false);
}
/**
* Validate and release the record currently in the buffer, if any. May cause a write trigger to fire.
*
* @param honorMultipleReferences
* {@code true} if the flushing of the existing record should be suppressed if the record is also
* loaded in other buffers.
*
* @throws ErrorConditionException
* if the buffer fails validation.
*/
public void release(boolean honorMultipleReferences)
{
buffer().release(false, true, honorMultipleReferences);
}
/**
* Validate the record currently in the buffer, if any.
*
* @throws ErrorConditionException
* if the buffer fails validation.
*/
@Override
public void validate()
{
try
{
buffer().validate(true);
}
catch (ValidationException e)
{
// rethrow as 4GL error condition
ErrorManager.recordOrThrowError(e.getNumber(), e.getMessage(), false, false);
}
}
/**
* Validate the record currently in the buffer, if any.
*
* @throws ErrorConditionException
* if the buffer fails validation.
*/
@Override
public logical bufferValidate()
{
RecordBuffer recBuf = buffer();
try
{
recBuf.validate(true);
return logical.TRUE;
}
catch (ValidationException e)
{
if (recBuf.isNewlyCreated())
{
// untested at this time for 'database behavior' related to throwing structured exceptions
// - see recordOrShowDatabaseError
ErrorManager.recordOrShowError(e.getNumber(), e.getMessage(), false, false, false);
}
}
return logical.FALSE;
}
/**
* Conversion of the RECORD-LENGTH built-in function.
*
* @return Length of the record currently in the buffer, in bytes.
*/
@Override
public integer recordLength()
{
if (!_available())
{
return new integer(); // quick out
}
RecordBuffer buffer = buffer();
BufferReference proxy = buffer.getDMOProxy();
Map<String, Method> getters = buffer.getDmoInfo().getLegacyGetterMap();
Map<String, Integer> extents = buffer.getDmoInfo().getExtentMap();
int counter = 15; // hidden 2 fields and any header
int fieldCounter = getters.size() + 2;
if (fieldCounter > 15)
{
counter += 3;
}
counter += 2 * ((fieldCounter + 1) / 16);
for (Map.Entry<String, Method> entry : getters.entrySet())
{
String prop = entry.getKey();
Method method = entry.getValue();
Integer ext = extents.get(prop);
if (ext == null)
{
try
{
BaseDataType bdt = (BaseDataType) method.invoke(proxy);
counter += bdt.getSize();
}
catch (IllegalAccessException | InvocationTargetException e)
{
// log it?
}
}
else
{
counter += 4; // probably one 32bit int = the size of the extent
if (ext > 15)
{
counter += 3;
}
counter += 2 * ((ext + 1) / 16);
for (int k = 0; k < ext; k++)
{
try
{
BaseDataType bdt = (BaseDataType) method.invoke(proxy, k);
counter += bdt.getSize();
}
catch (IllegalAccessException | InvocationTargetException e)
{
// log it ?
}
}
}
}
return new integer(counter);
}
/**
* Resolve the BUFFER-FIELD with the given converted DMO property name.
*
* @param propertyName
* The property name.
*
* @return See above.
*/
public BufferFieldImpl bufferFieldForProperty(String propertyName)
{
if (fieldsByName == null)
{
fieldsByName = new HashMap<>();
}
BufferFieldImpl field = fieldsByName.get(propertyName);
if (field == null)
{
RecordBuffer recBuf = buffer();
int fieldId = TableMapper.getLegacyFieldInfo(recBuf, propertyName).getFieldId();
field = new BufferFieldImpl(_instantiatingProcedure(), (DataModelObject) this, propertyName);
if (fields == null)
{
int numFields = _numFields();
fields = new BufferFieldImpl[numFields + 1];
}
fields[fieldId] = field;
fieldsByName.put(propertyName, field);
}
return field;
}
/**
* Conversion of BUFFER-FIELD() method (KW_BUF_FLD).
* <p>
* Gets the handle to a particular buffer of a query or DataSet object.
*
* @param fieldNumber
* field number. An INTEGER expression representing the sequence
* number of the field in the buffer.
*
* @return handle to a particular field in the buffer.
*/
@Override
public handle bufferField(int fieldNumber)
{
if (fieldHandles == null)
{
fieldHandles = new handle[_numFields() + 1];
}
if (fieldNumber <= 0 || fieldNumber > _numFields())
{
String msg = "BUFFER-FIELD sequence number must be from 1 to " +
_numFields() + " for buffer " + buffer().getDmoInfo().legacyTable;
ErrorManager.recordOrShowDatabaseError(7350, msg, false, false);
return new handle();
}
handle hf = fieldHandles[fieldNumber];
if (hf == null)
{
BufferField field = bufferFieldNative(fieldNumber);
if (field != null)
{
hf = new handle(field);
fieldHandles[fieldNumber] = hf;
}
else
{
hf = new handle();
}
}
return hf;
}
/**
* Conversion of BUFFER-FIELD() method (KW_BUF_FLD).
* <p>
* Gets the handle to a particular buffer of a query or DataSet object.
*
* @param fieldNumber
* field number. An INTEGER expression representing the sequence
* number of the field in the buffer.
*
* @return a particular field in the buffer.
*/
public BufferFieldImpl bufferFieldNative(int fieldNumber)
{
int fieldCount = _numFields();
RecordBuffer recBuf = buffer();
String legacyTableName = recBuf.getDmoInfo().legacyTable;
if (fieldNumber <= 0 || fieldNumber > fieldCount)
{
String msg = "BUFFER-FIELD sequence number must be from 1 to " +
fieldCount + " for buffer " + legacyTableName;
ErrorManager.recordOrShowDatabaseError(7350, msg, false, false);
return null;
}
BufferFieldImpl field = fields == null ? null : fields[fieldNumber];
if (field == null)
{
String hibernateName;
Property fieldProperty = recBuf.getDmoInfo().getExistingFieldProperty(fieldNumber);
if (fieldProperty.original == null || fieldProperty.original.isEmpty())
{
hibernateName = fieldProperty.name;
}
else
{
hibernateName = fieldProperty.original;
}
field = new BufferFieldImpl(_instantiatingProcedure(), (DataModelObject) this, hibernateName);
if (fields == null)
{
int numFields = _numFields();
fields = new BufferFieldImpl[numFields + 1];
}
fields[fieldNumber] = field;
if (fieldsByName == null)
{
fieldsByName = new HashMap<>();
}
fieldsByName.put(field.getOriginalPropertyName(), field);
}
return field;
}
/**
* Implementation of BUFFER-FIELD() method (KW_BUF_FLD).
* <p>
* Gets the handle to a particular field within a buffer.
*
* @param fieldName
* The legacy field name. A CHARACTER string expression representing the name
* of the field in the buffer.
*
* @return handle to a particular field in the buffer.
*/
@Override
public handle bufferField(String fieldName)
{
return new handle(bufferFieldNative(fieldName));
}
/**
* Implementation of BUFFER-FIELD() method (KW_BUF_FLD).
* <p>
* Gets a particular field within a buffer.
*
* @param fieldName
* The legacy field name. A CHARACTER string expression representing the name
* of the field in the buffer.
*
* @return a particular field in the buffer.
*/
public BufferField bufferFieldNative(String fieldName)
{
Property property = buffer().getDmoInfo().byLegacyName(fieldName);
if (property != null)
{
BufferFieldImpl field = bufferFieldForProperty(property.name);
if (field != null)
{
return field;
}
}
String msg = "BUFFER-FIELD " + fieldName + " was not found in buffer " + doGetName();
ErrorManager.recordOrShowDatabaseError(7351, msg, false, false);
// return unknown handle
return null;
}
/**
* Conversion of BUFFER-COPY() method (KW_BUF_COPY).
* <p>
* This method copies any common fields, determined by name, data type,
* and extent-matching, from the source buffer to the receiving buffer.
* If there are fields in one buffer that do not exist in the other, they
* are ignored. This method is used to accommodate temp-tables of joins.
*
* @param buffer
* The source buffer to copy from.
*
* @return <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
*/
public logical bufferCopy(Buffer buffer)
{
return bufferCopy(buffer, null, null);
}
/**
* Conversion of BUFFER-COPY() method (KW_BUF_COPY).
* <p>
* This method copies any common fields, determined by name, data type,
* and extent-matching, from the source buffer to the receiving buffer.
* If there are fields in one buffer that do not exist in the other, they
* are ignored. This method is used to accommodate temp-tables of joins.
*
* @param buffer
* The source buffer to copy from.
* @param except
* A comma-separated list of fields that will be ignored in the copy process.
*
* @return <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
*/
public logical bufferCopy(Buffer buffer, character except)
{
return bufferCopy(buffer, except, null);
}
/**
* Conversion of BUFFER-COPY() method (KW_BUF_COPY).
* <p>
* This method copies any common fields, determined by name, data type,
* and extent-matching, from the source buffer to the receiving buffer.
* If there are fields in one buffer that do not exist in the other, they
* are ignored. This method is used to accommodate temp-tables of joins.
*
* @param buffer
* The source buffer to copy from.
* @param except
* A comma-separated list of fields that will be ignored in the copy process.
* @param pairs
* A comma-separated list of field-name pairs to be copied. Each pair must contain
* one field name from the source and one field name from the target.
*
* @return <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
*/
public logical bufferCopy(Buffer buffer, character except, character pairs)
{
return bufferCopy(buffer, except, pairs, logical.FALSE);
}
/**
* Conversion of BUFFER-COPY() method (KW_BUF_COPY).
* <p>
* This method copies any common fields, determined by name, data type,
* and extent-matching, from the source buffer to the receiving buffer.
* If there are fields in one buffer that do not exist in the other, they
* are ignored. This method is used to accommodate temp-tables of joins.
*
* @param buffer
* The source buffer to copy from.
* @param except
* A comma-separated list of fields that will be ignored in the copy process.
* @param pairs
* A comma-separated list of field-name pairs to be copied. Each pair must contain
* one field name from the source and one field name from the target.
* @param noLobs
* If <code>TRUE</code>, BLOB and CLOB fields are ignored during the copy.
*
* @return <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
*/
public logical bufferCopy(Buffer buffer, character except, character pairs, logical noLobs)
{
buffer().setBufferCopyDestination(true);
return RecordBuffer.copy(buffer(), ((BufferImpl) buffer).buffer(), except, pairs, noLobs);
}
/**
* Conversion of BUFFER-COPY() method (KW_BUF_COPY).
* <p>
* This method copies any common fields, determined by name, data type,
* and extent-matching, from the source buffer to the receiving buffer.
* If there are fields in one buffer that do not exist in the other, they
* are ignored. This method is used to accommodate temp-tables of joins.
*
* @param bufHandle
* The handle of the source buffer to copy from.
*
* @return <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
*/
public logical bufferCopy(handle bufHandle)
{
return bufferCopy((Buffer) bufHandle.get());
}
/**
* Conversion of BUFFER-COPY() method (KW_BUF_COPY).
* <p>
* This method copies any common fields, determined by name, data type,
* and extent-matching, from the source buffer to the receiving buffer.
* If there are fields in one buffer that do not exist in the other, they
* are ignored. This method is used to accommodate temp-tables of joins.
*
* @param bufHandle
* The handle of the source buffer to copy from.
* @param except
* A comma-separated list of fields that will be ignored in the copy process.
*
* @return <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
*/
public logical bufferCopy(handle bufHandle, character except)
{
return bufferCopy((Buffer) bufHandle.get(), except);
}
/**
* Conversion of BUFFER-COPY() method (KW_BUF_COPY).
* <p>
* This method copies any common fields, determined by name, data type,
* and extent-matching, from the source buffer to the receiving buffer.
* If there are fields in one buffer that do not exist in the other, they
* are ignored. This method is used to accommodate temp-tables of joins.
*
* @param bufHandle
* The handle of the source buffer to copy from.
* @param except
* A comma-separated list of fields that will be ignored in the copy process.
* @param pairs
* A comma-separated list of field-name pairs to be copied. Each pair must contain
* one field name from the source and one field name from the target.
*
* @return <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
*/
public logical bufferCopy(handle bufHandle, character except, character pairs)
{
return bufferCopy((Buffer) bufHandle.get(), except, pairs);
}
/**
* Conversion of BUFFER-COPY() method (KW_BUF_COPY).
* <p>
* This method copies any common fields, determined by name, data type,
* and extent-matching, from the source buffer to the receiving buffer.
* If there are fields in one buffer that do not exist in the other, they
* are ignored. This method is used to accommodate temp-tables of joins.
*
* @param bufHandle
* The handle of the source buffer to copy from.
* @param except
* A comma-separated list of fields that will be ignored in the copy process.
* @param pairs
* A comma-separated list of field-name pairs to be copied. Each pair must contain
* one field name from the source and one field name from the target.
* @param noLobs
* If <code>TRUE</code>, BLOB and CLOB fields are ignored during the copy.
*
* @return <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
*/
public logical bufferCopy(handle bufHandle,
character except,
character pairs,
logical noLobs)
{
return bufferCopy((Buffer) bufHandle.get(), except, pairs, noLobs);
}
/**
* Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
* <p>
* This method does a rough compare of any common fields, determined by
* name, data type, and extent-matching, between the source buffer and the
* target buffer. The resulting logical value is either TRUE or FALSE as
* a whole. A single field that does not compare causes the entire buffer
* to return FALSE. If there are fields in one buffer that do not exist in
* the other, they are ignored.
*
* @param buffer
* Buffer with which to compare.
*
* @return <code>true</code> if BUFFER-COMPARE() succeeds,
* else <code>false</code>.
*/
public logical bufferCompare(Buffer buffer)
{
return bufferCompare(buffer, null, null, null);
}
/**
* Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
* <p>
* This method does a rough compare of any common fields, determined by
* name, data type, and extent-matching, between the source buffer and the
* target buffer. The resulting logical value is either TRUE or FALSE as
* a whole. A single field that does not compare causes the entire buffer
* to return FALSE. If there are fields in one buffer that do not exist in
* the other, they are ignored.
*
* @param buffer
* Buffer with which to compare.
* @param mode
* The text "binary" or "case-sensitive", to provide that type of comparison.
*
* @return <code>true</code> if BUFFER-COMPARE() succeeds,
* else <code>false</code>.
*/
public logical bufferCompare(Buffer buffer, character mode)
{
return bufferCompare(buffer, mode, null, null);
}
/**
* Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
* <p>
* This method does a rough compare of any common fields, determined by
* name, data type, and extent-matching, between the source buffer and the
* target buffer. The resulting logical value is either TRUE or FALSE as
* a whole. A single field that does not compare causes the entire buffer
* to return FALSE. If there are fields in one buffer that do not exist in
* the other, they are ignored.
*
* @param buffer
* Buffer with which to compare.
* @param mode
* The text "binary" or "case-sensitive", to provide that type of comparison.
* @param except
* A comma-separated list of fields that will be ignored in the compare process.
*
* @return <code>true</code> if BUFFER-COMPARE() succeeds,
* else <code>false</code>.
*/
public logical bufferCompare(Buffer buffer, character mode, character except)
{
return bufferCompare(buffer, mode, except, null);
}
/**
* Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
* <p>
* This method does a rough compare of any common fields, determined by
* name, data type, and extent-matching, between the source buffer and the
* target buffer. The resulting logical value is either TRUE or FALSE as
* a whole. A single field that does not compare causes the entire buffer
* to return FALSE. If there are fields in one buffer that do not exist in
* the other, they are ignored.
*
* @param srcBuffer
* Buffer with which to compare.
* @param mode
* The text "binary" or "case-sensitive", to provide that type of comparison.
* @param except
* A comma-separated list of fields that will be ignored in the compare process.
* @param pairs
* A comma-separated list of field-name pairs to be compared. Each pair must
* contain one field name from the source and one field name from the target.
*
* @return <code>true</code> if BUFFER-COMPARE() succeeds,
* else <code>false</code>.
*/
public logical bufferCompare(Buffer srcBuffer,
character mode,
character except,
character pairs)
{
return bufferCompare(srcBuffer, mode, except, pairs, null);
}
/**
* Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
* <p>
* This method does a rough compare of any common fields, determined by
* name, data type, and extent-matching, between the source buffer and the
* target buffer. The resulting logical value is either TRUE or FALSE as
* a whole. A single field that does not compare causes the entire buffer
* to return FALSE. If there are fields in one buffer that do not exist in
* the other, they are ignored.
*
* @param srcBuffer
* Buffer with which to compare.
* @param mode
* The text "binary" or "case-sensitive", to provide that type of comparison.
* @param except
* A comma-separated list of fields that will be ignored in the compare process.
* @param pairs
* A comma-separated list of field-name pairs to be compared. Each pair must
* contain one field name from the source and one field name from the target.
* @param noLobs
* If {@code true}, BLOB and CLOB fields are ignored during the comparison. May be
* {@code false} or {@code null} to include such fields.
*
* @return {@code true} if BUFFER-COMPARE() succeeds, else {@code false}.
*/
public logical bufferCompare(Buffer srcBuffer,
character mode,
character except,
character pairs,
logical noLobs)
{
return RecordBuffer.compare(((BufferImpl) srcBuffer).buffer(),
buffer(),
BufferCompare.Mode.fromString(mode),
except,
pairs,
noLobs);
}
/**
* Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
* <p>
* This method does a rough compare of any common fields, determined by
* name, data type, and extent-matching, between the source buffer and the
* target buffer. The resulting logical value is either TRUE or FALSE as
* a whole. A single field that does not compare causes the entire buffer
* to return FALSE. If there are fields in one buffer that do not exist in
* the other, they are ignored.
*
* @param bufHandle
* Handle of buffer with which to compare.
*
* @return <code>true</code> if BUFFER-COMPARE() succeeds,
* else <code>false</code>.
*/
public logical bufferCompare(handle bufHandle)
{
return bufferCompare((Buffer) bufHandle.get());
}
/**
* Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
* <p>
* This method does a rough compare of any common fields, determined by
* name, data type, and extent-matching, between the source buffer and the
* target buffer. The resulting logical value is either TRUE or FALSE as
* a whole. A single field that does not compare causes the entire buffer
* to return FALSE. If there are fields in one buffer that do not exist in
* the other, they are ignored.
*
* @param bufHandle
* Handle of buffer with which to compare.
* @param mode
* The text "binary" or "case-sensitive", to provide that type of comparison.
*
* @return <code>true</code> if BUFFER-COMPARE() succeeds,
* else <code>false</code>.
*/
public logical bufferCompare(handle bufHandle, character mode)
{
return bufferCompare((Buffer) bufHandle.get(), mode);
}
/**
* Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
* <p>
* This method does a rough compare of any common fields, determined by
* name, data type, and extent-matching, between the source buffer and the
* target buffer. The resulting logical value is either TRUE or FALSE as
* a whole. A single field that does not compare causes the entire buffer
* to return FALSE. If there are fields in one buffer that do not exist in
* the other, they are ignored.
*
* @param bufHandle
* Handle of buffer with which to compare.
* @param mode
* The text "binary" or "case-sensitive", to provide that type of comparison.
* @param except
* A comma-separated list of fields that will be ignored in the compare process.
*
* @return <code>true</code> if BUFFER-COMPARE() succeeds,
* else <code>false</code>.
*/
public logical bufferCompare(handle bufHandle, character mode, character except)
{
return bufferCompare((Buffer) bufHandle.get(), mode, except);
}
/**
* Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
* <p>
* This method does a rough compare of any common fields, determined by
* name, data type, and extent-matching, between the source buffer and the
* target buffer. The resulting logical value is either TRUE or FALSE as
* a whole. A single field that does not compare causes the entire buffer
* to return FALSE. If there are fields in one buffer that do not exist in
* the other, they are ignored.
*
* @param bufHandle
* Handle of buffer with which to compare.
* @param mode
* The text "binary" or "case-sensitive", to provide that type of comparison.
* @param except
* A comma-separated list of fields that will be ignored in the compare process.
* @param pairs
* A comma-separated list of field-name pairs to be compared. Each pair must
* contain one field name from the source and one field name from the target.
*
* @return <code>true</code> if BUFFER-COMPARE() succeeds,
* else <code>false</code>.
*/
public logical bufferCompare(handle bufHandle, character mode, character except, character pairs)
{
return bufferCompare((Buffer) bufHandle.get(), mode, except, pairs);
}
/**
* Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
* <p>
* This method does a rough compare of any common fields, determined by
* name, data type, and extent-matching, between the source buffer and the
* target buffer. The resulting logical value is either TRUE or FALSE as
* a whole. A single field that does not compare causes the entire buffer
* to return FALSE. If there are fields in one buffer that do not exist in
* the other, they are ignored.
*
* @param bufHandle
* Handle of buffer with which to compare.
* @param mode
* The text "binary" or "case-sensitive", to provide that type of comparison.
* @param except
* A comma-separated list of fields that will be ignored in the compare process.
* @param pairs
* A comma-separated list of field-name pairs to be compared. Each pair must
* contain one field name from the source and one field name from the target.
* @param noLobs
* If <code>TRUE</code>, BLOB and CLOB fields are ignored during the compare.
*
* @return <code>true</code> if BUFFER-COMPARE() succeeds,
* else <code>false</code>.
*/
public logical bufferCompare(handle bufHandle,
character mode,
character except,
character pairs,
logical noLobs)
{
return bufferCompare((Buffer) bufHandle.get(), mode, except, pairs, noLobs);
}
/**
* Conversion of BUFFER-CREATE() method (KW_BUF_CREA).
* <p>
* Creates a record, sets fields to their default values, and moves a copy of the record into the buffer.
*
* @return {@code true} if successful
*/
public logical bufferCreate()
{
if (isBeforeBuffer() && !setUpBeforeBuffer)
{
// the BEFORE-BUFFERS can only be queried, not altered in any way (REMOVE/CHANGE)
ErrorManager.recordOrThrowError(12373, doGetName(), "");
// You may not delete BEFORE-TABLE <name> record. Use ACCEPT-ROW-CHANGES instead.
return logical.FALSE;
}
RecordBuffer buffer = buffer();
if (!buffer.isTemporary() && !verifyTransaction(buffer))
{
return logical.FALSE;
}
return logical.of(buffer.create());
}
/**
* Conversion of BUFFER-RELEASE() method (KW_BUF_REL).
* <p>
* Releases a record from a buffer object. The BUFFER-RELEASE method
* corresponds to the RELEASE statement.
*
* @return <code>true</code> if BUFFER-RELEASE() succeeds,
* else <code>false</code>.
*/
public logical bufferRelease()
{
boolean success = true;
try
{
release();
}
catch (ErrorConditionException exc)
{
success = false;
}
return logical.of(success);
}
/**
* Emitted for <code>::</code> operator used as r-value in a dynamic extent assignment, as the conversion
* rules doesn't know if the target field is extent or scalar.
*
* @param memberName
* The name of the member to be extracted.
*
* @return The value of the member (scalar or extent) or unknown (?) if there is no such member.
*/
@Override
public Object derefPoly(String memberName)
{
Property property = buffer().getDmoInfo().byLegacyName(memberName);
if (property == null)
{
ErrorManager.recordOrShowError(7351, memberName, doGetName());
// BUFFER-FIELD <field-name> was not found in buffer <buffer-name>.
return new unknown();
}
if (property.extent == 0)
{
return dereference(memberName);
}
if (!buffer().isAvailable() && !buffer().isUnknownMode())
{
String msg1 = "No " + doGetName() + " record is available";
ErrorManager.displayError(91, msg1, true);
String msg2 = "Unable to extract BUFFER-VALUE for field " + memberName;
ErrorManager.displayError(7366, msg2, false);
return new unknown();
}
BaseDataType[] ret = new BaseDataType[property.extent];
for (int idx = 0; idx < ret.length; idx++)
{
ret[idx] = bufferFieldForProperty(property.name).value(idx + 1);
}
return ret;
}
/**
* Dereference a named member from this collection.
*
* @param memberName
* The legacy name of the member to be extracted.
*
* @return The value of the member or unknown (?) if there is no such member.
*/
@Override
public BaseDataType dereference(String memberName)
{
Property property = buffer().getDmoInfo().byLegacyName(memberName);
if (property == null)
{
ErrorManager.recordOrShowError(7351, memberName, doGetName());
// BUFFER-FIELD <field-name> was not found in buffer <buffer-name>.
return new unknown();
}
if (property.extent != 0)
{
ErrorManager.recordOrThrowError(14905);
// Whole-array assignment target and source must have the same extent unless the target is indeterminate. (14905)
return new unknown();
}
if (!buffer().isAvailable() && !buffer().isUnknownMode())
{
String msg1 = "No " + doGetName() + " record is available";
ErrorManager.recordOrShowDatabaseError(91, msg1, false, true);
String msg2 = "Unable to extract BUFFER-VALUE for field " + memberName;
ErrorManager.recordOrShowDatabaseError(7366, msg2, false, false);
return new unknown();
}
BaseDataType dbt = bufferFieldForProperty(property.name).value();
if (dbt instanceof character)
{
// quirk in 4GL: the character fields retrieved using dereference operator are always
// case-insensitive (no matter the initial field property case sensitivity)
((character) dbt).setCaseSensitive(false);
}
return dbt;
}
/**
* Dereference a named member from this collection. If this collection is a ProDataSet then
* the returned value is a {@link handle} to a named buffer, if the collection is a buffer
* the returned value is the field value with the respective name. In the case that this
* collection does not have a named member with the name specified <code>memberName</code>
* the unknown value is returned.
*
* @param index
* The subscript to be applied to an extent field.
* @param memberName
* The name of the member to be extracted.
*
* @return The value of the member or unknown (?) if there is no such member.
*/
@Override
public BaseDataType dereference(NumberType index, String memberName)
{
Property property = buffer().getDmoInfo().byLegacyName(memberName);
if (property == null)
{
ErrorManager.recordOrShowError(7351, memberName, doGetName());
// BUFFER-FIELD <field-name> was not found in buffer <buffer-name>.
return new unknown();
}
if (!buffer().isAvailable())
{
String msg1 = "No " + doGetName() + " record is available";
ErrorManager.recordOrShowDatabaseError(91, msg1, false, true);
String msg2 = "Unable to extract BUFFER-VALUE for field " + memberName;
ErrorManager.recordOrShowDatabaseError(7366, msg2, false, false);
return new unknown();
}
if (property.extent == 0)
{
return dereference(memberName);
}
if (index.isUnknown())
{
return new unknown();
}
long idx = index.longValue();
if (idx == 0)
{
// no error is shown/raised
return new unknown();
}
if (idx < 1 || idx > property.extent)
{
String msg1 = "Array subscript " + index + " is out of range";
ErrorManager.displayError(26, msg1, true);
String msg2 = "Unable to extract BUFFER-VALUE for field " + memberName;
ErrorManager.displayError(7366, msg2, false);
return new unknown();
}
return bufferFieldForProperty(property.name).value((int) idx);
}
/**
* Dereference a named member from this collection. If this collection is a ProDataSet then
* the returned value is a {@link handle} to a named buffer, if the collection is a buffer
* the returned value is the field value with the respective name. In the case that this
* collection does not have a named member with the name specified <code>memberName</code>
* the unknown value is returned.
*
* @param index
* The subscript to be applied to an extent field.
* @param memberName
* The name of the member to be extracted.
*
* @return The value of the member or unknown (?) if there is no such member.
*/
@Override
public BaseDataType dereference(long index, String memberName)
{
return dereference(new int64(index), memberName);
}
/**
* Dereference a named member from this collection. If this collection is a DataSet then
* the returned value is a {@link handle} to a named buffer, if the collection is a buffer
* the returned value is the field value with the respective name. In the case that this
* collection does not have a named member with the name specified {@code memberName}
* the unknown value is returned.
*
* @param type
* The expected type of the returned value.
* @param memberName
* The name of the member to be extracted.
*
* @return The value of the member or unknown (?) if there is no such member.
*/
@Override
public <T extends BaseDataType> T dereference(Class<T> type, String memberName)
{
BaseDataType val = dereference(memberName);
if (type.isAssignableFrom(val.getClass()))
{
return (T) val;
}
String errMsg2 =
"Lead attributes in a chained-attribute expression (a:b:c) must be type " +
"HANDLE or a user-defined type and valid (not UNKNOWN)";
ErrorManager.recordOrThrowError(10068, errMsg2, false, false);
return (T) BaseDataType.generateUnknown(type);
}
/**
* This overloaded method is used when the dereference is the left-side of an assignment.
* The implementation will extract the member from the collection and assign it the new value.
*
* @param memberName
* The name of the member to be extracted.
* @param value
* The new value that will be assigned to the extracted field.
*/
@Override
public void dereference(String memberName, Object value)
{
if (isBeforeBuffer() && !setUpBeforeBuffer)
{
ErrorManager.recordOrShowError(12375, doGetName());
// You may not update BEFORE-TABLE <name> record.
ErrorManager.recordOrShowError(142, doGetName());
// ** Unable to update <filename> Field.
ErrorManager.recordOrShowError(3131, "?", "", LegacyResource.BUFFER_FIELD);
// Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>
ErrorManager.recordOrThrowError(142, doGetName(), "");
// ** Unable to update <filename> Field.
return;
}
Property property = buffer().getDmoInfo().byLegacyName(memberName);
if (property == null)
{
String errMsg = "BUFFER-FIELD " + memberName + " was not found in buffer " + doGetName();
ErrorManager.recordOrThrowError(7351, errMsg);
return;
}
boolean bulk = value.getClass().isArray();
if (bulk)
{
BufferFieldImpl field = new BufferFieldImpl(_instantiatingProcedure(),
(DataModelObject) this,
property.name,
bulk);
try
{
field.changeValue(value);
}
finally
{
// force the delete, this instance is required for bulk assign of an extent field, and is not a
// real BUFFER-FIELD resource
field.forceDelete();
}
}
else
{
BufferFieldImpl field = bufferFieldForProperty(property.name);
field.changeValue(value);
}
}
/**
* This overloaded method is used when the dereference is the left-side of an assignment.
* The implementation will extract the member from the collection and assign it the new value.
*
* @param index
* The subscript to be applied to an extent field.
* @param memberName
* The name of the member to be extracted.
* @param value
* The new value that will be assigned to the extracted field.
*/
@Override
public void dereference(NumberType index, String memberName, Object value)
{
if (isBeforeBuffer() && !setUpBeforeBuffer)
{
ErrorManager.recordOrShowError(12375, doGetName());
// You may not update BEFORE-TABLE <name> record.
ErrorManager.recordOrShowError(142, doGetName());
// ** Unable to update <filename> Field.
ErrorManager.recordOrShowError(3131, "?", "", LegacyResource.BUFFER_FIELD);
// Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>
ErrorManager.recordOrThrowError(142, doGetName(), "");
// ** Unable to update <filename> Field.
return;
}
Property property = buffer().getDmoInfo().byLegacyName(memberName);
if (property == null)
{
String errMsg = "BUFFER-FIELD " + memberName + " was not found in buffer " + doGetName();
ErrorManager.recordOrThrowError(7351, errMsg);
return;
}
if (index.isUnknown())
{
ErrorManager.recordOrShowError(7365, memberName, String.valueOf(property.extent));
// <field> BUFFER-VALUE argument must be valid array index, less than or equal to field extent <extent>
ErrorManager.recordOrShowError(3131, "?", "", LegacyResource.BUFFER_FIELD);
// Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>
ErrorManager.recordOrThrowError(142, doGetName(), "");
// ** Unable to update <filename> Field.
return;
}
long idx = index.longValue();
if (property.extent == 0 && idx == 0)
{
dereference(memberName, value);
return;
}
if (idx == 0)
{
ErrorManager.recordOrShowError(26, String.valueOf(idx));
// ** Array subscript <index> is out of range. (26)
ErrorManager.recordOrShowError(142, doGetName(), "");
// ** Unable to update <filename> Field.
ErrorManager.recordOrShowError(3131, "?", "", LegacyResource.BUFFER_FIELD);
// Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>
ErrorManager.recordOrThrowError(142, doGetName(), "");
// ** Unable to update <filename> Field.
return;
}
if (idx < 1 || idx > property.extent)
{
ErrorManager.recordOrShowError(7365,
memberName,
String.valueOf(property.extent),
LegacyResource.BUFFER_FIELD);
// <field> BUFFER-VALUE argument must be valid array index, less than or equal to field extent <extent>
ErrorManager.recordOrShowError(3131, "?", "", LegacyResource.BUFFER_FIELD);
// Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>
ErrorManager.recordOrThrowError(142, doGetName(), "");
// ** Unable to update <filename> Field.
return;
}
bufferFieldForProperty(property.name).changeValue((int) idx, value);
}
/**
* This overloaded method is used when the dereference is the left-side of an assignment.
* The implementation will extract the member from the collection and assign it the new value.
*
* @param index
* The subscript to be applied to an extent field.
* @param memberName
* The name of the member to be extracted.
* @param value
* The new value that will be assigned to the extracted field.
*/
@Override
public void dereference(long index, String memberName, Object value)
{
dereference(new int64(index), memberName, value);
}
/**
* Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
* <p>
* Locates the record with the rowid you specify, then moves the record
* into the buffer.
*
* @param id
* An expression of type ROWID that represents the rowid of the
* desired record.
*
* @return <code>true</code> if it finds the record, else
* <code>false</code>.
*/
public logical findByRowID(rowid id)
{
return findByRowID(id, LockType.LT_SHARE);
}
/**
* Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
* <p>
* Locates the record with the rowid you specify, then moves the record into the buffer.
*
* @param id
* An expression of type ROWID that represents the rowid of the desired record.
* @param lockType
* Lock type
*
* @return {@code true} if it finds the record, else {@code false}.
*/
public logical findByRowID(rowid id, LockType lockType)
{
final RecordBuffer buffer = buffer();
final String dmoAlias = buffer.getDMOAlias();
if (id.isUnknown())
{
String msg = "Invalid rowid for FIND-BY-ROWID method of BUFFER object " + buffer.getLegacyName();
ErrorManager.recordOrShowError(7341, msg, false);
return logical.FALSE;
}
try
{
boolean err = ErrorManager.silent(() ->
{
buffer.initialize();
// manufacture a primary key sort clause; this will be discarded from the final FQL, but it is
// needed by RAQ.initialize
String sort = dmoAlias + "." + Session.PK + " asc";
FindQuery findQuery = new FindQuery(buffer,
dmoAlias + "." + Session.PK + " = ?",
null,
sort,
new Object[] { id },
lockType);
findQuery.unique();
});
return logical.of(!err);
}
catch (IllegalArgumentException | ErrorConditionException | QueryOffEndException exc)
{
// all ignored
}
return logical.FALSE;
}
/**
* Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
* <p>
* Locates the record with the rowid you specify, then moves the record into the buffer.
*
* @param id
* An expression of type ROWID that represents the rowid of the desired record.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
*
* @return {@code true} if it finds the record, else {@code false}.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findByRowID(rowid id, long lock)
{
LockType lockType = fromLockNowait(lock, 0);
return lockType == null ? logical.FALSE : findByRowID(id, lockType);
}
/**
* Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
* <p>
* Locates the record with the rowid you specify, then moves the record into the buffer.
*
* @param id
* An expression of type ROWID that represents the rowid of the desired record.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
*
* @return {@code true} if it finds the record, else {@code false}.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findByRowID(rowid id, NumberType lock)
{
LockType lockType = fromLockNowait(lock.longValue(), 0);
return lockType == null ? logical.FALSE : findByRowID(id, lockType);
}
/**
* Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
* <p>
* Locates the record with the rowid you specify, then moves the record into the buffer.
*
* @param id
* An expression of type ROWID that represents the rowid of the desired record.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* NO-WAIT
*
* @return {@code true} if it finds the record, else {@code false}.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findByRowID(rowid id, long lock, long nowait)
{
LockType lockType = fromLockNowait(lock, nowait);
return lockType == null ? logical.FALSE : findByRowID(id, lockType);
}
/**
* Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
* <p>
* Locates the record with the rowid you specify, then moves the record into the buffer.
*
* @param id
* An expression of type ROWID that represents the rowid of the desired record.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* NO-WAIT
*
* @return {@code true} if it finds the record, else {@code false}.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findByRowID(rowid id, NumberType lock, long nowait)
{
LockType lockType = fromLockNowait(lock.longValue(), nowait);
return lockType == null ? logical.FALSE : findByRowID(id, lockType);
}
/**
* Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
* <p>
* Locates the record with the rowid you specify, then moves the record into the buffer.
*
* @param id
* An expression of type ROWID that represents the rowid of the desired record.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* NO-WAIT
*
* @return {@code true} if it finds the record, else {@code false}.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findByRowID(rowid id, long lock, NumberType nowait)
{
LockType lockType = fromLockNowait(lock, nowait.longValue());
return lockType == null ? logical.FALSE : findByRowID(id, lockType);
}
/**
* Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
* <p>
* Locates the record with the rowid you specify, then moves the record into the buffer.
*
* @param id
* An expression of type ROWID that represents the rowid of the desired record.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* NO-WAIT
*
* @return {@code true} if it finds the record, else {@code false}.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findByRowID(rowid id, NumberType lock, NumberType nowait)
{
LockType lockType = fromLockNowait(lock.longValue(), nowait.longValue());
return lockType == null ? logical.FALSE : findByRowID(id, lockType);
}
/**
* Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
* <p>
* Locates the record with the rowid you specify, then moves the record into the buffer.
*
* @param id
* An expression of type ROWID that represents the rowid of the desired record.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* NO-WAIT
*
* @return {@code true} if it finds the record, else {@code false}.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findByRowID(rowid id, integer lock, int nowait)
{
LockType lockType = fromLockNowait(lock.longValue(), nowait);
return lockType == null ? logical.FALSE : findByRowID(id, lockType);
}
/**
* Conversion of FIND-FIRST() method (KW_FIND_1ST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @return <code>true</code> if FIND-FIRST() succeeds,
* else <code>false</code>.
*/
public logical findFirst()
{
return _find(null, FindMode.FIRST, LockType.SHARE);
}
/**
* Conversion of FIND-FIRST() method (KW_FIND_1ST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
*
* @return <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
*/
public logical findFirst(character predicate)
{
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.FIRST, LockType.SHARE);
}
/**
* Conversion of FIND-FIRST() method (KW_FIND_1ST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lockType
* Lock type
*
* @return <code>true</code> if FIND-FIRST() succeeds,
* else <code>false</code>.
*/
public logical findFirst(character predicate, LockType lockType)
{
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.FIRST, lockType);
}
/**
* Conversion of FIND-FIRST() method (KW_FIND_1ST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
*
* @return <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
*/
public logical findFirst(character predicate, long lock)
{
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.FIRST, LockType.fromInt((int) lock));
}
/**
* Conversion of FIND-FIRST() method (KW_FIND_1ST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
*
* @return <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
*/
public logical findFirst(character predicate, NumberType lock)
{
LockType lockType = lock.isUnknown() ? LockType.SHARE : LockType.fromInt(lock.intValue());
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.FIRST, lockType);
}
/**
* Conversion of FIND-FIRST() method (KW_FIND_1ST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findFirst(character predicate, long lock, long nowait)
{
LockType lockType = fromLockNowait(lock, nowait);
// toJavaType() returns null if predicate is unknown
return lockType == null
? logical.FALSE
: _find(predicate.toJavaType(), FindMode.FIRST, lockType);
}
/**
* Conversion of FIND-FIRST() method (KW_FIND_1ST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findFirst(character predicate, NumberType lock, long nowait)
{
long javaLock = lock.isUnknown() ? LockType.LT_SHARE : lock.longValue();
LockType lockType = fromLockNowait(javaLock, nowait);
// toJavaType() returns null if predicate is unknown
return lockType == null
? logical.FALSE
: _find(predicate.toJavaType(), FindMode.FIRST, lockType);
}
/**
* Conversion of FIND-FIRST() method (KW_FIND_1ST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findFirst(character predicate, long lock, NumberType nowait)
{
LockType lockType = fromLockNowait(lock, nowait.longValue());
// toJavaType() returns null if predicate is unknown
return lockType == null
? logical.FALSE
: _find(predicate.toJavaType(), FindMode.FIRST, lockType);
}
/**
* Conversion of FIND-FIRST() method (KW_FIND_1ST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findFirst(character predicate, NumberType lock, NumberType nowait)
{
long javaLock = lock.isUnknown() ? LockType.LT_SHARE : lock.longValue();
LockType lockType = fromLockNowait(javaLock, nowait.longValue());
// toJavaType() returns null if predicate is unknown
return lockType == null
? logical.FALSE
: _find(predicate.toJavaType(), FindMode.FIRST, lockType);
}
/**
* Conversion of FIND-LAST() method (KW_FIND_LST).
* <p>
* Gets the last record.
*
* @return <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
*/
public logical findLast()
{
return _find(null, FindMode.LAST, LockType.SHARE);
}
/**
* Conversion of FIND-LAST() method (KW_FIND_LST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
*
* @return <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
*/
public logical findLast(character predicate)
{
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.LAST, LockType.SHARE);
}
/**
* Conversion of FIND-LAST() method (KW_FIND_LST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lockType
* Lock type
*
* @return <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
*/
public logical findLast(character predicate, LockType lockType)
{
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.LAST, lockType);
}
/**
* Conversion of FIND-LAST() method (KW_FIND_LST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
*
* @return <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
*/
public logical findLast(character predicate, long lock)
{
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.LAST, LockType.fromInt((int) lock));
}
/**
* Conversion of FIND-LAST() method (KW_FIND_LST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
*
* @return <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
*/
public logical findLast(character predicate, NumberType lock)
{
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.LAST, LockType.fromInt(lock.intValue()));
}
/**
* Conversion of FIND-LAST() method (KW_FIND_LST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findLast(character predicate, long lock, long nowait)
{
LockType lockType = fromLockNowait(lock, nowait);
// toJavaType() returns null if predicate is unknown
return lockType == null
? logical.FALSE
: _find(predicate.toJavaType(), FindMode.LAST, lockType);
}
/**
* Conversion of FIND-LAST() method (KW_FIND_LST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findLast(character predicate, NumberType lock, long nowait)
{
LockType lockType = fromLockNowait(lock.longValue(), nowait);
// toJavaType() returns null if predicate is unknown
return lockType == null
? logical.FALSE
: _find(predicate.toJavaType(), FindMode.LAST, lockType);
}
/**
* Conversion of FIND-LAST() method (KW_FIND_LST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findLast(character predicate, long lock, NumberType nowait)
{
// TODO: add support for ? nowait mode
LockType lockType = fromLockNowait(lock, nowait.longValue());
// toJavaType() returns null if predicate is unknown
return lockType == null
? logical.FALSE
: _find(predicate.toJavaType(), FindMode.LAST, lockType);
}
/**
* Conversion of FIND-LAST() method (KW_FIND_LST).
* <p>
* Gets a single record. This method lets a user get the last record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findLast(character predicate, NumberType lock, NumberType nowait)
{
LockType lockType = fromLockNowait(lock.longValue(), nowait.longValue());
// toJavaType() returns null if predicate is unknown
return lockType == null ?
logical.FALSE :
_find(predicate.toJavaType(), FindMode.LAST, lockType);
}
/**
* Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
* <p>
* Gets a single record. This make sense only when there is only one record in the table.
* Otherwise, the error 3166 "More than one <tablename> records found by a unique FIND."
*
* @return <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
*/
public logical findUnique()
{
return _find(null, FindMode.UNIQUE, LockType.SHARE);
}
/**
* Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
* <p>
* Gets a single record. This method lets a user get a unique record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
*
* @return <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
*/
public logical findUnique(character predicate)
{
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.UNIQUE, LockType.SHARE);
}
/**
* Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
* <p>
* Gets a single record. This method lets a user get a unique record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lockType
* Lock type
*
* @return <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
*/
public logical findUnique(character predicate, LockType lockType)
{
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.UNIQUE, lockType);
}
/**
* Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
* <p>
* Gets a single record. This method lets a user get a unique record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
*
* @return <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
*/
public logical findUnique(character predicate, long lock)
{
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.UNIQUE, LockType.fromInt((int) lock));
}
/**
* Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
* <p>
* Gets a single record. This method lets a user get a unique record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
*
* @return <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
*/
public logical findUnique(character predicate, NumberType lock)
{
// toJavaType() returns null if predicate is unknown
return _find(predicate.toJavaType(), FindMode.UNIQUE, LockType.fromInt(lock.intValue()));
}
/**
* Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
* <p>
* Gets a single record. This method lets a user get a unique record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findUnique(character predicate, long lock, long nowait)
{
LockType lockType = fromLockNowait(lock, nowait);
// toJavaType() returns null if predicate is unknown
return lockType == null
? logical.FALSE
: _find(predicate.toJavaType(), FindMode.UNIQUE, lockType);
}
/**
* Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
* <p>
* Gets a single record. This method lets a user get a unique record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findUnique(character predicate, NumberType lock, long nowait)
{
LockType lockType = fromLockNowait(lock.longValue(), nowait);
// toJavaType() returns null if predicate is unknown
return lockType == null
? logical.FALSE
: _find(predicate.toJavaType(), FindMode.UNIQUE, lockType);
}
/**
* Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
* <p>
* Gets a single record. This method lets a user get a unique record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-UNIQUE() succeeds,
* else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findUnique(character predicate, long lock, NumberType nowait)
{
LockType lockType = fromLockNowait(lock, nowait.longValue());
// toJavaType() returns null if predicate is unknown
return lockType == null
? logical.FALSE
: _find(predicate.toJavaType(), FindMode.UNIQUE, lockType);
}
/**
* Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
* <p>
* Gets a single record. This method lets a user get a unique record that
* satisfies the predicate expression.
*
* @param predicate
* Predicate expression for find.
* @param lock
* SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
* @param nowait
* Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
*
* @return <code>true</code> if FIND-UNIQUE() succeeds,
* else <code>false</code>.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
public logical findUnique(character predicate, NumberType lock, NumberType nowait)
{
LockType lockType = fromLockNowait(lock.longValue(), nowait.longValue());
// toJavaType() returns null if predicate is unknown
return lockType == null ?
logical.FALSE :
_find(predicate.toJavaType(), FindMode.UNIQUE, lockType);
}
/**
* Conversion of the FIND-CURRENT() method.
* <p>
* Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED attribute.
*
* @return {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code false}.
*/
@LegacyMethod(name = "FIND-CURRENT")
public logical findCurrent()
{
return _current(LockType.SHARE);
}
/**
* Conversion of the FIND-CURRENT() method.
* <p>
* Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
* attribute.
*
* @param lockType
* Lock type.
*
* @return {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
* false}.
*/
@LegacyMethod(name = "FIND-CURRENT")
public logical findCurrent(LockType lockType)
{
if (lockType == null)
{
return logical.FALSE;
}
return _current(lockType);
}
/**
* Conversion of the FIND-CURRENT() method.
* <p>
* Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
* attribute.
*
* @param lock
* Lock type.
*
* @return {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
* false}.
*/
@LegacyMethod(name = "FIND-CURRENT")
public logical findCurrent(NumberType lock)
{
LockType lockType = fromLockNowait(lock.longValue(), 0);
return (lockType == null) ? logical.FALSE : _current(lockType);
}
/**
* Conversion of the FIND-CURRENT() method.
* <p>
* Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
* attribute.
*
* @param lock
* Lock type.
*
* @return {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
* false}.
*/
@LegacyMethod(name = "FIND-CURRENT")
public logical findCurrent(long lock)
{
LockType lockType = fromLockNowait(lock, 0);
return (lockType == null) ? logical.FALSE : _current(lockType);
}
/**
* Conversion of the FIND-CURRENT() method.
* <p>
* Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
* attribute.
*
* @param lockType
* Lock type.
* @param noWait
* Zero to lock without waiting; non-zero to wait to acquire the lock.
*
* @return {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
* false}.
*/
@LegacyMethod(name = "FIND-CURRENT")
public logical findCurrent(LockType lockType, long noWait)
{
if (lockType == null)
{
return logical.FALSE;
}
lockType = fromLockNowait(LockType.toInt(lockType), noWait);
return (lockType == null) ? logical.FALSE : _current(lockType);
}
/**
* Conversion of the FIND-CURRENT() method.
* <p>
* Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
* attribute.
*
* @param lock
* Lock type.
* @param noWait
* Zero to lock without waiting; non-zero to wait to acquire the lock.
*
* @return {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
* false}.
*/
@LegacyMethod(name = "FIND-CURRENT")
public logical findCurrent(NumberType lock, long noWait)
{
if (lock == null)
{
return logical.FALSE;
}
LockType lockType = fromLockNowait(lock.longValue(), noWait);
return (lockType == null) ? logical.FALSE : _current(lockType);
}
/**
* Conversion of the FIND-CURRENT() method.
* <p>
* Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
* attribute.
*
* @param lock
* Lock type.
* @param noWait
* Zero to lock without waiting; non-zero to wait to acquire the lock.
*
* @return {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
* false}.
*/
@LegacyMethod(name = "FIND-CURRENT")
public logical findCurrent(long lock, long noWait)
{
LockType lockType = fromLockNowait(lock, noWait);
return (lockType == null) ? logical.FALSE : _current(lockType);
}
/**
* Conversion of the FIND-CURRENT() method.
* <p>
* Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
* attribute.
*
* @param lockType
* Lock type.
* @param noWait
* Zero or unknown value to lock without waiting; non-zero to wait to acquire the
* lock.
*
* @return {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
* false}.
*/
@LegacyMethod(name = "FIND-CURRENT")
public logical findCurrent(LockType lockType, NumberType noWait)
{
if (lockType == null || noWait == null)
{
return logical.FALSE;
}
lockType = fromLockNowait(LockType.toInt(lockType), noWait.longValue());
return (lockType == null) ? logical.FALSE : _current(lockType);
}
/**
* Conversion of the FIND-CURRENT() method.
* <p>
* Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
* attribute.
*
* @param lock
* Lock type.
* @param noWait
* Zero or unknown value to lock without waiting; non-zero to wait to acquire the
* lock.
*
* @return {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
* false}.
*/
@LegacyMethod(name = "FIND-CURRENT")
public logical findCurrent(NumberType lock, NumberType noWait)
{
if (lock == null || noWait == null)
{
return logical.FALSE;
}
LockType lockType = fromLockNowait(lock.longValue(), noWait.longValue());
return (lockType == null) ? logical.FALSE : _current(lockType);
}
/**
* Conversion of the FIND-CURRENT() method.
* <p>
* Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
* attribute.
*
* @param lock
* Lock type.
* @param noWait
* Zero or unknown value to lock without waiting; non-zero to wait to acquire the
* lock.
*
* @return {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
* false}.
*/
@LegacyMethod(name = "FIND-CURRENT")
public logical findCurrent(long lock, NumberType noWait)
{
if (noWait == null)
{
return logical.FALSE;
}
LockType lockType = fromLockNowait(lock, noWait.longValue());
return (lockType == null) ? logical.FALSE : _current(lockType);
}
/**
* Conversion of NUM-FIELDS attribute (KW_NUM_FLD).
* <p>
* Getter of NUM-FIELDS attribute.
*
* @return current value of NUM-FIELDS attribute.
*/
public integer numFields()
{
return integer.of(_numFields());
}
/**
* Disable the triggers associated with the FIND event for the buffer's table.
*/
public void disableDumpTriggers()
{
final Class<?> dmoBuf = buffer().getDMOBufInterface();
if (Buffer.class.isAssignableFrom(dmoBuf))
{
DatabaseTriggerManager.disableDumpTriggers((Class<? extends Buffer>) dmoBuf);
}
}
/**
* Disable the triggers associated with the CREATE, WRITE, DELETE and ASSIGN events for the
* buffer's table.
*
* @param allowReplication
* If <code>true</code> then only CREATE, DELETE, ASSIGN and WRITE triggers are
* disabled, and not REPLICATION-CREATE, REPLICATION-DELETE and REPLICATION-WRITE
* triggers.
*/
public void disableLoadTriggers(boolean allowReplication)
{
final Class<?> dmoBuf = buffer().getDMOBufInterface();
if (Buffer.class.isAssignableFrom(dmoBuf))
{
DatabaseTriggerManager.disableLoadTriggers((Class<? extends Buffer>) dmoBuf, allowReplication);
}
}
/**
* Conversion of CURRENT-ITERATION attribute (KW_CUR_ITER).
* <p>
* Getter of CURRENT-ITERATION attribute.
*
* @return Current value of CURRENT-ITERATION attribute.
*/
public handle currentIteration()
{
// TODO: implement, returning unknown values for the moment
return new handle();
// TODO: this method should return an integer instead
// if (dataSet == null)
// {
// return new integer();
// }
// else compute iteration
}
/**
* Implements the GET-ITERATION() widget or buffer method.
*
* @param level
* The level of the iteration to get.
*
* @return The handle of the iteration, field-group or buffer.
*/
@Override
public handle getIteration(NumberType level)
{
return level == null || level.isUnknown() ? new handle() : getIteration(level.intValue());
}
/**
* Implements the GET-ITERATION() buffer method.
*
* @param level
* The level of the iteration to get. Record number.
*
* @return The handle of the iteration, field-group or buffer.
*/
@Override
public handle getIteration(int level)
{
// TODO: Implement me
return new handle();
}
/**
* Sets the ERROR-STRING attribute of 4GL temp-table.
*
* @param errStr The new value for the ERROR-STRING attribute.
*/
@Override
public void changeErrorString(character errStr)
{
RecordBuffer buf = buffer();
if (!buf.isTemporary())
{
// permanent table case
ErrorManager.recordOrShowError(11917, doGetName());
// Cannot set ERROR if there is no BEFORE-TABLE for <name>.
ErrorManager.recordOrShowError(3131, "ERROR-STRING", "", LegacyResource.BUFFER);
// Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
return;
}
if (!_available())
{
// temp-tables case
ErrorManager.recordOrShowError(11897, doGetName());
// <name> record is not available to set ERROR in.
ErrorManager.recordOrShowError(3131, "ERROR-STRING", "", LegacyResource.BUFFER);
// Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
return;
}
if (!isBeforeBuffer())
{
if (!isAfterBuffer() || // SIMPLE BUFFER
(buf.peerRowid() == null || buf.peerRowid().isUnknown())) // no AFTER-IMAGE
{
ErrorManager.recordOrShowError(11918, doGetName(), "");
// Cannot set ERROR if there is no BEFORE-TABLE row for <name>.
return;
}
}
buffer().errorString(errStr);
}
/**
* Obtain the current ERROR-STRING attribute of 4GL temp-table.
*
* @return the current ERROR-STRING of the temp-table
*/
@Override
public character errorString()
{
RecordBuffer recBuf = buffer();
if (!recBuf.isAvailable() || !recBuf.isTemporary())
{
return new character();
}
DataSet ds = _dataSet();
if (ds == null)
{
return new character();
}
return recBuf.errorString();
}
/**
* Returns the state of the ERROR attribute
*
* @return Current state of the ERROR attribute.
*/
@Override
public logical error()
{
RecordBuffer recBuf = buffer();
if (!recBuf.isAvailable() || !recBuf.isTemporary())
{
return logical.FALSE;
}
DataSet ds = _dataSet();
if (ds == null)
{
return logical.FALSE;
}
// NOTE: internally, the P4GL database the __error-flag__ is an integer storing multiple flags.
// We need to extract the right bit (ERROR_ERROR) and convert it to a logical value.
Integer flags = recBuf.errorFlags();
return logical.of(flags != null && (flags & Buffer.ERROR_ERROR) != 0);
}
/**
* Setter for {@code ERROR} attribute.<br>
* The {@code ERROR} attribute is writable for a {@code Buffer} object. The attribute can be set
* programmatically in order to manage the application response to errors.<br>
* If set to {@code FALSE}, the application will ignore all errors that might be returned for a
* statement using the {@code NO-ERROR} option.<br>
* However, manually setting this attribute won't raise the {@code ERROR} condition.
*
* @param value
* The new value of {@code ERROR} attribute.
*/
@Override
public void error(boolean value)
{
RecordBuffer recBuf = buffer();
if (!_available())
{
// temp-tables case
ErrorManager.recordOrShowError(11916, doGetName());
// <name> record is not available to set ERROR in.
ErrorManager.recordOrShowError(3131, "ERROR", "", LegacyResource.BUFFER);
// Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
return;
}
if (!recBuf.isTemporary())
{
// permanent table case
ErrorManager.recordOrShowError(11917, doGetName());
// Cannot set ERROR if there is no BEFORE-TABLE for <name>.
ErrorManager.recordOrShowError(3131, "ERROR", "", LegacyResource.BUFFER);
// Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
return;
}
if (!isBeforeBuffer())
{
if (!isAfterBuffer() || // SIMPLE BUFFER
(recBuf.peerRowid() == null || recBuf.peerRowid().isUnknown())) // no AFTER-IMAGE
{
ErrorManager.recordOrShowError(11918, doGetName(), "");
// Cannot set ERROR if there is no BEFORE-TABLE row for <name>.
return;
}
}
// NOTE: internally, the P4GL database the __error-flag__ is an integer storing multiple flags.
// We need to update the right bit (ERROR_ERROR) to new state (value).
recBuf.updateErrorFlags(Buffer.ERROR_ERROR, value);
}
/**
* Setter for ERROR attribute.
*
* @param value
* The new value of ERROR attribute.
*/
@Override
public void error(logical value)
{
error(value.booleanValue());
}
/**
* Returns the state of the DBNAME attribute
*
* @return Current value of the DBNAME attribute.
*/
public character getDbName()
{
final RecordBuffer buf = buffer();
if (buf.getParentTable() != null)
{
// in the case of temp-tables (defined statically or dynamically)
return new character("PROGRESST");
}
// permanent tables, it works as expected (the logical name of the database of the buffer)
return new character(buf.getLogicalDatabase());
}
/**
* Returns the state of the TABLE attribute
*
* @return Current value of the TABLE attribute.
*/
@Override
public character getTable()
{
if (buffer().isTemporary())
{
AbstractTempTable table = (AbstractTempTable) tableHandleNative();
return table.name();
}
else
{
return new character(buffer().getDmoInfo().legacyTable);
}
}
/**
* Conversion of TABLE-HANDLE attribute (KW_TAB_HAND).
* <p>
* Gets the handle of a temp-table object, if any, associated with the buffer object.
*
* @return the handle of a temp-table object.
*/
public handle tableHandle()
{
return buffer().tableHandle();
}
/**
* Get the TEMP-TABLE resource associated with this buffer.
*
* @return See above.
*/
public TempTable tableHandleNative()
{
return buffer().tableHandleResource();
}
/**
* Conversion of INDEX-INFORMATION method. Returns information about the first (according to
* the definition order) table index, as a string.
*
* @return Information about the first (according to definition order) index, as a string. The
* returned comma-separated list consists of the following in the specified order:
* <ul>
* <li>the index name;
* <li>three integers of value 0 (false) or 1 (true) depending on whether the index is
* unique, primary or a word index;
* <li>the names of the index fields, each followed by a 0 (ascending) or 1
* (descending).
* </ul>
* An example: "index-one,1,1,0,field-one,0".
*/
public character indexInformation()
{
return indexInformation(new integer(1));
}
/**
* Conversion of INDEX-INFORMATION method. Returns information about the specified table index,
* as a string.
*
* @param indexNum
* 1-based number of the index in the definition order.
*
* @return Information about the specified index, as a string. The returned comma-separated
* list consists of the following in the specified order:
* <ul>
* <li>the index name;
* <li>three integers of value 0 (false) or 1 (true) depending on whether the index is
* unique, primary or a word index;
* <li>the names of the index fields, each followed by a 0 (ascending) or 1
* (descending).
* </ul>
* An example: "index-one,1,1,0,field-one,0".
*/
public character indexInformation(int indexNum)
{
return indexInformation(new integer(indexNum));
}
/**
* Conversion of INDEX-INFORMATION method. Returns information about the specified table index,
* as a string.
*
* @param indexNum
* 1-based number of the index in the definition order.
*
* @return Information about the specified index, as a string. The returned comma-separated
* list consists of the following in the specified order:
* <ul>
* <li>the index name;
* <li>three integers of value 0 (false) or 1 (true) depending on whether the index is
* unique, primary or a word index;
* <li>the names of the index fields, each followed by a 0 (ascending) or 1
* (descending).
* </ul>
* An example: "index-one,1,1,0,field-one,0".
*/
public character indexInformation(NumberType indexNum)
{
if (indexNum.isUnknown())
{
return new character();
}
return new character(TableMapper.getLegacyIndexInfoString(this.buffer(), indexNum.intValue() - 1));
}
/**
* Conversion of DYNAMIC attribute. Returns <code>true</code> if the buffer is dynamic.
*
* @return <code>true</code> if the buffer is dynamic.
*/
@Override
public boolean _dynamic()
{
return buffer().isDynamic();
}
/**
* Conversion of DYNAMIC attribute. Returns <code>true</code> if the buffer is dynamic.
*
* @return <code>true</code> if the buffer is dynamic.
*/
@Override
public logical dynamic()
{
return logical.of(_dynamic());
}
/**
* Get the value of the ADM-DATA attribute.
*
* @return See above.
*/
@Override
public character getADMData()
{
return new character(this.admData);
}
/**
* Set the value of the ADM-DATA attribute.
*
* @param value
* The new value.
*/
@Override
public void setADMData(String value)
{
this.admData = value;
}
/**
* Set the value of the ADM-DATA attribute.
*
* @param value
* The new value.
*/
@Override
public void setADMData(character value)
{
this.admData = value.toJavaType();
}
/**
* Gets the unique ID number associated to this object by the underlying system. Not the
* same as the handle value itself.
*
* @return An integer representing an unique id between all buffers ever created in the
* current process.
*/
@Override
public integer getUniqueID()
{
if (uniqueID == null)
{
uniqueID = UniqueIdGenerator.getUniqueId(IdKind.BUFFER);
}
return new integer(this.uniqueID);
}
/**
* Returns the value of the NAMESPACE-URI attribute. Only for ELEMENT or ATTRIBUTE,
* for other attributes return unknown value.
*
* @return See above.
*/
@Override
public character namespaceURI()
{
return character.of(namespaceURI);
}
/**
* Setter for NAMESPACE-URI attribute.
*
* @param uri
* The new value of the NAMESPACE-URI attribute.
*/
@Override
public void namespaceURI(String uri)
{
if (uri == null)
{
ErrorManager.recordOrShowError(4083, "NAMESPACE-URI", LegacyResource.BUFFER + " widget");
return;
}
namespaceURI = uri;
}
/**
* Setter for NAMESPACE-URI attribute.
*
* @param uri
* The new value of the NAMESPACE-URI attribute.
*/
@Override
public void namespaceURI(character uri)
{
namespaceURI(uri.toJavaType());
}
/**
* Returns the value of the NAMESPACE-PREFIX attribute. Only for ELEMENT or ATTRIBUTE,
* for other attributes return unknown value.
*
* @return See above.
*/
@Override
public character namespacePrefix()
{
return character.of(namespacePrefix);
}
/**
* Setter for NAMESPACE-PREFIX attribute.
*
* @param prefix
* The new value of the NAMESPACE-PREFIX attribute.
*/
@Override
public void namespacePrefix(String prefix)
{
if (prefix == null)
{
ErrorManager.recordOrShowError(4083, "NAMESPACE-PREFIX", LegacyResource.BUFFER + " widget");
return;
}
namespacePrefix = prefix;
}
/**
* Setter for NAMESPACE-PREFIX attribute.
*
* @param prefix
* The new value of the NAMESPACE-PREFIX attribute.
*/
@Override
public void namespacePrefix(character prefix)
{
namespacePrefix(prefix.toJavaType());
}
/**
* Implementation of the read access of the {@code LABEL}.
*
* @return The current value of the {@code LABEL}.
*/
public character label()
{
ErrorManager.recordOrShowError(4052, "LABEL", "queryable", LegacyResource.BUFFER + " widget");
return character.UNKNOWN;
}
/**
* Implementation of the write access of the {@code LABEL}.
*
* @param sName
* the new value for the {@code LABEL}.
*/
public void label(Text sName)
{
ErrorManager.recordOrShowError(4052, "LABEL", "setable" /*sic*/, LegacyResource.BUFFER + " widget");
}
/**
* Implementation of the write access of the {@code LABEL}.
*
* @param sName
* The new value for the {@code LABEL}.
*/
public void label(String sName)
{
ErrorManager.recordOrShowError(4052, "LABEL", "setable" /*sic*/, LegacyResource.BUFFER + " widget");
}
/**
* Implementation of the read access of the {@code SERIALIZE-NAME} attribute.
*
* @return the current value of the {@code SERIALIZE-NAME} attribute.
*/
public character getSerializeName()
{
DmoMeta dmoInfo = buffer().getDmoInfo();
return serializeName == null || serializeName.trim().isEmpty() ? new character(dmoInfo.legacyTable) :
new character(serializeName);
}
/**
* Implementation of the write access of the {@code SERIALIZE-NAME} attribute.
*
* @param sName
* the new value for the {@code SERIALIZE-NAME} attribute.
*/
public void setSerializeName(Text sName)
{
setSerializeName(sName.toJavaType());
}
/**
* Implementation of the write access of the {@code SERIALIZE-NAME} attribute.
*
* @param sName
* the new value for the {@code SERIALIZE-NAME} attribute.
*/
public void setSerializeName(String sName)
{
serializeName = sName;
}
/**
* Implementation of the read access of the {@code XML-NODE-NAME} attribute.
*
* @return the current value of the {@code XML-NODE-NAME} attribute.
*/
public character getXmlNodeName()
{
return xmlNodeName == null || xmlNodeName.trim().isEmpty() ? getSerializeName() :
new character(xmlNodeName);
}
/**
* Implementation of the write access of the {@code XML-NODE-NAME} attribute.
*
* @param sName
* the new value for the {@code XML-NODE-NAME} attribute.
*/
public void setXmlNodeName(Text sName)
{
setXmlNodeName(sName.toJavaType());
}
/**
* Implementation of the write access of the {@code XML-NODE-NAME} attribute.
*
* @param sName
* the new value for the {@code XML-NODE-NAME} attribute.
*/
public void setXmlNodeName(String sName)
{
xmlNodeName = sName;
}
/**
* Obtain the XML Schema data type for the buffer-field object.
*
* @return the XML Schema data type for the buffer-field object.
*/
@Override
public character getXmlDataType()
{
handle.invalidAttribute("XML-DATA-TYPE", false, LegacyResource.BUFFER);
return new character();
}
/**
* Get the BUFFER:AUTO-SYNCHRONIZE attribute.
*
* @return the value of BUFFER:AUTO-SYNCHRONIZE attribute.
*/
@Override
public logical autoSynchronize()
{
if (dataSet == null)
{
return logical.UNKNOWN;
}
return logical.of(autoSynchronize);
}
/**
* Set the BUFFER:AUTO-SYNCHRONIZE attribute.
*/
@Override
public void autoSynchronize(boolean autoSyncronize)
{
if (dataSet == null)
{
return;
}
this.autoSynchronize = autoSyncronize;
}
/**
* Set the BUFFER:AUTO-SYNCHRONIZE attribute.
*/
@Override
public void autoSynchronize(logical autoSyncronize)
{
if (autoSyncronize == null || autoSyncronize.isUnknown())
{
ErrorManager.recordOrShowError(4083, "AUTO-SYNCHRONIZE", "BUFFER widget");
return;
}
autoSynchronize(autoSyncronize.booleanValue());
}
/**
* Obtain the CAN-READ data type for the buffer-field object.
*
* @return the CAN-READ data type for the buffer-field object.
*/
@Override
public logical canRead()
{
UnimplementedFeature.missing("BUFFER:CAN-READ");
return logical.UNKNOWN;
}
/**
* Implementation of the NUM-ITERATIONS getter.
*
* @return The current value of the NUM-ITERATIONS attribute.
*/
@Override
public integer numIterations()
{
UnimplementedFeature.missing("BUFFER:NUM-ITERATIONS");
return new integer();
}
/**
* Implementation of the NUM-REFERENCES getter.
*
* @return The current value of the NUM-REFERENCES attribute.
*/
@Override
public integer numReferences()
{
UnimplementedFeature.missing("BUFFER:NUM-REFERENCES");
return new integer();
}
/**
* Configures the XML Schema data type for the buffer-field object.
*
* @param newType the new XML Schema data type for the buffer-field object.
*/
@Override
public void setXmlDataType(String newType)
{
handle.invalidAttribute("XML-DATA-TYPE", true, LegacyResource.BUFFER);
}
/**
* Configures the XML Schema data type for the buffer-field object.
*
* @param newType the new XML Schema data type for the buffer-field object.
*/
@Override
public void setXmlDataType(Text newType)
{
handle.invalidAttribute("XML-DATA-TYPE", true, LegacyResource.BUFFER);
}
/**
* Obtain the XML node type of this object. This represents the getter for {@code XML-NODE-TYPE} ABL
* attribute.
*
* @return the XML node type of this object.
*/
@Override
public character getXmlNodeType()
{
handle.invalidAttribute("XML-NODE-TYPE", false, LegacyResource.BUFFER);
return new character();
}
/**
* Allows specifying how this object will be represented in XML and XML Schema. Valid XML node types are
* "ATTRIBUTE", "ELEMENT", "HIDDEN", and "TEXT". This represents the setter for {@code XML-NODE-TYPE} ABL
* attribute.
*
* @param newType
* The new type.
*/
@Override
public void setXmlNodeType(String newType)
{
handle.invalidAttribute("XML-NODE-TYPE", true, LegacyResource.BUFFER);
}
/**
* Allows specifying how this object will be represented in XML and XML Schema. Valid XML node types are
* "ATTRIBUTE", "ELEMENT", "HIDDEN", and "TEXT". This represents the setter for {@code XML-NODE-TYPE} ABL
* attribute.
*
* @param newType
* The new type.
*/
@Override
public void setXmlNodeType(Text newType)
{
handle.invalidAttribute("XML-NODE-TYPE", true, LegacyResource.BUFFER);
}
/**
* Obtain the list of key fields of the buffer as a comma-separated list.
*
* @return the list of key fields of the buffer. If the buffer is not part of a datasource, {@code rowid}
* is returned.
*/
@Override
public character keys()
{
if (parentDataSource == null || !parentDataSource.valid() || keys == null)
{
P2JIndex primaryIndex = buffer().getDmoInfo().getPrimaryIndex(true);
if (primaryIndex == null || !primaryIndex.isUnique())
{
return new character(DataSource.ROWID_KEY);
}
StringBuilder sb = new StringBuilder();
ArrayList<P2JIndexComponent> comps = primaryIndex.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent comp = comps.get(i);
if (sb.length() != 0)
{
sb.append(",");
}
sb.append(comp.getLegacyName());
}
return new character(sb.toString());
}
StringBuilder sb = new StringBuilder();
for (String key : keys)
{
if (Session.PK.equals(key))
{
continue;
}
if (sb.length() != 0)
{
sb.append(",");
}
sb.append(key);
}
return new character(sb.toString());
}
/**
* Obtain the list of key fields of the buffer as a comma-separated list.
*
* @param seqNum
* Ignored.
*
* @return the list of key fields of the buffer.
*/
@Override
public character keys(long seqNum)
{
return keys();
}
/**
* Obtain the list of key fields of the buffer as a comma-separated list.
*
* @param seqNum
* Ignored.
*
* @return the list of key fields of the buffer.
*/
@Override
public character keys(NumberType seqNum)
{
return keys();
}
/**
* Set the CURRENT-ITERATION widget handle for this frame. This attribute is writable only for frames.
*
* @param newIteration
* Ignored.
*/
@Override
public void setCurrentIteration(handle newIteration)
{
if (newIteration == null || newIteration.isUnknown())
{
ErrorManager.recordOrShowError(4083, "CURRENT-ITERATION", "BUFFER widget");
}
else
{
handle.invalidAttribute("CURRENT-ITERATION", true, LegacyResource.BUFFER);
}
}
/**
* Perform actual delete of an resource. At the time of this call, it is assumed the resource
* is valid for deletion (the handle and the resource are both valid).
* The method is overridden only to allow this object to maintain the structure of the chain of
* BUFFERs (to update the PERMANENT-TABLE pointer if it was pointing to this buffer AND if the
* {@link HandleChain#delete()} really disconnected it from the chain)
*/
@Override
public void delete()
{
// save next sibling because super.delete() will wipe it out
HandleChain nextResource = this.nextSibling;
super.delete();
if (this.nextSibling != null)
{
// the super.delete() did not removed this object from chain
return;
}
// special care for BUFFER resources: update the PERMANENT-TABLE pointer
if (this == HandleChain.getHead(PERMANENT_TABLE))
{
HandleChain.setHead(PERMANENT_TABLE, nextResource);
}
}
/**
* Conversion of BUFFER-FIELD() method (KW_BUF_FLD).
* <p>
* Gets the handle to a particular buffer of a query or DataSet object.
*
* @param fieldNumber
* field number. An INTEGER expression representing the sequence
* number of the field in the buffer.
*
* @return handle to a particular field in the buffer.
*/
@Override
public handle bufferField(NumberType fieldNumber)
{
if (fieldNumber == null || fieldNumber.isUnknown())
{
String msg = "Argument for BUFFER-FIELD method for buffer " +
buffer().getDmoInfo().legacyTable + " could not be evaluated";
// untested at this time for 'database behavior' related to throwing structured exceptions
// - see recordOrShowDatabaseError
ErrorManager.recordOrShowError(7349, msg, false, false, false);
return new handle();
}
return bufferField(fieldNumber.intValue());
}
/**
* Conversion of BUFFER-FIELD() method (KW_BUF_FLD).
* <p>
* Gets the handle to a particular buffer of a query or DataSet object.
*
* @param fieldName
* A CHARACTER string expression representing the name of the field in the buffer.
*
* @return handle to a particular field in the buffer.
*/
@Override
public handle bufferField(character fieldName)
{
if (fieldName.isUnknown())
{
String msg = "Argument for BUFFER-FIELD method for buffer " +
buffer().getDmoInfo().legacyTable + " could not be evaluated";
// untested at this time for 'database behavior' related to throwing structured exceptions
// - see recordOrShowDatabaseError
ErrorManager.recordOrShowError(7349, msg, false, false, false);
// TODO: if this message needs to be displayed, the code must be moved in other place
// String errMsg2 =
// "Lead attributes in a chained-attribute expression (a:b:c) must be type " +
// "HANDLE or a user-defined type and valid (not UNKNOWN)";
// ErrorManager.recordOrShowError(10068, errMsg2, false, false, false);
return new handle();
}
else
{
return bufferField(fieldName.getValue());
}
}
/**
* Conversion of BUFFER-FIELD() method (KW_BUF_FLD).
* <p>
* Gets the handle to a particular buffer of a query or DataSet object.
*
* @param fieldName
* A CHARACTER string expression representing the name of the field in the buffer.
*
* @return handle to a particular field in the buffer.
*/
@Override
public handle bufferField(BaseDataType fieldName)
{
if (Text.class.isAssignableFrom(fieldName.getClass()))
{
return bufferField(fieldName.toStringMessage());
}
if (NumberType.class.isAssignableFrom(fieldName.getClass()))
{
return bufferField((NumberType) fieldName);
}
if (BinaryData.class.isAssignableFrom(fieldName.getClass()))
{
BinaryData bd = (BinaryData) fieldName;
character nm = bd.getString(0);
return bufferField(nm);
}
// otherwise, it uses the 'hex representation' of the value to compute a read a string.
// we don't emulate this at this time.
UnimplementedFeature.missing("BUFFER-POLY(incompatible-BDT) not implemented!");
String msg = "BUFFER-FIELD " + fieldName + " was not found in buffer " +
buffer().getDmoInfo().legacyTable;
ErrorManager.recordOrShowDatabaseError(7351, msg, false, false);
// return unknown handle
return new handle();
}
/**
* Implementation of the READ-XML method. Reads XML data from the specified data source into
* this object.
* <p>
* Defaults applied:
* <ul>
* <li>Default field type mappings</li>
* <li>LOOSE schema verification mode</li>
* </ul>
*
* @param source
* Data source.
* @param readMode
* Mode in which data is read into buffer. Must evaluate to {@code APPEND}, {@code
* EMPTY}, {@code MERGE}, or {@code REPLACE}.
* @param schemaLocation
* Name of an external XML schema file, or empty string or unknown value to use the
* {@code xsi:noNamespaceSchemaLocation} value set in the XML data, if any.
* @param overrideDefaultMapping
* {@code True} to override the default string-to-binary data type mappings when
* creating a temp-table schema from an XML schema.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
public logical readXml(SourceData source,
character readMode,
character schemaLocation,
logical overrideDefaultMapping)
{
return readXml(source, readMode, schemaLocation, overrideDefaultMapping, new character());
}
/**
* Implementation of the READ-XML method. Reads XML data from the specified data source into
* this object.
* <p>
* Defaults applied:
* <ul>
* <li>LOOSE schema verification mode</li>
* </ul>
*
* @param source
* Data source.
* @param readMode
* Mode in which data is read into buffer. Must evaluate to {@code APPEND}, {@code
* EMPTY}, {@code MERGE}, or {@code REPLACE}.
* @param schemaLocation
* Name of an external XML schema file, or empty string or unknown value to use the
* {@code xsi:noNamespaceSchemaLocation} value set in the XML data, if any.
* @param overrideDefaultMapping
* {@code True} to override the default string-to-binary data type mappings when
* creating a temp-table schema from an XML schema.
* @param fieldTypeMapping
* Comma-delimited list of field name and data type name pairs to use to override
* data type mappings of specific fields. Specify unknown value to use default
* mappings.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
public logical readXml(SourceData source,
character readMode,
character schemaLocation,
logical overrideDefaultMapping,
character fieldTypeMapping)
{
return readXml(source,
readMode,
schemaLocation,
overrideDefaultMapping,
fieldTypeMapping,
new character("LOOSE"));
}
/**
* Implementation of the READ-XML method. Reads XML data from the specified data source into
* this object.
*
* @param source
* Data source.
* @param readMode
* Mode in which data is read into buffer. Must evaluate to {@code APPEND}, {@code
* EMPTY}, {@code MERGE}, or {@code REPLACE}.
* @param schemaLocation
* Name of an external XML schema file, or empty string or unknown value to use the
* {@code xsi:noNamespaceSchemaLocation} value set in the XML data, if any.
* @param overrideDefaultMapping
* {@code True} to override the default string-to-binary data type mappings when
* creating a temp-table schema from an XML schema.
* @param fieldTypeMapping
* Comma-delimited list of field name and data type name pairs to use to override
* data type mappings of specific fields. Specify unknown value to use default
* mappings.
* @param verifySchemaMode
* Schema verification mode. Must evaluate to {@code IGNORE}, {@code LOOSE}, or
* {@code STRICT}. Default is {@code LOOSE}.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
public logical readXml(SourceData source,
character readMode,
character schemaLocation,
logical overrideDefaultMapping,
character fieldTypeMapping,
character verifySchemaMode)
{
RecordBuffer buffer = buffer();
// sanity
if (!buffer.isTemporary())
{
ErrorManager.recordOrShowError(13158);
// XML Methods only allowed on TEMP-TABLE buffers.
return logical.FALSE;
}
Map<String, String> mapping = null;
if (fieldTypeMapping != null)
{
String javaStrMapping = fieldTypeMapping.toJavaType();
if (javaStrMapping != null)
{
mapping = new HashMap<>();
String[] splits = javaStrMapping.split(",");
int len = splits.length;
if (len == 0 || len % 2 == 1)
{
ErrorManager.recordOrShowError(13200);
// Odd number of values in field-type-mapping argument.
return logical.of(false);
}
// no other validation at this time
for (int k = 0; k < len; k += 2)
{
mapping.putIfAbsent(splits[k], splits[k + 1]); // do not overwrite if field is duplicated
}
}
}
// normalize parameters
XmlImport xmlImport = new XmlImport(source,
readMode.toJavaType(),
schemaLocation.toJavaType(),
!overrideDefaultMapping.isUnknown() &&
overrideDefaultMapping.booleanValue(),
mapping,
verifySchemaMode.toJavaType());
// perform import
return logical.of(xmlImport.importTable(this, RecordBuffer::loadRecord));
}
/**
* Implementation of the WRITE-XML method. Writes XML data from this object into the specified
* data target.
* <p>
* Defaults applied:
* <ul>
* <li>Unformatted text</li>
* <li>UTF-8 encoding</li>
* <li>xsi:noNamespaceSchemaLocation=?</li>
* <li>No XML schema written</li>
* <li>No before-image data written</li>
* <li>Do not omit temp-table fields which contain their initial values</li>
* </ul>
*
* @param target
* Data target.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
public logical writeXml(TargetData target)
{
return writeXml(target, null, null, null, null, null, null, null);
}
/**
* Implementation of the WRITE-XML method. Writes XML data from this object into the specified
* data target.
* <p>
* Defaults applied:
* <ul>
* <li>UTF-8 encoding</li>
* <li>xsi:noNamespaceSchemaLocation=?</li>
* <li>No XML schema written</li>
* <li>No before-image data written</li>
* <li>Do not omit temp-table fields which contain their initial values</li>
* </ul>
*
* @param target
* Data target.
* @param formatted
* {@code True} to format the XML to make it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
public logical writeXml(TargetData target, logical formatted)
{
return writeXml(target, formatted, null, null, null, null, null, null);
}
/**
* Implementation of the WRITE-XML method. Writes XML data from this object into the specified
* data target.
* <p>
* Defaults applied:
* <ul>
* <li>xsi:noNamespaceSchemaLocation=?</li>
* <li>No XML schema written</li>
* <li>No before-image data written</li>
* <li>Do not omit temp-table fields which contain their initial values</li>
* </ul>
*
* @param target
* Data target.
* @param formatted
* {@code True} to format the XML to make it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
* @param encoding
* Name of the character encoding to use.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
public logical writeXml(TargetData target, logical formatted, character encoding)
{
return writeXml(target, formatted, encoding, null, null, null, null, null);
}
/**
* Implementation of the WRITE-XML method. Writes XML data from this object into the specified
* data target.
* <p>
* Defaults applied:
* <ul>
* <li>No XML schema written</li>
* <li>No before-image data written</li>
* <li>Do not omit temp-table fields which contain their initial values</li>
* </ul>
*
* @param target
* Data target.
* @param formatted
* {@code True} to format the XML to make it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
* @param encoding
* Name of the character encoding to use.
* @param schemaLocation
* Name of an external XML schema file, or empty string or unknown value to set the
* {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
public logical writeXml(TargetData target, logical formatted, character encoding, character schemaLocation)
{
return writeXml(target, formatted, encoding, schemaLocation, null, null, null, null);
}
/**
* Implementation of the WRITE-XML method. Writes XML data from this object into the specified
* data target.
* <p>
* Defaults applied:
* <ul>
* <li>No before-image data written</li>
* <li>Do not omit temp-table fields which contain their initial values</li>
* </ul>
*
* @param target
* Data target.
* @param formatted
* {@code True} to format the XML to make it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
* @param encoding
* Name of the character encoding to use.
* @param schemaLocation
* Name of an external XML schema file, or empty string or unknown value to set the
* {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
* @param writeXmlSchema
* {@code True} to write XML schema information, else {@code false}.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
public logical writeXml(TargetData target,
logical formatted,
character encoding,
character schemaLocation,
logical writeXmlSchema)
{
return writeXml(target, formatted, encoding, schemaLocation, writeXmlSchema, null, null, null);
}
/**
* Implementation of the WRITE-XML method. Writes XML data from this object into the specified
* data target.
* <p>
* Defaults applied:
* <ul>
* <li>No before-image data written</li>
* <li>Do not omit temp-table fields which contain their initial values</li>
* </ul>
*
* @param target
* Data target.
* @param formatted
* {@code True} to format the XML to make it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
* @param encoding
* Name of the character encoding to use.
* @param schemaLocation
* Name of an external XML schema file, or empty string or unknown value to set the
* {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
* @param writeXmlSchema
* {@code True} to write XML schema information, else {@code false}.
* @param minXmlSchema
* {@code True} to write minimal XML schema information, else {@code false}. Must be
* {@code false} if {@code writeXmlSchema} is {@code false}.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
public logical writeXml(TargetData target,
logical formatted,
character encoding,
character schemaLocation,
logical writeXmlSchema,
logical minXmlSchema)
{
return writeXml(target, formatted, encoding, schemaLocation, writeXmlSchema, minXmlSchema, null, null);
}
/**
* Implementation of the WRITE-XML method. Writes XML data from this object into the specified
* data target.
* <p>
* Defaults applied:
* <ul>
* <li>Do not omit temp-table fields which contain their initial values</li>
* </ul>
*
* @param target
* Data target.
* @param formatted
* {@code True} to format the XML to make it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
* @param encoding
* Name of the character encoding to use.
* @param schemaLocation
* Name of an external XML schema file, or empty string or unknown value to set the
* {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
* @param writeXmlSchema
* {@code True} to write XML schema information, else {@code false}.
* @param minXmlSchema
* {@code True} to write minimal XML schema information, else {@code false}. Must be
* {@code false} if {@code writeXmlSchema} is {@code false}.
* @param writeBeforeImage
* {@code True} to write before-image data and error information, else {@code false}.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
public logical writeXml(TargetData target,
logical formatted,
character encoding,
character schemaLocation,
logical writeXmlSchema,
logical minXmlSchema,
logical writeBeforeImage)
{
return writeXml(target,
formatted,
encoding,
schemaLocation,
writeXmlSchema,
minXmlSchema,
writeBeforeImage,
null);
}
/**
* Implementation of the WRITE-XML method. Writes XML data from this object into the specified
* data target.
*
* @param target
* Data target.
* @param formatted
* {@code True} to format the XML to make it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
* @param encoding
* Name of the character encoding to use.
* @param schemaLocation
* Name of an external XML schema file, or empty string or unknown value to set the
* {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
* @param writeXmlSchema
* {@code True} to write XML schema information, else {@code false}.
* @param minXmlSchema
* {@code True} to write minimal XML schema information, else {@code false}. Must be
* {@code false} if {@code writeXmlSchema} is {@code false}.
* @param beforeImage
* {@code True} to write before-image data and error information, else {@code false}.
* @param noInitialVals
* {@code True} to omit data for fields whose values match their initial values.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
public logical writeXml(TargetData target,
logical formatted,
character encoding,
character schemaLocation,
logical writeXmlSchema,
logical minXmlSchema,
logical beforeImage,
logical noInitialVals)
{
RecordBuffer buffer = buffer();
boolean ret;
try
{
// sanity
if (!buffer.isTemporary())
{
ErrorManager.recordOrShowError(13158);
// XML Methods only allowed on TEMP-TABLE buffers.
return logical.FALSE;
}
if (!buffer.getParentTable().flushBuffers())
{
return logical.FALSE;
}
XmlExport export = new XmlExport(
target,
formatted != null && !formatted.isUnknown() && formatted.getValue(),
encoding == null ? null : encoding.toJavaType());
ret = export.writeTempTable(
(TemporaryBuffer) buffer,
schemaLocation == null ? null : schemaLocation.toJavaType(),
writeXmlSchema != null && !writeXmlSchema.isUnknown() && writeXmlSchema.getValue(),
minXmlSchema != null && !minXmlSchema.isUnknown() && minXmlSchema.getValue(),
beforeImage != null && !beforeImage.isUnknown() && beforeImage.getValue(),
noInitialVals != null && !noInitialVals.isUnknown() && noInitialVals.getValue());
buffer.release(false);
}
catch (XmlExport.Fatal e) // Emergency exit - for the 4GL compatibility
{
// Error was already reported, just stop;
ret = false;
}
finally
{
try
{
target.close();
}
catch (IOException e)
{
// TODO: log
ret = false;
}
}
return logical.of(ret);
}
/**
* Writes an XML representation of the definition of this {@code TempTable}.
*
* @param target
* Data target.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical writeXmlSchema(TargetData target)
{
return writeXmlSchema(target, null, null, null, null);
}
/**
* Writes an XML representation of the definition of this {@code TempTable}.
*
* @param target
* Data target.
* @param formatted
* Use {@code true} for a more human-readable document.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical writeXmlSchema(TargetData target, logical formatted)
{
return writeXmlSchema(target, formatted, null, null, null);
}
/**
* Writes an XML representation of the definition of this {@code TempTable}.
*
* @param target
* Data target.
* @param formatted
* Use {@code true} for a more human-readable document.
* @param encoding
* The character encoding. The default is UTF-8.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical writeXmlSchema(TargetData target, logical formatted, Text encoding)
{
return writeXmlSchema(target, formatted, encoding, null, null);
}
/**
* Writes an XML representation of the definition of this {@code TempTable}.
*
* @param target
* Data target.
* @param formatted
* Use {@code true} for a more human-readable document.
* @param encoding
* The character encoding. The default is UTF-8.
* @param minXmlSchema
* If {@code true}, the minimum amount of schema for the object will be written.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical writeXmlSchema(TargetData target, logical formatted, Text encoding, logical minXmlSchema)
{
return writeXmlSchema(target, formatted, encoding, minXmlSchema, null);
}
/**
* Writes an XML representation of the definition of this {@code TempTable}.
*
* @param target
* Data target.
* @param formatted
* Use {@code true} for a more human-readable document.
* @param encoding
* The character encoding. The default is UTF-8.
* @param minXmlSchema
* If {@code true}, the minimum amount of schema for the object will be written.
* @param omitInitVal
* Use {@code true} to omit the initial values.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical writeXmlSchema(TargetData target,
logical formatted,
Text encoding,
logical minXmlSchema,
logical omitInitVal)
{
RecordBuffer buffer = buffer();
boolean ret;
try
{
// sanity
if (!buffer.isTemporary())
{
ErrorManager.recordOrShowError(13158);
// XML Methods only allowed on TEMP-TABLE buffers.
return logical.FALSE;
}
XmlExport export = new XmlExport(
target,
formatted != null && !formatted.isUnknown() && formatted.getValue(),
encoding == null ? null : encoding.toJavaType());
ret = export.writeTableSchema(
(TemporaryBuffer) buffer,
minXmlSchema != null && !minXmlSchema.isUnknown() && minXmlSchema.getValue(),
omitInitVal != null && !omitInitVal.isUnknown() && omitInitVal.getValue());
}
finally
{
try
{
target.close();
}
catch (IOException e)
{
// TODO: log
ret = false;
}
}
return logical.of(ret);
}
/**
* Definition of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data
* target, using the specified format.
*
* @param targetFormat
* The format ("xml" or "json").
* @param target
* Data target.
* @param formatted
* {@code True} to format the data for making it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
* @param encoding
* Name of the character encoding to use.
* @param omitInitialValues
* {@code True} to omit data for fields whose values match their initial values.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
@Override
public logical serializeRow(character targetFormat,
TargetData target,
logical formatted,
character encoding,
logical omitInitialValues)
{
return logical.of(serializeRowImpl(targetFormat.toJavaType(),
target,
formatted.toJavaType(),
encoding.toJavaType(),
omitInitialValues.toJavaType(),
null));
}
/**
* Definition of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data
* target, using the specified format.
*
* @param format
* The format ("xml" or "json").
* @param target
* Data target.
* @param formatted
* {@code True} to format the data for making it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
* @param encoding
* Name of the character encoding to use.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
@Override
public logical serializeRow(character format, TargetData target, logical formatted, character encoding)
{
return logical.of(serializeRowImpl(
format.toJavaType(), target, formatted.toJavaType(), encoding.toJavaType(), null, null));
}
/**
* Definition of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data
* target, using the specified format.
*
* @param format
* The format ("xml" or "json").
* @param target
* Data target.
* @param formatted
* {@code True} to format the data for making it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
@Override
public logical serializeRow(character format, TargetData target, logical formatted)
{
return logical.of(serializeRowImpl(
format.toJavaType(), target, formatted.toJavaType(), null, null, null));
}
/**
* Definition of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data
* target, using the specified format.
*
* @param format
* The format ("xml" or "json").
* @param target
* Data target.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
@Override
public logical serializeRow(character format, TargetData target)
{
return logical.of(serializeRowImpl(format.toJavaType(), target, null, null, null, null));
}
/**
* Definition of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data
* target, using the specified format.
*
* @param targetFormat
* The format ("xml" or "json").
* @param target
* Data target.
* @param formatted
* {@code True} to format the data for making it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
* @param encoding
* Name of the character 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} if the operation completed successfully, else {@code false}.
*/
@Override
public logical serializeRow(character targetFormat,
TargetData target,
logical formatted,
character encoding,
logical omitInitialValues,
logical omitOuterObject)
{
return logical.of(serializeRowImpl(targetFormat.toJavaType(),
target,
formatted.toJavaType(),
encoding.toJavaType(),
omitInitialValues.toJavaType(),
omitOuterObject.toJavaType()));
}
/**
* Reads XML Schema from an XML document and uses that schema to either create a schema for a
* {@code TempTable} or {@code TempTable} or verify existing schema in a {@code TempTable},
* {@code TempTable} or temp-table {@code Buffer} object.
*
* @param source
* Data source.
* @param override
* If {@code true} the default mapping between XML Schema string and binary data types
* and ABL data types when creating an ABL temp-table schema from an XML Schema will
* be overwritten.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical readXmlSchema(SourceData source, logical override)
{
return readXmlSchema(source, override, null, null);
}
/**
* Reads XML Schema from an XML document and uses that schema to either create a schema for a
* {@code TempTable} or {@code TempTable} or verify existing schema in a {@code TempTable},
* {@code TempTable} or temp-table {@code Buffer} object.
*
* @param source
* Data source.
* @param override
* If {@code true} the default mapping between XML Schema string and binary data types
* and ABL data types when creating an ABL temp-table schema from an XML Schema will
* be overwritten.
* @param mapping
* A comma-delimited list of field name, data type pairs. This allows to specify the
* data type for a specific field.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical readXmlSchema(SourceData source, logical override, Text mapping)
{
return readXmlSchema(source, override, mapping, null);
}
/**
* Reads XML Schema from an XML document and uses that schema to either create a schema for a
* {@code TempTable} or {@code TempTable} or verify existing schema in a {@code TempTable},
* {@code TempTable} or temp-table {@code Buffer} object.
*
* @param source
* Data source.
* @param override
* If {@code true} the default mapping between XML Schema string and binary data types
* and ABL data types when creating an ABL temp-table schema from an XML Schema will
* be overwritten.
* @param mapping
* A comma-delimited list of field name, data type pairs. This allows to specify the
* data type for a specific field.
* @param verifyMode
* Specifies how the XML will be verified against the schema. Valid values: "LOOSE" or
* "STRICT".
*
* @return {@code true} if operation is successful.
*/
@Override
public logical readXmlSchema(SourceData source, logical override, Text mapping, Text verifyMode)
{
return TempTableBuilder.asTempTable((Temporary) this).readXmlSchema(source,
override,
mapping,
verifyMode);
}
/**
* Implementation of the READ-JSON method. Reads JSON data from the specified data source into
* this object.
* <p>
* Defaults applied:
* <ul>
* <li>{@code MERGE} read mode</li>
* </ul>
*
* @param source
* Data source.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
@Override
public logical readJson(SourceData source)
{
return readJson(source, new character("MERGE"));
}
/**
* Implementation of the READ-JSON method. Reads JSON data from the specified data source into
* this object.
*
* @param source
* Data source.
* @param readMode
* Mode in which data is read into buffer. Must evaluate to {@code APPEND}, {@code
* EMPTY}, {@code MERGE}, or {@code REPLACE}.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
@Override
public logical readJson(SourceData source, character readMode)
{
if (!source.configureSupportedSources(SourceData.SD_READ_JSON,
LegacyResource.TEMP_TABLE + " widget"))
{
return logical.FALSE;
}
RecordBuffer buffer = buffer();
// sanity
if (!buffer.isTemporary())
{
ErrorManager.recordOrShowError(15378);
// JSON methods only allowed on TEMP-TABLE buffers.
return logical.FALSE;
}
// normalize parameters
JsonImport jsonImport = new JsonImport(source, readMode.toJavaType());
boolean success;
try
{
// perform import
success = jsonImport.readTable((TemporaryBuffer) buffer, RecordBuffer::loadRecord);
}
catch (PersistenceException exc)
{
// TODO: log
success = false;
}
return logical.of(success);
}
/**
* Implementation of the WRITE-JSON method. Writes JSON data from this object into the
* specified data target.
* <p>
* Defaults applied:
* <ul>
* <li>Unformatted text</li>
* <li>UTF-8 encoding</li>
* <li>Do not omit temp-table fields which contain their initial values</li>
* <li>Do not omit data for the outermost object, unless {@code target} represents a {@code
* JsonArray} object.
* <li>No before-image data written</li>
* </ul>
*
* @param target
* Data target.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
@Override
public logical writeJson(TargetData target)
{
return writeJson(target,
logical.FALSE,
new character(),
logical.FALSE,
logical.FALSE,
logical.FALSE);
}
/**
* Implementation of the WRITE-JSON method. Writes JSON data from this object into the
* specified data target.
* <p>
* Defaults applied:
* <ul>
* <li>UTF-8 encoding</li>
* <li>Do not omit temp-table fields which contain their initial values</li>
* <li>Do not omit data for the outermost object, unless {@code target} represents a {@code
* JsonArray} object.
* <li>No before-image data written</li>
* </ul>
*
* @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}.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
@Override
public logical writeJson(TargetData target, logical formatted)
{
return writeJson(target,
formatted,
new character(),
logical.FALSE,
logical.FALSE,
logical.FALSE);
}
/**
* Implementation of the WRITE-JSON method. Writes JSON data from this object into the
* specified data target.
* <p>
* Defaults applied:
* <ul>
* <li>Do not omit temp-table fields which contain their initial values</li>
* <li>Do not omit data for the outermost object, unless {@code target} represents a {@code
* JsonArray} object.
* <li>No before-image data written</li>
* </ul>
*
* @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
* Name of the character encoding to use.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
@Override
public logical writeJson(TargetData target, logical formatted, character encoding)
{
return writeJson(target,
formatted,
encoding,
logical.FALSE,
logical.FALSE,
logical.FALSE);
}
/**
* Implementation of the WRITE-JSON method. Writes JSON data from this object into the
* specified data target.
* <p>
* Defaults applied:
* <ul>
* <li>Do not omit data for the outermost object, unless {@code target} represents a {@code
* JsonArray} object.
* <li>No before-image data written</li>
* </ul>
*
* @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
* Name of the character encoding to use.
* @param omitInitialValues
* {@code True} to omit data for fields whose values match their initial values.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
@Override
public logical writeJson(TargetData target,
logical formatted,
character encoding,
logical omitInitialValues)
{
return writeJson(target,
formatted,
encoding,
omitInitialValues,
logical.FALSE,
logical.FALSE);
}
/**
* Implementation of the WRITE-JSON method. Writes JSON data from this object into the
* specified data target.
* <p>
* Defaults applied:
* <ul>
* <li>No before-image data written</li>
* </ul>
*
* @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
* Name of the character 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} if the operation completed successfully, else {@code false}.
*/
@Override
public logical writeJson(TargetData target,
logical formatted,
character encoding,
logical omitInitialValues,
logical omitOuterObject)
{
return writeJson(target,
formatted,
encoding,
omitInitialValues,
omitOuterObject,
logical.FALSE);
}
/**
* Implementation of the WRITE-JSON method. Writes JSON data from this object into the
* specified data target.
*
* @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
* Name of the character 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.
* @param writeBeforeImage
* {@code True} to write before-image data and error information, else {@code false}.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
@Override
public logical writeJson(TargetData target,
logical formatted,
character encoding,
logical omitInitialValues,
logical omitOuterObject,
logical writeBeforeImage)
{
RecordBuffer buffer = buffer();
boolean success;
try
{
// sanity
if (!buffer.isTemporary())
{
ErrorManager.recordOrShowError(15378);
// JSON methods only allowed on TEMP-TABLE buffers.
return logical.FALSE;
}
if (writeBeforeImage != null &&
!writeBeforeImage.isUnknown() &&
writeBeforeImage.getValue())
{
ErrorManager.recordOrShowError(17366);
// write-before-image not allowed for TEMP-TABLE or BUFFER object.
ErrorManager.recordOrShowError(4065, false, "WRITE-JSON", "BUFFER widget");
// **The <attribute> attribute on the <widget id> has invalid arguments.
return logical.FALSE;
}
if (!buffer.getParentTable().flushBuffers())
{
return logical.FALSE;
}
com.fasterxml.jackson.core.JsonEncoding jsonEncoding = JsonExport.parseEncoding(encoding);
if (jsonEncoding == null)
{
ErrorManager.recordOrShowError(15342, encoding.toStringMessage(), "WRITE-JSON");
// Invalid encoding '<encoding>' argument for <method>.
return logical.FALSE;
}
JsonExport export = new JsonExport();
success = export.exportTempBuffer((TemporaryBuffer) buffer,
target,
formatted,
jsonEncoding,
omitInitialValues,
omitOuterObject);
buffer.release(false);
}
catch (PersistenceException exc)
{
// TODO: log
success = false;
}
finally
{
try
{
target.close();
}
catch (IOException e)
{
// TODO: log
success = false;
}
}
return logical.of(success);
}
/**
* Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
* records are dropped before copy operation.
*
* @param other
* Some other temp-table to copy from.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical copyTempTable(handle other)
{
return copyTempTable(other, false, false, false, null);
}
/**
* Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
* records are dropped before copy operation.
*
* @param other
* Some other temp-table to copy from.
* @param append
* Append mode.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical copyTempTable(handle other, logical append)
{
return copyTempTable(other, append, logical.FALSE, logical.FALSE, new character());
}
/**
* Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
* records are dropped before copy operation.
*
* @param other
* Some other temp-table to copy from.
* @param append
* Append mode.
* @param replace
* Replace mode.
*
* @return {@code true} if operation is successful
*/
@Override
public logical copyTempTable(handle other, logical append, logical replace)
{
return copyTempTable(other, append, replace, logical.FALSE, new character());
}
/**
* Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
* records are dropped before copy operation.
*
* @param other
* Some other temp-table to copy from.
* @param append
* Append mode.
* @param replace
* Replace mode.
* @param looseCopy
* Loose copy mode.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical copyTempTable(handle other, logical append, logical replace, logical looseCopy)
{
return copyTempTable(other, append, replace, looseCopy, new character());
}
/**
* Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
* records are dropped before copy operation.
*
* @param other
* Some other temp-table to copy from.
* @param append
* Append mode.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical copyTempTable(handle other, boolean append)
{
return copyTempTable(other, append, false, false, null);
}
/**
* Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
* records are dropped before copy operation.
*
* @param other
* Some other temp-table to copy from.
* @param append
* Append mode.
* @param replace
* Replace mode.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical copyTempTable(handle other, boolean append, boolean replace)
{
return copyTempTable(other, append, replace, false, null);
}
/**
* Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
* records are dropped before copy operation.
*
* @param other
* Some other temp-table to copy from.
* @param append
* Append mode.
* @param replace
* Replace mode.
* @param looseCopy
* Loose copy mode.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical copyTempTable(handle other, boolean append, boolean replace, boolean looseCopy)
{
return copyTempTable(other, append, replace, looseCopy, null);
}
/**
* Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
* records are dropped before copy operation.
* Only the first parameter is mandatory so this method will probably be heavy overloaded.
*
* @param other
* Some other temp-table to copy from.
* @param append
* Append mode.
* @param replace
* Replace mode.
* @param looseCopy
* Loose copy mode.
* @param prefix
* Prefix for naming the copied temp-table.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical copyTempTable(handle other,
logical append,
logical replace,
logical looseCopy,
character prefix)
{
TempTable tt = TempTableBuilder.asTempTable((Temporary) this);
if (tt == null)
{
LOG.log(Level.SEVERE,
"Nothing to copy into, this dynamic TEMP-TABLE was not prepared yet.",
new Throwable());
return logical.of(false);
}
return tt.copyTempTable(other, append, replace, looseCopy, prefix);
}
/**
* Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
* records are dropped before copy operation.
* Only the first parameter is mandatory so this method will probably be heavy overloaded.
*
* @param other
* Some other temp-table to copy from.
* @param append
* Append mode.
* @param replace
* Replace mode.
* @param looseCopy
* Loose copy mode.
* @param prefix
* Prefix for naming the copied temp-table.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical copyTempTable(handle other,
boolean append,
boolean replace,
boolean looseCopy,
String prefix)
{
TempTable tt = TempTableBuilder.asTempTable((Temporary) this);
if (tt == null)
{
LOG.log(Level.SEVERE,
"Nothing to copy into, this dynamic TEMP-TABLE was not prepared yet.",
new Throwable());
return logical.of(false);
}
return tt.copyTempTable(other,
logical.of(append),
logical.of(replace),
logical.of(looseCopy),
new character(prefix));
}
/**
* Obtain the last associated query (to which this buffer was added or set).
*
* @return The associated query or {@code unknown} if none exists.
*/
@Override
public handle getQueryAsHandle()
{
// a bit of maintenance
if (lastQuery != null && !lastQuery.valid())
{
lastQuery = null;
}
return lastQuery == null ? new handle() : new handle(lastQuery);
}
/**
* Set the query associated with the given resource. This is illegal when called directly by
* P4GL application and will raise 4052 condition.
*
* @param qry
* The query to associate.
*/
@Override
public void setQueryAsHandle(handle qry)
{
handle.invalidAttribute("QUERY", true, LegacyResource.BUFFER);
}
/**
* Set the query associated with the given resource. This is illegal when called directly by
* P4GL application and will raise 4052 condition.
*
* @param qry
* The query to associate.
*/
@Override
public void setQueryAsHandle(QueryWrapper qry)
{
handle.invalidAttribute("QUERY", true, LegacyResource.BUFFER);
}
/**
* Gets the maximum number of {@code DataSet} temp-table rows to retrieve in each {@code FILL}
* operation. The default value is zero (which retrieves all rows that satisfy the associated
* query).
*
* @return the maximum number of DataSet temp-table rows to retrieve in each {@code FILL}
* operation.
*/
@Override
public integer batchSize()
{
return new integer(batchSize);
}
/**
* Sets the maximum number of {@code DataSet} temp-table rows to retrieve in each {@code FILL}
* operation.
*
* @param size
* The new value for the {@code BATCH-SIZE} attribute.
*/
@Override
public void batchSize(long size)
{
if (dataSet == null || !dataSet.valid())
{
ErrorManager.recordOrShowError(12361);
// BATCH-SIZE may only be set for dataset temp-tables.
return;
}
batchSize = (int) size;
}
/**
* Sets the maximum number of {@code DataSet} temp-table rows to retrieve in each {@code FILL}
* operation.
*
* @param size
* The new value for the {@code BATCH-SIZE} attribute.
*/
@Override
public void batchSize(NumberType size)
{
if (size == null || size.isUnknown())
{
unableToAssignUnknown("BATCH-SIZE", "BUFFER widget");
return;
}
batchSize(size.longValue());
}
/**
* Gets the {@code ROWID} of the row in the after-image table that is the current version of
* the row in the before-image table currently associated with this buffer. This row can
* be a new or modified row.
*
* @return the {@code ROWID} of the row in the after-image table that is the current version
* of the row in the before-image table
*/
@Override
public rowid afterRowid()
{
RecordBuffer recBuf = buffer();
if (isAfterBuffer() || !recBuf.isAvailable())
{
// AFTER-ROWID is only available for BEFORE-BUFFERS of DATASETs
return new rowid();
}
return recBuf.peerRowid();
}
/**
* Gets the {@code ROWID} of the row in the before-image table that is the current version of
* the row in the before-image table currently associated with this buffer. This row can
* be a new or modified row.
*
* @return the {@code ROWID} of the row in the before-image table that is the current version
* of the row in the before-image table
*/
@Override
public rowid beforeRowid()
{
RecordBuffer recBuf = buffer();
if (isBeforeBuffer() || !recBuf.isAvailable())
{
// BEFORE-ROWID is only available for AFTER-BUFFERS of DATASETs
return new rowid();
}
return recBuf.peerRowid();
}
/**
* Obtain the current change state of the {@code DataSet} temp-table row associated with this
* buffer object.
*
* @return the current change state of the {@code DataSet} temp-table row associated with this
* buffer object.
*/
@Override
public integer rowState()
{
RecordBuffer recBuf = buffer();
if (!recBuf.isAvailable() || !recBuf.isTemporary() || beforeBufferType == BeforeType.SIMPLE)
{
return new integer();
}
// even if there is no dataset for this table, the rowState can be read.
return recBuf.rowState();
}
/**
* Accepts changes to the data in one row of this {@code DataSet} temp-table.
*
* @return {@code true} on success.
*/
@Override
public logical acceptRowChanges()
{
if (!isBeforeBuffer())
{
ErrorManager.recordOrShowError(11933, doGetName());
// ACCEPT-ROW-CHANGES must be run on the BEFORE table of buffer <name>.
return logical.FALSE;
}
if (!_available())
{
ErrorManager.recordOrShowError(11934, doGetName());
// ACCEPT-ROW-CHANGES buffer <name> not available.
return logical.FALSE;
}
int crtState = buffer().rowState().intValue();
if (crtState == Buffer.ROW_MODIFIED || crtState == Buffer.ROW_CREATED)
{
// find and update the after-image/record
BufferImpl after = afterBufferNative();
after.findByRowID(this.afterRowid());
RecordBuffer afterRecBuf = after.buffer();
afterRecBuf.peerRowid(new rowid());
afterRecBuf.rowState(Buffer.ROW_UNMODIFIED);
}
// otherwise (DELETE - after image is already deleted)
// then drop the before-image data:
setUpBeforeBuffer(true);
deleteRecord();
setUpBeforeBuffer(false);
return logical.TRUE;
}
/**
* Rejects changes to the data in one row of this {@code DataSet} temp-table.
*
* @return {@code true} on success.
*/
@Override
public logical rejectRowChanges()
{
if (!isBeforeBuffer())
{
ErrorManager.recordOrShowError(11939, doGetName());
// REJECT-ROW-CHANGES must be run on the BEFORE table of buffer <name>.
return logical.FALSE;
}
if (!_available())
{
ErrorManager.recordOrShowError(11936, doGetName());
// REJECT-ROW-CHANGES buffer <name> not available.
return logical.FALSE;
}
// find and update the after-image/record
BufferImpl after = afterBufferNative();
TemporaryBuffer afterImage = (TemporaryBuffer) after.buffer();
RecordBuffer recBuf = buffer();
switch (recBuf.rowState().toJavaIntegerType())
{
case Buffer.ROW_CREATED:
after.findByRowID(this.afterRowid());
after.deleteRecord();
break;
case Buffer.ROW_DELETED:
after.create();
BaseRecord.copy(afterImage.getCurrentRecord(), recBuf.getCurrentRecord(), false);
after.release();
break;
case Buffer.ROW_MODIFIED:
after.findByRowID(this.afterRowid());
BaseRecord.copy(afterImage.getCurrentRecord(), recBuf.getCurrentRecord(), false);
afterImage.peerRowid(new rowid());
afterImage.rowState(Buffer.ROW_UNMODIFIED);
after.release();
break;
}
// then drop the before-image data:
setUpBeforeBuffer(true);
deleteRecord();
setUpBeforeBuffer(false);
return logical.TRUE;
}
/**
* Saves changes from one row of the {@code DataSet} temp-table to the associated data
* source.
*
* @return {@code true} on success.
*/
@Override
public logical saveRowChanges()
{
return saveRowChangesImpl(0, null, false);
}
/**
* Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
*
* @param buffIndex
* The index of the buffer in the data source.
*
* @return {@code true} on success.
*/
@Override
public logical saveRowChanges(NumberType buffIndex)
{
int idx = buffIndex.isUnknown() ? 1 : buffIndex.intValue();
return saveRowChangesImpl(idx - 1, null, false);
}
/**
* Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
*
* @param buffName
* The name of the buffer in the data source.
*
* @return {@code true} on success.
*/
@Override
public logical saveRowChanges(Text buffName)
{
return saveRowChangesImpl(buffName.toJavaType(), null, false);
}
/**
* Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
*
* @param buffIndex
* The index of the buffer in the data source.
* @param skipList
* A list of field names that should not be assigned after a new row is created
* (comma-separated).
*
* @return {@code true} on success.
*/
@Override
public logical saveRowChanges(NumberType buffIndex, Text skipList)
{
int idx = buffIndex.isUnknown() ? 1 : buffIndex.intValue();
return saveRowChangesImpl(idx - 1, skipList.toJavaType(), false);
}
/**
* Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
*
* @param buffName
* The name of the buffer in the data source.
* @param skipList
* A list of field names that should not be assigned after a new row is created
* (comma-separated).
*
* @return {@code true} on success.
*/
@Override
public logical saveRowChanges(Text buffName, Text skipList)
{
return saveRowChangesImpl(buffName.toJavaType(), skipList.toJavaType(), false);
}
/**
* Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
* <p>
* <strong>Note:</strong> this form is used when unknown literal {@code ?} is used to specify
* the buffer. In either case, numeric or character, the meaning is the same.
*
* @param buffInfo
* Unknown value.
* @param skipList
* A list of field names that should not be assigned after a new row is created
* (comma-separated).
*
* @return {@code true} on success.
*/
@Override
public logical saveRowChanges(unknown buffInfo, Text skipList)
{
return saveRowChangesImpl(0, skipList.toJavaType(), false);
}
/**
* Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
*
* @param buffIndex
* The index of the buffer in the data source.
* @param skipList
* A list of field names that should not be assigned after a new row is created
* (comma-separated).
* @param noLobs
* Use {@code true} to specify the BLOB and CLOB fields should be ignored in the
* save operation.
*
* @return {@code true} on success.
*/
@Override
public logical saveRowChanges(NumberType buffIndex, Text skipList, logical noLobs)
{
boolean skipLobs = !noLobs.isUnknown() && noLobs.getValue();
int idx = buffIndex.isUnknown() ? 1 : buffIndex.intValue();
return saveRowChangesImpl(idx - 1, skipList.toJavaType(), skipLobs);
}
/**
* Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
* <p>
* <strong>Note:</strong> this form is used when unknown literal {@code ?} is used to specify
* the buffer. In either case, numeric or character, the meaning is the same.
*
* @param buffInfo
* Unknown value.
* @param skipList
* A list of field names that should not be assigned after a new row is created
* (comma-separated).
*
* @return {@code true} on success.
*/
@Override
public logical saveRowChanges(unknown buffInfo, Text skipList, logical noLobs)
{
boolean skipLobs = !noLobs.isUnknown() && noLobs.getValue();
return saveRowChangesImpl(0, skipList.toJavaType(), skipLobs);
}
/**
* Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
*
* @param buffName
* The name of the buffer in the data source.
* @param skipList
* A list of field names that should not be assigned after a new row is created
* (comma-separated).
* @param noLobs
* Use {@code true} to specify the BLOB and CLOB fields should be ignored in the
* save operation.
*
* @return {@code true} on success.
*/
@Override
public logical saveRowChanges(Text buffName, Text skipList, logical noLobs)
{
boolean skipLobs = !noLobs.isUnknown() && noLobs.getValue();
return saveRowChangesImpl(buffName.toJavaType(), skipList.toJavaType(), skipLobs);
}
/**
* Obtain the mode in which the {@link DataSet#fill()} method fills a {@code DataSet} member
* buffer. The default mode is {@code MERGE}.
*
* @return the fill mode for this buffer.
*/
@Override
public character fillMode()
{
if (dataSet == null)
{
// FILL-MODE is unknown for 'free' buffers
return new character();
}
return new character(fillMode);
}
/**
* Configures the mode in which the {@link DataSet#fill()} method fills a {@code DataSet}
* member buffer. The allowed modes are: {@code APPEND}, {@code EMPTY}, {@code MERGE},
* {@code NO-FILL}, and {@code REPLACE}.
*
* @param mode
* The new fill mode for this buffer.
*/
@Override
public void fillMode(String mode)
{
if (mode == null)
{
unableToAssignUnknown("FILL-MODE", "BUFFER widget");
return;
}
if (dataSet == null)
{
ErrorManager.recordOrShowError(11959);
// FILL-MODE is only valid for a dataset buffer.
ErrorManager.recordOrShowError(4058, "FILL-MODE", "BUFFER widget", mode);
// **Attribute <attribute> for the <widget id> has an invalid value of <string>.
return;
}
String upperMode = mode.toUpperCase();
switch (upperMode)
{
case FM_APPEND:
case FM_EMPTY:
case FM_MERGE:
case FM_NOFILL:
case FM_REPLACE:
// one of these is expected
break;
default:
// but this is not. Complain about it and use default, MERGE
ErrorManager.recordOrShowError(11960);
// FILL-MODE must be one of NO-FILL, MERGE, EMPTY, etc.
ErrorManager.recordOrShowError(4058, "FILL-MODE", "BUFFER widget", mode);
// **Attribute <attribute> for the <widget id> has an invalid value of <string>.
upperMode = FM_MERGE;
}
this.fillMode = upperMode;
}
/**
* Configures the mode in which the {@link DataSet#fill()} method fills a {@code DataSet}
* member buffer. The allowed modes are: {@code APPEND}, {@code EMPTY}, {@code MERGE},
* {@code NO-FILL}, and {@code REPLACE}.
*
* @param mode
* the new fill mode for this buffer.
*/
@Override
public void fillMode(Text mode)
{
fillMode(mode == null ? null : mode.toJavaType());
}
/**
* Attaches a {@code DataSource} object to this temp-table {@code Buffer} in a {@code DataSet}
* object.
*
* @param hds
* The handle to the {@code DataSource} to be attached.
*
* @return {@code true} on success.
*/
@Override
public logical attachDataSource(handle hds)
{
return attachDataSource(hds, null, null, null);
}
/**
* Attaches a {@code DataSource} object to this temp-table {@code Buffer} in a {@code DataSet}
* object.
*
* @param hds
* The handle to the {@code DataSource} to be attached.
* @param pairList
* A comma-delimited list of field name pairs that specify a mapping between different
* field names in the {@code DataSource} buffer and the {@code DataSet} temp-table
* buffer.
*
* @return {@code true} on success.
*/
@Override
public logical attachDataSource(handle hds, character pairList)
{
return attachDataSource(hds, pairList, null, null);
}
/**
* Attaches a {@code DataSource} object to this temp-table {@code Buffer} in a {@code DataSet}
* object.
*
* @param hds
* The handle to the {@code DataSource} to be attached.
* @param pairList
* A comma-delimited list of field name pairs that specify a mapping between different
* field names in the {@code DataSource} buffer and the {@code DataSset} temp-table
* buffer.
* @param exceptFields
* A comma-separated list of fields in the {@code DataSet} object buffer that will not
* be populated with data from the data source.
*
* @return {@code true} on success.
*/
@Override
public logical attachDataSource(handle hds, character pairList, character exceptFields)
{
return attachDataSource(hds, pairList, exceptFields, null);
}
/**
* Attaches a {@code DataSource} object to this temp-table {@code Buffer} in a {@code DataSet}
* object. This method provides much of the internal definition for the {@code FILL}. Among
* other things, this method creates the {@code FILL} query.
* <p>
* This Java method corresponds to ABL {@code ATTACH-DATA-SOURCE} method with all 4 parameters.
*
* @param hds
* The handle to the {@code DataSource} to be attached.
* @param pairList
* A comma-delimited list of field name pairs that specify a mapping between different
* field names in the {@code DataSource} buffer and the {@code DataSet} temp-table buffer.
* @param exceptFields
* A comma-separated list of fields in the {@code DataSet} object buffer that will not
* be populated with data from the data source.
* @param includeFields
* A comma-separated list of fields to include in the {@code DataSet} object buffer,
* as an ALTERNATIVE to specifying fields to exclude in except-fields.
*
* @return {@code true} on success.
*/
@Override
public logical attachDataSource(handle hds,
character pairList,
character exceptFields,
character includeFields)
{
// validate parameters
if (hds == null || !hds._isValid() || !hds.isType(LegacyResource.DATA_SOURCE))
{
ErrorManager.recordOrShowError(11957);
// ATTACH-DATA-SOURCE must have a valid data-source handle as the first argument.
return logical.FALSE;
}
DataSource newDSrc = (DataSource) hds.getResource();
if (newDSrc._getNumSourceBuffers() == 0)
{
ErrorManager.recordOrShowError(11958);
// ATTACH-DATA-SOURCE may not be called until source buffers have been assigned to the DATA-SOURCE.
return logical.FALSE;
}
if (exceptFields != null && !exceptFields.isUnknown() && !exceptFields.getValue().isEmpty() &&
includeFields != null && !includeFields.isUnknown() && !includeFields.getValue().isEmpty())
{
ErrorManager.recordOrShowError(12779);
// Cannot have both exclude list and include list.
return logical.FALSE;
}
Set<String> incFields = null;
Set<String> excFields = null;
if (exceptFields != null && !exceptFields.isUnknown() && !exceptFields.getValue().isEmpty())
{
excFields = new CaseInsensitiveHashSet<>();
// note: TAB, CR, LF are treated like normal character, only SPACE and , are separators
StringTokenizer st = new StringTokenizer(exceptFields.toJavaType(), " ,", false);
while (st.hasMoreTokens())
{
String tok = st.nextToken();
int i = tok.indexOf('.');
if (i != -1)
{
// .f1 is OK, <not-a-table>.f1 is also OK, f1 is not
excFields.add(tok.substring(i + 1));
}
else
{
excFields.add(tok);
}
}
}
else if (includeFields != null && !includeFields.isUnknown() && !includeFields.getValue().isEmpty())
{
incFields = new CaseInsensitiveHashSet<>();
// note: TAB, CR, LF are treated like normal character, only SPACE and , are separators
StringTokenizer st = new StringTokenizer(includeFields.toJavaType(), " ,", false);
while (st.hasMoreTokens())
{
String tok = st.nextToken();
int i = tok.indexOf('.');
if (i != -1)
{
// .f1 is OK, <not-a-table>.f1 is also OK, f1 is not
incFields.add(tok.substring(i + 1));
}
else
{
incFields.add(tok);
}
}
}
// silently return true if same data source is assigned
if (newDSrc == dataSource)
{
return logical.TRUE;
}
// this must be a DataSet buffer
if (dataSet == null)
{
ErrorManager.recordOrShowError(11952);
// ATTACH-DATA-SOURCE is only valid for buffers that are members of a dataset.
return logical.FALSE;
}
dstFieldMeta = new CaseInsensitiveHashMap<>();
// pre-compute field indexes into the record data array
DmoMeta dstMeta = buffer().getDmoInfo();
RecordMeta recordMeta = dstMeta.getRecordMeta();
PropertyMeta[] props = recordMeta.getPropertyMeta(false);
for (int i = 0, propsLength = props.length; i < propsLength; i++)
{
PropertyMeta pmeta = props[i];
dstFieldMeta.put(pmeta.getDmoProperty().legacyLower, pmeta);
}
// analyze field mapping between this buffer and those from new DataSource
String pairListStr = null;
ArrayList<TableMapper.LegacyFieldInfo> myFields = TableMapper.getLegacyOrderedList(this, true);
// The algorithm for creating the full list of pairs of fields takes into consideration the method
// parameters (the explicit pair list, except and include list of fields) but also analyses the list
// of fields in both current and data-source buffers for matching names. It looks something like this:
// * analyse/validate the provided list and mark those fields;
// - if a field name is found in multiple source buffers mark all of them;
// * for each source buffer
// - iterate all destination fields in the order of definition
// # if the pair is in attach list, use the pair
// # if fields have different name, skip the pair
// # if one of the fields is marked, skip the pair
// # otherwise (same name, not marked), use the pair.
//
// This algorithm will cause some interesting results. For example:
// * if attach-list is empty/unknown and each of N source and destination buffers have a commonly
// named field (f1), there will be N pairs (the destination field f1 is overwritten N time);
// * in the above scenario, if one of the source fields with same name is paired with a different
// field from destination (f2), all of the above pairs will be dropped. The field f1 remains
// unaffected, even if there are N-1 fields to pair from source buffers.
int numSourceBuffers = newDSrc._getNumSourceBuffers();
BufferImpl[] buffers = new BufferImpl[numSourceBuffers];
DmoMeta[] metaData = new DmoMeta[numSourceBuffers];
List<Pair<String, String>>[] orderedPairlist = new ArrayList[numSourceBuffers];
List<Pair<String, DataSource.FieldReference>>[] mappedNamesMap = new ArrayList[numSourceBuffers];
for (int k = 1; k <= numSourceBuffers; k++)
{
// fewer calls to buffer / getSourceBufferNative / getDmoInfo are better, that is why we precompute
BufferImpl buffer = newDSrc.getSourceBufferNative(k);
buffers[k - 1] = buffer;
metaData[k - 1] = buffer.buffer().getDmoInfo();
orderedPairlist[k - 1] = new ArrayList<>();
}
Set<String> marked = new HashSet<>(); // the marked fields are not matched automatically
if (pairList != null && !pairList.isUnknown())
{
// validate the list of field pairs
pairListStr = pairList.getValue();
String[] splits = pairListStr.isEmpty() ? new String[0] : pairListStr.split(",");
if (splits.length % 2 == 1)
{
ErrorManager.recordOrShowError(9035);
// Pairs list had too many pairs or odd number of entries.
return logical.FALSE;
}
String key = null;
for (String field : splits)
{
final String origField = field;
if (StringHelper.startsWithIgnoreCase(field, "rowid("))
{
field = field.substring(field.indexOf('(') + 1, field.indexOf(')')) + "." + Session.PK;
}
else
{
int k = field.indexOf('.');
if (k != -1)
{
marked.add(field.substring(k + 1).toLowerCase());
}
else
{
marked.add(field.toLowerCase());
}
}
if (key == null)
{
key = field;
}
else
{
String keyNoEx = key.indexOf('[') == -1 ? key : key.substring(0, key.indexOf('['));
String valNoEx = field.indexOf('[') == -1 ? field : field.substring(0, field.indexOf('['));
if (!hasFieldEx(keyNoEx))
{
if (!hasFieldEx(valNoEx))
{
ErrorManager.recordOrShowError(9034, key);
// Pair with <member> failed to have a member in each table.
return logical.FALSE;
}
// swap [key] and [field] so that [key] is in destination (current) buffer
String tmp = key;
key = field;
field = tmp;
tmp = keyNoEx;
keyNoEx = valNoEx;
valNoEx = tmp;
}
// lookup the buffer for [field] / [valNoEx]
int srcBufferIndex = 0;
for (int k = 1; k <= numSourceBuffers; k++)
{
BufferImpl buffer = newDSrc.getSourceBufferNative(k);
if (buffer.hasFieldEx(valNoEx))
{
if (field.indexOf('.') == -1)
{
field = buffer.doGetName() + "." + field;
}
srcBufferIndex = k;
break;
}
}
if (srcBufferIndex == 0)
{
// the [fld2] could not be found in any of the buffers of DataSource
ErrorManager.recordOrShowError(9034, origField);
// Pair with <member> failed to have a member in each table.
return logical.FALSE;
}
if (key.indexOf('.') == -1)
{
key = this.doGetName() + "." + key;
}
orderedPairlist[srcBufferIndex - 1].add(Pair.of(key, field));
key = null;
}
}
}
for (int k = 0; k < numSourceBuffers; k++)
{
DmoMeta bufMeta = metaData[k];
mappedNamesMap[k] = new ArrayList<>();
for (TableMapper.LegacyFieldInfo myField : myFields)
{
String fieldName = myField.getLegacyName();
if (marked.contains(myField.getLcLegacyName()))
{
continue;
}
if (bufMeta.byLegacyName(fieldName) != null)
{
if (incFields != null && !incFields.contains(fieldName) ||
excFields != null && excFields.contains(fieldName))
{
continue; // drop it!
}
Property prop2 = bufMeta.byLegacyName(fieldName);
Class<? extends BaseDataType> ftype2 = (prop2 == null) ? null : (Class) prop2._fwdType;
Integer offset2 = null;
if (prop2 != null)
{
offset2 = bufMeta.getRecordMeta().getIndexOfProperty(prop2.name);
}
// TODO: pass the Property to FieldReference ctor?
mappedNamesMap[k].add(
Pair.of(fieldName,
new DataSource.FieldReference(buffers[k], fieldName, ftype2, offset2)));
}
}
}
for (int k = 0; k < mappedNamesMap.length; k++)
{
BufferImpl buffer = buffers[k];
DmoMeta bufMeta = metaData[k];
//List<Pair<String, DataSource.FieldReference>> pairs = mappedNamesMap[k];
for (Pair<String, String> pair : orderedPairlist[k])
{
// normalize [fld1] a nd [fld2]
String fld1 = pair.getKey();
int j = fld1.indexOf('.');
if (j > 0)
{
fld1 = fld1.substring(j + 1);
}
String fld2 = pair.getValue();
j = fld2.indexOf('.');
if (j > 0)
{
fld2 = fld2.substring(j + 1);
}
Property prop2 = bufMeta.byLegacyName(fld2);
Class<? extends BaseDataType> ftype2 = (prop2 == null) ? null : (Class) prop2._fwdType;
Integer offset2 = (prop2 == null) ? null : bufMeta.getRecordMeta().getIndexOfProperty(prop2.name);
// TODO: pass the Property to FieldReference ctor?
mappedNamesMap[k].add(Pair.of(fld1.toLowerCase(),
new DataSource.FieldReference(buffer, fld2, ftype2, offset2)));
}
}
// cache the DATA-SOURCE-COMPLETE-MAP value
dataSrcMapping = new ArrayList<>();
StringBuilder sb = new StringBuilder();
String myName = (name == null) ? "?" : name;
for (List<Pair<String, DataSource.FieldReference>> pairs : mappedNamesMap)
{
for (Pair<String, DataSource.FieldReference> pair : pairs)
{
String ciName = pair.getKey();
DataSource.FieldReference dataSrcField = pair.getValue();
dataSrcMapping.add(Pair.of(ciName, dataSrcField));
if (sb.length() != 0)
{
sb.append(',');
}
sb.append(myName).append('.').append(ciName).append(',');
if (Session.PK.equals(dataSrcField.getName()))
{
sb.append("rowid(").append(dataSrcField.getBuffer()._name()).append(")");
}
else
{
sb.append(dataSrcField.getBuffer()._name()).append('.').append(dataSrcField.getName());
if (dataSrcField.getIndex() > 0)
{
sb.append("[").append(dataSrcField.getIndex()).append("]");
}
}
}
}
dataSrcMappingStr = sb.toString()/*.toLowerCase()*/;
// all fine, create the links between objects:
this.dataSource = newDSrc;
if (isAfterBuffer())
{
this.peerBuffer.dataSource = newDSrc;
}
this.attachedPairlist = pairListStr;
dataSource.attach(this, dataSrcMapping);
if (dataSource.hasExplicitQuery())
{
QueryWrapper query = (QueryWrapper) dataSource.getQueryAsHandle().getResource();
query.setSubstitutionBuffers(this);
}
return logical.TRUE;
}
/**
* Detaches the {@code DataSource} from the temp-table buffer in a {@code DataSet} object.
*
* @return {@code true} on success.
*/
@Override
public logical detachDataSource()
{
detachDataSourceImpl();
return logical.TRUE;
}
/**
* Get the handle to the {@code DataSource} object currently attached to the {@code DataSet}
* object buffer.
*
* @return The value of {@code DATA-SOURCE} as described above.
*/
@Override
public handle dataSource()
{
return new handle(dataSource);
}
/**
* Get the handle to the {@code DataSource} object currently attached to the {@code DataSet}
* object buffer.
*
* @return The value of {@code DATA-SOURCE} as described above.
*/
public DataSource dataSourceNative()
{
return dataSource;
}
/**
* Obtain the handle to the default buffer of the after-image table that corresponds to
* the buffer of the before-image table currently associated with this buffer handle.
*
* @return The value of {@code AFTER-BUFFER} attribute as described above.
*/
@Override
public handle afterBuffer()
{
return new handle(afterBufferNative());
}
/**
* Obtain the default buffer of the after-image table that corresponds to
* the buffer of the before-image table currently associated with this buffer.
*
* @return The value of {@code AFTER-BUFFER} attribute as described above.
*/
public BufferImpl afterBufferNative()
{
if (!isBeforeBuffer())
{
// permanent and simple temp-tables do not have BEFORE/AFTER buffers
return null;
}
return peerBuffer;
}
/**
* Obtain the {@code rowid} of the row in the original before-image table that corresponds to
* the row in the change table currently associated with this buffer handle.
* <p>
* Implementation note:
* FWD uses as a storage backing for both {@code ORIGIN-ROWID} and {@code DATA-SOURCE-ROWID}
* the same field ({@link TempRecord#_originRowid}) exclusively.
* <ul>
* <li>when the record is a normal dataset temp-table record the meaning is
* {@code DATA-SOURCE-ROWID}, for both BEFORE and AFTER image. In the case the
* {@code DataSource} must be also attached or an ABL error is raised. </li>
* <li>when we use it as a CHANGE dataset temp-table, the meaning of the field is
* {@code ORIGIN-ROWID}.</li>
* </ul>
*
* @return The value of {@code ORIGIN-ROWID} attribute as described above.
*
* @see #dataSourceRowid()
*/
@Override
public rowid originRowid()
{
RecordBuffer recBuf = buffer();
if (!recBuf.isTemporary() || !recBuf.isAvailable())
{
return new rowid();
}
if (!tableHandleNative().getOriginHandle()._isValid())
{
return new rowid();
}
if (!recBuf.isTemporary() || !recBuf.isAvailable())
{
return new rowid();
}
TempRecord record = (TempRecord) recBuf.getCurrentRecord();
return new rowid(record._originRowid());
}
/**
* Obtain the handle to the default buffer of the before-image table that corresponds to
* the buffer of the after-image table currently associated with this buffer handle.
*
* @return The value of {@code BEFORE-BUFFER} attribute as described above.
*/
@Override
public handle beforeBuffer()
{
return new handle(beforeBufferNative());
}
/**
* Obtain the default buffer of the before-image table that corresponds to
* the buffer of the after-image table currently associated with this buffer.
*
* @return The value of {@code BEFORE-BUFFER} attribute as described above.
*/
public BufferImpl beforeBufferNative()
{
if (!isAfterBuffer())
{
// permanent and simple temp-tables do not have BEFORE/AFTER buffers
return null;
}
return peerBuffer;
}
/**
* The list of field name pairs for all fields (in comm-delimited format) in a {@code DataSet}
* temp-table buffer that are mapped to corresponding fields in an attached data-source object.
*
* @return The value of {@code DATA-SOURCE-COMPLETE-MAP} attribute.
*/
@Override
public character dataSourceCompleteMap()
{
if (dataSet == null)
{
ErrorManager.recordOrShowError(12780, false);
// DATA-SOURCE-COMPLETE-MAP only legal for dataset tables.
return new character();
}
if (dataSource == null)
{
ErrorManager.recordOrShowError(12781, false);
// DATA-SOURCE-COMPLETE-MAP requires prior ATTACH-DATA-SOURCE method to be run.
return new character();
}
return new character(dataSrcMappingStr);
}
/**
* Obtains the value of {@code DATA-SOURCE-ROWID} attribute (the {@code DataSource} row corresponding to
* this buffer object).
*
* @return The current value of {@code DATA-SOURCE-ROWID} ABL attribute.
*/
@Override
public rowid dataSourceRowid()
{
if (dataSource == null || !dataSource.valid())
{
ErrorManager.recordOrShowError(14276, doGetName());
// Dataset record for <name> required for DATA-SOURCE-ROWID
return new rowid();
}
if (dataSet == null || !dataSet.valid())
{
if (!isBeforeBuffer() || peerBuffer.dataSet == null || !peerBuffer.dataSet.valid())
{
ErrorManager.recordOrShowError(14275, false, doGetName());
// Dataset record for <name> required for DATA-SOURCE-ROWID
return new rowid();
}
}
RecordBuffer buffer = buffer();
if (!buffer.isTemporary() || !buffer.isAvailable())
{
return new rowid();
}
TempRecord record = (TempRecord) buffer.getCurrentRecord();
Long bruteRow = record._datasourceRowid();
if (bruteRow == null)
{
return new rowid();
}
// validate the [bruteRow]: it must belong to an existing record of only buffer of the current datasource
BufferImpl srcBuf = dataSource.getSourceBufferNative();
try
{
Record srcRec = buffer.getPersistence().getSession(Persistence.TEMP_CTX)
.get(srcBuf.buffer().getDMOImplementationClass(), bruteRow);
if (srcRec == null)
{
return new rowid();
}
}
catch (PersistenceException e)
{
return new rowid();
}
return new rowid(bruteRow);
}
/**
* Obtains the {@code DataSource} row corresponding to this buffer object.
*
* @param joinLevel
* The level of join, if the data-source is a join.
*
* @return The current value of {@code DATA-SOURCE-ROWID} ABL attribute.
*/
@Override
public rowid dataSourceRowid(long joinLevel)
{
return dataSourceRowid(new int64(joinLevel));
}
/**
* Obtains the {@code DataSource} row corresponding to this buffer object.
*
* @param buffName
* The name of one of the data-source buffers.
*
* @return The current value of {@code DATA-SOURCE-ROWID} ABL attribute.
*/
@Override
public rowid dataSourceRowid(String buffName)
{
return dataSourceRowid(new character(buffName));
}
/**
* Obtains the {@code DataSource} row corresponding to this buffer object.
*
* @param joinLevel
* The level of join, if the data-source is a join.
*
* @return The current value of {@code DATA-SOURCE-ROWID} ABL attribute.
*/
@Override
public rowid dataSourceRowid(NumberType joinLevel)
{
if (dataSource == null || !dataSource.valid())
{
ErrorManager.recordOrShowError(14276, doGetName());
// Dataset record for <name> required for DATA-SOURCE-ROWID
return new rowid();
}
if (dataSet == null || !dataSet.valid())
{
ErrorManager.recordOrShowError(14275, false, doGetName());
// Dataset record for <name> required for DATA-SOURCE-ROWID
return new rowid();
}
RecordBuffer buffer = buffer();
if (!buffer.isTemporary() || !buffer.isAvailable())
{
return new rowid();
}
if (joinLevel.isUnknown() ||
joinLevel.intValue() == 1 && dataSource._getNumSourceBuffers() == 1)
{
return dataSourceRowid();
}
int jjl = joinLevel.intValue();
if (jjl < 1 || jjl > dataSource._getNumSourceBuffers())
{
ErrorManager.recordOrShowError(14277, "", "");
// Unable to identify DATA-SOURCE buffer <temp-table name> in DATA-SOURCE-ROWID. (14277)
return new rowid();
}
BufferImpl buf = dataSource.getSourceBufferNative(jjl);
String findDataSource = dataSource.getSaveWhereStringImpl(buf, false);
if (findDataSource == null)
{
ErrorManager.recordOrShowError(14278, buf.doGetName(), "");
// Unable to generate FIND for <source-name> in DATA-SOURCE-ROWID. (14278)
return new rowid();
}
logical ok = new logical();
ErrorManager.silent(() -> ok.assign(
buf._find(findDataSource, FindMode.UNIQUE, LockType.EXCLUSIVE, BufferImpl.this)));
if (ok.isUnknown() || !ok.getValue() || !buf._available())
{
return new rowid();
}
return buf.rowID();
}
/**
* Obtains the {@code DataSource} row corresponding to this buffer object.
*
* @param buffName
* The name of one of the data-source buffers.
*
* @return The current value of {@code DATA-SOURCE-ROWID} ABL attribute.
*/
@Override
public rowid dataSourceRowid(character buffName)
{
if (dataSource == null || !dataSource.valid())
{
ErrorManager.recordOrShowError(14276, doGetName());
// Dataset record for <name> required for DATA-SOURCE-ROWID
return new rowid();
}
if (dataSet == null || !dataSet.valid())
{
ErrorManager.recordOrShowError(14275, false, doGetName());
// Dataset record for <name> required for DATA-SOURCE-ROWID
return new rowid();
}
RecordBuffer buffer = buffer();
if (!buffer.isTemporary() || !buffer.isAvailable())
{
return new rowid();
}
BufferImpl buf = null;
if (!buffName.isUnknown())
{
String javaType = buffName.toJavaType();
int n = dataSource._getNumSourceBuffers();
for (int k = 1; k <= n; k++)
{
BufferImpl buff = dataSource.getSourceBufferNative(k);
if (buff.doGetName().equalsIgnoreCase(javaType))
{
buf = buff;
break;
}
}
}
if (buf == null)
{
ErrorManager.recordOrShowError(14277, "", "");
// Unable to identify DATA-SOURCE buffer <temp-table name> in DATA-SOURCE-ROWID. (14277)
return new rowid();
}
if (dataSource._getNumSourceBuffers() == 1)
{
return dataSourceRowid();
}
String findDataSource = dataSource.getSaveWhereStringImpl(buf, false);
if (findDataSource == null)
{
ErrorManager.recordOrShowError(14278, buf.doGetName(), "");
// Unable to generate FIND for <source-name> in DATA-SOURCE-ROWID. (14278)
return new rowid();
}
logical ok = new logical();
BufferImpl finalBuf = buf;
ErrorManager.silent(() -> ok.assign(
finalBuf._find(findDataSource, FindMode.UNIQUE, LockType.EXCLUSIVE, BufferImpl.this)));
if (ok.isUnknown() || !ok.getValue() || !finalBuf._available())
{
return new rowid();
}
return finalBuf.rowID();
}
/**
* Configures the {@code DataSource} row corresponding to this buffer object.
*
* @param val
* The new value of {@code DATA-SOURCE-ROWID} ABL attribute.
*/
@Override
public void dataSourceRowid(rowid val)
{
if (dataSet == null || !dataSet.valid())
{
ErrorManager.recordOrShowError(14275, false, doGetName());
// Dataset record for <name> required for DATA-SOURCE-ROWID
return;
}
RecordBuffer buffer = buffer();
if (!buffer.isTemporary() || !buffer.isAvailable())
{
return;
}
if (isBeforeBuffer())
{
peerBuffer.dataSourceRowid(val);
return;
}
setDatasourceRowidImpl(val.getValue());
// TODO: set the _datasourceRowid on the peer record (AFTER / BEFORE)
}
/**
* Configures the {@code DataSource} row corresponding to this buffer object.
*
* @param joinLevel
* The level of join, if the data-source is a join.
* @param val
* The new value of {@code DATA-SOURCE-ROWID} ABL attribute.
*/
@Override
public void dataSourceRowid(int64 joinLevel, rowid val)
{
if (dataSet == null || !dataSet.valid())
{
ErrorManager.recordOrShowError(14275, false, doGetName());
// Dataset record for <name> required for DATA-SOURCE-ROWID
return;
}
RecordBuffer buffer = buffer();
if (!buffer.isTemporary() || !buffer.isAvailable())
{
return;
}
if (isBeforeBuffer())
{
peerBuffer.dataSourceRowid(joinLevel, val);
return;
}
UnimplementedFeature.missing("BUFFER:DATA-SOURCE-ROWID(n)= not implemented");
}
/**
* Configures the {@code DataSource} row corresponding to this buffer object.
*
* @param buffName
* The name of one of the data-source buffers.
* @param val
* The new value of {@code DATA-SOURCE-ROWID} ABL attribute.
*/
@Override
public void dataSourceRowid(character buffName, rowid val)
{
if (dataSet == null || !dataSet.valid())
{
ErrorManager.recordOrShowError(14275, false, doGetName());
// Dataset record for <name> required for DATA-SOURCE-ROWID
return;
}
RecordBuffer buffer = buffer();
if (!buffer.isTemporary() || !buffer.isAvailable())
{
return;
}
if (isBeforeBuffer())
{
peerBuffer.dataSourceRowid(buffName, val);
return;
}
UnimplementedFeature.missing("BUFFER:DATA-SOURCE-ROWID(name)= not implemented");
}
/**
* Get the {@code DataSet} object of which the buffer is a member or {@code null} otherwise.
*
* @return the {@code DataSet} object of which the buffer is a member.
*/
public DataSet _dataSet()
{
return isBeforeBuffer() ? peerBuffer.ref().dataSet : dataSet;
}
/**
* Get the handle for the {@code DataSet} object of which the buffer is a member.
*
* @return handle for the {@code DataSet} object of which the buffer is a member.
*/
@Override
public handle dataSet()
{
return new handle(_dataSet());
}
/**
* Counts the ACTIVE relations for which the buffer is the parent. A buffer may be a parent in
* multiple relations, but a child in only one.
*
* @return The value of {@code NUM-CHILD-RELATIONS} attribute as described above.
*/
@Override
public integer numChildRelations()
{
if (dataSet == null)
{
// quick out, this is no dataset buffer
return new integer(0);
}
List<DataRelation> rels = dataSet.getRelations(this, true, false, true);
return new integer(rels.size());
}
/**
* Obtain the handle to the ACTIVE parent data-relation object for the parent of this buffer.
* Since a buffer may be a child in only one ACTIVE relation, there can be maximum only one
* parent for any buffer at any time.
*
* @return The value of {@code PARENT-RELATION} attribute as described above.
*/
@Override
public handle parentRelation()
{
return new handle(parentRelationNative());
}
/**
* Obtain the handle to the ACTIVE parent data-relation object for the parent of this buffer.
* Since a buffer may be a child in only one ACTIVE relation, there can be maximum only one
* parent for any buffer at any time.
*
* @return The value of {@code PARENT-RELATION} attribute as described above.
*/
public DataRelation parentRelationNative()
{
if (dataSet == null)
{
// quick out, this is no dataset buffer
return null;
}
List<DataRelation> rels = dataSet.getRelations(this, false, true, true);
if (rels.isEmpty())
{
return null;
}
for (DataRelation rel : rels)
{
if (rel._isActive())
{
return rel;
}
}
return null;
}
/**
* Creates a blank before-table record for each buffer record, with a {@code ROW-STATE} of
* {@code ROW-CREATED} for a table with only new data.
*
* @return {@code true} on success.
*/
@Override
public logical markNew()
{
if (!isAfterBuffer())
{
ErrorManager.recordOrShowError(14274);
// MARK-ROW-STATE and MARK-NEW may only be used on a dataset member buffer with an existing BEFORE-TABLE.
return logical.FALSE;
}
return logical.of(((TemporaryBuffer) buffer()).markNew(beforeBufferNative()));
}
/**
* Manually set the {@code ROW-STATE} attribute for the current record/row.
*
* @param newState
* The new state the record is marked with. Legal values: {@code ROW_CREATED},
* {@code ROW_MODIFIED}, {@code ROW_DELETED}, and {@code ROW_UNMODIFIED}.
*
* @return {@code true} on success.
*/
@Override
public logical markRowState(int64 newState)
{
int jState = (newState == null || newState.isUnknown()) ?
Buffer.ROW_UNMODIFIED :
newState.intValue();
return markRowState(jState, null);
}
/**
* Manually set the {@code ROW-STATE} attribute for the current record/row.
*
* @param newState
* The new state the record is marked with. Legal values: {@code ROW_CREATED},
* {@code ROW_MODIFIED}, {@code ROW_DELETED}, and {@code ROW_UNMODIFIED}.
*
* @return {@code true} on success.
*/
@Override
public logical markRowState(long newState)
{
return markRowState(newState, null);
}
/**
* Manually set the {@code ROW-STATE} attribute for the current record/row.
*
* @param newState
* The new state the record is marked with. Legal values: {@code ROW_CREATED},
* {@code ROW_MODIFIED}, {@code ROW_DELETED}, and {@code ROW_UNMODIFIED}.
* @param before
* A handle to the before-image source buffer.
*
* @return {@code true} on success.
*/
@Override
public logical markRowState(int64 newState, handle before)
{
int jState = (newState == null || newState.isUnknown()) ?
Buffer.ROW_UNMODIFIED :
newState.intValue();
return markRowState(jState, before);
}
/**
* Manually set the {@code ROW-STATE} attribute for the current record/row.
*
* @param newState
* The new state the record is marked with. Legal values: {@code ROW_CREATED},
* {@code ROW_MODIFIED}, {@code ROW_DELETED}, and {@code ROW_UNMODIFIED}.
* @param compareTo
* A handle to the before-image source buffer - used to compare to current data.
*
* @return {@code true} on success.
*/
@Override
public logical markRowState(long newState, handle compareTo)
{
if (!isAfterBuffer())
{
ErrorManager.recordOrShowError(14274);
// MARK-ROW-STATE and MARK-NEW may only be used on a dataset member buffer with an existing BEFORE-TABLE.
return logical.FALSE;
}
if (newState == Buffer.ROW_UNMODIFIED)
{
return logical.TRUE; // nothing to process
}
else if (newState != Buffer.ROW_CREATED &&
newState != Buffer.ROW_MODIFIED &&
newState != Buffer.ROW_DELETED)
{
ErrorManager.recordOrShowError(14288);
// Invalid ROW-STATE given to MARK-ROW-STATE.
return logical.FALSE;
}
if (!_available())
{
ErrorManager.recordOrShowError(14275, false, doGetName());
// Dataset record for <name> required for DATA-SOURCE-ROWID.
return logical.FALSE;
}
RecordBuffer compRB = null;
BufferImpl compBuffer = null;
if (compareTo != null && !compareTo.isUnknown())
{
if (!compareTo.isType(LegacyResource.BUFFER))
{
ErrorManager.recordOrShowError(14289);
// Invalid handle or handle type for MARK-ROW-STATE
return logical.FALSE;
}
compBuffer = (BufferImpl) compareTo.getResource();
if (!compBuffer._available())
{
ErrorManager.recordOrShowError(0, "MARK-ROW-STATE called with an empty BEFORE-BUFFER");
// Progress OpenEdge crashes.
return logical.FALSE;
}
if (compBuffer.buffer().isTemporary())
{
compRB = compBuffer.buffer();
}
}
integer rowState = rowState();
if (!rowState.isUnknown() && rowState.intValue() != Buffer.ROW_UNMODIFIED)
{
return logical.TRUE; // already has before image/record
}
BufferImpl b4Buffer = beforeBufferNative();
RecordBuffer b4RecBuf = b4Buffer.buffer();
RecordBuffer recBuf = buffer();
b4Buffer.setUpBeforeBuffer(true);
switch ((int) newState)
{
case ROW_CREATED:
b4Buffer.create();
recBuf.rowState((int) newState);
recBuf.peerRowid(b4RecBuf.rowID());
b4RecBuf.peerRowid(recBuf.rowID());
b4RecBuf.rowState((int) newState);
b4Buffer.release();
break;
case ROW_MODIFIED:
b4Buffer.create();
if (compRB != null)
{
RecordBuffer.copyCommonFields(b4RecBuf.getCurrentRecord(), compRB.getCurrentRecord());
}
else
{
RecordBuffer.copyCommonFields(b4RecBuf.getCurrentRecord(), recBuf.getCurrentRecord());
}
recBuf.rowState((int) newState);
recBuf.peerRowid(b4RecBuf.rowID());
b4RecBuf.peerRowid(recBuf.rowID());
b4RecBuf.rowState((int) newState);
b4Buffer.release();
break;
case ROW_DELETED:
// this buffer is not updated
b4Buffer.create();
if (compRB != null)
{
RecordBuffer.copyCommonFields(b4RecBuf.getCurrentRecord(), compRB.getCurrentRecord());
}
else
{
RecordBuffer.copyCommonFields(b4RecBuf.getCurrentRecord(), recBuf.getCurrentRecord());
}
b4RecBuf.peerRowid(new rowid());
b4RecBuf.rowState((int) newState);
b4Buffer.release();
break;
}
b4Buffer.setUpBeforeBuffer(false);
return logical.TRUE;
}
/**
* Obtain the handle to a {@code DataRelation} object for which the buffer is the parent.
*
* @param index
* The index of the data-relation object (1-based).
*
* @return the handle to a {@code DataRelation} object for which the buffer is the parent.
*/
@Override
public handle childRelation(long index)
{
if (dataSet == null || index < 1)
{
// quick out, this is no dataset buffer
return new handle();
}
List<DataRelation> rels = dataSet.getRelations(this, true, false, true);
if (index > rels.size())
{
return new handle();
}
return new handle(rels.get((int) (index - 1)));
}
/**
* Obtain the handle to a {@code DataRelation} object for which the buffer is the parent.
*
* @param index
* The index of the data-relation object (1-based).
*
* @return the handle to a {@code DataRelation} object for which the buffer is the parent.
*/
@Override
public handle childRelation(int64 index)
{
if (index == null || index.isUnknown())
{
return new handle();
}
return childRelation(index.longValue());
}
/**
* Definition of the SYNCHRONIZE method.
*
* @return <code>true</code> if the synchronization was performed.
*/
@Override
public logical querySynchronize()
{
if (dataSet == null)
{
return logical.FALSE;
}
List<DataRelation> childRelations = dataSet.getRelations(this, true, false, true);
boolean ok = true;
for (DataRelation rel : childRelations)
{
BufferImpl childBuffer = rel.getChildBufferNative();
if (!rel.isRecursive().booleanValue() || childBuffer != this)
{
synchronize(rel);
ok &= childBuffer.querySynchronize().booleanValue();
}
}
return logical.of(ok);
}
/**
* Obtain a comma-separated list of field name pairs for fields in a {@code DataSet}
* temp-table buffer that are mapped to corresponding fields in an attached {@code DataSource}
* object. This list includes only the field name pairs you specified with the most recently
* attached data-source object, in the order they were specified.
* <p>
* @return the list of attach fields, as described above.
*/
@Override
public character attachedPairlist()
{
return new character(attachedPairlist);
}
/**
* Returns the state of the REJECTED attribute
*
* @return Current state of the REJECTED attribute.
*/
@Override
public logical rejected()
{
RecordBuffer recBuf = buffer();
if (!recBuf.isAvailable() || !recBuf.isTemporary())
{
return logical.FALSE;
}
DataSet ds = _dataSet();
if (ds == null)
{
return logical.FALSE;
}
// NOTE: internally, the P4GL database the __error-flag__ is an integer storing multiple flags.
// We need to extract the right bit (ERROR_REJECTED) and convert it to a logical value.
Integer flags = recBuf.errorFlags();
return logical.of(flags != null && (flags & Buffer.ERROR_REJECTED) != 0);
}
/**
* Setter for REJECTED attribute.
*
* @param value The new value of REJECTED attribute.
*/
@Override
public void rejected(boolean value)
{
RecordBuffer recBuf = buffer();
if (!_available())
{
// temp-tables case
ErrorManager.recordOrShowError(11916, doGetName());
// <name> record is not available to set ERROR in.
ErrorManager.recordOrShowError(3131, "REJECTED", "", LegacyResource.BUFFER);
// Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
return;
}
if (!recBuf.isTemporary())
{
// permanent table case
ErrorManager.recordOrShowError(11917, doGetName());
// Cannot set ERROR if there is no BEFORE-TABLE for <name>.
ErrorManager.recordOrShowError(3131, "REJECTED", "", LegacyResource.BUFFER);
// Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
return;
}
if (!isBeforeBuffer())
{
if (!isAfterBuffer() || // SIMPLE BUFFER
(recBuf.peerRowid() == null || recBuf.peerRowid().isUnknown())) // no AFTER-IMAGE
{
ErrorManager.recordOrShowError(11918, doGetName(), "");
// Cannot set ERROR if there is no BEFORE-TABLE row for <name>.
return;
}
}
// NOTE: internally, the P4GL database the __error-flag__ is an integer storing multiple flags.
// We need to update the right bit (ERROR_ERROR) to [value].
recBuf.updateErrorFlags(Buffer.ERROR_REJECTED, value);
}
/**
* Setter for REJECTED attribute.
*
* @param value The new value of REJECTED attribute.
*/
@Override
public void rejected(logical value)
{
if (value == null || value.isUnknown())
{
unableToAssignUnknown("REJECTED", "BUFFER widget");
return;
}
rejected(value.getValue());
}
/**
* Fills a {@code Buffer} object, recursively, based on its defined data sources.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical fill()
{
if (dataSet == null || !dataSet.valid())
{
ErrorManager.recordOrShowError(11945);
// BUFFER:FILL is only valid for buffers that are members of a dataset.
return logical.FALSE;
}
lastBatch = false;
if (FM_NOFILL.equalsIgnoreCase(fillMode))
{
// nothing to do
return logical.TRUE;
}
return new FillWorker().fill();
}
/**
* Synchronize child query of the DATA-RELATION.
*
* @param rel
* The DATA-RELATION in question
*/
private boolean synchronize(DataRelation rel)
{
Buffer childBuffer = rel.getChildBufferNative();
String[] relFields = rel.getRelationFields(false);
String[] parentRelFields = rel.getRelationFields(true);
String cname = childBuffer.name().toJavaType();
String mname = this.name;
StringBuilder sb = new StringBuilder("FOR EACH ").append(cname).append(" WHERE ");
for (int nf = 0; nf < relFields.length; ++nf) {
if (nf > 0)
{
sb.append(" and ");
}
sb.append(cname).append(".").append(relFields[nf]).append(" = ").
append(mname).append(".").append(parentRelFields[nf]);
}
handle hQuery = new handle(false);
QueryWrapper.createQuery(hQuery);
P2JQuery query = hQuery.unwrapQuery();
query.addBuffer(childBuffer);
query.prepare(sb.toString());
query.hintFullResults();
query.queryOpen();
boolean rc = query.getFirst().booleanValue();
query.queryClose();
HandleOps.delete(hQuery);
return rc;
}
/**
* Assigns the value of {@code srcBuffer.df} to field {@code f} of this buffer. Takes care of the extents.
*
* @param myField
* The destination field of this buffer.
* @param df
* The source field.
* @param srcBuffer
* The source buffer.
* @param dstRecBuff
* The destination record buffer.
* @param fields
* A map of resolved {@link FieldStructure} for each destination field.
*/
private void assign(String myField,
DataSource.FieldReference df,
BufferImpl srcBuffer,
RecordBuffer dstRecBuff,
Map<String, FieldStructure> fields)
{
if (!srcBuffer._available())
{
return;
}
FieldStructure field = fields.get(myField);
if (field == null)
{
int dstIndex = 0;
String crtField;
int i = myField.indexOf('[');
if (i != -1)
{
int myFieldSize = myField.length();
char[] crtFieldChars = new char[i];
boolean openParenthesis = false;
for (int j = 0; j < myFieldSize; j++)
{
char current = myField.charAt(j);
if (current == '[')
{
openParenthesis = true;
}
else if (current == ']')
{
break;
}
else if (openParenthesis == false)
{
crtFieldChars[j] = current;
}
else if (openParenthesis == true)
{
dstIndex = dstIndex * 10 + ((int) current - '0');
}
}
crtField = new String(crtFieldChars);
}
else
{
crtField = myField;
}
DmoMeta dmoInfo = dstRecBuff.getDmoInfo();
Property dstProp = dmoInfo.byLegacyName(crtField);
if (dstProp == null)
{
return; // something is wrong... invalid [crtField] name. Raise OE condition?
}
field = new FieldStructure();
field.dstProp = dstProp;
field.crtField = crtField;
field.dstIndex = dstIndex;
fields.put(myField, field);
}
// TRACKING-CHANGES can't be active during FILL, so BEFORE-TABLE is not needed
// only TEMP-TABLES can perform FILL, so no triggers can be invoked
// this assign is wrapped in a start/endBatch bracket, so no validation/flush is done on individual field
// assignment
// TODO: extent
PropertyMeta dstPropMeta = field.dstProp.getMeta();
Integer dstFieldOffset = dstPropMeta == null ? null : dstPropMeta.getOffset();
Integer srcFieldOffset = df.getOffset();
RecordBuffer srcRecBuff = srcBuffer.buffer();
boolean directAssign = dstPropMeta != null &&
df.getType() != null &&
df.getType() == dstPropMeta.getType();
if (field.dstProp.extent == 0)
{
if (df.getIndex() == 0)
{
Object src = null;
if (directAssign && srcFieldOffset != null)
{
src = srcRecBuff.getDirectField(srcFieldOffset);
}
else
{
src = srcBuffer.dereference(df.getName());
}
if (directAssign && dstFieldOffset != null)
{
dstRecBuff.setDirectField(src, dstFieldOffset);
}
else
{
dereference(field.crtField, src);
}
}
else
{
dereference(field.crtField, srcBuffer.dereference(df.getIndex() /* -1? */ , df.getName()));
}
}
else
{
if (field.dstIndex != 0)
{
if (df.getIndex() == 0)
{
dereference(field.dstIndex, field.crtField, srcBuffer.dereference(df.getName()));
}
else
{
dereference(field.dstIndex /* -1? */,
field.crtField,
srcBuffer.dereference(df.getIndex() /* -1? */ , df.getName()));
}
}
else
{
// bulk copy: (assume) both source and destination are extent fields
for (int k = 0; k < field.dstProp.extent; k++)
{
BufferField srcField = srcBuffer.bufferFieldNative(df.getName());
BufferField destField = bufferFieldNative(field.crtField);
destField.changeValue(k + 1, srcField.value(k + 1));
}
}
}
}
/**
* Applies a callback procedure, which allows execution of a defined event without duplicating
* the event procedure definition.
*
* @param eventName
* The event whose callback will be called.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical applyCallback(String eventName)
{
if (dataSet == null || !dataSet.valid())
{
ErrorManager.recordOrShowError(11949);
// APPLY-CALLBACK is only for buffers that are members of datasets.
return logical.FALSE;
}
if (!isCallbackName(eventName))
{
return logical.FALSE;
}
return logical.of(invokeCallback(eventName.toUpperCase()));
}
/**
* Retrieves the name of the internal procedure associated with the ABL callback for the
* specified event.
*
* @param eventName
* The name of the event.
*
* @return the name of the internal procedure associated with specified event.
*/
@Override
public character getCallbackProcName(String eventName)
{
if (!isCallbackName(eventName))
{
return new character();
}
CallbackData cbd = callbacks == null ? null : callbacks.get(eventName.toUpperCase());
if (cbd == null)
{
return new character();
}
return new character(cbd.getTarget());
}
/**
* Retrieves the handle of the procedure that contains the internal procedure associated
* with the ABL callback for the specified event
*
* @param eventName
* The name of the event.
*
* @return a handle to the procedure that contains the callback procedure.
*/
@Override
public handle getCallbackProcContext(String eventName)
{
if (!isCallbackName(eventName))
{
return new handle();
}
CallbackData cbd = callbacks == null ? null : callbacks.get(eventName.toUpperCase());
if (cbd == null || cbd.getProcedure() == null)
{
return new handle();
}
return new handle(cbd.getProcedure());
}
/**
* Associates an internal procedure with an ABL callback event.
* <p>
* This method returns false if the event name is not valid.
*
* @param event
* The event name, must evaluate to one of the events supported by implementing object.
* @param proc
* The internal procedure to be executed for this event, which will be executed using
* the current THIS-PROCEDURE reference. To remove a callback mapping, set this
* parameter to the empty string.
*
* @return {@code true} if the event was registered.
*/
@Override
public logical setCallbackProcedure(String event, String proc)
{
return setCallbackProcedure(event, proc, null);
}
/**
* Associates an internal procedure with an ABL callback event.
* <p>
* This method returns false if the event name is not valid.
*
* @param event
* The event name.
* @param proc
* The internal procedure to be executed for this event, which will be executed using
* the current THIS-PROCEDURE reference. To remove a callback mapping, set this
* parameter to the empty string.
*
* @return {@code true} if the event was registered.
*/
@Override
public logical setCallbackProcedure(String event, character proc)
{
return setCallbackProcedure(event, proc.toJavaType(), null);
}
/**
* Associates an internal procedure with an ABL callback event.
* <p>
* This method returns false if the event name is not valid.
*
* @param event
* The event name.
* @param proc
* The internal procedure to be executed for this event, which will be executed using
* the current THIS-PROCEDURE reference. To remove a callback mapping, set this
* parameter to the empty string.
*
* @return {@code true} if the event was registered.
*/
@Override
public logical setCallbackProcedure(character event, character proc)
{
return setCallbackProcedure(event.toJavaType(), proc.toJavaType(), null);
}
/**
* Associates an internal procedure with an ABL callback event.
* <p>
* This method returns false if the event name is not valid or if the procedure's context is
* not a valid handle.
*
* @param event
* The event name, must evaluate to one of the events supported by implementing object.
* @param proc
* The internal procedure to be executed for this event. To remove a callback
* mapping, set this parameter to the empty string.
* @param context
* The procedure handle where the internal procedure will be searched and executed.
*
* @return {@code true} if the event was registered.
*/
@Override
public logical setCallbackProcedure(character event, String proc, handle context)
{
return setCallbackProcedure(event.toJavaType(), proc, context);
}
/**
* Associates an internal procedure with an ABL callback event.
* <p>
* This method returns false if the event name is not valid or if the procedure's context is
* not a valid handle.
*
* @param event
* The event name.
* @param proc
* The internal procedure to be executed for this event, which will be executed using
* the current THIS-PROCEDURE reference. To remove a callback mapping, set this
* parameter to the empty string.
* @param context
* The procedure handle where the internal procedure will be searched and executed.
*
* @return {@code true} if the event was registered.
*/
@Override
public logical setCallbackProcedure(String event, character proc, handle context)
{
return setCallbackProcedure(event, proc.toJavaType(), context);
}
/**
* Associates an internal procedure with an ABL callback event.
* <p>
* This method returns false if the event name is not valid or if the procedure's context is not a valid
* handle.
*
* @param event
* The event name.
* @param proc
* The internal procedure to be executed for this event, which will be executed using the current
* THIS-PROCEDURE reference. To remove a callback mapping, set this parameter to the empty
* string.
* @param context
* The procedure handle where the internal procedure will be searched and executed.
*
* @return {@code true} if the event was registered.
*/
@Override
public logical setCallbackProcedure(character event, character proc, handle context)
{
return setCallbackProcedure(event.toJavaType(), proc.toJavaType(), context);
}
/**
* Applies a callback procedure, which allows execution of a defined event without duplicating
* the event procedure definition.
*
* @param eventName
* The event whose callback will be called.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical applyCallback(Text eventName)
{
return applyCallback(eventName.toJavaType());
}
/**
* Retrieves the name of the internal procedure associated with the ABL callback for the
* specified event.
*
* @param eventName
* The name of the event.
*
* @return the name of the internal procedure associated with specified event.
*/
@Override
public character getCallbackProcName(Text eventName)
{
return getCallbackProcName(eventName.toJavaType());
}
/**
* Retrieves the handle of the procedure that contains the internal procedure associated
* with the ABL callback for the specified event
*
* @param eventName
* The name of the event.
*
* @return a handle to the procedure that contains the callback procedure.
*/
@Override
public handle getCallbackProcContext(Text eventName)
{
return getCallbackProcContext(eventName.toJavaType());
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
* @param context
* The context in which the callback will be executed.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are
* invalid.
*/
@Override
public logical setCallback(Text callbackName, Text routineName, object<?> context)
{
return setCallback(callbackName.toJavaType(), routineName.toJavaType(), context);
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
* @param context
* The context in which the callback will be executed.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are
* invalid.
*/
@Override
public logical setCallback(String callbackName, String routineName, handle context)
{
return setCallbackProcedure(callbackName, routineName, context);
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
* @param context
* The context in which the callback will be executed.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are
* invalid.
*/
@Override
public logical setCallback(Text callbackName, Text routineName, handle context)
{
return setCallbackProcedure(callbackName.toJavaType(), routineName.toJavaType(), context);
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
* @param context
* The context in which the callback will be executed.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are
* invalid.
*/
@Override
public logical setCallback(String callbackName, Text routineName, handle context)
{
return setCallbackProcedure(callbackName, routineName.toJavaType(), context);
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are
* invalid.
*/
@Override
public logical setCallback(String callbackName, String routineName)
{
return setCallback(callbackName, routineName, (object<?>) null);
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are
* invalid.
*/
@Override
public logical setCallback(Text callbackName, Text routineName)
{
return setCallback(callbackName.toJavaType(), routineName.toJavaType(), (object<?>) null);
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are
* invalid.
*/
@Override
public logical setCallback(Text callbackName, String routineName)
{
return setCallback(callbackName.toJavaType(), routineName);
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are
* invalid.
*/
@Override
public logical setCallback(String callbackName, Text routineName)
{
return setCallback(callbackName, routineName.toJavaType());
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
* @param context
* The context in which the callback will be executed.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are
* invalid.
*/
@Override
public logical setCallback(String callbackName, Text routineName, object<?> context)
{
return setCallback(callbackName, routineName.toJavaType(), context);
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
* @param context
* The context in which the callback will be executed.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are
* invalid.
*/
@Override
public logical setCallback(Text callbackName, String routineName, object<?> context)
{
return setCallback(callbackName.toJavaType(), routineName, context);
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
* @param context
* The context in which the callback will be executed.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are
* invalid.
*/
@Override
public logical setCallback(Text callbackName, String routineName, handle context)
{
return setCallback(callbackName.toJavaType(), routineName, context);
}
/**
* Associates an internal procedure with an ABL callback event.
* <p>
* This method returns false if the event name is not valid.
*
* @param event
* The event name.
* @param proc
* The internal procedure to be executed for this event, which will be executed using
* the current THIS-PROCEDURE reference. To remove a callback mapping, set this
* parameter to the empty string.
*
* @return {@code true} if the event was registered.
*/
@Override
public logical setCallbackProcedure(character event, String proc)
{
return setCallbackProcedure(event.toJavaType(), proc, null);
}
/**
* Associates an internal procedure with an ABL callback event.
*
* @param eventName
* The eventName of the event.
* @param callback
* The eventName of the internal procedure associated with the callback event.
* @param context
* A handle to a procedure that contains the internal procedure specified by
* {@code callback}. If not specified, {@code THIS-PROCEDURE} is used as the procedure
* context.
*
* @return {@code true} when the method ends with success.
*/
@Override
public logical setCallbackProcedure(String eventName, String callback, handle context)
{
if (dataSet == null || !dataSet.valid())
{
ErrorManager.recordOrShowError(11951);
// SET-CALLBACK-PROCEDURE is only valid for buffers that are members of a dataset.
return logical.FALSE;
}
if (!isCallbackName(eventName))
{
return logical.FALSE;
}
if (callback == null)
{
return logical.TRUE;
}
if (callback.isEmpty())
{
if (callbacks != null)
{
callbacks.remove(eventName.toUpperCase());
}
return logical.TRUE;
}
if (context == null || context.isUnknown())
{
// defaulting to THIS-PROCEDURE
context = pm.thisProcedure();
}
else
{
if (!context._isValid() || !(context.getResource() instanceof PersistentProcedure))
{
ErrorManager.recordOrShowError(13273);
// Third argument to SET-CALLBACK must be a valid procedure handle or object reference.
return logical.FALSE;
}
}
InvokeConfig callSite = getCallSite(eventName);
if (callbacks == null)
{
callbacks = new HashMap<>();
}
callbacks.put(eventName.toUpperCase(), new CallbackData(callback, context, null, callSite));
return logical.TRUE;
}
/**
* Configures a callback. Creates an association between a method within a class instance or
* an internal procedure within a persistent procedure, with an ABL callback event.
*
* @param callbackName
* The name of a callback.
* @param routineName
* The name of a method or an internal procedure to be associated.
* @param context
* The context in which the callback will be executed.
*
* @return {@code true} if operation is successful and {@code false} if the parameters are invalid.
*/
@Override
public logical setCallback(String callbackName, String routineName, object<?> context)
{
if (dataSet == null || !dataSet.valid())
{
ErrorManager.recordOrShowError(11951);
// SET-CALLBACK-PROCEDURE is only valid for buffers that are members of a dataset.
return logical.FALSE;
}
if (!isCallbackName(callbackName))
{
return logical.FALSE;
}
if (routineName == null)
{
return logical.TRUE;
}
if (routineName.isEmpty())
{
if (callbacks != null)
{
callbacks.remove(callbackName.toUpperCase());
}
return logical.TRUE;
}
CallbackData cbd;
if (context == null || context.isUnknown())
{
// defaulting to THIS-OBJECT or to THIS-PROCEDURE, depending on the context
if (ObjectOps.thisObject().isUnknown())
{
InvokeConfig callSite = getCallSite(callbackName);
cbd = new CallbackData(routineName, pm.thisProcedure(), null, callSite);
}
else
{
cbd = new CallbackData(routineName, null, ObjectOps.thisObject(), null);
}
}
else
{
cbd = new CallbackData(routineName, null, context, null);
}
if (callbacks == null)
{
callbacks = new HashMap<>();
}
callbacks.put(callbackName.toUpperCase(), cbd);
return logical.TRUE;
}
/**
* Loads the empty target {@code TempTable}) object with changed rows from original (source)
* object. This can be either the BEFORE or the AFTER buffer. If the BEFORE/AFTER temp-tables
* are already populated, they are emptied before copying the changes.
*
* @param original
* Handle to original object {@code DataSet} buffer (BEFORE or AFTER buffer) that
* contains the changed rows to load into the target object.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical getChanges(handle original)
{
if (original == null || original.isUnknown() || !original.isType(LegacyResource.BUFFER))
{
ErrorManager.recordOrShowError(11947);
// bufferhdl:GET-CHANGES must have a valid buffer handle to the original table, from which changes are coming, as the first argument.
return logical.FALSE;
}
// check the argument first: it must be an AFTER or BEFORE buffer:
BufferImpl srcBefore = (BufferImpl) original.getResource();
BufferImpl srcAfter = null;
BufferImpl buf = srcBefore.beforeBufferNative();
if (buf == null)
{
buf = srcBefore.afterBufferNative();
if (buf == null)
{
ErrorManager.recordOrShowError(11925, srcBefore.doGetName());
// GET/MERGE-CHANGES require a BEFORE-TABLE for table <name>.
return logical.FALSE;
}
else
{
srcAfter = buf;
}
}
else
{
// oops, [xHandle] is the BEFORE buffer and [srcBefore] is the AFTER buffer, fixing:
srcAfter = srcBefore;
srcBefore = buf;
}
// now check the current buffer: it also must be an AFTER or BEFORE buffer:
BufferImpl dstBefore = null;
BufferImpl dstAfter = null;
buf = beforeBufferNative();
if (buf == null)
{
buf = afterBufferNative();
if (buf == null)
{
ErrorManager.recordOrShowError(11925, doGetName());
// GET/MERGE-CHANGES require a BEFORE-TABLE for table <name>.
return logical.FALSE;
}
else
{
dstBefore = this;
dstAfter = buf;
}
}
else
{
dstBefore = buf;
dstAfter = this;
}
// @this moment we have the dst/src-Before/After initialized. Be sure dsts are empty first:
dstBefore.setUpBeforeBuffer(true);
AbstractTempTable dstTT = (AbstractTempTable) dstAfter.tableHandleNative();
boolean trackingChanges = dstTT._isTrackingChanges();
dstTT.setTrackingChanges(false);
try
{
if (!dstBefore.deleteAll().getValue() || !dstAfter.deleteAll().getValue())
{
return logical.FALSE;
}
}
finally
{
// restore back
if (trackingChanges)
{
dstTT.setTrackingChanges(true);
}
dstBefore.setUpBeforeBuffer(false);
}
boolean ok = TemporaryBuffer.copyChanges((TemporaryBuffer) srcBefore.buffer(),
(TemporaryBuffer) srcAfter.buffer(),
(TemporaryBuffer) dstBefore.buffer(),
(TemporaryBuffer) dstAfter.buffer());
// in 4GL, there is no guarantee as to the contents of the buffers involved, when the method returns
// to add predictability, FWD makes sure they are empty
srcBefore.release();
srcAfter.release();
if (ok)
{
TempTable origin = TempTableBuilder.asTempTable((Temporary) srcAfter);
((AbstractTempTable) TempTableBuilder.asTempTable((Temporary) dstBefore)).setOriginTable(origin);
((AbstractTempTable) TempTableBuilder.asTempTable((Temporary) dstAfter)).setOriginTable(origin);
}
return logical.of(ok);
}
/**
* Loads the empty target ({@code DataSet} or {@code TempTable}) object with changed rows from
* original (source) object.
*
* @param original
* Handle to original object ({@code DataSet} or {@code TempTable}) that contains the
* changed rows to load into the target object.
* @param parentMode
* When {@code TRUE} the target gets the changed parent rows of each changed child row.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical getChanges(handle original, logical parentMode)
{
return getChanges(original, parentMode.getValue());
}
/**
* Loads the empty target ({@code DataSet} or {@code TempTable}) object with changed rows from
* original (source) object.
*
* @param original
* Handle to original object ({@code DataSet} or {@code TempTable}) that contains the
* changed rows to load into the target object.
* @param parentMode
* When {@code TRUE} the target gets the changed parent rows of each changed child row.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical getChanges(handle original, boolean parentMode)
{
ErrorManager.recordOrThrowError(12778);
// GET-PARENT mode not supported for buffer GET-CHANGES.
return logical.FALSE;
}
/**
* Merges single changed row of a single temp-table in a source {@code DataSet} object loaded
* with the {@code GET-CHANGES()} method into the corresponding rows of a single temp-table
* into the original (target) {@code DataSet} buffer.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical mergeRowChanges()
{
return mergeRowChanges(null, false);
}
/**
* Merges single changed row of a single temp-table in a source {@code DataSet} object loaded
* with the {@code GET-CHANGES()} method into the corresponding rows of a single temp-table
* into the original (target) {@code DataSet} buffer.
*
* @param original
* A handle to the original (target) buffer object of a single target {@code DataSet}
* temp-table to merge with the changed rows from the source object.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical mergeRowChanges(handle original)
{
return mergeRowChanges(original, false);
}
/**
* Merges single changed row of a single temp-table in a source {@code DataSet} object loaded
* with the {@code GET-CHANGES()} method into the corresponding rows of a single temp-table
* into the original (target) {@code DataSet} buffer.
*
* @param original
* A handle to the original (target) buffer object of a single target {@code DataSet}
* temp-table to merge with the changed rows from the source object.
* @param allCopyMode
* Use {@code TRUE} to indicates that all rows of the source after-image table must be
* merged.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical mergeRowChanges(handle original, boolean allCopyMode)
{
if (!_available())
{
ErrorManager.recordOrShowError(11937, doGetName());
// MERGE-ROW-CHANGES buffer <name> not available.
return logical.FALSE;
}
BufferImpl afterBufferChanges = null;
if (!isBeforeBuffer())
{
if (!isAfterBuffer())
{
ErrorManager.recordOrShowError(11925, doGetName());
// GET/MERGE-CHANGES require a BEFORE-TABLE for table <name>.
return logical.FALSE;
}
else
{
afterBufferChanges = this;
}
}
else
{
afterBufferChanges = peerBuffer;
}
BufferImpl origBuffer;
AbstractTempTable originTable;
if (original == null)
{
TempTable parentTable = this.buffer().getParentTable();
originTable = (AbstractTempTable) parentTable.getOriginTable();
if (originTable == null || !originTable.valid())
{
ErrorManager.recordOrShowError(11944, afterBufferChanges.doGetName());
// MERGE-ROW-CHANGES finds ORIGIN-HANDLE in table <name> is no longer valid.
return logical.FALSE;
}
origBuffer = (BufferImpl) originTable.defaultBufferHandleNative();
}
else
{
if (!original._isValid() || !original.isType(LegacyResource.BUFFER))
{
ErrorManager.recordOrShowError(11943);
// First argument to MERGE-ROW-CHANGES must be a valid BUFFER handle.
return logical.FALSE;
}
origBuffer = (BufferImpl) original.getResource();
originTable = (AbstractTempTable) origBuffer.tableHandleNative();
}
if (originTable._isTrackingChanges())
{
ErrorManager.recordOrShowError(11922, originTable.name().toStringMessage());
// MERGE-CHANGES may not be run on table <name> with TRACKING-CHANGES on.
return logical.FALSE;
}
if (origBuffer.beforeBufferType == BeforeType.BEFORE)
{
// the mergeRowChangesImpl() requires the AFTER-BUFFER as second parameter
origBuffer = origBuffer.peerBuffer;
}
return logical.of(mergeRowChangesImpl(this.ref(), origBuffer.ref()));
}
/**
* Merges single changed row of a single temp-table in a source {@code DataSet} object loaded
* with the {@code GET-CHANGES()} method into the corresponding rows of a single temp-table
* into the original (target) {@code DataSet} buffer.
*
* @param original
* A handle to the original (target) buffer object of a single target {@code DataSet}
* temp-table to merge with the changed rows from the source object.
* @param allCopyMode
* Use {@code TRUE} to indicates that all rows of the source after-image table must be
* merged.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical mergeRowChanges(handle original, logical allCopyMode)
{
return mergeRowChanges(original, !allCopyMode.isUnknown() && allCopyMode.getValue());
}
/**
* Merges the changed rows of all temp-tables or a single temp-table in a source
* {@code DataSet} object loaded with the {@code GET-CHANGES()} method into the corresponding
* rows of all temp-tables or a single temp-table (respectively) in the original (target)
* {@code DataSet} object.
*
* @param original
* A handle to the original (target) {@code DataSet} object or the buffer object of a
* single target {@code DataSet} temp-table to merge with the changed rows from the
* source object.
* @return {@code true} if operation is successful.
*/
@Override
public logical mergeChanges(handle original)
{
return mergeChanges(original, false);
}
/**
* Merges the changed rows of all temp-tables or a single temp-table in a source
* {@code DataSet} object loaded with the {@code GET-CHANGES()} method into the corresponding
* rows of all temp-tables or a single temp-table (respectively) in the original (target)
* {@code DataSet} object.
*
* @param original
* A handle to the original (target) {@code DataSet} object or the buffer object of a
* single target {@code DataSet} temp-table to merge with the changed rows from the
* source object.
* @param allCopyMode
* Use {@code TRUE} to indicates that all rows of the source after-image table must be
* merged.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical mergeChanges(handle original, logical allCopyMode)
{
return mergeChanges(original, !allCopyMode.isUnknown() && allCopyMode.getValue());
}
/**
* Merges the changed rows of all temp-tables or a single temp-table in a source {@code DataSet} object
* loaded with the {@code GET-CHANGES()} method into the corresponding rows of all temp-tables or a single
* temp-table (respectively) in the original (target) {@code DataSet} object.
*
* @param original
* A handle to the original (target) {@code DataSet} object or the buffer object of a single
* target {@code DataSet} temp-table to merge with the changed rows from the source object.
* @param allCopyMode
* Use {@code TRUE} to indicates that all rows of the source after-image table must be merged.
*
* @return {@code true} if operation is successful.
*/
@Override
public logical mergeChanges(handle original, boolean allCopyMode)
{
// the parameter must be a valid dataset handle:
if (original == null || original.isUnknown() || !original.isType(LegacyResource.BUFFER))
{
ErrorManager.recordOrShowError(11953);
// First argument to MERGE-CHANGES must be a valid BUFFER handle.
return logical.FALSE;
}
BufferImpl origBuf = (BufferImpl) original.getResource();
origBuf = origBuf.ref();
AbstractTempTable origTT = (AbstractTempTable) tableHandleNative().getOriginTable();
if (origTT != origBuf.tableHandleNative())
{
ErrorManager.recordOrShowError(11923, origBuf.doGetName(), doGetName());
// MERGE-CHANGES run on original table <name> that does not match the ORIGIN-HANDLE in the GET-CHANGES table <name>.
return logical.FALSE;
}
if (origTT._isTrackingChanges())
{
ErrorManager.recordOrShowError(11922, origTT.name().toStringMessage());
// MERGE-CHANGES may not be run on table <name> with TRACKING-CHANGES on.
return logical.FALSE;
}
// make sure we use the AFTER-TABLE buffer
if (origBuf.isBeforeBuffer())
{
origBuf = origBuf.peerBuffer;
}
if (isAfterBuffer())
{
return logical.of(mergeChangesImpl(origBuf, allCopyMode));
}
else if (isBeforeBuffer())
{
return logical.of(peerBuffer.mergeChangesImpl(origBuf, allCopyMode));
}
ErrorManager.recordOrShowError(11925, doGetName());
// GET/MERGE-CHANGES require a BEFORE-TABLE for table <name>.
return logical.TRUE;
}
/**
* Accepts changes to the data in target object (a temp-table or all temp-tables in a {@code DataSet}).
*
* @return {@code true} if operation is successful.
*/
@Override
public logical acceptChanges()
{
if (!isBeforeBuffer())
{
ErrorManager.recordOrShowError(11887, doGetName());
// ACCEPT-CHANGES run on table <name> that has no BEFORE-TABLE.
return logical.FALSE;
}
RecordBuffer beforeBuf = buffer();
Persistence persistence = beforeBuf.getPersistence();
BufferImpl after = afterBufferNative();
RecordBuffer afterBuf = after.buffer();
// invalidate index-based caches because of low-level operations:
afterBuf.invalidateFFCache(0);
beforeBuf.invalidateFFCache(0);
// release all buffers
((TemporaryBuffer) afterBuf).getAllSlaveBuffers().forEach((b) -> b.release(false));
// execute a simple query to update the [_rowState], [_originRowid] and [_peerRowid] fields to default
// values in the AFTER-BUFFER. The [_dataSourceRowid] remains unmodified.
StringBuilder buf = new StringBuilder();
buf.append("update ").append(afterBuf.getTable()).append(" set ")
.append(Buffer.ROW_STATE_FIELD).append("=").append(Buffer.ROW_UNMODIFIED).append(", ")
.append(Buffer.ORIGIN_ROWID_FIELD).append("=NULL, ")
.append(Buffer.AFTER_ROWID_FIELD).append("=NULL")
.append(" where ")
.append(TemporaryBuffer.MULTIPLEX_FIELD_NAME).append(" = ?");
Object[] args = new Object[] {afterBuf.getMultiplexID()};
try
{
boolean noUndo = !beforeBuf.isUndoable();
int cnt = persistence.executeSQL(buf.toString(), Persistence.TEMP_CTX, args, noUndo);
afterBuf.updateCachedRecords((BaseRecord r) ->
{
// direct access, so the row's state doesn't get changed
// at this moment, all buffers are flushed, nothing is new/deleted/dirty
OrmUtils.setField(r, TempRecord._ROW_STATE_DATA_INDEX, Buffer.ROW_UNMODIFIED);
OrmUtils.setField(r, TempRecord._ORIGIN_ROWID_DATA_INDEX, null);
OrmUtils.setField(r, TempRecord._PEER_ROWID_DATA_INDEX, null);
});
if (LOG.isLoggable(Level.FINE))
{
LOG.log(Level.FINE, "ACCEPT-CHANGES accepted " + cnt + " changes from " + beforeBuf.getDMOName());
}
}
catch (PersistenceException e)
{
return logical.FALSE;
}
// then drop the before-image data for this table:
try
{
beforeBuf.delete(TemporaryBuffer.MULTIPLEX_FIELD_NAME + "=?", beforeBuf.getMultiplexID());
}
catch (PersistenceException e)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"ACCEPT-CHANGES: Failed to drop before images of " + afterBuf.getDMOName());
}
}
beforeBuf.pruneSessionCache();
afterBuf.getParentTable().setOriginTable(null);
beforeBuf.getParentTable().setOriginTable(null);
return logical.TRUE;
}
/**
* Rejects changes to the data in target object (a temp-table or all temp-tables in a
* {@code DataSet}).
*
* @return {@code true} if operation is successful.
*/
@Override
public logical rejectChanges()
{
if (!isBeforeBuffer())
{
ErrorManager.recordOrShowError(11919, doGetName());
// REJECT-CHANGES run on table <name> that has no BEFORE-TABLE.
return logical.FALSE;
}
TemporaryBuffer buffer = (TemporaryBuffer) buffer();
try
{
buffer.flush();
}
catch (ValidationException e)
{
// normally this should not happen because before tables do not have unique indexes
// TODO: but if it still happens? which error we we throw?
return logical.FALSE;
}
return logical.of(buffer.rejectChanges(afterBufferNative()));
}
/**
* Directly set the {@link #name()} attribute.
*
* @param name
* The name to be set.
*/
protected void nameInternal(character name)
{
this.name = name.getValue();
}
/**
* Tests whether the table from this buffer has a legacy field named {@code legFieldName}.
*
* @param legFieldName
* The legacy field name to be tested.
*
* @return {@code true} if the buffer has a legacy field as specified.
*/
protected boolean hasField(String legFieldName)
{
return buffer().getDmoInfo().byLegacyName(legFieldName) != null;
}
/**
* Tests whether the table from this buffer has a legacy field named {@code legFieldName}.
* Unlike the {@code #hasField} method, this method will validate the field names that also
* contain the buffer name as qualification.
*
* @param legFieldName
* The field name to be tested.
*
* @return {@code true} if the buffer has a legacy field as specified.
*/
protected boolean hasFieldEx(String legFieldName)
{
// normalize name (redundant with DmoMeta.byLegacyName below, but needed for pre-processing)
legFieldName = TextOps.rightTrimLower(legFieldName);
// check whether this is a scoped name
if (legFieldName.contains("."))
{
String prefix = (name == null ? "?" : name) + ".";
if (StringHelper.startsWithIgnoreCase(legFieldName, prefix))
{
legFieldName = legFieldName.substring(prefix.length());
}
else
{
return false; // not this buffer
}
}
if (legFieldName.equals(Session.PK))
{
return true;
}
return buffer().getDmoInfo().byLegacyName(legFieldName) != null;
}
/**
* Actual setting of the query. This is called indirectly, by FWD from the query when this
* buffer is set / added.
*
* @param qry
* The query to associate.
*/
public void setQuery(QueryWrapper qry)
{
if (qry == null || !qry.valid())
{
this.lastQuery = null;
return;
}
this.lastQuery = qry;
}
/**
* Reset the {@link #lastQuery} if is the same as the given one, as the query has been deleted.
*
* @param qry
* The qry to check against.
*/
protected void resetLastQuery(QueryWrapper qry)
{
if (lastQuery == qry)
{
this.lastQuery = null;
}
}
/**
* Sets the current 'parent' dataset. A buffer can be in at most one dataset at a specific
* time. Trying to add it to another one will generate error 11860. This is checked in
* dataset add... procedures.
*
* @param ds
* The new parent {@code DataSet}.
*/
protected void setDataSet(DataSet ds)
{
if (ds != null && this.dataSet != null && this.dataSet.valid())
{
LOG.warning("New dataset configured but one is already active.");
// return internal error ?
}
detachDataSourceImpl();
dataSet = (ds == null) ? null : ds.ref();
lastBatch = false;
}
/**
* Flags the bracketing internal management of BEFORE-BUFFER. ABL programmer's direct
* actions (CREATE/DELETE/ALTER buffer) are outside this bracket and will lead to errors.
* The flag is forwarded to {@code RecordBuffer}.
*
* @param on
* {@code true} to activate bracket, {@code false} to end it and activate errors.
*/
public void setUpBeforeBuffer(boolean on)
{
this.setUpBeforeBuffer = on;
}
/**
* Checks whether we are out of BEFORE-TABLE management mode. In this mode, changes to the
* record in buffer, including CREATE/DELETE <strong>WILL</strong> throw errors.
*
* @return {@code false} when {@link #setUpBeforeBuffer} is set.
*/
public boolean hasBeforeBufferConstraints()
{
return !setUpBeforeBuffer;
}
/**
* Tests whether this is a {@code BEFORE-BUFFER} of a dataset.
*
* @return {@code true} if this is a {@code BEFORE-BUFFER} of a dataset.
*/
public boolean isBeforeBuffer()
{
if (beforeBufferType == BeforeType.UNKNOWN)
{
computeBufferType();
}
return beforeBufferType == BeforeType.BEFORE;
}
/**
* Tests whether this is a {@code AFTER-BUFFER} of a dataset.
*
* @return {@code true} if this is a {@code AFTER-BUFFER} of a dataset.
*/
public boolean isAfterBuffer()
{
if (beforeBufferType == BeforeType.UNKNOWN)
{
computeBufferType();
}
return beforeBufferType == BeforeType.AFTER;
}
/**
* Constructs and returns a string containing the P4GL schema definition of the table. The result is not
* cached so this method is to be used only for debug.
*
* @return a string containing the P4GL legacy schema definition of the table.
*/
public String tableDefinition()
{
return buffer().getDmoInfo().getTableDefinition();
}
/**
* Computes the buffer type and if it is an {@code AFTER} or a {@code BEFORE} the
* peer (complementary) buffer is also stored in {@code peerBuffer}.
*/
void computeBufferType()
{
if (!(this instanceof Temporary))
{
peerBuffer = null;
beforeBufferType = BeforeType.SIMPLE;
return;
}
TempTable tempTable = TempTableBuilder.asTempTable((Temporary) this);
TempTable ttBefore = tempTable.getBeforeTableNative();
if (ttBefore != null)
{
peerBuffer = (BufferImpl) ttBefore.defaultBufferHandleNative();
peerBuffer = peerBuffer.ref();
beforeBufferType = BeforeType.AFTER;
return;
}
TempTable ttAfter = tempTable.getAfterTableNative();
if (ttAfter != null)
{
peerBuffer = (BufferImpl) ttAfter.defaultBufferHandleNative();
peerBuffer = peerBuffer.ref();
beforeBufferType = BeforeType.BEFORE;
return;
}
peerBuffer = null;
beforeBufferType = BeforeType.SIMPLE;
}
/**
* Invokes a defined callback if one exist.
*
* @param event
* Event name.
*
* @return {@code false} if the callback raised an error. If the callback does not exist or ended with
* success {@code true} is returned.
*/
boolean invokeCallback(String event)
{
if (dataSet == null)
{
return true;
}
CallbackData cbd = callbacks == null ? null : callbacks.get(event);
if (cbd == null)
{
return true;
}
return dataSet.invokeCallback(new handle(this), cbd);
}
/**
* Set NAME attribute without any checks.
*
* @param name
* Name to set.
*/
protected void doSetName(String name)
{
this.name = name;
}
/**
* Get the {@code NAME} attribute as java string.
*
* @return the {@code NAME} attribute as a java string. {@code null} if {@code NAME} is {@code unknown}.
*/
public String doGetName()
{
return this.name;
}
/**
* Makes a backup of the current name and it replaces it with another one.
* This is usually used by an argument proxy to temporarily replace the name.
* The override is successful only if the name wasn't overridden beforehand.
*
* @param newName
* The new name which should be temporarily used by this Buffer.
*
* @return true if the overriding was successful, false otherwise.
*/
protected boolean overrideName(String newName)
{
if (oldName != null)
{
return false;
}
oldName = name;
name = newName;
return true;
}
/**
* Restores the name by replacing the current name with the one stored in the backup.
*/
protected void restoreName()
{
name = oldName;
oldName = null;
}
/**
* Check if this resource supports the NEXT-SIBLING attribute. Since the session Buffer list
* is a simple linked list, the returned value is {@code true}. The chain is one-directional,
* starting at SESSION:FIRST-BUFFER.
* <p>
* Only the {@link #dynamic} buffers are stored in the chain. The method will return false for
* static buffers.
*
* @return {@code true} only for dynamic buffers.
*/
@Override
protected boolean hasNextSibling()
{
return buffer() != null && _dynamic();
}
/**
* Check if this resource supports the PREV-SIBLING attribute. Since the session Buffer list
* is only a simple linked list, the returned value is {@code false}. The buffers chain is
* one-directional, starting at SESSION:FIRST-BUFFER. There is no LAST-BUFFER attribute
* associated with the SESSION handle.
*
* @return Always {@code false}.
*/
@Override
protected boolean hasPrevSibling()
{
return false;
}
/**
* Interlinks the newly created HandleChain in the both chains. The back and forth links are
* created with respective elements.
* The heads of the chains are updated: if this is the first of its kind, it becomes the first
* element of the chain. It is always stored as the last resource in his chain.
* Even if this kind of resource does not support NEXT or PREV sibling, both heads of the
* chain are kept for a fast access in future maintenance.
* <p>
* BUFFERs use special order: TEMP-TABLES (static and dynamic in the order they are added)
* first, then PERMANENT buffers at the end of the list.
*/
@Override
protected void interlink()
{
if (!hasNextSibling() && !hasPrevSibling())
{
// quick out this resource is not chain-able
return;
}
String type = type();
boolean permanentTable = this.tableHandleNative() == null;
BufferImpl firstPermanent = (BufferImpl) HandleChain.getHead(PERMANENT_TABLE);
if (permanentTable)
{
// add buffer at the end, like other resources, but don't forget to update the
// first-permanent-buffer pointer:
if (firstPermanent == null)
{
// there are no others, this is the first permanent buffer
HandleChain.setHead(PERMANENT_TABLE, this);
}
// then we continue processing like any other resource,
// appending this resource at the end of the list
}
else
{
// we must insert this TEMP buffer just before the FIRST-PERMANENT pointer
if (firstPermanent != null)
{
if (firstPermanent.prevSibling == null)
{
// there are no TEMP-TABLE buffers? then install in front
HandleChain.setHead(type, this);
firstPermanent.prevSibling = this;
this.nextSibling = firstPermanent;
this.prevSibling = null;
}
else
{
// there are both TEMP and PERMANENT TABLE buffers, insert in between
this.nextSibling = firstPermanent;
this.prevSibling = firstPermanent.prevSibling;
((BufferImpl)this.nextSibling).prevSibling = this;
((BufferImpl)this.prevSibling).nextSibling = this;
}
return; // all processing done here
}
// else there are no other permanent buffers, continue processing like any other
// resource, appending this resource at the end of the list
}
super.interlink();
}
/**
* Check if the resource can veto the delete or not, before attempting the {@link #delete()} call.
*
* @param ref
* The exact handle holding the reference, on which the delete is attempted.
* @param explicit
* Flag indicating this is called via <code>DELETE OBJECT</code> from the application.
*
* @return <code>false</code> if the buffer's {@link #_dataSet() DATASET} is registered as a OUTPUT
* DATASET-HANDLE parameter.
*/
@Override
public boolean allowDelete(handle ref, boolean explicit)
{
DataSet bufds = _dataSet();
if (bufds != null && bufds.valid())
{
DataSetManager dmgr = DataSetManager.instance();
if (dmgr.isOutputDataSetHandle(bufds))
{
// TODO: when (and if) will we delete the buffer?
return false;
}
else if (explicit && _dynamic())
{
ErrorManager.recordOrThrowError(12326,
"Cannot delete temp-table member " + _name() +
" of dataset without deleting the dataset first. (12326)",
false,
true);
return false;
}
}
/*
CA: this code is disabled because 'isOutputTableHandle' checks for reference equality, which
is always false, as 'tableHandle()' always returns a new handle
handle htt = tableHandle();
if (htt._isValid() &&
htt.getResource() instanceof TempTableBuilder &&
BufferManager.get().isOutputTableHandle(htt))
{
// TODO: when (and if) will we delete the buffer?
return false;
}
*/
return true;
}
/**
* Delete the resource.
*
* @return <code>true</code> if the resource was deleted.
*/
@Override
protected boolean resourceDelete()
{
if (FwdServerJMX.JMX_DEBUG)
{
return RESOURCE_DEL.timerWithReturn(this::resourceDeleteImpl);
}
else
{
return resourceDeleteImpl();
}
}
/**
* Delete the resource.
*
* @return <code>true</code> if the resource was deleted.
*/
private boolean resourceDeleteImpl()
{
if (!_dynamic() && !implicitDeletion())
{
// static buffers can't be deleted, but no error message is shown
return false;
}
RecordBuffer recBuf = buffer();
boolean staticTT = recBuf.isTemporary() && !recBuf.isDynamic();
if (staticTT)
{
TempTable tt = tableHandleNative();
if (tt != null && tt.valid())
{
// if this is the default buffer for a GLOBAL SHARED TEMP-TABLE, then do not delete
if (recBuf.isGlobal() && tt.defaultBufferHandleNative() == this)
{
return false;
}
// if there are still other buffers bounded to it, then do not delete.
TemporaryBuffer tbuf = (TemporaryBuffer) recBuf;
if (tbuf.getMasterBuffer() == tbuf && tbuf.hasExplicitBuffers())
{
return false;
}
}
}
deleting = true;
// the order must be preserved: there are checks in resourceDeleted for the buffer's open scope count,
// which gets decremented in 'finished'.
recBuf.resourceDeleted();
recBuf.finished();
SharedVariableManager.cleanupBuffer(recBuf.getDMOProxy());
deleted = true;
deleting = false;
if (staticTT)
{
TemporaryBuffer tbuf = (TemporaryBuffer) recBuf;
TemporaryBuffer mbuf = (TemporaryBuffer) tbuf.getMasterBuffer();
if (!mbuf.isGlobal() && mbuf != tbuf && !mbuf.hasExplicitBuffers())
{
TempTable tt = mbuf.getParentTable();
if (tt != null && tt.valid() && !tt.instantiatingProcedure()._isValid())
{
// if the master table is referenced only from other programs and its program has been deleted,
// and this is the last child buffer referencing it, then delete it, too.
StaticTempTable stt = (StaticTempTable) tt;
stt.setForceDelete(true);
pm.deleteResource(stt);
}
}
}
if (fields != null)
{
for (int i = 0; i < fields.length; i++)
{
if (fields[i] != null)
{
fields[i].forceDelete();
}
}
}
fieldHandles = null;
return true;
}
/**
* Check whether an extent field index is within the valid range {@code 0 ≤ index < max}.
* Raise an error if not, unless errors are silenced.
* <p>
* This method is meant to be called by DMO proxy objects which extend this class, from
* within their indexed setter and getter methods which accept an {@code int} value for the
* element index.
*
* @param index
* Zero-based extent field index to check.
* @param max
* Maximum number of elements in the extent field.
*
* @return {@code true} if {@code index} falls within the valid range,
* {@code false} if outside the range and errors are silenced.
*
* @throws ErrorConditionException
* if {@code index} is outside the valid range and errors are not silenced.
*/
public boolean checkExtentRange(int index, int max)
{
if (index < 0 || index >= max)
{
outOfBoundsError(index);
return false;
}
return true;
}
/**
* Record or throw an "array subscript out of range" error condition for the given subscript.
*
* @param index
* Zero-based subscript value.
*/
protected void outOfBoundsError(int index)
{
String msg = "Array subscript " + (index + 1) + " is out of range";
ErrorManager.recordOrThrowError(26, msg);
}
/**
* Delete the record currently in the buffer, if any.
*
* @param validated
* <code>true</code> if there was validation and it passed. <code>false</code> if
* there was no validation.
*
* @return {@code true} if the delete was possible.
*
* @throws ErrorConditionException
* if no record currently is loaded in the buffer, or if there is
* an error deleting the current record from the database.
*/
protected logical deleteRecord(boolean validated)
{
RecordBuffer recBuf = buffer();
if (!recBuf.isTemporary())
{
if (!verifyTransaction(recBuf))
{
return logical.FALSE;
}
if (!validated)
{
Class<? extends DataModelObject> iface = recBuf.getDMOInterface();
if (TableMapper.hasValidation(iface))
{
String msg = "Table " + recBuf.getDmoInfo().legacyTable + " is defined with schema delete " +
"validation and may only be deleted by the DELETE statement";
ErrorManager.recordOrShowError(7347, msg, false, false);
return logical.FALSE;
}
}
}
if (dataSet != null) // implies buffer.isTemporary()
{
// checks whether tracking is active
if (recBuf.getParentTable()._isTrackingChanges())
{
if (!invokeCallback(ROW_DELETE_EVENT))
{
returnError(); // veto on record delete
}
}
}
try
{
recBuf.delete();
}
catch (PersistenceException exc)
{
return logical.FALSE;
}
return logical.TRUE;
}
/**
* Test whether a string is a valid CALLBACK name for {@code DataSet}s. This method is
* responsible for raising the 11950 error if the name specified is unknown or nor not a name
* of a known callback for {@code DataSet}.
*
* @param someName
* The name to test.
*
* @return {@code true} only if {@code someName} is equals to "BEFORE-FILL", "AFTER-FILL",
* "BEFORE-ROW-FILL", "AFTER-ROW-FILL", "ROW-CREATE", "ROW-DELETE", "ROW-UPDATE",
* "FIND-FAILED", or "SYNCHRONIZE", in ignore-case mode.
*/
private static boolean isCallbackName(String someName)
{
if (someName == null ||
!BEFORE_FILL_EVENT.equalsIgnoreCase(someName) &&
!AFTER_FILL_EVENT.equalsIgnoreCase(someName) &&
!BEFORE_ROW_FILL_EVENT.equalsIgnoreCase(someName) &&
!AFTER_ROW_FILL_EVENT.equalsIgnoreCase(someName) &&
!ROW_CREATE_EVENT.equalsIgnoreCase(someName) &&
!ROW_DELETE_EVENT.equalsIgnoreCase(someName) &&
!ROW_UPDATE_EVENT.equalsIgnoreCase(someName) &&
!FIND_FAILED_EVENT.equalsIgnoreCase(someName) &&
!SYNCHRONIZE_EVENT.equalsIgnoreCase(someName))
{
ErrorManager.recordOrShowError(11950, someName != null ? someName : "");
// Argument to CALLBACK method must be a valid buffer or dataset event name.
return false;
}
return true;
}
/**
* Get the {@link InvokeConfig callsite} for the specified event.
*
* @param eventName
* The event name.
*/
private static InvokeConfig getCallSite(String eventName)
{
switch (eventName.toUpperCase())
{
case BEFORE_FILL_EVENT:
return BEFORE_FILL_CALL_SITE;
case AFTER_FILL_EVENT:
return AFTER_FILL_CALL_SITE;
case BEFORE_ROW_FILL_EVENT:
return BEFORE_ROW_FILL_CALL_SITE;
case AFTER_ROW_FILL_EVENT:
return AFTER_ROW_FILL_CALL_SITE;
case ROW_CREATE_EVENT:
return ROW_CREATE_CALL_SITE;
case ROW_DELETE_EVENT:
return ROW_DELETE_CALL_SITE;
case ROW_UPDATE_EVENT:
return ROW_UPDATE_CALL_SITE;
case FIND_FAILED_EVENT:
return FIND_FAILED_CALL_SITE;
case SYNCHRONIZE_EVENT:
return SYNCHRONIZE_CALL_SITE;
}
return null;
}
/**
* Return {@code LockType} instance for the given internal lock and nowait with corresponding
* error processing.
*
* @param lock
* Integer value.
* @param nowait
* Integer value.
*
* @return {@code LockType} instance.
*
* @throws ErrorConditionException
* If locking operation fails.
*/
private static LockType fromLockNowait(long lock, long nowait)
throws ErrorConditionException
{
LockType lockType = null;
try
{
lockType = LockType.fromLockNowait(lock, nowait);
}
catch (LockTypeException e)
{
String err;
int num;
if (e.isNowaitCorrect())
{
err = "FIND-BY-ROWID, FIND-FIRST etc., second argument must be " +
"NO-LOCK, SHARE-LOCK or EXCLUSIVE-LOCK";
num = 7344;
}
else
{
err = "FIND method argument must be NO-WAIT, or 0 or ?";
num = 7343;
}
ErrorManager.recordOrThrowError(num, err);
}
return lockType;
}
/**
* Implementation of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data
* target, using the specified format.
*
* @param format
* The format ("xml" or "json").
* @param targetData
* Data target.
* @param formatted
* {@code True} to format the data for making it more readable; {@code false} to write it
* as unformatted text. Unknown value is treated as {@code false}.
* @param encoding
* Name of the character encoding to use.
* @param omitInitial
* {@code True} to omit data for fields whose values match their initial values.
* @param omitOuter
* {@code True} to omit data for the outermost object from the output.
*
* @return {@code True} if the operation completed successfully, else {@code false}.
*/
private boolean serializeRowImpl(String format,
TargetData targetData,
Boolean formatted,
String encoding,
Boolean omitInitial,
Boolean omitOuter)
{
try
{
if (!buffer().isTemporary())
{
ErrorManager.recordOrShowError(13158);
// XML Methods only allowed on TEMP-TABLE buffers.
return false;
}
// the buffer must not be empty
if (!_available())
{
ErrorManager.recordOrShowError(16017);
// No record in buffer for SERIALIZE-ROW.
return false;
}
// check the specified target format
if (format == null)
{
ErrorManager.recordOrShowError(16019, "unknown", "");
// Invalid target-format '<format>' for SERIALIZE-ROW.
return false;
}
boolean isXML = "XML".equalsIgnoreCase(format);
boolean isJSON = "JSON".equalsIgnoreCase(format);
if (!isJSON && !isXML)
{
ErrorManager.recordOrShowError(16019, format, "");
// Invalid target-format '<format>' for SERIALIZE-ROW.
return false;
}
if (formatted == null)
{
formatted = false;
}
if (omitInitial == null)
{
omitInitial = false;
}
if (omitOuter == null)
{
omitOuter = false;
}
if (isXML)
{
if (omitOuter)
{
ErrorManager.recordOrShowError(16020);
// omit-outer-object argument not allowed in SERIALIZE-ROW for XML
ErrorManager.recordOrShowError(4065, false, "SERIALIZE-ROW", "BUFFER widget");
// **The <attribute> attribute on the <widget id> has invalid arguments.
return false;
}
XmlExport export = new XmlExport(targetData, formatted, encoding);
return export.writeRecord(this, omitInitial);
}
else
{
// is JSON
com.fasterxml.jackson.core.JsonEncoding jsonEnc =
JsonExport.parseEncoding(new character(encoding));
if (jsonEnc == null)
{
ErrorManager.recordOrShowError(15342, encoding, "WRITE-JSON");
// Invalid encoding '<encoding>' argument for <method>.
return false;
}
JsonExport export = new JsonExport();
return export.exportRecord(this, targetData, formatted, jsonEnc, omitInitial, omitOuter);
}
}
finally
{
try
{
targetData.close();
}
catch (IOException e)
{
// TODO: log
}
}
}
/**
* Obtain the list of data-source keys of this buffer. If this buffer is not in a DATA-SOURCE, {@code null}
* is returned.
*
* @return the list of data-source keys of this buffer.
*/
List<String> keysList()
{
return keys;
}
/**
* Sets the {@code keys} for this source buffer, related to the specified {@code DataSource}. If a
* DATA-SOURCE was already configured using the buffer, an error message will be thrown back.
*
* @param parent
* The parent {@code DataSource}.
* @param keys
* The array of fields used as keys.
*
* @return {@code true} only if operation is successful.
*/
boolean keys(DataSource parent, String[] keys)
{
if (parentDataSource != null && parentDataSource.valid())
{
ErrorManager.recordOrThrowError(12298, doGetName(), "");
// Buffer <name> may be in only one data-source at a time. Use different buffers for the same table. (12298)
return false;
}
this.keys = Arrays.asList(keys);
this.parentDataSource = parent;
return true;
}
/**
* Clear the {@link #parentDataSource} and {@link #keys}, as the {@link #parentDataSource} has been
* deleted.
*/
void clearParentDataSource()
{
this.parentDataSource = null;
this.keys = null;
}
/**
* Getter of NUM-FIELDS attribute. Only normal fields are counted, the hidden fields are not
* taken into consideration.
*
* @return current value of NUM-FIELDS attribute.
*/
int _numFields()
{
return buffer().getNumFields();
}
/**
* Execute a dynamic FIRST, LAST or UNIQUE find query. All the public overloaded methods
* (findFirst, findLast and findUnique) will call this private implementation using the
* appropriate parameters. Internally this method uses the DynamicQueryHelper.parseFindQuery
* static parser to return a FindQuery object using the passed predicate and locking mode.
* After the FindQuery is obtained, the appropriate query is executed (FIRST, LAST or UNIQUE)
* and the result is returned to caller method.
* <p>
* If the method fails, it does not raise an error but displays a message. To suppress it,
* the method should be called with NO-ERROR clause.
*
* @param predicate
* Predicate expression for find that evaluates to the following syntax:
* [ WHERE [ logical-expression ] ] [ USE-INDEX index-name ]
* <ul>
* <li>Once evaluated, predicate-expression can contain only constants and
* unabbreviated references to fields from the buffer.
* <li>The predicate-expression itself can be built using a concatenation of
* character expressions.
* </ul>
* @param mode
* Search mode, one of FIRST, LAST or UNIQUE.
* @param lock
* Lock type. May additionally have no-wait flag set.
* @param substBuffers
* A list of substitution buffers.
*
* @return {@code true} if operation succeeds, else {@code false}.
*/
logical _find(String predicate, FindMode mode, LockType lock, Buffer... substBuffers)
{
final String qName = "find_" + mode + "_" + lock;
List<Buffer> substs = new ArrayList<>();
if (substBuffers.length > 0)
{
substs.addAll(Arrays.asList(substBuffers));
}
if (parentDataSource != null)
{
handle hbuf = parentDataSource.getDatasetBuffer();
if (hbuf._isValid())
{
BufferImpl buf = (BufferImpl) hbuf.getResource();
if (!substs.contains(buf))
{
substs.add(buf);
}
buf = buf.beforeBufferNative();
if (buf != null && !substs.contains(buf))
{
substs.add(buf);
}
}
}
P2JQuery query = DynamicQueryHelper.parseFindQuery(this, predicate, lock, qName, substs);
if (query == null)
{
String msg = "Invalid WHERE clause in FIND method for buffer " + buffer().getLegacyName() + " (10041)";
ErrorManager.recordOrShowError(
new int[] { 10041 },
new String[] { msg },
false, false, false, true, false); // no DOT at the end of error message!
return logical.FALSE;
}
boolean success = false;
try
{
switch (mode)
{
case FIRST:
query.first(lock);
break;
case LAST:
query.last(lock);
break;
case UNIQUE:
query.unique(lock);
break;
}
if (!ErrorManager.isPendingError())
{
success = _available();
}
}
catch (ErrorConditionException e)
{
// the error condition already has been handled by the ErrorManager, so we just return failure;
// unexpected, unchecked exceptions are allowed to unwind the stack
success = false;
}
return logical.of(success);
}
/**
* Detaches the {@code DataSource} from the temp-table buffer in a {@code DataSet} object.
*/
void detachDataSourceImpl()
{
// TODO: check if something is vetoing the detach process
if (dataSource != null)
{
dataSource.attach(null, null);
dataSource = null;
if (isAfterBuffer())
{
peerBuffer.dataSource = null;
}
}
attachedPairlist = null;
dataSrcMappingStr = null;
dataSrcMapping = null;
dstFieldMeta = null;
}
/**
* Clear the {@link #reusableQuery query} used for FILL operations.
*/
void clearQuery()
{
if (reusableQuery != null)
{
reusableQuery.setDataRelation(null);
reusableQuery.queryClose();
reusableQuery = null;
}
}
/**
* Internal implementation of FIND-CURRENT() method.
* <p>
* Changes the lock type for the record currently in the buffer. Refreshes the record if it was
* changed by other client. Resets the CURRENT-CHANGED attribute accordingly.
*
* @param lockType
* Extended lock type (includes the NO-WAIT status).
*
* @return {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code false}.
*/
private logical _current(LockType lockType)
{
RecordBuffer recBuf = buffer();
// make sure to ignore errors caused by LockUnavailableException
try
{
recBuf.txHelper.errHlp.ignore(() ->
{
recBuf.armCurrentChanged();
recBuf.reload(lockType, false);
});
}
finally
{
recBuf.updateCurrentChanged();
}
// NOTE: the reference manual states that if lock operation fails, a message is displayed.
// However, this is not how the behaviour was observed. Actually there is no error
// whatsoever. Instead, the buffer is discarded after a failure to acquire the lock.
// The error was not observable, in fact at all, in all my testcases.
return logical.of(recBuf.isAvailable());
}
/**
* Implementation of the merge changes. Always to be called for the AFTER-BUFFER. It will also
* process the changes for the BEFORE BUFFER (the DELETE changes only), too.
*
* @param porigBuf
* The original AFTER buffer.
* @param allCopyMode
* Use {@code true} to indicates that all rows of the source after-image table must be
* merged.
*
* @return {@code true} on success. When {@code false} is returned the error code was already
* configured in {@code ErrorManager}.
*/
private boolean mergeChangesImpl(BufferImpl porigBuf, boolean allCopyMode)
{
if (!isAfterBuffer() || !porigBuf.isAfterBuffer())
{
ErrorManager.recordOrShowError(11925, doGetName());
// GET/MERGE-CHANGES require a BEFORE-TABLE for table <name>.
return false;
}
logical ok = new logical(true);
TemporaryBuffer afterBuffer = (TemporaryBuffer) buffer();
TemporaryBuffer beforeBuffer = (TemporaryBuffer) peerBuffer.buffer();
Class<? extends Record> origAfterImplClass = porigBuf.buffer().getDmoInfo().getImplementationClass();
Class<? extends Record> origBeforeImplClass = porigBuf.beforeBufferNative()
.buffer().getDmoInfo().getImplementationClass();
Class<? extends Record> afterImplClass = afterBuffer.getDmoInfo().getImplementationClass();
Session session = afterBuffer.getPersistence().getSession(Persistence.TEMP_CTX);
beforeBuffer.readAllRows(
(dmo) -> {
if (!ok.booleanValue())
{
return;
}
// prepare the original before image
TempRecord origBefRecord = null;
try
{
origBefRecord = (TempRecord) session.get(origBeforeImplClass, dmo._originRowid());
}
catch (PersistenceException | ConditionException e)
{
// origBefRecord stays null
}
if (origBefRecord == null)
{
// this should not happen. The origin-rowid of the dmo should not be unknown! If it was not
// found then the changes in the original table were already accepted (?)
ErrorManager.recordOrShowError(11928, doGetName(), porigBuf.doGetName());
// INVALID origin-rowid of the change table <name> of <name>.
ErrorManager.recordOrShowError(11926);
// Found unexpected error while merging changes.
ok.assign(false);
return;
}
Integer errFlags = dmo._errorFlags();
if (errFlags != null && errFlags != 0)
{
BufferImpl porigBefBuf = porigBuf.peerBuffer;
porigBefBuf.buffer().loadRecord(origBefRecord);
// if the record's any error/rejected markers were set, drop it from merge operation
ok.assign(porigBefBuf.rejectRowChanges());
}
else
{
// prepare the original after image and put it in the [porigBuf] buffer
TempRecord origRecord = null;
try
{
origRecord = (TempRecord) session.get(origAfterImplClass, origBefRecord._peerRowid());
}
catch (PersistenceException | ConditionException e)
{
// origRecord stays null
}
porigBuf.buffer().loadRecord(origRecord);
// prepare the after image/record in current buffer
TempRecord afterRecord;
try
{
afterRecord = (TempRecord) session.get(afterImplClass, dmo._peerRowid());
}
catch (PersistenceException | ConditionException e)
{
afterRecord = null;
}
afterBuffer.loadRecord(afterRecord);
// run the merge-row-changes code for prepared records
ok.assign(mergeRowChangesImpl(this.ref(), porigBuf.ref()));
}
});
return ok.booleanValue();
}
/**
* Implementation of the row merge changes. Always to be called for the AFTER-BUFFER.
*
* @param changesBuf
* The changes AFTER buffer.
* @param origAfterBuf
* The original AFTER buffer.
*
* @return {@code true} on success. When {@code false} is returned the error code was already
* configured in {@code ErrorManager}.
*/
private static boolean mergeRowChangesImpl(BufferImpl changesBuf, BufferImpl origAfterBuf)
{
if (changesBuf.rowState().isUnknown() || changesBuf.rowState().intValue() == Buffer.ROW_UNMODIFIED)
{
return true; // unmodified record, nothing to merge
}
int recState = changesBuf.rowState().intValue();
boolean isError = changesBuf.error().booleanValue();
if (recState != Buffer.ROW_DELETED)
{
if (changesBuf.beforeBufferType == BeforeType.AFTER && isError)
{
changesBuf.peerBuffer.findByRowID(changesBuf.beforeRowid());
changesBuf = changesBuf.peerBuffer;
}
else if (changesBuf.beforeBufferType == BeforeType.BEFORE && !isError)
{
// on error keep the original (before), on non error, keep the change (after)
changesBuf.peerBuffer.findByRowID(changesBuf.afterRowid());
changesBuf = changesBuf.peerBuffer;
}
}
TempRecord dmo = (TempRecord) changesBuf.buffer().getCurrentRecord();
if (dmo == null)
{
LOG.log(Level.SEVERE, "mergeRowChangesImpl: null TempRecord in changesBuf");
return true;
}
Long originB4Rowid = dmo._originRowid();
BufferImpl origB4Buffer = origAfterBuf.peerBuffer;
if (recState == ROW_DELETED)
{
if (dmo._rowState() == Buffer.ROW_DELETED &&
origB4Buffer.findByRowID(new rowid(originB4Rowid)).getValue())
{
if (origB4Buffer.rowState().intValue() != Buffer.ROW_DELETED)
{
ErrorManager.recordOrShowError(11928, changesBuf.doGetName(), origAfterBuf.doGetName());
// INVALID origin-rowid of the change table <name> of <name>.
ErrorManager.recordOrShowError(11926);
// Found unexpected error while merging changes.
return false;
}
// just remove the BEFORE-IMAGE, the AFTER-IMAGE is long gone
origB4Buffer.setUpBeforeBuffer(true);
origB4Buffer.deleteRecord();
origB4Buffer.setUpBeforeBuffer(false);
}
return true;
}
// else CREATE / MODIFIED
if (originB4Rowid == null)
{
ErrorManager.recordOrShowError(11923, origAfterBuf.doGetName(), changesBuf.doGetName());
// MERGE-CHANGES run on original table <name> that does not match the ORIGIN-HANDLE in the GET-CHANGES table <name>.
return false;
}
else if (!origB4Buffer.findByRowID(new rowid(originB4Rowid)).getValue())
{
ErrorManager.recordOrShowError(11928, changesBuf.doGetName(), origAfterBuf.doGetName());
// INVALID origin-rowid of the change table <name> of <name>.
ErrorManager.recordOrShowError(11926);
// Found unexpected error while merging changes.
return false;
}
Long afterRowid = origB4Buffer.afterRowid().getValue();
if (afterRowid == null)
{
ErrorManager.recordOrShowError(11926);
// Found unexpected error while merging changes.
return false;
// exits lambda, continue with next record.
// The error message will be displayed multiple times, one for each record.
}
else if (!origAfterBuf.findByRowID(new rowid(afterRowid)).getValue())
{
ErrorManager.recordOrShowError(11923, origAfterBuf.doGetName(), changesBuf.doGetName());
// MERGE-CHANGES run on original table <name> that does not match the ORIGIN-HANDLE in the GET-CHANGES table <name>.
return false;
// exits lambda, continue with next record.
// The error message will be displayed multiple times, one for each record.
}
RecordBuffer origAfterRecBuf = origAfterBuf.buffer();
TempRecord dstRec = (TempRecord) origAfterRecBuf.getCurrentRecord();
Record snapshot = null;
if (recState == ROW_CREATED && isError)
{
origAfterBuf.deleteRecord(); // on error do a rollback = delete
}
else
{
if (dstRec._rowState() == null || dstRec._rowState() == Buffer.ROW_UNMODIFIED)
{
ErrorManager.recordOrShowError(11928, changesBuf.doGetName(), origAfterBuf.doGetName());
// INVALID origin-rowid of the change table <name> of <name>.
ErrorManager.recordOrShowError(11926);
// Found unexpected error while merging changes.
return false;
}
snapshot = dstRec.snapshot();
// accepts the changes by copying the after-image table rows to the corresponding
// after-image table rows in the original DataSet temp-table.
BaseRecord.copy(dstRec, dmo, false);
}
Long oldBefore = dstRec._peerRowid();
if (origAfterBuf.isAfterBuffer() && oldBefore != null)
{
// removes the before-image table row (if it has one).
if (origB4Buffer.findByRowID(new rowid(oldBefore)).getValue())
{
origB4Buffer.setUpBeforeBuffer(true);
origB4Buffer.deleteRecord();
origB4Buffer.setUpBeforeBuffer(false);
}
}
// also:
// sets the BEFORE-ROWID attribute of the row in the after-image table to the Unknown value (?)
// sets the ROW-STATE of the row in the after-image table to ROW-UNMODIFIED (0)
// resets ERROR/REJECTED attributes and ERROR-STRING to unknown value.
dstRec._peerRowid(null);
dstRec._errorFlags(null);
dstRec._errorString(null);
dstRec._peerRowid(null);
dstRec._rowState(Buffer.ROW_UNMODIFIED);
try
{
Map<String, List<Integer>> props = new HashMap<>();
props.put(Buffer.ROW_STATE_FIELD, null);
props.put(Buffer.AFTER_ROWID_FIELD, null);
props.put(Buffer.ERROR_FLAG_FIELD, null);
props.put(Buffer.ERROR_STRING_FIELD, null);
origAfterRecBuf.getChangeBroker().stateChanged(
RecordChangeEvent.Type.UPDATE, origAfterRecBuf, dstRec, snapshot, props);
}
catch (PersistenceException e)
{
ErrorManager.recordOrShowError(11926);
// Found unexpected error while merging changes.
return false;
}
// great, return with success
return true;
}
/**
* Sets the {@code _originRowid} internal field and register the change with the {@code ChangeBroker} to
* make sure the change is persisted.
*
* @param val
* The new value for {@code _originRowid} field. May be {@code null} for an unknown.
*/
private void setOriginRowidImpl(Long val)
{
RecordBuffer recBuf = buffer();
TempRecord dmo = (TempRecord) recBuf.getCurrentRecord();
TempRecord snapshot = (TempRecord) dmo.deepCopy();
dmo._originRowid(val);
try
{
Map<String, List<Integer>> props = new HashMap<>();
props.put(Buffer.ORIGIN_ROWID_FIELD, null);
recBuf.getChangeBroker().stateChanged(RecordChangeEvent.Type.UPDATE, recBuf, dmo, snapshot, props);
}
catch (PersistenceException e)
{
LOG.warning("Caught exception:", e);
}
}
/**
* Sets the {@code _dataSourceRowid} internal field and register the change with the {@code ChangeBroker}
* to make sure the change is persisted.
*
* @param val
* The new value for {@code _dataSourceRowid} field. May be {@code null} for an unknown.
*/
private void setDatasourceRowidImpl(Long val)
{
RecordBuffer recBuf = buffer();
TempRecord dmo = (TempRecord) recBuf.getCurrentRecord();
TempRecord snapshot = (TempRecord) dmo.deepCopy();
dmo._datasourceRowid(val);
try
{
Map<String, List<Integer>> props = new HashMap<>();
props.put(Buffer.DATA_SOURCE_ROWID_FIELD, null);
recBuf.getChangeBroker().stateChanged(RecordChangeEvent.Type.UPDATE, recBuf, dmo, snapshot, props);
}
catch (PersistenceException e)
{
LOG.warning("Caught exception:", e);
}
}
/**
* Partial implementation of {@code SAVE-ROW-CHANGES} method. Validates some parameters and
* calls the final worker.
*
* @param bufNo
* The 0-base buffer index in the attached {@code DataSource}.
* @param skip
* A list of field names that should not be assigned after a new row is created
* (comma-separated).
* @param noLobs
* Use {@code true} to specify the BLOB and CLOB fields should be ignored in the save
* operation.
*
* @return {@code true} if operation is successful.
*/
private logical saveRowChangesImpl(int bufNo, String skip, boolean noLobs)
{
if (!isBeforeBuffer())
{
ErrorManager.recordOrShowError(11938, doGetName());
// SAVE-ROW-CHANGES must be run on the BEFORE table of buffer <name>.
return logical.FALSE;
}
BufferImpl after = afterBufferNative();
DataSource dsrc = after.ref().dataSource;
if (dsrc == null || !dsrc.valid())
{
ErrorManager.recordOrShowError(11942, doGetName(), after.doGetName());
// SAVE-ROW-CHANGES for BEFORE buffer <name>, requires that the corresponding AFTER dataset member buffer <name> be attached to a DATA-SOURCE.
return logical.FALSE;
}
return saveRowChangesImpl2(dsrc, after, bufNo, skip, noLobs);
}
/**
* Implementation of {@code SAVE-ROW-CHANGES} method. Validates some parameters and
* calls the final worker.
*
* @param bufName
* The buffer name in the attached {@code DataSource}.
* @param skip
* A list of field names that should not be assigned after a new row is created
* (comma-separated).
* @param noLobs
* Use {@code true} to specify the BLOB and CLOB fields should be ignored in the save
* operation.
*
* @return {@code true} if operation is successful.
*/
private logical saveRowChangesImpl(String bufName, String skip, boolean noLobs)
{
if (!isBeforeBuffer())
{
ErrorManager.recordOrShowError(11938, doGetName());
// SAVE-ROW-CHANGES must be run on the BEFORE table of buffer <name>.
return logical.FALSE;
}
BufferImpl after = afterBufferNative();
DataSource dsrc = after.ref().dataSource;
if (dsrc == null || !dsrc.valid())
{
ErrorManager.recordOrShowError(11942, doGetName(), after.doGetName());
// SAVE-ROW-CHANGES for BEFORE buffer <name>, requires that the corresponding AFTER dataset member buffer <name> be attached to a DATA-SOURCE.
return logical.FALSE;
}
return saveRowChangesImpl2(dsrc, after, dsrc.getSourceBufferIndex(bufName), skip, noLobs);
}
/**
* Implementation of {@code SAVE-ROW-CHANGES} method.
*
* @param dSrc
* The {@code DataSource} that will be updated.
* @param after
* The AFTER-BUFFER to which the {@code DataSource} is attached.
* @param bufNo
* The 0-base buffer index in the attached {@code DataSource}.
* @param skip
* A list of field names that should not be assigned after a new row is created
* (comma-separated).
* @param noLobs
* Use {@code true} to specify the BLOB and CLOB fields should be ignored in the save
* operation.
*
* @return {@code true} if operation is successful.
*/
private logical saveRowChangesImpl2(DataSource dSrc,
BufferImpl after,
int bufNo,
String skip,
boolean noLobs)
{
if (!_available())
{
ErrorManager.recordOrShowError(11935, false, doGetName());
// SAVE-ROW-CHANGES buffer <name> not available.
return logical.FALSE;
}
if (bufNo < 0 || bufNo >= dSrc._getNumSourceBuffers())
{
ErrorManager.recordOrShowError(11906, false);
// Unable to identify data-source buffer for SAVE-ROW-CHANGES.
return logical.FALSE;
}
BufferImpl srcBuff = dSrc.getSourceBufferNative(bufNo + 1);
Set<String> skipFields = new HashSet<>();
if (skip != null && !skip.isEmpty())
{
String[] skipList = skip.split(",");
for (String tok : skipList)
{
if (tok.contains(".") || !srcBuff.hasField(tok))
{
ErrorManager.recordOrShowError(11909, false, tok, srcBuff.doGetName());
// Unable to find sequence field <name> in table <name> during SAVE-ROW-CHANGES.
return logical.FALSE;
}
skipFields.add(tok);
}
}
String saveWhere = dSrc.getSaveWhereString(bufNo + 1).toJavaType();
int rowState = rowState().intValue();
character allPairs = new character();
character allSrcFields = new character();
if (rowState != ROW_DELETED)
{
StringBuilder sb = new StringBuilder();
StringBuilder sbAllSrc = new StringBuilder();
for (Pair<String, DataSource.FieldReference> entry : after.dataSrcMapping)
{
String srcFld = entry.getValue().getName();
String dsFld = entry.getKey();
if (sbAllSrc.length() != 0)
{
sbAllSrc.append(",");
}
sbAllSrc.append(srcFld);
if (entry.getValue().getBuffer() == srcBuff && !skipFields.contains(srcFld))
{
if (sb.length() != 0)
{
sb.append(",");
}
sb.append(dsFld).append(",").append(srcFld);
}
}
allPairs.assign(sb.toString());
allSrcFields.assign(sbAllSrc.toString());
}
logical retval = new logical();
OnPhrase[] onPhrase0 = new OnPhrase[]{new OnPhrase(Condition.ERROR, Action.THROW, "save-row-changes-label")};
doBlock(TransactionType.FULL, "save-row-changes-label", onPhrase0, new Block() {
@Override
public void body()
{
if (rowState == ROW_CREATED)
{
srcBuff.create();
after.findByRowID(afterRowid());
logical ok = TemporaryBuffer.copy(
srcBuff.buffer(), after.buffer(), allSrcFields, allPairs, logical.of(noLobs));
if (ok.getValue())
{
after.setDatasourceRowidImpl(srcBuff.rowID().getValue());
}
if (!ok.booleanValue())
{
setErrorOnSave();
}
retval.assign(ok);
return;
}
// first, detect whether the record loaded in buffer has been changed. If so (changed or modified),
// update the DATA-SOURCE-MODIFIED attribute for me, my temp-table, the temp-table of the
// after-buffer, and dataset. The newly created record will not affect this attribute.
// NOTE: the attribute/function name suggests that the data source buffer was altered but, from my
// tests, changing the source buffers does not affect this attribute. Having a modified or
// deleted before-image loaded in before-buffer(this) will set the attribute, instead.
if (rowState == ROW_MODIFIED || rowState == ROW_DELETED)
{
BufferImpl.this.setDataSourceModified(true);
tableHandleNative().setDataSourceModified(true);
after.tableHandleNative().setDataSourceModified(true);
peerBuffer.dataSet.setDataSourceModified(true);
}
logical ok = new logical();
ErrorManager.silent(() -> ok.assign(
srcBuff._find(saveWhere, FindMode.UNIQUE, LockType.EXCLUSIVE, BufferImpl.this)));
if (ok.isUnknown() || !ok.getValue() || !srcBuff._available())
{
if (rowState == ROW_DELETED)
{
// this is a silent acceptance of the fact that the record was deleted from both
// locations (source and dataset table)
// in any case, update peer: the deleted row is now gone for good
setOriginRowidImpl(null);
retval.assign(true);
return;
}
ErrorManager.recordOrShowError(11912, false, srcBuff.doGetName());
// Unable to find record for db buffer <name> during SAVE-ROW-CHANGES
setErrorOnSave();
retval.assign(false);
return;
}
if (rowState == ROW_DELETED)
{
srcBuff.deleteRecord();
// update peer: the deleted row is now gone for good
setOriginRowidImpl(null);
retval.assign(true);
return;
}
if (rowState == ROW_MODIFIED)
{
after.findByRowID(afterRowid());
boolean preferDataset = dSrc.getPreferDataset().getValue();
boolean mergeByField = dSrc.getMergeByField().getValue();
ArrayList<TableMapper.LegacyFieldInfo> myFields =
TableMapper.getLegacyOrderedList(BufferImpl.this, true);
logical res = new logical();
try
{
if (preferDataset)
{
if (!mergeByField)
{
// overwrite and lose any DB changes to entire row
res.assign(TemporaryBuffer.copy(srcBuff.buffer(),
after.buffer(),
allSrcFields,
allPairs,
logical.of(noLobs)));
}
else
{
// save DS changes. Non overlapping DB field changes are kept.
StringBuilder sb = new StringBuilder();
for (int i = 0; i < myFields.size(); i++)
{
TableMapper.LegacyFieldInfo fieldInfo = myFields.get(i);
String fldName = fieldInfo.getLegacyName();
boolean modified;
if (fieldInfo.getExtent() == 0) // scalar mode
{
BaseDataType fldVal = Session.PK.equals(fldName) ? after.rowID()
: after.dereference(fldName);
modified = !areBDTsEqual(fldVal, dereference(fldName));
}
else //extent mode
{
modified = false;
BaseDataType[] srcVals = (BaseDataType[]) srcBuff.derefPoly(fldName);
BaseDataType[] originalVals = (BaseDataType[]) derefPoly(fldName);
for (int j = 0; j < srcVals.length; j++)
{
if (!areBDTsEqual(srcVals[j], originalVals[j]))
{
modified = true;
break;
}
}
}
if (modified)
{
DataSource.FieldReference srcField = getMappedField(after, fldName);
if (srcField == null || srcField.getBuffer() != srcBuff)
{
continue;
}
String srcName = srcField.getName();
if (sb.length() != 0)
{
sb.append(",");
}
sb.append(fldName).append(",").append(srcName);
}
}
character dsChangedPairs = new character(sb.toString());
res.assign(TemporaryBuffer.copy(srcBuff.buffer(),
after.buffer(),
allSrcFields,
dsChangedPairs,
logical.of(noLobs)));
}
}
else
{
if (!mergeByField)
{
// if any field was modified in database raise error, otherwise update all fields
for (int j = 0; j < myFields.size(); j++)
{
TableMapper.LegacyFieldInfo fieldInfo = myFields.get(j);
String fldName = fieldInfo.getLegacyName();
DataSource.FieldReference srcField = getMappedField(after, fldName);
if (srcField == null || srcField.getBuffer() != srcBuff)
{
continue;
}
String srcName = srcField.getName();
boolean changed;
if (fieldInfo.getExtent() == 0) // scalar mode
{
BaseDataType srcVal = Session.PK.equals(srcName) ? srcBuff.rowID()
: srcBuff.dereference(srcName);
changed = !areBDTsEqual(srcVal, (dereference(fldName)));
}
else // extent mode
{
changed = false;
BaseDataType[] srcVals = (BaseDataType[]) srcBuff.derefPoly(srcName);
BaseDataType[] originalVals = (BaseDataType[]) derefPoly(fldName);
for (int i = 0; i < srcVals.length; i++)
{
if (!areBDTsEqual(srcVals[j], originalVals[j]))
{
changed = true;
break;
}
}
}
// before-buffer and source must be the same (otherwise, there was a change
// in the database).
if (changed)
{
// synchronize before & after buffers with the source
TemporaryBuffer.copy(after.buffer(),
srcBuff.buffer(),
allSrcFields,
allPairs,
logical.of(noLobs));
setUpBeforeBuffer(true);
TemporaryBuffer.copy(buffer(),
srcBuff.buffer(),
allSrcFields,
allPairs,
logical.of(noLobs));
setUpBeforeBuffer(false);
// set ERROR & DATA-SOURCE-MODIFIED attributes
ErrorManager.recordOrShowError(11913, false, srcBuff.doGetName());
// SAVE-ROW-CHANGES found <name> record with conflicting change by another user.
setErrorOnSave();
res.assign(false);
break;
}
}
if (res.isUnknown()) // ie. no 11913 was encountered
{
res.assign(TemporaryBuffer.copy(srcBuff.buffer(),
after.buffer(),
allSrcFields,
allPairs,
logical.of(noLobs)));
}
}
else
{
// attempt to save DS changes for each field. If the field was altered in source
// buffer raise error and return.
StringBuilder sb = new StringBuilder();
nextField: for (int i = 0; i < myFields.size(); i++)
{
TableMapper.LegacyFieldInfo fieldInfo = myFields.get(i);
String fldName = fieldInfo.getLegacyName();
boolean isExtent = fieldInfo.getExtent() != 0;
boolean sourceModified = false;
String srcName = null;
if (isExtent)
{
BaseDataType[] fldVals = (BaseDataType[]) after.derefPoly(fldName);
BaseDataType[] originalVals = (BaseDataType[]) derefPoly(fldName);
boolean modified = false;
for (int j = 0; j < fldVals.length; j++)
{
if (!areBDTsEqual(fldVals[j], originalVals[j]))
{
modified = true;
DataSource.FieldReference srcField = getMappedField(after, fldName);
if (srcField == null || srcField.getBuffer() != srcBuff)
{
continue nextField;
}
srcName = srcField.getName();
sourceModified = !areBDTsEqual(srcBuff.dereference(j + 1, srcName), originalVals[j]);
break;
}
}
if (!modified)
{
continue;
}
}
else
{
BaseDataType fldVal;
BaseDataType originalVal;
if (Session.PK.equals(fldName))
{
fldVal = after.rowID();
originalVal = rowID();
}
else
{
fldVal = after.dereference(fldName);
originalVal = dereference(fldName);
}
if (!areBDTsEqual(fldVal, originalVal))
{
DataSource.FieldReference srcField = getMappedField(after, fldName);
if (srcField == null || srcField.getBuffer() != srcBuff)
{
continue;
}
srcName = srcField.getName();
BaseDataType srcVal = Session.PK.equals(srcName) ? srcBuff.rowID()
: srcBuff.dereference(srcName);
sourceModified = !areBDTsEqual(srcVal, originalVal);
}
else
{
continue;
}
}
if (sourceModified)
{
// synchronize before & after buffers with the source
TemporaryBuffer.copy(after.buffer(),
srcBuff.buffer(),
allSrcFields,
allPairs,
logical.of(noLobs));
setUpBeforeBuffer(true);
TemporaryBuffer.copy(buffer(),
srcBuff.buffer(),
allSrcFields,
allPairs,
logical.of(noLobs));
setUpBeforeBuffer(false);
// set ERROR & DATA-SOURCE-MODIFIED attributes
ErrorManager.recordOrShowError(11913, false, srcBuff.doGetName());
// SAVE-ROW-CHANGES found <name> record with conflicting change by another user.
res.assign(false);
break;
}
if (sb.length() != 0)
{
sb.append(",");
}
sb.append(fldName).append(",").append(srcName);
}
if (res.isUnknown()) // ie. no 11913 was encountered
{
res.assign(TemporaryBuffer.copy(srcBuff.buffer(),
after.buffer(),
allSrcFields,
new character(sb.toString()),
logical.of(noLobs)));
}
}
}
}
catch (ErrorConditionException ex)
{
ErrorManager.recordOrShowError(11910, srcBuff.doGetName());
// SAVE-ROW-CHANGES was unable to update the database table <table>.
setErrorOnSave();
retval.assign(false);
return;
}
srcBuff.release();
if (!res.booleanValue())
{
setErrorOnSave();
}
retval.assign(res);
return;
}
}
});
if (retval.isUnknown())
{
// something has gone wrong, there is no other valid value for [rowState]
setErrorOnSave();
retval.assign(false);
}
return retval;
}
/**
* A method for comparing BDTs, used to detect modified fields in saveRowChangesImpl2.
* character fields are compared using their String representation, this matches
* SAVE-ROW-CHANGES behavior in 4GL.
*
* @param op1
* The first operand of the method.
* @param op2
* The second operand of the method.
*
* @return true if the operands' values are equal.
*/
private boolean areBDTsEqual(BaseDataType op1, BaseDataType op2)
{
if (op1 instanceof Text && op2 instanceof Text)
{
return Text.valueEquals((Text) op1, (Text) op2);
}
else
{
return op1.equals(op2);
}
}
/**
* Returns the mapped {@code FieldReference} of a specific field of a buffer. To obtain the result to be
* returned, the buffer's {@code dataSrcMapping} is iterated for a matching key. If the buffer hasn't a
* datasource attached (hence the {@code dataSrcMapping}) is {@code null} or the field has no mapping,
* {@code null} is returned.
*
* @param buffer
* The buffer to be investigated.
* @param fldName
* The field name.
*
* @return The {@code FieldReference} mapped to the field in the attached data source or {@code null}
* otherwise.
*/
static DataSource.FieldReference getMappedField(BufferImpl buffer, String fldName)
{
if (buffer.dataSrcMapping == null)
{
return null;
}
for (Pair<String, DataSource.FieldReference> entry : buffer.dataSrcMapping)
{
if (entry.getKey().equalsIgnoreCase(fldName))
{
return entry.getValue();
}
}
return null;
}
/**
* Sets the {@code ERROR} attribute to {@code true} after a failed SAVE-ROW operation.
* Affected entities are:
* <ul>
* <li>current IMAGE (it is a BEFORE IMAGE)</li>
* <li>the AFTER-IMAGE of this BEFORE-IMAGE</li>
* <li>the TEMP-TABLE of current BUFFER</li>
* <li>the TEMP-TABLE of the AFTER-BUFFER</li>
* <li>the DATASET of the BUFFER</li>
* </ul>
*
* The is not affected.
* <p>
* The attribute is not set to FALSE on a successful SAVE operation, the programmer must do
* this by hand.
*/
private void setErrorOnSave()
{
if (!isBeforeBuffer())
{
return;
}
error(true); // current IMAGE (it is a BEFORE IMAGE)
peerBuffer.error(true); // the AFTER-IMAGE of this BEFORE-IMAGE
TempTable beforeTable = TempTableBuilder.asTempTable((Temporary) this);
TempTable afterTable = beforeTable.getAfterTableNative();
beforeTable.error(true); // the TEMP-TABLE of current BUFFER
afterTable.error(true); // the TEMP-TABLE of the AFTER-BUFFER
DataSet ds = this.dataSet != null ? this.dataSet : peerBuffer.dataSet;
if (ds != null && ds.valid())
{
ds.error(true); // the DATASET of the BUFFER
}
}
/**
* Read record data from the ObjectInputStream
*
* @param fields
* Record fields
* @param extents
* Record extents
* @param ois
* Source stream
*
* @return Values of the record fields or <code>null</code> if failed.
*/
private Map<String, BaseDataType[]> readValues(ArrayList<TableMapper.LegacyFieldInfo> fields,
Map<String, Integer> extents,
ObjectInputStream ois)
{
Map<String, BaseDataType[]> values = new HashMap<>();
try
{
for (int i = 0; i < fields.size(); i++)
{
TableMapper.LegacyFieldInfo lfi = fields.get(i);
String prop = lfi.getJavaName();
Integer extent = extents.get(prop);
Class<? extends BaseDataType> cls = lfi.getDataType();
BaseDataType[] val;
if (extent == null)
{
// scalar case
val = new BaseDataType[1];
// call the default constructor
val[0] = BaseDataTypeFactory.instantiate(cls);
// deserialize
val[0].readExternal(ois);
}
else
{
// array case
val = new BaseDataType[extent];
for (int j = 0; j < extent; j++)
{
// call the default constructor
val[j] = BaseDataTypeFactory.instantiate(cls);
// deserialize
val[j].readExternal(ois);
}
}
values.put(prop, val);
}
return values;
}
catch (IOException | SecurityException | ReflectiveOperationException | IllegalArgumentException ex)
{
// TODO: if something happened during the copy but after the create, do we need to put
// the original record back in?
ErrorManager.recordOrShowError(
4952,
"Couldn't extract raw value from source field of RAW-TRANSFER",
false, false, true
);
return null;
}
}
/**
* FILL operation worker.
*/
private class FillWorker
{
/** Buffer reference */
private final BufferImpl selfBuf = BufferImpl.this;
/** Buffer handle */
private final handle self = new handle(selfBuf);
/** FILL result */
private final logical fillOk = new logical(true);
/** FILL in append mode flag */
private final boolean isAppend = FM_APPEND.equals(fillMode);
/** FILL in merge mode flag */
private final boolean isMerge = FM_MERGE.equals(fillMode);
/** FILL in replace mode flag */
private final boolean isReplace= FM_REPLACE.equals(fillMode);
/** Number of source buffers */
private int srcBufCount;
/** Source buffers */
private BufferImpl[] srcBuffs;
/** Target record buffer */
private RecordBuffer recBuf;
/**
* Fill the target DATA-SET.
*
* @return 'true' on success
*/
public logical fill()
{
CallbackData bed = (callbacks == null) ? null : callbacks.get(BEFORE_FILL_EVENT);
boolean ok;
if (bed != null)
{
ok = dataSet.invokeCallback(self, bed);
if (!ok)
{
dataSet.error(true);
return logical.FALSE;
}
}
else if (dataSource == null || !dataSource.valid())
{
ErrorManager.recordOrShowError(11874, false, doGetName());
// FILL requires an attached data-source for buffer %1, or an active before-fill callback procedure.
dataSet.error(true);
return logical.FALSE;
}
if (TempTableBuilder.asTempTable((Temporary) selfBuf).isTrackingChanges().getValue())
{
ErrorManager.recordOrShowError(11863, doGetName());
// FILL is not valid for table <name> when TRACKING-CHANGES is on.
dataSet.error(true);
return logical.FALSE;
}
if (dataSource != null && dataSource.valid())
{
// check DATA-SOURCE sanity and decide whether we need to create a new query
srcBufCount = dataSource._getNumSourceBuffers();
if (srcBufCount > 1 && !dataSource.getQueryAsHandle()._isValid())
{
ErrorManager.recordOrShowError(11980);
// Must assign a user query to a data-source that is a join before filling.
ErrorManager.recordOrShowError(11875, doGetName());
// FILL of buffer <name> requires a DATA-SOURCE QUERY to be prepared and assigned for this type of buffer.
dataSet.error(true);
return logical.FALSE;
}
// we can start FILLing the table now
if (FM_EMPTY.equalsIgnoreCase(fillMode))
{
deleteAll();
}
initBuffers();
if (isReplace)
{
P2JIndex primaryIndex = recBuf.getDmoInfo().getPrimaryIndex(true);
if (primaryIndex == null || !primaryIndex.isUnique())
{
ErrorManager.recordOrShowError(11885);
// A unique primary index is required in the target table, each field of
// which is mapped to some source field, in order to do a replace-mode or
// parent-mode COPY/GET/MERGE/FILL type of operation on a dataset table
dataSet.error(true);
return logical.FALSE;
}
}
boolean queryOpen = true;
boolean queryClose = true;
QueryWrapper fillQuery = null;
if (reusableQuery != null && dataSet.isReuseFillQuery())
{
queryClose = false;
queryOpen = false;
fillQuery = reusableQuery;
}
if (fillQuery == null)
{
handle hQuery = dataSource.getQueryAsHandle();
if (!hQuery._isValid())
{
dataSet.error(true);
return new logical(false);
}
fillQuery = (QueryWrapper) hQuery.getResource();
if (fillQuery.prepareString().isUnknown())
{
ErrorManager.recordOrShowError(11876, doGetName());
// FILL of buffer <name> with user-supplied query requires that the query be QUERY-PREPARED.
dataSet.error(true);
return new logical(false);
}
if (dataSet.isReuseFillQuery())
{
queryClose = false;
queryOpen = true;
reusableQuery = fillQuery;
}
}
Boolean secondPassRequired = null;
Optional<DataRelation> relation = dataSet.getRelation(selfBuf);
List<DataRelation> relations = dataSet.getRelations(selfBuf, true, false, true);
boolean recursive = relation.map(DataRelation::isRecursive)
.map(logical::booleanValue).orElse(false);
if (recursive)
{
secondPassRequired = secondPassRequired(relation.get(), fillQuery);
if (secondPassRequired == null)
{
ErrorManager.recordOrShowError(14282);
// Unable to recurse during FILL. (14282)
relations = Collections.emptyList();
}
}
processDataSource(fillQuery, relations, queryOpen, queryClose);
if (Boolean.TRUE.equals(secondPassRequired))
{
if (reusableQuery != null)
{
clearQuery();
}
fillQuery.setDataRelation(relation.get());
processDataSource(fillQuery, Collections.emptyList(), true, true);
}
}
ok = dataSet.invokeCallback(self, (callbacks == null) ? null : callbacks.get(AFTER_FILL_EVENT));
ok = ok && fillOk.booleanValue();
dataSet.error(!ok);
return logical.of(ok);
}
/**
* Check if second pass is required (for recursive relation)
*
* @param relation
* The relation to be processed.
* @param fillQuery
* The source query for the FILL operation.
*
* @return <code>Boolean.TRUE</code> if second pass is required.
*/
private Boolean secondPassRequired(DataRelation relation, P2JQuery fillQuery)
{
if (!(fillQuery instanceof QueryWrapper))
{
return null;
}
QueryWrapper wrapper = (QueryWrapper) fillQuery;
RecordBuffer[] buffers = wrapper.getRecordBuffers();
String[] relFields = relation.getRelationFields(false);
DmoMeta dmoMeta = buffers[0].getDmoInfo();
for (String relField: relFields)
{
if (dmoMeta.byLegacyName(relField) == null)
{
BufferReference bufferRef = (BufferReference) relation.getChildBuffer().getResource();
String legacyTable = bufferRef.buffer().getDmoInfo().legacyTable;
ErrorManager.recordOrShowError(11883, dmoMeta.legacyTable, relField, legacyTable);
return null;
}
}
for (String relField: relation.getRelationFields(true))
{
if (dmoMeta.byLegacyName(relField) == null)
{
return Boolean.FALSE;
}
}
return Boolean.TRUE;
}
/**
* Process the DATA-SOURCE.
*
* @param fillQuery
* The source query for the FILL operation.
* @param childRelations
* The list of child relations.
* @param open
* Flag indicating that the fill query needs to be opened.
* @param close
* Flag indicating that the fill query needs to be closed.
*/
private void processDataSource(P2JQuery fillQuery,
List<DataRelation> childRelations,
boolean open,
boolean close)
{
ErrorHelper errHlp = recBuf.bufferManager.getTxHelper().errHlp;
boolean wmOn = errHlp.isWarningMode();
if (!wmOn)
{
errHlp.setWarningMode(true);
}
try
{
RecordBuffer buffer = buffer();
if (open)
{
fillQuery.hintFullResults();
fillQuery.queryOpen();
}
else
{
fillQuery.clearResults();
}
fillQuery.first();
Long[] rowids = dataSource.getRestartRowids();
if (rowids[0] != null)
{
fillQuery.repositionByID(rowids);
fillQuery._getNext();
}
else
{
int value = dataSource.getRestartRow().intValue();
if (value > 0)
{
fillQuery.reposition(value);
fillQuery._getNext();
}
}
// iterate the result and populate this buffer
integer batchRemaining = new integer(batchSize > 0 ? batchSize : -1);
logical testRes = new logical(); // safe to use, as is just assigned on each evaluation
LogicalOp test = () ->
{
testRes.assign(!fillQuery._isOffEnd() && batchRemaining.intValue() != 0);
return testRes;
};
String lbl = "fill-buffer-" + name;
logical hasMore = new logical();
Map<String, FieldStructure> fields = new HashMap<>();
Runnable batchFill = () ->
{
recBuf.bufferManager.addDirtyBatchBuffer(buffer, null);
if (!fillRow(fillQuery, hasMore, buffer, fields))
{
BlockManager.next(lbl);
}
};
Runnable update = () -> RecordBuffer.batch(batchFill);
BlockManager.repeatToWhile(TransactionType.SUB, lbl, null, test,
new Block((Body) () ->
{
if (!fillOk.getValue())
{
BlockManager.leave();
}
if (callbacks != null &&
!dataSet.invokeCallback(self, callbacks.get(BEFORE_ROW_FILL_EVENT)))
{
fillOk.assign(false);
BlockManager.leave();
}
selfBuf.create();
if (!isMerge && !isReplace)
{
try
{
update.run();
}
catch (ErrorConditionException var10)
{
fillOk.assign(false);
ErrorManager.recordOrShowError(11878, false, "-1", doGetName());
// FILL got error -1 creating record <buffer-name> (11878)
BlockManager.leave();
}
}
else
{
recBuf.txHelper.errHlp.silent(update);
recBuf.txHelper.errHlp.setPending(false);
}
if (batchRemaining.intValue() > 0)
{
batchRemaining.decrement();
}
if (callbacks != null &&
!dataSet.invokeCallback(self, callbacks.get(AFTER_ROW_FILL_EVENT)))
{
selfBuf.release();
fillOk.assign(false);
BlockManager.leave();
}
int childRelationsSize = childRelations.size();
for (int i = 0; i < childRelationsSize; i++)
{
DataRelation rel = childRelations.get(i);
Buffer childBuffer = rel.getChildBufferNative();
if (rel._isReposition())
{
childBuffer.lastBatch(true);
}
else if (!rel._isRecursive() || childBuffer != selfBuf)
{
fillOk.assign(childBuffer.fill().getValue());
if (!fillOk.getValue())
{
break;
}
}
}
selfBuf.release();
if (!fillOk.getValue())
{
BlockManager.leave();
}
hasMore.assign(fillQuery._getNext());
}));
if (batchSize > 0 && batchRemaining.intValue() != 0 && childRelations.isEmpty())
{
// the batch ended before [batchSize] records were processed
lastBatch = true;
}
else if (!hasMore.booleanValue())
{
// the last invocation to fill-query:get-next() returned false so there are no more batches
lastBatch = true;
}
}
finally
{
dataSource.updateFillRowidInfo(fillOk.getValue());
if (close)
{
fillQuery.setDataRelation(null);
fillQuery.queryClose();
}
selfBuf.release();
if (!wmOn)
{
errHlp.setWarningMode(false);
}
}
}
/**
* Initialize buffers
*/
private void initBuffers()
{
srcBuffs = new BufferImpl[srcBufCount];
for (int k = 0; k < srcBufCount; ++k)
{
srcBuffs[k] = (BufferImpl) dataSource.getSourceBuffer(k + 1).unwrapBuffer();
srcBuffs[k].buffer().initialize();
}
recBuf = buffer();
recBuf.initialize();
flushAll();
}
/**
* Populate the target record by iterating the data-source mapping and copying values between paired
* fields.
*
* @param fillQuery
* The source query.
* @param hasMore
* Flag indicating that more records are available.
* @param dstBuffer
* The destination record buffer.
* @param fields
* A map of resolved {@link FieldStructure} for each destination field.
*
* @return {@code true} on success.
*/
private boolean fillRow(P2JQuery fillQuery,
logical hasMore,
RecordBuffer dstBuffer,
Map<String, FieldStructure> fields)
{
int dataSrcMappingSize = dataSrcMapping.size();
for (int i = 0; i < dataSrcMappingSize; i++)
{
Pair<String, DataSource.FieldReference> entry = dataSrcMapping.get(i);
String myField = entry.getKey();
DataSource.FieldReference srcField = entry.getValue();
BufferImpl srcBuffer = srcField.getBuffer();
if (srcField.getName().equals(Session.PK))
{
dereference(myField, srcBuffer.buffer().rowID());
}
else
{
assign(myField, srcField, srcBuffer, dstBuffer, fields);
}
}
if (srcBufCount == 1)
{
TempRecord newRec = (TempRecord) recBuf.getCurrentRecord();
newRec._datasourceRowid(srcBuffs[0].buffer()._rowID());
}
if (isMerge || isReplace || isAppend)
{
boolean validated = false;
try
{
validated = recBuf.validate(isAppend);
}
catch (ValidationException | ErrorConditionException e)
{
if (isAppend)
{
// append mode: abort the fill operation
recBuf.release(true, false);
throw new ErrorConditionException(
"FILL: Failed with unique validation error in APPEND mode.", e);
}
}
// was there a unique conflict with this record?
if (!validated)
{
if (isMerge)
{
// merge mode: ignore it, just skip to next record
TransactionManager.disableLoopProtection(null);
hasMore.assign(fillQuery._getNext());
// we need to release the newly created record as if undone. We cannot call
// BlockManager.undoNext(lbl) here as it will affect the buffer(s) from the query
// driving the fill operation.
recBuf.release(true, false); // I.e. drop() - this form avoids a buffer() lookup
return false;
}
else if (isReplace)
{
// replace mode: eliminate competition, then try again on clean ground
try
{
OrmUtils.dropUniqueIndexConflicts(recBuf,
recBuf.getPersistence(),
recBuf.getMultiplexID());
}
catch (PersistenceException e)
{
throw new ErrorConditionException(
"FILL: Failed to make room for new record in REPLACE mode", e);
}
// now there should not be any collisions and the new record should be flushed
}
}
}
return true;
}
/**
* Flush buffers
*/
private void flushAll()
{
boolean inTx = false;
try
{
inTx = recBuf.persistenceContext.beginTransaction(null);
recBuf.flush();
for (BufferImpl srcBuff : srcBuffs)
{
srcBuff.buffer().flush();
}
}
catch (PersistenceException | ValidationException exc)
{
ErrorManager.recordOrThrowError(exc);
}
finally
{
if (inTx)
{
try
{
recBuf.persistenceContext.commit();
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
}
}
}
/**
* A structure to cache the resolved information for the destination field, used during FILL operations.
*/
private static class FieldStructure
{
/** The resolved extent index. */
public int dstIndex;
/** The resolved field name. */
public String crtField;
/** The associated property for this field. */
public Property dstProp;
}
}