SchemaDictionary.java
/*
** Module : SchemaDictionary.java
** Abstract : schema namespace dictionary
**
** Copyright (c) 2004-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20041216 @19178 Created initial version.
** 002 ECF 20050103 @19191 Changed configuration name from 'schema' to
** 'schema-dict'.
** 003 ECF 20050103 @19193 Modified isXXXX methods to return false in
** the event of an ambiguous search, rather than
** throw an exception.
** 004 ECF 20050118 @19337 Implemented updated support for creates and
** deletes of new databases, tables, and fields,
** and for promotion of a table's search
** precedence to a higher scope. New and changed
** public methods:
** - addDatabaseEntry
** - removeDatabaseEntry
** - addTableEntry
** - removeTableEntry
** - addFieldEntry
** - removeFieldEntry
** - addFieldEntries
** - promoteTable
** - dump (no-arg)
** - dump (takes PrintStream)
** Updated test harness to include a dump ("U")
** command to create a debug dump file in the
** current directory (schema.dmp).
** 005 ECF 20050128 @19458 Overloaded promoteTable method. The new
** version accepts a flag to force a refresh of
** the promoted table in the top scope if it
** already exists there.
** 006 ECF 20050203 @19564 Fixed a problem in addEntries to ensure the
** "from" node found is not the same as the "to"
** node specified. This defect was causing temp
** tables created with an unqualified name which
** matched a schema table name, to fail to add
** field name nodes to the temp table's scope.
** 007 ECF 20050203 @19565 Modified search algorithm to find schema
** entries. This change was made to allow us to
** mimic the actual symbol resolution performed
* by Progress. Progress looks first for an
** exact match in all scopes. Only if this fails
** are all scopes searched again for an
** abbreviated match. Added a parameter to the
** findNode method of inner class Scope to
** specify whether search must find an exact
** match or whether an abbreviated match is
** permitted.
** 008 ECF 20050203 @19567 Modified isDatabase, isTable, isField methods
** to eat exceptions caused by malformed entity
** names (IllegalArgumentException thrown by
** EntityName class upon construction).
** 009 ECF 20050207 @19626 Changes to entity promotion algorithm to
** remove certain cases of ambiguity left behind
** after #007. The promoteTable methods were
** refactored to use the more generic
** promoteEntry method. Promotion now involves
** copying NameNode's from lower to higher
** scopes by reference rather than by value.
** The promoteTable(String, boolean) method is
** no longer necessary and has been removed.
** 010 ECF 20050207 @19641 Made findEntry(String, int) package private
** to permit NamespaceLoader to call it for a
** database name lookup when loading schema data
** and metadata.
** 011 ECF 20050209 @19732 Added special handling for record entries,
** since different record types (table, buffer,
** temp table, and work table) each are treated
** very differently in terms of scopes to which
** they may be added, and whether references to
** them in Progress source code may be
** abbreviated. Also, partially reverted search
** algorithm to pre-007 behavior based on
** additional testing against live code.
** 012 ECF 20050209 @19741 Refined the add and search rules for buffer
** record entries. Buffers can only be present
** in two scope levels in Progress: that of an
** external procedure, or that of an internal
** procedure or function. The table add- and
** find-related methods now implement this
** requirement. Modified scope lookup as well.
** Changed scopeMap to use weak references
** within a WeakHashMap to allow stale mappings
** to be garbage collected. Removed all "remove"
** methods for individual entities, as it was
** decided this functionality was not needed,
** and its implementation would otherwise have
** complicated this change.
** 013 ECF 20050210 @19743 Added database alias support. Added the
** createAlias method and a constructor which
** does not load default schemas. Modified
** Scope.findNode to substitute database name
** for alias during a lookup. Modified
** getQualifiedName implementation to correctly
** qualify entity names which use aliases.
** 014 ECF 20050210 @19753 Fixed regression in addEntries(String,
** NameNode, int). A temp-table with the same
** name as a real table was trying to copy its
** own fields to itself.
** 015 ECF 20050210 @19754 Handle special case when copying entries from
** one parent node to another: if a buffer is
** created from an unqualified name which is
** common between a temp-table and a real table,
** the buffer must alias the real table and not
** the temp-table. In our implementation, this
** translates to copying the set of field
** entries from the real table's namespace
** (rather than the temp-table's namespace) into
** the buffer's scope's global table namespace.
** 016 ECF 20050211 @19775 Yet another rewrite of the search algorithm.
** The complex findTableEntry method added in
** #011 (@19732) has been removed in favor of a
** simpler approach implemented at the Scope
** level. This is made possible by changes to
** Scope (it is now aware of its context within
** the stack), and promoteEntry, which now
** copies only the leaf name nodes (representing
** fields) to the highest scope. Previously,
** parent nodes were copied as well, which
** caused lookup collisions at the table
** qualifier level.
** 017 ECF 20050211 @19780 Fixed defect in promoteEntry. Code was not
** looking up a source record correctly when it
** was a buffer in a function.
** 018 ECF 20050214 @19783 Modified scope processing to expect four
** standard scope levels. From outermost to
** innermost:
** 0. SCHEMA_GLOBAL_SCOPE
** Assigned to schema data loaded from
** persistence; currently, new records of
** type table are not added by the parser,
** but if they were, they would be added to
** this level.
** 1. USER_GLOBAL_SCOPE
** Assigned to temp-table and work-table
** data.
** 2. EXT_PROC_SCOPE
** Associated with an external procedure's
** default scope. Buffers created anywhere in
** an external procedure are represented
** here.
** 3. INT_PROC_SCOPE
** Associated with the scope of an internal
** procedure or function. Buffers created
** anywhere in an internal procedure or a
** function are represented here.
** 019 ECF 20050214 @19785 Modified addEntry to add special handling for
** buffer fields. A field entry is now only
** added to the global namespace of a scope if
** its parent is not a buffer record. This fixes
** an edge case where an unqualified field
** lookup is ambiguous because a buffer field of
** the same name was present. The buffer fields
** can thus only be found during a lookup if the
** enclosing buffer previously has been
** promoted.
** 020 ECF 20050216 @19837 Added support for qualified buffer name
** lookups. This involved creating a fake name
** node (a shadow) to represent the buffer's
** parent database, and storing it in the
** global database namespace of the buffer's
** scope.
** 021 ECF 20050217 @19840 Fixed defect in addEntries. First search for
** "from" node was only looking in bottom two
** scopes. Needs to look in all scopes. This was
** preventing the definition of a buffer based
** upon another buffer.
** 022 ECF 20050308 @20247 Modified getQualifiedName to correctly handle
** temp- and work-table names. These do not have
** database qualifiers, which broke the previous
** code in NameNode.toAliasedName.
** 023 ECF 20050322 @20600 Added functionality to load and expose more
** schema data. The hierarchy of Progress schema
** objects (e.g., ProgressSchema, etc.) is now
** used internally to manage table indices and
** table and field options. Added methods to
** store, access, and persist these new objects.
** The temp-table database is now persisted for
** each source file.
** 024 ECF 20050413 @20713 Replaced ProgressEntity-based internal data
** representation with an AST-based mechanism,
** based on the output of a new SchemaParser.
** This approach is much more compatible with
** the Progress parser and symbol resolver. This
** entailed a rewrite of significant portions of
** this class. Also enabled sharing a single
** instance of this class across multiple source
** code parsing runs, which significantly cuts
** down the time required for all but the first
** run in a series.
** 025 ECF 20050415 @20846 Refactored constructors to accommodate
** loading a primary database by name. Fixed
** buffer alias defect which caused improper
** name to be returned by getQualifiedName.
** 026 ECF 20050502 @20988 Fix for loading an AST. Logic was altering an
** AST during a tree walk, causing an infinite
** loop inside antlr.BaseAST. Changed to use a
** copy of the AST instead of the original.
** 027 ECF 20050603 @21403 Fixed a defect in the addIndex method. This
** method was creating 2 layers of INDEX AST
** nodes by grafting an AST node to the wrong
** parent.
** 028 ECF 20050623 @21605 Added getIndex method variant. Convenience
** version to find index based on table name and
** index name.
** 029 ECF 20050817 @22153 Added methods getTableRecordType and
** getFieldRecordType. They accept the name of
** a table or field, respectively, and report
** back the token type of the associated record.
** For buffers, the type of the backing record
** is returned instead of BUFFER.
** 030 ECF 20050922 @22272 Added getBufferForTable, getBufferForField.
** These methods get the record (buffer) name
** for a raw record or field name, respectively,
** as found in Progress source code. The name
** returned is always unabbreviated and is
** qualified by database only if the input was
** also so qualified.
** 031 ECF 20050928 @22896 Added getLogicalDatabaseForXXX() methods.
** These are used by the parser to retrieve the
** logical database name, given a table or field
** name.
** 032 ECF 20050930 @22921 Modified getQualifiedName method. Substitutes
** backing table name for buffer name in all
** cases, not just when requesting full table
** name.
** 033 ECF 20051005 @22977 Fixed NPE in getLogicalDatabaseForXXX()
** methods. If the name passed in represented a
** temp- or work-table (or a field contained
** within such a table), the database name node
** would always be null. Dereferencing the node
** to get its name was causing the error.
** 034 ECF 20060202 @24254 Assign a standard database name to the P2Os
** persisted for temp/work-table schema ASTs.
** This name determines the package in which DMO
** classes are created. It must match the temp
** table database used to support temp tables at
** runtime.
** 035 ECF 20060226 @24817 Added support for field-level validation
** conversion. Added methods to retrieve field
** properties. Modified getRecordName method to
** aggressively prepend a database qualifier.
** 036 ECF 20060407 @25424 Always lowercase database qualifier before
** returning record name from getRecordName().
** 037 ECF 20060407 @25425 Update to resetState(). Remove all aliases.
** 038 ECF 20060407 @25426 Fixed defect in copying field nodes. Child
** AST nodes were not being copied correctly.
** 039 ECF 20060426 @25737 Enhanced support for adding a field like an
** existing field. If the source field is an
** extent, it is possible to make a like field
** which is based upon an individual element of
** the source field.
** 040 ECF 20060427 @25822 Further improvements to LIKE field support.
** Extended changes in #039 (@25737) to be more
** generic.
** 041 GES 20060912 @29486 Split copyProperties into field and table
** versions. The field version overrides props
** in the target AST if they already exist (it
** previously just added to the props). Special
** processing for case sensitivity was also
** added.
** 042 ECF 20061020 @30587 Fixed LIKE processing for temp/work-table
** fields. If defined LIKE a permanent table's
** field, a temp/work-table field is annotated
** as verbose. This allows verbose conversion of
** field names downstream.
** 043 ECF 20061025 @30651 Refined fix for LIKE processing for temp/work
** table fields. Fields of a temp table which is
** like another temp table which is like a
** permanent table were not properly being
** annotated as verbose. The verbose annotation
** is now inherited properly, regardless of the
** number of levels of indirection.
** 044 ECF 20061126 @31400 New annotation for temp/work-tables defined
** LIKE other tables. The 'model' annotation
** records the schema name of the table after
** which the table was modelled. This will be
** the original table, in the event multiple
** generations of LIKE phrases separate a temp
** table from its original template. This is
** used downstream to associate the appropriate
** table hints, if any, with a temp table.
** 045 ECF 20070221 @32211 Fixed defect when defining a buffer for a
** temp-table with the same name. The algorithm
** implemented in addEntries() to recover from
** a same-name condition only handled the case
** where the source table was a permanent table,
** not a temp/work-table.
** 046 GES 20070329 @32653 Moved a method into the uast package to make
** it a common utility routine.
** 047 GES 20070330 @32697 Force permanent databases (and contents)
** to be loaded into the schema global scope.
** This is needed to ensure that the lookup of
** abbreviated names can be handled since
** abbreviations are only processed for
** namespaces in the schema global scope.
** 048 GES 20070402 @32705 Added database load option to the test
** harness. Made scope copy constructor make
** a deeper copy to ensure that modifications
** are dropped between runs.
** 049 GES 20070404 @32791 Since internal procedures, user-defined
** functions and triggers can be nested, there
** cannot be a hard coded scope to treat as the
** internal procedure scope. In particular, all
** 3 types can be nested inside inner blocks,
** triggers can be nested inside all other
** types and triggers can even be multiply
** nested. This removes the INT_PROC_SCOPE and
** replaces it with a calculation of the nearest
** scope that is marked as internal.
** 050 GES 20070406 @32829 Added safety code to allow a temp-table like
** another temp-table (when there are no
** properties in the source table).
** 051 ECF 20070410 @32922 Modified getAllFieldProperties() to return a
** Map instead of a List. This makes it easier
** to correlate multiple sets of field-level
** properties from the same table to the proper
** fields.
** 052 GES 20070418 @33114 Added support for COM-HANDLE type.
** 053 GES 20070629 @34340 Removed terse flag from AST persistence so
** that we retail line/column info in our ASTs.
** Added the "srcfile" annotation to the
** database node in the AST so that the original
** .df filename can easily be found later.
** Reworked APIs and logic for temp-table
** creation to use ASTs from the source file
** to set the line and column numbers in the
** generated AST nodes. This allows them to be
** cross-referenced back to the defining source
** code.
** 054 GES 20070723 @34642 Force those field and index definitions that
** are copied from another table to have the
** same line and column numbers as the temp
** table definition node. Force the line/col
** numbers for more deeply nested nodes in other
** "like" situations.
** 055 GES 20070907 @35015 Support for 10.1B data types.
** 056 ECF 20070911 @35052 Fixed addEntries(). Was not considering table
** type of WORK_TABLE.
** 057 GES 20070911 @35142 Refactored into an instance approach instead
** of the singleton approach previously used.
** This handles the majority of the data as
** read-only and shared across all instances.
** A given instance of this class is still NOT
** thread-safe, however different instances can
** now be simultaneously used, each on a
** different thread, without problems.
** 058 GES 20070926 @35258 Once a table is promoted, don't ever promote
** it again. Propagate namespace contents from
** a removed scope into its enclosing scope.
** 059 GES 20071106 @35894 Propagation may cause ambiguity problems
** when buffer nodes are created in nested
** (internal) scopes and then the same buffer
** name is created again in the external scope.
** In this case, nodes with the same name are
** removed from a scope before a buffer node
** is added. Added safety code to avoid NPE
** in copyIndex().
** 060 GES 20071204 @36190 Fixed typo that always forced promotion.
** Disable propagation of buffer nodes across
** internal scope boundaries. Also disabled
** propagation of any other nodes that are
** "bound" to buffers that are explicitly
** defined inside an internal scope. Together
** these actions make the propagation behavior
** closer to the 4GL.
** 061 GES 20071207 @36285 Promotion is temporarily reset inside
** internal scopes and then restored when the
** internal scope pops. This means that things
** that have already been promoted at the
** external procedure level will promote again
** in an internal procedure. Weak scopes that
** have buffers associated (such that the
** buffer is a weak reference) will force any
** references to that buffer inside that weak
** scope to not be propagated.
** 062 ECF 20081126 @40696 Keep track of explicit database qualifiers
** for promoted buffers. These may be needed for
** unqualified references to the same buffers in
** the current or a more deeply nested scope.
** This prevents the instantiation of separate
** buffers for such unqualified references.
** 063 ECF 20081216 @40936 Exposed some private constants. Changed these
** to package private access.
** 064 GES 20090421 @41832 Matched utility class package change.
** 065 GES 20090429 @42052 Match package and class name changes.
** 066 GES 20090515 @42221 Moved to AstManager from AstRegistry/AstPersister.
** 067 GES 20090518 @42401 Moved filename normalization here from brainwash().
** 068 ECF 20130225 Fixed addEntries, which was not dealing with some buffer-related
** edge cases properly.
** 069 ECF 20130302 Fixed bug during initial loading. Duplicate field and table names
** added to the schema global scope were replacing nodes whose
** unqualified names were the same. This replacement is only supposed to
** happen during promotion, in case the same table is promoted multiple
** times, not during the initial load, when all fields must be added to
** the schema global scope.
** 070 CA 20130304 Fixed H069, to include temp-tables too. Fixed promoteEntry, to
** include all the record types when checking for a weak reference.
** The promoted tracking must be done by each scope, and not just by the
** the scope for top level blocks.
** 071 CA 20130307 Fixed abbreviated field name disambiguation - the tables which are
** weak reference in the current scope have precedence over others.
** 072 CA 20130307 Fixed honoring of VALIDATE clause for temp-table fields defined using
** the LIKE clause and tables defined using the LIKE clause.
** 073 CA 20130524 When temp-tables are defined, their fields need to be added proper
** ORDER clause, to reflect their actual position at the temp-table
** definition.
** 074 CA 20131024 Inferred generics. Allow each context to have their own instance.
** 075 CA 20140114 Save the legacy table name in an annotation, for later use.
** 076 ECF 20141008 Remove MANDATORY and KW_FLD_TRG nodes from the target temp-table
** field or variable defined using LIKE.
** 077 ECF 20150103 When defining a temp-table like an existing table, and there exist
** both a temp-table and a persistent table with the same name as the
** source/from table, use the persistent table in preference to the
** temp-table.
** 078 ECF 20150104 Implemented support for implicit DICTDB alias. Ambiguous matches on
** table and field names without a database qualifier are now retried
** using this alias as a database qualifier.
** 079 VMN 20150215 Propagate the schemaname of the original source of the field
** defined with LIKE clause.
** 080 OM 20150824 Copied TEMP-TABLE specific annotations to new data ASTs.
** 081 EVL 20160224 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 082 ECF 20160313 Refactored some methods to allow table data copies across schema
** dictionaries. Added API to remove all name nodes from dictionary,
** except for a subset provided by the caller. Added getFieldInfo and
** performed related refactoring.
** 083 ECF 20170303 Honor FOR TEMP-TABLE clause in DEFINE BUFFER/PARAMETER statements.
** 084 ECF 20170826 Added support for various field options.
** 085 CA 20180404 CAN-FIND allows field abbreviations to be resolved even if ambiguous:
** it will return the first field which matches the prefix.
** 086 CA 20180410 Unqualified fields referred in BY sort order clauses will look up
** with precedence the current table: if BY clauses are just fields and
** they match an index in the current table, then that will be used.
** CA 20180510 Added temp-table options: LABEL, XML-NODE-NAME, NAMESPACE-URI,
** NAMESPACE-PREFIX and SERIALIZE-NAME.
** CA 20180526 USE-INDEX prim-idx for LIKE clause can be overridden by an explicit
** PRIMARY index.
** 087 CA 20180621 Track temp-table fields defined as LIKE via a 'like' annotation with
** the source field name. This is required to properly parse any
** VALEXP.
** 088 ECF 20181116 Improved heap utilization by caching all database ASTs loaded, even
** if they are not loaded by default.
** 089 CA 20181213 Fixed DEF BUFFER FOR [TEMP-TABLE] - TEMP-TABLE forces a temp-table,
** and without it a persistent table must have precedence over a temp
** table.
** 090 ECF 20190620 EmptyIterator API change.
** 091 CA 20190720 Load implicit default databases - these databases are for non-default
** schemas in p2j.cfg.xml, so that they will not be loaded during normal
** conversion unless a hint is specified. We emulate this in the
** directory, in the 'default-databases' section, where the databases as
** specified for each Java package prefix.
** OM 20190723 Preserve camel-casing of historical names for fields, indexes and
** tables.
** CA 20190817 Load implicit default databases - these databases are for non-default
** schemas in p2j.cfg.xml, so that they will not be loaded during normal
** conversion unless a hint is specified. We emulate this in the
** directory, in the 'default-databases' section, where the databases
** as specified for each Java package prefix.
** ECF 20190827 Fixed runtime SchemaDictionary initialization in the event
** "default-databases" is configured.
** OM 20190829 Manufactured BEFORE-TABLE specific HIDDEN fields and index.
** 092 VVT 20190829 WORK-TABLE must never have indices (see #4103 and #3379 for the
** explanation).
** 093 EVL 20190928 Fix for NPE in finalizeTableDefine for incoming null ast.
** 094 GES 20190518 Add disambiguation if only one match is strongly scoped to the
** current scope.
** ECF 20190718 Fixed table promotion. The field nodes were being promoted, but not
** the table node itself.
** ECF 20191013 Allow only one buffer of a given name to be defined at a time. This
** already was enforced naturally in normal mode, but it was possible to
** create duplicate buffer definitions for the same buffer name in OO
** pre-scan mode. Only the latest definition is preserved.
** 095 ECF 20200301 Added APIs needed by SymbolResolver, which expose a table's NameNode
** and which allow more specific NameNode lookup algorithms for an
** unqualified field name if its table's private namespace is known.
** GES 20200302 Changed getFieldInfo() to findFieldInfo() and return null if there
** is no match found.
** 096 CA 20200514 Added support for SOAP web services (getAllFields(tableName), to build the WSDL
** type for table or dataset request/response messages).
** 097 IAS 20200601 Added support for the tables/fields string attributes (*-SA properties).
** 098 CA 20200714 The reserved properties for a before-table must have their hard-coded field ID
** as set at the ReservedProperty and also must be added before any other fields, at
** the table definition.
** 099 ECF 20200825 Once a buffer is promoted for a block to which it is strongly scoped, do not
** permit it to be resolved outside that block.
** GES 20200826 Disabled the strong scope removal.
** ECF 20200910 When promoting a defined buffer to a block to which it is strongly scoped, do
** not allow its promoted node or its fields' promoted nodes to propagate out of
** that strong scope.
** 100 CA 20200924 Fixed a leak in 'databases' map for dynamic conversion.
** Added a cache of Scope instances - this is used for dynamic query conversion, to
** not re-compute the temp-table scope each time the same list of temp-table buffers
** is used.
** OM 20201001 Improved DMO manipulation performance by caching slow Property annotation access.
** CA 20201011 Scope.promoted changed to an identity hash set.
** CA 20201015 Replaced java.util.Stack with a non-synchronized custom implementation.
** AIL 20210309 Clean local schema dictionary instances.
** OM 20210908 Made TEMP_TABLE_DB public. Added META_DB public constant. Marked imported .dict
** files with dedicated flag.
** VVT 20210917 Javadoc fixed, missing @Override annotations added.
** HC 20211001 Implementation of i18n support.
** OM 20211122 Added hints-based support for 'unloading' schema. A bit of code maintenance.
** OM 20211020 Added _DATASOURCE_ROWID property.
** ECF 20220321 Resolve ambiguity for unqualified, metadata tables or fields, in a multi-database
** configuration by searching only within the last schema loaded. Format cleanup.
** ECF 20220325 Support a mode where an unqualified field lookup will be found in the default
** buffer of a table, rather than in any explicitly defined buffer for that table.
** ECF 20220405 Addressed flaw in previous fix to resolve ambiguity for unqualified metadata
** table/field references in multi-database configurations.
** OM 20220727 FieldId and PropertyId are different for denormalized extent fields.
** TJD 20220504 Java 11 compatibility minor changes
** CA 20220501 Each profile has the same structure as the main 'global' config. Allow multiple
** profiles to be ran at once, with the conversion switching the state between
** profiles, when a resource (like a file or namespace) is being processed. Only
** front phase is supported at this time.
** GES 20210208 Added table-level signature calculation (saved as an annotation). Added
** findTableInfo() to provide a set of related data about a matched record. This
** saves multiple calls each having to resolve the same name node lookup.
** CA 20220531 During parsing, the temp-tables are gathered from the entire hierarchy of a class
** and loaded in the SchemaDictionary. But these must not be persisted to the
** current class' .dict file!
** CA 20220707 'findTableInfo' must return the database alias (if used), and not the database
** schema name for that alias.
** CA 20220727 Improved memory management for parsing; cleanup is done in two phases:
** 1. after each legacy class file has finished parsing, the SchemaDictionary will
** keep only protected temp-tables (all private tables are removed, as these
** can't be reached from sub-classes; all permanent tables are removed, as each
** class has its own reference to the permanent tables). Also, the class def
** instance will reduce its own used memory, which is not required when parsing
** sub-classes.
** 2. after parsing of the entire file set is finished, any SchemaDictionary or
** other ASTs referenced by the ClassDefinition are released.
** CA 20220728 Fixed CA/20220727 - 'ClassDefinition.schemaDict' must preserve the global
** namespace, to allow buffer definitions for permanent tables to be resolved from
** sub-classes.
** TJD 20230220 Implement isExactMatchField to allow checking exact field names
** CA 20230110 Let 'getImmediateChild' work with a set, if an array is used for the types.
** 101 CA 20230321 A TEMP-TABLE's signature must include index information (this includes the index
** name, options, field position and type).
** 102 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 103 OM 20230115 Replaced absolutePath(), relativePath(), upPath() and downPath() with faster
** versions, based on node types rather on string paths.
** 104 CA 20230619 In ECF/20220321, the ambiguity for unqualified, metadata tables or fields, in a
** multi-database configuration, must use the first connected database (configured
** as first in p2j.cfg.xml namespace). This is required because the 4GL compiler
** fixes the database for such unqualified meta tables at compile time, when
** generating the .r file, and this can not be changed by runtime. In previous fix,
** CA may have compiled the .r file from the Procedure Editor instead of command
** line.
** 105 CA 20230722 Check for empty schemata in 'localInstance'.
** 106 OM 20240318 Special handling of tables in mutable permanent databases.
** 107 OM 20240410 The the information on mutable databases are mergeFromAst() into global scope.
** 108 SP 20240729 If the field is unqualified and promoted names are more than 1, don't limit
** the lookup to a specific table, use the normal node search algorithm instead
** 109 RNC 20241030 Added null check in getTableProperty(String, int).
** 110 CA 20241104 'getFirstDatabase' must check 'bannedSchemas'.
*/
/*
** 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.lang.ref.*;
import java.util.*;
import java.util.logging.*;
import com.goldencode.ast.*;
import com.goldencode.p2j.cfg.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.orm.ReservedProperty;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.uast.*;
import com.goldencode.util.*;
import com.goldencode.util.Stack;
import com.goldencode.p2j.util.*;
/**
* A dictionary used by {@link com.goldencode.p2j.uast.SymbolResolver} to
* perform lookups of database entity names. Defines an API to load one or
* more Progress schemas into the primary lookup scope, to add arbitrary
* database, table, and field name hierarchies into additional lookup scopes,
* and to remove scopes in the reverse order of that in which they were added.
* Allows lookups of abbreviated table and field names, and of qualified and
* unqualified (see {@link Namespace namespace class description}) database,
* table, and field names.
* <p>
* Using qualified or unqualified names (abbreviated as well for tables and
* fields), callers can:
* <ul>
* <li>determine the existence of databases, tables, and fields;
* <li>query table types and field data types; and
* <li>convert unqualified table and field names to their fully qualified,
* unabbreviated representations.
* </ul>
* All lookups are performed from innermost (most recently added) to outermost
* (least recently added) scope. Any match immediately terminates the lookup
* and remaining, unsearched scopes are ignored.
* <p>
* A set of default database schemas, as defined by the schema configuration,
* is loaded into the dictionary upon construction. If a caller wishes to
* add additional schema information dynamically, a new scope should first
* be created with the {@link #addScope} method. This ensures the dynamically
* added schema information is easily removed when it goes out of scope, with
* a subsequent call to {@link #removeScope}.
* <p>
* The most recently added scope may be removed in its entirety via the
* {@link #removeScope} method. The outermost scopes -- the schema global and
* user global scopes -- are protected from removal. Individual entries may
* not be removed from the schema global scope. Namespaces for each
* non-protected scope are maintained by the {@link #promoteTable} method
* which adds name nodes to the namespaces of the top-most scope. This changes
* the lookup processing, making some normally ambiguous names unambiguous
* within the current scope's context. As scopes are removed, the namespace
* contents of the removed scope are propagated into the corresponding
* namespace of the enclosing scope. This duplicates Progress' behavior such
* that subsequent code in the enclosing block can be disambiguated too.
* <p>
* This dictionary manages requests to store and retrieve other schema
* information as well, such as field and table options and table indices.
* This data is stored internally as a set of ASTs rooted at the database
* level.
* <p>
* <strong>Known issues/limitations:</strong>
* <p>
* <ul>
* <li> Buffer definitions in 4GL do not have the effect of promotion
* but in this class there is no distinction. It is suspected that
* any "no reference" (in buffer scoping terms) would also cause no
* promotion. If this is the case, this is also an issue here. The
* current approach to limit this problem (which has been successful
* to date) has been to disable propagation of buffer nodes when
* internal scopes are popped. This behavior may itself be flawed in
* any scenario where the buffer would actually propagate in the 4GL.
* <li> Currently, all known cases of name resolution are handled with
* an implementation that uses a concept of promotion and propagation.
* It is possible that in Progress, the implementation is based on
* buffer scoping. Since buffer scoping is not handled at parsing
* time, if requirements are found that cause buffer scoping to be
* a requirement in order to properly resolve all names, then a major
* rewrite will be needed to both move buffer scoping here and to
* use it properly.
* <li> This class is not thread-safe. A fundamental assumption is that
* this class is provided solely to service the UAST symbol resolver,
* which is driven in a single thread by the Progress parser.
* </ul>
*/
public final class SchemaDictionary
implements SchemaParserTokenTypes
{
/** Logger. */
private static final ConversionStatus LOG = ConversionStatus.get(SchemaDictionary.class);
/** Indicates that a token type cannot be determined */
public static final int UNKNOWN = -1;
/** Temporary table database name which is persisted in ASTs */
public static final String TEMP_TABLE_DB = "_temp";
/** Meta database name which is persisted in ASTs. */
public static final String META_DB = "_meta";
/** Annotation name for a mutable database. The value is not important (always 'true') but its presence. */
public static final String MUTABLE_ANNOTATION = "mutable";
/** Reserved name of temp-table parent database */
static final String TEMP_DB = "@temp_db";
/** Reserved name of work-table parent database */
static final String WORK_DB = "@work_db";
/** Implicit DICTDB alias */
private static final String DICTDB = "dictdb";
/** Index of schema global scope in stack */
private static final int SCHEMA_GLOBAL_SCOPE = 0;
/** Index of user global scope in stack */
private static final int USER_GLOBAL_SCOPE = SCHEMA_GLOBAL_SCOPE + 1;
/** Index of external procedure scope */
private static final int EXT_PROC_SCOPE = SCHEMA_GLOBAL_SCOPE + 2;
/** Token types which represent permitted field properties for copy purposes */
private static final int[] FIELD_PROPS =
{
// these token types are used in a binary search, so they must be in ascending order by
// token number
KW_COL_L_SA, KW_HELP_SA, KW_SQL_WID, KW_VALEXP, KW_VALMSG, KW_VALMG_SA,
KW_CASE_SEN, KW_COL_LAB, KW_DECIMALS, KW_FORMAT, KW_HELP, KW_LABEL,
KW_NOT, KW_VIEW_AS,
KW_COL_CP, KW_DESCR, KW_EXTENT, KW_INIT, KW_LENGTH, KW_MAND,
KW_ORDER, KW_POS, KW_SERIALZH, KW_SERIALZN,
KW_XML_DTYP, KW_XML_NNAM, KW_XML_NTYP,
};
/** Token types which represent permitted table properties for copy purposes */
private static final int[] TABLE_PROPS =
{
// these token types are used in a binary search, so they must be in ascending order by
// token number
KW_LABEL_SA, KW_VALMSG, KW_VALMG_SA, KW_LABEL, KW_NAMESP_P, KW_NAMESP_U, KW_SERIALZN, KW_XML_NNAM
};
// Sort field properties token type array into ascending numeric order,
// so that it can be used in a binary search algorithm later.
static
{
Arrays.sort(FIELD_PROPS);
Arrays.sort(TABLE_PROPS);
}
/** List of namespaces by entity type. */
private static final int[] NS_LIST = new int[]
{
EntityName.DATABASE,
EntityName.TABLE,
EntityName.FIELD
};
/** List of all non-metadata field types */
private static final Set<Integer> FIELD_TYPE_LIST = new HashSet<>();
// Populate FIELD_TYPE_LIST.
static
{
for (int i = BEGIN_FIELDTYPES + 1; i < END_FIELDTYPES; i++)
{
FIELD_TYPE_LIST.add(i);
}
}
/** Context-local SchemaDictionary instance. */
private static final ContextLocal<SchemaDictionary> localDict = new ContextLocal<SchemaDictionary>()
{
@Override
protected SchemaDictionary initialValue()
{
try
{
return new SchemaDictionary((Set<String>) null);
}
catch (SchemaException e)
{
throw new RuntimeException(e);
}
}
};
/** Context-local SchemaDictionary instance of non-default databases. */
private static ContextLocal<Map<String, SchemaDictionary>> nonDefDict =
new ContextLocal<Map<String, SchemaDictionary>>()
{
@Override
protected Map<String, SchemaDictionary> initialValue()
{
return new HashMap<>();
}
};
/** Original unmodified schema global scope. */
private static Scope pristineScope = null;
/** Original unmodified map of database ASTs. */
private static Map<String, Aast> pristineDbs = null;
/** Original unmodified map of aliases to database names. */
private static Map<String, String> pristineAliases = null;
/** Original unmodified schema global scope for non-default cases. */
private static Map<String, Scope> nonDefPristineScope = new HashMap<>();
/** Original unmodified map of database ASTs for non-default cases. */
private static Map<String, Map<String, Aast>> nonDefPristineDbs = new HashMap<>();
/** Original unmodified map of aliases to database names for non-default cases. */
private static Map<String, Map<String, String>> nonDefPristineAliases = new HashMap<>();
/** Database ASTs, mapped by database name */
private Map<String, Aast> databases = new LinkedHashMap<>();
/** Map of database aliases to database names */
private Map<String, String> aliases = new HashMap<>();
/** Object which loads namespace data from persistent storage */
private SchemaLoader loader = null;
/** Stack of namespace scopes for contextual changes to dictionary */
private final Stack<Scope> scopes = new Stack<>();
/**
* The set of banned schemas. The {@code Namespace} will not recognize element from these schemas even is
* they are loaded.
*/
private final Set<String> bannedSchemas;
/** Number of protected scopes (i.e., those which cannot be removed) */
private int protectedScopeCount = 0;
/** When finding an unqualified field name node, prefer a default buffer to an explicitly defined buffer */
private boolean preferDefaultBuffer = false;
/**
* Map of weak references to scopes, indexed by name nodes. This can hold
* stale data as scope removal does not clear this map. Make sure the
* retrieved scope is still in use.
*/
private final Map<NameNode, WeakReference<Scope>> scopeMap = new WeakHashMap<>();
/** Map of promoted buffers to explicit database qualifiers */
private final ScopedDictionary<NameNode, String> qualifiers = new ScopedDictionary<>();
/** Table which defines state being copied in a temp-table "like" clause */
private Aast modelTable = null;
/**
* A flag indicating that all fields should be marked as preferred. Set before a record phrase
* parse.
*/
private boolean markPreferred = false;
/**
* A flag indicating that all fields should be associated with a strong buffer scope. Set
* before a record phrase parse.
*/
private boolean markStrongScope = false;
/**
* Flag which forces an unique field match when an abbreviation is used. If this is off, it
* will return the first matching field. Used by CAN-FIND statements.
*/
private boolean uniqueFieldMatch = true;
/**
* A lazy-populated map of longest unique table indexes (as in, no other index in this list
* is a prefix for another index).
*/
private final Map<Aast, Map<Aast, List<Aast>>> tableIndexes = new HashMap<>();
/** Cache of temp-table added scopes. */
private final Map<String, Scope> cachedScopes = new HashMap<>();
/**
* Create a schema dictionary based upon a single database schema. This
* is necessary during schema parsing when resolving inline Progress code
* for various schema constructs, such as view-as phrases and validation
* expressions. To parse this code, the Progress parser requires a symbol
* resolver which can resolve schema entity names. Thus, this constructor
* is called with an early version of the target database schema AST,
* such that the symbol resolver has enough information to resolve the
* table and field references within the inlined code.
*
* @param root
* The root node of an AST representing a database schema.
*/
SchemaDictionary(Aast root)
{
initialize(root);
bannedSchemas = null;
}
/**
* Create a schema dictionary based upon a single database schema. Uses
* the {@link SchemaLoader} to load the schema named <code>database</code>.
*
* @param database
* Name of logical database whose schema should be loaded.
*
* @throws SchemaException
* if any error occurs reading configuration information.
*/
SchemaDictionary(String database)
throws SchemaException
{
// Create loader.
loader = new SchemaLoader();
bannedSchemas = null;
// Initialize using AST for target database.
Aast root = loader.loadSchema(database);
initialize(root);
}
/**
* Get the context-local {@link SchemaDictionary} instance, configured to work with specified
* set of schemata.
* <p>
* Note that the first time this is invoked for a particular combination of schemata, the
* dictionary must be loaded with the corresponding ASTs, which can be slow. Subsequent
* requests with the same combination will get a cached instance.
* <p>
* TODO: share instances in a thread-safe way across contexts for better scalability.
*
* @param schemata
* An alphabetically sorted set of schema names to be loaded into the dictionary.
* The set is alphabetical in order to create a consistent key into a cache of
* dictionaries, since configuring a new dictionary for every request would be
* extremely time-consuming.
*
* @return See above.
*/
public static SchemaDictionary localInstance(SortedSet<String> schemata)
{
SchemaDictionary res;
if (schemata != null && !schemata.isEmpty())
{
String key = schemata.toString();
SchemaDictionary dict = nonDefDict.get().get(key);
if (dict == null)
{
try
{
dict = new SchemaDictionary(key, schemata, null);
}
catch (SchemaException e)
{
throw new RuntimeException(e);
}
nonDefDict.get().put(key, dict);
}
res = dict;
}
else
{
res = localDict.get();
}
// refresh these, otherwise temp-tables will get 'leaked'
res.clear();
return res;
}
/**
* A constructor which reads schema configuration data and loads default namespace schema information into
* the outermost (global) lookup scope.
* <p>
* Loading the default schema information is quite time-consuming, so the first time an instance is
* created, the loaded default data is backed up to a pristine copy. This copy is duplicated on subsequent
* runs to ensure a clean slate without the extra load time. This is especially useful since this data is
* never edited, so it can be (and is) shared among multiple simultaneously used instances of this class.
*
* @param bannedSchemas
* The set of schemas which must be 'unloaded'. The {@code Namespace} will not recognize elements
* from these schemas even is they are loaded.
*
* @throws SchemaException
* if any error occurs reading configuration information.
*/
public SchemaDictionary(Set<String> bannedSchemas)
throws SchemaException
{
this(null, null, bannedSchemas);
}
/**
* Default constructor which reads schema configuration data and loads default namespace schema information
* into the outermost (global) lookup scope.
* <p>
* Loading the default schema information is quite time-consuming, so the first time an instance is
* created, the loaded default data is backed up to a pristine copy. This copy is duplicated on subsequent
* runs to ensure a clean slate without the extra load time. This is especially useful since this data is
* never edited, so it can be (and is) shared among multiple simultaneously used instances of this class.
*
* @param key
* The key for the non-default set.
* @param nonDefaults
* The set of non-default schemas, to be loaded, beside the default ones.
* @param bannedSchemas
* The set of schemas which must be 'unloaded'. The {@code Namespace} will not recognize elements
* from these schemas even is they are loaded.
*
* @throws SchemaException
* if any error occurs reading configuration information.
*/
private SchemaDictionary(String key, Set<String> nonDefaults, Set<String> bannedSchemas)
throws SchemaException
{
// Create loader.
loader = new SchemaLoader();
this.bannedSchemas = bannedSchemas;
// Protect the initialization and use of static data.
synchronized (SchemaDictionary.class)
{
Scope pristineScope = null;
Map<String, Aast> pristineDbs = null;
Map<String, String> pristineAliases = null;
if (nonDefaults == null)
{
Configuration cfg = Configuration.getInstance();
if (cfg.isMultiProfile())
{
Object o = cfg.getProfileState(SchemaDictionary.class.getName());
Object[] state = (Object[]) o;
pristineScope = state == null ? null : (Scope) state[0];
pristineDbs = state == null ? null : (Map<String, Aast>) state[1];
pristineAliases = state == null ? null : (Map<String, String>) state[2];
}
else
{
pristineScope = SchemaDictionary.pristineScope;
pristineDbs = SchemaDictionary.pristineDbs;
pristineAliases = SchemaDictionary.pristineAliases;
}
}
else
{
pristineScope = SchemaDictionary.nonDefPristineScope.get(key);
pristineDbs = SchemaDictionary.nonDefPristineDbs.get(key);
pristineAliases = SchemaDictionary.nonDefPristineAliases.get(key);
}
if (pristineScope == null)
{
// First time through.
// Create schema global scope.
addScope(false);
// Load default schemas into global scope.
loader.loadDefaults(this);
if (nonDefaults != null)
{
for (String db : nonDefaults)
{
Aast ast = loader.loadSchema(db);
loadFromAst(ast, false);
}
}
// Backup the schema global scope.
pristineScope = new Scope(scopes.get(SCHEMA_GLOBAL_SCOPE));
// Backup the database map.
pristineDbs = new HashMap<>(databases);
// Backup the alias map.
pristineAliases = new HashMap<>(aliases);
if (nonDefaults == null)
{
if (Configuration.getInstance().isMultiProfile())
{
Object[] state = new Object[3];
state[0] = pristineScope;
state[1] = pristineDbs;
state[2] = pristineAliases;
Configuration.getInstance().setProfileState(SchemaDictionary.class.getName(), state);
}
else
{
SchemaDictionary.pristineScope = pristineScope;
SchemaDictionary.pristineDbs = pristineDbs;
SchemaDictionary.pristineAliases = pristineAliases;
}
}
else
{
SchemaDictionary.nonDefPristineScope.put(key, pristineScope);
SchemaDictionary.nonDefPristineDbs.put(key, pristineDbs);
SchemaDictionary.nonDefPristineAliases.put(key, pristineAliases);
}
}
else
{
// Subsequent time through. Reset to pristine state.
// Restore the global scope and add it back.
scopes.push(new Scope(pristineScope));
// Restore the default databases map.
databases = new LinkedHashMap<>();
Iterator<String> iter = Configuration.getSchemaConfig().defaultDatabases();
while (iter.hasNext())
{
String database = iter.next();
Aast ast = pristineDbs.get(database);
if (ast != null)
{
databases.put(database, ast);
}
}
if (nonDefaults != null)
{
for (String database : nonDefaults)
{
Aast ast = pristineDbs.get(database);
if (ast != null)
{
databases.put(database, ast);
}
}
}
// Restore the aliases map.
aliases = new HashMap<>(pristineAliases);
}
}
// Create user global scope.
addScope();
// Protect schema and user global scopes, such that they cannot be removed.
protectedScopeCount = scopes.size();
// Add database ASTs for temp and work tables.
newDatabase(TEMP_DB);
newDatabase(WORK_DB);
}
/**
* Check if there is a cached {@link Scope}, which can be used to replace the top of the {@link #scopes}
* stack.
*
* @param key
* The key in the {@link #cachedScopes} map.
*
* @return <code>true</code> if a cached scope was found and re-used as the top of {@link #scopes}.
*/
public boolean reuseCachedScope(String key)
{
if (key.isEmpty())
{
return true;
}
Scope scope = cachedScopes.get(key);
if (scope != null)
{
scopes.pop();
scopes.push(scope);
return true;
}
return false;
}
/**
* Cache the top of the {@link #scopes} stack.
*
* @param key
* The key to use in the {@link #cachedScopes}.
*/
public void cacheScope(String key)
{
Scope scope = scopes.peek();
cachedScopes.put(key, scope);
}
/**
* Load a database schema which is identified in the schema configuration
* by the specified database name.
*
* @param database
* Name which identifies in the configuration the namespace data
* to be loaded.
*
* @throws SchemaException
* if the schema namespace information cannot be loaded for any
* reason, including an invalid database name.
*/
public void loadSchema(String database)
throws SchemaException
{
loadSchema(database, true);
}
/**
* Load a database schema which is identified in the schema configuration
* by the specified database name.
*
* @param database
* Name which identifies in the configuration the namespace data
* to be loaded.
* @param global
* <code>true</code> to add entry to global scope, else
* <code>false</code> to add entry to top scope. Only considered
* if <code>parent</code> is not specified; otherwise, entry is
* added to scope in which <code>parent</code> resides.
*
* @throws SchemaException
* if the schema namespace information cannot be loaded for any
* reason, including an invalid database name.
*/
public void loadSchema(String database, boolean global)
throws SchemaException
{
// try to load AST (which can be large) from cache first
Aast ast = pristineDbs.get(database);
if (ast == null)
{
synchronized (SchemaDictionary.class)
{
ast = pristineDbs.get(database);
if (ast == null)
{
// load AST from schema loader and cache it
ast = loader.loadSchema(database);
pristineDbs.put(database, ast);
}
}
}
loadFromAst(ast, true, global);
}
/**
* Create an alias for a previously loaded database. Any schema name
* lookups which use the alias as a qualifier will map to the specified
* database instead. Chained aliases (i.e., an alias for another alias)
* are disallowed; however, reseting an existing alias to a new underlying
* database name is allowed.
*
* @param alias
* Alias to use for the database qualifier portion of a schema
* entity name.
* @param database
* Logical name of the database for which the alias is being
* created. May not be <code>null</code>.
*
* @return <code>true</code> if the alias was created; <code>false</code>
* if <code>database</code> could not be found in the dictionary,
* or if the caller tried to create an alias for another alias.
*/
public boolean createAlias(String alias, String database)
{
alias = alias.toLowerCase();
database = database.toLowerCase();
// Disallow a chained alias or an an alias to a non-existent database
// name. This test works because logical database names must be aliases
// to themselves (mapping is created when a database entry is added).
if (!database.equals(aliases.get(database)))
{
return false;
}
// Map alias to logical database name.
aliases.put(alias, database);
return true;
}
/**
* Add a dictionary entry for the specified database and return a
* {@link NameNode} object representing the database name, which can be
* used as a parent node for subsequent table entry additions.
*
* @param name
* Database name to be added.
*
* @return Name node representing database name.
*/
public NameNode addDatabaseEntry(String name)
{
// Always use lower case names.
name = name.toLowerCase();
// Add a self-referencing alias for this database name.
aliases.put(name, name);
// Create AST and name node.
Aast ast = newDatabase(name);
NameNode node = addEntry(null, ast, EntityName.DATABASE, true);
return node;
}
/**
* Add a dictionary entry for the specified table and return a
* {@link NameNode} object representing the table name, which can be
* used as a parent node for subsequent field entry additions.
*
* @param parent
* Database name node which represents this table's parent entity.
* May be <code>null</code>.
* @param name
* Table name to be added.
* @param tokenType
* Type of table to be added (e.g., normal schema-defined table,
* buffer, temp table, work table).
* @param globalScope
* <code>true</code> to add entry to global scope, else
* <code>false</code> to add entry to top scope.
* @param tnode
* The node that triggered the table creation.
*
* @return Name node representing table name.
*/
public NameNode addTableEntry(NameNode parent, String name, int tokenType, boolean globalScope, Aast tnode)
{
String legacyName = name;
// Always use lower case names.
name = name.toLowerCase();
// Create an AST. If the new table is a BUFFER, this AST is just a shim
// whose parent will later be set to the backing, "real" table's AST.
Aast ast = new ProgressAst();
ast.setType(tokenType);
ast.setText(name);
ast.setLine(tnode.getLine());
ast.setColumn(tnode.getColumn());
ast.putAnnotation("legacy_name", legacyName);
ast.putAnnotation("historical", legacyName);
if (tnode.isAnnotation("tt_shared"))
{
ast.putAnnotation("tt_shared", (Boolean) tnode.getAnnotation("tt_shared"));
if (tnode.isAnnotation("tt_global"))
{
ast.putAnnotation("tt_global", (Boolean) tnode.getAnnotation("tt_global"));
}
if (tnode.isAnnotation("tt_new"))
{
ast.putAnnotation("tt_new", (Boolean) tnode.getAnnotation("tt_new"));
}
}
// Create name node.
NameNode node = addEntry(parent, ast, EntityName.TABLE, globalScope);
// Add AST to the larger tree, unless new table is a BUFFER, in which
// case its backing AST is already part of the larger tree.
Aast database = null;
switch (tokenType)
{
case TABLE:
database = parent.getAst();
break;
case TEMP_TABLE:
database = databases.get(TEMP_DB);
break;
case WORK_TABLE:
database = databases.get(WORK_DB);
break;
default:
return node;
}
database.addChild(ast);
ast.setParent(database);
return node;
}
/**
* Add a dictionary entry for the specified field and return a
* {@link NameNode} object representing the field name.
*
* @param parent
* Table name node which represents this field's parent entity.
* @param fnode
* The node that triggered the field creation.
* @param name
* Field name to be added.
* @param type
* Data type of field to be added (e.g., integer, character, raw,
* etc.).
* @param globalScope
* <code>true</code> to add entry to global scope, else
* <code>false</code> to add entry to top scope.
*
* @return Name node representing field name.
*/
public NameNode addFieldEntry(NameNode parent, Aast fnode, String name, int type, boolean globalScope)
{
Aast ast = newField(parent, fnode, name, type);
NameNode node = addEntry(parent, ast, EntityName.FIELD, globalScope);
return node;
}
/**
* Add a dictionary entry for a field which is <em>like</em> an existing
* field, and return a {@link NameNode} object representing the field name.
* This is used when processing temp-tables and work-tables.
*
* @param parent
* Table name node which represents this field's parent entity,
* a temp-table or work-table. It should not be <code>null</code>.
* @param fnode
* The node that triggered the field creation.
* @param name
* Field name to be added.
* @param source
* Name node for the existing field which provides the basis of the
* definition of the new field. Can be qualified or unqualified,
* abbreviated or unabbreviated, but must be unambiguous.
* @param extent
* One-based index of the element (for an extent field) after
* which the new field entry is to be modelled. This is only
* relevant for the initializer, so that we copy the correct
* initial value in the case of an initializer list for an extent
* field. Will be <code>0</code> if the "like" field is not an
* extent field, or if the entire extent field is the target of
* the like phrase, rather than only a specific element.
*
* @return Name node representing new field name.
*
* @throws SchemaException
* if there is an error copying the field's AST.
*/
public NameNode addFieldEntry(NameNode parent, Aast fnode, String name, NameNode source, int extent)
throws SchemaException
{
Aast srcAst = source.getAst();
Aast copy = Variable.copyOptions(srcAst, extent);
// propagate the schemaname of the original source of the field definition
String likeAnnotation = srcAst.isAnnotation("like") ?
(String) srcAst.getAnnotation("like") :
source.toQualifiedName();
copy.putAnnotation("like", likeAnnotation);
copy.putAnnotation("historical", name);
// Graft a copy of the source AST onto the table's AST, but rename it.
NameNode node = null;
try
{
name = name.toLowerCase();
Aast table = getTable(parent);
copy.setText(name);
copy.setLine(fnode.getLine());
copy.setColumn(fnode.getColumn());
// force all children to have the same line/col numbers as the dup
// node, rather than the line/col nums from the copied (from) table
forceLineColumnNums(copy);
table.graft(copy);
// Add a new field name node based on the copied AST.
node = addEntry(parent, copy, EntityName.FIELD, true);
}
catch (AstException exc)
{
throw new SchemaException("Error copying field AST: " + name, exc);
}
return node;
}
/**
* Add a dictionary entry for a field which is <em>like</em> the field
* described by the AST specified by the <code>like</code> parameter.
* Return a {@link NameNode} object representing the field name. A copy
* of <code>like</code> is made, irrelevant properties are removed from
* the copy, and the resulting AST is grafted onto the schema AST under
* the table AST stored in <code>parent</code>.
*
* @param parent
* Table name node which represents this field's parent entity.
* It should not be <code>null</code>.
* @param fnode
* The node that triggered the field creation.
* @param name
* Name of field to be added.
* @param type
* Token type of field to be added.
* @param like
* An AST which describes the properties of the new field. This
* AST might not be a field (in fact, it likely represents a
* variable). As such, it may contain additional properties
* which we do not care about for our field definition. These
* are filtered out when a copy of the AST is made and grafted
* into the appropriate schema AST.
*
* @return Name node representing new field name.
*
* @throws AmbiguousSchemaNameException
* if <code>like</code> is an ambiguous field reference.
* @throws SchemaException
* if an error occurs grafting a copy of <code>like</code> onto
* the schema AST.
*/
public NameNode addFieldEntry(NameNode parent, Aast fnode, String name, int type, Aast like)
throws SchemaException
{
NameNode node = null;
try
{
// Graft a copy of the source AST onto the table's AST, but rename it
// and re-type it.
name = name.toLowerCase();
Aast table = getTable(parent);
Aast copy = like.graftCopyTo(table, FIELD_PROPS);
copy.setText(name);
copy.setType(type);
copy.setLine(fnode.getLine());
copy.setColumn(fnode.getColumn());
// Add a new field name node based on the copied AST.
node = addEntry(parent, copy, EntityName.FIELD, true);
}
catch (AstException exc)
{
throw new SchemaException(
"Error copying source AST to field: " + like.getText(), exc);
}
return node;
}
/**
* Create a new table index under table <code>parent</code>, which is a
* copy of the specified index <code>like</code>. Normalize the index
* field ASTs to look like those provided by the schema directly.
*
* @param parent
* Name node of table with which this index is associated.
* @param name
* Name of the index.
* @param like
* The original index AST to be copied.
*
* @return Object which represents the new table index.
*
* @throws SchemaException
* if an error occurs grafting a copy of <code>like</code> onto
* the schema AST.
*/
public Aast addIndex(NameNode parent, String name, Aast like)
throws SchemaException
{
Aast table = getTable(parent);
// Copy index AST.
Aast ast;
try
{
ast = like.graftCopyTo(table, null);
}
catch (AstException exc)
{
throw new SchemaException("Error copying index", exc);
}
// Each INDEX_FIELD AST has some sort of a FIELD_* AST child, which is
// duplicative with the information stored in the (temp/work)-table
// itself. Remove these, but first copy the text up to the INDEX_FIELD
// AST, since it is not present there.
List<Aast> remove = new ArrayList<>();
Aast indexField = ast.getImmediateChild(INDEX_FIELD, null);
while (indexField != null)
{
Aast dup = (Aast) indexField.getFirstChild();
if (dup != null)
{
remove.add(dup);
indexField.setText(dup.getText());
}
indexField = ast.getImmediateChild(INDEX_FIELD, indexField);
}
Iterator<Aast> iter = remove.iterator();
while (iter.hasNext())
{
Aast next = iter.next();
next.remove();
}
return ast;
}
/**
* Create a new table index field under index <code>parent</code>.
*
* @param parent
* Index object with which this index field is associated.
* @param inode
* The node that triggered the index creation.
* @param name
* Name of the index field.
*
* @return Object which represents the new table index field.
*/
public Aast addIndexField(Aast parent, Aast inode, String name)
{
Aast field = new ProgressAst();
field.setType(INDEX_FIELD);
field.setText(name.toLowerCase());
field.setLine(inode.getLine());
field.setColumn(inode.getColumn());
parent.addChild(field);
field.setParent(parent);
return field;
}
/**
* Remove all traces of all temp-tables and buffers from the schema dictionary, except for
* those with the specified names. The given names are expected to resolve to table entities
* in the user global scope or in a higher scope. Permanent tables are also removed.
* <p>
* For the entities not in the exception list, all name nodes and children name nodes will be
* removed from all their respective global and private namespaces, and all associated ASTs
* will be pruned from their respective schema ASTs.
*
* @param preserve
* Names of those tables to preserve.
*/
public void removeAllExcept(String[] preserve)
{
// collect the name nodes associated with the names of the tables to preserve
Set<NameNode> except = new HashSet<>();
for (String name : preserve)
{
try
{
NameNode node = findEntry(name, EntityName.TABLE);
except.add(node);
}
catch (AmbiguousSchemaNameException exc)
{
LOG.log(Level.SEVERE, "", exc);
}
}
// walk each scope from top-most to and including the user global scope, and purge all
// table information not associated with the exception list
for (int i = scopes.size() - 1; i >= USER_GLOBAL_SCOPE; i--)
{
Scope scope = scopes.get(i);
scope.removeAllNodes(except, EntityName.TABLE);
}
}
/**
* Copy the index with the given name from the current model, source
* table to the specified destination table. Optionally set the index
* as a primary index.
* <p>
* The current model source table is set at the beginning of
* <code>like</code> clause processing for a temp-table and is nulled out
* when {@link #finalizeTableDefine} is invoked.
*
* @param destTable
* Destination temp-table to which the index will be copied.
* @param fnode
* The node that triggered the field creation.
* @param name
* Name of the index to be copied from the model table.
* @param primary
* <code>true</code> if the index is the primary index for the
* target temp-table. Note: the primary property is only set
* for the index if this argument is <code>true</code>.
*
* @throws SchemaException
* if no index named <code>name</code> is found in the source
* table or if an error occurs grafting a copy of the source
* index onto the destination temp-table AST.
*/
public void copyIndex(NameNode destTable, Aast fnode, String name, boolean primary)
throws SchemaException
{
// Index AST which is the source template for the graft.
name = name.toLowerCase();
Aast source = getChildAst(modelTable, INDEX, name);
if (source == null)
{
throw new SchemaException(
"Index not found in table " + modelTable.getText() + ": " +
name);
}
// Temp-table AST which is the destination for the graft.
Aast parent = destTable.getAst();
// Detect the primary index, if any, of the destination table.
Aast destPrimary = null;
Aast next = parent.getImmediateChild(INDEX, null);
while (next != null && destPrimary == null)
{
destPrimary = next.getImmediateChild(KW_PRIMARY, null);
next = parent.getImmediateChild(INDEX, next);
}
// Graft a copy of the source onto the destination temp-table.
Aast copy = null;
try
{
copy = source.graftCopyTo(parent, null);
copy.setLine(fnode.getLine());
copy.setColumn(fnode.getColumn());
// force all children to have the same line/col numbers as the dup
// node, rather than the line/col nums from the copied (from) index
forceLineColumnNums(copy);
}
catch (AstException exc)
{
throw new SchemaException("Error copying index: " + name, exc);
}
// Detect whether the copied index is defined as the primary index
// for its table.
Aast copyProps = copy.getImmediateChild(PROPERTIES, null);
Aast copyPrimary = null;
if (copyProps != null)
{
copyPrimary = copyProps.getImmediateChild(KW_PRIMARY, null);
}
// Primary index processing. Only one index can be the primary index
// for a table.
if (primary)
{
// This index is defined in the source code as primary, which
// trumps all other indices. If another index is already defined
// as primary, remove that designation.
if (destPrimary != null)
{
destPrimary.remove();
}
// Set the copied index as primary, if it is not already so set.
if (copyPrimary == null)
{
copyPrimary = new ProgressAst();
copyPrimary.setType(KW_PRIMARY);
copyPrimary.setText("primary");
copyPrimary.setParent(copyProps);
copyProps.addChild(copyPrimary);
}
}
else if (copyPrimary != null && destPrimary != null)
{
// No AS PRIMARY clause was present, but if the copied index is
// defined as primary, we need to make sure no other index has
// already been defined for this temp-table as the primary index.
// If there is one, it must override this one as the primary.
copyPrimary.remove();
}
}
/**
* Finalize the definition of a temp-table. This method is invoked by
* the Progress parser (via its symbol resolver) to notify the schema
* dictionary that the current temp-table definition "transaction" has
* completed. A "model" source table is set at the beginning of
* <code>like</code> clause processing for a temp-table. If the given
* temp-table at this point contains no indices, we copy the index
* definitions described by the model table. Otherwise, we assume that
* this default behavior has been overridden by individual indices
* explicitly copied or defined in the Progress source code.
*
* @param destTable
* Name node of the temp-table being defined.
* @param ast
* The created table AST.
*
* @throws SchemaException
* Wrapping an AstException if one is detected during processing.
*
* @see #addEntries
* @see #copyIndex
* @see #modelTable
*/
public void finalizeTableDefine(NameNode destTable, Aast ast)
throws SchemaException
{
Aast table = getTable(destTable);
if (ast != null)
{
copyTableProperties(ast, table);
}
// calculate and save the table signature
calculateSignature(table);
if (modelTable == null)
{
// Temp-table has been defined without using a "like" clause.
return;
}
// If the destination is a temp-table does not yet have any indices
// defined, copy all indices from the current model table.
// Note: work-tables never have indices.
if (ast != null && ast.getType() == KW_TEMP_TAB)
{
try
{
if (table.isAnnotation("after-table"))
{
// the BEFORE-TABLEs have only one index, primary named rowState with a single field
// __row-state__
ProgressAst index = new ProgressAst();
index.setText(Buffer.ROW_STATE_IDX);
index.setType(INDEX);
ProgressAst indexField = new ProgressAst();
indexField.setText(Buffer.__ROW_STATE__);
indexField.setType(INDEX_FIELD);
index.graft(indexField);
table.graft(index);
}
Iterator<Aast> iter = indices(table);
if (!iter.hasNext())
{
iter = indices(modelTable);
while (iter.hasNext())
{
Aast next = iter.next();
next.graftCopyTo(table, null);
}
}
Aast lastPrimaryIndex = null;
// keep only the last primary index; multiple primary indexes can happen if both
// USE-INDEX pk-idx and INDEX IS PRIMARY is defined
while (iter.hasNext())
{
Aast next = iter.next();
if (next.downPath(PROPERTIES, KW_PRIMARY))
{
if (lastPrimaryIndex != null)
{
lastPrimaryIndex.remove();
}
lastPrimaryIndex = next;
}
}
}
catch (AstException exc)
{
throw new SchemaException("Error copying index", exc);
}
}
// Reset the modelTable variable for further temp-table definition
// "transactions".
modelTable = null;
}
/**
* Get the table object associated with the given table name.
*
* @param name
* Name of the target table. Can be qualified or unqualified,
* abbreviated or unabbreviated, but must be unambiguous.
*
* @return The table object associated with <code>name</code>.
*
* @throws SchemaException
* if no table is found with the given name.
* @throws AmbiguousSchemaNameException
* if <code>name</code> is an ambiguous table reference.
*/
public Aast getTable(String name)
throws SchemaException
{
NameNode node = findEntry(name, EntityName.TABLE);
if (node == null)
{
throw new SchemaException("No table matching name: " + name);
}
return getTable(node);
}
/**
* Get the AST associated with the given table name node. If the given
* node represents a buffer, the AST of the record which backs the buffer
* is returned instead.
*
* @param node
* Name node of the target table.
*
* @return Table AST.
*/
public Aast getTable(NameNode node)
{
Aast ast = node.getAst();
// If table node represents a buffer, we have to get the AST
// associated with the backing table.
if (ast.getType() == BUFFER)
{
ast = ast.getParent();
}
return ast;
}
/**
* Get the field AST associated with the given field name.
*
* @param name
* Name of the target field. Can be qualified or unqualified,
* abbreviated or unabbreviated, but must be unambiguous.
*
* @return The field AST associated with <code>name</code>.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> is an ambiguous field reference.
*/
public Aast getField(String name)
throws AmbiguousSchemaNameException
{
NameNode node = findEntry(name, EntityName.FIELD);
return node.getAst();
}
/**
* Determines if the given node is contained in this dictionary.
*
* @param node
* The namenode to check.
*
* @return <code>true</code> if the given node can be found in this dictionary.
*/
public boolean containsNode(NameNode node)
{
return retrieveNode(node) != null;
}
/**
* Get the Progress parser token type of a table, temp-table, work-table,
* or buffer. If <code>name</code> represents a buffer, the type of the
* backing record is returned. That is, a type of <code>BUFFER</code> is
* never returned, only the type of the record which backs the buffer.
* <p>
* If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
* with no preference for temp-table or persistent table.
*
* @param name
* Case insensitive record name; may be qualified or not. If
* qualified, only the table portion of the name may be
* abbreviated.
* @param forceTemp
* If <code>true</code>, force a temp-table lookup.
* @param forcePersistent
* If <code>true</code>, force a persistent table lookup.
*
* @return One of the Progress parser token types <code>TABLE</code>,
* <code>TEMP_TABLE</code>, or <code>WORK_TABLE</code>, or
* <code>UNKNOWN</code> if <code>name</code> cannot be found.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> is an ambiguous identifier for the
* target record.
*/
public TableInfo findTableInfo(String name, boolean forceTemp, boolean forcePersistent)
throws AmbiguousSchemaNameException
{
// TODO: The recordType lookup used scopes.size() - 1 instead of nearestEnclosingInternal(), is this
// an issue? It seems like it should have been consistent with the other lookups, so this is
// being made consistent here.
int l1 = computeLevel(forceTemp, forcePersistent, true, nearestEnclosingInternal());
int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
// Look up record name node.
NameNode node = findEntry(name, EntityName.TABLE, l1, l2, false);
TableInfo info = null;
if (node != null)
{
NameNode dbNode = node.getParent();
// don't just use getAst() here, we need the AST for the backing table if this is a buffer
// (this will work for both the simple case and the buffer case)
Aast ast = getTable(node);
String sig = ast != null ? (String) ast.getAnnotation("db_signature") : null;
String sname = getQualifiedName(node);
String bname = getRecordName(node, EntityName.TABLE, name);
String dname = getLogicalDatabaseForTable(name, forceTemp, forcePersistent);
int ttype = getTable(node).getType();
info = new TableInfo(sig, sname, bname, dname, ttype);
}
return info;
}
/**
* Get an object which exposes schema-related information about a field which is referenced in
* source code.
*
* @param tableNode
* Name node asssociated with the field reference. If not {@code null}, only the
* private namespace of the given {@code tableNode} is searched (for an exact match)
* of {@code fieldName}. If {@code null}, a lenient search (i.e., abbreviations
* permitted) is performed across all scopes in the schema dictionary.
* @param fieldName
* Unaltered field name, as referenced in source code.
* @param preferDefaultBuffer
* {@code true} to indicate unqualified fields should prefer the default buffer over an
* explicitly defined buffer match.
*
* @return Field schema information if found or {@code null} if nothing is found.
*
* @throws AmbiguousSchemaNameException
* if {@code tableNode} is {@code null} and a search for {@code fieldName} finds more
* than one result.
* @throws SchemaException
* If there is some other problem in the lookup.
*/
public FieldInfo findFieldInfo(NameNode tableNode, String fieldName, boolean preferDefaultBuffer)
throws AmbiguousSchemaNameException,
SchemaException
{
NameNode fieldNode = null;
try
{
// temporarily use the provided preferDefaultBuffer setting; this works as long as this method
// cannot recursively call itself, which it does not at the time this is being written
this.preferDefaultBuffer = preferDefaultBuffer;
boolean unqualified = false;
if (Configuration.isRuntimeConfig())
{
// if the field is unqualified and promoted names are more than 1, don't limit the lookup to
// a specific table, use the normal node search algorithm instead
unqualified = !fieldName.contains(".") && scopes.get(scopes.size() - 1).promoted.size() > 1;
}
fieldNode = findFieldNode(unqualified ? null : tableNode, fieldName);
}
finally
{
// set the preferDefaultBuffer instance variable back to its default after the lookup
this.preferDefaultBuffer = false;
}
if (fieldNode == null)
{
return null;
}
// get AST
Aast ast = fieldNode.getAst();
// get qualified field name
String qualifiedName = getQualifiedName(fieldNode);
// get buffer name
String bufferName = getRecordName(fieldNode, EntityName.FIELD, fieldName);
// get database name associated with buffer
EntityName eName = new EntityName(EntityName.TABLE, bufferName);
String dbName = eName.getDatabase();
if (dbName == null)
{
NameNode bufNode = findEntry(bufferName,
EntityName.TABLE,
nearestEnclosingInternal(),
SCHEMA_GLOBAL_SCOPE,
false);
NameNode dbNode = bufNode.getParent();
dbName = (dbNode != null ? dbNode.getName() : "");
}
// get record type
int ttype = UNKNOWN;
NameNode parent = fieldNode.getParent();
if (parent != null)
{
// getTable() method automatically resolves a buffer reference to its backing table
ttype = getTable(parent).getType();
}
FieldInfo info = new FieldInfo(ast, qualifiedName, bufferName, dbName, ttype);
return info;
}
/**
* Get a copy of the AST which represents field <code>name</code>, with
* possible modifications. Specifically, if <code>extent</code> is
* greater than 0 and the source field is an extent field, the copy will
* represent a copy of a single element of the original extent field.
* The effect of this is to remove the KW_EXTENT child node, and to use
* only the initializer (if any) which corresponds with the element at
* index <code>extent</code>.
*
* @param name
* Name of the existing field which provides the basis of the
* definition of the copy. Can be qualified or unqualified,
* abbreviated or unabbreviated, but must be unambiguous.
* @param extent
* One-based index of the element (for an extent field) after
* which the new field entry is to be modelled. This is only
* relevant for the initializer, so that we copy the correct
* initial value in the case of an initializer list for an extent
* field. Will be <code>0</code> if the "like" field is not an
* extent field, or if the entire extent field is the target of
* the like phrase, rather than only a specific element.
*
* @return Copy of the AST corresponding with the given name, with the
* possible modifications described above.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> is an ambiguous field reference.
*/
public Aast getFieldCopy(String name, int extent)
throws AmbiguousSchemaNameException
{
return Variable.copyOptions(getField(name), extent);
}
/**
* Get a specific index AST for a table.
*
* @param table
* Node of the target table. Can be qualified or unqualified,
* abbreviated or unabbreviated, but must be unambiguous.
* @param name
* Name of the target index defined for the target table.
*
* @return Index AST or <code>null</code> if not found.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> is an ambiguous field reference.
*/
public Aast getIndex(String table, String name)
throws AmbiguousSchemaNameException
{
NameNode node = findEntry(table, EntityName.TABLE);
return getIndex(node, name);
}
/**
* Get a specific index AST for a table.
*
* @param tableNode
* Name node of the target table.
* @param name
* Name of the target index defined for the target table. May be
* abbreviated.
*
* @return Index AST or <code>null</code> if not found.
*/
public Aast getIndex(NameNode tableNode, String name)
{
Aast index = null;
Aast table = getTable(tableNode);
if (table != null)
{
// Try for an exact match first.
Aast tmp = null;
while (index == null)
{
tmp = table.getImmediateChild(INDEX, tmp);
if (tmp == null)
{
break;
}
if (tmp.getText().toLowerCase().equals(name))
{
index = tmp;
}
}
// Try for an abbreviated match if necessary. First match wins.
tmp = null;
while (index == null)
{
tmp = table.getImmediateChild(INDEX, tmp);
if (tmp == null)
{
break;
}
if (tmp.getText().toLowerCase().startsWith(name))
{
index = tmp;
}
}
}
return index;
}
/**
* Get the longest unique indexes for this table. First, the {@link #tableIndexes} is checked;
* if there is an entry, that is returned. Otherwise, all longest unique indexes (with no
* other index being a prefix for another index) for this table are collected.
*
* @param table
* The table for which the indexes are required.
*
* @return See above.
*/
public Map<Aast, List<Aast>> getUniqueIndexes(Aast table)
{
Map<Aast, List<Aast>> indexes = tableIndexes.get(table);
if (indexes != null)
{
return indexes;
}
indexes = new HashMap<>();
tableIndexes.put(table, indexes);
Iterator<Aast> iter = indices(table);
while (iter.hasNext())
{
Aast next = iter.next();
boolean unique = false;
Aast props = next.getImmediateChild(PROPERTIES, null);
while (!unique && props != null)
{
unique = (props.getImmediateChild(KW_UNIQUE, null) != null);
props = next.getImmediateChild(PROPERTIES, props);
}
if (!unique)
{
continue;
}
List<Aast> fields = new ArrayList<>();
Aast field = null;
while ((field = next.getImmediateChild(INDEX_FIELD, field)) != null)
{
fields.add(field);
}
indexes.put(next, fields);
}
// keep only the indexes which don't have another index as a prefix
l1: while (true)
{
Iterator<Aast> iter1 = indexes.keySet().iterator();
while (iter1.hasNext())
{
Aast idx1 = iter1.next();
List<Aast> fields1 = indexes.get(idx1);
Iterator<Aast> iter2 = indexes.keySet().iterator();
while (iter2.hasNext())
{
Aast idx2 = iter2.next();
if (idx2 == idx1)
{
continue;
}
List<Aast> fields2 = indexes.get(idx2);
if (fields1.size() == fields2.size())
{
// not a possible prefix
continue;
}
boolean match = true;
for (int i = 0; i < Math.min(fields1.size(), fields2.size()); i++)
{
Aast fld1 = fields1.get(i);
Aast fld2 = fields2.get(i);
if (!fld1.getText().equalsIgnoreCase(fld2.getText()))
{
match = false;
break;
}
}
if (match)
{
if (fields1.size() > fields2.size())
{
iter2.remove();
}
else
{
iter1.remove();
}
continue l1;
}
}
}
// we've reached here means no other change has been made
break;
}
return indexes;
}
/**
* Get an iterator on all of the table ASTs defined for a given database.
* The tables are iterated in the order in which they were added to the
* dictionary. All types are included in the iteration: TABLE, TEMP_TABLE,
* WORK-TABLE, and BUFFER. Metadata tables are included in the iteration.
*
* @param databaseName
* Name of the target database.
*
* @return An iterator of tables for the specified database.
*/
public Iterator<Aast> tables(String databaseName)
{
final Aast database = databases.get(databaseName);
// Return a safe iterator if the database was not found.
if (database == null)
{
return EmptyIterator.get();
}
// Define an iterator that walks the table children of the database.
return new Iterator<Aast>()
{
private Aast next = advance();
@Override
public boolean hasNext()
{
return (next != null);
}
@Override
public Aast next()
{
if (next == null)
{
throw new NoSuchElementException();
}
Aast ast = next;
next = advance();
return ast;
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
private Aast advance()
{
Aast test = null;
Aast ast = null;
if (next == null)
{
test = (Aast) database.getFirstChild();
}
else
{
test = (Aast) next.getNextSibling();
}
while (ast == null && test != null)
{
switch (test.getType())
{
case TABLE:
case BUFFER:
case TEMP_TABLE:
case WORK_TABLE:
ast = test;
break;
default:
ast = null;
break;
}
test = (Aast) test.getNextSibling();
}
return ast;
}
};
}
/**
* Get an iterator on all of the field ASTs defined for a given table.
* The fields are iterated in the order in which they were added to the
* dictionary. All field types (including metatypes) are included in the
* iteration. Fields of metadata tables are included in the iteration.
* By convention, these fields' names seem to begin with a leading
* underscore, while normal field names typically do not.
*
* @param table
* Target table AST.
*
* @return An iterator of fields for the specified table.
*/
public Iterator<Aast> fields(final Aast table)
{
// Return a safe iterator if the table is null.
if (table == null)
{
return EmptyIterator.get();
}
// Define an iterator that walks the field children of the table.
return new Iterator<Aast>()
{
private Aast next = advance();
@Override
public boolean hasNext()
{
return (next != null);
}
@Override
public Aast next()
{
if (next == null)
{
throw new NoSuchElementException();
}
Aast ast = next;
next = advance();
return ast;
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
private Aast advance()
{
Aast test = null;
Aast ast = null;
if (next == null)
{
test = (Aast) table.getFirstChild();
}
else
{
test = (Aast) next.getNextSibling();
}
while (ast == null && test != null)
{
int type = test.getType();
if ((type > BEGIN_FIELDTYPES && type < END_FIELDTYPES) ||
(type > BEGIN_METATYPES && type < END_METATYPES))
{
ast = test;
}
test = (Aast) test.getNextSibling();
}
return ast;
}
};
}
/**
* Get the index ASTs associated with a database table. The indices are
* iterated in the order in which they were added to the dictionary.
*
* @param table
* Target table AST.
*
* @return An iterator of indices for the specified table.
*/
public Iterator<Aast> indices(final Aast table)
{
// Return a safe iterator if the table was null.
if (table == null)
{
return EmptyIterator.get();
}
// Define an iterator that walks the index children of the table.
return new Iterator<Aast>()
{
private Aast next = advance();
@Override
public boolean hasNext()
{
return (next != null);
}
@Override
public Aast next()
{
if (next == null)
{
throw new NoSuchElementException();
}
Aast ast = next;
next = advance();
return ast;
}
@Override
public void remove()
{
throw new UnsupportedOperationException();
}
private Aast advance()
{
Aast test = null;
Aast ast = null;
if (next == null)
{
test = (Aast) table.getFirstChild();
}
else
{
test = (Aast) next.getNextSibling();
}
while (ast == null && test != null)
{
if (test.getType() == INDEX)
{
ast = test;
}
test = (Aast) test.getNextSibling();
}
return ast;
}
};
}
/**
* Promotes <code>table</code> and all of its fields from the global scope
* to the top-most scope, if not already present there. Creates the parent
* database node in the same scope if not already present. Name nodes
* that already exist in the current scope will no be re-added but any
* missing nodes will be added.
*
* @param table
* Name of table to be promoted. Must be an unambiguous reference
* within the global scope.
*
* @throws AmbiguousSchemaNameException
* if <code>table</code> matches more than one entry in the
* global scope of the dictionary.
* @throws SchemaException
* if <code>table</code> does not exist in the global scope or
* if there is no scope above the global scope to which this
* table may be promoted.
*/
public void promoteTable(String table)
throws SchemaException
{
promoteTable(table, true, false, true);
}
/**
* Promotes <code>table</code> and all of its fields from the global scope
* to the top-most scope, if not already present there. Creates the parent
* database node in the same scope if not already present. If the named
* table has ever been promoted in the current instance, it will not be
* promoted again, unless the <code>force</code> flag is on.
*
* @param table
* Name of table to be promoted. Must be an unambiguous reference
* within the global scope.
* @param force
* Override the default promotion behavior and force a refresh
* of the promoted table even if it was already promoted
* previously.
* @param noProp
* <code>true</code> to force any promoted nodes to have their
* no propagate flag set. This disables propagation of these
* nodes across the current scope boundary.
* @param self
* <code>true</code> to promote the node directly associated with
* <code>name</code>; <code>false</code> to promote only its
* descendants.
*
* @throws AmbiguousSchemaNameException
* if <code>table</code> matches more than one entry in the
* global scope of the dictionary.
* @throws SchemaException
* if <code>table</code> does not exist in the global scope or
* if there is no scope above the global scope to which this
* table may be promoted.
*/
public void promoteTable(String table, boolean force, boolean noProp, boolean self)
throws SchemaException
{
promoteEntry(table, self, EntityName.TABLE, force, noProp);
}
/**
* Post-process the given field, in terms of honoring the VALIDATE clause, if present. This
* will:
* <ul>
* <li>
* If the VALIDATE clause is not present, it will remove the VALEXP and VALMSG children
* </li>
* <li>
* If the VALIDATE clause is present, it will remove all extent and LBRACKET information
* from the VALEXP nodes. Also, any reference of the source field in the VALEXP will
* be rewritten to reference the new field.
* </li>
* </ul>
*
* @param field
* The field which needs to be processed. This is a field defined using a
* <code>LIKE table.sourceField</code> clause, with or without the VALIDATE clause.
* @param hasValidate
* When <code>true</code>, it indicates the VALIDATE clause was present and the
* VALEXP node will need to be processed.
* @param sourceSchemaName
* The schema name of the source field
*
* @throws AmbiguousSchemaNameException
* If field matches more than one entry in the dictionary.
*/
public void processLikeField(Aast field, boolean hasValidate, String sourceSchemaName)
throws AmbiguousSchemaNameException
{
if (!hasValidate)
{
// if fields do not have the VALIDATE clause specified, then the VALMSG and VALEXP
// nodes need to be removed
if (field.downPath(VALEXP))
{
field.getImmediateChild(VALEXP, null).remove();
}
if (field.downPath(VALMSG))
{
field.getImmediateChild(VALMSG, null).remove();
}
}
// target temp-table field (or variable?) can't be mandatory, so we remove this node
if (field.downPath(MANDATORY))
{
field.getImmediateChild(MANDATORY, null).remove();
}
// target temp-table field (or variable?) can't have triggers, so we remove this node
if (field.downPath(KW_FLD_TRG))
{
field.getImmediateChild(KW_FLD_TRG, null).remove();
}
Aast valexp = field.getImmediateChild(VALEXP, null);
if (valexp != null)
{
Iterator<Aast> iter = valexp.iterator();
Set<Aast> remove = new HashSet<>();
while (iter.hasNext())
{
Aast child = iter.next();
if (child.getType() > BEGIN_FIELDTYPES &&
child.getType() < END_FIELDTYPES &&
sourceSchemaName.equals(child.getAnnotation("schemaname")))
{
child.removeAnnotation("extent");
// get rid of any LBRACKET, as this is no longer needed
if (child.downPath(LBRACKET))
{
remove.add(child.getImmediateChild(LBRACKET, null));
}
// in this case, rewrite this to reference the new field, not the source field
String name = field.getText();
String fullname = getFullFieldName(name);
String bufname = getBufferForField(name);
String dbname = getLogicalDatabaseForTable(bufname);
if (fullname != null)
{
child.putAnnotation("schemaname", fullname);
}
if (bufname != null)
{
child.putAnnotation("bufname", bufname);
}
if (dbname != null)
{
child.putAnnotation("dbname", dbname);
}
child.putAnnotation("name", name);
child.setText(name);
}
}
// remove the collected nodes
for (Aast ast : remove)
{
ast.remove();
}
}
}
/**
* Given a table name representing table whose structure is to be copied (e.g., for a LIKE
* clause), find the name node associated with that table. Prefer persistent tables to
* temp-tables. Ensure that the node returned is a table node, not a buffer node, and ensure
* that the source (i.e., "from") node is not the same as the target (i.e., "to") node, which
* can happen for an unqualified lookup.
*
* @param fromTable
* Name of the table whose node is to be found.
* @param toNode
* Copy target node.
* @param skipGlobal
* if (@code true}, do not look in the global schema scope to find {@code fromTable}.
*
* @return Name node as described above.
*
* @throws AmbiguousSchemaNameException
* if the lookup results in ambiguity within a scope.
*/
public NameNode findTableFromNode(String fromTable, NameNode toNode, boolean skipGlobal)
throws AmbiguousSchemaNameException
{
int type = EntityName.TABLE;
// Try to find the "from" node in any scope.
String fromName = fromTable;
// look first for a persistent table in the schema global scope; a persistent table takes
// precedence over a temp-table with the same name, unless this behavior is explicitly
// overridden (e.g. DEFINE BUFFER x FOR TEMP-TABLE y)
NameNode fromNode = null;
if (!skipGlobal)
{
try
{
fromNode = findEntry(fromTable, type, SCHEMA_GLOBAL_SCOPE, SCHEMA_GLOBAL_SCOPE, false);
}
catch (AmbiguousSchemaNameException exc)
{
// if an unambiguous match for a persistent table could not be found, allow fromNode
// to remain null
}
}
// if not found there, look for a temp-table in a higher scope
if (fromNode == null)
{
fromNode = findEntry(fromTable, type, scopes.size() - 1, USER_GLOBAL_SCOPE, false);
}
// Make sure the node we found is not our "to" node. This can happen if the "to" node is
// named the same as the "from" node, and was not qualified to differentiate it when it was
// added. Also ensure that the "from" node is not a buffer; we need the backing table.
int i = scopes.size() - 2;
while ((fromNode == toNode || (fromNode != null && fromNode.getType() == BUFFER)) &&
i >= SCHEMA_GLOBAL_SCOPE)
{
boolean decrement = true;
if (fromNode != toNode && fromNode.getType() == BUFFER)
{
String tableName = fromNode.getAst().getParent().getText();
if (!tableName.equals(fromName))
{
fromName = tableName;
decrement = false;
}
}
fromNode = findEntry(fromName, type, i, SCHEMA_GLOBAL_SCOPE, true);
if (decrement)
{
i--;
}
}
return fromNode;
}
/**
* Copies all of the field entries currently stored in {@code fromNode} into the specified
* target table node.
*
* @param fromTable
* Name of table from which field entries are to be copied. Must
* be an unambiguous reference within the global scope.
* @param fromNode
* Name node which represents the table from which the field entries
* will be copied.
* @param toNode
* Name node which represents the table to which the field entries
* will be copied.
* @param forceTemp
* if (@code true}, do not look in the global schema scope to find a "real" table to
* replace the temp/work-table {@code fromNode} previously resolved.
*
* @throws AmbiguousSchemaNameException
* if <code>table</code> matches more than one entry in the
* global scope of the dictionary.
* @throws SchemaException
* if <code>fromTable</code> does not exist in the global scope.
*/
public void addFieldEntries(String fromTable, NameNode fromNode, NameNode toNode, boolean forceTemp)
throws SchemaException
{
addEntries(fromTable, fromNode, toNode, EntityName.TABLE, forceTemp);
Iterator<Aast> fromIter = fields(fromNode.getAst());
Iterator<Aast> toIter = fields(toNode.getAst());
List<Aast> fromFields = new ArrayList<>();
List<Aast> toFields = new ArrayList<>();
while (toIter.hasNext() && fromIter.hasNext())
{
Aast fromField = fromIter.next();
Aast toField = toIter.next();
fromFields.add(fromField);
toFields.add(toField);
}
Comparator<Aast> orderSort = (n1, n2) ->
{
if (n1.isAnnotation("order"))
{
return (int) ((long) n1.getAnnotation("order") - (long) n2.getAnnotation("order"));
}
else if (n1.downPath(ORDER))
{
Aast o1 = (Aast) n1.getImmediateChild(ORDER, null).getFirstChild();
Aast o2 = (Aast) n2.getImmediateChild(ORDER, null).getFirstChild();
return Integer.parseInt(o1.getText()) - Integer.parseInt(o2.getText());
}
else if (n1.isAnnotation("position"))
{
return (int) ((long) n1.getAnnotation("position") - (long) n2.getAnnotation("position"));
}
else if (n1.downPath(POSITION))
{
Aast o1 = (Aast) n1.getImmediateChild(POSITION, null).getFirstChild();
Aast o2 = (Aast) n2.getImmediateChild(POSITION, null).getFirstChild();
return Integer.parseInt(o1.getText()) - Integer.parseInt(o2.getText());
}
return n1.getIndexPos() - n2.getIndexPos();
};
Collections.sort(fromFields, orderSort);
Collections.sort(toFields, orderSort);
fromIter = fromFields.iterator();
toIter = toFields.iterator();
while (toIter.hasNext() && fromIter.hasNext())
{
Aast fromField = fromIter.next();
Aast toField = toIter.next();
String like = fromField.isAnnotation("like")
? (String) fromField.getAnnotation("like")
: fromNode.toQualifiedName() + "." + fromField.getText();
toField.putAnnotation("like", like);
}
// force all children to have the same line/col numbers as the table
// node, rather than the line/col nums from the copied (from) table
forceLineColumnNums(toNode.getAst());
}
/**
* Indicate whether the specified name represents a valid database in the
* dictionary.
*
* @param database
* Case insensitive database name; may not be abbreviated.
*
* @return <code>true</code> if an entry for this database is found in
* the dictionary, else <code>false</code>.
*/
public boolean isDatabase(String database)
{
boolean result = false;
try
{
NameNode node = findEntry(database,
EntityName.DATABASE,
USER_GLOBAL_SCOPE,
SCHEMA_GLOBAL_SCOPE,
true);
result = (node != null);
}
catch (IllegalArgumentException exc)
{
// Indicates that entity name was malformed.
// TODO: log error.
}
catch (AmbiguousSchemaNameException exc)
{
// TODO: log error.
}
return result;
}
/**
* Indicate whether the specified name represents a valid table in the dictionary.
*
* @param table
* Case insensitive table name; may be qualified or not. If
* qualified, only the table portion of the name may be
* abbreviated.
*
* @return <code>true</code> if an entry for this table is found in
* the dictionary, else <code>false</code>.
*/
public boolean isTable(String table)
{
return isTable(table, false, false);
}
/**
* Indicate whether the specified name represents a valid table in the dictionary.
* <p>
* If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
* with no preference for temp-table or persistent table.
*
* @param table
* Case insensitive table name; may be qualified or not. If
* qualified, only the table portion of the name may be
* abbreviated.
* @param forceTemp
* If <code>true</code>, force a temp-table lookup.
* @param forcePersistent
* If <code>true</code>, force a persistent table lookup.
*
* @return <code>true</code> if an entry for this table is found in
* the dictionary, else <code>false</code>.
*/
public boolean isTable(String table, boolean forceTemp, boolean forcePersistent)
{
boolean result = false;
try
{
int l1 = computeLevel(forceTemp, forcePersistent, true, scopes.size() - 1);
int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
NameNode node = findEntry(table, EntityName.TABLE, l1, l2, false);
result = (node != null);
}
catch (IllegalArgumentException exc)
{
// Indicates that entity name was malformed.
// TODO: log error.
}
catch (AmbiguousSchemaNameException exc)
{
// TODO: log error.
}
return result;
}
/**
* Indicate whether the specified exact name represents a valid field in the
* dictionary, exact name match required.
*
* @param field
* Case insensitive potential field name; may be qualified or not. No abbreviations allowed.
*
* @return <code>true</code> if an entry for this field is found in
* the dictionary, else <code>false</code>.
*/
public boolean isExactMatchField(String field)
{
boolean result = false;
try
{
result = (findEntry(field, EntityName.FIELD, scopes.size() - 1, SCHEMA_GLOBAL_SCOPE, true) != null);
}
catch (IllegalArgumentException exc)
{
// Indicates that entity name was malformed.
// TODO: log error.
}
catch (AmbiguousSchemaNameException exc)
{
// TODO: log error.
}
return result;
}
/**
* Indicate whether the specified name represents a valid field in the
* dictionary.
*
* @param field
* Case insensitive field name; may be qualified or not. If
* qualified, only the table and/or field portion of the name may
* be abbreviated.
*
* @return <code>true</code> if an entry for this field is found in
* the dictionary, else <code>false</code>.
*/
public boolean isField(String field)
{
boolean result = false;
try
{
result = (findEntry(field, EntityName.FIELD) != null);
}
catch (IllegalArgumentException exc)
{
// Indicates that entity name was malformed.
// TODO: log error.
}
catch (AmbiguousSchemaNameException exc)
{
// TODO: log error.
}
return result;
}
/**
* Perform a search for the specified table name and if found, return the
* table's fully qualified, unabbreviated name.
*
* @param table
* Case insensitive table name; may be qualified or not. If
* qualified, only the table portion of the name may be
* abbreviated.
*
* @return The table's fully qualified, unabbreviated name, or
* <code>null</code> if the lookup produced no match.
*
* @throws IllegalArgumentException
* if <code>table</code> is malformed.
* @throws AmbiguousSchemaNameException
* if <code>table</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* table entry was mistakenly added to the dictionary with the
* same name, or (b) <code>table</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
public String getFullTableName(String table)
throws AmbiguousSchemaNameException
{
return getFullTableName(table, false, false);
}
/**
* Perform a search for the specified table name and if found, return the
* table's fully qualified, unabbreviated name.
* <p>
* If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
* with no preference for temp-table or persistent table.
*
* @param table
* Case insensitive table name; may be qualified or not. If
* qualified, only the table portion of the name may be
* abbreviated.
* @param forceTemp
* If <code>true</code>, force a temp-table lookup.
* @param forcePersistent
* If <code>true</code>, force a persistent table lookup.
*
* @return The table's fully qualified, unabbreviated name, or
* <code>null</code> if the lookup produced no match.
*
* @throws IllegalArgumentException
* if <code>table</code> is malformed.
* @throws AmbiguousSchemaNameException
* if <code>table</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* table entry was mistakenly added to the dictionary with the
* same name, or (b) <code>table</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
public String getFullTableName(String table, boolean forceTemp, boolean forcePersistent)
throws AmbiguousSchemaNameException
{
int l1 = computeLevel(forceTemp, forcePersistent, true, nearestEnclosingInternal());
int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
return getQualifiedName(table, EntityName.TABLE, l1, l2);
}
/**
* Perform a search for the specified field name and if found, return the
* field's fully qualified, unabbreviated name.
*
* @param field
* Case insensitive field name; may be qualified or not. If
* qualified, only the table and/or field portion of the name may
* be abbreviated.
*
* @return The field's fully qualified, unabbreviated name, or
* <code>null</code> if the lookup produced no match.
*
* @throws IllegalArgumentException
* if <code>field</code> is malformed.
* @throws AmbiguousSchemaNameException
* if <code>field</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* field entry was mistakenly added to the dictionary with the
* same name, or (b) <code>field</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
public String getFullFieldName(String field)
throws AmbiguousSchemaNameException
{
return getQualifiedName(field, EntityName.FIELD, scopes.size() - 1, SCHEMA_GLOBAL_SCOPE);
}
/**
* Get the logical database name for the database associated with the
* specified table. If <code>table</code> includes a database qualifier,
* this is returned. Otherwise, the logical database name is determined
* by a lookup.
*
* @param table
* A possibly qualified, possibly abbreviated name of a table,
* buffer, temp-table, or work-table.
*
* @return The logical name of the database associated with the buffer
* represented by <code>table</code>. An empty string is returned
* if <code>table</code> represents a temp- or work-table.
*
* @throws IllegalArgumentException
* if <code>table</code> is malformed.
* @throws AmbiguousSchemaNameException
* if <code>table</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* table entry was mistakenly added to the dictionary with the
* same name, or (b) <code>table</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
public String getLogicalDatabaseForTable(String table)
throws AmbiguousSchemaNameException
{
return getLogicalDatabaseForTable(table, false, false);
}
/**
* Get the logical database name for the database associated with the
* specified table. If <code>table</code> includes a database qualifier,
* this is returned. Otherwise, the logical database name is determined
* by a lookup.
* <p>
* If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
* with no preference for temp-table or persistent table.
*
* @param table
* A possibly qualified, possibly abbreviated name of a table,
* buffer, temp-table, or work-table.
* @param forceTemp
* If <code>true</code>, force a temp-table lookup.
* @param forcePersistent
* If <code>true</code>, force a persistent table lookup.
*
* @return The logical name of the database associated with the buffer
* represented by <code>table</code>. An empty string is returned
* if <code>table</code> represents a temp- or work-table.
*
* @throws IllegalArgumentException
* if <code>table</code> is malformed.
* @throws AmbiguousSchemaNameException
* if <code>table</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* table entry was mistakenly added to the dictionary with the
* same name, or (b) <code>table</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
public String getLogicalDatabaseForTable(String table, boolean forceTemp, boolean forcePersistent)
throws AmbiguousSchemaNameException
{
EntityName eName = new EntityName(EntityName.TABLE, table);
String dbName = eName.getDatabase();
if (dbName != null)
{
return dbName;
}
int l1 = computeLevel(forceTemp, forcePersistent, true, nearestEnclosingInternal());
int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
NameNode node = findEntry(table, EntityName.TABLE, l1, l2, false);
NameNode dbNode = node.getParent();
return (dbNode != null ? dbNode.getName() : "");
}
/**
* Get the logical database name for the database associated with the
* specified field. If <code>field</code> includes a database qualifier,
* this is returned. Otherwise, the logical database name is determined
* by a lookup.
*
* @param field
* A possibly qualified, possibly abbreviated name of a field.
*
* @return The logical name of the database associated with the buffer
* represented by <code>field</code>'s parent. An empty string is
* returned if <code>field</code> is contained within a temp- or
* work-table.
*
* @throws IllegalArgumentException
* if <code>field</code> is malformed.
* @throws AmbiguousSchemaNameException
* if <code>field</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* field entry was mistakenly added to the dictionary with the
* same name, or (b) <code>field</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
public String getLogicalDatabaseForField(String field)
throws AmbiguousSchemaNameException
{
EntityName eName = new EntityName(EntityName.FIELD, field);
String dbName = eName.getDatabase();
if (dbName != null)
{
return dbName;
}
NameNode node = findEntry(field, EntityName.FIELD, scopes.size() - 1, SCHEMA_GLOBAL_SCOPE, false);
NameNode dbNode = node.getParent().getParent();
return (dbNode != null ? dbNode.getName() : "");
}
/**
* Get the unabbreviated buffer name associated with the possibly
* abbreviated name <code>table</code>. If a database alias is provided,
* it is passed through to the result unaltered. Otherwise, the buffer's
* default database qualifier is prepended.
*
* @param table
* A possibly qualified, possibly abbreviated name of a table,
* buffer, temp-table, or work-table.
*
* @return The unabbreviated form of <code>table</code>, using the same
* database qualifier, if any.
*
* @throws IllegalArgumentException
* if <code>table</code> is malformed.
* @throws AmbiguousSchemaNameException
* if <code>table</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* table entry was mistakenly added to the dictionary with the
* same name, or (b) <code>table</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
public String getBufferForTable(String table)
throws AmbiguousSchemaNameException
{
return getBufferForTable(table, false, false);
}
/**
* Get the unabbreviated buffer name associated with the possibly
* abbreviated name <code>table</code>. If a database alias is provided,
* it is passed through to the result unaltered. Otherwise, the buffer's
* default database qualifier is prepended.
* <p>
* If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
* with no preference for temp-table or persistent table.
*
* @param table
* A possibly qualified, possibly abbreviated name of a table,
* buffer, temp-table, or work-table.
* @param forceTemp
* If <code>true</code>, force a temp-table lookup.
* @param forcePersistent
* If <code>true</code>, force a persistent table lookup.
*
* @return The unabbreviated form of <code>table</code>, using the same
* database qualifier, if any.
*
* @throws IllegalArgumentException
* if <code>table</code> is malformed.
* @throws AmbiguousSchemaNameException
* if <code>table</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* table entry was mistakenly added to the dictionary with the
* same name, or (b) <code>table</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
public String getBufferForTable(String table, boolean forceTemp, boolean forcePersistent)
throws AmbiguousSchemaNameException
{
int l1 = computeLevel(forceTemp, forcePersistent, true, nearestEnclosingInternal());
int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
NameNode node = findEntry(table, EntityName.TABLE, l1, l2, false);
return getRecordName(node, EntityName.TABLE, table);
}
/**
* Get the unabbreviated buffer name associated with the possibly
* abbreviated name <code>field</code>. If a database alias is provided,
* it is passed through to the result unaltered. Otherwise, the buffer's
* default database qualifier is prepended.
*
* @param field
* A possibly qualified, possibly abbreviated name of a field.
*
* @return The unabbreviated form of <code>field</code>, using the same
* database qualifier, if any, with the field portion discarded.
*
* @throws IllegalArgumentException
* if <code>field</code> is malformed.
* @throws AmbiguousSchemaNameException
* if <code>field</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* field entry was mistakenly added to the dictionary with the
* same name, or (b) <code>field</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
public String getBufferForField(String field)
throws AmbiguousSchemaNameException
{
return getBufferForField(field, false, false);
}
/**
* Get the unabbreviated buffer name associated with the possibly
* abbreviated name <code>field</code>. If a database alias is provided,
* it is passed through to the result unaltered. Otherwise, the buffer's
* default database qualifier is prepended.
* <p>
* If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
* with no preference for temp-table or persistent table.
*
* @param field
* A possibly qualified, possibly abbreviated name of a field.
* @param forceTemp
* If <code>true</code>, force a temp-table lookup.
* @param forcePersistent
* If <code>true</code>, force a persistent table lookup.
*
* @return The unabbreviated form of <code>field</code>, using the same
* database qualifier, if any, with the field portion discarded.
*
* @throws IllegalArgumentException
* if <code>field</code> is malformed.
* @throws AmbiguousSchemaNameException
* if <code>field</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* field entry was mistakenly added to the dictionary with the
* same name, or (b) <code>field</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
public String getBufferForField(String field, boolean forceTemp, boolean forcePersistent)
throws AmbiguousSchemaNameException
{
int l1 = computeLevel(forceTemp, forcePersistent, true, scopes.size() - 1);
int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
NameNode node = findEntry(field, EntityName.FIELD, l1, l2, false);
return getRecordName(node, EntityName.FIELD, field);
}
/**
* Perform a search for the specified table name and if found, return the {@link NameNode}
* associated with it.
* <p>
* If both forceTemp and forcePersistent are {@code false}, then the lookup is performed
* with no preference for temp-table or persistent table.
*
* @param name
* Case insensitive table name; may be qualified or not. If qualified, only the table
* portion of the name may be abbreviated.
* @param forceTemp
* If {@code true}, force a temp-table lookup.
* @param forcePersistent
* If {@code true}>, force a persistent table lookup.
*
* @return The {@code NameNode} associated with the given table name, or {@code null} if the
* lookuup produced no match.
*
* @throws AmbiguousSchemaNameException
* if {@code table} matches more than one entry in the dictionary. This would
* indicate that either (a) more than one table entry mistakenly was added to the
* dictionary with the same name, or (b) {@code table} represents an ambiguous
* abbreviation that matches more than one entry.
* @throws SchemaException
* if no lookup scopes exist.
*/
public NameNode findTableNode(String name, boolean forceTemp, boolean forcePersistent)
throws SchemaException
{
int l1 = computeLevel(forceTemp, forcePersistent, true, scopes.size() - 1);
int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
NameNode node = findEntry(name, EntityName.TABLE, l1, l2, false);
return node;
}
/**
* Perform a search for the specified table name and if found, return the table's type (i.e.,
* schema table, temp table, work table, buffer).
* <p>
* If both forceTemp and forcePersistent are {@code false}, then the lookup is performed
* with no preference for temp-table or persistent table.
*
* @param name
* Case insensitive table name; may be qualified or not. If qualified, only the table
* portion of the name may be abbreviated.
* @param forceTemp
* If {@code true}, force a temp-table lookup.
* @param forcePersistent
* If {@code true}>, force a persistent table lookup.
*
* @return The table's type will be {@link #UNKNOWN} if the lookup produced no match.
*
* @throws AmbiguousSchemaNameException
* if {@code table} matches more than one entry in the dictionary. This would
* indicate that either (a) more than one table entry mistakenly was added to the
* dictionary with the same name, or (b) {@code table} represents an ambiguous
* abbreviation that matches more than one entry.
* @throws SchemaException
* if no lookup scopes exist.
*/
public int getTableType(String name, boolean forceTemp, boolean forcePersistent)
throws SchemaException
{
NameNode node = findTableNode(name, forceTemp, forcePersistent);
return (node == null ? UNKNOWN : node.getType());
}
/**
* Get the Progress parser token type of a table, temp-table, work-table,
* or buffer. If <code>name</code> represents a buffer, the type of the
* backing record is returned. That is, a type of <code>BUFFER</code> is
* never returned, only the type of the record which backs the buffer.
*
* @param name
* Case insensitive record name; may be qualified or not. If
* qualified, only the table portion of the name may be
* abbreviated.
*
* @return One of the Progress parser token types <code>TABLE</code>,
* <code>TEMP_TABLE</code>, or <code>WORK_TABLE</code>, or
* <code>UNKNOWN</code> if <code>name</code> cannot be found.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> is an ambiguous identifier for the
* target record.
*/
public int getTableRecordType(String name)
throws AmbiguousSchemaNameException
{
return getTableRecordType(name, false, false);
}
/**
* Get the Progress parser token type of a table, temp-table, work-table,
* or buffer. If <code>name</code> represents a buffer, the type of the
* backing record is returned. That is, a type of <code>BUFFER</code> is
* never returned, only the type of the record which backs the buffer.
* <p>
* If both forceTemp and forcePersistent are <code>false</code>, then the lookup is performed
* with no preference for temp-table or persistent table.
*
* @param name
* Case insensitive record name; may be qualified or not. If
* qualified, only the table portion of the name may be
* abbreviated.
* @param forceTemp
* If <code>true</code>, force a temp-table lookup.
* @param forcePersistent
* If <code>true</code>, force a persistent table lookup.
*
* @return One of the Progress parser token types <code>TABLE</code>,
* <code>TEMP_TABLE</code>, or <code>WORK_TABLE</code>, or
* <code>UNKNOWN</code> if <code>name</code> cannot be found.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> is an ambiguous identifier for the
* target record.
*/
public int getTableRecordType(String name, boolean forceTemp, boolean forcePersistent)
throws AmbiguousSchemaNameException
{
int l1 = computeLevel(forceTemp, forcePersistent, true, scopes.size() - 1);
int l2 = computeLevel(forceTemp, forcePersistent, false, SCHEMA_GLOBAL_SCOPE);
// Look up record name node.
NameNode node = findEntry(name, EntityName.TABLE, l1, l2, false);
int ttype = UNKNOWN;
if (node != null)
{
// Get record type; getTable() method automatically resolves a
// buffer reference to its backing table.
ttype = getTable(node).getType();
}
return ttype;
}
/**
* Get the Progress parser token type of a table, temp-table, work-table,
* or buffer which contains the field represented by <code>name</code>. If
* the record containing field <code>name</code> is a buffer, the type of
* the backing record is returned. That is, a type of <code>BUFFER</code>
* is never returned, only the type of the record which backs the buffer.
*
* @param name
* Case insensitive field name; may be qualified or not. If
* qualified, only the table and field portions of the name may
* be abbreviated.
*
* @return One of the Progress parser token types <code>TABLE</code>,
* <code>TEMP_TABLE</code>, or <code>WORK_TABLE</code>, or
* <code>UNKNOWN</code> if <code>name</code> cannot be found.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> is an ambiguous identifier for the
* target field.
*/
public int getFieldRecordType(String name)
throws AmbiguousSchemaNameException
{
// Look up field name node.
NameNode node = findEntry(name, EntityName.FIELD);
int ttype = UNKNOWN;
if (node != null)
{
// Get field's parent name node (a table, temp-table, work-table,
// or buffer).
NameNode parent = node.getParent();
if (parent != null)
{
// Get record type; getTable() method automatically resolves a
// buffer reference to its backing table.
ttype = getTable(parent).getType();
}
}
return ttype;
}
/**
* Perform a search for the specified field name and if found, return the field's data type.
*
* @param tableNode
* An optional, private name node for a table, within which to limit the lookup.
* If {@code null}, the normal node search algorithm is used.
* @param fieldName
* Case insensitive field name; may be qualified or not. If qualified, only the table
* and/or field portion of the name may be abbreviated.
*
* @return The field's data type; will be type {@code UNKNOWN} if the lookup produced no
* match.
*
* @throws AmbiguousSchemaNameException
* if {@code field} matches more than one entry in the dictionary. This would
* indicate that either (a) more than one field entry mistakenly was added to the
* dictionary with the same name, or (b) {@code field} represents an ambiguous
* abbreviation that matches more than one entry.
* @throws SchemaException
* if no lookup scopes exist.
*/
public int getFieldDataType(NameNode tableNode, String fieldName)
throws SchemaException
{
NameNode fieldNode = findFieldNode(tableNode, fieldName);
return (fieldNode == null ? UNKNOWN : fieldNode.getType());
}
/**
* Push a new scope onto the lookup stack. When performing a lookup,
* scopes are searched in LIFO (last in first out) order. The scope will
* be marked as one that is not an internal procedure, user-defined
* function or trigger.
*/
public void addScope()
{
addScope(false);
}
/**
* Push a new scope onto the lookup stack. When performing a lookup,
* scopes are searched in LIFO (last in first out) order.
*
* @param mark
* <code>true</code> to mark the associated scope as one which
* is an internal procedure, user-defined function or trigger.
* <code>false</code> for any other case including external
* procedures and inner blocks (<code>DO, REPEAT or FOR</code>).
*/
public void addScope(boolean mark)
{
scopes.push(new Scope(scopes.size(), mark));
qualifiers.addScope(null);
}
/**
* Pop the most recently added scope off the lookup stack. Does not permit
* popping of protected scopes. No propagation of namespace contents will
* occur.
*
* @throws SchemaException
* if the stack is already empty.
*/
public void removeScope()
throws SchemaException
{
removeScope(false);
}
/**
* Pop the most recently added scope off the lookup stack. Does not permit
* popping of protected scopes.
*
* @param propagate
* <code>true</code> to force the propagation of namespace
* contents from the removed scope into the scope that is now
* on the top of the stack (the enclosing scope).
*
* @throws SchemaException
* if the stack is already empty.
*/
public void removeScope(boolean propagate)
throws SchemaException
{
if (scopes.size() == protectedScopeCount)
{
throw new SchemaException("No scope available to remove");
}
// No need to catch EmptyStackException; the above check has handled
// that...
Scope oldScope = scopes.pop();
if (propagate)
{
// Copy the contents from the old to the new.
propagate(oldScope, scopes.peek());
}
qualifiers.deleteScope();
}
/**
* Persist schema dictionary information which is specific to the source
* file currently being parsed. For now, only temp-table and work-table
* schema data is preserved; buffer schema data is transient, and the
* regular schema is persisted separately and shared across all source
* files.
*
* @param srcFile
* Name of the 4GL source code file containing the schema
* definitions being persisted.
* @param schemaFile
* Name of schema persistence file.
*
* @throws AstException
* if any error occurs writing persistence file.
*/
public void persist(String srcFile, String schemaFile)
throws AstException
{
List<Aast> list = new ArrayList<>();
// Check for temp tables to save.
Iterator<Aast> iter = tables(TEMP_DB);
if (iter.hasNext())
{
list.add(databases.get(TEMP_DB));
}
// Check for work tables to save.
iter = tables(WORK_DB);
if (iter.hasNext())
{
list.add(databases.get(WORK_DB));
}
if (list.isEmpty())
{
// No tables to save.
return;
}
String normal = Configuration.normalizeFilename(schemaFile);
// Collect all tables under one, unified, database AST. Save the
// original 4GL source filename in the root node.
Aast db = new ProgressAst();
db.setType(DATABASE);
db.setText(TEMP_TABLE_DB);
db.putAnnotation("srcfile", srcFile);
db.brainwash(normal);
AstManager.get().setFlag(db.getId(), AstGenerator.FLAG_DICT);
// Graft all temp/work tables to the new database AST. IDs are fixed
// up as part of the graft.
iter = list.iterator();
while (iter.hasNext())
{
Aast nextDb = iter.next();
Aast nextChild = (Aast) nextDb.getFirstChild();
while (nextChild != null)
{
if (nextChild.getFilename() == null)
{
// only tables which were added in this parse, avoid the tables loaded from super-classes.
switch (nextChild.getType())
{
case TEMP_TABLE:
case WORK_TABLE:
nextChild.graftCopyTo(db, null);
break;
}
}
nextChild = (Aast) nextChild.getNextSibling();
}
}
if (db.getNumImmediateChildren() != 0)
{
AstManager.get().saveTree(db, schemaFile, false);
}
}
/**
* Debug method to dump the contents of each scope currently defined in
* the dictionary. Scopes are printed from innermost to outermost, the same
* order in which they are searched. Only the global namespace contents of
* each scope are dumped.
*
* @param stream
* Destination stream.
*/
public void dump(PrintStream stream)
{
// Dump aliases.
stream.println("ALIASES:");
stream.println();
stream.println(" " + aliases);
stream.println();
// Dump scopes.
int size = scopes.size();
ListIterator<Scope> iter = scopes.listIterator(size);
for (int i = size - 1; iter.hasPrevious(); i--)
{
stream.println("Scope " + i + ":");
stream.println();
iter.previous().dump(stream);
}
}
/**
* Load a persisted schema AST from the specified file. This is intended
* primarily to load temp- and work-tables into an instance of the
* dictionary which already has a primary schema loaded.
*
* @param schemaFile
* The name of a persisted schema file which is associated with
* a Progress source code file.
*
* @throws SchemaException
* if any error occurs reading the schema data from persistence.
*/
public void loadFromPersistence(String schemaFile)
throws SchemaException
{
try
{
loadFromAst(AstManager.get().loadTree(schemaFile), false);
}
catch (AstException exc)
{
throw new SchemaException("Error reading schema data from " + schemaFile, exc);
}
}
/**
* Retrieve the AST corresponding with a table property.
*
* @param name
* Name of the target table. Can be qualified or unqualified,
* abbreviated or unabbreviated, but must be unambiguous.
* @param tokenType
* Token type of the property to retrieve.
*
* @return AST representing the requested property, or <code>null</code>
* if the property was not found.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> is ambiguous as an identifier for the
* target table.
* @throws SchemaException
* if a table with the name <code>name</code> cannot be found.
*/
public Aast getTableProperty(String name, int tokenType)
throws SchemaException
{
// Table properties exist under an artificial PROPERTIES node, which
// is a child of the table.
Aast props = getTable(name).getImmediateChild(PROPERTIES, null);
if (props == null)
{
return null;
}
return props.getImmediateChild(tokenType, null);
}
/**
* Get all the AST fields for this table.
*
* @param name
* The name of the table.
*
* @return See above.
*
* @throws SchemaException
* If no table is found with the given name.
*/
public List<Aast> getAllFields(String name)
throws SchemaException
{
List<Aast> fields = new ArrayList<>();
Aast table = getTable(name);
Aast field = table.getImmediateChild(FIELD_TYPE_LIST, null);
while (field != null)
{
fields.add(field);
// Get next field.
field = table.getImmediateChild(FIELD_TYPE_LIST, field);
}
return fields;
}
/**
* Given a table name, retrieve a map of the ASTs corresponding with all
* field properties of a particular type. This method returns field-level
* properties only, even if <code>tokenType</code> might represent a
* table-level property as well. To retrieve a table-level property, use
* {@link #getTableProperty}.
*
* @param name
* Name of the <i>table</i> containing the target fields. Can be
* qualified or unqualified, abbreviated or unabbreviated, but
* must be unambiguous.
* @param tokenType
* Token type of the property to retrieve.
*
* @return Map containing the ASTs representing the requested properties,
* or an empty map if the property was not found in any field in
* the designated table. The map's keys are the AST instances
* for the fields (i.e., the parent ASTs of the property ASTs).
* Never returns <code>null</code>.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> is ambiguous as an identifier for the
* target field.
* @throws SchemaException
* if a field with the name <code>name</code> cannot be found.
*/
public Map<Aast, Aast> getAllFieldProperties(String name, int tokenType)
throws SchemaException
{
Map<Aast, Aast> props = new IdentityHashMap<>();
Aast table = getTable(name);
Aast field = table.getImmediateChild(FIELD_TYPE_LIST, null);
while (field != null)
{
Aast prop = field.getImmediateChild(tokenType, null);
if (prop != null)
{
props.put(field, prop);
}
// Get next field.
field = table.getImmediateChild(FIELD_TYPE_LIST, field);
}
return props;
}
/**
* Retrieve the AST corresponding with a field property.
*
* @param name
* Name of the target field. Can be qualified or unqualified,
* abbreviated or unabbreviated, but must be unambiguous.
* @param tokenType
* Token type of the property to retrieve.
*
* @return AST representing the requested property, or <code>null</code>
* if the property was not found.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> is ambiguous as an identifier for the
* target field.
* @throws SchemaException
* if a field with the name <code>name</code> cannot be found.
*/
public Aast getFieldProperty(String name, int tokenType)
throws SchemaException
{
Aast field = getField(name);
if (field == null)
{
throw new SchemaException("Field '" + name + "' not found");
}
return field.getImmediateChild(tokenType, null);
}
/**
* Copy a subset of properties from AST <code>like</code> into the field
* AST represented by <code>name</code>. Only those properties which are
* relevant to schema fields are copied; all others are discarded.
*
* @param name
* Name of the target field. Can be qualified or unqualified,
* abbreviated or unabbreviated, but must be unambiguous.
* @param like
* AST containing the field properties to be copied.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> is ambiguous as an identifier for the
* target field.
* @throws SchemaException
* if a field with the name <code>name</code> cannot be found.
*/
public void setFieldProperties(String name, Aast like)
throws SchemaException
{
NameNode node = findEntry(name, EntityName.FIELD);
if (node == null)
{
throw new SchemaException("Field cannot be found: " + name);
}
copyFieldProperties(like, node.getAst());
// link the source and fabricated field nodes to allow to link field references to the original
// source field definition nodes, which happens later during parsing
// TODO: There is a similar approach using tempIdx to implement deferred refid support, consider
// merging this implementation with tempIdx.
UUID uuid = UUID.randomUUID();
long defidLow = uuid.getLeastSignificantBits();
long defidHigh = uuid.getMostSignificantBits();
like.putAnnotation("fdefid-low", defidLow);
like.putAnnotation("fdefid-high", defidHigh);
node.getAst().putAnnotation("fdefid-low", defidLow);
node.getAst().putAnnotation("fdefid-high", defidHigh);
}
/**
* Copy the specified property into the <code>PROPERTIES</code> child AST
* of <code>table</code>. It is assumed that <code>table</code> does not
* already contain a property of the same type as <code>property</code>,
* as it is expected that this method is invoked only by the Progress
* parser as it builds up a temp- or work-table definition from valid
* Progress source code.
*
* @param table
* AST representing the target table.
* @param property
* AST representing the property to be copied.
*
* @throws SchemaException
* if any error occurs copying the property.
*/
public void setTableProperty(Aast table, Aast property)
throws SchemaException
{
Aast props = table.getImmediateChild(PROPERTIES, null);
if (props == null)
{
props = makePropertiesAst(table);
}
try
{
property.graftCopyTo(props, null);
}
catch (Exception exc)
{
throw new SchemaException("Error setting table property", exc);
}
}
/**
* Disable or enable unique field matches when abbreviations are used - if ambiguous, it will
* return the first found field which matches the prefix. Used by CAN-FIND statements.
*
* @param unique
* When <code>true</code>, it forces an unique match; otherwise, first field is
* return, when abbreviations are used.
*
* @return The previous flag state.
*/
public boolean setUniqueFieldMatch(boolean unique)
{
boolean old = uniqueFieldMatch;
uniqueFieldMatch = unique;
return old;
}
/**
* Set the {@link #markPreferred} flag, so all info for the tables which are weak references
* will be collected in the {@link Scope#preferredNodes} set for the current scope.
*
* @param preferred
* The new value of the flag.
*
* @return The old value of the {@link #markPreferred} field.
*/
public boolean markPreferred(boolean preferred)
{
boolean old = this.markPreferred;
this.markPreferred = preferred;
return old;
}
/**
* Set the strong scope flag for the current scope, so all info for the tables which are
* strong references will be stored for special disambiguation.
*
* @param strong
* The new value of the flag.
*/
public void markStrongScope(boolean strong)
{
this.markStrongScope = strong;
}
/**
* Load the schema dictionary from an AST of schema information ultimately
* imported from a Progress export file. For each database, table, and
* field, create a companion name node entry to allow namespace lookups.
*
* @param root
* Root of the AST representing a database. This is added to the
* map of databases managed by the dictionary. Its nodes are
* walked to create the name node mirror hierarchy.
* @param dictdb
* <code>true</code> to assign the implicit DICTDB alias to this database,
* else <code>false</code>.
*/
public void loadFromAst(Aast root, boolean dictdb)
{
loadFromAst(root, dictdb, true);
}
/**
* Load the schema dictionary from an AST of schema information ultimately
* imported from a Progress export file. For each database, table, and
* field, create a companion name node entry to allow namespace lookups.
*
* @param root
* Root of the AST representing a database. This is added to the
* map of databases managed by the dictionary. Its nodes are
* walked to create the name node mirror hierarchy.
* @param dictdb
* <code>true</code> to assign the implicit DICTDB alias to this database,
* else <code>false</code>.
* @param global
* <code>true</code> to add entry to global scope, else
* <code>false</code> to add entry to top scope. Only considered
* if <code>parent</code> is not specified; otherwise, entry is
* added to scope in which <code>parent</code> resides.
*/
public void loadFromAst(Aast root, boolean dictdb, boolean global)
{
databases.put(root.getText(), root);
NameNode lastDb = null;
NameNode lastTable = null;
Iterator<Aast> iter = root.iterator();
while (iter.hasNext())
{
Aast next = iter.next();
switch (next.getType())
{
case DATABASE:
if (next.getParent() != null)
{
break;
}
String name = next.getText().toLowerCase();
if (dictdb)
{
aliases.put(DICTDB, name);
}
aliases.put(name, name);
lastDb = addEntry(null, next, EntityName.DATABASE, global);
break;
case TEMP_TABLE:
if (next.getParent().getType() == DATABASE)
{
Aast database = databases.get(TEMP_DB);
Aast copy = next.duplicate(null);
database.addChild(copy);
copy.setParent(database);
lastTable = addEntry(null, copy, EntityName.TABLE, global);
}
break;
case WORK_TABLE:
if (next.getParent().getType() == DATABASE)
{
Aast database = databases.get(WORK_DB);
Aast copy = next.duplicate(null);
database.addChild(copy);
copy.setParent(database);
lastTable = addEntry(null, copy, EntityName.TABLE, global);
}
break;
case TABLE:
if (next.getParent().getType() == DATABASE)
{
lastTable = addEntry(lastDb, next, EntityName.TABLE, global);
}
break;
case FIELD_BLOB:
case FIELD_CHAR:
case FIELD_CLASS:
case FIELD_CLOB:
case FIELD_COM_HANDLE:
case FIELD_DATE:
case FIELD_DATETIME:
case FIELD_DATETIME_TZ:
case FIELD_DEC:
case FIELD_HANDLE:
case FIELD_INT:
case FIELD_INT64:
case FIELD_LOGICAL:
case FIELD_RAW:
case FIELD_RECID:
case FIELD_ROWID:
case FIELD_BIGINT:
case FIELD_BYTE:
case FIELD_DOUBLE:
case FIELD_FIXCHAR:
case FIELD_FLOAT:
case FIELD_SHORT:
case FIELD_TIMESTAMP:
switch (next.getParent().getType())
{
case TABLE:
case TEMP_TABLE:
case WORK_TABLE:
addEntry(lastTable, next, EntityName.FIELD, global);
break;
}
break;
}
}
}
/**
* Merge the schema dictionary from an AST into this dictionary. This should be used for addition of nodes
* of mutable tables into a mutable database. For each database, table, and field, if the node does not
* exist, one is created otherwise the existing companion name node entry will allow namespace lookups.
*
* @param root
* Root of the AST representing a database. If missing, this is added to the map of databases
* managed by the dictionary. Its nodes are walked to create the name node mirror hierarchy.
*/
public void mergeFromAst(Aast root)
{
databases.putIfAbsent(root.getText(), root);
NameNode dbNode = null;
NameNode crtTable = null;
Iterator<Aast> iter = root.iterator();
while (iter.hasNext())
{
Aast next = iter.next();
switch (next.getType())
{
case DATABASE:
if (next.getParent() != null || dbNode != null)
{
break; // validation
}
String name = next.getText().toLowerCase();
aliases.putIfAbsent(name, name);
try
{
dbNode = findEntry(next.getText(), EntityName.DATABASE, 0/*GLOBAL*/, 0/*GLOBAL*/, true);
}
catch (AmbiguousSchemaNameException e)
{
// silent. This should really not happen. If it does, we will make it worse, next
}
if (dbNode == null)
{
dbNode = addEntry(null, next, EntityName.DATABASE, true);
}
break;
case TABLE:
if (next.getParent().getType() != DATABASE || dbNode == null)
{
break; // validation
}
crtTable = null;
try
{
crtTable = dbNode.getNamespace().find(next.getText(), true, null);
}
catch (AmbiguousSchemaNameException e)
{
// silent. This should really not happen. If it does, we will make it worse, next
}
if (crtTable == null)
{
crtTable = addEntry(dbNode, next, EntityName.TABLE, true);
}
break;
case FIELD_BLOB:
case FIELD_CHAR:
case FIELD_CLASS:
case FIELD_CLOB:
case FIELD_COM_HANDLE:
case FIELD_DATE:
case FIELD_DATETIME:
case FIELD_DATETIME_TZ:
case FIELD_DEC:
case FIELD_HANDLE:
case FIELD_INT:
case FIELD_INT64:
case FIELD_LOGICAL:
case FIELD_RAW:
case FIELD_RECID:
case FIELD_ROWID:
case FIELD_BIGINT:
case FIELD_BYTE:
case FIELD_DOUBLE:
case FIELD_FIXCHAR:
case FIELD_FLOAT:
case FIELD_SHORT:
case FIELD_TIMESTAMP:
if (next.getParent().getType() != TABLE || crtTable == null)
{
break; // validation
}
NameNode fieldNode = null;
try
{
fieldNode = crtTable.getNamespace().find(next.getText(), true, null);
}
catch (AmbiguousSchemaNameException e)
{
// silent. This should really not happen. If it does, we will make it worse, next
}
if (fieldNode == null)
{
addEntry(crtTable, next, EntityName.FIELD, true);
}
break;
}
}
}
/**
* Look up the name node for the field associated with the specified name. Searches all scopes
* from innermost to outermost. The search is halted as soon as a match is found, or when
* there are no more scopes left to search.
*
* @param name
* Case insensitive database, table, or field name; may be
* qualified or not. If qualified, only the table and/or field
* portion of the name may be abbreviated.
*
* @return see above
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> matches more than one field entry in the
* dictionary. This would indicate that either (a) more than one
* entry was mistakenly added to the dictionary with the same
* entity name, or (b) <code>name</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
public NameNode findFieldNode(String name)
throws AmbiguousSchemaNameException
{
return findEntry(name, EntityName.FIELD);
}
/**
* Compose an entity's fully qualified, unabbreviated name. For nodes
* which refer to buffers (vs. tables, temp-tables, or work-tables), this
* returns a name which includes the fully qualified, unabbreviated name
* of the table <strong>which backs</strong> the buffer, not the name of
* the buffer itself.
*
* @param node
* Name node of the entity which will determine the most specific
* part of the fully qualified name. Must not be
* <code>null</code>.
*
* @return The entity's fully qualified, unabbreviated name.
*
* @throws AmbiguousSchemaNameException
* if <code>node</code> matches more than one entry in the
* dictionary. This can only occur if <code>node</code> refers
* to a buffer, since a lookup only occurs in this case.
*/
String getQualifiedName(NameNode node)
throws AmbiguousSchemaNameException
{
node = getTableNodeIfBuffer(node);
StringBuilder buf = new StringBuilder(node.getName());
while ((node = node.getParent()) != null)
{
buf.insert(0, '.');
node = getTableNodeIfBuffer(node);
buf.insert(0, node.getName());
}
return buf.toString();
}
/**
* Compose a record name from the given name node, entity type, and raw
* name. The resulting name will have the same database qualifier (always
* lowercased) as <code>name</code>, or will prepend the database qualifier
* (if any) in the event <code>name</code> contains none.
*
* @param node
* Name node for a record or field entity.
* @param type
* <code>EntityName</code> constant indicating whether
* <code>name</code> represents a field or record level entity.
* @param name
* Raw name for the schema entity, possibly abbreviated, possibly
* qualified, as drawn directly from Progress source code.
*
* @return The unabbreviated form of <code>name</code>, using the same
* database qualifier, if any. If <code>name</code> represents
* a database field, the field portion of the name is discarded.
*/
String getRecordName(NameNode node, int type, String name)
{
if (node == null)
{
return null;
}
EntityName eName = new EntityName(type, name);
NameNode table = (type == EntityName.FIELD ? node.getParent() : node);
StringBuilder buf = new StringBuilder();
String database = eName.getDatabase();
if (database == null)
{
// If buffer found represents a promoted buffer with an explicit
// database qualifier, use that qualifier in preference to the
// default...
database = qualifiers.lookup(table);
// ...otherwise, use the default database name defined in the schema.
if (database == null)
{
NameNode dbNode = table.getParent();
if (dbNode != null)
{
database = dbNode.getName();
}
}
}
if (database != null)
{
buf.append(database.toLowerCase()).append('.');
}
buf.append(table.getName());
return buf.toString();
}
/**
* Initialize the schema dictionary by adding a global scope, loading the
* schema information in <code>root</code> into it, then adding a second
* scope into which user global data can be loaded (secondary databases,
* temp-tables, work-tables, etc.).
*
* @param root
* AST of primary database to load.
*/
private void initialize(Aast root)
{
// Create schema global scope.
addScope();
// Walk database AST to set internal state.
loadFromAst(root, true);
// Create user global scope.
addScope();
// Protect schema and user global scopes, such that they cannot be removed.
protectedScopeCount = scopes.size();
}
/**
* Force all the line and column numbers (for non-artificial nodes in the
* tree) to be the same as the root node which is provided here.
*
* @param root
* Parent node of the sub-tree which is to be processed.
*/
private void forceLineColumnNums(Aast root)
{
int line = root.getLine();
int col = root.getColumn();
Iterator<Aast> iter = root.iterator();
while (iter.hasNext())
{
Aast child = iter.next();
// only change those nodes that aren't artificial (those which
// already have a line/col number)
if (child.getLine() != 0 && child.getColumn() != 0)
{
child.setLine(line);
child.setColumn(col);
}
}
}
/**
* Look up the name node associated with the specified name and entity
* type. Searches all scopes from innermost to outermost. The search is
* halted as soon as a match is found, or when there are no more scopes
* left to search.
*
* @param name
* Case insensitive database, table, or field name; may be
* qualified or not. If qualified, only the table and/or field
* portion of the name may be abbreviated.
* @param type
* <code>EntityName</code> constant indicating whether name
* represents a <code>DATABASE</code>, <code>TABLE</code>, or
* <code>FIELD</code>.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* entry was mistakenly added to the dictionary with the same
* entity name, or (b) <code>name</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
private NameNode findEntry(String name, int type)
throws AmbiguousSchemaNameException
{
return findEntry(name, type, scopes.size() - 1, SCHEMA_GLOBAL_SCOPE, false);
}
/**
* Perform a search for the specified field name and if found, return the field's name node.
*
* @param tableNode
* An optional, private name node for a table, within which to limit the lookup.
* If {@code null}, the normal node search algorithm is used.
* @param field
* Case insensitive field name; may be qualified or not. If qualified, only the table
* and/or field portion of the name may be abbreviated.
*
* @return The field's name node, or {@code null} if the lookup produced no match.
*
* @throws AmbiguousSchemaNameException
* if {@code field} matches more than one entry in the dictionary. This would
* indicate that either (a) more than one field entry mistakenly was added to the
* dictionary with the same name, or (b) {@code field} represents an ambiguous
* abbreviation that matches more than one entry.
* @throws SchemaException
* if no lookup scopes exist.
*/
private NameNode findFieldNode(NameNode tableNode, String field)
throws SchemaException
{
NameNode fieldNode = null;
if (tableNode != null)
{
Namespace ns = tableNode.getNamespace();
fieldNode = ns.find(field, true, bannedSchemas);
}
if (fieldNode == null)
{
fieldNode = findEntry(field, EntityName.FIELD);
}
return fieldNode;
}
/**
* Iterate through the direct child nodes of <code>parent</code> and
* retrieve the first child whose token type matches <code>tokenType</code>
* and whose text matches <code>name</code>. If <code>name</code> is
* <code>null</code>, the first child node which matches the given token
* type is returned.
*
* @param parent
* AST whose immediate children are checked.
* @param tokenType
* Token type of target child node.
* @param name
* Text of target child node, or <code>null</code> to match the
* first child of the appropriate token type.
*
* @return The first child matching the criteria described above, or
* <code>null</code> if no match is found.
*/
private Aast getChildAst(Aast parent, int tokenType, String name)
{
Aast ast = null;
Aast next = (Aast) parent.getFirstChild();
// Iterate over children.
while (next != null && ast == null)
{
if (next.getType() == tokenType &&
(name == null || next.getText().equals(name)))
{
ast = next;
}
next = (Aast) next.getNextSibling();
}
return ast;
}
/**
* Add a new database AST to the map of databases.
*
* @param name
* Database name.
*
* @return Database object created.
*/
private Aast newDatabase(String name)
{
Aast ast = new ProgressAst();
ast.setText(name);
ast.setType(DATABASE);
databases.put(name, ast);
return ast;
}
/**
* Create a field AST and add it as a child of the AST associated with the
* <code>parent</code> name node.
*
* @param parent
* Table name node which represents this field's parent entity.
* @param fnode
* The node that triggered the field creation.
* @param name
* Name of field.
* @param tokenType
* Data type of field (e.g., integer, character, raw, etc.).
*
* @return Field object created.
*/
private Aast newField(NameNode parent, Aast fnode, String name, int tokenType)
{
ProgressAst ast = new ProgressAst();
ast.setType(tokenType);
ast.setText(name.toLowerCase());
ast.setLine(fnode.getLine());
ast.setColumn(fnode.getColumn());
ast.putAnnotation("historical", name);
Aast table = getTable(parent);
table.addChild(ast);
ast.setParent(table);
return ast;
}
/**
* Look up the name node associated with the specified name and entity type.
* Searches scopes in reverse order from {@code startScope} to {@code endScope}.
*
* @param name
* Case insensitive database, table, or field name; may be
* qualified or not. If qualified, only the table and/or field
* portion of the name may be abbreviated.
* @param type
* {@code EntityName} constant indicating whether name represents a
* {@code DATABASE}, {@code TABLE}, or {@code FIELD}.
* @param startScope
* Index of scope at which search begins. Must be greater than or
* equal to {@code endScope}.
* @param endScope
* Index of scope at which search ends. Must be greater than or
* equal to zero.
* @param exact
* {@code true} to require an exact match; {@code false} to allow an exact
* or an abbreviated name to match.
*
* @return Name node if found, else {@code null}.
*
* @throws IllegalArgumentException
* if the scope index range provided is invalid.
* @throws AmbiguousSchemaNameException
* if {@code name} matches more than one entry in the dictionary. This would
* indicate that either (a) more than one entry was mistakenly added to the
* dictionary with the same entity name, or (b) {@code name} represents an
* ambiguous abbreviation that matches more than one entry.
*/
private NameNode findEntry(String name, int type, int startScope, int endScope, boolean exact)
throws AmbiguousSchemaNameException
{
// Validate scope range.
if (startScope >= scopes.size() || startScope < endScope || endScope < 0)
{
throw new IllegalArgumentException(
"Invalid scope search range: " + startScope + " to " + endScope);
}
NameNode node = null;
EntityName entityName = new EntityName(type, name.toLowerCase());
if (preferDefaultBuffer && type == EntityName.FIELD && entityName.getTable() == null)
{
// if we are searching for a field node for an unqualified field name, and we are set to prefer the
// default buffer over any explicitly defined buffer, limit the search to USER_GLOBAL_SCOPE and
// lower scopes
startScope = USER_GLOBAL_SCOPE;
}
// Walk through all scopes from higher to lower and try to match the name in each one in turn.
// The loop ends as soon as we have a match (or if Scope.findNode throws AmbiguousSchemaNameException).
try
{
for (int i = startScope; node == null && i >= endScope; i--)
{
Scope scope = scopes.get(i);
node = scope.findNode(entityName, exact);
}
}
catch (AmbiguousSchemaNameException exc)
{
// if we had an ambiguous match on a metadata table or field with no database qualifier,
// prepend the implicit qualifier of the last connected database to the name and try again;
// otherwise, rethrow the exception
// We only do this disambiguation for metadata references; this is a side effect of the way we
// merge metadata schema into each primary database schema
if (entityName.getDatabase() == null && name.startsWith("_"))
{
String qualified = null;
String firstDatabase = getFirstDatabase();
switch (type)
{
case EntityName.TABLE:
qualified = String.format("%s.%s", firstDatabase, name);
break;
case EntityName.FIELD:
if (entityName.getTable() == null)
{
// check whether the ambiguity is at the table level, or only at the database level
String foundTable = null;
for (NameNode next : exc.getMatches())
{
// extract the table name from each match; if they are not all the same, the
// ambiguity is at the table level and we rethrow the exception
String table = next.getParent().getName();
if (foundTable == null)
{
foundTable = table;
}
else if (!table.equals(foundTable))
{
// this should not happen if the metadata schema is valid, but for sanity...
throw exc;
}
}
qualified = String.format("%s.%s.%s", firstDatabase, foundTable, name);
}
else
{
qualified = String.format("%s.%s", firstDatabase, name);
}
break;
default:
throw exc;
}
if (qualified != null)
{
return findEntry(qualified, type, startScope, endScope, exact);
}
}
throw exc;
}
return node;
}
/**
* Get the database qualifier for the first "connected" (i.e. loaded/configured) database, which is not
* a temp/work table database.
* <p>
* N.B. This is not the most efficient method, as it iterates all the entries in a linked hash map to get
* to the first one. However, it is anticipated that this method only will be used when resolving an
* exceptional condition, so it is not performance sensitive. If this changes, refactoring should be
* considered.
*
* @return The database qualifier for the first connected database.
*/
private String getFirstDatabase()
{
String firstDatabase = null;
for (String next : databases.keySet())
{
if ((bannedSchemas == null || !bannedSchemas.contains(next)) &&
!next.startsWith("@") &&
!next.equals("_temp"))
{
firstDatabase = next;
break;
}
}
return firstDatabase;
}
/**
* Searches scopes from most nested to least nested to find a
* <code>BUFFER</code> node that matches the given table name. If no
* match is found, the search stops at the nearest enclosing internal
* scope. If no internal scope is or encloses the current scope, then
* this search immediately fails.
*
* @param name
* Case insensitive table name; may be qualified or not. The
* table portion of the name may be abbreviated.
*
* @return <code>true</code> if there is a matching buffer scoped to an
* enclosing internal block.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* entry was mistakenly added to the dictionary with the same
* entity name, or (b) <code>name</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
private boolean findInternalBuffer(String name)
throws AmbiguousSchemaNameException
{
int start = scopes.size() - 1;
int end = nearestEnclosingInternal();
Scope last = scopes.get(end);
if (!last.isInternal())
{
// There is no internal scope so there is no internal buffer to find.
return false;
}
NameNode node = null;
EntityName entityName = new EntityName(EntityName.TABLE, name.toLowerCase());
// Walk through all scopes from higher to lower and try to match the
// name in each one in turn. The loop ends as soon as we have a match
// which is a BUFFER node (or if Scope.findNode throws an
// AmbiguousSchemaNameException).
for (int i = start; i >= end; i--)
{
Scope scope = scopes.get(i);
node = scope.findNode(entityName, false);
if (node != null && node.getType() == BUFFER)
{
return true;
}
}
// We didn't find a BUFFER node enclosed within an internal scope.
return false;
}
/**
* Searches scopes from most nested to least nested to find a matching
* <code>BUFFER</code> node that is associated with a weak scope.
*
* @param possible
* Node to test.
*
* @return <code>true</code> if there is a matching buffer scoped to an
* enclosing weak block.
*/
private boolean findWeakScopedBuffer(NameNode possible)
throws AmbiguousSchemaNameException
{
NameNode weak = null;
for (int i = scopes.size() - 1; i > EXT_PROC_SCOPE; i--)
{
Scope next = scopes.get(i);
weak = next.getWeakReference();
if (weak != null && weak.compareTo(possible) == 0)
{
return true;
}
}
// We didn't find a matching BUFFER node enclosed within a weak scope.
return false;
}
/**
* Add a name node entry to a specific scope. The target scope is
* determined by
* <ol>
* <li>the scope in which <code>parent</code> resides; or if this parameter
* is <code>null</code></li>
* <li>the scope determined by <code>globalScope</code>: the user global
* scope if <code>true</code>, else the innermost (most recently added)
* scope.</li>
* </ol>
* Within that scope, the new name node entry is added both to the
* appropriate global namespace and to the private namespace of its parent
* node, if any. The map of nodes to scopes is updated to reflect the
* addition.
*
* @param parent
* New entry's parent name node. If new entry represents a
* database, this should be <code>null</code>; if a table, this
* should be a database node or <code>null</code> (if added by
* parser); if a field, this should be a table node or
* <code>null</code> (if added by parser).
* @param ast
* AST which backs the new name node and contains property
* information specific to the schema entity represented by this
* name node.
* @param entityType
* <code>EntityName</code> constant indicating whether the new
* entry is a <code>DATABASE</code>, <code>TABLE</code>, or
* <code>FIELD</code>.
* @param globalScope
* <code>true</code> to add entry to global scope, else
* <code>false</code> to add entry to top scope. Only considered
* if <code>parent</code> is not specified; otherwise, entry is
* added to scope in which <code>parent</code> resides.
*
* @return Name node which was added as a dictionary entry.
*/
private NameNode addEntry(NameNode parent, Aast ast, int entityType, boolean globalScope)
{
// NameNode constructor automatically adds node to parent's namespace.
NameNode node = new NameNode(parent, ast);
// Also add node to global namespace of the target scope, but only if
// the parent is not a buffer, as this may interfere with unqualified
// lookups of identically named fields which are children of other
// record types in the same scope. The buffer's fields will be added
// to the global namespace of the proper scope once the buffer is
// promoted.
if (parent == null ||
entityType != EntityName.FIELD ||
parent.getType() != BUFFER)
{
int nodeType = ast.getType();
if (nodeType == TEMP_TABLE || nodeType == WORK_TABLE)
{
// new temp-table, init the lastOrder annotation
ast.putAnnotation("lastOrder", 0l);
}
else if (nodeType >= BEGIN_FIELDTYPES && nodeType <= END_FIELDTYPES)
{
Aast pAst = parent != null ? parent.getAst() : ast.getParent();
int pType = pAst.getType();
if (pType == TEMP_TABLE || pType == WORK_TABLE)
{
// for temp-table fields, explicitly adjust their order
long lastOrder = (Long) pAst.getAnnotation("lastOrder");
Aast oAst = null;
if (ast.downPath(ORDER))
{
oAst = ast.getImmediateChild(ORDER, null);
}
else
{
oAst = new ProgressAst();
oAst.setType(ORDER);
oAst.setText("ORDER");
ast.graft(oAst);
Aast nAst = new ProgressAst();
nAst.setType(NUM_LITERAL);
oAst.graft(nAst);
}
oAst.getFirstChild().setText(Long.toString(lastOrder));
lastOrder += 10;
pAst.putAnnotation("lastOrder", lastOrder);
}
}
Scope scope = getScope(parent, ast, globalScope);
// Buffers that are created here must not be propagated across
// internal scope boundaries. Force the exclude flag on in these
// cases.
scope.addOrReplaceNode(node, entityType, (node.getType() == BUFFER), globalScope);
// Update the scope map so we can quickly find the scope associated
// with the new name node. The scope object has a strong reference
// tie to key node, so wrap it in a WeakReference.
scopeMap.put(node, new WeakReference<>(scope));
}
return node;
}
/**
* Copies all of the child entries currently stored in {@code fromNode} into the specified
* target table node, and with the exception of buffers, into the global namespace of the
* scope containing the parent entity.
* <p>
* Record buffer types require some special handling here in the following cases:
* <ul>
* <li>If the destination node is a buffer and the source node is a
* temp-table, we have to make sure that the temp-table name node is
* not masking a real table of the same name. According to the Progress
* documentation, a buffer in this case will actually refer to the
* real table rather than the temp-table.
* <li>If the destination node is a buffer and the source node is a real
* table, we have to create a fake database node in the buffer's scope
* to represent that buffer's parent database. The fake database node
* (termed a "shadow") does not contain any children except buffer
* nodes. Since shadows reside in a higher scope than the "real"
* database nodes created at the schema level global scope, qualified
* references to buffers can be resolved. Qualified references to real
* tables can still be resolved (unless they are masked by identically
* named buffers) because they will not be matched at the higher scope,
* and the search will continue down to the schema global scope.
* </ul>
*
* @param from
* Name of entity from which child entries are to be copied.
* @param fromNode
* Actual name node which references the data structures to be copied.
* @param toNode
* Name node which represents the parent entity to which the child entries will be
* copied.
* @param type
* Type of the parent entity to which child entries are to be
* added (using {@link EntityName} constants).
* @param forceTemp
* if (@code true}, do not look in the global schema scope to find a "real" table to
* replace the temp/work-table {@code fromNode} previously resolved.
*
* @throws AmbiguousSchemaNameException
* if a database lookup produces more than one match.
* @throws SchemaException
* if there is any error copying fields.
*/
private void addEntries(String from, NameNode fromNode, NameNode toNode, int type, boolean forceTemp)
throws AmbiguousSchemaNameException,
SchemaException
{
switch (toNode.getType())
{
// SPECIAL HANDLING FOR BUFFERS
case BUFFER:
{
int fromType = fromNode.getType();
if (fromType == TEMP_TABLE || fromType == WORK_TABLE)
{
// forceTemp indicates we know the from table should be a temp/work table; only
// search in the schema global scope if set to false
if (!forceTemp)
{
// If fromNode is a temp table and toNode is a buffer, make
// sure fromNode is not masking a real table of the same name.
try
{
NameNode realTable = findEntry(from,
type,
SCHEMA_GLOBAL_SCOPE,
SCHEMA_GLOBAL_SCOPE,
true);
if (realTable != null)
{
fromNode = realTable;
}
}
catch (AmbiguousSchemaNameException exc)
{
// This answers our question: there is no real table by this name.
}
}
}
else if (fromType == TABLE)
{
// If fromNode is a "real" table, create a fake copy (a
// shadow) of its parent database node and store it in the
// same scope as the toNode buffer. This will allow qualified
// name lookups of this buffer to work. Note, however, that
// a shadow database node must only refer to buffers as its
// children and must be shared across all buffers in this
// scope, since multiples would create ambiguity at the
// database qualifier level.
// First get the scope from the scope map.
Scope scope = retrieveNode(toNode);
// Try to find an existing shadow by the name of the
// fromNode's parent.
int parentType = EntityName.DATABASE;
String parentName = fromNode.getParent().getName();
EntityName dbName = new EntityName(parentType, parentName);
NameNode shadow = scope.findNode(dbName, true);
// Create one if it did not already exist in this scope.
if (shadow == null)
{
Aast dbAst = databases.get(parentName);
shadow = new NameNode(null, dbAst);
// these nodes should not propagate since they are only
// associated with buffers
scope.addNode(shadow, parentType, true);
scopeMap.put(shadow, new WeakReference<>(scope));
}
// special handling for OO pre-scan mode: in this mode, we do not have the normal
// scoping behavior, so it is possible to define multiple buffers for the same
// backing table; we must disallow duplicates to avoid ambiguity downstream, and
// we preserve only the most recent association
Namespace shadowNS = shadow.getNamespace();
String toName = toNode.getName();
NameNode old = shadowNS.find(toName, true, bannedSchemas);
if (old != null)
{
shadowNS.remove(old);
old.getAst().setParent(null);
}
// assign the shadow database node as the parent of the buffer
toNode.setParent(shadow);
}
// Map buffer name to underlying table. We do this by setting the
// buffer's AST's parent to the backing table's AST, but we DO
// NOT set the buffer's AST as a child of the backing table's
// AST. This one-way relationship is exploited in other places
// to associate the backing table's properties with the buffer.
toNode.getAst().setParent(fromNode.getAst());
break;
}
// If the target represents a temp-table or work-table, we need to
// store the from table as the model table for possible follow-up
// index definitions and table and field options.
case TEMP_TABLE:
case WORK_TABLE:
{
modelTable = getTable(fromNode);
break;
}
}
addEntries(fromNode, toNode, type);
}
/**
* Copy properties from a source AST to a destination AST. The set of
* properties (essentially child AST nodes) copied may optionally be
* filtered by token type. It is assumed that <code>source</code> and
* <code>destination</code> are compatible types, such that the general
* AST structure is similar between them.
*
* @param source
* AST which is the source of the copy.
* @param destination
* AST which is the destination of the copy.
* @param filter
* An array of token types in ascending numeric order, which
* represents the set of tokens which are to be included in the
* copy. Any property with a token type not in this set is
* excluded from the copy. If this parameter is <code>null</code>,
* all properties are copied.
*
* @throws SchemaException
* if any error occurs during the copy.
*/
private void copyTableProperties(Aast source, Aast destination, int[] filter)
throws SchemaException
{
Aast srcProps = source.getImmediateChild(PROPERTIES, null);
Aast dstProps = destination.getImmediateChild(PROPERTIES, null);
if (srcProps == null)
{
// nothing to copy, we are done
return;
}
if (dstProps == null)
{
dstProps = makePropertiesAst(destination);
}
try
{
Aast next = (Aast) srcProps.getFirstChild();
while (next != null)
{
if (filter != null &&
Arrays.binarySearch(filter, next.getType()) >= 0)
{
next.graftCopyTo(dstProps, null);
}
next = (Aast) next.getNextSibling();
}
}
catch (AstException exc)
{
throw new SchemaException("Error copying table properties.", exc);
}
}
/**
* Copy FIELD properties from a source AST to a destination AST. The set
* of properties (essentially child AST nodes) copied will be limited to
* those that are valid in a field (filtered by token type). It is assumed
* that <code>source</code> and <code>destination</code> are compatible
* types, such that the general AST structure is similar between them.
* <p>
* If the <code>source</code> AST contains a type that already exists in
* the <code>destination</code>, the <code>source</code> AST will take
* precedence (the subtree of that same type in the destination will be
* overwritten).
*
* @param source
* AST which is the source of the copy.
* @param destination
* AST which is the destination of the copy.
*
* @throws SchemaException
* if any error occurs during the copy.
*/
private void copyFieldProperties(Aast source, Aast destination)
throws SchemaException
{
try
{
Aast next = (Aast) source.getFirstChild();
while (next != null)
{
int type = next.getType();
if (Arrays.binarySearch(FIELD_PROPS, type) >= 0)
{
boolean copy = true;
// special processing for disabling case-sensitive
if (type == KW_NOT)
{
// remove case-sensitivity if it exists, don't copy
type = KW_CASE_SEN;
copy = false;
}
// check if the type exists in the destination
Aast target = destination.getImmediateChild(type, null);
if (target != null)
{
// remove the target since the source AST should take
// precedence
target.remove();
}
// add to the end
if (copy)
next.graftCopyTo(destination, null);
}
next = (Aast) next.getNextSibling();
}
}
catch (AstException exc)
{
throw new SchemaException("Error copying field properties.", exc);
}
}
/**
* Calculate and save the signature for the table. Each field is represented by "datatype" or
* "datatype[extent]" format. The table level signature is all of the field signatures separated
* by "|" delimiter with the number and order of the fields matching the order in the AST.
*
* @param table
* The table AST substree.
*/
private void calculateSignature(Aast table)
{
String signature = "";
String idxSignature = "";
String idxName = "";
boolean isWord = false;
boolean isUnique = false;
boolean isPrimary = false;
boolean isInactive = false;
Map<String, Integer> fields = new HashMap<>();
Map<String, Integer> fieldTypes = new HashMap<>();
int idx = 0;
Iterator<Aast> iter = table.iterator();
// iterate through all fields
while (iter.hasNext())
{
Aast next = iter.next();
int type = next.getType();
// if this is a field, calculate the field's portion of the signature
if (type >= BEGIN_FIELDTYPES && type <= END_FIELDTYPES ||
type >= BEGIN_METATYPES && type <= END_METATYPES)
{
Aast ext = next.getImmediateChild(KW_EXTENT, null);
String extent = null;
// read the extent value if any
if (ext != null)
{
if (ext.getNumImmediateChildren() == 0)
{
String err = String.format("Indeterminate EXTENT in a temp-table definition for %s",
next.dumpTree());
// indeterminate extent does not make sense for a temp-table, should not happen
throw new RuntimeException(err);
}
else
{
// the only child is a NUM_LITERAL
Aast num = (Aast) ext.getFirstChild();
extent = num.getText();
}
}
// create the field portion of the signature
String fsig = SchemaParser.fieldSignature(type, extent);
// add it in to the table signature
signature += (signature.length() == 0) ? fsig : "|" + fsig;
String hist = (String) next.getAnnotation("historical");
if (hist == null)
{
hist = next.getText();
}
String fname = hist.toLowerCase();
fields.put(fname, idx++);
fieldTypes.put(fname, type);
}
else if (type == KW_WORD_IDX)
{
isWord = true;
}
else if (type == KW_UNIQUE)
{
isUnique = true;
}
else if (type == KW_PRIMARY)
{
isPrimary = true;
}
else if (type == KW_INACTIVE)
{
isInactive = true;
}
else if (type == INDEX)
{
String idxProps = "";
if (isWord)
{
idxProps += "W";
}
if (isUnique)
{
idxProps += "U";
}
if (isPrimary)
{
idxProps += "P";
}
if (isInactive)
{
idxProps += "I";
}
signature = signature + (idxSignature.length() > 0 ? "%" : "") + idxProps + idxSignature;
// reset the index signature
idxSignature = "";
isWord = false;
isUnique = false;
isPrimary = false;
isInactive = false;
idxName = next.getText().toLowerCase();
}
else if (type == INDEX_FIELD)
{
String fname = next.getText().toLowerCase();
int ftype = fieldTypes.get(fname);
String fsig = SchemaParser.fieldSignature(ftype, null); // TODO: extent?
int fidx = fields.get(fname);
idxSignature = (idxSignature.length() == 0 ? "^" + idxName : idxSignature) + "^" + fidx + "|" + fsig;
}
next = (Aast) next.getNextSibling();
}
// save the result
if (signature.isEmpty())
{
// there should always be at least 1 field
throw new RuntimeException(String.format("Invalid temp-table, no fields!\n%s", table.dumpTree()));
}
else
{
if (idxSignature.length() > 0)
{
String idxProps = "";
if (isWord)
{
idxProps += "W";
}
if (isUnique)
{
idxProps += "U";
}
if (isPrimary)
{
idxProps += "P";
}
if (isInactive)
{
idxProps += "I";
}
idxSignature = idxProps + idxSignature;
}
table.putAnnotation("db_signature",
signature + (idxSignature.length() > 0 ? "%" : "") + idxSignature);
}
}
/**
* Copy TABLE properties from a source AST to a destination AST. The set
* of properties (essentially child AST nodes) copied will be limited to
* those that are valid in a table (filtered by token type). It is assumed
* that <code>source</code> and <code>destination</code> are compatible
* types, such that the general AST structure is similar between them.
* <p>
* If the <code>source</code> AST contains a type that already exists in
* the <code>destination</code>, the <code>source</code> AST will take
* precedence (the subtree of that same type in the destination will be
* overwritten).
*
* @param source
* AST which is the source of the copy.
* @param destination
* AST which is the destination of the copy.
*
* @throws SchemaException
* if any error occurs during the copy.
*/
private void copyTableProperties(Aast source, Aast destination)
throws SchemaException
{
try
{
Aast next = (Aast) source.getFirstChild();
while (next != null)
{
int type = next.getType();
if (Arrays.binarySearch(TABLE_PROPS, type) >= 0)
{
// make edits in our local schema dictionary
setTableProperty(destination, next.duplicateFresh());
}
next = (Aast) next.getNextSibling();
}
}
catch (AstException exc)
{
throw new SchemaException("Error copying table properties.", exc);
}
}
/**
* Create an AST of type <code>PROPERTIES</code> and insert it as the
* first child of <code>parent</code>.
*
* @param parent
* AST node of which the new properties AST will become the
* first child.
*
* @return The new properties AST.
*/
private Aast makePropertiesAst(Aast parent)
{
Aast props = new ProgressAst();
props.setType(PROPERTIES);
props.setText("properties");
props.setParent(parent);
props.setNextSibling(parent.getFirstChild());
parent.setFirstChild(props);
return props;
}
/**
* Copies all of the child entries currently stored in the global scope
* under the parent entity identified by <code>fromNode</code> into the
* specified target table node.
*
* @param fromNode
* Name node of entity from which child entries are to be copied.
* @param toNode
* Name node which represents the parent entity to which the child
* entries will be copied.
* @param type
* Type of the parent entity to which child entries are to be
* added (using {@link EntityName} constants).
*
* @throws SchemaException
* if there is any error copying a field or properties.
*/
private void addEntries(NameNode fromNode, NameNode toNode, int type)
throws SchemaException
{
if (fromNode == null)
{
return;
}
// If we are copying to a temp-table or work-table, we must create
// the required AST objects and copy table and field properties.
if (modelTable != null)
{
Aast source = null;
Aast target = null;
switch (type)
{
case EntityName.TABLE:
source = modelTable;
target = getTable(toNode);
copyTableProperties(source, target, null);
String modelName = (String) source.getAnnotation("model");
if (modelName == null)
{
modelName = modelTable.getParent().getText() + "." + modelTable.getText();
}
target.putAnnotation("model", modelName);
break;
case EntityName.FIELD:
source = fromNode.getAst();
target = source.duplicateFresh();
try
{
// Graft the AST field clone to the parent table.
toNode.getParent().getAst().graft(target);
// Update the NameNode-->AST linkage.
toNode.setAst(target);
}
catch (AstException exc)
{
throw new SchemaException("Error grafting field to table", exc);
}
// Store the source field's fully qualified name in the target
// field, so we can query the converted name later.
int fromType = modelTable.getType();
boolean verbose =
(fromType != TEMP_TABLE && fromType != WORK_TABLE) ||
(source.isAnnotation("verbose") &&
(Boolean) source.getAnnotation("verbose"));
target.putAnnotation("verbose", verbose ? Boolean.TRUE : Boolean.FALSE);
break;
}
}
if (EntityName.hasChildType(type))
{
// Add copies of all children of "from" node as new entries beneath "to" node.
boolean tempTable = (toNode.getType() == TEMP_TABLE || toNode.getType() == WORK_TABLE);
// manufacture BEFORE-TABLE specific HIDDEN fields
if (tempTable && toNode.getAst().isAnnotation("after-table"))
{
manufactureHiddenBeforeTableField(ReservedProperty._ERRORFLAG, FIELD_INT, toNode, true);
manufactureHiddenBeforeTableField(ReservedProperty._ORIGINROWID, FIELD_ROWID, toNode, false);
manufactureHiddenBeforeTableField(ReservedProperty._DATASOURCEROWID, FIELD_ROWID, toNode, false);
manufactureHiddenBeforeTableField(ReservedProperty._ERRORSTRING, FIELD_CHAR, toNode, true);
manufactureHiddenBeforeTableField(ReservedProperty._PEERROWID, FIELD_ROWID, toNode, false);
manufactureHiddenBeforeTableField(ReservedProperty._ROWSTATE, FIELD_INT, toNode, true);
}
Iterator<NameNode> iter = fromNode.getNamespace().nodes(false, tempTable);
int childType = EntityName.getChildType(type);
while (iter.hasNext())
{
NameNode orig = iter.next();
Aast ast = orig.getAst().duplicate(null);
// Add new child entry itself.
NameNode copy = addEntry(toNode, ast, childType, true);
// save this ast, we will need info from it
Aast backup = copy.getAst();
// Recursively add all grandchildren entries.
addEntries(orig, copy, childType);
if (tempTable)
{
// copy the ORDER node from backup AST to copy.getAst()
Aast cAst = copy.getAst();
if (cAst.downPath(ORDER))
{
cAst.getImmediateChild(ORDER, null).remove();
}
Aast oAst = backup.getImmediateChild(ORDER, null);
oAst.graftCopyTo(cAst, null);
}
}
}
}
/**
* Manufactures a hidden BEFORE-TABLE field and add it to a {@code NameNode} as a
* EntityName.FIELD.
*
* @param prop
* The reserved property.
* @param fieldType
* The progress type of the node.
* @param toNode
* The target table to add.
* @param unknown
* If {@code true} the default value is declared to be unknown for this field.
*/
private void manufactureHiddenBeforeTableField(ReservedProperty prop,
int fieldType,
NameNode toNode,
boolean unknown)
{
Aast ast = new ProgressAst();
ast.setType(fieldType);
ast.setText(prop.legacy);
ast.putAnnotation("historical", prop.legacy);
ast.putAnnotation("__fieldId", (long) prop.propId);
if (unknown)
{
Aast initAst = new ProgressAst();
initAst.setText("INIT");
initAst.setType(KW_INIT);
Aast unkAst = new ProgressAst();
unkAst.setText("?");
unkAst.setType(UNKNOWN_VAL);
unkAst.putAnnotation("is-literal", true);
ast.graft(initAst);
initAst.graft(unkAst);
}
addEntry(toNode, ast, EntityName.FIELD, true);
toNode.getAst().graft(ast);
}
/**
* Promotes the youngest descendants of <code>name</code> (field level
* entities) from the global scope to the top-most scope.
* <p>
* The concept of "promotion" means that the reference to the youngest
* descendants of a {@link NameNode} are copied from a lower scope to a
* higher scope, so that they enjoy precedence over name nodes in lower
* scopes when performing a name lookup. This is how ambiguity is removed
* when the source code being processed by the Progress parser refers to a
* schema entity by an unqualified or abbreviated (or both) name, which
* would otherwise be ambiguous, if its representative name node had not
* been promoted.
* <p>
* If <code>name</code>'s youngest descendants are already present in the
* top scope, they are not added again. No attempt is made to remove
* descendants which no longer exist (which <b>did</b> exist during a
* previous promotion of an ancestor), because this would represent an
* invalid condition.
*
* @param name
* Name of entity whose descendants are to be promoted. Must be an
* unambiguous reference within one of the "named" scopes (i.e.,
* schema global, user global, external procedure, internal
* procedure/function).
* @param self
* <code>true</code> to promote the node directly associated with
* <code>name</code>; <code>false</code> to promote only its
* descendants.
* @param type
* Type of entity to be promoted (using {@link EntityName}
* constants).
* @param force
* Override the default promotion behavior and force a refresh
* of the promoted table even if it was already promoted
* previously.
* @param noProp
* <code>true</code> to force any promoted nodes to have their
* no propagate flag set. This disables propagation of these
* nodes across the current scope boundary. This is only set
* <code>true</code> for weak scopes.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> matches more than one entry in the
* global scope of the dictionary.
* @throws SchemaException
* if <code>name</code> does not exist in the global scope or
* if there is no scope above the global scope to which this
* name may be promoted.
*/
private void promoteEntry(String name, boolean self, int type, boolean force, boolean noProp)
throws AmbiguousSchemaNameException,
SchemaException
{
// Get index of local (top) scope.
int index = scopes.size() - 1;
// Verify there is a scope to which entry can be promoted.
if (index <= USER_GLOBAL_SCOPE)
{
throw new SchemaException("Add a scope before promoting entry " + name);
}
// Find name node of entity to be promoted. If not there, this is an error.
NameNode fromNode = findEntry(name, type);
if (fromNode == null)
{
throw new SchemaException("Entry not found: " + name);
}
boolean noPropagate = false;
// records associated with a weak scope are not propagated.
int fromType = fromNode.getType();
if (noProp && fromType >= BEGIN_RECORDTYPES && fromType <= END_RECORDTYPES)
{
scopes.get(scopes.size() - 1).setWeakReference(fromNode);
noPropagate = true;
}
// Don't ever promote the same node twice since it will be copied
// up the stack of scopes during scope removal (see propagate()).
// Except if the force flag is specified, in which case we will try
// to re-promote. This is used during temp-table definition (et al)
// to allow multiple promotions within the same scope (each one
// refreshing the namespaces with more data) and thus allowing
// disambiguation as needed.
Scope scope = scopes.get(index);
if (!scope.promoted.contains(fromNode) || force)
{
if (markStrongScope && fromNode.getType() == BUFFER)
{
// we are promoting a defined buffer (and all its fields) into a strong scope; these nodes
// can only be accessed within the strong scope and thus must not be propagated back into the
// enclosing scope
noPropagate = true;
}
if (!noPropagate && type == EntityName.TABLE)
{
// Disable propagation if this is a record reference to an
// explicitly defined buffer that is scoped to an internal scope.
noPropagate = (findInternalBuffer(name) || findWeakScopedBuffer(fromNode));
}
if (self)
{
// promote "self" node
promoteNode(fromNode, type, scope, noPropagate);
}
// promote descendants
promoteYoungest(fromNode, type, scope, noPropagate);
scope.promoted.add(fromNode);
if (type == EntityName.TABLE)
{
// If this promoted buffer uses an explicit database qualifier,
// remember it, in case we come across an unqualified reference to
// the same buffer in this or a more deeply nested scope.
EntityName eName = new EntityName(type, name);
String database = eName.getDatabase();
if (database != null)
{
qualifiers.addEntry(false, fromNode, database);
}
}
}
}
/**
* Promote the youngest descendants of <code>node</code> (by reference)
* to <code>scope</code>. This adds these nodes to the appropriate global
* namespaces of <code>scope</code>. If the target scope already contains
* the node, do not add it again. This method is recursive.
*
* @param node
* Name node whose youngest descendent nodes are to be promoted.
* @param type
* Type of <code>node</code> (using {@link EntityName} constants.
* @param scope
* Target scope into which references to descendants of
* <code>node</code> are copied.
* @param noPropagate
* <code>true</code> to force any promoted nodes to have their
* no propagate flag set. This disables propagation of these
* nodes across an internal scope boundary.
*
* @see #promoteEntry
*/
private void promoteYoungest(NameNode node, int type, Scope scope, boolean noPropagate)
{
if (EntityName.hasChildType(type))
{
// Promote children recursively.
int childType = EntityName.getChildType(type);
Iterator<NameNode> iter = node.getNamespace().nodes();
while (iter.hasNext())
{
NameNode child = iter.next();
promoteYoungest(child, childType, scope, noPropagate);
}
}
else
{
// We have reached a leaf node; add it to scope and scope map.
promoteNode(node, type, scope, noPropagate);
}
}
/**
* Promote a node into the given scope. If the target scope already contains the node, do not
* add it again.
*
* @param node
* Name node to be promoted.
* @param type
* Type of {@code node} (using {@link EntityName} constants.
* @param scope
* Target scope into which references to descendants of {@code node} are copied.
* @param noPropagate
* {@code true} to force promoted node to have its no propagate flag set. This
* disables propagation of this node across an internal scope boundary.
*
* @see #promoteEntry
*/
private void promoteNode(NameNode node, int type, Scope scope, boolean noPropagate)
{
if (!scope.containsNode(node, type))
{
scope.addNode(node, type, noPropagate);
scopeMap.put(node, new WeakReference<>(scope));
}
}
/**
* Propagate all namespace contents from the old scope into the
* corresponding namespaces in the new scope. This also maintains the
* scope map (node to scope lookup) mechanism. This propagation mechanism
* duplicates behavior in Progress where tables promoted in a more deeply
* nested block can disambiguate following code in enclosing blocks.
* <p>
* In the case where the <code>oldScope</code> is an internal scope, then
* any nodes in its namespaces that have been marked as excludable will
* not be propagated. This matches a 4GL behavior where promoted nodes
* enclosed in internal scopes WHICH ALSO are using a buffer that is
* scoped inside that internal scope, are not propagated.
*
* @param oldScope
* Scope providing the source of the namespace contents.
* @param newScope
* Scope providing the target for the namespace contents.
*/
private void propagate(Scope oldScope, Scope newScope)
{
boolean exclude = (oldScope.isInternal() || oldScope.isWeak() || oldScope.isStrong());
for (int i = 0; i < NS_LIST.length; i++)
{
Namespace oldns = oldScope.getNamespace(NS_LIST[i]);
Namespace newns = newScope.getNamespace(NS_LIST[i]);
// obtain an iterator, but if we are transitioning away from an internal, weak, or strong scope,
// we must mask off any nodes that have been marked for exclusion
Iterator<NameNode> nodes = oldns.nodes(exclude);
while (nodes.hasNext())
{
NameNode nn = nodes.next();
if (!newns.contains(nn))
{
newns.add(nn);
scopeMap.put(nn, new WeakReference<>(newScope));
}
}
}
}
/**
* Perform a search for the specified entity name and if found, return the
* entity's fully qualified, unabbreviated name. For names which refer to
* buffers (vs. tables, temp-tables, or work-tables), this returns a name
* which includes the fully qualified, unabbreviated name of the table
* <strong>which backs</strong> the buffer, not the name of the buffer
* itself.
*
* @param name
* Case insensitive entity name; may be qualified or not. If
* qualified, only the table and/or field portion of the name may
* be abbreviated.
* @param type
* <code>EntityName</code> constant indicating whether name
* represents a <code>DATABASE</code>, <code>TABLE</code>, or
* <code>FIELD</code>.
*
* @return The entity's fully qualified, unabbreviated name, or
* <code>null</code> if the lookup produced no match.
*
* @throws IllegalArgumentException
* if <code>name</code> is malformed. An entity name is malformed
* if it contains more than two dot delimiters.
* @throws AmbiguousSchemaNameException
* if <code>name</code> matches more than one entry in the
* dictionary. This would indicate that either (a) more than one
* entry was mistakenly added to the dictionary with the same
* entity name, or (b) <code>name</code> represents an ambiguous
* abbreviation that matches more than one entry.
*/
private String getQualifiedName(String name, int type, int startScope, int endScope)
throws AmbiguousSchemaNameException
{
NameNode node = findEntry(name, type, startScope, endScope, false);
if (node != null)
{
return getQualifiedName(node);
}
return null;
}
/**
* Special handling for buffer name nodes, which are really aliases to a
* backing table definition.
* <p>
* Given a name node, detect if the node represents a buffer, and if so,
* return the node associated with the table which backs that buffer.
* Otherwise, simply return the reference to the node which was passed in.
*
* @param node
* A name node which may or may not refer to a buffer.
*
* @return <code>node</code>, or if <code>node</code> represents a
* buffer, the table node which backs the buffer.
*
* @throws AmbiguousSchemaNameException
* if <code>node</code> matches more than one entry in the
* dictionary. This can only occur if <code>node</code> refers
* to a buffer, since a lookup only occurs in this case.
*/
private NameNode getTableNodeIfBuffer(NameNode node)
throws AmbiguousSchemaNameException
{
if (node.getType() == BUFFER)
{
Aast tt = getTable(node);
String tname = tt.getText();
if (tt.getParent() != null &&
!(tt.getParent().getText().equals(TEMP_DB) ||
tt.getParent().getText().equals(WORK_DB)))
{
tname = tt.getParent().getText() + "." + tname;
}
NameNode tableNode = findEntry(tname, EntityName.TABLE);
if (tableNode != null)
{
return tableNode;
}
}
return node;
}
/**
* Returns the topmost internal scope index or the lesser of the
* <code>EXT_PROC_SCOPE</code> and the last index position in the case
* that no scope is marked as internal.
*
* @return The internal scope index.
*/
private int nearestEnclosingInternal()
{
for (int i = scopes.size() - 1; i > EXT_PROC_SCOPE; i--)
{
Scope next = scopes.get(i);
if (next.isInternal())
{
return i;
}
}
// no internal scope
return Math.min(EXT_PROC_SCOPE, scopes.size() - 1);
}
/**
* Get the scope in which <code>node</code> resides. If <code>node</code>
* is <code>null</code>, get the scope indicated by the <code>global</code>
* flag: the user global scope if <code>true</code>, else the most recently
* added (innermost or top) scope in the lookup stack.
*
* @param node
* Name node for which we want to find the enclosing scope. We
* look within the global namespace of each scope to find this,
* beginning with the most deeply nested (top) scope, and working
* our way back to the outermost (bottom) scope. This is usually
* the parent of the name node associated with the <code>ast</code>.
* @param ast
* The AST defining the text and token type of the resource being found.
* @param global
* <code>true</code> to get user global scope; <code>false</code>
* to get top (innermost) scope. Only considered if
* <code>node</code> argument is <code>null</code> or if no scope
* was found containing <code>node</code>.
*
* @return Target scope.
*/
private Scope getScope(NameNode node, Aast ast, boolean global)
{
Scope scope = retrieveNode(node);
// If that didn't work, use the global argument and possibly the data
// type (used for records and databases but not for fields).
if (scope == null)
{
int tokenType = ast.getType();
int last = scopes.size() - 1;
int index = (global ? Math.min(USER_GLOBAL_SCOPE, last) : last);
// Force records and databases to be placed in the correct scope.
switch (tokenType)
{
// The special database for temp-tables must be located in the
// user global scope so that all temp-tables will also be in that
// scope. Permanent databases (and their contents) must be
// located in the schema global scope.
case DATABASE:
index = (TEMP_TABLE_DB.equals(ast.getText()))
? USER_GLOBAL_SCOPE
: SCHEMA_GLOBAL_SCOPE;
break;
case TABLE:
index = SCHEMA_GLOBAL_SCOPE;
break;
case TEMP_TABLE:
case WORK_TABLE:
index = USER_GLOBAL_SCOPE;
break;
case BUFFER:
index = (global ? EXT_PROC_SCOPE : nearestEnclosingInternal());
break;
}
scope = scopes.get(index);
}
return scope;
}
/**
* Finds the scope of a node based on the {@code scopeMap} structure. This also
* makes sure that the information provided is not stale (due to an eventual scope
* removal).
*
* @param node
* The node whose scope should be retrieved.
*
* @return The scope in which the provided node can be found.
*/
private Scope retrieveNode(NameNode node)
{
// The scope map contains a weak reference to the last scope to which
// this node was added, if any. It's OK if node is null; we'll just get
// null back from the scope map.
Scope scope = null;
WeakReference<Scope> ref = scopeMap.get(node);
if (ref != null)
{
scope = ref.get();
// Entry in scope map may be stale if scope was popped. In this
// case remove it and move on.
if (!scopes.contains(scope))
{
scope = null;
scopeMap.remove(node);
}
}
return scope;
}
/**
* Remove all scopes except {@code SCHEMA_GLOBAL_SCOPE} and add back an empty {@code USER_GLOBAL_SCOPE}.
*/
private void clear()
{
protectedScopeCount = 1;
while (scopes.size() - 1 > SCHEMA_GLOBAL_SCOPE)
{
try
{
removeScope();
}
catch (SchemaException e)
{
// this shouldn't happen as we explicitly set the protected scope count
}
}
addScope();
// Protect schema and user global scopes, such that they cannot be removed.
protectedScopeCount = scopes.size();
// Add database ASTs for temp and work tables.
newDatabase(TEMP_DB);
newDatabase(WORK_DB);
}
/**
* Compute the start or ending scoping level, considering if there is a temp-table or
* persistent table preference.
* <p>
* In case of a persistent table lookup, the scope is limited to {@link #SCHEMA_GLOBAL_SCOPE}.
* <p>
* For a temp-table lookup, the scope starts with <code>defLvl</code> and ends with
* {@link #USER_GLOBAL_SCOPE}.
*
* @param forceTemp
* If <code>true</code>, force a temp-table lookup.
* @param forcePersistent
* If <code>true</code>, force a persistent table lookup.
* @param first
* Flag indicating the starting level (nearest) is computed.
* @param defLvl
* The default level to use if no preference over temp-table or persistent table.
*
* @return The computed level.
*/
private static int computeLevel(boolean forceTemp, boolean forcePersistent, boolean first, int defLvl)
{
if (forcePersistent)
{
return SCHEMA_GLOBAL_SCOPE;
}
else if (forceTemp)
{
return first ? defLvl : USER_GLOBAL_SCOPE;
}
else
{
return defLvl;
}
}
/**
* Test driver for this class. Currently, it only tests a single scope.
* Loops continually, prompting the user for an entity type and name to
* look up, and prints the results of the lookup. Can also be used to
* dump schema dictionary contents to a file in the current directory
* named <code>schema.dmp</code>.
*
* @param args
* Not used.
*/
public static void main(String[] args)
{
try
{
SchemaDictionary dictionary = new SchemaDictionary((Set<String>) null);
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
while (true)
{
System.out.println("---------------------------------");
System.out.println("Enter command (case insensitive):");
System.out.println(" D for database search");
System.out.println(" T for table search");
System.out.println(" F for field search");
System.out.println(" A to create a database alias");
System.out.println(" L to load a database");
System.out.println(" U to dump dictionary to ./schema.dmp");
System.out.println();
System.out.println(" Q to quit");
System.out.print("Command: ");
String command = in.readLine();
System.out.println();
if ("Q".equalsIgnoreCase(command))
{
break;
}
else if ("U".equalsIgnoreCase(command))
{
FileOutputStream fout = null;
PrintStream ps = null;
try
{
File file = new File("schema.dmp");
fout = new FileOutputStream(file);
ps = new PrintStream(fout);
dictionary.dump(ps);
System.out.println(file.getAbsolutePath() + " dump file created");
}
finally
{
if (ps != null)
{
ps.close();
}
else if (fout != null)
{
fout.close();
}
}
continue;
}
else if ("A".equalsIgnoreCase(command))
{
System.out.print("Enter alias name : ");
String alias = in.readLine();
System.out.print("Enter database to be aliased: ");
String database = in.readLine();
System.out.println();
boolean result = dictionary.createAlias(alias, database);
if (result)
{
System.out.println("Created alias '" + alias +
"' for database '" + database + "'");
}
else
{
System.out.println("Database '" + database + "' not found");
}
System.out.println();
continue;
}
else if ("L".equalsIgnoreCase(command))
{
System.out.print("Enter database name : ");
String db = in.readLine();
System.out.println();
try
{
dictionary.loadSchema(db);
System.out.println("Database '" + db + "' loaded.");
}
catch (SchemaException se)
{
LOG.log(Level.FINE, "Unable to load database '" + db + "'.", se);
}
System.out.println();
continue;
}
System.out.print("Search Name : ");
String name = in.readLine();
System.out.println();
try
{
if ("D".equalsIgnoreCase(command))
{
boolean exists = dictionary.isDatabase(name);
System.out.println("Database exists: " + exists);
}
else if ("T".equalsIgnoreCase(command))
{
boolean exists = dictionary.isTable(name);
System.out.println("Table exists : " + exists);
String fullName = dictionary.getFullTableName(name);
if (exists)
{
System.out.println("Full table name: " + fullName);
Aast ast = dictionary.getTable(name);
String typeText = ast.getSymbolicTokenType();
System.out.println("Table type : " + typeText);
}
}
else if ("F".equalsIgnoreCase(command))
{
boolean exists = dictionary.isField(name);
System.out.println("Field exists : " + exists);
String fullName = dictionary.getFullFieldName(name);
if (exists)
{
System.out.println("Full field name: " + fullName);
Aast ast = dictionary.getField(name);
String typeText = ast.getSymbolicTokenType();
System.out.println("Field data type: " + typeText);
}
}
else
{
System.out.println("Invalid command");
}
}
catch (AmbiguousSchemaNameException exc)
{
System.out.println(exc);
}
System.out.println();
}
}
catch (Exception exc)
{
LOG.log(Level.SEVERE, "", exc);
}
}
/**
* Encapsulates several namespaces into a specific lookup scope. Schema
* dictionary entries are organized by scope, and namespace lookups are
* performed from narrowest (innermost or most recently added) to widest
* (outermost or least recently added) scope.
* <p>
* Each scope contains the following three global namespaces:
* <ul>
* <li>all connected databases;</li>
* <li>all tables across all databases; and</li>
* <li>all fields across all tables in all databases.</li>
* </ul>
* See the description of the {@link #findNode} method for search
* implementation details.
*/
private class Scope
{
/** Stores the list of name nodes that have been promoted */
private Set<NameNode> promoted = Collections.newSetFromMap(new IdentityHashMap<>());
/** Unique identifier for this scope. */
private int id = -1;
/** Marks this scope as an internal procedure, function or trigger. */
private boolean internal = false;
/** A weak reference that is associated with this scope. */
private NameNode weak = null;
/** Global namespaces indexed by entity type (database, table, field) */
private Map<Integer, Namespace> globals = new LinkedHashMap<>();
/**
* Set of preferred nodes. These nodes are the fields belonging to the table(s) which are
* a weak reference in the current scope.
*/
private Set<NameNode> preferredNodes = new HashSet<>();
/** Set of nodes associated with a strong scope at this scope level. */
private Set<NameNode> strongNodes = null;
/**
* Default constructor which initializes the database, table, and field
* level global namespaces.
*
* @param id
* Unique identifier for this scope.
* @param internal
* {@code true} to mark this scope as one which is an internal procedure, user-defined function
* or trigger. {@code false} for any other case including external procedures and inner blocks.
*/
Scope(int id, boolean internal)
{
this.id = id;
this.internal = internal;
// All databases in this scope.
globals.put(EntityName.DATABASE, new Namespace(false));
// All tables in this scope across all databases.
globals.put(EntityName.TABLE, new Namespace(true));
// All fields in this scope across all tables in all databases.
globals.put(EntityName.FIELD, new Namespace(true));
}
/**
* Copy constructor which makes a new instance with the same data
* as the given instance. Note that subsequent modifications to one
* instance will not affect the other.
*
* @param other
* The instance from which to copy.
*/
Scope(Scope other)
{
this.id = other.id;
this.internal = other.internal;
// perform a deep copy of the contents of the map
this.globals = new LinkedHashMap<>();
Iterator<Map.Entry<Integer, Namespace>> iter = other.globals.entrySet().iterator();
while (iter.hasNext())
{
Map.Entry<Integer, Namespace> next = iter.next();
Integer key = next.getKey();
Namespace val = new Namespace(next.getValue());
this.globals.put(key, val);
}
}
/**
* Find the name node, if any, corresponding with the database named or
* aliased by <code>name</code>. <code>name</code> is assumed to be an
* alias for a logical database name. A lookup is done in the aliases
* map maintained at the <code>SchemaDictionary</code> level. If the
* lookup returns a name, the alias is replaced with this name. Logical
* database names are artificially aliases to themselves, so this
* substitution is always safe.
*
* @param namespace
* Namespace in which to search for this database name node.
* @param name
* Logical name or alias for a database. May not be
* abbreviated.
*
* @return The matching name node, or <code>null</code> if not found.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> matches more than one entry in the
* dictionary.
*/
protected NameNode findDatabaseNode(Namespace namespace, String name)
throws AmbiguousSchemaNameException
{
name = name.toLowerCase();
// Look up the logical database name based on the alias.
// Logical names are aliased to themselves, so this is safe.
String database = aliases.get(name);
if (database != null)
{
name = database;
}
// Database names are never abbreviated.
return namespace.find(name, true, bannedSchemas);
}
/**
* Find a matching node for a table-like record entity. Record types
* include tables, buffers, temp tables, and work tables. The following
* rules and assumptions apply to the lookup:
* <ul>
* <li>Buffer, temp-table, and work-table names may never be
* abbreviated. Their name nodes always exist above the schema
* global scope, in either the user global scope, or in the internal
* procedure/function level scope (buffers only).
* <li>Table name nodes exist only in the schema global scope. They are
* never promoted above this scope (only their child nodes are, for
* the sake of unqualified lookups).
* <li>Table names may be abbreviated generally, but this may be
* overridden by the caller if a test for an exact match only is
* required.
* <li>If we are in any scope other than those mentioned above,
* <code>null</code> is always returned, since no record entries
* will be present.
* </ul>
*
* @param namespace
* Namespace in which to search for this field name node.
* @param name
* A record name which may be abbreviated if it represents a
* regular table, but may not be abbreviated if it represents
* a buffer, temp table, or work table.
* @param exact
* Overrides the abbreviation rule for a regular table. If
* <code>false</code>, a regular table name may be abbreviated,
* as normal; if <code>true</code>, it can not.
*
* @return The matching name node, or <code>null</code> if not found.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> matches more than one entry in the
* dictionary.
*/
protected NameNode findRecordNode(Namespace namespace, String name, boolean exact)
throws AmbiguousSchemaNameException
{
if (id != SCHEMA_GLOBAL_SCOPE)
{
exact = true;
}
return namespace.find(name, exact, bannedSchemas);
}
/**
* Find the name node, if any, for the field associated with
* <code>name</code>.
*
* @param namespace
* Namespace in which to search for this field name node.
* @param name
* Name of a field. May be abbreviated.
* @param exact
* Exact name matching required (no abbrevations if true)
* @param preferred
* A set of priority nodes used to disambiguate abbreviated fields. This set is
* populated with the fields for the tables which have weak references.
* @param strong
* A set of priority nodes used to disambiguate in strong scopes. May be null.
*
* @return The matching name node, or <code>null</code> if not found.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> matches more than one entry in the
* dictionary.
*/
protected NameNode findFieldNode(Namespace namespace,
String name,
boolean exact,
Set<NameNode> preferred,
Set<NameNode> strong)
throws AmbiguousSchemaNameException
{
return namespace.find(name, exact, uniqueFieldMatch, preferred, strong, bannedSchemas);
}
/**
* Reports if this instance is marked as an internal procedure,
* user-defined function or trigger.
*
* @return <code>true</code> if this is an internal scope.
*/
boolean isInternal()
{
return internal;
}
/**
* Reports if this instance is marked as a weak scope.
*
* @return {@code true} if this is a weak scope.
*/
boolean isWeak()
{
return (weak != null);
}
/**
* Reports if this instance is marked as a strong scope.
*
* @return {@code true} if this is a strong scope.
*/
boolean isStrong()
{
return (strongNodes != null && !strongNodes.isEmpty());
}
/**
* Obtains the buffer that associated with a weak reference, which
* is a block defined by a <code>FOR</code> statement,
* <code>DO PRESELECT</code> statement or a
* <code>REPEAT PRESELECT</code> statement.
*
* @return Buffer node.
*/
NameNode getWeakReference()
{
return weak;
}
/**
* Sets a node as the weak reference associated with a block created
* created by a <code>FOR</code> statement, <code>DO PRESELECT</code>
* statement or a <code>REPEAT PRESELECT</code> statement.
*
* @param weak
* Buffer node.
*/
void setWeakReference(NameNode weak)
{
this.weak = weak;
}
/**
* Indicate whether the global namespace indicated by <code>type</code>
* within this scope contains a reference to <code>node</code>.
*
* @param node
* Name node to check.
* @param type
* <code>EntityName</code> constant indicating whether
* <code>node</code> represents a <code>DATABASE</code>,
* <code>TABLE</code>, or <code>FIELD</code>.
*
* @return <code>true</code> if the node already exists in this scope,
* else <code>false</code>.
*/
boolean containsNode(NameNode node, int type)
{
return getNamespace(type).contains(node);
}
/**
* Add a name node to the appropriate global namespace, determined by
* <code>type</code>.
*
* @param node
* Node to be added to a global namespace of this scope.
* @param type
* <code>EntityName</code> constant indicating whether
* <code>node</code> represents a <code>DATABASE</code>,
* <code>TABLE</code>, or <code>FIELD</code>.
* @param exclude
* <code>true</code> to mark this node as one that can be
* optionally excluded from an iterator.
*/
void addNode(NameNode node, int type, boolean exclude)
{
if (markPreferred)
{
// collect all nodes which need preferred matching
this.preferredNodes.add(node);
}
if (markStrongScope)
{
if (strongNodes == null)
{
strongNodes = new HashSet<>();
}
// collect all nodes which are strongly scoped to this scope
this.strongNodes.add(node);
}
getNamespace(type).add(node, exclude);
}
/**
* Add a new name node or replace any existing name node (of the same
* name) in the appropriate global namespace, determined by
* <code>type</code>.
*
* @param node
* Node to be added to or replaced in a global namespace of
* this scope.
* @param type
* <code>EntityName</code> constant indicating whether
* <code>node</code> represents a <code>DATABASE</code>,
* <code>TABLE</code>, or <code>FIELD</code>.
* @param exclude
* <code>true</code> to mark this node as one that can be
* optionally excluded from an iterator.
* @param qualifiedFind
* <code>true</code> to match only on a qualified node name before replacing an
* existing node; else just use the unqualified name.
*/
void addOrReplaceNode(NameNode node, int type, boolean exclude, boolean qualifiedFind)
{
Namespace ns = getNamespace(type);
NameNode old = null;
String name = (qualifiedFind ? node.toQualifiedName() : node.getName());
try
{
old = ns.find(name, true, bannedSchemas);
}
catch (AmbiguousSchemaNameException ambig)
{
// if we are already screwed up, so be it, we'll make it worse below
}
if (old != null)
{
ns.remove(old);
}
ns.add(node, exclude);
}
/**
* Remove all name nodes of the given type from all namespaces in which they reside, and
* purge their descendants from all namespaces and the corresponding AST branches from
* their ASTs.
*
* @param except
* Set of name nodes to keep.
* @param type
* Entity type of the nodes to be removed.
*/
void removeAllNodes(Set<NameNode> except, int type)
{
// remove parent nodes from the global namespace of their type
Namespace ns = globals.get(type);
List<NameNode> removed = ns.removeAll(except);
// clean up all other references from the removed nodes
noLooseEnds(removed, getChildEntityType(type), false);
}
/**
* Find the name node entry, if any, associated with the specified
* entity name. The search is performed as follows:
* <ol>
* <li>iterate through the constituent parts of the name;</li>
* <li>search the global namespace associated with that name part;</li>
* <li>if a node is found, remember it and get its namespace to narrow
* the search for the next iteration of the loop ("namespace
* reduction").</li>
* </ol>
* The loop terminates if any namespace lookup fails to return a match,
* and <code>null</code> is returned. When all parts of the name have
* been looked up in the appropriate namespace, the search is complete,
* and the last found name node is returned.
* <p>
* The lookup logic for each name part follows different rules,
* depending upon whether that part represents a database qualifier,
* record qualifier, or field qualifier. This function is delegated to
* the <code>findXXXXNode</code> methods listed below.
*
* @param name
* Entity name for which to search.
* @param exact
* <code>true</code> to require an exact match;
* <code>false</code> to allow an exact or abbreviated name to
* match.
*
* @return Name node which matches the specified entity name, or
* <code>null</code> if <code>name</code> could not be
* matched against any entry in this scope.
*
* @throws AmbiguousSchemaNameException
* if <code>name</code> matches more than one entry in this
* scope, based upon the lookup rules applied.
*
* @see #findDatabaseNode
* @see #findRecordNode
* @see #findFieldNode
*/
NameNode findNode(EntityName name, boolean exact)
throws AmbiguousSchemaNameException
{
NameNode node = null;
Namespace namespace = null;
// Walk through the components of the entity name and try to match
// a name node with each qualifier, and finally with the most
// specific part of the name. The iteration will be in the correct
// order because globals is a LinkedHashMap.
Iterator<Integer> iter = globals.keySet().iterator();
while (iter.hasNext())
{
// Determine what part of the entity name we've got.
int nextType = iter.next();
// Get the name, if any, of that entity. It may represent a
// qualifier or the actual target entity.
String part = name.getPart(nextType);
if (part == null)
{
continue;
}
// If we haven't got a namespace yet (from a previous iteration
// of the loop), get the global one for the current name part.
if (namespace == null)
{
namespace = getNamespace(nextType);
}
// Different portions of the name require different search logic.
switch (nextType)
{
case EntityName.DATABASE:
node = findDatabaseNode(namespace, part);
break;
case EntityName.TABLE:
node = findRecordNode(namespace, part, exact);
break;
case EntityName.FIELD:
node = findFieldNode(namespace, part, exact, preferredNodes, strongNodes);
break;
}
// If lookup has failed, abort the loop.
if (node == null)
{
break;
}
// Get the found node's private namespace for the next iteration
// of the loop, if any (if not, we already have our target node).
namespace = node.getNamespace();
}
return node;
}
/**
* Get the global namespace associated with the specified
* <code>EntityName</code> type.
*
* @param type
* <code>EntityName</code> constant indicating whether
* to return the global <code>DATABASE</code>,
* <code>TABLE</code>, or <code>FIELD</code> namespace.
*
* @return The global namespace for the specified entity type.
*/
Namespace getNamespace(int type)
{
Namespace namespace = globals.get(type);
if (namespace == null)
{
// Note: represents a compile time error only (invalid entity
// type constant); no need for callers to catch this.
throw new IllegalArgumentException("Invalid schema entity type: " + type);
}
return namespace;
}
/**
* Debug method to dump the contents of all global namespaces managed
* by this scope object to a stream. Private namespaces of each name
* node are not written.
*
* @param stream
* Destination stream.
*/
void dump(PrintStream stream)
{
final String[] titles =
{
" Database entries:",
" Table entries:",
" Field entries:",
};
Iterator<Integer> iter = globals.keySet().iterator();
for (int i = 0; iter.hasNext(); i++)
{
stream.println(titles[i]);
dumpNamespace(stream, globals.get(iter.next()));
stream.println();
}
}
/**
* Given an {@code EntityName} part type, get the type of its child part, if any.
*
* @param type
* {@code EntityName} part type constant.
*
* @return Child entity name part type (e.g., {@code TABLE}, {@code FIELD}), or -1 if {@code type}
* is neither {@code EntityName.DATABASE} nor {@code EntityName.TABLE}.
*/
private int getChildEntityType(int type)
{
switch (type)
{
case EntityName.DATABASE:
return EntityName.TABLE;
case EntityName.TABLE:
return EntityName.FIELD;
default:
return -1;
}
}
/**
* Given a list of name nodes that have been removed from their global namespace, sever all
* of their connections with private namespaces and remove their associated ASTs from the
* larger tree. Recursively do the same for their children.
*
* @param nodes
* Nodes to be purged.
* @param type
* {@code EntityName} type of the nodes to be purged.
* @param global
* {@code true} to remove the nodes from their respective global namespace.
*/
private void noLooseEnds(List<NameNode> nodes, int type, boolean global)
{
// iterate through the removed nodes and clean up all loose ends
for (NameNode node : nodes)
{
noLooseEnds(node, type, global);
}
}
/**
* Given a name node that has been removed from its global namespace, sever all of its
* connections with its private namespace and remove its associated AST from the larger tree.
* Recursively do the same for its children.
*
* @param node
* Node to be purged.
* @param type
* {@code EntityName} type of the node to be purged.
* @param global
* {@code true} to remove the node from its respective global namespace.
*/
private void noLooseEnds(NameNode node, int type, boolean global)
{
// remove the node from its global namespace
if (global && type != -1)
{
Namespace ns = getNamespace(type);
ns.remove(node);
}
// remove the node from its parent's private namespace
node.setParent(null);
// remove the node's associated AST from its parent AST
Aast ast = node.getAst();
ast.remove();
// remove all of the node's children from the node's private namespace
Namespace ns = node.getNamespace();
Namespace globalns = type == -1 ? null : globals.get(type);
List<NameNode> removed = ns.removeAll(Collections.emptySet());
List<NameNode> globalRemoved = globalns == null ? null : globalns.removeAll(Collections.emptySet());
int childType = getChildEntityType(type);
// recursively do the purge for all removed nodes
if (!removed.isEmpty())
{
noLooseEnds(removed, childType, true);
}
if (globalRemoved != null && !globalRemoved.isEmpty())
{
noLooseEnds(globalRemoved, childType, true);
}
}
/**
* Debug method to dump the contents of the specified namespace to a
* stream. The fully qualified name and type information for each node
* is written.
*
* @param stream
* Destination stream.
* @param namespace
* Target namespace.
*/
private void dumpNamespace(PrintStream stream, Namespace namespace)
{
Iterator<NameNode> iter = namespace.nodes();
while (iter.hasNext())
{
NameNode node = iter.next();
// Get data type.
String dataType = node.getAst().getSymbolicTokenType();
// Get parent type, if any.
NameNode parent = node.getParent();
String parentType = null;
if (parent != null)
{
parentType = parent.getAst().getSymbolicTokenType();
}
// Compose descriptive string for next entry.
StringBuilder buf = new StringBuilder(" ");
buf.append(node).append(" (");
if (parentType != null)
{
buf.append(parentType).append(':');
}
buf.append(dataType).append(")");
stream.println(buf);
}
}
}
}