P2OLookup.java
/*
** Module : P2OLookup.java
** Abstract : Helper class which loads P2O ASTs and enables lookup access
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ---------------------------------- Description ----------------------------------
** 001 ECF 20051013 @23034 Created initial version. Lazily loads P2O
** ASTs and enables lookups of Java names for
** legacy Progress table and field names.
** This first implementation supports fixed
** tables only (not temp- or work-tables).
** Also exposes index information by table.
** 002 ECF 20051017 @23054 Change to P2JIndex construction parameters.
** Use historical index name instead of
** converted name.
** 003 ECF 20051023 @23095 Added javaPropertyName() convenience method.
** Looks up a Java property name for a given
** legacy field name, optionally qualifying it
** with a buffer name.
** 004 ECF 20051027 @23176 Added 'ignore-case' processing for index
** components. Takes new annotation in P2O AST
** into account when creating indexes.
** 005 ECF 20051102 @23181 Added legacyFieldNames method. Retrieves set
** of legacy field names for a Progress table,
** in display order.
** 006 ECF 20051121 @23423 Added isValidatable method. Indicates whether
** a schema field must invoke validation
** processing before it may be saved to its DMO.
** 007 ECF 20060202 @24252 Added support for temp/work-table schema
** data. These data are refreshed by pattern
** workers each time a new source file AST is
** visited.
** 008 ECF 20060227 @24818 Minor fix for field-level validation support.
** Made check for validatable annotation safer.
** 009 ECF 20060408 @25429 Added isDMOInterface() method. Indicates
** whether a given class name collides with any
** known DMO interface name. Temp table DMOs not
** currently handled.
** 010 ECF 20061026 @30795 Added isForeignReference() method. Indicates
** whether a given table is referenced by the
** foreign key of any other table.
** 011 ECF 20061101 @30882 Added dumpTableDependencies() method. Debug
** method to print tree of dependencies for a
** table.
** 012 ECF 20070628 @35342 Minor optimization. Replaced StringBuffer
** with StringBuilder.
** 013 ECF 20080304 @37468 Modified to support changes in P2JIndex.
** 014 SVL 20080326 @37683 Synthetic indexes are ignored in the
** initializeIndexes().
** 015 GES 20090424 @41953 Import change.
** 016 GES 20090429 @42051 Match package and class name changes.
** 017 GES 20090515 @42220 Moved to AstManager from AstRegistry/AstPersister.
** 018 GES 20090518 @42398 Import change.
** 019 LMR 20110106 Fixes to reflect renaming of load() method in
** Configuration class to getSchemaConfig() and
** removal of its 'key' parameter.
** 020 ECF 20130130 Add metadata to P2OLookup data structures, under each primary schema
** loaded. Implemented generics.
** 021 OM 20130305 Fix java path for metadata tables. Added message when a table name
** conflict is detected.
** 022 ECF 20131013 Don't add synthetic legacy fields to legacy field name map.
** 023 CA 20131028 All the static info is kept context-local, for runtime query
** conversion support. All filenames must be normalized.
** 024 OM 20131030 Added support for checking if a schema contains a certain dmo iface.
** 025 VMN 20140328 Added support for denormalized fields with extent.
** 026 SVL 20140329 loadAst loads data from a resource in runtime mode.
** 027 OM 20140424 Added CASE_SENSITIVE constant used for dynamic temp-tables
** generation.
** 028 ECF 20140921 Memory optimization: intern many duplicated strings.
** 029 OM 20150212 Forced lowercase legacy access for javaName accessors while saving
** the original legacy names for later extraction.
** Simplified String operations. Update code to Java 7.
** 030 ECF 20150304 Refactor storage of P2OLookup instances to keep permanent database
** info in a static, shared data structure, to save memory. These data
** structures, which can be quite large, were being duplicated across
** all contexts.
** 031 VMN 20150313 Fixed denormalized field assignments.
** 032 ECF 20150715 Replace StringBuffer with StringBuilder.
** 033 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 034 ECF 20160907 Added initialize method to allow lookups to be primed at server
** startup.
** 035 ECF 20180305 Added isReadOnly API to check for a read-only table.
** 036 ECF 20180908 Added support for unused index and field reporting.
** 037 CA 20190508 Buffers must be looked in the class hierarchy, not only in the
** current class (thus we need to recursively load .p2o files).
** 038 CA 20190724 Fixed case sensitivity of javaPackage, validatables and readOnlyTables
** (meta tables are lowercased).
** 039 GES 20190620 Added multiple inheritance support (for OO interface defs).
** 040 OM 20200806 Fixed NPE.
** 041 OM 20200924 P2JIndexComponent carries multiple information to avoid map lookups for them.
** CA 20201008 Added API to get an iterator over the property ASTs. The propsMap is no longer
** cleared for normal conversion - just for runtime conversion.
** OM 20210711 In case of buffer parameters, use the super .Buf interface name to allow
** calls with 'sibling' tables. Stored that in the new [javaInterfaceMap].
** OM 20210716 Fixed population of [javaInterfaceMap]. Local optimizations.
** OM 20210908 [javaPackage] uses computed java name instead of legacy name.
** IAS 20211928 Added isNullable method.
** ECF 20211108 Format cleanup.
** OM 20211020 Added _DATASOURCE_ROWID property.
** OM 20220212 Use ReservedProperty static method for testing reserved properties names.
** OM 20220330 Moved database conversion artifacts to ${cvtpath} folder.
** CA 20230110 Let 'getImmediateChild' work with a set, if an array is used for the types.
** OM 20230111 Prioritize the _temp database in case of dynamic conversion.
** 042 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 043 OM 20240318 Special handling of dynamic tables in mutable permanent databases.
** 044 OM 20240416 Replaced string literal with SchemaDictionary.MUTABLE_ANNOTATION constant.
** 045 RAA 20240821 Added file name for indeterminate sorting queries report.
** RAA 20240906 Added writeHeaderToIndeterminateSortFile() method.
** 046 OM 20241128 Multi-tenant conversion support: added getTenantType() method.
** 047 DDF 20240205 Added NON_NULLABLE annotation to index component.
*/
/*
** 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.schema;
import java.io.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import com.goldencode.ast.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.util.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.uast.*;
/**
* A helper class which exposes information stored in P2O ASTs for access
* during source conversion processing. Information is exposed via static
* methods, which resolve lookups using individual instances of this class.
* The following data currently is exposed:
* <ul>
* <li>Java DMO class names (given legacy table names)
* <li>Java DMO property names (given legacy field names)
* <li>Java DMO package names (given legacy table names)
* <li>Index descriptors (given legacy table names)
* <li>Whether a field has schema-level validation configured (given legacy
* field names)
* <li>Whether a DMO interface exists in any schema (given a Java class
* name)
* <li>Whether a database table is referenced by the foreign key of any
* other table (given the legacy name of the table which is the
* potential referent)
* </ul>
*
* @see P2JIndex
* @see P2JIndexComponent
*/
public final class P2OLookup
implements DataModelTokenTypes
{
/** Name of schema dictionary configuration */
public static final String CFG_NAME = "schema-dict";
/** File extension appended to schema and source filenames */
public static final String P2O_POSTFIX = ".p2o";
/** Key for "package" database AST annotation */
public static final String PACKAGE = "package";
/** Key for "historical" class/property AST annotation */
public static final String HISTORICAL = "historical";
/** Key for "label" class/property AST annotation */
public static final String LABEL = "label";
/** Key for "col_lab" property AST annotation */
public static final String COLUMN_LABEL = "col_lab";
/** Key for "format" class/property AST annotation */
public static final String FORMAT = "format";
/** Key for "datatype" property AST annotation */
public static final String DATATYPE = "datatype";
/** Key for "extent" property AST annotation */
public static final String EXTENT = "extent";
/** Key for "validatable" property AST annotation */
public static final String VALIDATABLE = "validatable";
/** Key for non-nullable property AST annotation */
public static final String NON_NULLABLE = "not-null";
/** Key for "decimals" property AST annotation */
public static final String DECIMALS = "decimals";
/** Key for "read-only" class AST annotation */
public static final String READ_ONLY = "read-only";
/** Key for "table" class AST annotation */
public static final String TABLE = "table";
/** Key for "unique" index AST annotation */
public static final String UNIQUE = "unique";
/** Key for "primary" index AST annotation */
public static final String PRIMARY = "primary";
/** Key for "synthetic" index AST annotation */
public static final String SYNTHETIC = "synthetic";
/** Key for "word" index AST annotation */
public static final String WORD = "word";
/** Key for "refid" index column AST annotation */
public static final String REFID = "refid";
/** Key for "descend" index column AST annotation */
public static final String DESCEND = "descend";
/** Key for "ignore-case" index column AST annotation */
public static final String IGNORE_CASE = "ignore-case";
/** Key for "case-sensitive" annotation in data model AST */
public static final String CASE_SENSITIVE = "case-sensitive";
/** Key for "abbreviated" index column AST annotation */
public static final String ABBREVIATED = "abbreviated";
/** Key for "foreign" relation AST annotation */
public static final String FOREIGN = "foreign";
/** Key for "references" relation AST annotation */
public static final String REFERENCES = "references";
/** File name for unindexed query report */
static final String UNINDEXED_QUERIES_FILE = "unindexed_queries.txt";
/** File name for indeterminate sorting queries report */
static final String INDETERMINATE_SORT_QUERIES_FILE = "indeterminate_sort_queries.csv";
/** File name for unused index report */
private static final String UNUSED_INDEX_FILE = "unused_database_indices.txt";
/** File name for unused field report */
private static final String UNUSED_FIELD_FILE = "unreferenced_database_fields.txt";
/** Key for current temp table schema in schema map */
private static final String TEMP_TABLE_KEY = null;
/** The types used by {@link #initializeRelations(String, Aast)}. */
private static final Set<Integer> types = new HashSet<>(Arrays.asList(ONE_TO_MANY, ONE_TO_ONE));
/** Container with user-specific data. */
private static final ContextLocal<WorkArea> local = new ContextLocal<WorkArea>()
{
@Override
protected WorkArea initialValue()
{
return new WorkArea();
}
};
/** Shared map of permanent database schema names to <code>P2OLookup</code> instances */
private static final Map<String, P2OLookup> permSchemaMap = new HashMap<>();
/** Stores the list of meta tables */
private final Set<String> historicalMetaTables = new HashSet<>();
/** Map of AST IDs to class ASTs */
private final Map<Long, Aast> classByIDMap = new HashMap<>();
/** Map of historical table/field names to Java class/property names */
private final Map<String, Map<Long, String>> javaNameMap = new LinkedHashMap<>();
/** Map of historical table/field names to Java super-interface names (for temp-table only). */
private final Map<String, String> javaInterfaceMap = new LinkedHashMap<>();
/**
* Map with normalized to original names of table/field names. To save memory, this map
* contains only the entries that are not in lowercase (normalized).
* If an entry is a key in {@code javaNameMap} but not here, then the name is already the
* original form.
*/
private final Map<String, String> originalNamesMap = new HashMap<>();
/** Set of historical fully qualified denormalized properties of fields with custom extent */
private final Set<String> denormalizedProperties = new HashSet<>();
/** Map of fully qualified, normalized, historical table names to index descriptors */
private final Map<String, List<P2JIndex>> indexMap = new HashMap<>();
/** Mutable index information for unused index reporting */
private final TableResourceMap mutableIndexMap = new TableResourceMap();
/** Mutable field information for unused field reporting */
private final TableResourceMap mutableFieldMap = new TableResourceMap();
/** Map of historical table names to sets of referencing tables */
private final Map<String, Set<Long>> foreignReferenceMap = new HashMap<>();
/** Map of property AST unique IDs to the AST nodes. */
private final Map<Long, Aast> propsMap = new HashMap<>();
/** Set of schema names of validatable fields */
private final Set<String> validatables = new HashSet<>();
/** Set of schema names of non-nullable fields */
private final Set<String> nonnullables = new HashSet<>();
/** Set of schema names of read-only tables */
private final Set<String> readOnlyTables = new HashSet<>();
/** Set of all unqualified DMO Java interface names */
private final Set<String> javaNames = new HashSet<>();
/**
* The tenant table type. Only for multi-tenant tables. Mapped values:
* 0 = single-tenant (not store here),
* 1 = multi-tenant,
* 2 = multi-tenant-with-default.
*/
private final Map<String, Integer> multiTenant = new HashMap<>();
/** Logger */
private static final ConversionStatus LOG = ConversionStatus.get(P2OLookup.class);
/** Java package associated with this lookup instance */
private String javaPackage = null;
/** Java package associated with this lookup instance metadata */
private String metaPackage = null;
/** The P2O information in any super-class (interfaces can have multiple). */
private P2OLookup[] parents = null;
/**
* Constructor which initializes this object with the information in the
* P2O AST associated with <code>schema</code>.
*
* @param schema
* Database schema name which determines which P2O AST to load.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
private P2OLookup(String schema)
throws SchemaException
{
initialize(schema);
propsMap.clear();
}
/**
* Attempts to locate the {@code P2OLookup} for an existing dynamic or temporary schema. The AST tree of
* {@code filename} is loaded and the {@code dynamic} flag from its root is analyzed for deciding whether
* it is dynamic or not. In the first case, the existing {@code P20Lookup} instance is returned if one
* exist. Otherwise, the application defaults to temp-table mode and a new {@code P2OLookup} instance
* is created and returned.
*
* @param filename
* Name of file containing the P2O AST to load. File may or may not exist.
* @param clsDef
* The associated class definition for this file, otherwise {@code null}.
*
* @return A {@code P2OLookup} instance as described above.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
private static P2OLookup dynamicGet(String filename, ClassDefinition clsDef)
throws SchemaException
{
if (AstManager.get().isExistingAst(filename))
{
Aast root = AstManager.get().loadTree(filename);
if (root.isAnnotation(SchemaDictionary.MUTABLE_ANNOTATION))
{
P2OLookup dynamicInstance = getInstance(root.getText(), 0);
if (dynamicInstance != null)
{
dynamicInstance.initialize(root.getText(), root, false);
return dynamicInstance;
}
}
}
return new P2OLookup(TEMP_TABLE_KEY, filename, clsDef);
}
/**
* Constructor which initializes this object with the information in the
* P2O AST stored in <code>filename</code>. This variant is intended to
* be used to load temp/work-table schema information. If there is no
* file with name <code>filename</code>, we still update the static
* schema map, in order to replace the <code>P2OLookup</code> instance
* (if any) already associated with <code>schema</code>. This ensures
* that we are not using out-of-date temp table schema information.
*
* @param schema
* Database schema name to which this instance is mapped.
* @param filename
* Name of file containing the P2O AST to load. File may or may not exist.
* @param clsDef
* The associated class definition for this file, otherwise <code>null</code>.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
private P2OLookup(String schema, String filename, ClassDefinition clsDef)
throws SchemaException
{
if (AstManager.get().isExistingAst(filename))
{
try
{
initialize(schema, AstManager.get().loadTree(filename), false);
}
catch (AstException exc)
{
throw new SchemaException(
"Error loading P2O AST for database schema: " + schema,
exc);
}
// do not clear this map for full conversion
if (Configuration.isRuntimeConfig())
{
propsMap.clear();
}
}
if (clsDef != null)
{
ClassDefinition[] pars = clsDef.getParents();
if (pars != null)
{
ArrayList<P2OLookup> parr = new ArrayList<>();
SchemaException[] exc = new SchemaException[1];
Consumer<ClassDefinition> consumer = (ClassDefinition cls) ->
{
if (exc[0] == null && !cls.isBuiltIn())
{
String parentFile = cls.getFilename();
parentFile += P2O_POSTFIX;
parentFile = Configuration.normalizeFilename(parentFile);
try
{
parr.add(new P2OLookup(schema, parentFile, cls));
}
catch (SchemaException e)
{
exc[0] = e;
}
}
};
clsDef.processParentGraph(consumer);
if (exc[0] != null)
{
throw exc[0];
}
else
{
parents = parr.toArray(new P2OLookup[0]);
}
}
}
WorkArea wa = locate();
wa.tempP2OLookup = this;
}
/**
* Initialize a {@code P2OLookup} instance for the schema of each primary database managed by
* this server.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public static void initialize()
throws SchemaException
{
for (Database database : DatabaseManager.getManagedDatabases())
{
if (database.isPrimary())
{
String schema = DatabaseManager.getSchema(database);
getInstance(schema, EntityName.DATABASE);
}
}
}
/**
* Attempt to load the P2O AST stored in <code>filename</code>, which must
* represent a temp/work-table schema. This method should be invoked by
* conversion service pattern workers which need to load temporary schema
* information upon loading a new source file AST. A new instance of this
* class is created as a result, and is stored in the schema map, under
* the temp table schema key. If this method is invoked more than once in
* succession with the same file name, only the first invocation triggers
* the P2O AST to be loaded.
* <p>
* If no file exists with the name <code>filename</code>, an empty
* instance of this class is stored in the schema map under the temp table
* schema key. This ensures that an out-of-date <code>P2OLookup</code>
* instance is not maintained under the temp table key beyond it useful
* life.
*
* @param filename
* Name of file containing the P2O AST to load. File may or may
* not exist, but this value may not be <code>null</code>.
* @param clsDef
* The associated class definition for this file, otherwise <code>null</code>.
*
* @throws RuntimeException
* if there is any error loading or parsing the P2O AST data for
* the given file; non-existence of the file is not considered
* an error.
*/
public static void loadTemporarySchema(String filename, ClassDefinition clsDef)
{
filename += P2O_POSTFIX;
filename = Configuration.normalizeFilename(filename);
WorkArea wa = locate();
if (!filename.equals(wa.lastTempFilename))
{
try
{
P2OLookup.dynamicGet(filename, clsDef);
}
catch (SchemaException exc)
{
throw new RuntimeException(exc);
}
wa.lastTempFilename = filename;
}
}
/**
* Report whether the given class name is the unqualified name of any DMO
* interface, in any schema tracked by the current P2J configuration.
*
* @param name
* An unqualified class name.
*
* @return <code>true</code> if <code>name</code> collides with any DMO
* interface in any schema, else <code>false</code>.
*
* @throws SchemaException
* if there is an error loading schema names from configuration.
*/
public static boolean isDMOInterface(String name)
throws SchemaException
{
Iterator<String> iter = schemaNames();
while (iter.hasNext())
{
String schema = iter.next();
if (isDMOInterface(name, schema))
{
return true;
}
}
return false;
}
/**
* Report whether the given class name is the unqualified name of a DMO interface, in the
* <code>schema</code> tracked by the current P2J configuration.
*
* @param name
* An unqualified class name.
* @param schema
* The name of the schema to look into.
*
* @return <code>true</code> if <code>name</code> is present as java name of a table from
* the specified <code>schema</code> and <code>false</code> if <code>schema</code>
* does not exist or it does not contain such interface.
*
* @throws SchemaException
* if there is an error loading schema names from configuration.
*/
public static boolean isDMOInterface(String name, String schema)
throws SchemaException
{
P2OLookup lookup = getInstance(schema, EntityName.DATABASE);
return lookup.containsDMOInterface(name);
}
/**
* Report whether the given, historical table will be mapped in the
* converted application to a database table which represents a foreign
* reference of another table's foreign key.
*
* @param historical
* Fully qualified, legacy Progress table name (not mandatory normalized).
*
* @return <code>true</code> if the given table represents the foreign
* end of a one-to-one or a one-to-many relation between two
* tables.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public static boolean isForeignReference(String historical)
throws SchemaException
{
P2OLookup lookup = getInstance(historical, EntityName.TABLE);
return lookup.containsForeignReference(historical);
}
/**
* Dump an exhaustive tree of nested table dependencies. A dependency is
* a foreign key relation from the dependent table to the "parent" table.
* The tree is printed to stdout. Each indent indicates a dependency to
* the next outer level.
*
* @param historical
* Fully qualified, legacy Progress table name.
*
* @throws SchemaException
* if there is any error loading the AST from persistence.
*/
public static void dumpTableDependencies(String historical)
throws SchemaException
{
P2OLookup lookup = getInstance(historical, EntityName.TABLE);
lookup.dumpTableDependencies(historical, 0);
}
/**
* Retrieve the Java package name of the DMO associated with the given
* legacy Progress table name.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a table.
*
* @return Corresponding Java package name, or <code>null</code> if no
* associated DMO or package name was found.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public static String javaPackage(String historical)
throws SchemaException
{
P2OLookup inst = getInstance(historical, EntityName.TABLE);
return inst.getJavaPackage(historical);
}
/**
* Retrieve a Java class or variable name, based upon the legacy Progress
* schema name of the corresponding table or field, respectively.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a table or
* field.
* @param type
* <code>EntityName</code> constant to indicate whether
* <code>historical</code> represents a table or a field.
*
* @return Corresponding, unqualified, Java class or variable name.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the table or field portion of <code>historical</code>
* cannot be resolved to a corresponding Java name.
*/
public static String javaName(String historical, int type)
throws SchemaException
{
return javaName(historical, type, null);
}
/**
* In the case of temp-tables, look up the name of a Java DMO super interface, given its legacy name.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a temp-table.
*
* @return Corresponding, unqualified, Java super interface.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the table or field portion of {@code historical} cannot be resolved to a corresponding Java
* name.
*/
public static String javaInterfaceName(String historical)
throws SchemaException
{
String normalized = historical.toLowerCase();
return getInstance(normalized, EntityName.TABLE).javaInterfaceMap.get(normalized);
}
/**
* Retrieve a Java class or variable name, based upon the legacy Progress schema name of the
* corresponding table or field, respectively.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a table or field (may not be
* normalized).
* @param type
* <code>EntityName</code> constant to indicate whether <code>historical</code>
* represents a table or a field.
* @param index
* Zero-based index of custom property name for denormalized field with extent,
* otherwise null.
*
* @return Corresponding, unqualified, Java class or variable name.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the table or field portion of <code>historical</code> cannot be resolved to a
* corresponding Java name.
*/
public static String javaName(String historical, int type, Long index)
throws SchemaException
{
String normalized = historical.toLowerCase();
String name = null;
if (Configuration.isRuntimeConfig())
{
P2OLookup tempP2OLookup = locate().tempP2OLookup;
if (tempP2OLookup != null)
{
// prioritize the _temp database in case of dynamic conversion. Statistically, the runtime will
// process objects from _temp database. Also, this will allow non-standard field names to be
// processed (for example with names containig "." character).
name = tempP2OLookup.getJavaName(normalized, index);
}
// if not found (name stays null), the full P2OLookup instance lookup is performed
}
if (name == null)
{
name = getInstance(normalized, type).getJavaName(normalized, index);
}
if (name == null)
{
// attempt to identify special fields of dataset (before) temp-tables
String unqualified = normalized;
int k = unqualified.lastIndexOf('.');
if (k > 0)
{
unqualified = unqualified.substring(k + 1);
}
if (ReservedProperty.isReservedProperty(unqualified))
{
switch (unqualified)
{
case Buffer.__ERROR_FLAG__: return Buffer.ERROR_FLAG_FIELD;
case Buffer.__ORIGIN_ROWID__: return Buffer.ORIGIN_ROWID_FIELD;
case Buffer.__DATA_SOURCE_ROWID__: return Buffer.DATA_SOURCE_ROWID_FIELD;
case Buffer.__ERROR_STRING__: return Buffer.ERROR_STRING_FIELD;
case Buffer.__AFTER_ROWID__: return Buffer.AFTER_ROWID_FIELD;
case Buffer.__ROW_STATE__: return Buffer.ROW_STATE_FIELD;
}
}
// if no match, throw error:
StringBuilder buffer = new StringBuilder("No java name found for legacy name: ");
buffer.append(historical);
if (index != null)
{
buffer.append(", index = ");
buffer.append(index);
}
throw new SchemaException(buffer.toString());
}
return name;
}
/**
* Retrieve the full set of fully qualified, legacy field names for a
* legacy Progress table. The set is guaranteed to iterate in the default
* display order defined for the original, Progress schema for that table.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a table.
* @param includeHidden
* If {@code true} the hidden fields are also included.
*
* @return Set of fully qualified, legacy field names.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if {@code historical} does not represent a known table.
*/
public static Set<String> legacyFieldNames(String historical, boolean includeHidden)
throws SchemaException
{
P2OLookup lookup = getInstance(historical, EntityName.TABLE);
Set<String> names = lookup.getLegacyFieldNames(historical, includeHidden);
if (names.isEmpty())
{
throw new SchemaException("No table found for legacy name: " + historical);
}
return names;
}
/**
* Retrieve a Java property name, based upon the legacy Progress schema
* name of the corresponding table or field, respectively. If specified,
* the property name will be qualified with a buffer name, suitable for
* insertion into an HQL where clause or order by clause. The convention
* for the buffer name qualifier is to use the short DMO class name, with
* the first letter lowercased.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a table or
* field.
* @param qualified
* <code>true</code> if property name should be qualified with a
* buffer name; <code>false</code> if it should remain
* unqualified.
*
* @return Corresponding Java property name.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the table or field portion of <code>historical</code>
* cannot be resolved to a corresponding Java name.
*/
public static String javaPropertyName(String historical, boolean qualified)
throws SchemaException
{
return javaPropertyName(historical, qualified, null);
}
/**
* Retrieve a Java property name, based upon the legacy Progress schema
* name of the corresponding table or field, respectively. If specified,
* the property name will be qualified with a buffer name, suitable for
* insertion into an HQL where clause or order by clause. The convention
* for the buffer name qualifier is to use the short DMO class name, with
* the first letter lowercased.
*
* @param historical
* Fully qualified, legacy, Progress schema name for a table or
* field.
* @param qualified
* <code>true</code> if property name should be qualified with a
* buffer name; <code>false</code> if it should remain
* unqualified.
* @param index
* Zero-based index of custom property name for denormalized field with extent,
* otherwise null.
*
* @return Corresponding Java property name.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence;
* if the table or field portion of <code>historical</code>
* cannot be resolved to a corresponding Java name.
*/
public static String javaPropertyName(String historical, boolean qualified, Long index)
throws SchemaException
{
String property = javaName(historical, EntityName.FIELD, index);
if (!qualified)
{
return property;
}
historical = historical.substring(0, historical.lastIndexOf("."));
String className = javaName(historical, EntityName.TABLE);
return StringHelper.changeCase(className, false) + '.' + property;
}
/**
* Obtain the multi-tenant table type. The values are:
* 0 = single-tenant (not store here),
* 1 = multi-tenant,
* 2 = multi-tenant-with-default.
*
* @param table
* The table name.
*
* @return The table multi-tenant type, as described above.
*
* @throws SchemaException
* In case of error accessing internal data.
*/
public static int getTenantType(String table)
throws SchemaException
{
P2OLookup lookup = getInstance(table, EntityName.TABLE);
Integer mt = lookup.multiTenant.get(table);
return mt == null ? 0 : mt;
}
/**
* The given index in the given table has been referenced in business logic. Remove it from
* the mutable index resource map.
*
* @param table
* Qualified, normalized, legacy table name.
* @param index
* Legacy name of index which was referenced.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public static void indexReferenced(String table, String index)
throws SchemaException
{
P2OLookup lookup = getInstance(table, EntityName.TABLE);
lookup.processIndexedReferenced(table, index);
}
/**
* The given field has been referenced in business logic. Remove it from the mutable field
* resource map.
*
* @param field
* Legacy name (normalized schemaname) of field which was referenced.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public static void fieldReferenced(String field)
throws SchemaException
{
EntityName eName = new EntityName(EntityName.FIELD, field);
String database = eName.getDatabase();
String table = (database != null ? database + '.' : "") + eName.getTable();
P2OLookup lookup = getInstance(table, EntityName.TABLE);
lookup.processFieldReferenced(table, eName);
}
/**
* Delete previous database resource report files, if any.
*/
public static void deleteDatabaseResourceReports()
{
String[] names = new String[]
{
UNUSED_INDEX_FILE,
UNUSED_FIELD_FILE,
UNINDEXED_QUERIES_FILE,
INDETERMINATE_SORT_QUERIES_FILE,
};
for (String next : names)
{
File file = new File(next);
if (file.exists() && file.isFile())
{
file.delete();
}
}
}
/**
* Write the header for the indeterminate sorting queries report.
*/
public static void writeHeaderToIndeterminateSortFile()
throws IOException
{
String sep = System.lineSeparator();
String header = "File, Line number, Table, Chosen index, Risk, Query";
FileWriter fw = new FileWriter(P2OLookup.INDETERMINATE_SORT_QUERIES_FILE, true);
try (BufferedWriter bw = new BufferedWriter(fw))
{
bw.write(header + sep);
bw.close();
}
}
/**
* Emit content into the unused index report.
*
* @param temp
* {@code true} if content relates to temp-tables; {@code false} for permanent
* tables.
* @throws IOException
* if there is an error writing the report.
*/
public static void unusedIndexReport(boolean temp)
throws IOException
{
unusedResourceReport(UNUSED_INDEX_FILE, (lookup) -> lookup.mutableIndexMap, temp);
}
/**
* Emit content into the unused field report.
*
* @param temp
* {@code true} if content relates to temp-tables; {@code false} for permanent
* tables.
* @throws IOException
* if there is an error writing the report.
*/
public static void unusedFieldReport(boolean temp)
throws IOException
{
unusedResourceReport(UNUSED_FIELD_FILE, (lookup) -> lookup.mutableFieldMap, temp);
}
/**
* Get an iterator on all the index descriptors associated with the given,
* legacy table. There is no guaranteed order of this iteration; it is
* dictated by the order in which in which the indexes were read from the
* exported Progress schema.
*
* @param historical
* Fully qualified, legacy Progress table name (may not be normalized).
*
* @return Iterator on all indexes associated with the table named
* <code>historical</code>.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public static Iterator<P2JIndex> indexes(String historical)
throws SchemaException
{
P2OLookup lookup = getInstance(historical, EntityName.TABLE);
List<P2JIndex> indexList = lookup.getIndexes(historical);
if (indexList == null)
{
throw new SchemaException("Table '" + historical + "' not recognized");
}
return indexList.iterator();
}
/**
* Get an iterator on all the property ASTs associated with the given, legacy table. There is no guaranteed
* order of this iteration; it is dictated by the order in which in which the properties were read from the
* exported Progress schema.
*
* @param historical
* Fully qualified, legacy Progress table name (may not be normalized).
*
* @return Iterator on all properties associated with the table named <code>historical</code>.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public static Iterator<Aast> properties(String historical)
throws SchemaException
{
P2OLookup lookup = getInstance(historical, EntityName.TABLE);
List<Aast> propList = lookup.getProperties(historical);
if (propList == null || propList.isEmpty())
{
throw new SchemaException("Table '" + historical + "' not recognized");
}
propList.sort(Comparator.comparingLong(c -> (Long) c.getAnnotation("fieldid")));
return propList.iterator();
}
/**
* Compose a bean-like getter/setter method name from the Java DMO
* property associated with the specified, legacy, Progress field name.
* The name of the property is capitalized and prepended with
* <code>prefix</code>.
*
* @param prefix
* Verb to prepend to the capitalized property name; typically,
* "get", "is", or "set".
* @param historical
* Fully qualified, legacy, Progress schema name for a database
* field.
*
* @return Method name as described above.
*
* @throws SchemaException
* if this request results in the loading of a persisted P2O AST
* and there is any error loading P2J schema configuration data
* or loading the AST from persistence; if the field portion of
* <code>historical</code> cannot be resolved to a corresponding
* Java name.
*/
public static String beanMethodName(String prefix, String historical)
throws SchemaException
{
return beanMethodName(prefix, historical, null);
}
/**
* Compose a bean-like getter/setter method name from the Java DMO
* property associated with the specified, legacy, Progress field name.
* The name of the property is capitalized and prepended with
* <code>prefix</code>.
*
* @param prefix
* Verb to prepend to the capitalized property name; typically,
* "get", "is", or "set".
* @param historical
* Fully qualified, legacy, Progress schema name for a database field.
* @param index
* Zero-based index of custom property name for denormalized field with extent,
* otherwise null.
*
* @return Method name as described above.
*
* @throws SchemaException
* if this request results in the loading of a persisted P2O AST
* and there is any error loading P2J schema configuration data
* or loading the AST from persistence; if the field portion of
* <code>historical</code> cannot be resolved to a corresponding
* Java name.
*/
public static String beanMethodName(String prefix, String historical, Long index)
throws SchemaException
{
StringBuilder buf = new StringBuilder(prefix);
String prop = javaName(historical, EntityName.FIELD, index);
buf.append(StringHelper.changeCase(prop, true));
return buf.toString();
}
/**
* Check if given historical fully qualified property is denormalized.
*
* @param historical
* Historical fully qualified property name.
*
* @return <code>true</code> if given property is denormalized.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public static boolean isDenormalizedProperty(String historical)
throws SchemaException
{
P2OLookup lookup = getInstance(historical, EntityName.FIELD);
return lookup.containsDenormalizedProperty(historical);
}
/**
* Indicate whether a DMO property can be validated at runtime, given its
* legacy schema name. Possible forms of validation include:
* <ul>
* <li>mandatory (non-nullable) constraint;
* <li>participation in a unique constraint;
* <li>explicit validation method (converted from a Progress validation
* expression).
* </ul>
*
* @param historical
* Fully qualified, legacy Progress field name.
*
* @return <code>true</code> if the field should be validated at runtime,
* else <code>false</code>.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public static boolean isValidatable(String historical)
throws SchemaException
{
P2OLookup lookup = getInstance(historical, EntityName.FIELD);
return lookup.containsValidatable(historical);
}
/**
* Indicate whether a DMO property is nullable.
*
* @param historical
* Fully qualified, legacy Progress field name.
*
* @return <code>true</code> if the field is nullable, else <code>false</code>.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public static boolean isNullable(String historical)
throws SchemaException
{
P2OLookup lookup = getInstance(historical, EntityName.FIELD);
return lookup.containsNullable(historical);
}
/**
* Indicate whether a DMO class represents a read-only table, given its schema name.
*
* @param historical
* Fully qualified, legacy Progress table name.
*
* @return {@code true} if the table is read-only, else {@code false}.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
public static boolean isReadOnly(String historical)
throws SchemaException
{
P2OLookup lookup = getInstance(historical, EntityName.TABLE);
return lookup.containsReadOnly(historical);
}
/**
* Get an iterator on all schema names managed by the current P2J
* configuration.
*
* @return Iterator on all known schema names.
*
* @throws SchemaException
* if there is an error loading schema names from configuration.
*/
private static Iterator<String> schemaNames()
throws SchemaException
{
WorkArea wa = locate();
if (wa.schemaNames == null)
{
wa.schemaNames = new HashSet<>();
SchemaConfig config = Configuration.getSchemaConfig();
String meta = config.getMetadata().getName();
Iterator<String> iter = config.databases();
while (iter.hasNext())
{
String next = iter.next();
if (!next.equals(meta))
{
wa.schemaNames.add(next);
}
}
}
return wa.schemaNames.iterator();
}
/**
* Emit content into an unused database resource report.
*
* @param file
* Name of the file into which to emit content.
* @param mapFunc
* Function to return a {@code TableResourceMap}, given a {@code P2OLookup} object.
* @param temp
* {@code true} if content relates to temp-tables; {@code false} for permanent
* tables.
* @throws IOException
* if there is an error writing the report.
*/
private static void unusedResourceReport(String file,
Function<P2OLookup, TableResourceMap> mapFunc,
boolean temp)
throws IOException
{
WorkArea wa = null;
TableResourceMap trm = null;
// quick out for frequently invoked temp-table case
if (temp)
{
wa = locate();
P2OLookup lookup = wa.tempP2OLookup;
trm = mapFunc.apply(lookup);
if (trm.isEmpty())
{
return;
}
}
FileWriter fw = new FileWriter(file, true);
try (BufferedWriter bw = new BufferedWriter(fw))
{
if (temp)
{
String p2oFile = wa.lastTempFilename;
int pos = p2oFile.lastIndexOf('.');
p2oFile = p2oFile.substring(0, pos);
String heading = "Source File: " + p2oFile;
trm.writeContent(bw, heading);
}
else
{
synchronized (permSchemaMap)
{
for (Map.Entry<String, P2OLookup> entry : permSchemaMap.entrySet())
{
String schema = entry.getKey();
String heading = "Schema: " + schema;
P2OLookup lookup = entry.getValue();
trm = mapFunc.apply(lookup);
trm.writeContent(fw, heading);
}
}
}
}
}
/**
* Get the context-local {@link WorkArea} instance.
*
* @return See above.
*/
private static WorkArea locate()
{
return local.get();
}
/**
* Get or create a <code>P2OLookup</code> instance, given a schema name or
* a fully qualified, legacy table or field name. If the specified schema
* has not been processed previously, a new instance is created and
* cached; otherwise, a cached instance is returned.
*
* @param historical
* Schema name or fully qualified, legacy table or field name (may not be normalized).
* @param type
* <code>EntityName</code> type, indicating whether the name
* <code>historical</code> represents a schema, table, or field.
*
* @return A <code>P2OLookup</code> instance.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
private static P2OLookup getInstance(String historical, int type)
throws SchemaException
{
EntityName eName = new EntityName(type, historical.toLowerCase());
String schema = eName.getDatabase();
if (schema == TEMP_TABLE_KEY)
{
WorkArea wa = locate();
return wa.tempP2OLookup;
}
synchronized (permSchemaMap)
{
P2OLookup lookup = permSchemaMap.get(schema);
if (lookup == null)
{
lookup = new P2OLookup(schema);
permSchemaMap.put(schema, lookup);
}
return lookup;
}
}
/**
* Get the Java package name for the DMO interface classes associated with
* this lookup object's schema.
*
* @param historical
* Fully qualified, legacy Progress table name (not mandatory normalized).
*
* @return DMO interface package for this schema.
*/
private String getJavaPackage(String historical)
{
if (javaNameMap.containsKey(historical))
{
boolean meta = historicalMetaTables.contains(historical);
return meta ? metaPackage : javaPackage;
}
if (parents != null)
{
for (P2OLookup parent : parents)
{
String pkg = parent.getJavaPackage(historical);
if (pkg != null)
return pkg;
}
}
return null;
}
/**
* Report whether the given class name is the unqualified name of any DMO
* interface in the schema represented by this lookup object.
*
* @param name
* An unqualified class name.
*
* @return <code>true</code> if <code>name</code> collides with any DMO
* interface in this schema, else <code>false</code>.
*/
private boolean containsDMOInterface(String name)
{
if (javaNames.contains(name))
return true;
if (parents != null)
{
for (P2OLookup parent : parents)
{
if (parent.containsDMOInterface(name))
return true;
}
}
return false;
}
/**
* Check if given historical fully qualified property is denormalized.
*
* @param historical
* Historical fully qualified property name.
*
* @return <code>true</code> if given property is denormalized.
*/
private boolean containsDenormalizedProperty(String historical)
{
if (denormalizedProperties.contains(historical))
return true;
if (parents != null)
{
for (P2OLookup parent : parents)
{
if (parent.containsDenormalizedProperty(historical))
return true;
}
}
return false;
}
/**
* Report whether the given, historical table will be mapped in the
* converted application to a database table which represents a foreign
* reference of another table's foreign key.
*
* @param historical
* Fully qualified, legacy Progress table name (not mandatory normalized).
*
* @return <code>true</code> if the given table represents the foreign
* end of a one-to-one or a one-to-many relation between two
* tables.
*/
private boolean containsForeignReference(String historical)
{
if (foreignReferenceMap.containsKey(historical.toLowerCase()))
return true;
if (parents != null)
{
for (P2OLookup parent : parents)
{
if (parent.containsForeignReference(historical))
return true;
}
}
return false;
}
/**
* Indicate whether a DMO class represents a read-only table, given its schema name.
*
* @param historical
* Fully qualified, legacy Progress table name.
*
* @return {@code true} if the table is read-only, else {@code false}.
*/
private boolean containsReadOnly(String historical)
{
if (readOnlyTables.contains(historical))
return true;
if (parents != null)
{
for (P2OLookup parent : parents)
{
if (parent.containsReadOnly(historical))
return true;
}
}
return false;
}
/**
* Indicate whether a DMO property can be validated at runtime, given its
* legacy schema name. Possible forms of validation include:
* <ul>
* <li>mandatory (non-nullable) constraint;
* <li>participation in a unique constraint;
* <li>explicit validation method (converted from a Progress validation
* expression).
* </ul>
*
* @param historical
* Fully qualified, legacy Progress field name.
*
* @return <code>true</code> if the field should be validated at runtime,
* else <code>false</code>.
*/
private boolean containsValidatable(String historical)
{
if (validatables.contains(historical))
return true;
if (parents != null)
{
for (P2OLookup parent : parents)
{
if (parent.containsValidatable(historical))
return true;
}
}
return false;
}
/**
* Indicate whether a DMO property is nullable.
*
* @param historical
* Fully qualified, legacy Progress field name.
*
* @return <code>true</code> if the field is nullable, else <code>false</code>.
*/
private boolean containsNullable(String historical)
{
return !nonnullables.contains(historical);
}
/**
* The given index in the given table has been referenced in business logic. Remove it from
* the mutable index resource map.
*
* @param table
* Qualified, normalized, legacy table name.
* @param index
* Legacy name of index which was referenced.
*/
private void processIndexedReferenced(String table, String index)
{
if (mutableIndexMap.remove(table, index))
{
return;
}
if (parents != null)
{
for (P2OLookup parent : parents)
{
parent.processIndexedReferenced(table, index);
}
}
}
/**
* The given field has been referenced in business logic. Remove it from the mutable field
* resource map.
*
* @param table
* Qualified, normalized, legacy table name.
* @param eName
* Legacy name (normalized schemaname) of the field which was referenced.
*/
private void processFieldReferenced(String table, EntityName eName)
{
if (mutableFieldMap.remove(table, eName.getField()))
{
return;
}
if (parents != null)
{
for (P2OLookup parent : parents)
{
parent.processFieldReferenced(table, eName);
}
}
}
/**
* Get the converted, Java name of the class or property associated with
* the given, legacy table or field name (respectively).
*
* @param historical
* Fully qualified, legacy table or field name. Must be normalized (in lowercase).
*
* @return Unqualified, Java class name (for a table) or property name
* (for a field).
*/
private String getJavaName(String historical)
{
return getJavaName(historical, null);
}
/**
* Get the converted, Java name of the class or property associated with
* the given, legacy table or field name (respectively).
*
* @param historical
* Fully qualified, legacy table or field name. Must be normalized (in lowercase).
* @param index
* Zero-based index of custom property name for denormalized field with extent,
* otherwise null.
*
* @return Unqualified, Java class name (for a table) or property name
* (for a field).
*/
private String getJavaName(String historical, Long index)
{
String name = javaNameMap.containsKey(historical)
? javaNameMap.get(historical).get(index == null ? null : index + 1)
: null;
if (name != null)
{
return name;
}
if (parents != null)
{
for (P2OLookup parent : parents)
{
name = parent.getJavaName(historical, index);
if (name != null)
{
return name;
}
}
}
return null;
}
/**
* Get the set of legacy field names for a particular legacy table.
*
* @param historical
* Fully qualified, legacy table or field name (not necessarily normalized).
* @param includeHidden
* If {@code true} the hidden fields are also included.
*
* @return Set of legacy field names for the given table.
*/
private Set<String> getLegacyFieldNames(String historical, boolean includeHidden)
{
Set<String> names = new LinkedHashSet<>();
String test = historical.toLowerCase() + ".";
for (String next : javaNameMap.keySet())
{
if (next.startsWith(test))
{
// prepare to return the original historical name
String original = originalNamesMap.get(next);
original = (original == null) ? next : original;
boolean isHidden = false;
int k = original.lastIndexOf('.');
if (k > 0)
{
isHidden = ReservedProperty.isReservedProperty(original.substring(k + 1));
}
if (includeHidden || !isHidden)
{
names.add(original);
}
}
}
if (names.isEmpty() && parents != null)
{
for (P2OLookup parent : parents)
{
names = parent.getLegacyFieldNames(historical, includeHidden);
if (!names.isEmpty())
{
break;
}
}
}
return names;
}
/**
* Get the full list of index descriptors associated with a Progress table.
*
* @param historical
* Fully qualified, legacy table name (not necessarily normalized).
*
* @return A list of index descriptors associated with the table named {@code historical}. The list may be
* empty, but will not be {@code null}.
*/
private List<P2JIndex> getIndexes(String historical)
{
List<P2JIndex> res = indexMap.get(historical.toLowerCase());
if (res != null)
{
return res;
}
if (parents != null)
{
for (P2OLookup parent : parents)
{
res = parent.getIndexes(historical);
if (res != null)
return res;
}
}
return null;
}
/**
* Get the full list of index descriptors associated with a Progress table.
*
* @param historical
* Fully qualified, legacy table name (not necessarily normalized).
*
* @return A list of index descriptors associated with the table named {@code historical}. The list may be
* empty, but will not be {@code null}.
*/
private List<Aast> getProperties(String historical)
{
List<Aast> res = new ArrayList<>();
for (Aast ast : propsMap.values())
{
String hist = (String) ast.getParent().getAnnotation("historical");
if (historical.equalsIgnoreCase(hist))
{
res.add(ast);
}
}
if (!res.isEmpty())
{
return res;
}
if (parents != null)
{
for (P2OLookup parent : parents)
{
res = parent.getProperties(historical);
if (!res.isEmpty())
{
return res;
}
}
}
return null;
}
/**
* Initialize this <code>P2OLookup</code> instance by loading the P2O AST associated with the
* specified schema, walking it, and extracting and organizing the relevant information.
* Currently, initialization does the following:
* <ul>
* <li>maps fully qualified, legacy schema table and field names to their converted
* (unqualified) Java equivalents;
* <li>maps a table's fully qualified, legacy schema table name to a list of index
* descriptors defined for that table.
* </ul>
* <p>
* After the primary schema is walked, we also walk the metadata schema, loading all of its
* elements under the primary schema. This ensures all metadata references are resolved to the
* correct database.
*
* @param schema
* Name of database schema associated with the target P2O AST.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
private void initialize(String schema)
throws SchemaException
{
// load and initialize the primary schema and then the metadata schema, with the latter
// being initialized under the former, so that references to metadata elements will match
// the correct database
Aast root = null;
String[] schemas = null;
SchemaConfig.Metadata meta = Configuration.getSchemaConfig().getMetadata();
if (meta != null && !meta.getTables().isEmpty())
{
schemas = new String[] { schema, meta.getName() };
}
else
{
schemas = new String[] { schema };
}
for (String next : schemas)
{
try
{
root = loadAst(next);
}
catch (Exception exc)
{
throw new SchemaException("Error loading P2O AST for database schema: " + next, exc);
}
initialize(schema, root, next != schema); // OK to compare pointers, the value is stored in array
}
}
/**
* Initialize this <code>P2OLookup</code> instance by walking the given
* P2O AST and extracting and organizing the relevant information.
* Currently, initialization does the following:
* <ul>
* <li>maps fully qualified, legacy schema table and field names to
* their converted (unqualified) Java equivalents;
* <li>maps a table's fully qualified, legacy schema table name to a
* list of index descriptors defined for that table.
* </ul>
*
* @param schema
* Name of database schema associated with the target P2O AST.
* @param root
* Root node of P2O AST to inspect.
* @param meta
* Flags the schema as metadata information.
*
* @throws SchemaException
* if there is any error loading P2J schema configuration data;
* if there is any error loading the AST from persistence.
*/
private void initialize(String schema, Aast root, boolean meta)
throws SchemaException
{
// map the Java package to the database (i.e., schema) name
if (meta)
{
metaPackage = (root.getAnnotation(PACKAGE) + "._meta").intern();
}
else
{
javaPackage = (root.getAnnotation(PACKAGE) + "." +
(schema == null ? SchemaDictionary.TEMP_TABLE_DB : root.getText())).intern();
}
// Walk the class and property nodes, mapping each fully qualified, historical schema name to an
// unqualified, converted Java name.
// The Java names of classes are capitalized; those of properties are not.
Aast nextClass = root.getImmediateChild(CLASS, null);
while (nextClass != null)
{
classByIDMap.put(nextClass.getId(), nextClass);
String ifaceName = nextClass.getText().intern();
// store the name in a set for quick tests later by isDMOInterface()
if (!javaNames.add(ifaceName))
{
LOG.log(Level.INFO,"Duplicate dmo interface name detected: [" + ifaceName + "]");
}
StringBuilder buf = new StringBuilder();
if (schema != null)
{
buf.append(schema).append(".");
}
buf.append(nextClass.getAnnotation(HISTORICAL));
String histTable = buf.toString().intern();
String histTableNorm = histTable.toLowerCase();
addJavaName(histTable, histTableNorm, ifaceName, null, null);
if (nextClass.isAnnotation("multi-tenant"))
{
multiTenant.put(histTableNorm, nextClass.isAnnotation("no-default-area") ? 1 : 2);
}
// save the super-interface for later use
if (nextClass.isAnnotation("tt_interface"))
{
// only in case of temp tables, the super-interface will be used as the 'wider' parameter type
javaInterfaceMap.put(histTableNorm, (String) nextClass.getAnnotation("tt_interface"));
}
// store the meta tables, if the case
if (meta)
{
historicalMetaTables.add(histTableNorm);
}
// add to set of read-only tables if annotation is present
if (nextClass.isAnnotation(READ_ONLY) && (Boolean) nextClass.getAnnotation(READ_ONLY))
{
readOnlyTables.add(histTableNorm);
}
boolean runtime = Configuration.isRuntimeConfig();
// retrieve first property in this class
Aast nextProp = nextClass.getImmediateChild(PROPERTY, null);
while (nextProp != null)
{
String histField = (String) nextProp.getAnnotation(HISTORICAL);
// compose fully qualified, legacy field name, normalized and interned
String legacyName = (histTable + '.' + histField).toLowerCase().intern();
// map legacy field name to Java property name
if (!nextProp.isAnnotation(SYNTHETIC))
{
// add field to mutable field map
if (!runtime && !meta)
{
mutableFieldMap.add(histTableNorm, histField.toLowerCase());
}
if (nextProp.isAnnotation("customextent"))
{
denormalizedProperties.add(legacyName);
long index = (Long)nextProp.getAnnotation("customextent");
String denormalizedPropertyName =
(String) nextProp.getAnnotation("denormalizedpropertyname");
addJavaName(legacyName, legacyName, nextProp.getText(), index, denormalizedPropertyName);
}
else
{
addJavaName(legacyName, legacyName, nextProp.getText(), null, null);
}
}
// add to set of validatable properties if annotation is present
if (nextProp.isAnnotation(VALIDATABLE) &&
(Boolean) nextProp.getAnnotation(VALIDATABLE))
{
validatables.add(legacyName);
}
if (nextProp.isAnnotation(NON_NULLABLE ) && (Boolean) nextProp.getAnnotation(NON_NULLABLE))
{
nonnullables.add(legacyName);
}
// store property AST by ID for reference later during initialization
propsMap.put(nextProp.getId(), nextProp);
// Retrieve next property in this class.
nextProp = nextClass.getImmediateChild(PROPERTY, nextProp);
}
// process indexes
initializeIndexes(histTable, nextClass, runtime, meta);
// process relations
initializeRelations(histTable, nextClass);
// retrieve next class in this schema
nextClass = root.getImmediateChild(CLASS, nextClass);
}
}
/**
* Adds property name to javaNameMap where the key is legacy field name and value is map
* based on nullable index. Index is not null only for denormalized field with extent, in
* this case index corresponds to denormalization table hint.
*
* @param originalName
* Legacy field name (not necessarily normalized).
* @param normalizedName
* Legacy field name (mandatory normalized and interned).
* @param propertyName
* Property name.
* @param index
* Null-based index of custom property name for denormalized field with extent, otherwise null.
* @param denormalizedPropertyName
* Denormalized property name for denormalized field with extent, otherwise null.
*/
private void addJavaName(String originalName,
String normalizedName,
String propertyName,
Long index,
String denormalizedPropertyName)
{
propertyName = propertyName.intern();
Map<Long, String> customProperties;
if (javaNameMap.containsKey(normalizedName))
{
customProperties = javaNameMap.get(normalizedName);
}
else
{
customProperties = new HashMap<>();
if (index != null && denormalizedPropertyName != null)
{
customProperties.put(null, denormalizedPropertyName.intern());
}
javaNameMap.put(normalizedName, customProperties);
}
customProperties.put(index, propertyName);
// save the original name for later access only if different than the normalized
if (!normalizedName.equals(originalName))
{
originalNamesMap.put(normalizedName, originalName.intern());
}
}
/**
* Compose a list of index descriptors for the index children of the
* given P2O class node and map it to the given, legacy table name.
*
* @param historical
* Fully qualified, legacy table name (not necessarily normalized).
* @param classAst
* P2O AST node describing a DMO; the direct parent of all P2O index nodes.
* @param runtime
* {@code true} if this is a runtime configuration; {@code false} if this is a static
* conversion configuration.
* @param meta
* Flags the schema as metadata information.
*/
private void initializeIndexes(String historical, Aast classAst, boolean runtime, boolean meta)
{
String table = historical.toLowerCase().intern();
List<P2JIndex> indexList = new ArrayList<>();
Aast nextIndex = classAst.getImmediateChild(INDEX, null);
while (nextIndex != null)
{
if (!nextIndex.isAnnotation(SYNTHETIC))
{
String indexName = ((String) nextIndex.getAnnotation(HISTORICAL)).intern();
P2JIndex indexDef = new P2JIndex(
(String) classAst.getAnnotation(TABLE),
indexName,
nextIndex.isAnnotation(UNIQUE),
nextIndex.isAnnotation(PRIMARY),
nextIndex.isAnnotation(WORD));
indexList.add(indexDef);
if (!runtime && !meta)
{
mutableIndexMap.add(table, indexName);
}
// Retrieve first component in the index.
Aast nextCol = nextIndex.getImmediateChild(INDEX_COL, null);
while (nextCol != null)
{
// Retrieve the property referenced by the index column.
Long refid = (Long) nextCol.getAnnotation(REFID);
Aast propAst = propsMap.get(refid);
// Add a component to the index descriptor.
String idxFldName = historical + '.' + propAst.getAnnotation(HISTORICAL);
indexDef.addComponent(
idxFldName.intern(), null, null,
nextCol.isAnnotation(DESCEND),
"character".equals(propAst.getAnnotation(DATATYPE)),
nextCol.isAnnotation(IGNORE_CASE),
nextCol.isAnnotation(ABBREVIATED),
nextCol.isAnnotation(NON_NULLABLE));
// Retrieve next component in the index.
nextCol = nextIndex.getImmediateChild(INDEX_COL, nextCol);
}
}
// Retrieve next index in this class.
nextIndex = classAst.getImmediateChild(INDEX, nextIndex);
}
indexMap.put(table, indexList);
}
/**
* Compose and store the set of tables which refer to a database table via
* a foreign key, given the referent table's P2O class node and its legacy
* name. The referring tables are actually stored as a set AST IDs, which
* is mapped it to the given, legacy table name.
*
* @param historical
* Fully qualified, legacy table name (may not be normalized).
* @param classAst
* P2O AST node describing a DMO; the direct parent of all P2O relation nodes.
*/
private void initializeRelations(String historical, Aast classAst)
{
historical = historical.toLowerCase().intern();
Aast nextRel = null;
while ((nextRel = classAst.getImmediateChild(types, nextRel)) != null)
{
boolean referent = false;
switch (nextRel.getType())
{
case ONE_TO_MANY:
referent = true;
break;
case ONE_TO_ONE:
referent = !(Boolean) nextRel.getAnnotation(FOREIGN);
break;
}
if (referent)
{
// Add AST ID to set of referring tables for this referent table.
Set<Long> referrers = foreignReferenceMap.get(historical);
if (referrers == null)
{
referrers = new LinkedHashSet<>();
foreignReferenceMap.put(historical, referrers);
}
referrers.add((Long) nextRel.getAnnotation(REFERENCES));
}
}
}
/**
* Load the persisted, P2O AST associated with the specified database
* schema.
*
* @param database
* Logical name of database schema associated with the P2O
* AST to be loaded.
*
* @return P2O AST associated with <code>database</code>.
*
* @throws ConfigurationException
* if there is any error loading P2J schema configuration data.
* @throws AstException
* if there is any error loading the AST from persistence.
*/
private Aast loadAst(String database)
throws ConfigurationException,
AstException
{
SchemaConfig config = Configuration.getSchemaConfig();
boolean runtime = Configuration.isRuntimeConfig();
String path;
if (runtime)
{
path = config.getSchemaResource(database, P2O_POSTFIX);
if (path == null)
{
throw new ConfigurationException("Cannot find " + P2O_POSTFIX + " for database " + database);
}
}
else
{
File file = config.getSchemaFile(database, P2O_POSTFIX);
path = Configuration.normalizeFilename(file.getAbsolutePath());
}
return AstManager.get().loadTree(path);
}
/**
* Dump an exhaustive tree of nested table dependencies. A dependency is
* a foreign key relation from the dependent table to the "parent" table.
* The tree is printed to stdout. Each indent indicates a dependency to
* the next outer level.
*
* @param historical
* Fully qualified, legacy Progress table name.
* @param indent
* Level of indent to apply when printing this table.
*/
private void dumpTableDependencies(String historical, int indent)
{
StringBuilder buf = new StringBuilder();
StringHelper.repeatChar(' ', indent * 3, buf);
buf.append(historical);
buf.append(" (");
buf.append(getJavaName(historical));
buf.append(")");
LOG.log(Level.INFO, buf.toString());
Set<Long> referrers = foreignReferenceMap.get(historical);
if (referrers != null)
{
for (Long id : referrers)
{
Aast classAst = classByIDMap.get(id);
if (classAst != null)
{
StringBuilder hstBuf = new StringBuilder();
hstBuf.append(classAst.getParent().getText());
hstBuf.append(".");
String hist = (String) classAst.getAnnotation(HISTORICAL);
hstBuf.append(hist);
dumpTableDependencies(hstBuf.toString(), indent + 1);
}
}
}
}
/** Container with user-specific data. */
private static class WorkArea
{
/** Current temp-table <code>P2OLookup</code> instance */
private P2OLookup tempP2OLookup = null;
/** Set of all known schema names from P2J conversion configuration */
private Set<String> schemaNames = null;
/** Name of last temp-table P2O AST file to be loaded */
private String lastTempFilename = null;
}
/**
* Test harness for property name lookup feature. Expects no arguments.
*
* @param args
* Not used.
*/
public static void main(String[] args)
{
try
{
BufferedReader in = new BufferedReader(
new InputStreamReader(System.in));
while (true)
{
LOG.log(Level.INFO, "Enter test data or ':q' to quit: ");
String input = in.readLine();
if (":q".equalsIgnoreCase(input))
{
LOG.log(Level.INFO, "Goodbye.");
break;
}
try
{
// LOG.info("Qualified java property name: " +
// P2OLookup.javaPropertyName(input, true));
// LOG.info("Legacy field names: " +
// P2OLookup.legacyFieldNames(input));
// LOG.infon("Is DMO interface ---> " +
// P2OLookup.isDMOInterface(input));
// LOG.info("Is referenced by foreign key ---> " +
// P2OLookup.isForeignReference(input));
LOG.log(Level.INFO, "Table Dependencies:");
P2OLookup.dumpTableDependencies(input.toLowerCase());
}
catch (SchemaException exc)
{
LOG.log(Level.SEVERE, "", exc);
}
}
}
catch (Exception exc)
{
LOG.log(Level.SEVERE, "", exc);
}
}
}