BaseRecord.java
/*
** Module : BaseRecord.java
** Abstract : The abstract base class for all business data model object classes.
**
** Copyright (c) 2019-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20191001 Created initial version with basic data and accessors.
** OM 20200110 Added support various data types and setter/getter signatures.
** 002 CA 20200922 Added getDatum(offset).
** ECF 20201201 Cleaned up file and added some missing javadoc.
** AIL 20201210 Added bulk set datum for specific buffer-copy operations.
** ECF 20201210 Added direct access to data array for RecordBuffer.
** 20210126 Fixed getUnvalidatedIndices to account optionally for untouched, default property values.
** AIL 20210129 Added support for bulkDataChanged. Improved bulk copy (doing lock and report only once).
** OM 20201027 copy() method will update the status flags to allow the record to be correctly saved.
** OM 20210404 CLOB fields are created and returned with their CP set.
** ECF 20210423 Fixed mismatch between updates to dirtyProps and activePreviousValue, which resulted in
** incorrect before/after diffs being reported. Renamed setAllDatum to setAllData.
** ECF 20210519 Added "in-use" tracking to report whether this record is referenced by any buffers or is
** being tracked for UNDO purposes.
** ECF 20210805 Properly enable nested calls to set and restore the activeBuffer and related state.
** ECF 20210924 Refactored debug string representation.
** CA 20210929 Cache the PK getter method.
** OM 20211020 Added _DATASOURCE_ROWID property.
** CA 20220420 Extracted 'initialize' method which sets the default field values and sets the initial
** state for indices, nulls and validation bitsets. This is required for Record arguments
** used in a Java REST service, which must not be associated with a session.
** ECF 20221005 Added support for dynamic initial values.
** TJD 20220504 Upgrade do Java 11 minor changes
** CA 20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as
** 'partial/incomplete'.
** TJD 20230103 The defaults MUST be initialized BEFORE the updateState call
** CA 20220613 'getCodePage' must ignore an empty codepage set at the field.
** CA 20230110 getDirtyIndices and getUnvalidatedIndices now return null instead of an empty BitSet, when
** no indices are found.
** CA 20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as
** 'partial/incomplete'.
** 003 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 004 EVL 20230605 Fix from OM for crash with updating state of the rolled back record.
** 005 SR 20230731 Created getSize() method to return the size of a record in bytes.
** 006 AL2 20240409 Reworked references to store both the number of real references and transient references.
** 007 SP 20240805 Added new restoreActiveState method.
** 008 TJD 20230303 Allow performing a sub-transaction level changeset reset and roll-up
** TJD 20230306 Reset changeset changes on roll-up
** TJD 20231019 fix resetChangeset function name
** 009 AS 20241010 Reworked activeOffset into activeOffsets(BitSet) to support
** validation of a subset of indices.
** AS 20241010 Reworked activePropPreviousValues as Object[] to hold multiple values.
** 010 AL2 20241105 Added isReferenced API.
** 011 ES 20241203 Added new hasIndices method.
** 012 AS 20250220 Refactored getActiveUpdateDiff to return the diff for only one property.
** 013 AS 20250227 Store the result of applyInitialValues in nullProps, rather than pass it as parameter.
*/
/*
** 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.orm;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.rest.serializers.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import java.lang.reflect.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.atomic.*;
/**
* Base record class which stores data and current state for a single record, and which provides services to
* application-specific, DMO implementation subclasses.
* <p>
* Access to the internal state of this class is not thread-safe. Thus, instances are <strong>not</strong>
* intended to be accessed from more than one thread.
*/
public abstract class BaseRecord
implements DmoState,
Persistable
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(BaseRecord.class);
/** The cached getter method for the PK property. */
public static final Method PK_GETTER;
static
{
try
{
PK_GETTER = BaseRecord.class.getMethod("primaryKey");
}
catch (NoSuchMethodException |
SecurityException e)
{
throw new RuntimeException("Could not resolve PK getter!", e);
}
}
/** Surrogate primary key of the record */
protected Long id = null;
/** The data represented by this record, in it's low-level (database friendly) form */
protected final Object[] data;
/** For an incomplete record, this holds the fields which were read or assigned in this record. */
protected BitSet readFields = null;
/** Current state of the record */
protected final RecordState state = new RecordState();
/**
* Count of references to this DMO; while positive, it must remain in the current session cache.
* The 4 high bytes represent the number of buffers the record is transiently loaded in.
* The 4 low bytes represent the number of active buffers the record is loaded in.
*/
protected long references = 0;
/** Map of properties touched, each bit corresponds with a property in the data array */
private final BitSet dirtyProps;
/** Map of properties currently requiring validation */
private final BitSet unvalidated;
/** Map of properties currently set to null, indicating unknown value */
private final BitSet nullProps;
/** Tracker to manage which indices have been updated by property updates */
private final IndexState indexState;
/** Record identifier (lock variant, which uses table name) for this record (created lazily) */
private RecordIdentifier<String> recid = null;
/** The currently active buffer when state is changing */
private RecordBuffer activeBuffer = null;
/** The offsets of the properties currently being modified. */
protected BitSet activeOffsets = null;
/** The most recent values of the properties currently being modified, for rollback purposes */
private Object[] activePropPreviousValues = null;
/** Object which tracks change to enable rollback of the record's data and state */
private ChangeSet changeSet = null;
/** Current version number, for stale checks */
private int version = DmoVersioning.UNVERSIONED;
/** Shared version data structure, for version updates and stale checks */
private transient AtomicIntegerArray sharedVersion = null;
/**
* Default constructor which can only be invoked by subclasses. This constructor does not initialize
* most state-related data structures. If these are needed for legacy record behavior, the {@link
* #initialize(Session, boolean)} method should be invoked after construction.
*/
protected BaseRecord()
{
RecordMeta recMeta = _recordMeta();
int size = recMeta.getPropertyMeta(false).length;
// initialize data array
data = new Object[size];
// initialize property-level state bit sets
dirtyProps = new BitSet(size);
unvalidated = new BitSet(size);
nullProps = new BitSet(size);
// track all indices which require full update (and validation) to enable record visibility
int ulen = recMeta.uniqueIndices.length;
int nlen = recMeta.nonuniqueIndices.length;
indexState = new IndexState(ulen, nlen, recMeta.getPrimaryIndexId());
activeOffsets = new BitSet();
activePropPreviousValues = new Object[size];
}
/**
* Copy the source record to the destination record.
* <p>
* The records are checked first to decide whether they are compatible (I.e. the number of properties
* matches and also the type for each field, including the optional extent attribute). If not, the method
* returns {@code false} and not copy operation is executed.
* <p>
* If the records pass validation, all properties are copied from source ({@code from}) to destination
* {@code to}). At the same time, the status flags are updated so that the destination record can be
* correctly saved.
*
* @param to
* The destination record.
* @param from
* The source record.
* @param b4reserved
* If {@code true} the before-table specific attributes are copied, too. Otherwise only the normal
* properties are processed.
*
* @return {@code true} if the copy was performed (record's tables match).
*/
public static boolean copy(BaseRecord to, BaseRecord from, boolean b4reserved)
{
PropertyMeta[] fromProps = from._recordMeta().getPropertyMeta(false);
PropertyMeta[] toProps = to._recordMeta().getPropertyMeta(false);
if (fromProps.length != toProps.length)
{
return false;
}
for (int i = 0; i < fromProps.length; i++)
{
PropertyMeta fromProp = fromProps[i];
PropertyMeta toProp = toProps[i];
if (fromProp.getType() != toProp.getType() || fromProp.getExtent() != toProp.getExtent())
{
return false;
}
}
for (int i = 0; i < fromProps.length; i++)
{
if (!b4reserved)
{
switch (fromProps[i].getId())
{
case ReservedProperty.ID_ERROR_FLAG:
case ReservedProperty.ID_ERROR_STRING:
case ReservedProperty.ID_ROW_STATE:
case ReservedProperty.ID_ORIGIN_ROWID:
case ReservedProperty.ID_DATASOURCE_ROWID:
case ReservedProperty.ID_PEER_ROWID:
// skip these attributes/properties
continue;
}
}
// TODO: LOB data is mutable, needs to be duplicated explicitly
boolean change = to.data[i] != from.data[i];
if (change)
{
to.data[i] = from.data[i];
// update the null property bitset and mark the property as changed to be able to save it
to.nullProps.set(i, to.data[i] == null);
to.dirtyProps.set(i);
}
}
// mark it as changed
to.updateState(CHANGED, true);
return true;
}
/**
* Populate the record's data with its default, initial values and update the record and
* property states accordingly. Mark the record as newly created.
* <p>
* This method should be invoked only once, immediately after the object is constructed. It
* is not invoked directly from the constructor, because the only use case which requires the
* initialization performed here is a new data record being created on behalf of application
* logic.
* <p>
* This is done separately from construction because there are use cases where an instance of
* this class is needed, but we do not need the overhead of initializing the data with default
* values, as they will be overwritten anyway. For instance, if the object is read from the
* database or a copy/snapshot is being made, the data array will be populated with the values
* retrieved from the database or copied from the other record, respectively.
* <p>
* It is not invoked optionally from the constructor, because a default constructor called
* from subclasses is preferable, as the subclasses are often constructed by reflection.
* <p>
* This operation updates the not-null bitset, based on the values of non-nullable columns.
*
* @param session
* Database session, which is needed if this record must have its changes tracked for
* UNDO/rollback purposes; otherwise, may be {@code null}.
* @param setDefaults
* If {@code true}, set all default property values; if {@code false}, leave data array
* filled with null values.
*/
public void initialize(Session session, boolean setDefaults)
{
// the defaults MUST be initialized BEFORE the updateState call
initialize(setDefaults);
// mark the record as newly created; this should be the only place this flag is set;
// a newly created record is considered invalid until it passes validation, even though
// its initial data may in fact be valid
updateState(session, NEW, true);
}
/**
* Initialize this record instance, by setting all fields to their default values and also initializing
* the {@link #indexState}, {@link #unvalidated} and {@link #nullProps} states.
* <p>
* This method should be invoked only once, immediately after the object is constructed. It is not invoked
* directly from the constructor, because the only use case which requires the initialization performed
* here is a new data record being created on behalf of application logic.
* <p>
* This will be used by, {@link RecordSerializer JSON deserialization} when creating a new record for an
* instance associated with the web service request (either request or response).
*
* @param setDefaults
* If {@code true}, set all default property values; if {@code false}, leave data array
* filled with null values.
*/
public void initialize(boolean setDefaults)
{
RecordMeta recMeta = _recordMeta();
// for a newly created record, all index state bits need to be set; they will be cleared one at a
// time, as each index is updated
indexState.setAll(recMeta.uniqueIndices.length, recMeta.nonuniqueIndices.length);
if (setDefaults)
{
nullProps.clear();
nullProps.or(recMeta.applyInitialValues(data));
unvalidated.or(recMeta.getValidatableProperties());
}
}
/**
* Indicate whether this DMO is referenced by any buffer.
*
* @return {@code true} if in use by the above definition, else {@code false}.
*/
public boolean isReferenced()
{
return references > 0;
}
/**
* Indicate whether this DMO is "in use"; that is, whether buffers reference it or it is being tracked
* for UNDO processing.
*
* @return {@code true} if in use by the above definition, else {@code false}.
*/
public boolean isInUse()
{
return isReferenced() || checkState(TRACKED);
}
/**
* Get the update state of this record's indices.
*
* @return Index state.
*/
public IndexState getIndexState()
{
return indexState;
}
/**
* Get the database identifier for this persistable object. This generally equates to the
* backing table's primary key. However, it will be {@code null} for DMO instances which have
* not yet been persisted to the database.
*
* @return Unique, database identifier associated with this object.
*/
@Override
public final Long primaryKey()
{
return id;
}
/**
* Set the database identifier for this persistable object (i.e., surrogate primary key).
*
* @param id
* Unique, database identifier to be associated with this object.
*/
@Override
public final void primaryKey(Long id)
{
this.id = id;
}
/**
* Copy this record's data and record state, including its primary key. The copy MUST NOT find its way
* into the {@link Session} cache.
*
* @return Copy of this record.
*/
public BaseRecord deepCopy()
{
BaseRecord copy = null;
try
{
copy = getClass().getDeclaredConstructor().newInstance();
}
catch (ReflectiveOperationException exc)
{
throw new RuntimeException(exc);
}
// copies data and primary key
copy.copy(this);
// OR works here because copy's bitsets are initialized to all bits cleared
copy.dirtyProps.or(this.dirtyProps);
copy.nullProps.or(this.nullProps);
// record state
copy.state.state = this.state.state;
// record identifier is immutable; ok to reference same object from copy
copy.recid = this.recid;
return copy;
}
/**
* Makes this a copy of a source {@code BaseRecord} object. For permanent databases, the
* internal {@code data} and the {@code id} of the {@code BaseRecord} are copied.
* <p>
* The only state copied is that of which properties are dirty (all of them after the copy).
* Thus, this method is for specialized, internal use only, such as creating a new record in
* order to copy data from one temp-table buffer to another.
*
* @param from
* The source to copy data from.
*/
public void copy(BaseRecord from)
{
// TODO: BLOB data is mutable, needs to be duplicated explicitly
int len = this.data.length;
this.id = from.id;
System.arraycopy(from.data, 0, this.data, 0, len);
dirtyProps.set(0, len); // TODO: nullProps?
}
/**
* Checks whether a set of record state flags are set for this record.
*
* @param flags
* One or more {@code RecordState} flags, bitwise OR'd together.
*
* @return {@code true} if ALL flags are set.
*
* @see RecordState
*/
public boolean checkState(int flags)
{
return (state.state & flags) == flags;
}
/**
* Indicate whether the record has passed validation. A newly created and initialized record
* or a record which has had any of its properties changed is considered invalid, until it
* passes validation.
*
* NOTE: in fact, this is the state the record after the last validation, it might have been
* changes meanwhile so the state might be staled, check the NEED_VALIDATION bit to see
* if this is the case and call validation routines
*
* @return {@code true} if this record has passed validation and has not since had any of its
* properties changed, else {@code false}.
*/
public boolean isValid()
{
return !checkState(INVALID);
}
/**
* Updates the set of state flags for this record. Depending on {@code set} parameter, the
* flags can be set (when {@code true}) or removed (when {@code false}). If one flag is already
* set and {@code set == true} that flag remains unchanged. The same applies for un-setting an
* inactive flag.
*
* @param session
* Database session (used for UNDO purposes).
* @param flags
* A set of OR-ed {@code RecordState} flags to be updated.
* @param set
* If {@code true} the flags will be set, otherwise they will be unset.
*
* @see RecordState
*/
public void updateState(Session session, int flags, boolean set)
{
// if this update will actually change the current state, track the change for possible rollback;
// only track state changes which do not affect the fundamental nature of the record
if (session != null &&
(state.state & flags) != (set ? flags : 0) &&
((flags & (NOUNDO | COPY)) == 0) &&
(set || flags != CACHED))
{
try
{
int txLevel = prepareChangeSet(session);
if (txLevel >= 0)
{
changeSet.logStateChange(state.state, txLevel);
}
}
catch (PersistenceException exc)
{
// TODO: handle
LOG.severe("", exc);
}
}
updateState(flags, set);
}
/**
* Get the unique identifier for this record, comprised of its table name and primary key.
*
* @return Record identifier.
*/
public RecordIdentifier<String> getRecordLockIdentifier()
{
if (recid == null)
{
recid = new RecordIdentifier<>(getTable(), id);
}
return recid;
}
/**
* Report whether the table associated with this record has any (legacy) index.
*
* @return {@code true} if there is at least one legacy index defined in the table of this record.
*/
public boolean isIndexed()
{
return !_recordMeta().allIndexedProps.isEmpty();
}
/**
* Set the record buffer which is currently active for this record. This method is called to set the
* active buffer temporarily associated with this record before some work is done to update this record's
* data. This may be an update to a single datum or a bulk update.
* <p>
* A call to this method must be bracketed with a call to {@code run} the {@code Runnable} returned by
* this method. This allows nested invocations and ensures that the active buffer and related state are
* reset properly after the update work is done.
*
* @param buffer
* The buffer which is considered the "active" buffer for a subsequent unit of update
* work.
*
* @return A {@code Runnable} which MUST be executed in a {@code finally} block when the update work
* for which the active buffer was set is finished.
*/
public Runnable setActiveBuffer(RecordBuffer buffer)
{
// remember the previously active buffer, offset, and datum value for restoration of state
RecordBuffer previousBuffer = activeBuffer;
BitSet previousOffsets = (BitSet) activeOffsets.clone();
Object[] propPreviousValue = activePropPreviousValues.clone();
boolean propPreviousValueEmpty = true;
for (int i = 0; i < propPreviousValue.length; i++)
{
if (propPreviousValue[i] != null)
{
propPreviousValueEmpty = false;
}
}
// set active buffer
activeBuffer = buffer;
if (previousBuffer == null && previousOffsets.cardinality() == 0 && propPreviousValueEmpty)
{
return this::restoreActiveState;
}
return () ->
{
// reset active buffer to previous state (null for outermost call; no-op for nested call)
activeBuffer = previousBuffer;
// reset remaining state to previous values
activePropPreviousValues = propPreviousValue.clone();
activeOffsets = previousOffsets;
};
}
/**
* Restore to default active buffer, offset, and datum value.
*/
public void restoreActiveState()
{
activeBuffer = null;
activeOffsets.clear();
activePropPreviousValues = new Object[_recordMeta().getPropertyMeta(false).length];
}
/**
* Indicate whether any property in the record currently is dirty (i.e., touched, not necessarily changed).
*
* @return {@code true} if any property was touched, else {@code false}.
*/
public boolean isDirty()
{
return !dirtyProps.isEmpty();
}
/**
* Indicate whether all index components for any index on this record are dirty (i.e., have been touched,
* not necessarily changed..
*
* @return {@code true} if any index is fully dirty.
*/
public boolean isAnyIndexFullyDirty()
{
RecordMeta rm = _recordMeta();
return isAnyIndexFullyDirty(rm.uniqueIndices) || isAnyIndexFullyDirty(rm.nonuniqueIndices);
}
/**
* Indicate whether tables have any indices.
*
* @return {@code true} if the table have any index.
*/
public boolean hasIndices()
{
RecordMeta rm = _recordMeta();
return rm.uniqueIndices.length > 0 || rm.nonuniqueIndices.length > 0;
}
// Undo the current/active update
public void undoActive()
{
// TODO: complete this implementation: what about state? we probably need to preserve the
// previous state and roll this back as well
for (int i = activeOffsets.nextSetBit(0); i != -1; i = activeOffsets.nextSetBit(i + 1))
{
data[i] = activePropPreviousValues[i];
}
}
/**
* Return a string representation of this object, primarily for debug purposes.
*
* @return String representation of this object.
*/
@Override
public String toString()
{
return toString(true);
}
/**
* Return a string representation of this object, primarily for debug purposes, optionally including the
* record's data in the output.
*
* @param includeData
* {@code true} to include the record's data. If {@code false}, all other state still will be
* included.
*
* @return String representation of this object.
*/
public String toString(boolean includeData)
{
String sep = System.lineSeparator();
StringBuilder buf = new StringBuilder(_recordMeta().tables[0]);
buf.append(':').append(primaryKey()).append(sep)
.append(state).append(sep)
.append("references: ").append(references).append(sep)
.append("dirty: ").append(dirtyProps).append(sep)
.append("unvalidated: ").append(unvalidated);
if (includeData)
{
int len = data.length;
buf.append(sep).append("data: {");
for (int i = 0; i < len; i++)
{
if (i > 0)
{
buf.append(", ");
}
buf.append(data[i]);
}
buf.append('}');
}
return buf.toString();
}
/**
* This accessor allows direct access to private {@code data} array <strong>only to</strong>
* instances of {@code PropertyMapper}. This is needed because the these instances need direct
* and fast access the {@code data} and avoid normal status management of a record when the
* properties are accessed using the {@code Record}'s setters and getters. Instances of
* {@code PropertyMapper} are created only when the import is run so that management is not
* required nor needed.
*
* @param pm
* The {@code PropertyMapper} which requests access.
*
* @return The reference to internal {@code data} array if the passed parameter is a
* {@code PropertyMapper}
*
* @throws NullPointerException
* If the passed in parameter is {@code null}.
*/
public Object[] getData(PropertyMapper pm)
{
Objects.requireNonNull(pm);
return data;
}
/**
* This accessor allows direct access to private {@code data} array <strong>only to</strong>
* instances of {@code RecordBuffer}.
*
* @param rb
* The {@code RecordBuffer} which requests access.
*
* @return The reference to internal {@code data} array if the passed parameter is a
* {@code RecordBuffer}
*
* @throws NullPointerException
* If the passed in parameter is {@code null}.
*/
public Object[] getData(RecordBuffer rb)
{
Objects.requireNonNull(rb);
return data;
}
/**
* Indicate whether any of this record's validatable properties are dirty and have not been validated.
*
* @return {@code true} if record needs validation, else {@code false}.
*/
public boolean needsValidation()
{
return !unvalidated.isEmpty();
}
/**
* Get a bit set representing dirty unique or non-unique indices, subject to the conditions described by
* the parameters. A set bit in the returned set indicates a dirty index. The position of each set bit
* corresponds with the 0-based position of the associated index in the array of unique or non-unique
* indices for the record.
*
* @param dirtyOffsets
* Optional offsets of dirty properties which must be a component of the indices returned.
* @param unique
* {@code true} to return dirty unique indices; {@code false} to return dirty non-unique indices.
* @param remaining
* If {@code true}, only <em>unvalidated</em> indices are returned. Otherwise, validation is not
* considered.
*
* @return A bit set, as described above. <code>null</code> is returned when no indices are found.
*/
public BitSet getDirtyIndices(BitSet dirtyOffsets, boolean unique, boolean remaining)
{
RecordMeta recMeta = _recordMeta();
BitSet filter = new BitSet();
BitSet[] indices = unique ? recMeta.uniqueIndices : recMeta.nonuniqueIndices;
if (remaining)
{
filter.or(indexState.getBitSet(unique));
}
else
{
filter.set(0, indices.length);
}
return getDirtyIndices(indices, dirtyOffsets, filter, (state.state & NEW) == NEW);
}
/**
* Get an Object array representing the old and new values of the property currently being changed
* with the properties' offsets as keys and a two element arrays as values.
* The first element is the previous value of the property; the second is the new value. If the property
* has not been touched or the old and new values are the same, {@code null} is returned.
* <p>
* <strong>Note:</strong> this method will only return a non-null result while a property update is
* active; that is, while a DMO property is being updated via the {@code RecordBuffer} invocation handler.
* It is only at that moment that the previous value for the property being changed is guaranteed to
* contain meaningful data.
*
* @param ormIndex
* The index of the property.
* @param extentIndex
* The offset index of the extent.
*
* @return See above.
*/
public Object[] getActiveUpdateDiff(int ormIndex, int extentIndex)
{
int actualIndex = ormIndex + extentIndex;
if (dirtyProps.get(actualIndex) && !Objects.equals(activePropPreviousValues[actualIndex],
data[actualIndex]))
{
Object[] res = new Object[2];
res[0] = activePropPreviousValues[actualIndex];
res[1] = data[actualIndex];
return res;
}
return null;
}
/**
* Return the set of bits which are marked dirty. For performance reasons, the object is not copied before
* being returned. The caller MUST NOT alter its state!
*
* @return Bit set of dirty properties (those that have been touched, not necessarily changed).
*/
public BitSet getDirtyProps()
{
return dirtyProps;
}
/**
* Get the {@link #readFields}.
*
* @return The {@link #readFields}.
*/
public BitSet _getReadFields()
{
return readFields;
}
/**
* Set a single datum into the data array, and update the record and property state accordingly.
*
* @param offset
* Zero-based offset of the datum in the data array.
* @param datum
* Value to be set into the data array.
*/
protected void setDatum(int offset, Object datum)
{
if (readFields != null)
{
readFields.set(offset);
}
if (activeBuffer != null)
{
activeOffsets.set(offset);
}
// update the bitset of dirty (touched) properties
dirtyProps.set(offset);
// temporarily save the old value (even if it is the same as the new value)
activePropPreviousValues[offset] = data[offset];
// for BigDecimal, equals checks the scale - we need compareTo, to check if the values are really the
// same, regardless of the scale value.
if (!(Objects.equals(data[offset], datum) ||
(datum instanceof Comparable &&
data[offset] != null &&
((Comparable) datum).compareTo(data[offset]) == 0)))
{
// ensure we have an exclusive lock on the record
if (!lockForUpdate())
{
// do not pass go, do not collect $200
return;
}
try
{
dataChanged(offset, datum);
}
catch (PersistenceException exc)
{
throw new RuntimeException(exc);
}
// update the value in the data array
data[offset] = datum;
}
}
/**
* Set all data in the data array. This works only if the source has the same structure as the
* destination (this). A prevalidation should be done - this will only check integrity, type and extents.
* The order of the matching will be from left to right and it will avoid reserved properties.
*
* @param source
* A record from which the data will be copied to this data array.
*
* @return {@code true} if a valid copying could be done; so the properties match in type and extent and
* the number of properties match (except the reserved ones).
*/
protected boolean setAllData(BaseRecord source)
{
PropertyMeta[] fromProps = source._recordMeta().getPropertyMeta(false);
PropertyMeta[] toProps = this._recordMeta().getPropertyMeta(false);
Map<Integer, Integer> mapping = new HashMap<>();
int i, j;
// do the mapping and check if this copy makes sense (we have a correct mapping)
for (i = 0, j = 0; i < fromProps.length && j < toProps.length;)
{
PropertyMeta fromProp = fromProps[i];
PropertyMeta toProp = toProps[j];
if (fromProp.isReserved())
{
i++;
continue;
}
if (toProp.isReserved())
{
j++;
continue;
}
if (fromProp.getType() != toProp.getType() ||
fromProp.getExtent() != toProp.getExtent())
{
return false;
}
mapping.put(j, i);
j++;
i++;
}
// the mapping is broken; the source can't be copied here so this method was used incorrectly
if (!(i == fromProps.length && j == toProps.length))
{
return false;
}
// do an initial filtering of the offsets which should be updated
List<Integer> updatedOffsets = null;
List<Object> updatedDatums = null;
for (Map.Entry<Integer, Integer> entry : mapping.entrySet())
{
Integer toOffset = entry.getKey();
Integer fromOffset = entry.getValue();
Object datum = source.getDatum(fromOffset);
// update the bitset of dirty properties
dirtyProps.set(toOffset);
activeOffsets.set(toOffset);
// for BigDecimal, equals checks the scale - we need compareTo, to check if the values are really the
// same, regardless of the scale value.
if (!(Objects.equals(data[toOffset], datum) ||
(datum instanceof Comparable &&
data[toOffset] != null &&
((Comparable) datum).compareTo(data[toOffset]) == 0)))
{
if (updatedOffsets == null)
{
updatedOffsets = new ArrayList<>();
updatedDatums = new ArrayList<>();
}
updatedOffsets.add(toOffset);
updatedDatums.add(datum);
}
}
// we shouldn't do any copy as no offset will be updated
if (updatedOffsets == null || updatedOffsets.size() == 0)
{
return true;
}
// we will do a copy, so upgrade lock at first
if (!lockForUpdate())
{
return false;
}
try
{
// notify data change in bulk mode
// NOTE: this does not set activeOffset as we try to update all offsets at once!
bulkDataChanged(updatedOffsets, updatedDatums);
}
catch (PersistenceException exc)
{
throw new RuntimeException(exc);
}
// finally, do the data set
for (int idx = 0; idx < updatedOffsets.size(); idx++)
{
int offset = updatedOffsets.get(idx);
Object datum = updatedDatums.get(idx);
data[offset] = datum;
}
return true;
}
/**
* Get the field's value on the specified offset.
*
* @param offset
* Zero-based offset of the datum in the data array.
*
* @return The field's value.
*/
protected Object getDatum(int offset)
{
return data[offset];
}
/**
* Get the static record metadata for the concrete subclass.
*
* @return Static record metadata for the subclass.
*/
protected abstract RecordMeta _recordMeta();
/**
* Obtain the codepage of the {@code clob} object at position {@code offset}.
* <p>
* Note: in order for this method to work, the {@code activeBuffer} must be set in advance. After the
* method was invoked, it is recommended to clear its value.
*
* @param offset
* The position for {@code clob} field.
*
* @return The 4GL name of codepage of the {@code clob} field at position {@code offset}. If a codepage is
* not specified or if the field is not a {@code clob}, {@code null} is returned.
*/
protected String getCodePage(int offset)
{
if (activeBuffer == null)
{
return null;
}
TempTable parentTable = activeBuffer.getParentTable();
Property prop = activeBuffer.getDmoInfo().getProperty(offset);
String cp = null;
if (parentTable != null)
{
cp = parentTable.getCodePage(prop.name);
}
else
{
// must be a permanent table, use TableMapper
TableMapper.LegacyFieldInfo lfi = TableMapper.getLegacyFieldInfo(activeBuffer, prop.name);
if (lfi != null)
{
cp = lfi.getCodePage();
}
}
return cp == null || cp.isEmpty() ? null : cp;
// Property property = activeBuffer.getDmoInfo().getProperty(offset);
// if (property == null || property.codePage.isEmpty())
// {
// return null;
// }
//
// return property.codePage;
}
/**
* Check if for an incomplete record, the specified field was read or assigned.
*
* @param index
* The field index.
*/
protected void checkIncomplete(int index)
{
if (checkState(DmoState.INCOMPLETE) && !readFields.get(index))
{
RecordMeta meta = _recordMeta();
String tname = meta.legacyName;
String fname = meta.getPropertyMeta(false)[index].getLegacyName();
String msg = "Field " + fname + " from " + tname + " record (recid " + primaryKey() +
") was missing from FIELDS phrase";
ErrorManager.recordOrThrowError(8826, msg, false);
}
}
/**
* Mark this record as incomplete.
*
* @param session
* Database session (used for UNDO purposes).
*/
void markIncomplete(Session session)
{
updateState(session, DmoState.INCOMPLETE, true);
readFields = new BitSet(data.length);
}
/**
* An index was updated.
*
* @param idxUid
* Index identifier.
*/
void indexUpdated(int idxUid)
{
indexState.clear(idxUid);
}
/**
* Compare the bitsets in the given array with the bitset of dirty properties for this record
* and indicate whether all index components for any index in the array are dirty. The
* positions of the bits correspond with the positions of the properties in the record's data
* array.
*
* @param indices
* Array of bitsets, where each set bit represents a component of an index.
*
* @return {@code true} if all of the components of any index represented in the array are
* dirty, else {@code false}.
*/
boolean isAnyIndexFullyDirty(BitSet[] indices)
{
int len = indices.length;
for (int i = 0; i < len; i++)
{
BitSet orig = indices[i];
BitSet copy = (BitSet) orig.clone();
copy.and(dirtyProps);
if (copy.equals(orig))
{
return true;
}
}
return false;
}
/**
* Mark all dirty properties as having been validated.
*/
void markValidated()
{
unvalidated.andNot(dirtyProps);
}
/**
* Mark properties as having been validated.
*
* @param bitset
* A bitset of offsets representing the properties which has been validated.
*/
void markValidated(BitSet bitset)
{
unvalidated.andNot(bitset);
}
/**
* Get a bit set representing the unique indices, if any, which are either fully or partially updated
* and which have not already been validated. If the record is newly created and has not yet been
* flushed to the database, a fully updated index is required; otherwise, a partially updated index is
* sufficient.
*
* @param dirtyOffsets
* Optional offsets of dirty properties which must be a component of the indices returned.
* An unvalidated indices bitset is computed for each dirty offset and is being added to the
* resulting bitset.
* @param allUnvalidated
* If {@code true}, then any unique index containing any property not previously validated is
* included in the returned bit set. If {@code false}, then only indices containing properties
* that have been explicitly made dirty are included. Setting this to {@code true} effectively
* considers all properties with untouched, default values in the check.
*
* @return A bit set as described above. <code>null</code> is returned if no index is found.
*/
BitSet getUnvalidatedIndices(BitSet dirtyOffsets, boolean allUnvalidated)
{
if (dirtyProps.cardinality() == 0 && unvalidated.cardinality() == 0 && !allUnvalidated)
{
return new BitSet();
}
BitSet unvalidatedIndices = new BitSet();
int dirtyOffset = dirtyOffsets.nextSetBit(0);
boolean dirtyPropsEmpty = dirtyProps.isEmpty();
do {
if (!allUnvalidated && (dirtyPropsEmpty || (dirtyOffset >= 0 && !unvalidated.get(dirtyOffset))))
{
dirtyOffset = dirtyOffsets.nextSetBit(dirtyOffset + 1);
continue;
}
BitSet result = null;
boolean isNew = (state.state & NEW) == NEW;
BitSet[] unique = _recordMeta().uniqueIndices;
int len = unique.length;
for (int i = 0; i < len; i++) {
BitSet nextUnique = unique[i];
if (allUnvalidated) {
// if the unique index contains any unvalidated property (including untouched default values),
// include it in the set of indices which require validation
if (nextUnique.intersects(unvalidated)) {
if (result == null) {
result = new BitSet();
}
result.set(i);
}
continue;
}
// skip this one if:
// * an offset is provided, but it is not a component of the index; or
// * no offset is provided, but none of the unvalidated properties are a component of the index
if ((dirtyOffset >= 0 && !nextUnique.get(dirtyOffset)) ||
(dirtyOffset < 0 && !nextUnique.intersects(unvalidated))) {
continue;
}
if (isNew) {
// every component of the index must have been touched
BitSet copy = (BitSet) nextUnique.clone();
copy.and(dirtyProps);
if (copy.equals(nextUnique)) {
if (result == null) {
result = new BitSet();
}
result.set(i);
}
} else {
// any component of the index must have been touched; the tests above already have required
// this, so just set the bit in the result
if (result == null) {
result = new BitSet();
}
result.set(i);
}
}
if (result != null) {
unvalidatedIndices.or(result);
}
dirtyOffset = dirtyOffsets.nextSetBit(dirtyOffset + 1);
}
while (dirtyOffset != -1);
return unvalidatedIndices;
}
/**
* Get the offsets of the properties currently being updated, if any. When a DMO's setter method
* is invoked, the DMO goes into an 'active' state, during which the offsets of the
* properties being updated are stored temporarily as the active offsets. When the update is
* complete, no property is considered active.
* <p>
* The active state may be reset by this method. This should only be done from a {@link
* Validation} object.
*
* @return A BitSet representing the offsets of properties being updated.
*/
BitSet getActiveOffsets()
{
return (BitSet) activeOffsets.clone();
}
/**
* Reset record and property state after database is synchronized with the record's data
* (i.e., the most recent set of changes have been persisted).
* <p>
* This method does not turn off the {@code INVALID} flag.
*/
void clearDirtyState(Session session)
throws PersistenceException
{
dirtyProps.clear();
unvalidated.clear();
indexState.clearAll();
updateState(session, CHANGED, false);
}
/**
* Indicate whether the property at the given offset in the DMO's data array is dirty (i.e., has been
* touched, but not necessarily changed).
*
* @param offset
* Zero-based offset of the target property.
*
* @return {@code true} if the property is dirty, else {@code false}.
*/
boolean isPropertyDirty(int offset)
{
return dirtyProps.get(offset);
}
/**
* Get a bit set indicating the positions of data elements which currently are set to {@code null}.
*
* @return Null properties bit set.
*/
BitSet getNullProps()
{
return nullProps;
}
/**
* Read a property directly from a {@code ResultSet}.
*
* @param propOffset
* The index of the property to be read.
* @param rs
* The result set to read from.
* @param rsOffset
* The index in {@code ResultSet} to read from. If the property is a multiple column
* then more than one column will ve read from {@code ResultSet}.
*
* @return The number of columns actually read from the {@code ResultSet}.
*/
final int readProperty(int propOffset, ResultSet rs, int rsOffset)
throws PersistenceException
{
return readProperty(propOffset, rs, rsOffset, false);
}
/**
* Read a property directly from a {@code ResultSet}.
*
* @param propOffset
* The index of the property to be read.
* @param rs
* The result set to read from.
* @param rsOffset
* The index in {@code ResultSet} to read from. If the property is a multiple column
* then more than one column will ve read from {@code ResultSet}.
* @param skip
* Flag indicating if this property will not be read from the result-set.
*
* @return The number of columns actually read from the {@code ResultSet}.
*/
final int readProperty(int propOffset, ResultSet rs, int rsOffset, boolean skip)
throws PersistenceException
{
PropertyMeta pm = _recordMeta().getPropertyMeta(false)[propOffset];
PropertyReader propertyReader = pm.getDataHandler();
if (skip)
{
return propertyReader.propertySize();
}
try
{
if (readFields != null)
{
readFields.set(propOffset);
}
return propertyReader.readProperty(rs, rsOffset, data, propOffset);
}
catch (java.sql.SQLException sqle)
{
throw new PersistenceException(sqle);
}
}
/**
* Set this record's version to the canonical, shared version number for this record.
*
* @param sharedVersion
* DMO version information.
*/
void version(AtomicIntegerArray sharedVersion)
{
this.sharedVersion = sharedVersion;
this.version = sharedVersion.get(DmoVersioning.VERSION);
}
/**
* Indicate whether this record's data is stale, based on changes committed by other user contexts.
* <p>
* Always returns {@code false} for temp-table records.
*
* @return {@code true} if this record's version does not match the canonical, shared version of the
* record known to the FWD runtime; {@code false} if the versions match or if versioning is
* disabled for this record.
*/
boolean isStale()
{
if (sharedVersion == null)
{
return false;
}
return version != sharedVersion.get(DmoVersioning.VERSION);
}
/**
* The current application transaction has been committed. We do not need to maintain a change set any
* longer. This is only invoked if this record was modified during the transaction, so increment this
* DMO's version and the shared version.
*/
void commit()
{
// discard tracked changes for UNDO
changeSet = null;
// update the canonical, shared version of this record
if (sharedVersion != null)
{
// note: we rely on the fact that SavepointManager (which invokes this method from its
// Block.commit() method) registers for master transaction commit AFTER the caller of
// Session.commit(), so the transaction should have been committed before this method is invoked
do
{
version = sharedVersion.incrementAndGet(DmoVersioning.VERSION);
} while (version == DmoVersioning.UNVERSIONED);
}
}
/**
* Perform a full or sub-transaction level rollback of this record to its state as it was immediately
* before the block of the specified transaction nesting level was entered.
*
* @param txLevel
* The nesting level of sub-transactions of the block being rolled back, where 0 represents
* the full transaction block, and a positive number represents that number of nested
* sub-transactions.
*/
void rollback(int txLevel)
throws PersistenceException
{
if (changeSet == null)
{
// this should not happen; we should only get this call if this record had at least one change
// during an application transaction
return;
}
changeSet.rollback(txLevel, this);
// discard the change set after rollback of the full transaction
if (txLevel == 0)
{
changeSet = null;
}
}
/**
* Perform a sub-transaction level changeset reset of this record
* @param txLevel
* The nesting level of sub-transactions of the block being rolled back, where 0 represents
* the full transaction block, and a positive number represents that number of nested
* sub-transactions.
*/
void resetChangeSet(int txLevel)
{
if (changeSet == null)
{
// this should not happen; we should only get this call if this record had at least one change
// during an application transaction
return;
}
changeSet.resetTxLevel(txLevel);
}
/**
* Perform a sub-transaction level changeset roll-up of this record
* @param txLevel
* The nesting level of sub-transactions of the block being rolled back, where 0 represents
* the full transaction block, and a positive number represents that number of nested
* sub-transactions.
*/
public void rollUpChanges(int txLevel)
{
if (changeSet == null)
{
// this should not happen; we should only get this call if this record had at least one change
// during an application transaction
return;
}
changeSet.rollUpChanges(txLevel);
resetChangeSet(txLevel+1);
}
/**
* Get a bit set representing dirty unique or non-unique indices, subject to the conditions described by
* the parameters. A set bit in the returned set indicates a dirty index. The position of each set bit
* corresponds with the 0-based position of the associated index in the array of unique or non-unique
* indices for the record.
*
* @param indices
* The array of bit sets representing the indices to be checked. This must be either the set of
* unique indices or non-unique indices associated with the record. Only one or the other can be
* checked by a call to this method.
* @param dirtyOffsets
* Optional offsets of dirty properties which must be a component of the indices returned.
* @param filter
* An inclusive filter to apply to the results. If non-empty, each bit in the filter's bit set
* represents the position of an index to be checked in the {@code indices} parameter. If empty,
* the returned bit set will be empty.
* @param full
* If {@code true}, ALL index components must be dirty for an index to be considered dirty.
* If {@code false}, one or more index components must be dirty for an index to be considered
* dirty.
*
* @return A bit set, as described above. <code>null</code> is returned when no indices are found.
*/
private BitSet getDirtyIndices(BitSet[] indices, BitSet dirtyOffsets, BitSet filter, boolean full)
{
BitSet result = new BitSet();
int dirtyOffset = dirtyOffsets.nextSetBit(0);
do
{
if (filter.isEmpty() || (dirtyOffset >= 0 && !dirtyProps.get(dirtyOffset)))
{
// filter is set to filter ALL indices OR property at offset is not dirty, so there can be no
// fully dirty indices containing the given property
dirtyOffset = dirtyOffsets.nextSetBit(dirtyOffset + 1);
continue;
}
BitSet dirty = null;
for (int i = filter.nextSetBit(0); i >= 0; i = filter.nextSetBit(i + 1))
{
BitSet orig = indices[i];
if (dirtyOffset >= 0 && !orig.get(dirtyOffset))
{
continue;
}
BitSet copy = (BitSet) orig.clone();
copy.and(dirtyProps);
if (full ? copy.equals(orig) : !copy.isEmpty())
{
if (dirty == null)
{
dirty = new BitSet();
}
dirty.set(i);
}
}
if (dirty != null)
{
result.or(dirty);
}
dirtyOffset = dirtyOffsets.nextSetBit(dirtyOffset + 1);
}
while(dirtyOffset != -1);
return result;
}
/**
* A property's value has changed. Update the record's internal state accordingly and set up rollback
* information if the record is undoable.
*
* @param offset
* The datum's offset in the record's data array.
* @param datum
* The new value of the datum (may be {@code null}).
*/
private void dataChanged(int offset, Object datum)
throws PersistenceException
{
if (activeBuffer == null)
{
return;
}
// update the unvalidated properties bitset
if (_recordMeta().validatable.get(offset))
{
unvalidated.set(offset);
}
// update the null property bitset
nullProps.set(offset, datum == null);
int flags = CHANGED;
updateState(flags, true);
int txLevel = prepareChangeSet(activeBuffer.getSession());
if (txLevel >= 0)
{
changeSet.logStateChange(flags, txLevel);
changeSet.logDataChange(txLevel, offset, datum);
}
}
/**
* Multiple property values have changed. Update the record's internal state accordingly and set up rollback
* information if the record is undoable.
*
* @param offsets
* A list of offsets for the changed values in record's data array.
* @param datums
* A list of new values for the changed datums.
*/
private void bulkDataChanged(List<Integer> offsets, List<Object> datums)
throws PersistenceException
{
if (activeBuffer == null)
{
return;
}
for (int i = 0; i < offsets.size(); i++)
{
int offset = offsets.get(i);
Object datum = datums.get(i);
// update the unvalidated properties bitset
if (_recordMeta().validatable.get(offset))
{
unvalidated.set(offset);
}
// update the null property bitset
nullProps.set(offset, datum == null);
}
int flags = CHANGED;
updateState(flags, true);
int txLevel = prepareChangeSet(activeBuffer.getSession());
if (txLevel >= 0)
{
changeSet.logStateChange(flags, txLevel);
for (int i = 0; i < offsets.size(); i++)
{
int offset = offsets.get(i);
Object datum = datums.get(i);
changeSet.logDataChange(txLevel, offset, datum);
}
}
}
/**
* Updates the set of state flags for this record. Depending on {@code add} parameter, the
* flags can be set (when {@code true}) or removed (when {@code false}. If one flag is already
* set and {@code add == true} that flag remains unchanged. The same stands for un-setting a
* not active flag.
*
* @param flags
* A set of OR-ed {@code RecordState} flags to be updated.
* @param set
* If {@code true} the flags will be set, otherwise they will be unset.
*
* @see RecordState
*/
private void updateState(int flags, boolean set)
{
if (set)
{
state.state |= flags;
}
else
{
state.state &= ~flags;
}
}
/**
* If this is the first update to this record since an application transaction began, prepare a
* change set to track updates made to the record during the transaction, to be used for full or
* sub-transaction rollback, if needed.
* <p>
* For a change set to be prepared, the following conditions must be met:
* <ul>
* <li>a record buffer must be associated with the record, indicating that this call is being made
* in the context of a business logic call into the record;</li>
* <li>the record must be associated with an undoable table;</li>
* <li>an application-level transaction must currently be active.</li>
* </ul>
* <p>
* If a change set for the current transaction already exists, another is not created.
* <p>
* A non-negative return value indicates a change set exists and is ready to track changes.
*
* @param session
* Database session to be interrogated for transaction information.
*
* @return The current transaction nesting level, or -1 if any of the above conditions are not met.
*/
private int prepareChangeSet(Session session)
throws PersistenceException
{
Long txNestingId;
if ((state.state & NOUNDO) == NOUNDO ||
(state.state & COPY) == COPY ||
session == null ||
(txNestingId = session.getTxNestingId()) == null)
{
// record is not in a valid state for tracking undoable changes
return -1;
}
// high order 2 words of transaction nesting ID are nesting level, low order 2 words are
// transaction ID
int txLevel = (int) ((txNestingId & (0xFFFFFFFFL << 32)) >>> 32);
int txId = (int) (txNestingId & (0xFFFFFFFFL));
if (txLevel >= 0)
{
// create a change set if one with the same transaction ID does not already exist
if (changeSet == null || !changeSet.verify(session, txId))
{
changeSet = new ChangeSet(session, txId, data, state.state, txLevel);
}
// register this undoable record with the savepoint manager at the current transaction nesting
// level
session.trackUndoable(this);
}
return txLevel;
}
/**
* Obtain an exclusive lock for the current record on behalf of the active buffer, if any.
*
* @return {@code true} if the lock is successfully obtained, or if it is unneeded due to system state,
* or if there is no active buffer. {@code false} if there was an error obtaining the lock (and
* we are in silent error mode).
*/
private boolean lockForUpdate()
{
if (DatabaseManager.isInitializing())
{
// no locking needed at this time
return true;
}
// TODO: in certain, unstudied cases [activeBuffer] remains null, causing a NPE to be thrown
if (activeBuffer == null)
{
//Exception exc = new RuntimeException("BaseRecord.lockForUpdate(activeBuffer == null)");
//LOG.log(Level.FINER, "", exc);
return true;
}
try
{
// try to get an exclusive lock
activeBuffer.upgradeLock();
return true;
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(
142,
"Unable to update " + _recordMeta().tables[0] + " Field",
exc);
return false;
}
}
/**
* Get the name of the primary table associated with this record.
*
* @return Primary table name.
*/
private String getTable()
{
String[] tables = _recordMeta().tables;
return (tables != null && tables.length > 0) ? tables[0] : "";
}
/**
* Get the size of the record.
*
* @return Size of the record in bytes.
*/
public long getSize()
{
PropertyMeta[] pm = _recordMeta().getPropertyMeta(false);
long size = 0;
for (int i = 0; i < data.length; i++)
{
size += pm[i].getDataHandler().getFieldSizeInIndex(data[i]);
}
return size;
}
}