DmoMeta.java
/*
** Module : DmoMeta.java
** Abstract : Stores information (both legacy and SQL) on a DMO.
**
** Copyright (c) 2019-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 OM 20191015 First revision.
** 002 ECF 20200916 Added schema name.
** OM 20200919 Improved access to dmo metadata.
** CA 20200922 Added propsByGetter.
** OM 20209024 Fixed getter names in getPropsByGetterMap(). Small optimization.
** OM 20200924 Index components carry multiple information to avoid map lookups for them.
** CA 20200927 Use IdentityHashMap instead of plain map when the key is a Class.
** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access.
** OM 20201002 Use DmoMeta cached information instead of map lookups.
** OM 20201012 Moved here delegated methods from manager.
** AIL 20201125 Added DMO signature computation in constructor.
** 20201126 Added unique constraints to DMO signature.
** 20201203 Added helper methods.
** 20201210 Changes signature from string to specialized class.
** OM 20201120 Added NO-UNDO attribute support.
** OM 20201202 Added getPrimaryIndex() method.
** IAS 20201210 Added support for word tables.
** IAS 20210219 Word tables support for custom extents
** IAS 29210401 Added 'isVST' and 'isMeta' methods
** CA 20210508 Added a signature for the DMO schema, used by shared buffer/temp-table match.
** ECF 20210511 Normalize field name (right-trim and lowercase) before processing in byLegacyName.
** ECF 20210910 Minor cleanup.
** ECF 20211202 Manage a shared map of P2JField descriptors which are needed to define dynamic temp-tables
** modeled after the DMO described by this DMO metadata, rather than recreating the
** descriptors for every request to build such a dynamic temp-table.
** OM 20211020 Added _DATASOURCE_ROWID property.
** OM 20220301 Fixed initialization of fields with [INITIAL ?] option.
** OM 20220303 Better implementation of initial value of fields.
** OM 20220707 Shared meta-knowledge about fields and properties of a record was collected in
** DmoMeta to avoid duplicating their collection and storage.
** OM 20220727 FieldId and PropertyId are different for denormalized extent fields. Renamed some fields
** to a better suited name.
** TJD 20220504 Upgrade do Java 11 minor changes.
** CA 20221114 Added API to get the denormalized properties for a DMO extent property name.
** ECF 20221207 Changed heuristic to extract schema name from full DMO interface name.
** OM 20221220 Added getFieldCount() method.
** OM 20220706 The objects returned by getDatabaseIndexes() carry the legacy names, too.
** OM 20220914 Use MAX-WIDTH to generate custom sized varchar columns.
** IAS 20221018 Fixed processing of 'now' and 'today' INITIAL attribute value.
** 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 20230118 Eliminated some of the uses of String.toUpperCase and/or String.toLowerCase
** for performance.
** CA 20221114 Added API to get the denormalized properties for a DMO extent property name.
** ECF 20221207 Changed heuristic to extract schema name from full DMO interface name.
** 003 AL2 20230120 Compute if the underlying DMO requires extra computed columns.
** 004 AL2 20230317 Extents and date-time extra column are not computed columns; it is safe to exclude them.
Avoid lazy initializing hasComputedColumns to avoid concurrency issues.
** 005 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 006 AL2 20230512 Use CaseInsensitiveHashMap instead of IdentityHashMap for byLegacyName (#7336).
** AL2 20230516 Cache the number of datetime-tz fields.
** AL2 20230517 Cache the number of SQL fields instead, independently from the field type.
** 007 SVL 20230516 Support for more granular configuration of dirty sharing functionality.
** 008 DDF 20230627 Replaced static variables of DirtyShareSupport with static method calls.
** 009 AD 20231123 Added array of properties ordered by their original property id.
** AD 20231127 Changed array creation in computeExistingFields() to have exact capacity from the start.
** 010 AL2 20231211 Check if the DMO has non-expanded extents.
** 011 OM 20231218 Made [propsByLegacy] immutable and initialized it in c'tor.
** 012 CA 20240318 INITIAL value for a DATETIME(-TZ) field will always use 'mdy' as date format.
** 013 OM 20240328 Fixed serialize name in getTableDefinition().
** 014 OM 20240510 Added auto detection for date format, depending on the context and a quick heuristic
** analysis of the text.
** 015 OM 20240524 Added initialString attribute. Refined algorithm from H014. It also caused regressions.
** 016 TJD 20240123 Java 17 compatibility updates
** 017 SP 20240531 Fix for getWordTables() to set extentTableName only for normalized fields.
** 018 AD 20240313 Changed constructor access modifier to public.
** 019 AD 20240321 Created static getter for the properties list set in the constructor.
** 020 AP 20240926 Remove the processing of Relation annotation in order to rely only on runtime query
** processing.
** 021 TJD 20240408 Renamed isEnabledCrossSection to isEnabledCrossSession
** 022 DDF 20240205 Added an additional parameter to addIndexComponent which represents
** the non-null property value of the component.
** 023 OM 20250326 Replaced in isVST() with getMetaTable() method, for better flexibility and performance.
** 024 AS 20250529 Changed the access modifier for getExistingFieldsMap() from private to public.
** Return an immutable map in getExistingFieldsMap.
*/
/*
** 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.convert.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.annotation.*;
import com.goldencode.p2j.persist.annotation.Table;
import com.goldencode.p2j.persist.dirty.*;
import com.goldencode.p2j.persist.meta.*;
import com.goldencode.p2j.persist.orm.types.TypeManager;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.util.CaseInsensitiveHashMap;
import com.google.common.collect.*;
import org.apache.commons.lang3.tuple.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.logging.*;
import java.util.stream.*;
/**
* An immutable structure that contains the annotation information on a table and its fields.
* The data is accessed directly from the annotation objects in order to avoid duplicating the
* information locally.
*/
public class DmoMeta
{
/** Logger */
private static final CentralLogger log = CentralLogger.get(DmoMeta.class.getName());
/** The id generator. It guarantees that the {@code id} of each {@code DmoMeta} is unique. */
private static final AtomicInteger idSeed = new AtomicInteger(0);
/** The annotation for the table. */
protected final Table table;
/** The record's runtime-meta data. */
protected RecordMeta recordMeta = null;
/** The set of annotations for each declared property, mapped by its FQL property name. */
protected Map<String, Property> propsByName = new LinkedHashMap<>();
/**
* The annotation to for the properties corresponding to custom extents. If a property was denormalized, it
* will not be part of this list.
*/
protected Map<String, List<Property>> customExtents = new LinkedHashMap<>();
/** The set of annotations for each declared property, mapped by its legacy name. */
protected final Map<String, Property> propsByLegacy;
/** The list of indexes of this table. */
protected List<Index> indexes = new ArrayList<>();
/** The interface of this DMO. */
protected final Class<? extends DataModelObject> iface;
/** The interface that will be implemented by {@code DmoClass} es. */
protected final Class<? extends DataModelObject> dmoImplInterface;
/** The implementation class of this DMO. */
protected Class<? extends Record> implClass = null;
/** The map of DMO property names to field descriptors for this DMO metadata. */
private volatile Map<String, P2JField> existingFieldsMap = null;
/** The list of field descriptors for this DMO properties, ordered by their id */
private volatile List<Property> existingFieldPropertiesList = null;
/** The cached list of original fields / properties (lazy initialized in {@code getFields}). */
private List<Property> originalProperties = null;
/** The cached list of indexes (lazy initialized in {@code getDatabaseIndexes()}). */
private List<P2JIndex> databaseIndexes = null;
/** The set of properties mapped by their setters. */
private Map<Method, PropertyMeta> propsBySetter = null;
/** The set of properties mapped by their getters. */
private Map<Method, PropertyMeta> propsByGetter = null;
/** The cached list of sets of properties participating in UNIQUES indexes. */
private List<Set<String>> uniqueConstraints = null;
/** The cached list of lists of whole property instances participating in UNIQUES indexes. */
private ArrayList<List<Property>> uniqueProps = null;
/** The cached list of mandatory fields. */
private List<Property> mandatoryFields = null;
/** The set of indexed, character columns. The value is if they are case-sensitive or not. */
private Map<String, Boolean> computedIndexedProperties = null;
/**
* Map of all foreign relation descriptors, indexed by foreign class.
*/
private final Map<Class<? extends DataModelObject>, RelationInfo> relations = new ConcurrentHashMap<>();
/** Map of all foreign relation descriptors, indexed by local class */
private final Map<Class<?>, RelationInfo> inverseRelations = new ConcurrentHashMap<>();
/** Flags DMOs of temporary tables. */
public final boolean tempTable;
/** Flags DMOs of multi-tenant private tables (include both with-default and no-default cases). */
public final boolean multiTenant;
/** Flags DMOs of multi-tenant private tables with default. Only valid if {@code multiTenant == true}*/
public final boolean multiTenantWithDefault;
/** Schema name associated with this DMO */
public final String schema;
/** Caches the {@code name} attribute (SQL table name) from {@link Table} annotation. */
public final String sqlTable;
/** Caches the interned {@code legacy} attribute from {@link Table} annotation. */
public final String legacyTable;
/** Caches the interned normalized {@code legacy} attribute from {@link Table} annotation. */
public final String normalizedLegacyTable;
/** Caches the {@code afterTable} attribute from {@link Table} annotation. */
public final String afterTable;
/** Caches the {@code beforeTable} attribute from {@link Table} annotation. */
public final String beforeTable;
/** Caches the {@code category} attribute from {@link Table} annotation. Unused at this time. */
public final String category;
/**
* The {@link MetadataManager.SystemTable}, in case this object hold information on a meta table,
* otherwise {@code null}.
*/
public final MetadataManager.SystemTable metaTable;
/** Caches the {@code dirtyRead} attribute from {@link Table} annotation. */
public final boolean dirtyRead;
/** Caches the {@code dirtyIntraRead} attribute from {@link Table} annotation. */
public final boolean dirtyIntraRead;
/** Caches the {@code hasValidation} attribute from {@link Table} annotation. */
public final boolean hasValidation;
/** Caches the {@code isLikeSequential} attribute from {@link Table} annotation. */
public final boolean isLikeSequential;
/** Caches the {@code serializeName} attribute from {@link Table} annotation. */
public final String serializeName;
/** Caches the {@code xmlNodeName} attribute from {@link Table} annotation. */
public final String xmlNodeName;
/** Caches the {@code namespaceUri} attribute from {@link Table} annotation. */
public final String namespaceUri;
/** Caches the {@code namespacePrefix} attribute from {@link Table} annotation. */
public final String namespacePrefix;
/** A signature containing information about the DMO structure and field constraints. */
private final DmoSignature signature;
/** Caches the {@code NO-UNDO} attribute from {@link Table} annotation (TEMP-TABLE specific). */
public final boolean noUndo;
/** The set of properties and their metadata, indexed by their in-memory positions. */
private Pair<Property, PropertyMeta>[] properties = null;
/** Number of physical database fields. This may be different than the number of properties. */
private final int sqlFieldCount;
/** The unique id of this DMO. */
private final int id = idSeed.incrementAndGet();
/**
* Collects all properties of the record whose fields are {@code Progress.Lang.Object}-typed. Only makes
* sense for temp tables. When empty, the OO support (ref-counting) is disabled.
*/
private Set<String> ooFields = null;
/** {@code UnmodifiableMap} of property names to getter methods for simple POJO fields only. */
private Map<String, Method> pojoGetterMap = null;
/** {@code UnmodifiableMap} of property names to setter methods for simple POJO fields only. */
private Map<String, Method> pojoSetterMap = null;
/** Map of property names to getter methods for legacy fields only */
private Map<String, Method> legacyGetterMap = null;
/** Map of property names to setter methods for legacy fields only */
private Map<String, Method> legacySetterMap = null;
/** Map of property names to extents for DMO array properties */
private Map<String, Integer> extentMap = null;
/** Check if this DMO is going to have computed columns in the underlying database representation */
private final Boolean[] hasComputedColumns = new Boolean[2];
/** {@code true} if this DMO has normalized extents */
private boolean nonExpandedExtents;
/** The map used in type-widening operations. */
private static final Map<Class<? extends BaseDataType>, Class<? extends BaseDataType>> toWider =
new IdentityHashMap<>();
static
{
// toWider.put(blob.class , blob.class);
toWider.put(character.class , Text.class);
// toWider.put(clob.class , clob.class);
// toWider.put(comhandle.class , comhandle.class);
toWider.put(datetimetz.class, date.class);
toWider.put(datetime.class , date.class);
// toWider.put(date.class , date.class);
toWider.put(decimal.class , NumberType.class);
// toWider.put(handle.class , handle.class);
toWider.put(int64.class , NumberType.class);
toWider.put(integer.class , NumberType.class);
// toWider.put(logical.class , logical.class);
// toWider.put(object.class , object.class);
toWider.put(raw.class , BinaryData.class);
toWider.put(recid.class , NumberType.class);
// toWider.put(rowid.class , rowid.class);
}
/**
* The constructor initializes the data structure.
*
* @param dmoImplInterface
* The interface that will be implemented by {@code DmoClass} es.
* @param iface
* The interface that contains the field declarations and the DMO annotations. It is a
* parent of {@code dmoImplInterface} or {@code dmoImplInterface} itself. Its (getter)
* methods are iterated in order to extract the annotations for the fields.
* @param table
* The {@code Table} annotation of {@code iface}. This is the same value from
* {@code iface.getAnnotation(Table.class)}.
*/
public DmoMeta(Class<? extends DataModelObject> dmoImplInterface,
Class<? extends DataModelObject> iface,
Table table)
{
this.table = table;
this.sqlTable = table.name().intern();
this.legacyTable = table.legacy().intern();
this.normalizedLegacyTable = this.legacyTable.toLowerCase().intern();
this.dirtyRead = table.dirtyRead();
this.dirtyIntraRead = table.dirtyIntraRead();
this.afterTable = table.afterTable();
this.beforeTable = table.beforeTable();
this.category = table.category();
this.hasValidation = table.hasValidation();
this.isLikeSequential = table.isLikeSequential();
this.serializeName = table.serializeName();
this.xmlNodeName = table.xmlNodeName();
this.namespaceUri = table.namespaceUri();
this.namespacePrefix = table.namespacePrefix();
this.noUndo = table.noUndo();
this.metaTable = MetadataManager.byLegacyName(this.legacyTable);
String mtType = table.tenantType();
this.multiTenant = mtType.equals("multi-tenant") || mtType.equals("multi-tenant, with-default");
this.multiTenantWithDefault = mtType.equals("multi-tenant, with-default");
this.iface = iface;
this.dmoImplInterface = dmoImplInterface;
this.tempTable = Temporary.class.isAssignableFrom(iface);
this.schema = getSchemaByInterface(iface, tempTable);
// iterate [Indices] annotation and save its content
Indices indices = iface.getAnnotation(Indices.class);
if (indices != null)
{
indexes = Arrays.asList(indices.value());
}
List<Property> properties = getPropertiesList(iface, tempTable);
// sort properties according to their [id] field. This way, the [fields] [LinkedHashMap]
// will iterate them in the right order (this is because the [getMethods()] returned
// the list of methods in a chaotic order)
properties.sort(Comparator.comparingInt(property -> property.propId));
// now put them in the [fields] member, mapped by name
// at the same time populate the immutable [propsByLegacy] map
int fieldsCount = 0;
Map<String, Property> tmpMap = new CaseInsensitiveHashMap<>(true);
for (Property property : properties)
{
propsByName.put(property.name, property);
if (property.original != null)
{
customExtents.computeIfAbsent(property.original, n -> new ArrayList<>()).add(property);
}
if (property.propId > 0) // skipping reserved properties
{
tmpMap.put(property.legacyLower, property);
}
boolean normalized = property.extent > 0 && property.index == 0;
if (!normalized)
{
Class<?> fwdType = property._fwdType;
if (BaseDataType.class.isAssignableFrom(fwdType))
{
fieldsCount += TypeManager.getDataHandler((Class<? extends BaseDataType>) fwdType).propertySize();
}
else
{
// is this a good fall-back?
fieldsCount++;
}
}
else
{
this.nonExpandedExtents = true;
}
}
sqlFieldCount = fieldsCount;
this.propsByLegacy = Collections.unmodifiableMap(tmpMap);
// build a String signature suitable for identifying if two DMOs are compatible in certain
// operations (for example copy-temp-table).
signature = DMOSignatureHelper.buildSignature(this);
// the eventual relations cannot be created at this moment, they will be processed at a
// later time, when the respective DMO interfaces will be loaded.
}
/**
* Static getter of properties for a DMO interface.
* @param iface
* The class representing a DMO interface.
* @param tempTable
* True if this DMO represents a temp table. False otherwise.
*
* @return All the properties of the given DMO interface, plus reserved properties if needed.
*/
public static List<Property> getPropertiesList(Class<? extends DataModelObject> iface, boolean tempTable)
{
// iterate all methods to identity the fields of the table. They are the getters which
// are annotated with [Property]
Method[] allMethods = iface.getMethods();
List<Property> properties = new ArrayList<>(allMethods.length / 2 + 5);
for (Method method : allMethods)
{
com.goldencode.p2j.persist.annotation.Property ann = method.getAnnotation(
com.goldencode.p2j.persist.annotation.Property.class);
if (ann != null && ann.id() > 0)
{
properties.add(new Property(ann, method));
}
}
// add reserved properties:
properties.add(ReservedProperty.ID); // for all tables
if (tempTable)
{
// for all temp-tables:
properties.add(ReservedProperty._MULTIPLEX);
// these are available even if there is no before-table option for this temp-table.
properties.add(ReservedProperty._ERRORFLAG);
properties.add(ReservedProperty._ORIGINROWID);
properties.add(ReservedProperty._DATASOURCEROWID);
properties.add(ReservedProperty._ERRORSTRING);
properties.add(ReservedProperty._PEERROWID);
properties.add(ReservedProperty._ROWSTATE);
}
return properties;
}
/**
* Return the schema name associated with the given DMO interface.
*
* @param dmoIface
* A DMO interface; if a temporary DMO, the temp table schema name is returned.
* @param temporary
* {@code true} if {@code dmoIface} represents a legacy temp-table, else {@code false}.
*
* @return Schema name for the given interface.
*/
private static String getSchemaByInterface(Class<? extends DataModelObject> dmoIface, boolean temporary)
{
if (temporary)
{
return DatabaseManager.TEMP_TABLE_SCHEMA;
}
String schema = null;
// heuristically attempt to find schema using the package name
String[] nameTokens = dmoIface.getName().split("\\.");
int len = nameTokens.length;
if (len > 1)
{
schema = nameTokens[len - 2];
}
return schema;
}
/**
* Obtain the interface that contains the annotations.
*
* @return the interface that contains the annotations.
*/
public Class<? extends DataModelObject> getAnnotatedInterface()
{
return iface;
}
/**
* Obtain the class that implements the DMO interface.
*
* @return the class that implements the DMO interface.
*/
public Class<? extends Record> getImplementationClass()
{
return implClass;
}
/**
* Obtain the implemented DMO interface.
*
* @return the implemented DMO interface.
*/
public Class<? extends DataModelObject> getDmoImplInterface()
{
return dmoImplInterface;
}
/**
* Obtain the schema name for the DMO represented by this object.
*
* @return Schema name.
*/
public String getSchema()
{
return schema;
}
/**
* Obtain the unique {@code id} of this instance. The id is unique among all databases. The {@code id} is
* usually not the same for multiple runs because the {@code id}s are assigned in the order the tables are
* processed.
*
* @return the unique id of this instance.
*/
public int getId()
{
return id;
}
/**
* Get the {@code RecordMeta} holding the runtime meta information of this table.
*
* @return the {@code RecordMeta} holding the runtime meta information of this table.
*/
public RecordMeta getRecordMeta()
{
return recordMeta;
}
/**
* Obtain the name of the SQL table for this DMO. For temporary tables representing legacy
* temp-tables, this name represents the base name. When created, a suffix which is unique
* at runtime is added to this base name.
*
* @return the name of the SQL table for this DMO.
*/
public String getSqlTableName()
{
return sqlTable;
}
/**
* Test whether the DMO belongs to a temp table.
*
* @return {@code true} if the DMO belongs to a temp table.
*/
public boolean isTempTable()
{
return tempTable;
}
/**
* Test whether the table of this DMO was marked as "dirty read", that is, if the DMO should
* share its dirty data.
*
* @return {@code true} if the table needs to be tracked by the dirty share manager and was
* hinted at conversion time with {@code dirty-read} XML attribute.
*/
public boolean isDirtyRead()
{
return DirtyShareSupport.isEnabledCrossSession() &&
(DirtyShareSupport.isForceCrossSession() || dirtyRead) ||
DirtyShareSupport.isEnabledIntraSession() &&
(DirtyShareSupport.isForceIntraSession() || dirtyIntraRead);
}
/**
* Obtain meta information about a field specified by its property name.
*
* @param propName
* The field's property name.
*
* @return Meta information about {@code propName} field if it exists, or {@code null} otherwise.
*/
public Property getFieldInfo(String propName)
{
return propsByName.get(propName);
}
/**
* Get the list of denormalized properties associated with this extent DMO property.
*
* @param propName
* The field's property name.
*
* @return The list of {@link #customExtents denormalized} properties, or <code>null</code> if this is
* not a denormalized or not a extent property.
*/
public List<Property> getCustomExtentFieldInfo(String propName)
{
return customExtents.get(propName);
}
/**
* Get the number of properties in this DMO.
*
* @return The size of {@link #propsByName}.
*/
public int getFieldSize()
{
return propsByName.size();
}
/**
* Looks up a {@code Property} by its legacy field name.
*
* @param legacyField
* The name of the legacy field.
*
* @return The {@code Property} whose legacy name was passed as parameter.
*/
public Property byLegacyName(String legacyField)
{
return propsByLegacy.get(legacyField);
}
/**
* Obtain the list of properties for this DMO.
*
* @param extra
* If {@code true} the surrogate/hidden properties ({@code id}, {@code _multiplex})
* are also added, otherwise only the original fields are returned.
*
* @return An iterator for list of properties for this DMO.
*/
public Iterator<Property> getFields(boolean extra)
{
if (extra)
{
return propsByName.values().iterator();
}
if (originalProperties == null)
{
ArrayList<Property> buildList = new ArrayList<>(propsByName.size());
Set<Map.Entry<String, Property>> entries = propsByName.entrySet();
for (Map.Entry<String, Property> entry : entries)
{
Property prop = entry.getValue();
if (prop.propId >= 0)
{
buildList.add(prop);
}
}
buildList.trimToSize();
originalProperties = Collections.unmodifiableList(buildList);
}
return originalProperties.iterator();
}
/**
* Obtain the number of fields in the table.
*
* @param extra
* If true, the "hidden" fields are taken into account. Use {@code false} to get the number of
* fields in the original table definition.
*
* @return the number of fields in the table, as described above.
*/
public int getFieldCount(boolean extra)
{
if (extra)
{
return propsByName.size();
}
getFields(false); // triggers setup of [originalProperties]
return originalProperties.size();
}
/**
* Get the descriptor of a field corresponding with the given DMO property name.
*
* @param name
* DMO property name for the desired field descriptor.
*
* @return Field descriptor.
*/
public P2JField getExistingField(String name)
{
return getExistingFieldsMap().get(name);
}
/**
* Get the descriptor of a field corresponding with the given DMO field id.
*
* @param fieldId
* DMO property field id for the desired field descriptor.
*
* @return Field descriptor.
*/
public Property getExistingFieldProperty(int fieldId)
{
return getExistingFieldsList().get(fieldId - 1);
}
/**
* Get the field descriptors for the table associated with this metadata object.
*
* @return Collection of the fields of the target table.
*/
public Collection<P2JField> getExistingFields()
{
return getExistingFieldsMap().values();
}
/**
* Determine whether the given DMO property represents the leading component of any index on this table.
*
* @param property
* DMO property to be tested.
*
* @return {@code true} if {@code property} is a leading index component; else {@code false}.
*/
public boolean isLeadingIndexComponent(String property)
{
Iterator<P2JIndex> ixdIt = getDatabaseIndexes();
if (ixdIt == null)
{
return false;
}
while (ixdIt.hasNext())
{
ArrayList<P2JIndexComponent> comps = ixdIt.next().components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent leadingProp = comps.get(i);
if (property.equals(leadingProp.getPropertyName()))
{
return true;
}
}
}
return false;
}
/**
* Return the number of indices for this table (declared in DMO annotations). Depending on the parameter
* the number of unique or non-unique indices will be returned.
*
* @param unique
* Use {@code true} to get the number of unique indices and {@code false} to get the number of
* non-unique indices. To obtain the number of all indices, the called must add the two results.
*
* @return the number of indices, unique or non-unique.
*/
public int getIndexCount(boolean unique)
{
return unique ? recordMeta.getUniqueIndexNames().length : recordMeta.getNonuniqueIndexNames().length;
}
/**
* Get word tables' names map by the field name
*
* @return Word tables' names map by the field name.
*/
public Map<String, WordIndexData> getWordTables()
{
Map<String, WordIndexData> map = new HashMap<>();
for(Index idx: indexes)
{
if (!idx.word())
{
continue;
}
IndexComponent indexComponent = idx.components()[0];
String fieldName = indexComponent.name();
String wordtablename = idx.wordtablename();
String extentTableName = null;
Property property = propsByName.get(fieldName);
if (property != null)
{
int extent = property.extent;
extentTableName = (extent == 0 || property.denormalized)? null: sqlTable + "__" + extent;
fieldName = property.column;
}
else if (indexComponent.extent() > 0)
{
String legacy = indexComponent.legacy();
property = propsByName.values().stream().filter(
f -> legacy.equals(f.legacy)).findFirst().orElse(null);
}
if (property == null)
{
throw new IllegalStateException("Unknown field: " + fieldName);
}
map.put(fieldName, new WordIndexData(wordtablename, extentTableName, property.caseSensitive));
}
return map;
}
/**
* Get word tables' names map by the index name
*
* @return Word tables' names map by the index name.
*/
public Map<String, String> getWordTablesByIndexName()
{
return indexes.stream()
.filter(Index::word)
.collect(Collectors.toMap(Index::name, Index::wordtablename));
}
/**
* Get an iterator on all indexes of the DMO. The resulting iterator returns {@code P2JIndex}
* objects, each of which defines the columns participating in an index.
*
* @return Iterator on index definition objects.
*/
public Iterator<P2JIndex> getDatabaseIndexes()
{
if (databaseIndexes == null)
{
ArrayList<P2JIndex> buildList = new ArrayList<>();
for (Index index : indexes)
{
P2JIndex p2jIndex = new P2JIndex(
sqlTable, index.name(), index.unique(), index.primary(),
index.word(), index.wordtablename());
p2jIndex.setLegacyName(index.legacy());
for (IndexComponent comp : index.components())
{
String compName = comp.name();
String legacy;
String column;
int extent = 0;
Property property = propsByName.get(compName);
if (property == null)
{
legacy = comp.legacy();
extent = comp.extent();
if (extent == 0)
{
throw new IllegalStateException("Unknown field: " + compName + "/" + legacy);
}
List<Property> props = propsByName.values().stream().filter(
p -> legacy.equals(p.legacy)).collect(Collectors.toList());
property = props.get(0);
column = comp.name();
}
else
{
legacy = property.legacy;
column = property.column;
}
boolean ignoreCase = property._isCharacter && !property.caseSensitive;
boolean abbr = false; // TODO: add support
p2jIndex.addComponent(legacy,
extent,
compName,
column,
comp.descending(),
property._isCharacter,
ignoreCase,
abbr,
property.mandatory);
}
buildList.add(p2jIndex);
}
buildList.trimToSize();
this.databaseIndexes = Collections.unmodifiableList(buildList);
}
return databaseIndexes.iterator();
}
/**
* Get the primary index. If a PRIMARY index is found it is returned. If no index was declared PRIMARY and
* {@code implicit == false} the method returns {@code false}. If {@code implicit == true}, the first
* declared index is returned. If the table does not define any fields, {@code null} is returned.
*
* @param implicit
* If {@code true}, if there is no explicit primary index, the first index is returned as being
* the <i>implicit</i> primary index. Otherwise, in the event there is no primary index declared,
* the method returns {@code null}.
*
* @return The PRIMARY index, the primary IMPLICIT index or {@code null} as described above.
*/
public P2JIndex getPrimaryIndex(boolean implicit)
{
Iterator<P2JIndex> it = getDatabaseIndexes();
if (!it.hasNext())
{
return null; // no indexes defined, whatsoever
}
while (it.hasNext())
{
P2JIndex index = it.next();
if (index.isPrimary())
{
return index;
}
}
return implicit ? this.databaseIndexes.get(0) : null;
}
/**
* Get the list of mandatory fields.
*
* @return The {@link #mandatoryFields}.
*/
public List<Property> getMandatoryFields()
{
if (mandatoryFields == null)
{
List<Property> mfl = new ArrayList<>();
Iterator<Property> iter = getFields(false);
while (iter.hasNext())
{
Property field = iter.next();
if (field.mandatory)
{
mfl.add(field);
}
}
this.mandatoryFields = mfl.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(mfl);
}
return mandatoryFields;
}
/**
* For a given DMO property which may represent an indexed text column, indicate whether that
* column is set to ignore case.
*
* @param property
* DMO property name.
*
* @return {@code true} to ignore case, {@code false} to preserve case sensitivity,
* {@code null} if the property does not represent an indexed text column.
*/
public Boolean isIndexedIgnoreCase(String property)
{
if (computedIndexedProperties == null)
{
Iterator<P2JIndex> iter = getDatabaseIndexes();
Map<String, Boolean> cip = new HashMap<>();
while (iter.hasNext())
{
P2JIndex index = iter.next();
if (index.isWord())
{
continue; // TODO: WORD indices are not yet supported by FWD
}
ArrayList<P2JIndexComponent> comps = index.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent indexComp = comps.get(i);
Property field = propsByName.get(indexComp.getPropertyName());
if (field._isCharacter)
{
cip.put(field.name, !field.caseSensitive);
}
}
}
computedIndexedProperties = cip.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(cip);
}
return computedIndexedProperties.get(property);
}
/**
* Obtain the the sets of unique constraints for each DMO. That is, the set of index components
* for each unique index for this DMO. Uses lazy initialization, the list for each DMO is
* computed at first access.
*
* @return list of sets of properties participating in UNIQUES indexes.
*/
public List<Set<String>> getUniqueConstraints()
{
if (uniqueConstraints == null)
{
List<Set<String>> ucl = new ArrayList<>();
Iterator<P2JIndex> it = getDatabaseIndexes();
it.forEachRemaining(idx -> {
if (idx.isUnique())
{
Set<String> idxComps = new HashSet<>();
ArrayList<P2JIndexComponent> comps = idx.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent comp = comps.get(i);
idxComps.add(comp.getPropertyName());
}
idxComps = Collections.unmodifiableSet(idxComps);
ucl.add(idxComps);
}
});
uniqueConstraints = ucl.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(ucl);
}
return uniqueConstraints;
}
/**
* Obtain the lists of unique constraints for each DMO. That is, the list of index components
* for each unique index for this DMO. Uses lazy initialization, the list for each DMO is
* computed at first access.
*
* @return list of lists of whole property instances participating in UNIQUES indexes.
*/
public ArrayList<List<Property>> getUniqueConstraintsAsProps()
{
if (uniqueProps == null)
{
uniqueProps = new ArrayList<>();
List<Set<String>> uniqueIdxs = getUniqueConstraints();
for (Set<String> idx : uniqueIdxs)
{
List<Property> props = new ArrayList<>();
for (String prop : idx)
{
props.add(propsByName.get(prop));
}
uniqueProps.add(props);
}
}
return uniqueProps;
}
/**
* Get the {@code Property} that correspond to the k-th entry in {@code data} table of the class
* implementing this DMO.
*
* @param k
* The index of the property.
*
* @return the k-th Property.
*/
public Property getProperty(int k)
{
return properties[k].getLeft();
}
/**
* Get {@code PropertyMeta} that correspond to the k-th entry in {@code data} table of the class
* implementing this DMO.
*
* @param k
* The index of the property.
*
* @return the k-th Property.
*/
public PropertyMeta getMeta(int k)
{
return properties[k].getRight();
}
/**
* Get the relation descriptor for the relation from this object's DMO class to
* {@code foreign}.
*
* @param foreign
* DMO interface for the foreign table.
*
* @return Relation descriptor or {@code null} if this DMO has no foreign key relation with
* {@code foreign}.
*/
public RelationInfo getRelation(Class<?> foreign)
{
return relations.get(foreign);
}
/**
* Get a map of converted DMO property names corresponding to simple, bean getter methods.
*
* @return Map as described above.
*/
public Map<String, Method> getPojoGetterMap()
{
if (pojoGetterMap == null)
{
pojoGetterMap = PropertyHelper.pojoGettersByProperty(iface);
}
return pojoGetterMap;
}
/**
* Get a map of converted DMO property names corresponding to simple, bean setter methods.
*
* @return Map as described above.
*/
public Map<String, Method> getPojoSetterMap()
{
if (pojoSetterMap == null)
{
pojoSetterMap = PropertyHelper.pojoSettersByProperty(iface);
}
return pojoSetterMap;
}
/**
* Obtain the set of fields which hold {@code Progress.Lang.Object} of the record. This only makes sense
* for temp-tables. When empty, the OO support (ref-counting) is disabled.
*
* @return An unmodifiable set of {@code Progress.Lang.Object} fields of the table.
*/
public Set<String> getOoFields()
{
if (ooFields == null)
{
// populate the [ooFields]
if (tempTable)
{
Set<String> coll = new HashSet<>();
for (Map.Entry<String, Method> pair : getPojoGetterMap().entrySet())
{
if (pair.getValue().getReturnType() == object.class)
{
coll.add(pair.getKey());
}
}
ooFields = Collections.unmodifiableSet(coll);
}
else
{
ooFields = Collections.emptySet();
}
}
return ooFields;
}
/**
* Get a map of converted DMO property names to corresponding, legacy, getter methods. Legacy methods
* include simple bean getters for scalar fields, and an indexed getter which takes a simple {@code int}
* parameter as the subscript value for extent fields. Even if an extent field has been denormalized, the
* latter is provided for backward compatibility, as some runtime code may not have been converted to use
* the denormalized methods directly.
*
* @return Map as described above.
*/
public Map<String, Method> getLegacyGetterMap()
{
if (legacyGetterMap == null)
{
legacyGetterMap = PropertyHelper.legacyGettersByProperty(iface);
}
return legacyGetterMap;
}
/**
* Get a map of converted DMO property names to corresponding, legacy, setter methods. Legacy methods
* include simple bean setters for scalar fields, and an indexed setter which takes a simple {@code int}
* parameter as the subscript value for extent fields. Even if an extent field has been denormalized, the
* latter is provided for backward compatibility, as some runtime code may not have been converted to use
* the denormalized methods directly.
*
* @return Map as described above.
*/
public Map<String, Method> getLegacySetterMap()
{
if (legacySetterMap == null)
{
legacySetterMap = PropertyHelper.legacySettersByProperty(iface);
}
return legacySetterMap;
}
/**
* Get the map of DMO property names to extents for the DMO type associated with this buffer,
* initializing it first if necessary.
*
* @return Map of DMO array properties to extents.
*/
public Map<String, Integer> getExtentMap()
{
if (extentMap == null)
{
extentMap = PropertyHelper.extentsByProperty(iface);
}
return extentMap;
}
/**
* Use the passed {@code implClass} to extract the property metadata and store locally in an indexed
* array for fast access. The indexed array contain both the {@code Property} annotation and the
* {@code PropertyMeta} data paired so they can be accessed using {@code getProperty(int)} and
* {@code getMeta(int)} methods respectively.
*
* @param implClass
* The implementation class to be processed for extraction of property metadata.
*
* @throws IllegalAccessException
* when the implClass cannot be accessed via reflection.
* @throws NoSuchFieldException
* when the implClass cannot be accessed via reflection.
*/
void setImplClass(Class<? extends Record> implClass)
throws IllegalAccessException, NoSuchFieldException
{
this.implClass = implClass;
Field metaField = implClass.getDeclaredField("_metadata");
metaField.setAccessible(true);
recordMeta = (RecordMeta) metaField.get(null);
PropertyMeta[] propertyMetas = recordMeta.getPropertyMeta(false);
int len = propertyMetas.length;
properties = new Pair[len];
for (int i = 0; i < len; i++)
{
PropertyMeta meta = propertyMetas[i];
properties[i] = Pair.of(propsByName.get(meta.getName()), meta);
}
}
/**
* Add an object which describes a foreign relation from this table to another.
*
* @param foreign
* DMO interface for the foreign table.
* @param info
* Relation descriptor.
*/
void putRelation(Class<? extends DataModelObject> foreign, RelationInfo info)
{
relations.put(foreign, info);
}
/**
* Get an iterator on all foreign relation descriptors for the current
* DMO, which describe relations <em>from</em> this table to another.
*
* @return Iterator on {@link RelationInfo} objects.
*/
public Iterator<RelationInfo> relations()
{
return relations.values().iterator();
}
/**
* Get the relation descriptor for the relation to this object's DMO class from {@code local}.
*
* @param local
* DMO interface for the local table.
*
* @return Relation descriptor or {@code null} if this DMO has no inverse foreign key relation
* with {@code local}.
*/
RelationInfo getInverseRelation(Class<?> local)
{
return inverseRelations.get(local);
}
/**
* Add an object which describes a foreign relation to this table from another.
*
* @param local
* DMO interface for the local table.
* @param info
* Relation descriptor.
*/
void putInverseRelation(Class<?> local, RelationInfo info)
{
inverseRelations.put(local, info);
}
/**
* Get an iterator on all foreign relation descriptors for the current DMO, which describe
* relations <em>to</em> this table from another.
*
* @return Iterator on {@link RelationInfo} objects.
*/
public Iterator<RelationInfo> inverseRelations()
{
return inverseRelations.values().iterator();
}
/**
* Returns (after eventually has constructed) the map of getters to {@code PropertyMeta} map, used to
* fast-access record copy (bypassing the reflection to retrieve a field's value).
*
* @return the map of getters to {@code PropertyMeta} map.
*/
public Map<Method, PropertyMeta> getPropsByGetterMap()
{
if (propsByGetter == null)
{
Map<Method, PropertyMeta> ret = new HashMap<>();
boolean nonBeforeTable = afterTable.isEmpty();
for (Pair<Property, PropertyMeta> property : properties)
{
PropertyMeta prop = property.getRight();
// the hidden before-table specific fields are accessible only for ... before-tables
if (nonBeforeTable && prop.getterName.startsWith("get_"))
{
// skip these properties (their name start with underscore) for the rest of the tables
continue;
}
Class<?>[] sig = (prop.getExtent() == 0) ? new Class[] { } : new Class[] { int.class };
try
{
ret.put(iface.getMethod(prop.getterName, sig), prop);
}
catch (NoSuchMethodException e)
{
if (log.isLoggable(Level.INFO))
{
log.log(Level.INFO,
"Failed to access getter for property '" + prop.getName() + "'.", e);
}
}
Property dmoProp = property.getLeft();
String oName = dmoProp.original;
if (!oName.isEmpty() && dmoProp.index == 1)
{
// denormalized extent fields, handle only once for each original property (for 1st element)
sig = new Class[] { int.class };
try
{
String getterName = (prop.getType() == logical.class ? "is" : "get") +
Character.toUpperCase(oName.charAt(0)) + oName.substring(1);
ret.put(iface.getMethod(getterName, sig), prop);
}
catch (NoSuchMethodException e)
{
if (log.isLoggable(Level.INFO))
{
log.log(Level.INFO,
"Failed to access original indexed getter for property '" + prop.getName() + "'.", e);
}
}
}
}
propsByGetter = ImmutableMap.copyOf(ret);
}
return propsByGetter;
}
/**
* Returns (after eventually has constructed) the map of setters to {@code PropertyMeta} map, used by
* {@code RecordBuffer$Handler}.
*
* @return the map of setters to {@code PropertyMeta} map.
*/
public Map<Method, PropertyMeta> getPropsBySetterMap()
{
if (propsBySetter == null)
{
Map<Method, PropertyMeta> ret = new HashMap<>();
boolean nonBeforeTable = afterTable.isEmpty();
for (Pair<Property, PropertyMeta> property : properties)
{
PropertyMeta prop = property.getRight();
// the hidden before-table specific fields are accessible only for ... before-tables
if (nonBeforeTable && prop.setterName.startsWith("set_"))
{
// skip these properties (their name start with underscore) for the rest of the tables
continue;
}
Class<? extends BaseDataType> wider = toWider.getOrDefault(prop.getType(), prop.getType());
Class<?>[] sig = (prop.getExtent() == 0) ? new Class[] { wider }
: new Class[] { int.class, wider };
try
{
ret.put(iface.getMethod(prop.setterName, sig), prop);
}
catch (NoSuchMethodException e)
{
if (log.isLoggable(Level.INFO))
{
log.log(Level.INFO,
"Failed to access setter for property '" + prop.getName() + "'.", e);
}
}
Property ann = property.getLeft();
String oName = ann.original;
if (!oName.isEmpty() && ann.index == 1)
{
// denormalized extent fields, handle only once for each original property (for 1st element)
sig = new Class[] { int.class, wider };
try
{
String setterName = "set" + Character.toUpperCase(oName.charAt(0)) + oName.substring(1);
ret.put(iface.getMethod(setterName, sig), prop);
}
catch (NoSuchMethodException e)
{
if (log.isLoggable(Level.INFO))
{
log.log(Level.INFO,
"Failed to access original indexed setter for property '" + prop.getName() + "'.", e);
}
}
}
}
propsBySetter = ImmutableMap.copyOf(ret);
}
return propsBySetter;
}
/**
* Get the structure-based signature for this DMO.
*
* @return The signature of this DMO.
*/
public DmoSignature getSignature()
{
return signature;
}
/**
* Obtain a short description of this object, usually for debugging.
*
* @return a short description of this object.
*/
@Override
public String toString()
{
return "DmoMeta{" + iface.getName() + " SQL:" + sqlTable + " 4GL:" + legacyTable + '}';
}
/**
* 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 getTableDefinition()
{
StringBuilder sb = new StringBuilder("DEFINE ");
sb.append(tempTable ? "TEMP-TABLE " : "TABLE ").append(legacyTable);
if (noUndo)
{
sb.append(" NO-UNDO");
}
if (!namespaceUri.isEmpty())
{
sb.append("\n NAMESPACE-URI ").append(namespaceUri);
}
if (!namespacePrefix.isEmpty())
{
sb.append("\n NAMESPACE-PREFIX ").append(namespacePrefix);
}
if (!xmlNodeName.isEmpty())
{
sb.append("\n XML-NODE-NAME ").append(xmlNodeName);
}
if (!serializeName.isEmpty())
{
sb.append("\n SERIALIZE-NAME ").append(serializeName);
}
if (!beforeTable.isEmpty())
{
sb.append("\n BEFORE-TABLE ").append(beforeTable);
}
for (Map.Entry<String, Property> entry : propsByName.entrySet())
{
Property p = entry.getValue();
if (p.propId < 0)
{
continue; // this is a hidden/reserved property, skip it
}
sb.append("\n FIELD ").append(p.legacy).append(" AS ").append(p._ablType.toUpperCase());
if (p.caseSensitive)
{
sb.append(" CASE-SENSITIVE");
}
if (p.extent > 0)
{
sb.append(" EXTENT ").append(p.extent);
}
if (p.scale != 0)
{
sb.append(" DECIMALS ").append(p.scale);
}
if (p.width != 0)
{
sb.append(" MAX-WIDTH ").append(p.width);
}
if (p.initialNull)
{
sb.append(" INITIAL ?");
}
else if (!p.initial.isEmpty())
{
sb.append(" INITIAL ").append(p.initial);
}
if (p.label != null && !p.label.isEmpty())
{
sb.append(" LABEL ").append(p.label);
}
if (p.columnLabel != null && !p.columnLabel.isEmpty())
{
sb.append(" COLUMN-LABEL ").append(p.columnLabel);
}
}
for (Index index : indexes)
{
sb.append("\n INDEX ").append(index.legacy()).append(" AS ");
if (index.primary())
{
sb.append("PRIMARY ");
}
if (index.unique())
{
sb.append("UNIQUE ");
}
if (index.word())
{
sb.append("WORD-INDEX ");
}
for (IndexComponent comp : index.components())
{
sb.append(comp.legacy()).append(comp.descending() ? " DESC " : " ");
}
}
return sb.toString();
}
/**
* Check if DMO corresponds to a meta table. The project must have been converted with support for the
* specific meta table.
*
* @return {@code true} only if DMO corresponds to a system/meta table.
*/
public boolean isMeta()
{
return metaTable != null;
}
/**
* Obtains the {@link MetadataManager.SystemTable} if this corresponds to a meta table. The project must
* have been converted with support for the specific meta table.
*
* @return The {@code SystemTable} if DMO corresponds to a system/meta table, and {@code null} otherwise.
*/
public MetadataManager.SystemTable getMetaTable()
{
return metaTable;
}
/**
* Retrieve the number of fields which are stored physically in the database table. This may not equal
* the number of properties, as some data types require two SQL fields (i.e. datetime-tz).
*
* @return The number of fields which are generated in the database table for this model.
*/
public int getSqlFieldCount()
{
return sqlFieldCount;
}
/**
* Check if this DMO has computed columns in the underlying database representation. This is
* used mostly for temporary tables and may not be completely compatible with persistent tables.
*
* @param includeInvisible
* {@code true} to include the columns which are defined as invisible in the underlying
* database. Invisible columns are the ones which are not implicitly retrieved using
* SELECT * or direct access.
*
* @return {@code true} if we also have other columns that the legacy ones in the database.
*/
public boolean hasComputedColumns(boolean includeInvisible)
{
int position = includeInvisible ? 0 : 1;
if (hasComputedColumns[position] == null)
{
hasComputedColumns[position] = false;
// temp-tables have invisible columns; it will always match the FWD internal representation
boolean relaxed = tempTable && !includeInvisible;
if (!relaxed)
{
Set<Map.Entry<String, Property>> fieldEntries = this.propsByName.entrySet();
Set<Property> indexedProperties = new HashSet<>();
Iterator<P2JIndex> databaseIndexes = this.getDatabaseIndexes();
while (databaseIndexes.hasNext())
{
P2JIndex index = databaseIndexes.next();
ArrayList<P2JIndexComponent> components = index.components();
for (int i = 0; i < components.size(); i++)
{
P2JIndexComponent comp = components.get(i);
Property property = this.getFieldInfo(comp.getPropertyName());
indexedProperties.add(property);
}
}
for (Map.Entry<String, Property> field : fieldEntries)
{
Property property = field.getValue();
if (indexedProperties.contains(property) && property._isCharacter)
{
hasComputedColumns[position] = true;
break;
}
}
}
}
return hasComputedColumns[position];
}
/**
* Creates, if needed, the existingFieldsMap, map of DMO property names to {@code P2JField}
* objects describing the corresponding fields and array of {@code Property},
* existingFieldsPropertyList, holding properties for each DMO field, where the index in
* array is the id of the property minus 1.
*/
private synchronized void computeExistingFields()
{
if (existingFieldsMap == null)
{
Map<String, P2JField> map = new LinkedHashMap<>();
ArrayList<Property> list;
Iterator<Property> it = getFields(false);
list = new ArrayList<>(originalProperties.size());
while (it.hasNext())
{
Property p = it.next();
if (p.propId < 0)
{
// skip ReservedProperties
continue;
}
int extent = p.extent;
if (extent != 0 && p.index > 1)
{
// skip denormalized fields other than the first from series
continue;
}
Class<?> bdtType = p._fwdType;
Object init = null;
if (BaseDataType.class.isAssignableFrom(bdtType))
{
try
{
if (p.initialNull)
{
init = bdtType.getConstructor().newInstance();
}
else
{
if (bdtType == date.class)
{
init = "today".equalsIgnoreCase(p.initial)
? new character(p.initial)
: date.parseInitial(p.initial);
}
else if (bdtType == datetime.class)
{
init = "now".equalsIgnoreCase(p.initial)
? new character(p.initial)
: datetime.parseInitial(p.initial);
}
else if (bdtType == datetimetz.class)
{
init = "now".equalsIgnoreCase(p.initial)
? new character(p.initial)
: datetimetz.parseInitial(p.initial);
}
else if (bdtType == logical.class)
{
String fmt = p.format.isEmpty() ? logical.DEFAULT_FORMAT : p.format;
init = new logical(p.initial, fmt);
}
else // other data types which does not have particular initialization cases
{
init = bdtType.getConstructor(String.class).newInstance(p.initial);
}
}
}
catch (ReflectiveOperationException e)
{
if (log.isLoggable(Level.SEVERE))
{
log.log(Level.SEVERE,
"Failed to instantiate default value for " + p.name + " of " + iface);
}
}
}
P2JField existingLegacyField = new P2JField(p.legacy,
ParmType.fromClass(bdtType),
extent,
p.format,
(BaseDataType) init,
p.label,
p.columnLabel,
p.caseSensitive,
p.codePage,
p.help,
p.serializeHidden,
p.serializeName,
p.xmlDataType,
p.xmlNodeName,
p.xmlNodeType,
p.mandatory,
p.scale,
p.width);
map.put(p.name, existingLegacyField);
while (list.size() < p.id)
{
list.add(null);
}
list.set(p.id - 1, p);
}
list.trimToSize();
existingFieldsMap = Collections.unmodifiableMap(map);
existingFieldPropertiesList = Collections.unmodifiableList(list);
}
}
/**
* Check if this DMO has normalized extents. These should be loaded separately.
*
* @return {@code true} if the DMO has non-expanded extents (kept in a separate SQL table).
*/
public boolean hasNotExpandedExtents()
{
return nonExpandedExtents;
}
/**
* Get the map of DMO property names to {@code P2JField} objects describing the corresponding fields,
* creating it first, if necessary.
*
* @return Map of property names to field descriptors.
*/
public Map<String, P2JField> getExistingFieldsMap()
{
if (existingFieldsMap == null)
{
computeExistingFields();
}
return Collections.unmodifiableMap(existingFieldsMap);
}
/**
* Get the list of DMO {@code Property} objects describing the corresponding fields,
* creating it first, if necessary.
*
* @return List of field descriptors, ordered by their id and each index corresponds
* the objects id.
*/
private List<Property> getExistingFieldsList()
{
if (existingFieldPropertiesList == null)
{
computeExistingFields();
}
return existingFieldPropertiesList;
}
/**
* Word index data.
*/
public static class WordIndexData
{
/** word table name */
public final String wordTableName;
/** extent table name */
public final String extentTableName;
/** Flag indicating that field is case sensitive */
public final boolean caseSensitive;
/**
* Constructor
*
* @param wordTableName
* Word table name
* @param extentTableName
* Extent table name
* @param caseSensitive
* Flag indicating that field is case sensitive
*/
public WordIndexData(String wordTableName, String extentTableName, boolean caseSensitive)
{
this.wordTableName = wordTableName;
this.extentTableName = extentTableName;
this.caseSensitive = caseSensitive;
}
}
}