MetadataManager.java
/*
** Module : MetadataManager.java
** Abstract : Manages databases which contain metadata information
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20131001 Created initial version.
** 002 ECF 20131028 Moved to meta sub-package.
** 003 ECF 20140315 Fixed path from which remote port is read in directory.
** 004 ECF 20141204 Use schema name rather than physical database name to load metaschema data.
** Do not initialize the metadata manager (and server) if no metadata is in use.
** 005 OM 20151130 Adjusted import for H2Helper.
** 006 ECF 20160225 Changes required by new PropertyHelper implementation.
** 007 OM 20160722 Added support for _file._template. Use caching for fast access.
** 008 OM 20160712 Added setupIdentityManager() method for creating p2j_id_generator_sequence.
** 009 OM 20181207 Populated DatabaseFeature metadata table.
** 010 IAS 20200518 Populating _File, _Field, _Index, and _Index-Field metadata tables.
** 011 IAS 20200604 Added support for *-SA and CAN-* fields in _File/_Field tables.
** 012 IAS 20200617 No fatal error if p2j.cfg.xml is not found; mandatory meta tables support
** 013 ECF 20200906 New ORM implementation.
** 014 IAS 20200622 Re-worked metadata configuration in p2j.cfg.xml.
** IAS 20200624 Added _Field._View-As field population.
** CA 20200706 Flush the session before doing the commit, to release the DMOs from Hibernate.
** IAS 20200809 Added _Field.(_Charset, _Collation, _Decimals, _Desc, _dtype, _Fetch-Type, _Fld-case,
** _Sys_field, _Width) fields population.
** GES 20200810 Removed Apache dependency.
** IAS 20200819 Added more mandatory tables.
** OM 20200924 Index components carry multiple information to avoid map lookups for them.
** CA 20200924 Replaced Method.invoke with ReflectASM.
** 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.
** CA 20201003 Use an identity HashSet where possible.
** OM 20201012 Force use locally cached meta information instead of map lookup.
** IAS 20201210 Added support for word tables.
** IAS 20210409 Added option to disable SSL for the remote-meta mode.
** IAS 20210419 Population of the _Area, _Filelist, and _Tenant metatables.
** IAS 20220525 Added metadata reset.
** CA 20210709 Set the _File.desc from the Table.description() annotation.
** ECF 20210910 Code cleanup.
** OM 20220301 Correctly set unknown value as initial value of the field.
** IAS 20220324 Added FILE_NUM_BY_TBL_NAME map.
** IAS 20220509 Add _User table to the FILE_NUM and FILE_NUM_BY_TBL_NAME maps.
** OM 20220516 A bit of code cleanup.
** ECF 20220630 Fixed hard-coded references to converted metadata column/property names which have
** changed.
** TJD 20220504 Upgrade do Java 11 minor changes
** SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
** performance.
** 015 OM 20230411 Fixed support for DUMP-NAME table attribute.
** 016 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 017 OM 20240229 Fixed the meta field type for datetime-tz.
** 018 ES 20240229 Get rid of parsing of the *.meta.xml files for population of the MetaDatabase,
** and made use of DmoMeta, Property, P2JIndex and annotations.
** OM 20240311 Bug fixes. Simplified code. Maintenance.
** OM 20240314 Added support for dynamic permanent tables in addTableToFile().
** 019 OM 20240402 Improved management for mutable permanent tables.
** 020 OM 20240422 Improved logging for tables from mutable databases.
** 021 OM 20240521 Forced a short-lived local transaction in removeDynamicTable() if none is active.
** 022 TJD 20240123 Java 17 compatibility updates
** 023 RAA 20240807 Redirected tenant database to core database in getFileNum.
** 024 OM 20240815 TenantManager.getDatabase() returns a Database object of same type as the argument.
** OM 20240909 Basic implementation of populateTenant() method.
** 025 OM 20241001 Improved _tenant metadata support.
** 026 OM 20240708 Simplified log for creation of table meta information.
** OM 20241021 Added support for initializing extent fields/properties in meta tables.
** 027 OM 20241031 Marked _sec-authentication-system and _sec-authentication-domain tables as REGULAR.
** 028 ICP 20250123 Used integer, logical and character constants to leverage caches instances.
** 029 DDF 20250219 Added filterTables() to replace mandatoryTables().
** DDF 20250221 Corrected the filterTables() condition.
** 030 OM 20250128 Added synchronization of _Tenant table with landlord database via TenantTableUpdater.
** 031 OM 20250310 Added support for READ/ONLY metadata tables.
** 032 OM 20250331 Make sure the [byName] map's keys are in lowercase. Avoid possible duplication of default
** tenant record in _tenant meta table.
*/
/*
** 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.meta;
import java.io.*;
import java.lang.reflect.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import java.util.logging.*;
import com.goldencode.p2j.persist.orm.Property;
import com.goldencode.p2j.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.Record;
import com.goldencode.p2j.persist.annotation.*;
import com.goldencode.p2j.persist.annotation.Trigger;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.orm.DmoMeta.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.CentralLogger;
/**
* This class creates, prepares, and populates the embedded database(s) used to serve metadata
* information to converted business logic and to the persistence framework. All methods are
* static; the class is never instantiated. Most methods are only called during server startup.
* The {@link #isMetaDMO(Class)} method, however, is meant to be used by callers who need to
* determine whether a particular DMO interface is metadata-related.
*/
public final class MetadataManager
{
/** Well-known name of metadata schema */
public static final String META_SCHEMA = "_meta";
/** Well-known prefix of metadata DMOs */
public static final String META_PREFIX = "Meta";
/** Well-known name of DDL script to create tables */
public static final String DDL_TABLE = "meta_table_ddl.sql";
/** Well-known name of DDL script to create indexes */
public static final String DDL_INDEX = "meta_index_ddl.sql";
/** Constant used to identify the {@code _File} meta table. Used a key in {@code helpers}. */
public static final String _FILE = "File";
/** Constant used to identify the {@code _Db} meta table. Used a key in {@code helpers}. */
public static final String _DB = "Db";
/** Constant used to identify the {@code _Lock} meta table. Used a key in {@code helpers}. */
public static final String _LOCK = "Lock";
/** Constant used to identify the {@code _MyConnection} meta table. Used a key in {@code helpers}. */
public static final String _MYCONNECTION = "Myconnection";
/** Constant used to identify the {@code _Connect} meta table. Used a key in {@code helpers}. */
public static final String _CONNECT = "Connect";
/** Constant used to identify the {@code _Trans} meta table. Used a key in {@code helpers}. */
public static final String _TRANS = "Trans";
/** Constant used to identify the {@code _Index} meta table. Used a key in {@code helpers}. */
public static final String _INDEX = "Index";
/** Constant used to identify the {@code _Index} meta table. Used a key in {@code helpers}. */
public static final String _INDEX_FIELD = "IndexField";
/** Constant used to identify the {@code _Field} meta table. Used a key in {@code helpers}. */
public static final String _FIELD = "Field";
/** Constant used to identify the {@code _UserTableStat} meta table. Used a key in {@code helpers}. */
public static final String _USERTABLESTAT = "Usertablestat";
/** Logger */
private static final CentralLogger log = CentralLogger.get(MetadataManager.class);
/** Do we log WARNING-level events ? */
private static final boolean LOG_WARNING = log.isLoggable(Level.WARNING);
/** Do we log INFO-level events ? */
private static final boolean LOG_INFO = log.isLoggable(Level.INFO);
/** The recid of the database in {@code _Db} table. */
private static final int DB_RECID = 4032;
/** Map of data type names to associated _Field._dtype value */
private static final Map<String, Integer> DTYPE = Collections.unmodifiableMap(
new HashMap<String, Integer>()
{{
put("character", 1);
put("date", 2);
put("logical", 3);
put("integer", 4);
put("decimal", 5);
put("recid", 7);
put("raw", 8);
put("blob", 18);
put("clob", 19);
put("short", 22);
put("float", 24);
put("double", 25);
put("fixchar", 31);
put("bigint", 32);
put("time", 33);
put("timestamp", 34);
put("datetime", 34);
put("datetime-tz", 40);
put("int64", 41);
}}
);
/** Map of data type names to associated _Field._fetch-type value */
private static final Map<String, String> FETCH_TYPE = Collections.unmodifiableMap(
new HashMap<String, String>()
{{
put("character", "varchar");
put("date", "date");
put("logical", "bit");
put("integer", "integer");
put("decimal", "numetic");
put("recid", "integer");
put("raw", "varbinary");
put("blob", "varbinary");
put("clob", "varchar");
put("short", "smallint");
put("float", "float");
put("double", "real");
put("fixchar", "character");
put("bigint", "bigint");
put("time", "time");
put("timestamp", "timestamp");
put("datetime", "timestamp");
put("datetime-tz", "timestamp_timezone");
put("int64", "bigint");
}}
);
/** Character field types */
private static final Set<String> CHAR_TYPES = Collections.unmodifiableSet(
new HashSet<String>()
{{
add("character");
add("fixchar");
add("clob");
}}
);
/** Map of short meta DMO names to helper objects */
private static final Map<String, Helper> helpers = new HashMap<>();
/** Map that stores the default (logical) database for a tenant database. */
private static final Map<Database, Database> coreDatabases = new HashMap<>();
/** Set of DMO interfaces which are metadata DMOs */
private static final Set<Class<? extends DataModelObject>> dmoMetaTypes =
Collections.newSetFromMap(new IdentityHashMap<>());
/** Base directory for DMO resources in jar file */
private static final String baseDir = DmoMetadataManager.getDmoBasePackage().replace('.', '/');
/** Stores the status of the metadata server. */
private static boolean inUse = false;
/**
* Cached the template records ({@code _file._template} values) for fast access. The keys are
* the DMO implementation classes of the tables from permanent database. The values are the
* {@link rowid} values as {@link Long} data.
*/
private static final Map<Class<? extends Record>, Long> templateRecords = new IdentityHashMap<>();
/** _File table ID generator */
private static final ConcurrentMap<Database, ConcurrentMap<String, AtomicLong>> TABLE_IDS =
new ConcurrentHashMap<>();
/** _File._File_num values for the primary databases */
private static final ConcurrentMap<Database, ConcurrentMap<Class<?>, integer>> FILE_NUM =
new ConcurrentHashMap<>();
/** _File._File_num values for the primary databases by table name */
private static final ConcurrentMap<Database, ConcurrentMap<String, Integer>> FILE_NUM_BY_TBL_NAME =
new ConcurrentHashMap<>();
/** Word tables' names for the primary databases */
private static final ConcurrentMap<String, Map<String, Map<String, WordIndexData>>> WORD_TABLES =
new ConcurrentHashMap<>();
/** The system tables indexed by their legacy name. The keys are in lowercase. */
private static final Map<String, SystemTable> byName = new HashMap<>();
/**
* Private constructor; this class is not instantiated.
*/
private MetadataManager()
{
}
/**
* Reset in-memory metadata.
*
* @param primaryDB
* primary database
* @param metaDB
* meta database
*/
public static void reset(Database primaryDB, Database metaDB)
{
TABLE_IDS.remove(metaDB);
FILE_NUM.remove(primaryDB);
}
/**
* Indicate whether the given DMO interface represents a metaschema table.
*
* @param dmoIface
* DMO interface to test.
*
* @return {@code true} if {@code dmoIface} represents a metaschema table, else {@code false}.
*/
public static boolean isMetaDMO(Class<? extends DataModelObject> dmoIface)
{
return dmoMetaTypes.contains(dmoIface);
}
/**
* Obtain a {@code SystemTable} by its legacy name.
*
* @param legacyName
* The legacy name.
*
* @return The metadata {@code SystemTable} with specified legacy name, if one exists,
* otherwise {@code null}.
*/
public static SystemTable byLegacyName(String legacyName)
{
return byName.get(legacyName.toLowerCase());
}
/**
* Get _File._File_num value for the table
*
* @param db
* primary db
* @param dmoImpl
* Table DMO class
* @return _File._File_num value for the table
*/
public static integer getFileNum(Database db, Class<?> dmoImpl)
{
return FILE_NUM.computeIfAbsent(db, d -> new ConcurrentHashMap<>()).get(dmoImpl);
}
/**
* Get _File._File_num value for the table
*
* @param db
* The primary database.
* @param tableName
* The converted table name.
*
* @return _File._File_num value for the table
*/
public static Integer getFileNum(Database db, String tableName)
{
if (TenantConfig.isEnabled())
{
// redirect the tenant database to the core one
Database coreDb = coreDatabases.get(db);
if (coreDb == null)
{
String ldb = ConnectionManager.get().getLDBName(db);
if (ldb == null)
{
ldb = db.getName();
}
coreDb = TenantManager.getDatabase(TenantManager.DEFAULT_TENANT_NAME, ldb, true, db.getType());
coreDatabases.put(db, coreDb);
}
db = coreDb;
}
return FILE_NUM_BY_TBL_NAME.computeIfAbsent(db, d -> new ConcurrentHashMap<>()).get(tableName);
}
/**
* Initialize this class and start up a secure database server to allow remote P2J servers to
* access metadata.
*
* @return <code>true</code> if the metadata manager was initialized; <code>false</code> if
* there are no metadata classes found.
*
* @throws PersistenceException
* if there is any error initializing the class or starting the server.
*/
@SuppressWarnings("unchecked")
public static boolean initialize()
throws PersistenceException
{
boolean active;
try
{
active = Configuration.getSchemaConfig().isMetadataActive();
}
catch (Exception e)
{
// TODO: read active metadata and meta files from directory
active = false;
}
if (!active)
{
if (LOG_INFO)
{
log.log(Level.INFO, "Metadata server was not started; no metadata in use");
}
return false;
}
Set<String> legacyTables = SystemTable.filterTables(SystemTable.MANDATORY);
SchemaConfig.Metadata cfgMetadata = Configuration.getSchemaConfig().getMetadata();
if (cfgMetadata != null)
{
legacyTables.addAll(cfgMetadata.getTables());
}
for (String aTable : legacyTables)
{
SystemTable systemTable = byLegacyName(aTable);
if (systemTable == null)
{
// TODO: log a warning ?
continue;
}
if (systemTable.isRegular())
{
// the regular tables are merged in each database, there is no such DMO in meta database
continue;
}
String shortName = getShortName(aTable);
String dmoIfaceName =
DmoMetadataManager.getDmoBasePackage() + "." + META_SCHEMA + "." + META_PREFIX + shortName;
Class<? extends DataModelObject> dmoIface;
Class<? extends Record> dmoImpl;
try
{
dmoIface = (Class<? extends DataModelObject>) Class.forName(dmoIfaceName);
DmoMeta dmoMeta = DmoMetadataManager.registerDmo(dmoIface);
dmoImpl = dmoMeta.getImplementationClass();
}
catch (Exception e)
{
// catch all error cases; just skip the table, but let a message in log
if (LOG_WARNING)
{
log.log(Level.WARNING, "Failed to load _meta DMO: " + dmoIfaceName, e);
}
continue;
}
helpers.put(shortName, new Helper(dmoIface, dmoImpl));
dmoMetaTypes.add(dmoIface);
}
if (LOG_INFO)
{
log.log(Level.INFO, "Metadata server modules: " + helpers.keySet());
}
// start the server for mixed mode connections
DirectoryService ds = DirectoryService.getInstance();
if (!ds.bind())
{
throw new PersistenceException("Directory bind failed");
}
try
{
if (Utils.getDirectoryNodeBoolean(ds, "persistence/remote-meta/enabled", false, false))
{
int port = Utils.getDirectoryNodeInt(ds,
"persistence/remote-meta/port",
DatabaseManager.MIXED_MODE_SSL_PORT,
false);
String portString = String.valueOf(port);
if (LOG_INFO)
{
log.log(Level.INFO,
"Starting secure, mixed mode, H2 metaschema server on port " +
portString + "...");
}
if (Utils.getDirectoryNodeBoolean(ds, "persistence/remote-meta/ssl", true, false))
{
H2Helper.startMixedModeServer("-tcpSSL", "-tcpPort", portString, "-tcpAllowOthers");
}
else
{
H2Helper.startMixedModeServer("-tcpPort", portString, "-tcpAllowOthers");
}
if (LOG_INFO)
{
log.log(Level.INFO, "Successfully started secure mixed mode H2 server");
}
}
}
catch (PersistenceException exc)
{
log.log(Level.SEVERE, "Error starting mixed mode, remote metaschema server", exc);
throw exc;
}
finally
{
ds.unbind();
}
inUse = true;
return true;
}
/**
* Checks the status of one of the tables of the metadata subsystem. Note that this method
* returns {@code true} from the moment the {@link #initialize()} has finished with success.
* At that moment the whole metadata system is not yet ready as the metadata tables are not yet
* populated.
* <p>
* Use this method to check if the project was not configured with this feature or if metadata
* initialization failed, and quickly leave the calling method in these cases. If this method
* returns {@code false} it means that:
* <ul>
* <li>metadata was completely disabled from server configuration; or</li>
* <li>the specified table was not added to metadata block in static config file; or</li>
* <li>the DMO for the specified _meta table was not converted, not found in classpath
* or failed to load and register as a standard DMO interface.</li>
* </ul>
*
* @param meta
* The short name (see {@code _<name>} constants in this class) of a metadata table.
*
* @return {@code true} if the {@code MetadataManager} was successfully initialized, queries
* on internal data are possible and the specified {@code meta} table is active in the
* current configuration.
* Returns {@code false} if the initialization failed, the metadata is not "in use" or
* the specified table does not appear in the {@code metadata} block of the current
* config file.
*/
public static boolean inUse(String meta)
{
return MetadataManager.inUse && helpers.containsKey(meta);
}
/**
* Checks the status of the metadata subsystem. Note that this method returns {@code true} from
* the moment the {@link #initialize()} has finished with success. At that moment the whole
* metadata system is not yet ready as the metadata tables are not yet populated.
* <p>
* Use this method to check if the project was not configured with this feature or if metadata
* initialization failed, and quickly leave the calling method in these cases.
*
* @return {@code true} if the {@code MetadataManager} was successfully initialized and
* queries on internal data are possible and {@code false} if the initialization
* failed and the metadata is not "in use".
*/
public static boolean inUse()
{
return MetadataManager.inUse;
}
/**
* Create and load the embedded metadata database for the associated primary database.
* start up a secure database server to allow remote P2J servers to access this metadata.
*
* @param database
* Database information object.
*
* @throws PersistenceException
* if there is any error creating or loading the database.
*/
public static void prepareDatabase(Database database)
throws PersistenceException
{
if (!Configuration.getSchemaConfig().isMetadataActive())
{
return;
}
try (Session session = new Session(database))
{
Connection conn = session.getConnection();
// define schema
executeDDLScript(database, conn, DDL_TABLE);
// apply indexes
executeDDLScript(database, conn, DDL_INDEX);
}
catch (PersistenceException exc)
{
DBUtils.handleException(database, exc);
throw exc;
}
}
/**
* Set up sequence identity mManager for this database by creating the fresh instance of
* {@code p2j_id_generator_sequence}. Because it is repopulated at each startup the start value
* of the sequence is not crucial; the value is hardcoded to 10000.
*
* @param database
* The database to set up the identity manager for.
* @param dialect
* The dialect for the database. It is used to build the DDL for the sequence create
* statement.
*
* @throws PersistenceException
* if something goes wrong with database connection.
*/
public static void setupIdentityManager(Database database, Dialect dialect)
throws PersistenceException
{
try (Session session = new Session(database))
{
String seq_ddl = dialect.getCreateSequenceString(Persistence.ID_GEN_SEQUENCE, 10000);
Session.createSQLQuery(seq_ddl).executeUpdate(session);
}
}
/**
* Populate the specified, embedded database with metaschema information read from DMO interface
* annotations.
*
* @param metaDb
* Metaschema database to populate.
*
* @throws PersistenceException
* if there is any problem reading the DMO annotations, instantiating DMO instances, or
* inserting records into the database.
*/
public static void populateDatabase(Database metaDb)
throws PersistenceException
{
if (!Configuration.getSchemaConfig().isMetadataActive())
{
if (LOG_INFO)
{
log.log(Level.INFO, "Metadata server was not started; no metadata in use");
}
return;
}
if (!inUse(_FILE))
{
log.log(Level.WARNING,
_FILE + " metadata table not present. Metaschema support for '" +
metaDb.getName() + "' database disabled.");
return;
}
try
{
Persistence p = PersistenceFactory.getInstance(metaDb);
p.beginTransaction(Persistence.META_CTX);
SystemTable.populateAll(metaDb, p);
p.commit(Persistence.META_CTX);
p.executeSQL("analyze", Persistence.META_CTX);
}
catch (Exception exc)
{
throw new PersistenceException("Error populating metadata database " + metaDb, exc);
}
if (LOG_INFO)
{
log.log(Level.INFO, "Metaschema database " + metaDb + " ready");
log.log(Level.INFO, "Caching " + templateRecords.size() + " template record ids");
}
}
/**
* Get the word index data.
*
* @param dbName
* Database name.
* @param tableName
* Master table name.
* @param fieldName
* Field name.
*
* @return The word index data.
*/
public static WordIndexData getWordTableName(String dbName, String tableName, String fieldName)
{
Map<String, Map<String, WordIndexData>> tables = WORD_TABLES.get(dbName);
if (tables == null)
{
return null;
}
Map<String, WordIndexData> fields = tables.get(tableName);
if (fields == null)
{
return null;
}
return fields.get(fieldName);
}
/**
* Adds the details of a dynamic permanent table to meta tables.
*
* @param primaryDb
* The primary database where the table belongs to.
* @param dmoName
* The DMO of the dynamic table.
*/
public static void addDynamicTableToFile(Database primaryDb, String dmoName)
{
Helper fileHelper = getHelper(SystemTable._File.shortName);
if (fileHelper == null)
{
return;
}
Database metaDB = new Database(primaryDb.getName(), Database.Type.META, true);
Persistence p = PersistenceFactory.getInstance(metaDB);
addTableToFile(primaryDb, metaDB, dmoName, fileHelper, p);
}
/**
* Removes a registered dynamic permanent table from this manager.
*
* @param primaryDb
* The primary database
* @param dmoMeta
* The meta information of the table to be removed.
*/
public static void removeDynamicTable(Database primaryDb, DmoMeta dmoMeta)
{
// drop WORD indices, if any
Map<String, WordIndexData> wt = dmoMeta.getWordTables();
if (!wt.isEmpty())
{
Map<String, Map<String, WordIndexData>> schemaMap = WORD_TABLES.get(dmoMeta.schema /*primaryDb ?*/);
if (schemaMap != null)
{
schemaMap.remove(dmoMeta.sqlTable);
}
}
integer fileNum1 = null;
Integer fileNum2 = null;
Long templateFileNum = templateRecords.remove(dmoMeta.getImplementationClass());
ConcurrentMap<Class<?>, integer> fileNumMap = FILE_NUM.get(primaryDb);
if (fileNumMap != null)
{
fileNum1 = fileNumMap.remove(dmoMeta.getImplementationClass());
}
ConcurrentMap<String, Integer> fileByNameMap = FILE_NUM_BY_TBL_NAME.get(primaryDb);
if (fileByNameMap != null)
{
fileNum2 = fileByNameMap.remove(dmoMeta.sqlTable);
}
if (fileNum2 == null)
{
return; // cannot advance
}
// remove from _File meta table
Helper fileHelper = getHelper(SystemTable._File.shortName);
if (fileHelper == null)
{
return;
}
Database metaDB = new Database(primaryDb.getName(), Database.Type.META, true);
Persistence p = PersistenceFactory.getInstance(metaDB);
Session session = p.getSession(Persistence.META_CTX);
Connection conn = null;
boolean inTx = false;
// this is the 'orthodox' method which is rather resource intensive
// Record file = p.getSession().get(fileHelper.dmoImpl, (long) fileNum2);
// p.getSession().delete(file);
// do the same for fields, indices and index components
try
{
conn = session.getConnection();
inTx = session.beginTransaction();
// extract the list of indices of this table ...
PreparedStatement stmt = conn.prepareStatement(
"select " + Session.PK + " from meta_index where file_recid=?");
stmt.setLong(1, (long) fileNum2);
ResultSet rs = stmt.executeQuery();
while (rs.next())
{
// ... and drop their index components
deleteRecord(
conn, rs.getInt(1), "delete from meta_index_field where index_recid=?", "index components");
}
rs.close();
stmt.close();
// then, delete them from _Index
deleteRecord(conn, (long) fileNum2, "delete from meta_index where file_recid=?", "indices");
// next, delete from _Field
deleteRecord(conn, (long) fileNum2, "delete from meta_field where file_recid=?", "fields");
// finally, delete from _File
deleteRecord(conn, (long) fileNum2, "delete from meta_file where " + Session.PK + "=?", "tables");
if (inTx)
{
session.commit();
}
if (LOG_INFO)
{
log.log(Level.INFO,
"Deleted meta information on table " + dmoMeta.legacyTable + " (" + fileNum2 + ").");
}
}
catch (PersistenceException | SQLException e)
{
if (inTx)
{
try
{
session.rollback();
}
catch (PersistenceException pe)
{
log.log(Level.SEVERE, "Failed to rollback changes in " + conn, pe);
}
}
if (LOG_WARNING)
{
log.log(Level.WARNING,
"Failed to clean meta information on dropped table '" + dmoMeta.legacyTable + "'.",
e);
}
}
}
/**
* Executes a specific delete statement and optionally log the result using the logger's configuration.
*
* @param conn
* The {@code Connection} to database.
* @param sql
* The SQL statement to be executed.
* @param p
* The unique long parameter which identifies the records to be deleted.
* @param what
* Used only when logging is enabled, additional information on what is deleted.
*
* @throws SQLException
* If the statement failed to be executed.
*/
private static void deleteRecord(Connection conn, long p, String sql, String what)
throws SQLException
{
PreparedStatement stmt = conn.prepareStatement(sql);
stmt.setLong(1, p);
if (LOG_INFO)
{
log.log(Level.INFO, "Deleted " + stmt.executeUpdate() + " " + what + ".");
}
else
{
stmt.executeUpdate();
}
stmt.close();
}
/**
* Populates the {@code _File} meta table with initial values.
*
* @param database
* The database structure whose features need to be added to {@code _Database-Feature}
* table.
* @param p
* The {@code Persistence} that manages the database.
*
*/
private static void populateFileTable(Database database, Persistence p)
{
Helper fileHelper = getHelper(SystemTable._File.shortName);
if (fileHelper == null)
{
return;
}
helpers.values().forEach(h -> addMetaTableToFile(database, fileHelper, h.dmoIface, h.dmoImpl, p));
Database master = new Database(database.getName());
DatabaseManager.getDatabaseDMOs(master).forEach(n -> addTableToFile(master, database, n, fileHelper, p));
}
/**
* Add record for a table from the primary database to the _File table.
*
* @param primary
* primary database
* @param meta
* meta database
* @param dmoName
* table DMO interface
* @param fileHelper
* _File table helper
* @param p
* Persistence for the meta database
*/
private static void addTableToFile(Database primary,
Database meta,
String dmoName,
Helper fileHelper,
Persistence p)
{
boolean isDynamic = false;
DmoMeta dmoMeta;
Class<? extends DataModelObject> dmoIface;
try
{
dmoIface = (Class<? extends DataModelObject>) Class.forName(dmoName);
dmoMeta = DmoMetadataManager.registerDmo(dmoIface);
}
catch (ClassNotFoundException | NullPointerException | IllegalArgumentException e)
{
try
{
// 2nd chance: in case of the dynamic DMO interfaces (which were loaded with AsmClassLoader)
// then the default classloader won't recognize them, but they are already in memory and the
// DmoMetadataManager will provide not only the interface but the DMO info, as well
dmoMeta = DmoMetadataManager.getDmoInfo(dmoName);
dmoIface = dmoMeta.getAnnotatedInterface();
isDynamic = true;
}
catch (Exception ex)
{
if (log.isLoggable(Level.FINE))
{
log.log(Level.FINE, "Failed to load " + dmoName, e);
}
else if (LOG_WARNING)
{
log.log(Level.WARNING,
"Failed to load " + dmoName + " (set debug level to FINE for stacktrace)");
}
return;
}
}
registerWordTables(dmoMeta);
Class<? extends Record> dmoImpl = dmoMeta.getImplementationClass();
// be sure the _User meta table is handled appropriately
if (addMetaTableToFile(meta, fileHelper, dmoIface, dmoImpl, p))
{
return; // if it was processed stop here
}
long fileNum = nextId(meta, SystemTable._File.fileName);
integer pk = new integer();
Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> children =
childData(meta, dmoIface, (int)fileNum, pk);
setMetaTableRecord(p, fileHelper,
new FileTableRecordBuilder()
.put(Session.PK, fileNum)
.put("fileNumber", integer.of(fileNum))
.put("template", integer.of(-fileNum))
.put("tblType", character.of("T"))
.build(dmoIface, pk, children)
);
templateRecords.put(dmoImpl, -fileNum);
FILE_NUM.computeIfAbsent(primary, d -> new ConcurrentHashMap<>()).
putIfAbsent(dmoImpl, new integer(fileNum));
FILE_NUM_BY_TBL_NAME.computeIfAbsent(primary, d -> new ConcurrentHashMap<>()).
putIfAbsent(dmoIface.getAnnotation(Table.class).name(), (int)fileNum);
saveChildren(p, children);
if ((isDynamic && LOG_INFO) || log.isLoggable(Level.FINE))
{
log.log(Level.INFO,
"Created meta information on table " + dmoMeta.legacyTable + " (" + fileNum + ").");
}
}
/**
* Register word tables for a master table.
*
* @param dmoMeta
* DmoMeta for master table
*/
private static void registerWordTables( DmoMeta dmoMeta)
{
Map<String, WordIndexData> wt = dmoMeta.getWordTables();
if (!wt.isEmpty())
{
WORD_TABLES.computeIfAbsent(dmoMeta.schema, db -> new HashMap<>())
.put(dmoMeta.sqlTable, wt);
}
}
/**
* Add record for a table from the meta database to the _File table.
*
* @param metaDb
* meta database
* @param fileHelper
* _File table helper
* @param dmoIface
* table DMO interface
* @param dmoImpl
* table DMO
* @param p
* Persistence for the meta database.
*
* @return <code>true</code> if successful
*/
private static boolean addMetaTableToFile(Database metaDb,
Helper fileHelper,
Class<? extends DataModelObject> dmoIface,
Class<? extends Record> dmoImpl,
Persistence p)
{
Table tableAnn = dmoIface.getAnnotation(Table.class);
if (tableAnn == null ||
!tableAnn.isHidden() ||
dmoIface.getSimpleName().equals("MetaUser") ||
dmoIface.getSimpleName().equals("MetaSecAuthenticationDomain") ||
dmoIface.getSimpleName().equals("MetaSecAuthenticationSystem"))
{
// not a hidden (meta) table
return false;
}
String ifaceName = dmoIface.getSimpleName();
SystemTable systemTable = SystemTable.forIface(ifaceName);
if (systemTable == null)
{
// not a system table
return false;
}
integer pk = new integer();
Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> children = childData(
metaDb, dmoIface, systemTable.fileNum, pk);
long id = nextId(metaDb, SystemTable._File.fileName);
setMetaTableRecord(p, fileHelper,
new FileTableRecordBuilder()
.put(Session.PK, id)
.put("fileNumber", new integer(systemTable.fileNum))
.put("template", new integer(-id))
.put("tblType", new character("S"))
.build(dmoIface, pk, children)
);
saveChildren(p, children);
if (systemTable.isRegular())
{
templateRecords.put(dmoImpl, -id);
}
return true;
}
/**
* Save child metadata (_Field, _Index, _Index-Field, ... ) of the table.
*
* @param p
* Persistence for the meta database
* @param children
* child metadata
*/
private static void saveChildren(Persistence p,
Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> children)
{
children.forEach((key, value) ->
{
Helper h = getHelper(key.shortName);
if (h == null)
{
return;
}
value.forEach(m -> setMetaTableRecord(p, h, m));
});
}
/**
* Populates the {@code _Sequence} meta table with initial values. These are specific
* to FWD implementation, specific to backing database server or may be set manually in a
* configuration file.
*
* @param metaDb
* The database structure whose features need to be added to {@code _Database-Feature}
* table.
* @param p
* The {@code Persistence} that manages the database.
*/
private static void populateSequence(Database metaDb, Persistence p)
{
Helper seqHelper = getHelper(SystemTable._Sequence.shortName);
if (seqHelper == null)
{
return;
}
String seqClass = DmoMetadataManager.getDmoBasePackage() + "." + metaDb.getName() + "._Sequences";
Class<?> seqEnum = null;
try
{
seqEnum = Class.forName(seqClass);
}
catch (ClassNotFoundException e)
{
if (LOG_INFO)
{
log.log(Level.INFO, "No sequences were detected for " + metaDb.getName() + " database.");
}
return;
}
if (!Enum.class.isAssignableFrom(seqEnum))
{
if (LOG_WARNING)
{
log.log(Level.WARNING, "Invalid _Sequences class for " + metaDb.getName() + " database.");
}
return;
}
Method m;
try
{
m = seqEnum.getDeclaredMethod("values");
Object[] seqs = (Object[])Utils.invoke(m, null, new Object[0]);
for(Object o: seqs )
{
Field fs = seqEnum.getDeclaredField(o.toString());
Sequence seq = fs.getAnnotation(Sequence.class);
if (seq == null)
{
// TODO: log warning
continue;
}
long nextId = nextId(metaDb, SystemTable._Sequence.fileName);
setMetaTableRecord(p, seqHelper,
new MapBuilder<>().
put(Session.PK, nextId).
put("seqName", new character(seq.legacy())).
put("seqInit", new int64(seq.start())).
put("seqIncr", new int64(seq.increment())).
put("seqMin", new int64(seq.minValue())).
put("seqMax",new int64(seq.maxValue())).
put("cycleOk",new logical(seq.cycle())).
put("dbRecid",new recid(nextId)).
build()
);
}
} catch (NoSuchMethodException | SecurityException |
InvocationTargetException | IllegalAccessException | NoSuchFieldException e)
{
if (LOG_WARNING)
{
log.log(Level.WARNING, "Failed to populate _Sequence table", e);
}
}
}
/**
* Populates the {@code _Database-Feature} meta table with initial values. These are specific
* to FWD implementation, specific to backing database server or may be set manually in a
* configuration file.
*
* @param database
* The database structure whose features need to be added to {@code _Database-Feature}
* table.
* @param p
* The {@code Persistence} that manages the database.
*/
private static void populateDatabaseFeatures(Database database, Persistence p)
{
Helper h = getHelper("DatabaseFeature");
if (h == null)
{
return; // _DatabaseFeature metatable is not converted/active
}
setDatabaseFeature(p, h, 1, "OpenEdge Replication", false, false);
setDatabaseFeature(p, h, 2, "Failover Clusters", false, false);
setDatabaseFeature(p, h, 3, "DataXtend Remote Edition", false, false);
setDatabaseFeature(p, h, 4, "Reserved", false, false);
setDatabaseFeature(p, h, 5, "Large Files", false, false);
setDatabaseFeature(p, h, 6, "Database Auditing", false, false);
setDatabaseFeature(p, h, 7, "JTA", false, false);
setDatabaseFeature(p, h, 8, "After Image Mangement/Archiver", false, false);
setDatabaseFeature(p, h, 9, "64 Bit DBKEYS", true, true);
// database is a META database, we need information about its MASTER
Dialect dialect = DatabaseManager.getDialect(new Database(database.getName(), Database.Type.PRIMARY));
int indexLimit = dialect.getIndexLengthLimit();
setDatabaseFeature(p, h, 10, "Large Keys", indexLimit == 0 || indexLimit > 200, true);
setDatabaseFeature(p, h, 11, "64 Bit Sequences", true, true);
setDatabaseFeature(p, h, 12, "DataXtend Integration Edition", false, false);
setDatabaseFeature(p, h, 13, "Encryption", false, false);
setDatabaseFeature(p, h, 14, "Multi-tenancy", false, false);
setDatabaseFeature(p, h, 15, "Concurrent JTA and Replication", false, false);
setDatabaseFeature(p, h, 16, "Reserved", false, false);
setDatabaseFeature(p, h, 17, "MT Index Rebuild", false, false);
setDatabaseFeature(p, h, 18, "MT Data Move", false, false);
setDatabaseFeature(p, h, 19, "Roll Forward Restricted Mode", false, false);
setDatabaseFeature(p, h, 20, "TP Index Rebuild", false, false);
setDatabaseFeature(p, h, 21, "Table Partitioning", false, false);
setDatabaseFeature(p, h, 22, "Read-only Partitions", false, false);
setDatabaseFeature(p, h, 23, "New VST Tables", true, true);
setDatabaseFeature(p, h, 24, "Reserved", false, false);
setDatabaseFeature(p, h, 25, "Partition Copy", false, false);
setDatabaseFeature(p, h, 26, "Authentication Gateway", false, false);
// TODO: to be completed with missing values.
// to be updated with correct values
}
/**
* Creates and saves a record in {@code MetaDb} table with the given recId.
*
* @param database
* The database to be processed.
* @param p
* The persistence context to be used when saving.
*/
private static void createDbMetaRecord(Database database, Persistence p)
{
Helper h = getHelper(_DB);
if (h == null)
{
return;
}
setMetaTableRecord(p, h,
new MapBuilder<>()
.put(Session.PK, (long) DB_RECID)
.put("dbName", new character("_meta"))
.put("dbType", new character(""))
.put("dbComm", new character(""))
.put("dbSlave", new logical(false))
.put("dbLocal", new logical(false))
.build()
);
}
/**
* Populates the {@code _Area} meta table with initial values.
* This is a "mock" since _Area data has no meaningful counterparts in FWD
*
* @param database
* The database structure whose features need to be added to {@code _Area}
* table.
* @param p
* The {@code Persistence} that manages the database.
*/
private static void populateArea(Database database, Persistence p)
{
Helper h = getHelper("Area");
if (h == null)
{
return; // _Area metatable is not converted/active
}
setArea(p, h, 1, 1, "Control Area", 6, 0, 0, 1, 4096, 5, 1);
setArea(p, h, 1, 3, "Primary Recovery Area", 3, 0, 0, 1, 8192, 0, 1);
setArea(p, h, 1, 6, "Schema Area", 6, 0, 0, 1, 4096, 5, 1);
setArea(p, h, 7, 7, "Data", 6, 0, 0, 64, 4096, 6, 1);
setArea(p, h, 7, 8, "Index", 6, 0, 0, 8, 4096, 5, 1);
setArea(p, h, 1, 9, "After Image Area 1", 7, 0, 0, 1, 8192, 0, 1);
}
/**
* Populates the {@code _Filelist} meta table with initial values.
* This is a "mock" since _Filelist data has no meaningful counterparts in FWD
*
* @param database
* The database structure whose features need to be added to {@code _Area}
* table.
* @param p
* The {@code Persistence} that manages the database.
*/
private static void populateFilelist(Database database, Persistence p)
{
Helper h = getHelper("Filelist");
if (h == null)
{
return; // _Filelist metatable is not converted/active
}
addFilelist(p, h, 1, 32, 256, 6, 4096, "BOTHIO", ".4filelist.db");
addFilelist(p, h, 2, 2176, 512, 272, 8192, "UNBUFIO", ".4filelist.b1");
addFilelist(p, h, 3, 146944, 256, 36704, 4096, "BOTHIO", ".4filelist.d1");
}
/**
* Populates the {@code _Startup} meta table with an initial record.
*
* @param database
* The database.
* @param p
* The {@code Persistence} that manages the database.
*/
private static void populateStartupTable(Database database, Persistence p)
{
Helper h = getHelper("Startup");
if (h == null)
{
return; // _Startup metatable is not converted/active
}
setMetaTableRecord(p, h,
new MapBuilder<>()
.put(Session.PK, 1L)
.put("startupLockTable", new integer("8192"))
.put("startupMaxUsers", new integer("64"))
.build()
);
}
/**
* Populates the {@code _Trans} meta table with an initial record.
*
* @param database
* The database.
* @param p
* The {@code Persistence} that manages the database.
*/
private static void populateTransTable(Database database, Persistence p)
{
Helper h = getHelper(_TRANS);
if (h == null)
{
return; // _Startup metatable is not converted/active
}
setMetaTableRecord(p, h, new MapBuilder<>().put(Session.PK, 1L).build());
}
/**
* Populates the {@code _Tenant} meta table with initial values.
* This is a "mock" since _Tenant data has no meaningful counterparts in FWD
*
* @param database
* The database structure whose features need to be added to {@code _Area}
* table.
* @param p
* The {@code Persistence} that manages the database.
*/
private static void populateTenant(Database database, Persistence p)
{
Helper h = getHelper("Tenant");
if (h == null)
{
return; // _Tenant metatable is not converted/active
}
try {
// set the Default tenant for all databases is always available
long tenIdPk = 0;
setMetaTableRecord(p, h,
new MapBuilder<>()
.put(Session.PK, tenIdPk)
.put("tenantId", new integer(TenantManager.DEFAULT_TENANT_ID))
.put("tenantName", new character(TenantManager.DEFAULT_TENANT_NAME))
.put("tenantExtId", new character(""))
.put("tenantDescription", new character("Default Tenant"))
.put("tenantAttributes", 0, logical.TRUE)
.put("tenantType", new integer(TenantManager.TYPE_DEFAULT))
.put("tenantSequenceBlock", new recid(0))
.put("tenantDataAreaDefault", new integer(0))
.put("tenantIndexAreaDefault", new integer(0))
.put("tenantLobAreaDefault", new integer(0))
.put("tenantAllocationDefault", new character("None"))
.build());
ArrayList<TenantManager.Tenant> tenants = TenantManager.listTenants(new ArrayList<>());
if (tenants == null)
{
return; // no other tenant defined locally
}
for (int i = 0; i < tenants.size(); i++)
{
TenantManager.Tenant tenant = tenants.get(i);
if (tenant.id() == TenantManager.DEFAULT_TENANT_ID)
{
continue; // already saved above
}
for (Map.Entry<String, TenantDatabaseDescriptor> it : tenant.getDatabaseCredentials().entrySet())
{
if (it.getValue().getLdbName().equalsIgnoreCase(database.getName()))
{
++tenIdPk;
setMetaTableRecord(p, h,
new MapBuilder<>()
.put(Session.PK, tenIdPk)
.put("tenantId", new integer(tenant.id()))
.put("tenantName", new character(tenant.getTenantName()))
.put("tenantExtId", new character(tenant.getId().toString())) /*UUID*/
.put("tenantDescription", new character(tenant.getTenantDescription()))
.put("tenantAttributes", 0, logical.of(tenant.getStatus()))
.put("tenantType", new integer(tenant.getType()))
.put("tenantSequenceBlock", new recid(tenIdPk + 0x1000_0000))
.put("tenantDataAreaDefault", new integer(tenIdPk * 100 + 0))
.put("tenantIndexAreaDefault", new integer(tenIdPk * 100 + 1))
.put("tenantLobAreaDefault", new integer(tenIdPk * 100 + 2))
.put("tenantAllocationDefault", new character("Immediate"))
.build());
}
}
}
}
finally
{
TenantManager.registerMetaListener(new TenantTableUpdater(p, database));
}
}
/**
* Sets a single database feature. Only the id, name and active/enabled status are set, the
* reserved fields are ignored.
*
* @param p
* Persistence instance for the table
* @param h
* The helper to be used.
* @param featureId
* The id of the feature to be set.
* @param featureName
* The name of the feature to be set.
* @param isActive
* Whether the feature is active at startup.
* @param isEnabled
* Whether the feature is enabled at startup.
*
*/
private static void setDatabaseFeature(Persistence p,
Helper h,
int featureId,
String featureName,
boolean isActive,
boolean isEnabled)
{
setMetaTableRecord(p, h,
new MapBuilder<>().
put(Session.PK, (long) featureId).
put("dbfeatureId", new int64(featureId)).
put("dbfeatureName", new character(featureName)).
put("dbfeatureActive", new character(isActive ? "1" : "0")).
put("dbfeatureEnabled",new character(isEnabled ? "1" : "0")).
build()
);
}
/**
* Sets a single _Area metatable data. _Area-misc field is not populated
*
* @param p
* Persistence instance for the table
* @param h
* The helper to be used.
* @param areaVersion
* area version
* @param areaNumber
* area number
* @param areaName
* area name
* @param areaType
* area type
* @param areaBlock
* area block
* @param areaAttrib
* area attributes
* @param areaClustersize
* area cluster size
* @param areaBlocksize
* area block size
* @param areaRecbits
* area recbits (rows per block = 2 ^ recbits)
* @param areaExtents
* area extents
*/
private static void setArea(Persistence p,
Helper h,
int areaVersion,
int areaNumber,
String areaName,
int areaType,
int areaBlock,
int areaAttrib,
int areaClustersize,
int areaBlocksize,
int areaRecbits,
int areaExtents)
{
setMetaTableRecord(p, h,
new MapBuilder<>().
put(Session.PK, (long) areaNumber).
put("areaVersion", new int64(areaVersion)).
put("areaNumber", new int64(areaNumber)).
put("areaName", new character(areaName)).
put("areaType", new int64(areaType)).
put("areaBlock", new int64(areaBlock)).
put("areaAttrib", new int64(areaAttrib)).
put("areaClustersize", new int64(areaClustersize)).
put("areaBlocksize", new int64(areaBlocksize)).
put("areaRecbits", new int64(areaRecbits)).
put("areaExtents", new int64(areaExtents)).
build()
);
}
/**
* Sets a single _Filelist metatable data. _FileList-Misc field is not populated
*
* @param p
* Persistence instance for the table
* @param h
* The helper to be used.
* @param fileListId
* fileList id
* @param fileListSize
* fileList size
* @param fileListExtend
* fileList extend
* @param fileListLogicalSz
* fileList logical size
* @param fileListBlkSize
* fileList block size
* @param fileListOpenmode
* fileList open mode
* @param fileListName
* fileList name
*/
private static void addFilelist(Persistence p,
Helper h,
int fileListId,
int fileListSize,
int fileListExtend,
int fileListLogicalSz,
int fileListBlkSize,
String fileListOpenmode,
String fileListName)
{
setMetaTableRecord(p, h,
new MapBuilder<>().
put(Session.PK, (long) fileListId).
put("fileListId", new int64(fileListId)).
put("fileListSize", new int64(fileListSize)).
put("fileListExtend", new int64(fileListExtend)).
put("fileListLogicalSz", new int64(fileListLogicalSz)).
put("fileListBlkSize", new int64(fileListBlkSize)).
put("fileListOpenmode", new character(fileListOpenmode)).
put("fileListName", new character(fileListName)).
build()
);
}
/**
* Created a new meta-table record using the helper constructor, populates it with the values passed in the
* map and save it to (meta) database.
*
* @param p
* Persistence instance for the table
* @param h
* The helper to be used.
* @param values
* A map containing the values for specific properties. A property is specified by a {@code Tuple}
* of ({@code property} / {@code index}). The {@code index} should be {@code null} for scalar
* fields.
*
* @throws IllegalAccessException
* @throws InstantiationException
* @throws InvocationTargetException
* @throws IllegalArgumentException
* @throws PersistenceException
*/
private static void setMetaTableRecord(Persistence p, Helper h, Map<Tuple<String, Integer>, Object> values)
{
try
{
Record dmo = h.dmoImpl.getDeclaredConstructor().newInstance();
dmo.initialize(null, true);
for (Map.Entry<Tuple<String, Integer>, Object> e: values.entrySet())
{
Tuple<String, Integer> prop = e.getKey();
h.setValue(dmo, prop.getKey(), prop.getValue(), e.getValue());
}
p.save(dmo, dmo.primaryKey());
}
catch (ReflectiveOperationException | PersistenceException e)
{
throw new RuntimeException("Failed to set record for " + h.dmoIface.getName(), e);
}
}
/**
* Return the rowid of the template record for a permanent table.
*
* @param table
* The permanent table to lookup.
*
* @return The value of the {@link rowid} of the template record. If metadata is not enabled
* for this project or {@code _file} metatable is not configured or {@code table} is
* an invalid name {@code null} is returned.
*/
public static Long getTemplateRowid(Class<?> table)
{
return MetadataManager.inUse() ? templateRecords.get(table) : null;
}
/**
* Gets the metadata helper for a specified table. In case such helper is not found, the event is logged
* with WARNING level and {@code null} is returned.
*
* @param type
* The name of meta file.
*
* @return The helper corresponding to {@code type}, or {@code null} if no support such table was set up.
*/
private static Helper getHelper(String type)
{
Helper h = helpers.get(type);
if (h == null)
{
if (LOG_WARNING)
{
log.log(Level.WARNING,
"Could not obtain the helper class for calling DMO setter methods. _" +
type + " metadata not available.");
}
}
return h;
}
/**
* Execute a script containing DDL SQL statements.
*
* @param database
* Target database.
* @param connection
* Connection to target database.
* @param script
* Short name of script file, which is expected to be found in the application jar
* file. The following path is implied:
* <pre>
* {base DMO package}._meta.{script name}
* </pre>
*
* @throws PersistenceException
* if there is any error connecting to the target database or executing the DDL
* statements.
*/
private static void executeDDLScript(Database database, Connection connection, String script)
throws PersistenceException
{
String path = String.format("%s/%s/%s", baseDir, META_SCHEMA, script);
BufferedReader br = null;
try
{
InputStream in = ClassLoader.getSystemResourceAsStream(path);
if (in == null)
{
throw new MissingDataException("Could not load DDL script " + path);
}
br = new BufferedReader(new InputStreamReader(in));
H2Helper.runScript(connection, br);
}
catch (Exception exc)
{
throw new PersistenceException("Error executing DDL for " + database, exc);
}
finally
{
if (br != null)
{
try
{
br.close();
}
catch (IOException exc)
{
}
}
}
}
/**
* Calculate short DMO interface name for a table.
*
* @param tableName
* The table name.
*
* @return short DMO interface name
*/
private static String getShortName(String tableName)
{
StringBuilder sb = new StringBuilder();
int k = tableName.indexOf("-");
int rest = 1; // drop the first _ char at 0
while (k != -1)
{
sb.append(Character.toUpperCase(tableName.charAt(rest)));
sb.append(tableName, rest + 1, k);
rest = k + 1;
k = tableName.indexOf("-", k + 1);
}
sb.append(Character.toUpperCase(tableName.charAt(rest)));
sb.append(tableName.substring(rest + 1));
return sb.toString();
}
/**
* Helper class for calling DMO setter methods when populating a metaschema record.
*/
private static class Helper
{
/** Well-known name of primary key setter method */
private static final String SET_ID = "primaryKey";
/** Primary key setter method */
private static final Method idSetter;
/** DMO implementation class */
private final Class<? extends Record> dmoImpl;
/** DMO interface class */
private final Class<? extends DataModelObject> dmoIface;
/** Map of DMO property setters (excluding primary key setter) */
private final Map<String, Method> setters;
static
{
try
{
idSetter = BaseRecord.class.getDeclaredMethod(SET_ID, Long.class);
}
catch (NoSuchMethodException | SecurityException e)
{
throw new RuntimeException(e);
}
}
/**
* Constructor.
*
* @param dmoIface
* DMO interface.
* @param dmoImpl
* DMO implementation class.
*/
Helper(Class<? extends DataModelObject> dmoIface, Class<? extends Record> dmoImpl)
{
this.dmoImpl = dmoImpl;
this.dmoIface = dmoIface;
this.setters = PropertyHelper.pojoSettersByProperty(dmoIface);
}
/**
* Set a property value into a DMO instance.
*
* @param dmo
* Target DMO instance.
* @param property
* Name of property to be set.
* @param index
* Optional string representation of index (for extent fields only). Should be the
* {@code null} if property does not represent an extent field.
* @param value
* Value of the appropriate data type (i.e., data wrapper instance) for the target
* property.
*
* @throws PersistenceException
* if <code>index</code> cannot be parsed into an <code>Integer</code>.
* @throws IllegalAccessException
* if the target setter method cannot be accessed.
* @throws IllegalArgumentException
* if <code>value</code> is the wrong type.
* @throws InvocationTargetException
* if the underlying setter method throws an exception.
*/
void setValue(Object dmo, String property, Integer index, Object value)
throws PersistenceException,
IllegalAccessException,
IllegalArgumentException,
InvocationTargetException
{
Method m = getSetter(property);
if (index == null)
{
Utils.invoke(m, dmo, value);
}
else
{
Utils.invoke(m, dmo, index, value);
}
}
/**
* Retrieve the DMO property setter method for the specified property name.
*
* @param property
* Name of a DMO property (case-sensitive).
*
* @return Matching setter method.
*
* @throws PersistenceException
* if a setter method cannot be found for the specified property.
*/
private Method getSetter(String property)
throws PersistenceException
{
Method m = setters.get(property);
if (m == null && Session.PK.equals(property))
{
m = idSetter;
}
if (m == null)
{
String msg = String.format("No setter method found for %s#%s", dmoIface.getName(), property);
throw new PersistenceException(msg);
}
return m;
}
}
/**
* Return record id for table in the meta database.
*
* @param metaDb
* meta database
* @param table
* meta table
* @return record id for table in the meta database
*/
private static long nextId(Database metaDb, String table)
{
return TABLE_IDS.computeIfAbsent(metaDb, db -> new ConcurrentHashMap<>()).
computeIfAbsent(table.toLowerCase(), t -> new AtomicLong()).incrementAndGet();
}
/**
* Collect child metadata for a record in the _File meta table.
*
* @param metaDb
* meta database
* @param dmoIface
* table DMO interface
* @param _file_num
* _File._file_num field value
* @param pk
* table primary key id in the _Index table holder
*
* @return child metadata for a record in the _File meta table.
*/
private static Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> childData(
Database metaDb,
Class<? extends DataModelObject> dmoIface,
int _file_num,
integer pk)
{
Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> children = new EnumMap<>(SystemTable.class);
List<Map<Tuple<String, Integer>, Object>> fields = new LinkedList<>();
List<Map<Tuple<String, Integer>, Object>> indexes = new LinkedList<>();
List<Map<Tuple<String, Integer>, Object>> indexFields = new LinkedList<>();
List<Map<Tuple<String, Integer>, Object>> fileTriggers = new LinkedList<>();
List<Map<Tuple<String, Integer>, Object>> fieldTriggers = new LinkedList<>();
Map<String, integer> fieldRecidByName = new LinkedHashMap<>();
Map<String, integer> fieldRposByName = new LinkedHashMap<>();
children.put(SystemTable._Field, fields);
children.put(SystemTable._Index, indexes);
children.put(SystemTable._Index_Field, indexFields);
children.put(SystemTable._File_Trig, fileTriggers);
children.put(SystemTable._Field_Trig, fieldTriggers);
DmoMeta meta = DmoMetadataManager.getDmoInfo(dmoIface);
Iterator<Property> properties = meta.getFields(false);
while (properties.hasNext())
{
Property prop = properties.next();
Class<?> ftype = prop._fwdType;
if (prop.index > 1)
{
continue; // skip duplicates caused by custom extents
}
fields.add(fieldData(metaDb, _file_num, prop, ftype, fieldRecidByName, fieldRposByName));
}
Iterator<P2JIndex> indices = meta.getDatabaseIndexes();
indexData(metaDb, dmoIface.getSimpleName(), _file_num, indices, pk, indexes, indexFields, fieldRecidByName);
Triggers legacyTriggers = dmoIface.getAnnotation(Triggers.class);
Trigger[] triggers = legacyTriggers == null ? new Trigger[0] : legacyTriggers.value();
for (Trigger trigger: triggers)
{
String field = trigger.field();
if (!field.isEmpty())
{
integer fieldRecId = fieldRecidByName.get(field);
integer fieldRpos = fieldRposByName.get(field);
if (fieldRecId == null || fieldRpos == null)
{
if (LOG_WARNING)
{
log.log(Level.WARNING,
"Unknown field [" + field + "] in the trigger definition for [" +
dmoIface.getName() + "]");
}
continue;
}
fieldTriggers.add(
new MapBuilder<>().
put(Session.PK, nextId(metaDb, SystemTable._Field_Trig.fileName)).
put("fieldRecid", fieldRecId).
put("fileRecid", integer.of(_file_num)).
put("fieldRpos", fieldRpos).
put("event", new character(trigger.event())).
put("procName", new character(trigger.procedure())).
put("override", logical.of(trigger.overridable())).
build()
);
}
else
{
fileTriggers.add(
new MapBuilder<>().
put(Session.PK, nextId(metaDb, SystemTable._File_Trig.fileName)).
put("fileRecid", integer.of(_file_num)).
put("event", new character(trigger.event())).
put("procName", new character(trigger.procedure())).
put("override", logical.of(trigger.overridable())).
build()
);
}
}
return children;
}
/**
* Collect index metadata for a record in the _File meta table.
* @param metaDb
* meta database
* @param tblDmoName
* table DMO
* @param _file_num
* _File._file_num field value
* @param indices
* P2JIndex iterator
* @param pk
* table primary key id in the _Index table holder
* @param indexes
* table indexes metadata
* @param indexFields
* table index fields metadata
* @param fieldRecidByName
* Map of property names to their recid.
*/
private static void indexData(Database metaDb,
String tblDmoName,
int _file_num,
Iterator<P2JIndex> indices,
integer pk,
List<Map<Tuple<String, Integer>, Object>> indexes,
List<Map<Tuple<String, Integer>, Object>> indexFields,
Map<String, integer> fieldRecidByName)
{
while (indices.hasNext())
{
P2JIndex idx = indices.next();
long id = nextId(metaDb, SystemTable._Index.fileName);
if (idx.isPrimary())
{
pk.assign(integer.of(id));
}
indexes.add(new MapBuilder<>().
put(Session.PK, id).
put("fileRecid", integer.of(_file_num)).
put("indexName", new character(idx.getName())).
put("unique", logical.of(idx.isUnique())).
build()
);
int seqNo = 1;
ArrayList<P2JIndexComponent> comps = idx.components();
for (int i = 0; i < comps.size(); i++)
{
P2JIndexComponent fld = comps.get(i);
String compName = fld.getPropertyName();
integer fieldRecid = fieldRecidByName.get(compName);
if (fieldRecid == null)
{
log.warning("Unknown index field [" + compName + "] for table [" + tblDmoName + "]");
continue;
}
indexFields.add(new MapBuilder<>().
put(Session.PK, nextId(metaDb, SystemTable._Index_Field.fileName)).
put("indexRecid", integer.of(id)).
put("fieldRecid", fieldRecidByName.getOrDefault(compName, integer.UNKNOWN)).
put("indexSeq", integer.of(seqNo++)).
build()
);
}
}
}
/**
* Collect _Field table data for a DMO field.
*
* @param metaDb
* meta database
* @param _file_num
* _File._file_num field value
* @param property
* {@code Property} annotation of the field.
* @param fieldRecidByName
* Fields' record id by property name map.
* @param fieldRposByName
* Fields' (r)pos id by property name map.
*
* @return _Field table data for a DMO field.
*/
private static Map<Tuple<String, Integer>, Object> fieldData(Database metaDb,
int _file_num,
Property property,
Class<?> ftype,
Map<String, integer> fieldRecidByName,
Map<String, integer> fieldRposByName)
{
long id = nextId(metaDb, SystemTable._Field.fileName);
fieldRecidByName.put(property.name, new integer(id));
fieldRposByName.put(property.name, integer.of(property.order));
int wd = property.width;
integer width = wd >= 0 ? integer.of(wd) : integer.UNKNOWN;
boolean caseSensitive = property.caseSensitive;
String typeName = ftype == datetimetz.class ? "datetime-tz" : ftype.getSimpleName();
boolean charField = CHAR_TYPES.contains(typeName);
String charset = charField ? "iso8859-1" : null;
String collation = charField ? (caseSensitive ? "basic_S" : "basic_I") : null;
return new MapBuilder<>().
put(Session.PK, id).
put("fileRecid", integer.of(_file_num)).
put("fieldName", new character(property.legacy)).
put("dataType", character.of(typeName)).
put("mandatory", logical.of(property.mandatory)).
put("label", new character(property.label)).
put("labelSa", new character(property.label_sa)).
put("colLabel", new character(property.columnLabel)).
put("colLabelSa", new character(property.columnLabel_sa)).
put("format", character.of(property.format)).
put("formatSa", character.of(property.format_sa)).
put("fieldRpos", integer.of(property.position)).
put("fieldPhyspos", integer.of(property.order)). // TODO: order/position?
put("order", integer.of(property.order)).
put("help", new character(property.help)).
put("helpSa", new character(property.help_sa)).
put("valexp", new character(property.validateExpression)).
put("valmsg", new character(property.validateMessage)).
put("valmsgSa", new character(property.valmsg_sa)).
put("initial", property.initialNull ? character.UNKNOWN : new character(property.initial)).
put("initialSa", new character(property.initial_sa)).
put("canRead", new character(property.canRead)).
put("canWrite", new character(property.canWrite)).
put("extent", integer.of(property.extent)).
put("viewAs", character.of(property.viewAs)).
put("desc", new character(property.desc)).
put("width", integer.of(width)).
put("decimals", integer.of(property.scale)).
put("dtype", integer.of(DTYPE.get(typeName))).
put("fetchType", character.of(FETCH_TYPE.get(typeName))).
put("fldCase" , logical.of(caseSensitive)).
put("sysField" , logical.of(false)).
put("charset", character.of(charset)).
put("collation", character.of(collation)).
build();
}
/** System table categories */
public enum Category
{
/** Virtual system table */
VST,
/** Constraints */
DS_SCHEMA,
/** Physical layout */
PHY_ST,
/** Authentication */
AUTH,
/** Logging */
LOG,
/** Audit */
AUDIT,
/** SQL statistics */
SQL_STAT,
/** SQL catalog */
SQL_CAT,
/** SQL permissions */
SQL_AUTH,
/** SQL stored procedures */
SQL_SP,
/** SQL triggers */
SQL_TR,
/** DB schema */
SCHEMA,
/** SQL views */
SQL89,
}
/** System tables' flags */
private interface TableFlags
{
/** Flag indicating that the table is a part of the main db schema, not a meta database */
int REGULAR = 1 << 0;
/** Flag indicating that the table should always present */
int MANDATORY = 1 << 1;
/** Flag indicating that the table is read-only from 4GL code. */
int READONLY = 1 << 2;
}
/** System tables (as of Progress 11.6.3) */
public enum SystemTable
implements TableFlags
{
/** Provides detailed information about registered objects of a legacy database. */
_DbServiceManagerObjects(-16436, "_DbServiceManagerObjects", Category.VST),
/** Provides detailed information about the Replication plugin manager communication area. */
_DbServiceManager(-16435, "_DbServiceManager", Category.VST),
/** Provides dynamic information about active OpenEdge Replication agents. */
_Repl_AgentActivity(-16434, "_Repl-AgentActivity", Category.VST),
/** Provides dynamic information about active replication agents this replication server controls. */
_Repl_AgentControlActivity(-16433, "_Repl-AgentControlActivity", Category.VST),
/** */
_DbParams(-16432, "_DbParams", Category.VST),
/** */
_UserIndexStat(-16431, "_UserIndexStat", Category.VST),
/** */
_UserTableStat(-16430, "_UserTableStat", Category.VST, MANDATORY),
/** */
_AreaThreshold(-16429, "_AreaThreshold", Category.VST),
/** */
_Code_Feature(-16428, "_Code-Feature", Category.VST),
/** */
_Database_Feature(-16427, "_Database-Feature", Category.VST, MANDATORY,
MetadataManager::populateDatabaseFeatures),
/** Provides detailed information about a replication agent. */
_Repl_Agent(-16426, "_Repl-Agent", Category.VST),
/** Provides detailed information about replication agents this replication server controls. */
_Repl_AgentControl(-16425, "_Repl-AgentControl", Category.VST),
/** Provides detailed information about the OpenEdge Replication server. */
_Repl_Server(-16424, "_Repl-Server", Category.VST),
/** */
_AreaStatus(-16423, "_AreaStatus", Category.VST),
/** */
_MyConnection(-16422, "_MyConnection", Category.VST , MANDATORY),
/** */
_UserStatus(-16421, "_UserStatus", Category.VST),
/** */
_StatBase(-16420, "_StatBase", Category.VST),
/** */
_TxeLock(-16419, "_TxeLock", Category.VST),
/** */
_Resrc(-16418, "_Resrc", Category.VST),
/** */
_Latch(-16417, "_Latch", Category.VST),
/** */
_IndexStat(-16416, "_IndexStat", Category.VST),
/** */
_TableStat(-16415, "_TableStat", Category.VST),
/** */
_License(-16414, "_License", Category.VST),
/** */
_Trans(-16413, "_Trans", Category.VST, MetadataManager::populateTransTable),
/** */
_Lock(-16412, "_Lock", Category.VST, MANDATORY),
/** */
_Checkpoint(-16411, "_Checkpoint", Category.VST),
/** */
_LockReq(-16410, "_LockReq", Category.VST),
/** */
_UserIO(-16409, "_UserIO", Category.VST),
/** */
_Block(-16408, "_Block", Category.VST),
/** */
_ActOther(-16407, "_ActOther", Category.VST),
/** */
_ActRecord(-16406, "_ActRecord", Category.VST),
/** */
_ActIndex(-16405, "_ActIndex", Category.VST),
/** */
_ActSpace(-16404, "_ActSpace", Category.VST),
/** */
_ActIOFile(-16403, "_ActIOFile", Category.VST),
/** */
_ActIOType(-16402, "_ActIOType", Category.VST),
/** */
_ActLock(-16401, "_ActLock", Category.VST),
/** */
_ActAILog(-16400, "_ActAILog", Category.VST),
/** */
_ActBILog(-16399, "_ActBILog", Category.VST),
/** */
_ActPWs(-16398, "_ActPWs", Category.VST),
/** */
_ActBuffer(-16397, "_ActBuffer", Category.VST),
/** */
_ActServer(-16396, "_ActServer", Category.VST),
/** */
_ActSummary(-16395, "_ActSummary", Category.VST),
/** */
_UserLock(-16394, "_UserLock", Category.VST),
/** */
_Filelist(-16393, "_Filelist", Category.VST, MANDATORY, MetadataManager::populateFilelist),
/** */
_Startup(-16392, "_Startup", Category.VST, MANDATORY, MetadataManager::populateStartupTable),
/** */
_Servers(-16391, "_Servers", Category.VST),
/** */
_Segments(-16390, "_Segments", Category.VST),
/** */
_Logging(-16389, "_Logging", Category.VST),
/** */
_BuffStatus(-16388, "_BuffStatus", Category.VST),
/** */
_DbStatus(-16387, "_DbStatus", Category.VST),
/** */
_MstrBlk(-16386, "_MstrBlk", Category.VST),
/** */
_Connect(-16385, "_Connect", Category.VST, MANDATORY),
/** */
_Constraint_Keys(-361, "_Constraint-Keys", Category.DS_SCHEMA),
/** */
_Constraint(-360, "_Constraint", Category.DS_SCHEMA),
/** */
_Partition_Set_Detail(-351, "_Partition-Set-Detail", Category.PHY_ST),
/** */
_Partition_Set(-350, "_Partition-Set", Category.PHY_ST),
/** */
_Tenant(-340, "_Tenant", Category.AUTH, MANDATORY, MetadataManager::populateTenant),
/** */
_KeyEvent(-208, "_KeyEvent", Category.LOG),
/** */
_sec_authentication_domain(-207, "_sec-authentication-domain", Category.AUTH, REGULAR | READONLY),
/** */
_sec_authentication_system(-206, "_sec-authentication-system", Category.AUTH, REGULAR | READONLY),
/** */
_sec_granted_role_condition(-205, "_sec-granted-role-condition", Category.AUTH),
/** */
_sec_granted_role(-204, "_sec-granted-role", Category.AUTH),
/** */
_sec_role(-203, "_sec-role", Category.AUTH),
/** */
_Db_Option(-202, "_Db-Option", Category.AUTH),
/** */
_Db_Detail(-201, "_Db-Detail", Category.AUDIT),
/** */
_Client_Session(-200, "_Client-Session", Category.AUDIT),
/** */
_Sysautostats(-127, "_Sysautostats", Category.SQL_STAT),
/** */
_SQL_Properties(-126, "_SQL_Properties", Category.SQL_CAT),
/** */
_Sysschemas(-125, "_Sysschemas", Category.SQL_CAT),
/** */
_Sysroles(-124, "_Sysroles", Category.SQL_CAT),
/** */
_Syschkconstr_Name_Map(-123, "_Syschkconstr_Name_Map", Category.SQL_CAT),
/** */
_Sysviews_Name_Map(-122, "_Sysviews_Name_Map", Category.SQL_CAT),
/** */
_Sysseqauth(-121, "_Sysseqauth", Category.SQL_AUTH),
/** */
_Syststzstat(-120, "_Syststzstat", Category.SQL_STAT),
/** */
_Sysnvarcharstat(-119, "_Sysnvarcharstat", Category.SQL_STAT),
/** */
_Sysncharstat(-118, "_Sysncharstat", Category.SQL_STAT),
/** */
_Sysbigintstat(-117, "_Sysbigintstat", Category.SQL_STAT),
/** */
_Systinyintstat(-116, "_Systinyintstat", Category.SQL_STAT),
/** */
_Systsstat(-115, "_Systsstat", Category.SQL_STAT),
/** */
_Sysrealstat(-114, "_Sysrealstat", Category.SQL_STAT),
/** */
_Syscharstat(-113, "_Syscharstat", Category.SQL_STAT),
/** */
_Systimestat(-112, "_Systimestat", Category.SQL_STAT),
/** */
_Sysfloatstat(-110, "_Sysfloatstat", Category.SQL_STAT),
/** */
_Sysdatestat(-109, "_Sysdatestat", Category.SQL_STAT),
/** */
_Sysnumstat(-108, "_Sysnumstat", Category.SQL_STAT),
/** */
_Sysintstat(-107, "_Sysintstat", Category.SQL_STAT),
/** */
_Syssmintstat(-106, "_Syssmintstat", Category.SQL_STAT),
/** */
_Sysvarcharstat(-105, "_Sysvarcharstat", Category.SQL_STAT),
/** */
_Syscolstat(-104, "_Syscolstat", Category.SQL_STAT),
/** */
_Sysproctext(-103, "_Sysproctext", Category.SQL_SP),
/** */
_Sysprocbin(-102, "_Sysprocbin", Category.SQL_SP),
/** */
_Systrigcols(-99, "_Systrigcols", Category.SQL_TR),
/** */
_Systrigger(-98, "_Systrigger", Category.SQL_TR),
/** */
_Sysproccolumns(-97, "_Sysproccolumns", Category.SQL_SP),
/** */
_Sysprocedures(-96, "_Sysprocedures", Category.SQL_SP),
/** */
_Sysidxstat(-94, "_Sysidxstat", Category.SQL_STAT),
/** */
_Systblstat(-93, "_Systblstat", Category.SQL_STAT),
/** */
_Syscalctable(-92, "_Syscalctable", Category.SQL_CAT),
/** */
_Sysdatatypes(-91, "_Sysdatatypes", Category.SQL_CAT),
/** */
_Syschkcolusage(-90, "_Syschkcolusage", Category.SQL_CAT),
/** */
_Systblconstrs(-89, "_Systblconstrs", Category.SQL_CAT),
/** */
_Syschkconstrs(-88, "_Syschkconstrs", Category.SQL_CAT),
/** */
_Sysrefconstrs(-87, "_Sysrefconstrs", Category.SQL_CAT),
/** */
_Syskeycolusage(-86, "_Syskeycolusage", Category.SQL_CAT),
/** */
_Syssynonyms(-84, "_Syssynonyms", Category.SQL_CAT),
/** */
_Syscolauth(-83, "_Syscolauth", Category.SQL_AUTH),
/** */
_Systabauth(-82, "_Systabauth", Category.SQL_AUTH),
/** */
_Sysdbauth(-81, "_Sysdbauth", Category.SQL_AUTH),
/** */
_Sysviews(-80, "_Sysviews", Category.SQL_CAT),
/** */
_Word_rule(-79, "_Word-rule", Category.SCHEMA),
/** */
_AreaExtent(-72, "_AreaExtent", Category.PHY_ST),
/** */
_Area(-71, "_Area", Category.PHY_ST, MANDATORY, MetadataManager::populateArea),
/** */
_StorageObject(-70, "_StorageObject", Category.PHY_ST),
/** */
_Collation(-69, "_Collation", Category.SCHEMA),
/** */
_Codepage(-68, "_Codepage", Category.SCHEMA),
/** */
_Sequence(-67, "_Sequence", Category.SCHEMA, MANDATORY, MetadataManager::populateSequence),
/** */
_Field_Trig(-66, "_Field-Trig", Category.SCHEMA, MANDATORY),
/** */
_File_Trig(-65, "_File-Trig", Category.SCHEMA, MANDATORY),
/** */
_View_Ref(-32, "_View-Ref", Category.SQL89),
/** */
_View_Col(-31, "_View-Col", Category.SQL89),
/** */
_View(-30, "_View", Category.SQL89),
/** */
_Db(-6, "_Db", Category.SCHEMA, MANDATORY, MetadataManager::createDbMetaRecord),
/** */
_User(-5, "_User", Category.AUTH, REGULAR | MANDATORY),
/** */
_Index_Field(-4, "_Index-Field", Category.SCHEMA, MANDATORY),
/** */
_Index(-3, "_Index", Category.SCHEMA, MANDATORY),
/** */
_Field(-2, "_Field", Category.SCHEMA, MANDATORY),
/** */
_File(-1, "_File", Category.SCHEMA, MANDATORY, MetadataManager::populateFileTable);
/** Table id */
public final int fileNum;
/** Table name */
public final String fileName;
/** Table converted name */
public final String shortName;
/** DMO interface simple name == FWD table name*/
public final String ifaceName;
/** Table category */
public final Category category;
/** table flags */
public final int flags;
/** A bi-function which populates the table on startup */
public final BiConsumer<Database, Persistence> init;
/**
* Constructor.
*
* @param fileNum
* table id
* @param fileName
* table name
* @param category
* table category
* @param flags
* table flags
* @param init
* table initializer
*/
SystemTable(int fileNum,
String fileName,
Category category,
int flags,
BiConsumer<Database, Persistence> init)
{
this.fileNum = fileNum;
this.fileName = fileName;
this.shortName = getShortName(fileName);
this.ifaceName = META_PREFIX + shortName;
this.category = category;
this.flags = flags;
this.init = init == null || !LOG_INFO ? init : (d, p) ->
{
long start = System.nanoTime();
log.log(Level.INFO, String.format("Populating [%s]", fileName));
init.accept(d, p);
log.log(Level.INFO, String.format("Populated [%s] in %d ms",
fileName, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)
)
);
};
MetadataManager.byName.put(fileName.toLowerCase(), this);
}
/**
* Constructor.
*
* @param fileNum
* table id
* @param fileName
* table name
* @param category
* table category
*/
private SystemTable(int fileNum, String fileName, Category category)
{
this(fileNum, fileName, category, 0, null);
}
/**
* Constructor
*
* @param fileNum
* table id
* @param fileName
* table name
* @param category
* table category
* @param flags
* table flags
*/
private SystemTable(int fileNum, String fileName, Category category, int flags)
{
this(fileNum, fileName, category, flags, null);
}
/**
* Constructor
*
* @param fileNum
* table id
* @param fileName
* table name
* @param category
* table category
* @param populate
* table initializer
*/
private SystemTable(int fileNum,
String fileName,
Category category,
BiConsumer<Database,
Persistence> populate)
{
this(fileNum, fileName, category, 0, populate);
}
/**
* Populate all metatables with provided initializer.
*
* @param metaDb
* meta database
* @param p
* Persistence instance for the meta database
*/
public static void populateAll(Database metaDb, Persistence p)
{
Arrays.stream(values())
.map(t -> t.init)
.filter(Objects::nonNull)
.forEach(init -> init.accept(metaDb, p));
}
/**
* Get system table by DMO interface short name
*
* @param iface
* table DMO interface short name
*
* @return System table with a given DMO interface short name.
*/
public static SystemTable forIface(String iface)
{
return Arrays.stream(values())
.filter(t -> t.ifaceName.equalsIgnoreCase(iface))
.findFirst()
.orElse(null);
}
/**
* Get a set of tables names that match the provided filter.
*
* @param flag
* The flag used to filter the tables. The filter can be any flag from
* {@link TableFlags} and is applied to {@link SystemTable#values()}.
*
* @return Set of table names that match the provided filter.
*/
public static Set<String> filterTables(int flag)
{
Set<String> filteredTables = new HashSet<>();
SystemTable[] tables = SystemTable.values();
for (int i = 0; i < tables.length; i++)
{
SystemTable t = tables[i];
if ((t.flags & flag) == flag)
{
filteredTables.add(t.fileName.trim().toLowerCase());
}
}
return filteredTables;
}
/**
* Check if the table is in the master database
*
* @return <code>true</code> if the table is in the master database.
*/
public boolean isRegular()
{
return (this.flags & REGULAR) != 0;
}
/**
* Check if the table is mandatory
*
* @return <code>true</code> if the table is mandatory.
*/
public boolean isMandatory()
{
return (this.flags & MANDATORY) != 0;
}
/**
* Check if the table is read-only.
*
* @return {@code true} if the table is mandatory.
*/
public boolean isReadOnly()
{
return (this.flags & READONLY) != 0;
}
}
/**
* {@code Map<String, Object>} fluent builder.
*
* @param <T> builder extension class
*/
private static class MapBuilder<T extends MapBuilder<T>>
{
/** Map of properties to be built. */
private final Map<Tuple<String, Integer>, Object> m = new LinkedHashMap<>();
/**
* Return reference to the actual class (required for the fluent operations' extension)
*
* @return reference to the actual class
*/
public T self()
{
return (T) this;
}
/**
* Add key/value pair to the map. This is the method used for scalar properties.
*
* @param key
* key
* @param value
* value
*
* @return The builder instance.
*/
public T put(String key, Object value)
{
m.put(new Tuple<>(key, null), value);
return self();
}
/**
* Add key/index/value tuple for mapping. This method is to be used for extent properties.
*
* @param key
* The extent property to be populated.
* @param idx
* The index within the extent property to be populated.
* @param value
* The value for the property.
*
* @return The builder instance.
*/
public T put(String key, Integer idx, Object value)
{
m.put(new Tuple<>(key, idx), value);
return self();
}
/**
* Add all key/value pairs from the provided map
*
* @param map
* Map to be added.
*
* @return The builder instance.
*/
public T putAll(Map<Tuple<String, Integer>, Object> map)
{
m.putAll(map);
return self();
}
/**
* Build the immutable map.
*
* @return the immutable map
*/
public Map<Tuple<String, Integer>, Object> build()
{
return Collections.unmodifiableMap(m);
}
}
/**
* _File table record field/value map builder.
*/
private static class FileTableRecordBuilder extends MapBuilder<FileTableRecordBuilder>
{
/**
* Return reference to the actual class (required for the fluent operations' extension)
*
* @return Reference to the actual class.
*/
@Override
public FileTableRecordBuilder self()
{
return this;
}
/**
* Build the immutable map for the _File table record.
*
* @param dmoIface
* _File table DMO interface.
* @param pk
* The _prime-index field value holder.
* @param children
* Records' data for the child tables.
*
* @return The immutable map for the _File table record.
*/
public Map<Tuple<String, Integer>, Object> build(
Class<? extends DataModelObject> dmoIface,
integer pk,
Map<SystemTable, List<Map<Tuple<String, Integer>, Object>>> children)
{
Table table = dmoIface.getAnnotation(Table.class);
int numfld = children.get(SystemTable._Field).size();
return put("dbRecid", integer.of(DB_RECID)).
put("fileName", new character(table.legacy())).
put("owner", character.of("PUB")).
put("userMisc", new character(table.name())).
put("dumpName", new character(table.dumpName())).
put("frozen", logical.of(table.isFrozen())).
put("hidden", logical.of(table.isHidden())).
put("category", new character(table.category())).
put("numfld", integer.of(numfld)).
put("primeIndex", pk).
put("desc", new character(table.description())).
put("fileLabel", new character(table.label())).
put("fileLabelSa", new character(table.label_sa())).
put("valexp", new character(table.valexp())).
put("valmsg", new character(table.valmsg())).
put("valmsgSa", new character(table.valmsg_sa())).
put("canRead", character.of(table.canRead())).
put("canWrite", character.of(table.canWrite())).
put("canCreate", character.of(table.canCreate())).
put("canDelete", character.of(table.canDelete())).
put("canDump", character.of(table.canDump())).
put("canLoad", character.of(table.canLoad())).
build();
}
}
}